Skip to content
Merged
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
56 changes: 56 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Rust

# This repo shipped with no CI at all — `.github/` held only banner images —
# so `rust/src/auth/refresh.rs` could sit for weeks as 174 lines that never
# compiled (no `mod refresh;`), and the design tokens could drift from the CSS
# unnoticed. Every check below exists because something got through without it.

on:
pull_request:
branches: [main]
push:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
rust:
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Format check
run: cargo fmt --check

# `--all-targets` so tests and examples are linted too; `-p`-scoped or
# target-less clippy silently skips test code.
- name: Clippy (default features — the no_std `ui` build)
run: cargo clippy --all-targets -- -D warnings

- name: Clippy (all features)
run: cargo clippy --all-targets --all-features -- -D warnings

# Run BOTH feature sets: the `ui` tests are the only ones in the default
# build, and the `auth` tests only exist behind the feature — running one
# configuration would report a green suite that never touched half the crate.
- name: Test (default features)
run: cargo test

- name: Test (all features)
run: cargo test --all-features

module-tree:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Catches the `refresh.rs` class: a .rs file on disk that no `mod`
# declaration reaches, so it never compiles and no other check sees it.
- name: Every .rs file is reachable from the module tree
run: python3 scripts/check-module-tree.py
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@

---

> **Every Smoo AI Rust client needs the same three things — so they live in one crate.** Design tokens + the smoo monogram (`ui`), the full Supabase auth story — browser OAuth with PKCE, email+password, session refresh, M2M `client_credentials` — with a shared 0600 on-disk credential store (`auth`), and an LLM session exchange (`llm`, **stub today**). One Rust crate, feature-gated so the bare `ui` build stays `no_std` with zero dependencies. Consumed in production by the [`th` CLI](https://github.com/SmooAI/smooth). **Rust-only today; not yet on crates.io** — npm / NuGet / PyPI siblings are planned, not built.
> **Every Smoo AI Rust client needs the same three things — so they live in one crate.** Design tokens + the smoo monogram (`ui`), the full Supabase auth story — browser OAuth with PKCE, email+password, session refresh, M2M `client_credentials` — with a shared 0600 on-disk credential store (`auth`), One Rust crate, feature-gated so the bare `ui` build stays `no_std` with zero dependencies. Consumed in production by the [`th` CLI](https://github.com/SmooAI/smooth). **Rust-only today; not yet on crates.io** — npm / NuGet / PyPI siblings are planned, not built.

## What is this?

A Smoo AI Rust client (smooblue, observability-studio, `th`, `smoo admin`, …) typically needs the same three things:

1. **Design tokens + monogram** — so the UI looks like Smoo AI.
2. **Auth** — Supabase user OAuth (browser login), email+password, session refresh, AND M2M `client_credentials` grant (service accounts), with one shared on-disk `CredentialsStore`.
3. **LLM access** — exchanging a user session JWT for an org-scoped `llm.smoo.ai` bearer. *(Not implemented yet — see [Honest status](#honest-status).)*
3. **LLM access** — exchanging a user session JWT for an org-scoped `llm.smoo.ai` bearer. *(Not built — there is deliberately no feature flag for it yet; see [Honest status](#honest-status).)*

Each of these has been re-implemented in every consumer at least once. This crate makes them one dependency. It absorbs the standalone [`SmooAI/ui`](https://github.com/SmooAI/ui) crate: `ui` is one module among siblings (`auth`, `llm`) — same constants, same paths, byte-identical `shared/` sources.
Each of these has been re-implemented in every consumer at least once. This crate makes them one dependency. It absorbs the standalone [`SmooAI/ui`](https://github.com/SmooAI/ui) crate: `ui` is one module alongside `auth` — same constants, same paths. This repo's `shared/` is the **source of truth** for the design system; SmooAI/ui carries a copy, and a CI gate there fails if the two ever diverge.

```mermaid
%%{init: {'theme':'base','themeVariables':{
Expand All @@ -42,7 +42,6 @@ flowchart LR
subgraph CRATE["smooai-client-shared"]
UI["ui (default)<br/>STYLES · MONOGRAM_SVG · tokens::*<br/>zero deps · no_std"]
AUTH["auth (feature)<br/>oauth · password · refresh · m2m<br/>CredentialsStore (0600)"]
LLM["llm (feature)<br/>STUB — pending"]
end
AUTH -->|"PKCE localhost callback"| SB[("Supabase<br/>/auth/v1")]
AUTH -->|"client_credentials"| TOK[("auth.smoo.ai/token")]
Expand Down Expand Up @@ -162,25 +161,26 @@ smooai-client-shared = { git = "https://github.com/SmooAI/client-shared.git", fe
| --- | --- | --- | --- |
| `ui` (default) | `STYLES`, `MONOGRAM_SVG`, `tokens::*` | nothing — `no_std` | ✅ working |
| `auth` | Supabase OAuth + password + refresh, M2M, `CredentialsStore` | `tokio`, `reqwest`, `axum`, `serde`, … | ✅ working, 28 unit tests |
| `llm` | JWT → `llm.smoo.ai` org-session exchange | (implies `auth`) | 🚧 **stub — compiles, no usable surface** (pearl th-f7b20f) |

Run the tests yourself — 32 unit tests across `ui` + `auth` (OAuth callback/PKCE, token rotation, store round-trips, permission bits):
An `llm` feature (JWT → `llm.smoo.ai` org-session exchange, pearl th-f7b20f) is **planned and deliberately absent**. It previously existed as a flag over a six-line doc-comment module: `--features llm` compiled and produced nothing, which is worse than an honest gap. It returns when there is code behind it.

Run the tests yourself — 34 unit tests across `ui` + `auth` (OAuth callback/PKCE, token rotation, store round-trips, permission bits, token/CSS drift):

```bash
cd rust && cargo test --features auth,llm
cd rust && cargo test --all-features
```

There is no CI in this repo yetrun the tests locally before merging.
CI (`.github/workflows/rust.yml`) runs `cargo fmt --check`, `clippy --all-targets -D warnings` and the test suite in **both** feature configurations — the default `no_std` `ui` build and `--all-features` — plus a module-tree check that fails if any `.rs` file is unreachable from a `mod` declaration.

---

## Honest status

| Surface | Status |
| --- | --- |
| **Rust `ui`** | ✅ Working — byte-identical to the `smooai-ui` crate's surface; drift-tested against `shared/styles.css` |
| **Rust `ui`** | ✅ Working — the `tokens` constants are **generated** from `shared/tokens.json` at build time, and cross-checked against `shared/styles.css` in both directions (every token matches its custom property; every colour the CSS declares has a token) |
| **Rust `auth`** | ✅ Working — OAuth PKCE localhost-callback (387 LOC), password grant, refresh with rotation, M2M, 0600 `CredentialsStore`; 28 unit tests; consumed by the `th` CLI in production |
| **Rust `llm`** | 🚧 **Stub** — the feature flag exists and compiles, but the module is a doc-comment placeholder. Building with `--features llm` gives you no usable API today |
| **Rust `llm`** | ❌ Not built — no module, no feature flag. Pearl th-f7b20f tracks it |
| **crates.io** | ❌ Not published — git dependency is the only install path |
| **npm / NuGet / PyPI** | 📦 Planned, no code — the `src/`, `dotnet/`, `python/` directories in the layout below don't exist yet |

Expand All @@ -191,14 +191,15 @@ client-shared/
├── shared/ # cross-language source of truth
│ ├── styles.css # OKLCH tokens + base component CSS
│ ├── monogram.svg # smoo monogram
│ └── tokens.json # tokens as plain JSON (no consumer yet)
│ ├── tokens.json # THE token source — the Rust `tokens` module is generated from it
│ ├── tokens_codegen.rs # generator, run from rust/build.rs
│ └── tokens_css_check.rs # asserts tokens.json <-> styles.css agree, both ways
└── rust/ # smooai-client-shared (git dependency; crates.io planned)
├── Cargo.toml
└── src/
├── lib.rs
├── ui/ # lifted verbatim from smooai-ui
├── auth/ # oauth · password · refresh · m2m · storage (feature = "auth")
└── llm/ # STUB — pending pearl th-f7b20f (feature = "llm")
└── auth/ # oauth · password · refresh · m2m · storage (feature = "auth")
```

npm (`src/`), NuGet (`dotnet/`), and PyPI (`python/`) packages are roadmap, not directories.
Expand Down Expand Up @@ -240,7 +241,7 @@ The `ui` module is API-compatible with `smooai-ui`: same constants, same paths,

## 🤝 Contributing

PRs welcome. `cd rust && cargo test --features auth,llm` must pass; keep the bare `ui` build zero-dep and `no_std`, and gate anything heavier behind a feature flag.
PRs welcome. `cd rust && cargo test --all-features` must pass; keep the bare `ui` build zero-dep and `no_std`, and gate anything heavier behind a feature flag.

## 📄 License

Expand Down
18 changes: 13 additions & 5 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name = "smooai-client-shared"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "SmooAI's cross-runtime client shared library — design tokens, monogram, auth, and llm session primitives shared across every SmooAI Rust app (smooblue, observability-studio, th, th admin, …)."
description = "SmooAI's cross-runtime client shared library — design tokens, monogram, and auth primitives shared across every SmooAI Rust app (smooblue, observability-studio, th, th admin, …)."
repository = "https://github.com/SmooAI/client-shared"
homepage = "https://github.com/SmooAI/client-shared"
keywords = ["smooai", "design-system", "auth", "llm", "client"]
keywords = ["smooai", "design-system", "auth", "client"]
categories = ["gui", "authentication", "config"]
readme = "README.md"
authors = ["SmooAI <brent@smoo.ai>"]
Expand All @@ -16,25 +16,33 @@ authors = ["SmooAI <brent@smoo.ai>"]
include = [
"Cargo.toml",
"README.md",
"build.rs",
"src/**/*.rs",
"../shared/styles.css",
"../shared/monogram.svg",
"../shared/tokens.json",
"../shared/tokens_codegen.rs",
"../shared/tokens_css_check.rs",
"../LICENSE",
]

[features]
# Default to the lightweight `ui` module so existing smooai-ui
# consumers (smooblue, observability-studio) get the same dependency
# tree they had before — no tokio, no reqwest, still no_std-friendly.
# Opt into `auth` / `llm` only when needed.
# Opt into `auth` only when needed.
default = ["ui"]
ui = []
auth = ["dep:tokio", "dep:reqwest", "dep:serde", "dep:serde_json", "dep:url", "dep:axum", "dep:anyhow", "dep:chrono", "dep:dirs-next", "dep:webbrowser", "dep:rand", "dep:base64", "dep:sha2"]
llm = ["auth"]

[build-dependencies]
# Build-script only: parses shared/tokens.json to generate the `tokens`
# module. Build dependencies are host-side and never reach consumers, so the
# bare `ui` build stays zero-dependency and `no_std`.
serde_json = "1"

[dependencies]
# Heavy deps gated behind `auth` / `llm` features so the bare `ui`
# Heavy deps gated behind the `auth` feature so the bare `ui`
# build stays no_std-compatible and zero-dep.
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "net", "sync", "time"], optional = true }
reqwest = { version = "0.12", features = ["json"], optional = true }
Expand Down
17 changes: 17 additions & 0 deletions rust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! Generate the `tokens` module from `shared/tokens.json`.
//!
//! The generator itself lives in `shared/tokens_codegen.rs` so SmooAI/ui and
//! SmooAI/client-shared generate identically from the identical input — the
//! `shared/**` drift gate keeps those files byte-for-byte equal. This build
//! script is only the shim that feeds it.

include!("../shared/tokens_codegen.rs");

fn main() {
println!("cargo:rerun-if-changed=../shared/tokens.json");
println!("cargo:rerun-if-changed=../shared/tokens_codegen.rs");

let json = std::fs::read_to_string("../shared/tokens.json").expect("read shared/tokens.json");
let out = std::path::Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR")).join("tokens.rs");
std::fs::write(out, generate_tokens_rs(&json)).expect("write tokens.rs");
}
20 changes: 10 additions & 10 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
//! `smoo admin` (the Smooth CLI), and any future Rust client.
//!
//! Replaces the standalone `smooai-ui` crate (which only carried the
//! `ui` slice) by adding `auth` and `llm` modules behind feature
//! flags. The bare `default-features = ["ui"]` build stays
//! `ui` slice) by adding an `auth` module behind a feature flag. The bare `default-features = ["ui"]` build stays
//! `no_std`-compatible with zero runtime dependencies — same shape as
//! the old `smooai-ui` so existing consumers don't inherit any new
//! tree.
Expand All @@ -18,10 +17,14 @@
//! - `ui` (default) — design tokens, base CSS, monogram. Zero deps,
//! `no_std`.
//! - `auth` — Supabase user OAuth (localhost-callback flow), M2M
//! `client_credentials` grant, on-disk `CredentialsStore`. Pulls
//! in `tokio`, `reqwest`, `serde`, `axum`.
//! - `llm` — JWT → `llm.smoo.ai` org-scoped LLM session exchange.
//! Implies `auth`.
//! `client_credentials` grant, refresh-token rotation, on-disk
//! `CredentialsStore`. Pulls in `tokio`, `reqwest`, `serde`, `axum`.
//!
//! An `llm` feature (JWT → `llm.smoo.ai` org-scoped session exchange)
//! is planned under pearl th-f7b20f. It is deliberately **absent**
//! rather than stubbed: a feature flag that compiles to an empty
//! module advertises a capability that does not exist. It comes back
//! when there is something behind it.
//!
//! ## Migrating from `smooai-ui`
//!
Expand All @@ -42,7 +45,7 @@
//! path it lived at under `smooai_ui::` (e.g. `STYLES`,
//! `MONOGRAM_SVG`, `tokens::*`).

#![cfg_attr(not(any(feature = "auth", feature = "llm")), no_std)]
#![cfg_attr(not(feature = "auth"), no_std)]
#![doc(html_root_url = "https://docs.rs/smooai-client-shared/0.1.0")]
#![warn(missing_docs)]

Expand All @@ -51,6 +54,3 @@ pub mod ui;

#[cfg(feature = "auth")]
pub mod auth;

#[cfg(feature = "llm")]
pub mod llm;
6 changes: 0 additions & 6 deletions rust/src/llm/mod.rs

This file was deleted.

95 changes: 22 additions & 73 deletions rust/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,23 @@ pub const STYLES: &str = include_str!("../../../shared/styles.css");
/// backdrop.
pub const MONOGRAM_SVG: &str = include_str!("../../../shared/monogram.svg");

/// Brand + semantic token *values* as `&'static str`, for code paths
/// that need a colour outside of CSS (custom-painted egui widgets,
/// native menu chrome, chart libraries, etc.). The single source of
/// truth is the CSS in [`STYLES`]; these constants are mirrored from
/// it and validated by the `tokens_match_css` test below.
/// Brand + semantic token *values*, for code paths that need a colour,
/// radius, spacing step, or font stack outside of CSS (custom-painted egui
/// widgets, native menu chrome, chart libraries, etc.).
///
/// **Generated** at build time from
/// [`shared/tokens.json`](https://github.com/SmooAI/client-shared/blob/main/shared/tokens.json)
/// by `shared/tokens_codegen.rs` — nobody hand-writes these, so a token cannot
/// exist in the design system and be missing here. `shared/tokens_css_check.rs`
/// then asserts each one equals the resolved value of its custom property in
/// [`STYLES`], and that every colour the CSS declares has a token.
pub mod tokens {
/// Brand orange — primary CTA stop, accent.
pub const SMOOAI_ORANGE: &str = "oklch(0.769 0.164 71)";
/// Brand red — destructive, "like" affordance.
pub const SMOOAI_RED: &str = "oklch(0.712 0.181 22.4)";
/// Brand green — confirm / "repost" affordance / focus ring.
pub const SMOOAI_GREEN: &str = "oklch(0.657 0.112 194.8)";
/// Lightest brand blue — reply hover.
pub const SMOOAI_BLUE_300: &str = "oklch(0.803 0.074 230.9)";
/// Mid brand blue.
pub const SMOOAI_BLUE_400: &str = "oklch(0.725 0.102 233.4)";
/// Active nav-rail item background.
pub const SMOOAI_BLUE_500: &str = "oklch(0.55 0.13 233)";
/// Brand dark blue — base background.
pub const SMOOAI_DARK_BLUE: &str = "oklch(0.13 0.043 265.1)";
/// Sidebar tint.
pub const SMOOAI_DARK_BLUE_850: &str = "oklch(0.177 0.074 266)";
/// Hover on rail items.
pub const SMOOAI_DARK_BLUE_700: &str = "oklch(0.303 0.154 265.8)";
/// Brand white.
pub const SMOOAI_WHITE: &str = "oklch(0.984 0.003 247.9)";
/// Muted text.
pub const SMOOAI_GRAY_400: &str = "oklch(0.715 0 89.9)";
include!(concat!(env!("OUT_DIR"), "/tokens.rs"));

/// Default page background.
pub const BACKGROUND: &str = "oklch(0.145 0.014 265)";
/// Default text color.
pub const FOREGROUND: &str = SMOOAI_WHITE;
/// Card surface background.
pub const CARD: &str = "oklch(0.205 0.015 265)";
/// Border / divider.
pub const BORDER: &str = "oklch(0.3 0.008 260)";

/// Signature brand gradient — gradient-as-string for `background: …;`.
/// Note that CSS doesn't accept gradients in inline `color:` — this is
/// for `background` and SVG paint only.
pub const GRADIENT_BRAND: &str =
"linear-gradient(135deg, oklch(0.769 0.164 71) 0%, oklch(0.712 0.181 22.4) 100%)";

/// Default radius for cards + buttons (matches `--radius` in CSS).
pub const RADIUS_PX: u16 = 10;
/// The default corner radius, as used by cards and buttons. Retained as an
/// alias of [`RADIUS_MD_PX`] so consumers pinned to the pre-codegen name
/// keep compiling.
pub const RADIUS_PX: u16 = RADIUS_MD_PX;
}

#[cfg(test)]
Expand All @@ -81,35 +52,9 @@ mod tests {
assert!(MONOGRAM_SVG.contains("fill=\"currentColor\""));
}

/// Validate that every token value in the `tokens` module is also
/// present in the CSS, so the two sources of truth can't silently
/// drift.
#[test]
fn tokens_match_css() {
let css = STYLES;
for (name, value) in [
("SMOOAI_ORANGE", tokens::SMOOAI_ORANGE),
("SMOOAI_RED", tokens::SMOOAI_RED),
("SMOOAI_GREEN", tokens::SMOOAI_GREEN),
("SMOOAI_BLUE_300", tokens::SMOOAI_BLUE_300),
("SMOOAI_BLUE_400", tokens::SMOOAI_BLUE_400),
("SMOOAI_BLUE_500", tokens::SMOOAI_BLUE_500),
("SMOOAI_DARK_BLUE", tokens::SMOOAI_DARK_BLUE),
("SMOOAI_DARK_BLUE_850", tokens::SMOOAI_DARK_BLUE_850),
("SMOOAI_DARK_BLUE_700", tokens::SMOOAI_DARK_BLUE_700),
("SMOOAI_WHITE", tokens::SMOOAI_WHITE),
("SMOOAI_GRAY_400", tokens::SMOOAI_GRAY_400),
("BACKGROUND", tokens::BACKGROUND),
("CARD", tokens::CARD),
("BORDER", tokens::BORDER),
] {
assert!(
css.contains(value),
"token {name} = {value:?} not found in shared/styles.css — Rust \
constants and CSS have drifted",
);
}
}
// The token <-> CSS cross-check (both directions) lives in `shared/`, so
// SmooAI/client-shared and SmooAI/ui run the identical assertions.
include!("../../../shared/tokens_css_check.rs");

#[test]
fn semantic_classes_exist() {
Expand All @@ -123,6 +68,10 @@ mod tests {
".rail",
".rail__btn",
".brand-badge",
".input",
".input--lg",
".input-error",
".input-hint",
] {
assert!(STYLES.contains(cls), "missing class {cls}");
}
Expand Down
Loading
Loading