From 822486b53f37d314332e2ae6972d380f5aefa9d1 Mon Sep 17 00:00:00 2001 From: Brent Rager Date: Wed, 9 Sep 2026 15:40:22 -0400 Subject: [PATCH] =?UTF-8?q?th-a455be:=20SMOOTH=5FDEMO=20mode=20=E2=80=94?= =?UTF-8?q?=20a=20host-safe=20daemon=20for=20the=20App=20Store=20reviewer?= =?UTF-8?q?=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Big Smooth iOS can't be reviewed without a paired Mac daemon ("Open Big Smooth on your Mac"), so submitting it needs a safe hosted demo a review account auto-connects to over the relay. But relay phones authenticate as the daemon OWNER (full toolset, Bypass) — Plan mode and family RBAC don't gate them — and the demo creds ship in the review notes, so env-only lockdown isn't airtight. SMOOTH_DEMO=1 clamps every turn to a deny-by-default read-only allowlist (DEMO_SAFE_TOOLS), applied LAST and unconditionally in tools_for, so a reviewer gets chat + safe reads only (read_file/list_files/grep confined to SMOOTH_WORKSPACE, web_search/crawl/knowledge_search/recall, datetime/weather, create_artifact, cd/present_plan/todo_write) and never bash / write_file / edit_file / th / create_skill / send_file / remember / calendar / reminders / imessage / contacts / MCP / send_sidekick — regardless of auto-mode or principal. Pair with SMOOTH_WORKSPACE (throwaway) + SMOOTH_EGRESS_ALLOWLIST. Runbook: docs/Operations/App-Store-Reviewer-Demo.md (stand-up + verify + ASC review-notes template). Test: demo_mode_clamps_to_the_safe_set_even_for_an_owner _in_auto proves the clamp holds for an owner in Auto (the relay reviewer's exact posture). Parent th-73e3bf. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HVAvzYG7unJjCD9c3B9j1f --- .changeset/demo-daemon-mode.md | 7 ++ crates/smooth-daemon/src/operator.rs | 106 +++++++++++++++++++++ docs/Operations/App-Store-Reviewer-Demo.md | 86 +++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 .changeset/demo-daemon-mode.md create mode 100644 docs/Operations/App-Store-Reviewer-Demo.md diff --git a/.changeset/demo-daemon-mode.md b/.changeset/demo-daemon-mode.md new file mode 100644 index 000000000..0207610be --- /dev/null +++ b/.changeset/demo-daemon-mode.md @@ -0,0 +1,7 @@ +--- +'@smooai/smooth': patch +--- + +Add SMOOTH_DEMO mode: a locked-down daemon for the App Store reviewer demo (th-a455be). + +Big Smooth iOS can't be reviewed without a paired Mac daemon (its empty state is "Open Big Smooth on your Mac"), so submitting it needs a safe hosted demo a review account auto-connects to over the relay. But relay phones authenticate as the daemon owner (full toolset, Bypass) — Plan mode and family RBAC don't gate them — so env-only lockdown isn't airtight. `SMOOTH_DEMO=1` clamps every turn to a deny-by-default read-only allowlist (`DEMO_SAFE_TOOLS`) applied last and unconditionally, so a reviewer (or anyone with the demo creds) gets chat + safe reads only — never bash / write / th / calendar / imessage / MCP — regardless of mode or principal. Pair with `SMOOTH_WORKSPACE` (throwaway dir) + `SMOOTH_EGRESS_ALLOWLIST`. Runbook: docs/Operations/App-Store-Reviewer-Demo.md. diff --git a/crates/smooth-daemon/src/operator.rs b/crates/smooth-daemon/src/operator.rs index 0dba37f7f..d0af279b4 100644 --- a/crates/smooth-daemon/src/operator.rs +++ b/crates/smooth-daemon/src/operator.rs @@ -158,6 +158,46 @@ struct SandboxedToolProvider { /// self-notify fan-out. `None` for the ephemeral/test providers that don't /// wire push; the always-on daemon passes the shared [`crate::notify::TurnNotifier`]. notify_sink: Option>, + /// App Store reviewer demo (`SMOOTH_DEMO`, th-a455be). When true, `tools_for` + /// clamps the per-turn set to [`DEMO_SAFE_TOOLS`] — deny-by-default, applied + /// LAST and UNCONDITIONALLY. This is the ONLY thing that constrains a relay + /// reviewer: the relay bridge authenticates as the owner (Role::Admin, no + /// `role:` group), so family RBAC doesn't gate it, and Plan mode is + /// per-conversation and toggleable from the phone — neither can be trusted + /// when the demo creds ship in the App Store review notes. + demo: bool, +} + +/// The tools the App Store reviewer demo (`SMOOTH_DEMO`) exposes — chat plus a +/// strictly read-only, host-safe subset. Deny-by-default: anything not listed +/// (`write_file`, `edit_file`, `bash`, `th`, `create_skill`, `send_file`, +/// `remember`, calendar/reminders/imessage/contacts, plugins, MCP, +/// `send_sidekick`, `notify`) never reaches the model, regardless of auto-mode or +/// principal. Read/list/grep stay confined to `SMOOTH_WORKSPACE`; `web_search`/ +/// `crawl` stay behind the egress allowlist — so a reviewer sees a working agent +/// that cannot touch the host. NB: no `contacts` (macOS personal data) even +/// though Plan mode allows it — a reviewer must not read the host's address book. +const DEMO_SAFE_TOOLS: &[&str] = &[ + "read_file", + "list_files", + "grep", + "web_search", + "knowledge_search", + "crawl", + "recall", + "get_current_datetime", + "get_weather", + "create_artifact", + "cd", + "present_plan", + "todo_write", +]; + +/// True when `SMOOTH_DEMO` is set to a truthy value (App Store reviewer demo). +/// Same truthy grammar as `fast_mode` — unset / `0` / `false` / `no` / `off` / +/// blank all read as off. +fn demo_mode() -> bool { + matches!(std::env::var("SMOOTH_DEMO"), Ok(v) if !matches!(v.trim().to_ascii_lowercase().as_str(), "" | "0" | "false" | "no" | "off")) } /// The tools a **Plan-mode** turn may keep — a strict read-only allowlist @@ -407,6 +447,20 @@ impl ToolProvider for SandboxedToolProvider { "plan mode: filtered to read-only tools" ); } + // App Store reviewer demo (th-a455be): clamp to the host-safe allowlist, + // LAST and UNCONDITIONAL. A relay reviewer authenticates as the owner in + // Bypass and can toggle Plan off from the phone, so this is the only + // filter that actually holds — deny-by-default, nothing outside + // DEMO_SAFE_TOOLS reaches the model no matter the mode or principal. + if self.demo { + let before = tools.len(); + tools.retain(|t| DEMO_SAFE_TOOLS.contains(&t.schema().name.as_str())); + tracing::info!( + kept = tools.len(), + dropped = before - tools.len(), + "SMOOTH_DEMO: clamped to host-safe reviewer tool set" + ); + } tools } } @@ -469,6 +523,7 @@ pub fn local_tool_provider_full( family, modes, notify_sink, + demo: demo_mode(), }) } @@ -1830,6 +1885,57 @@ mod tests { } } + #[tokio::test] + async fn demo_mode_clamps_to_the_safe_set_even_for_an_owner_in_auto() { + use smooth_operator_svc::access_control::AccessContext; + // Build the provider directly with demo=true (no SMOOTH_DEMO env, so no + // cross-test race) and an OWNER principal in the default (Auto) mode — + // exactly what a relay reviewer is. The clamp must still hold. + let provider = SandboxedToolProvider { + cwd: SessionCwd::new(std::env::temp_dir()), + proxy: None, + memory: Arc::new(smooth_operator::InMemoryMemory::new()), + mcp: None, + family: None, + modes: crate::session_mode::SessionModes::new(), + notify_sink: None, + demo: true, + }; + let sink = Arc::new(std::sync::Mutex::new(serde_json::Value::Null)); + let mut ctx = ToolProviderContext::new(Some("org".into()), AccessContext::new(Some("owner".into()), vec![])).with_conversation_id("demo-conv"); + ctx.directive_sink = Some(sink); + let names: Vec = provider.tools_for(&ctx).await.iter().map(|t| t.schema().name).collect(); + + // Every dangerous tool is gone — a reviewer cannot mutate the host, run a + // shell, text anyone, read contacts, or delegate. + for banned in [ + "write_file", + "edit_file", + "bash", + "send_file", + "create_skill", + "remember", + "th", + "send_sidekick", + "calendar", + "reminders", + "imessage", + "contacts", + "notify", + ] { + assert!(!names.iter().any(|n| n == banned), "demo mode must DROP {banned}: {names:?}"); + } + // Chat + safe reads remain so the reviewer sees a working agent. + for keep in ["read_file", "web_search", "get_current_datetime"] { + assert!(names.iter().any(|n| n == keep), "demo mode keeps {keep}: {names:?}"); + } + // Nothing outside the allowlist survived — deny-by-default. + assert!( + names.iter().all(|n| DEMO_SAFE_TOOLS.contains(&n.as_str())), + "demo mode leaves only allowlisted tools: {names:?}" + ); + } + #[test] fn gateway_llm_factory_builds_config_from_env_gateway() { let _guard = GATEWAY_ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); diff --git a/docs/Operations/App-Store-Reviewer-Demo.md b/docs/Operations/App-Store-Reviewer-Demo.md new file mode 100644 index 000000000..eac6e81d6 --- /dev/null +++ b/docs/Operations/App-Store-Reviewer-Demo.md @@ -0,0 +1,86 @@ +# App Store Reviewer Demo — the locked-down Big Smooth daemon + +Big Smooth iOS is a client for the user's **own** Big Smooth daemon on their Mac. +Its empty state says "Open Big Smooth on your Mac", so an App Store reviewer can't +exercise it without a paired daemon — and Apple rejects apps they can't fully test +(Guideline 4.2 / 2.1). This runbook stands up a **safe, always-on demo daemon** that +a demo/review Smoo account auto-connects to over the relay, so the reviewer just +signs in and chats. Pearl th-73e3bf (submission) / th-a455be (this daemon mode). + +## Why it's safe + +The relay bridge authenticates a phone as the daemon **owner** (full toolset, Bypass +mode) — Plan mode + family RBAC do **not** constrain a relay reviewer, and the demo +creds ship in the review notes, so the daemon must be airtight on its own. Three +layers, in order of importance: + +1. **`SMOOTH_DEMO=1`** — the load-bearing one. The daemon clamps every turn to a + deny-by-default read-only allowlist (`DEMO_SAFE_TOOLS` in + `crates/smooth-daemon/src/operator.rs`): `read_file`, `list_files`, `grep`, + `web_search`, `knowledge_search`, `crawl`, `recall`, `get_current_datetime`, + `get_weather`, `create_artifact`, `cd`, `present_plan`, `todo_write`. Everything + else — `bash`, `write_file`, `edit_file`, `th`, `create_skill`, `send_file`, + `remember`, calendar/reminders/imessage/contacts, plugins, MCP, `send_sidekick`, + `notify` — never reaches the model, regardless of auto-mode or principal. Applied + LAST and unconditionally, so no phone-side mode toggle can escape it. +2. **`SMOOTH_WORKSPACE=/opt/demo-scratch`** — a throwaway dir with a couple of sample + files. `read_file`/`list_files`/`grep` are confined here (`resolve_workspace_path`), + so the reviewer can demo "ask it about these files" without seeing anything real. +3. **`SMOOTH_EGRESS_ALLOWLIST=llm.smoo.ai,api.smoo.ai,auth.smoo.ai`** — even the + surviving network tools can only reach the gateway; everything else is kernel-denied + by the goalie proxy. + +Run it on **Linux** for a fourth layer: the macOS personal-data tools +(calendar/reminders/imessage/contacts/location) are `#[cfg(target_os = "macos")]` and +are not even compiled in, so they can never be reached. `SMOOTH_DEMO` already drops +them from the allowlist, so macOS hosting is safe too — Linux is defense-in-depth. + +## Stand it up + +1. **Create a demo Smoo account** (e.g. `demo@smoo.ai`) with its own org. Keep the org + empty (no real CRM/knowledge) — `knowledge_search`/`recall` read this org. +2. **Provision a small always-on host** (Linux preferred). Install `th` / the daemon. +3. **Sign the box in as the demo account** — headless device-code flow: + ```bash + th auth login # approve once in a browser; writes ~/.smooth/auth/smooai-user.json + ``` + The credential heartbeat keeps the relay registration alive indefinitely. +4. **Prepare the scratch workspace:** + ```bash + mkdir -p /opt/demo-scratch + printf 'Big Smooth demo. Ask me to summarize this file, search the web, or draft something.\n' > /opt/demo-scratch/README.txt + ``` +5. **Run the daemon locked down:** + ```bash + SMOOTH_DEMO=1 \ + SMOOTH_WORKSPACE=/opt/demo-scratch \ + SMOOTH_EGRESS_ALLOWLIST=llm.smoo.ai,api.smoo.ai,auth.smoo.ai \ + SMOOTH_RELAY_LABEL="Big Smooth (Demo)" \ + th daemon + ``` + (Wrap in a launchd/systemd unit for keepalive.) Confirm the log shows + `SMOOTH_DEMO: clamped to host-safe reviewer tool set` on the first turn. + +## Verify before submitting + +- On a device signed into the demo account, open Big Smooth → it auto-connects to + "Big Smooth (Demo)" (single online daemon, no picker). +- Send: "what can you do?" and "summarize /opt/demo-scratch/README.txt" → works. +- Try to make it misbehave: "run `ls ~` in bash", "delete a file", "text someone", + "read my calendar" → it has no such tool and declines. This is the check that + matters. + +## App Store Connect review notes (template) + +> Big Smooth is a client for your own AI assistant that runs on your Mac. To review +> without a Mac, sign in with the demo account below — it auto-connects to a hosted +> demo assistant you can chat with. +> +> Demo account: demo@smoo.ai / +> (Or use Sign in with Apple / Google on the sign-in screen.) +> +> The assistant can chat, search the web, and read the sample files in its demo +> workspace. On a real install it runs on the user's own Mac and can do more, gated +> by the user's approval. + +Fill the password in ASC, not here.