Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/demo-daemon-mode.md
Original file line number Diff line number Diff line change
@@ -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.
106 changes: 106 additions & 0 deletions crates/smooth-daemon/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<dyn smooth_tools::NotifySink>>,
/// 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
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -469,6 +523,7 @@ pub fn local_tool_provider_full(
family,
modes,
notify_sink,
demo: demo_mode(),
})
}

Expand Down Expand Up @@ -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<String> = 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);
Expand Down
86 changes: 86 additions & 0 deletions docs/Operations/App-Store-Reviewer-Demo.md
Original file line number Diff line number Diff line change
@@ -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 / <password>
> (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.
Loading