@ondeinference / onde / commits / 1e393a2

Embed GresIQ credentials at build time via dotenvy

- Add dotenvy to dependencies and load .env in build.rs to embed GRESIQ_API_KEY and GRESIQ_API_SECRET at SDK build time. - PulseClient now reads credentials from option_env! instead of runtime env vars. - Consumer apps no longer set GresIQ credentials; only ONDE_EDGE_ID is read at runtime. - Update .gitignore to exclude .env (build-time secrets). - Update pulse module docs to reflect new credential embedding approach.

keypair34 committed Apr 18, 2026 at 21:42 UTC 1e393a26bc52ea33b3d99f8370b54b46c67716e2
7 files changed +74 -54
.gitignore
+3
@@ -29,3 +29,6 @@ sdk/python/.pytest_cache/
29
30 # Zed settings
31 .zed
32 +
33 +# SDK credentials (build-time secrets)
34 +.env
Cargo.lock
+7
@@ -1357,6 +1357,12 @@ dependencies = [
1357 "litrs",
1358 ]
1359
1360 +[[package]]
1361 +name = "dotenvy"
1362 +version = "0.15.7"
1363 +source = "registry+https://github.com/rust-lang/crates.io-index"
1364 +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
1365 +
1366 [[package]]
1367 name = "dtoa"
1368 version = "1.0.11"
@@ -3635,6 +3641,7 @@ version = "0.1.4"
3641 dependencies = [
3642 "anyhow",
3643 "cc",
3644 + "dotenvy",
3645 "hf-hub",
3646 "home",
3647 "log",
Cargo.toml
+1
@@ -45,6 +45,7 @@ version = "0.3"
45 uniffi = { version = "=0.31.0", features = ["build"] }
46 # Used by build.rs to compile the tvOS ___chkstk_darwin assembly stub.
47 cc = "1"
48 +dotenvy = "0.15"
49
50 # Android: CPU-only GGUF inference via candle; also needs hf-hub and
51 # mistralrs-core for explicit cache path seeding (dirs::home_dir() panics
build.rs
+17
@@ -1,4 +1,21 @@
1 fn main() {
2 + // ── SDK credentials ──────────────────────────────────────────────────
3 + // Load .env from the onde crate root so GresIQ credentials are available
4 + // to option_env!() in pulse/client.rs. Consumer apps never set these —
5 + // they're Onde Inference's own infrastructure secrets.
6 + println!("cargo:rerun-if-changed=.env");
7 + let _ = dotenvy::from_filename(
8 + std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(".env"),
9 + );
10 + for var in ["GRESIQ_API_KEY", "GRESIQ_API_SECRET"] {
11 + println!("cargo:rerun-if-env-changed={var}");
12 + if let Ok(val) = std::env::var(var) {
13 + if !val.is_empty() {
14 + println!("cargo:rustc-env={var}={val}");
15 + }
16 + }
17 + }
18 +
19 // ── UniFFI scaffolding ───────────────────────────────────────────────
20 // Not strictly required when using `uniffi::setup_scaffolding!()` in
21 // lib.rs (the proc-macro approach), but having it here ensures the
src/inference/engine.rs
+5 -3
@@ -219,7 +219,9 @@ impl ChatEngine {
219 /// Create a new engine with no model loaded.
220 pub fn new() -> Self {
221 let environment = Self::pulse_environment();
222 - let pulse = crate::pulse::PulseClient::from_env(environment);
222 + let edge_id = std::env::var("ONDE_EDGE_ID")
223 + .unwrap_or_else(|_| "onde-unknown".to_string());
224 + let pulse = crate::pulse::PulseClient::new(environment, edge_id);
225
226 match &pulse {
227 Some(_) => log::info!(
@@ -227,7 +229,7 @@ impl ChatEngine {
229 ),
230 None => log::info!(
231 "ChatEngine: pulse telemetry disabled \
230 - (GRESIQ_API_KEY / GRESIQ_API_SECRET / ONDE_CLIENT_KEY / ONDE_CLIENT_SECRET not set)"
232 + (GRESIQ_API_KEY / GRESIQ_API_SECRET not embedded at SDK build time)"
233 ),
234 }
235
@@ -237,7 +239,7 @@ impl ChatEngine {
239 }
240 }
241
240 - /// Resolve the GresIQ environment from the `GRESIQ_ENVIRONMENT` env var.
242 + /// Resolve the pulse environment from the `GRESIQ_ENVIRONMENT` env var.
243 /// Defaults to `Production` when the var is absent or unrecognised.
244 fn pulse_environment() -> smbcloud_gresiq_sdk::Environment {
245 match std::env::var("GRESIQ_ENVIRONMENT").as_deref() {
src/pulse/client.rs
+33 -35
@@ -1,12 +1,20 @@
1 -use std::collections::HashMap;
2 -
1 use smbcloud_gresiq_sdk::{Environment, GresiqClient, GresiqCredentials};
2
3 use super::events::{InferenceEvent, ModelLoadedEvent};
4
7 -/// Onde telemetry client. Wraps GresiqClient so pulse events land in the
8 -/// right GresIQ-managed tables without this crate knowing anything about
9 -/// the HTTP auth layer underneath.
5 +/// GresIQ API key embedded at SDK build time.
6 +/// Consumer apps never set this — it's Onde Inference's own credential.
7 +const EMBEDDED_API_KEY: Option<&str> = option_env!("GRESIQ_API_KEY");
8 +
9 +/// GresIQ API secret embedded at SDK build time.
10 +const EMBEDDED_API_SECRET: Option<&str> = option_env!("GRESIQ_API_SECRET");
11 +
12 +/// Onde telemetry client. Wraps GresiqClient so pulse events land in the
13 +/// right GresIQ-managed tables without consumer apps knowing anything about
14 +/// the GresIQ auth layer underneath.
15 +///
16 +/// GresIQ credentials are embedded in the SDK at build time.
17 +/// Consumer apps only provide an `edge_id` (stable device identifier).
18 ///
19 /// Cheap to clone: the inner GresiqClient holds an Arc-backed reqwest::Client,
20 /// so cloning is a pointer bump, not a new TCP connection.
@@ -17,49 +25,39 @@ pub struct PulseClient {
25 }
26
27 impl PulseClient {
20 - /// Reads credentials from the environment and resolves the GresIQ
21 - /// gateway URL from the given `Environment`.
28 + /// Build a pulse client using the GresIQ credentials embedded in the SDK.
29 ///
23 - /// GresIQ layer (shared across all SDK clients):
24 - /// `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`
30 + /// Returns `None` if the SDK was compiled without `GRESIQ_API_KEY` /
31 + /// `GRESIQ_API_SECRET` (e.g. a local dev build of onde without `.env`).
32 + /// In that case telemetry is silently disabled — no panic, no partial state.
33 ///
26 - /// SDK client layer (this app’s own credentials):
27 - /// `ONDE_CLIENT_KEY`, `ONDE_CLIENT_SECRET`
28 - /// `ONDE_EDGE_ID` (optional, defaults to `"onde-unknown"`)
29 - ///
30 - /// Returns `None` if any required var is absent. Nothing blows up —
31 - /// the engine just skips telemetry for the whole run.
32 - pub fn from_env(environment: Environment) -> Option<Self> {
33 - // GresIQ credentials
34 - let gresiq_api_key = std::env::var("GRESIQ_API_KEY").ok()?;
35 - let gresiq_api_secret = std::env::var("GRESIQ_API_SECRET").ok()?;
36 -
37 - // SDK client credentials
38 - let client_key = std::env::var("ONDE_CLIENT_KEY").ok()?;
39 - let client_secret = std::env::var("ONDE_CLIENT_SECRET").ok()?;
40 - let edge_id = std::env::var("ONDE_EDGE_ID")
41 - .unwrap_or_else(|_| "onde-unknown".to_string());
34 + /// `edge_id` is a stable device identifier (installation UUID).
35 + /// Pass an empty string to default to `"onde-unknown"`.
36 + pub fn new(environment: Environment, edge_id: String) -> Option<Self> {
37 + let api_key = EMBEDDED_API_KEY?;
38 + let api_secret = EMBEDDED_API_SECRET?;
39
43 - let mut extra = HashMap::new();
44 - extra.insert("X-Onde-Client-Key".to_string(), client_key);
45 - extra.insert("X-Onde-Client-Secret".to_string(), client_secret);
40 + let edge_id = if edge_id.is_empty() {
41 + "onde-unknown".to_string()
42 + } else {
43 + edge_id
44 + };
45
46 let credentials = GresiqCredentials {
48 - api_key: &gresiq_api_key,
49 - api_secret: &gresiq_api_secret,
47 + api_key,
48 + api_secret,
49 };
50
52 - let inner = GresiqClient::from_credentials(environment, credentials)
53 - .with_extra_headers(extra);
51 + let inner = GresiqClient::from_credentials(environment, credentials);
52
53 Some(PulseClient { inner, edge_id })
54 }
55
56 /// Spawns a background task that writes the model-load event to the
59 - /// pulse/model_loaded table, then returns immediately. Slow network
57 + /// pulse/model_loaded table, then returns immediately. Slow network
58 /// responses don't block the first inference request.
59 ///
62 - /// Failed writes emit a warn! log line -- no retry, no queue,
60 + /// Failed writes emit a warn! log line — no retry, no queue,
61 /// and no effect on the caller.
62 pub fn record_model_loaded(&self, model_id: String, model_name: String, load_duration_ms: u64) {
63 let client = self.clone();
@@ -77,7 +75,7 @@ impl PulseClient {
75 }
76
77 /// Same fire-and-forget pattern as record_model_loaded but for inference
80 - /// completions. Writes to pulse/inference_event. Logs on failure, no retry.
78 + /// completions. Writes to pulse/inference_event. Logs on failure, no retry.
79 pub fn record_inference(
80 &self,
81 model_id: String,
src/pulse/mod.rs
+8 -16
@@ -1,21 +1,13 @@
1 //! Sends timing data to the Onde Inference dashboard.
2 //!
3 -//! Opt-in at the infra level, not the code level. Set the four env vars and
4 -//! telemetry starts flowing. Set none and nothing happens — no warnings,
5 -//! no stubs, the branches just never run.
3 +//! GresIQ credentials are embedded in the SDK at build time — consumer apps
4 +//! never set them. Telemetry activates automatically when the SDK was built
5 +//! with a valid `.env` file containing `GRESIQ_API_KEY` and `GRESIQ_API_SECRET`.
6 //!
7 -//! The engine reads the vars once in `ChatEngine::new()`. You can't flip
8 -//! telemetry on mid-run without restarting.
9 -//!
10 -//! | Variable | Example |
11 -//! |-----------------------|------------------------------|
12 -//! | `ONDE_BASE_URL` | `https://ondeinference.com` |
13 -//! | `ONDE_CLIENT_KEY` | `<issued by Onde Inference>` |
14 -//! | `ONDE_CLIENT_SECRET` | `<issued by Onde Inference>` |
15 -//!
16 -//! `ONDE_EDGE_ID` is what this machine gets called in the dashboard.
17 -//! Anything works — just keep it stable across restarts or you'll
18 -//! end up with duplicate edges. Defaults to `"onde-unknown"`.
7 +//! `ONDE_EDGE_ID` (read from the process environment at runtime) is what this
8 +//! machine gets called in the dashboard. Anything works — just keep it stable
9 +//! across restarts or you'll end up with duplicate edges. Defaults to
10 +//! `"onde-unknown"`.
11
12 mod client;
13 mod events;
@@ -25,7 +17,7 @@ pub use events::{InferenceEvent, ModelLoadedEvent};
17
18 use std::sync::atomic::{AtomicU64, Ordering};
19
28 -/// Cheap unique ID per inference request. No uuid crate needed.
20 +/// Cheap unique ID per inference request. No uuid crate needed.
21 /// Looks like `onde-1720000000000-42`.
22 pub(crate) fn next_request_id() -> String {
23 static COUNTER: AtomicU64 = AtomicU64::new(0);