Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sdk/runanywhere-cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ set(RCLI_SOURCES
src/catalog/model_ref.cpp
src/commands/cmd_version.cpp
src/commands/cmd_info.cpp
src/commands/cmd_auth.cpp
src/commands/cmd_backends.cpp
src/commands/cmd_list.cpp
src/commands/cmd_lora.cpp
Expand All @@ -49,12 +50,14 @@ set(RCLI_SOURCES
src/commands/cmd_show.cpp
src/commands/cmd_stt.cpp
src/commands/cmd_embed.cpp
src/commands/cmd_telemetry.cpp
src/commands/cmd_tts.cpp
src/commands/cmd_vad.cpp
src/commands/cmd_voice.cpp
src/commands/engine_options.cpp
src/commands/model_setup.cpp
src/config/cli_paths.cpp
src/net/control_plane.cpp
src/io/wav_io.cpp
src/io/image_io.cpp
src/io/output.cpp
Expand Down
48 changes: 47 additions & 1 deletion sdk/runanywhere-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,57 @@ clear unsupported-backend error.
| `rcli serve [model]` | OpenAI-compatible HTTP server (`/v1/chat/completions`, `/v1/models`, `/health`). LLM-only, one model per process |
| `rcli backends` | Registered inference backends per primitive |
| `rcli info` / `rcli version` | Environment / versions |
| `rcli auth login` | Real control-plane handshake: API key → JWT, device registration, model-assignment fetch |
| `rcli telemetry emit --modality <m>` | Emit model-free telemetry events of one modality through the real pipeline |
| `rcli telemetry blast` | Emit events of all 12 modalities in one run and print a per-modality result table |

Global flags: `--json` (one machine-readable document on stdout),
`--home <dir>`, `-v/--verbose`, `-q/--quiet`, `--no-progress`.
`--home <dir>`, `-v/--verbose`, `-q/--quiet`, `--no-progress`, plus the
control-plane connection flags below.
Exit codes: `0` ok · `1` runtime error · `2` usage error · `130` cancelled.

## Control plane

rcli can drive any RunAnywhere control plane — including a local backend on
`http://localhost` — with three global flags (each with an env-var fallback):

| Flag | Env var | Meaning |
|---|---|---|
| `--environment <dev\|staging\|prod>` | `RUNANYWHERE_ENV` | `dev` (default) is offline — no control plane. `staging` allows `http://` and localhost URLs. `prod` requires `https://` and rejects localhost |
| `--base-url <url>` | `RUNANYWHERE_BASE_URL` | Backend origin, e.g. `https://api.runanywhere.ai` or `http://127.0.0.1:8000` |
| `--api-key <key>` | `RUNANYWHERE_API_KEY` | Control-plane API key (≥ 10 chars), required for staging/prod |

Combos are validated client-side before any network call: staging/prod
require both a key and a URL; passing credentials while in dev mode is an
error. With no flags at all, every command behaves exactly as before
(offline development mode).

```console
$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY auth login
organization 293beb67-…
device e87d77a2-…
token expires 2026-07-19T08:44:31Z
device row registered
assignments 0 model(s)

$ rcli --environment staging --base-url http://127.0.0.1:8000 --api-key $KEY telemetry blast
MODALITY RESULT STATUS RECEIVED STORED SKIPPED
llm ok HTTP 200 1 1 0
… (one row per modality, 12 total)
```

- `auth login` runs the same handshake the mobile SDKs run
(`/api/v1/auth/sdk/authenticate` → `/api/v1/devices/register` →
model assignments) and exits non-zero with the server's error surfaced when
anything fails.
- `telemetry emit|blast` drive the real commons telemetry pipeline: payloads
are batched per modality and POSTed to `/api/v2/sdk/telemetry/{modality}`
with the JWT from the login handshake. The V2 endpoints require a JWT, so
both commands log in first — one process performs login + emit (the token
is held in-process, not persisted). 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.

### `rcli run` REPL

Launched when you give no prompt and stdin is a TTY. Line editing + history
Expand Down
16 changes: 16 additions & 0 deletions sdk/runanywhere-cli/src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ void configure_app(CLI::App& app, GlobalOptions& options) {
"RunAnywhere home directory (default: $RUNANYWHERE_HOME or "
"~/.local/share/runanywhere; models live under <home>/Models)");

// Control-plane connection. Absent flags keep the historical offline
// development-mode defaults; validation happens in resolve_connection().
app.add_option("--environment", options.environment,
"Control-plane environment: dev (default, offline), staging "
"(http + localhost allowed) or prod (https only)")
->envname("RUNANYWHERE_ENV")
->check(CLI::IsMember({"dev", "development", "staging", "prod", "production"}));
app.add_option("--base-url", options.base_url,
"Control-plane base URL, e.g. https://api.runanywhere.ai or "
"http://localhost:8000 (staging/prod)")
->envname("RUNANYWHERE_BASE_URL");
app.add_option("--api-key", options.api_key, "Control-plane API key (staging/prod)")
->envname("RUNANYWHERE_API_KEY");

commands::register_version(app, options);
commands::register_info(app, options);
commands::register_backends(app, options);
Expand All @@ -44,6 +58,8 @@ void configure_app(CLI::App& app, GlobalOptions& options) {
commands::register_vad(app, options);
commands::register_voice(app, options);
commands::register_serve(app, options);
commands::register_auth(app, options);
commands::register_telemetry(app, options);
}

int run(int argc, char** argv) {
Expand Down
108 changes: 103 additions & 5 deletions sdk/runanywhere-cli/src/bootstrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "rac/core/rac_core.h"
#include "rac/core/rac_logger.h"
#include "rac/core/rac_platform_adapter.h"
#include "rac/core/rac_sdk_state.h"
#include "rac/desktop/rac_desktop.h"
#include "rac/infrastructure/device/rac_device_identity.h"
#include "rac/infrastructure/model_management/rac_model_paths.h"
Expand All @@ -18,6 +19,7 @@
#include "catalog/catalog.h"
#include "config/cli_paths.h"
#include "io/output.h"
#include "net/control_plane.h"

#if defined(RCLI_HAS_LLAMACPP)
#include "rac/backends/rac_llm_llamacpp.h"
Expand Down Expand Up @@ -150,7 +152,7 @@ const char *desktop_platform() {
#endif
}

void initialize_sdk_metadata() {
void initialize_sdk_metadata(const Connection &connection) {
char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {};
const rac_result_t device_rc =
rac_device_get_or_create_persistent_id(device_id, sizeof(device_id));
Expand All @@ -163,10 +165,21 @@ void initialize_sdk_metadata() {
const std::string locale = detect_locale();
const std::string timezone = detect_timezone();

// Mirror rac_sdk_init_phase1_proto's step order: runtime state first (the
// auth / device-registration / telemetry paths read env + credentials from
// rac_state), then the copied SDK configuration + client info.
const rac_result_t state_rc = rac_state_initialize(
connection.environment, connection.api_key.c_str(),
connection.base_url.c_str(), device_id[0] != '\0' ? device_id : "");
if (state_rc != RAC_SUCCESS) {
out::status_line("warning: SDK state init failed: " +
out::describe_result(state_rc));
}

rac_sdk_config_t sdk_config = {};
sdk_config.environment = RAC_ENV_DEVELOPMENT;
sdk_config.api_key = "";
sdk_config.base_url = "";
sdk_config.environment = connection.environment;
sdk_config.api_key = connection.api_key.c_str();
sdk_config.base_url = connection.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;
Expand All @@ -185,15 +198,94 @@ void initialize_sdk_metadata() {
}
}

bool parse_environment_name(const std::string &name, rac_environment_t *out) {
if (name.empty() || name == "dev" || name == "development") {
*out = RAC_ENV_DEVELOPMENT;
return true;
}
if (name == "staging") {
*out = RAC_ENV_STAGING;
return true;
}
if (name == "prod" || name == "production") {
*out = RAC_ENV_PRODUCTION;
return true;
}
return false;
}

} // namespace

rac_result_t resolve_connection(const GlobalOptions &options, Connection *out,
std::string *error) {
Connection connection;
if (!parse_environment_name(options.environment, &connection.environment)) {
if (error) {
*error = "invalid --environment '" + options.environment +
"' (expected dev, staging or prod)";
}
return RAC_ERROR_INVALID_CONFIGURATION;
}
connection.base_url = options.base_url;
connection.api_key = options.api_key;

if (connection.environment == RAC_ENV_DEVELOPMENT) {
if (!connection.api_key.empty() || !connection.base_url.empty()) {
if (error) {
*error = "development mode (the default) has no control plane; pass "
"--environment staging (or prod) together with --base-url "
"and --api-key";
}
return RAC_ERROR_INVALID_CONFIGURATION;
}
if (out) {
*out = connection;
}
return RAC_SUCCESS;
}

const rac_validation_result_t key_rc = rac_validate_api_key(
connection.api_key.empty() ? nullptr : connection.api_key.c_str(),
connection.environment);
if (key_rc != RAC_VALIDATION_OK) {
if (error) {
*error = std::string(rac_validation_error_message(key_rc)) +
" (--api-key / RUNANYWHERE_API_KEY)";
}
return RAC_ERROR_INVALID_CONFIGURATION;
}
const rac_validation_result_t url_rc = rac_validate_base_url(
connection.base_url.empty() ? nullptr : connection.base_url.c_str(),
connection.environment);
if (url_rc != RAC_VALIDATION_OK) {
if (error) {
*error = std::string(rac_validation_error_message(url_rc)) +
" (--base-url / RUNANYWHERE_BASE_URL)";
}
return RAC_ERROR_INVALID_CONFIGURATION;
}

if (out) {
*out = connection;
}
return RAC_SUCCESS;
}

rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) {
const std::string home = paths::resolve_home(options.home_override);
if (home.empty()) {
out::error_line("cannot resolve RunAnywhere home ($HOME unset?)");
return RAC_ERROR_NOT_INITIALIZED;
}

Connection connection;
std::string connection_error;
if (resolve_connection(options, &connection, &connection_error) !=
RAC_SUCCESS) {
out::error_line(connection_error);
return RAC_ERROR_INVALID_CONFIGURATION;
}

if (!g_bootstrapped) {
rac_result_t rc = rac_desktop_adapter_init(nullptr, &g_adapter);
if (rc != RAC_SUCCESS) {
Expand Down Expand Up @@ -234,7 +326,13 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) {
return rc;
}

initialize_sdk_metadata();
initialize_sdk_metadata(connection);

// Platform wiring for the control-plane flows (`rcli auth login`,
// `rcli telemetry ...`): device-manager callbacks route registration
// POSTs through the registered curl transport. Same role the per-SDK
// bridges play; a no-op for commands that never touch the network.
net::register_device_callbacks();

#if defined(RCLI_HAS_LLAMACPP)
if (rac_backend_llamacpp_register() != RAC_SUCCESS) {
Expand Down
33 changes: 33 additions & 0 deletions sdk/runanywhere-cli/src/bootstrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <string>

#include "rac/core/rac_types.h"
#include "rac/infrastructure/network/rac_environment.h"

namespace rcli {

Expand All @@ -27,8 +28,40 @@ struct GlobalOptions {
bool quiet = false;
bool no_progress = false;
std::string home_override; // --home flag

// Control-plane connection. Empty defaults preserve the historical
// offline development-mode behavior exactly. CLI11 fills these from
// --base-url/--api-key/--environment with RUNANYWHERE_BASE_URL /
// RUNANYWHERE_API_KEY / RUNANYWHERE_ENV env-var fallbacks (app.cpp).
std::string environment; // dev|development|staging|prod|production ("" → dev)
std::string base_url; // required for staging/prod (http allowed on staging)
std::string api_key; // required for staging/prod (≥10 chars)
};

/**
* Validated control-plane connection resolved from GlobalOptions.
* bootstrap() threads these values into rac_state / rac_sdk_config so the
* commons auth, device-registration, and telemetry paths can read them.
*/
struct Connection {
rac_environment_t environment = RAC_ENV_DEVELOPMENT;
std::string base_url;
std::string api_key;
};

/**
* Resolve + validate the connection flags client-side (before any network
* call). On failure fills `error` with an actionable message and returns
* RAC_ERROR_INVALID_CONFIGURATION.
*
* Rules (mirrors commons rac_validate_api_key / rac_validate_base_url):
* - dev (default): no credentials allowed — pass --environment staging to
* target a real control plane (localhost is allowed on staging).
* - staging: api key (≥10 chars) + http(s) base URL required.
* - prod: api key + https base URL required; localhost rejected.
*/
rac_result_t resolve_connection(const GlobalOptions& options, Connection* out, std::string* error);

/** Resolved environment after bootstrap. */
struct Bootstrapped {
std::string home; // RunAnywhere home (storage base dir)
Expand Down
Loading
Loading