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
42 changes: 42 additions & 0 deletions .changeset/th-branding-white-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'@smooai/smooth': minor
---

`th branding` — white-label a Smoo AI org, logo included, from the CLI (SMOODEV-2820)

New top-level command (alias `th brand`): `show` / `from-url` / `set` / `enable` /
`disable` / `preview` / `clear`. It wraps the org's white-label row and, unlike
pasting a URL into the dashboard, actually re-hosts the logo — `--logo`,
`--logo-dark` and `--favicon` each take a local path or a remote URL, and a
remote one is fetched and uploaded to the org's brand assets so a partner's own
server is never left as the source of truth for their mark.

Three things it refuses to do, on purpose:

- **Go live on an unreadable theme.** `enable` (and `from-url --enable`) computes
WCAG contrast for foreground/background, primaryForeground/primary and
mutedForeground/background and stops at anything under 4.5:1. `--force`
overrides. Shipping an illegible dashboard to a partner is the failure mode
the whole gate exists for.
- **Fetch a private host.** Remote logo URLs are vetted the way the server's
`vetUrl` does — http(s) only, no loopback / RFC1918 / `169.254.` (the cloud
metadata endpoint), no redirect following, 5 MB cap — and the bytes are
magic-byte sniffed against the platform's allowlist before upload.
- **Silently wipe a theme.** The server's PUT replaces the whole `themeJson`
column, so every partial `set` is a read-modify-write over the current row.

`from-url` is a dry run by default: it prints the derived swatch table, the logo
candidates and the contrast verdict, and writes nothing. `--apply` stages
(`enabled` stays false, previewable via `?brandPreview=1`); `--enable` goes live.
It marks which candidate it picked per slot — the extractor can return several
per kind and the first isn't always the mark — and `--logo` / `--logo-dark` /
`--favicon` override the pick. A verdict with no measured ratios is reported as
vacuous rather than as a pass.

The Aurora meaning tokens (`--color-heat-0..5`, `--color-ai`, `--gradient-aurora`,
ok/warn/crit) are never white-labeled and the command exposes no flags for them.

Two server-side gaps are surfaced as diagnoses rather than bare errors: the
platform's write validator is still Phase 1, so the surface tokens
(`--background`, `--card`, `--sidebar`, …) 400 today; and `from-url` 404s until
the propose endpoint deploys.
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Twelve crates. `ls crates/` is the source of truth; this list is kept in sync wi
```
smooth/
├── crates/
│ ├── smooth-cli/ # Binary `th` — clap entry point (53 top-level commands)
│ ├── smooth-cli/ # Binary `th` — clap entry point (54 top-level commands)
│ ├── smooth-daemon/ # Binary + lib — Big Smooth: the always-on personal-agent daemon
│ ├── smooth-tools/ # Library — agent tools (fs/grep/bash) + the kernel OS sandbox
│ ├── smooth-policy/ # Library — policy types, TOML parsing, auto-mode, ext trust
Expand All @@ -42,7 +42,7 @@ smooth/

### Key Crates

- **smooth-cli** (`crates/smooth-cli/`): the `th` binary. clap entry point in `src/main.rs`, 53 top-level commands. Platform (api.smoo.ai) subcommands live in `src/smooai/`; cross-org admin in `src/admin/`.
- **smooth-cli** (`crates/smooth-cli/`): the `th` binary. clap entry point in `src/main.rs`, 54 top-level commands. Platform (api.smoo.ai) subcommands live in `src/smooai/`; cross-org admin in `src/admin/`.
- **smooth-daemon** (`crates/smooth-daemon/`): **Big Smooth.** The always-on, single-tenant personal-agent daemon (EPIC th-c89c2a). It hosts smooth-operator's `LocalServer` in-process — canonical WS protocol, no bespoke agent loop — with durable SQLite storage, scheduled/proactive turns, web push, tailnet exposure, and the security hooks. `th daemon` runs it directly; `th up` also launches it.
- **smooth-operator**: the agent engine (LLM client, agent loop, tool registry + hooks, conversation, checkpointing, cast, permissions, `DenyPolicy`). **It is not in this workspace** — it's a git/crates.io dependency from the separate `SmooAI/smooth-operator` repo. Don't look for `crates/smooth-operator/`.
- **smooth-tools** (`crates/smooth-tools/`): the reusable agent tool surface the daemon registers — `read_file`, `write_file`, `edit_file`, `list_files`, `grep`, `bash`, `cd`, `crawl`, `web_search`, `knowledge_search`, `remember`, `th`, `create_skill`, and (macOS only) `calendar`. Every filesystem path goes through `path::resolve_workspace_path`; `bash` runs only inside `sandbox.rs`'s kernel OS sandbox. `calendar` is the one documented exception (pearl th-94cc4a): it shells `ical` **outside** the sandbox because seatbelt blocks EventKit's XPC/mach lookups — argv-only, fixed binary, verb allowlist (reads + `add`/`update`/`delete`), still Narc-visible. Setup: `th doctor --setup-calendar`.
Expand Down Expand Up @@ -83,6 +83,10 @@ smooth/
# Smoo platform — replaces every curl to api.smoo.ai
th api orgs|agents|smooth-operator|knowledge|jobs|members|config|keys|observability|profile|testing

# White-label an org — theme + logos (logo re-hosted from a path OR a remote URL).
# `enable` is the live switch and refuses a theme that fails WCAG AA contrast.
th branding show|from-url|set|enable|disable|preview|clear

# Cross-org admin (planned — pearl th-feebd2, blocked on th-abc4e2)
th admin onboard-customer / mint-key / set-secret / org list|show

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/smooth-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
reqwest = { workspace = true, features = ["blocking"] }
reqwest = { workspace = true, features = ["blocking", "multipart"] }
chrono.workspace = true
chrono-tz.workspace = true
dirs-next.workspace = true
Expand Down
11 changes: 11 additions & 0 deletions crates/smooth-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ enum Commands {
#[command(subcommand)]
cmd: smooai::crm::Cmd,
},
/// White-label a Smoo AI org — app name, chrome colors, and logos (local
/// path or remote URL, always re-hosted on our CDN). `from-url` derives a
/// theme from the partner's website; `enable` is the live switch and
/// refuses a theme that fails WCAG AA contrast. The Aurora meaning tokens
/// (heat / ai / gradients) are never white-labeled.
#[command(visible_alias = "brand")]
Branding {
#[command(subcommand)]
cmd: smooai::branding::Cmd,
},
/// Run a pearl through a Smooth operative — dispatches to Big Smooth
/// (`th up` must be running) and streams agent events to stdout.
Run {
Expand Down Expand Up @@ -1720,6 +1730,7 @@ async fn main() -> Result<()> {
Some(Commands::Search { args }) => smooai::websearch::run(args).await,
Some(Commands::Knowledge { cmd }) => smooai::knowledge::cmd(cmd).await,
Some(Commands::Crm { cmd }) => smooai::crm::cmd(cmd).await,
Some(Commands::Branding { cmd }) => smooai::branding::cmd(cmd).await,
Some(Commands::WebSearch { cmd }) => smooai::websearch::cmd(cmd).await,
Some(Commands::Llm { cmd }) => smooai::llm_gateway::cmd(cmd).await,
Some(Commands::Notify {
Expand Down
Loading
Loading