| name | breeze-helper |
| description | Quick reference for the Breeze Helper Tauri desktop app — architecture, Rust backend commands, React frontend, config files, IPC with the Go agent, helper chat API routes, tool approval flow, and tray integration. Use when working on helper code, debugging helper issues, adding helper features, or understanding how the Helper communicates with the API and agent. |
Breeze Helper Reference
Lightweight Tauri v2 desktop app providing an AI chat assistant, device info, and tool approval interface. Runs in the system tray, spawned by the Go agent on Windows as a SYSTEM process in the user's session.
Architecture Overview
apps/helper/
src/
App.tsx # Main UI (chat, device info, history, approval popup)
main.tsx # React entry point
stores/chatStore.ts # Zustand state (auth, messages, SSE streaming, approvals)
styles.css # Dark-theme styling
src-tauri/
src/lib.rs # Rust backend (~776 lines): config, HTTP client, tray, streaming
tauri.conf.json # App config (380×600 frameless, tray icon, CSP)
Cargo.toml # Rust deps (tauri v2, reqwest, tokio, serde_yaml)
build.rs # Tauri build script
vite.config.ts # Frontend build
package.json # React + Zustand + react-markdown
Tauri Commands (Rust → Frontend)
Registered in lib.rs via generate_handler![]:
| Command | Returns | Purpose |
|---|
read_agent_config | AgentConfig | Load agent.yaml + secrets.yaml, init HTTP client |
helper_fetch | HelperFetchResponse | Authenticated HTTP proxy with mTLS + SSRF protection |
hide_window | — | Hide window to tray |
minimize_window | — | Minimize window |
get_os_username | String | OS username (falls back to USERNAME env for SYSTEM context) |
get_helper_config | HelperConfig | Load helper_config.yaml (tray menu toggles) |
update_chat_active | — | Write helper_status.yaml (agent reads for idle detection) |
helper_fetch — Key Design
The central HTTP proxy command. All frontend API calls go through this.
- SSRF protection: Validates request URL starts with configured
api_url
- Auth injection: Always sets
Authorization: Bearer <token>, prevents frontend override
- mTLS: Builds
reqwest::Client with Identity::from_pem if cert+key present in secrets.yaml
- Streaming: When
stream: true, spawns tokio task that emits helper-fetch-stream Tauri events as StreamChunkEvent { stream_id, chunk, done, error }
- Non-stream: Returns full body inline in
HelperFetchResponse
Config Files
All in the agent config directory (same as agent.yaml):
| File | Writer | Reader | Purpose |
|---|
agent.yaml | Go agent | Helper | server_url, agent_id |
secrets.yaml | Go agent | Helper | auth_token, mtls_cert_pem, mtls_key_pem (0640) |
helper_config.yaml | Go agent | Helper | Tray menu toggles, portal URL, device name/status |
helper_status.yaml | Helper | Go agent | version, chat_active, last_activity, pid |
Config Paths
| Platform | Path |
|---|
| macOS | /Library/Application Support/Breeze/ |
| Windows | %ProgramData%\Breeze\ |
| Linux | /etc/breeze/ |
HelperConfig Fields
show_open_portal: true
show_device_info: true
show_request_support: true
portal_url: ""
device_name: ""
device_status: ""
last_checkin: ""
Config reloads every 60s; tray menu rebuilds on change.
Frontend (React + Zustand)
Views (in App.tsx)
- Connection states — connecting, error, disconnected (with retry)
- UsernamePrompt — one-time name entry (persists to localStorage, falls back to OS username)
- SessionHistory — list past conversations, load by ID
- DeviceInfoView — hostname, OS, status, agent version (from
GET /devices/by-agent/:agentId)
- Chat — messages, markdown rendering, tool indicators, thinking dots, approval popup
Chat Store (chatStore.ts)
State: connectionState, agentConfig, sessionId, messages, isStreaming, pendingApproval, sessions, username
Key methods:
initialize() — loads agent config via Tauri invoke (or VITE_* env vars for dev)
sendMessage(content) — creates session if needed, POSTs via SSE stream
loadSessions() / loadSession(id) — session history
approveExecution(executionId, approved) — tool approval
clearMessages() — closes session, resets state
HTTP Layer (Tauri vs Dev)
Two helper functions abstract Tauri invoke vs native fetch:
helperRequest(config, url, options) — non-streaming requests
helperStreamRequest(config, url, options, onChunk, onDone) — SSE streaming
In Tauri: calls helper_fetch (Rust mTLS client). In browser dev: uses native fetch() with VITE_API_URL / VITE_AGENT_TOKEN / VITE_AGENT_ID.
Stream listener race fix: Event listener registered BEFORE helper_fetch invoke to avoid missing chunks when proxy buffers the full response.
SSE Event Processing
processSSELines() handles: message_start, content_delta, tool_use_start, tool_result, message_end, approval_required, error, done. Plan events (plan_approval_required, etc.) are received but not yet rendered.
Helper Chat API Routes
apps/api/src/routes/helper/index.ts — mounted at /api/v1/helper/*
Auth: Agent bearer token (brz_ prefix) via helperAuth middleware (SHA-256 hash lookup against devices.agentTokenHash). Creates synthetic AuthContext scoped to device's org.
| Method | Path | Purpose |
|---|
| POST | /chat/sessions | Create session (optional helperUser, permissionLevel) |
| POST | /chat/sessions/:id/messages | Send message + SSE response stream |
| GET | /chat/sessions | List device sessions (filterable by ?helperUser=) |
| GET | /chat/sessions/:id/messages | Load message history |
| DELETE | /chat/sessions/:id | Close session |
| POST | /chat/sessions/:id/approve/:executionId | Approve/reject tool execution |
| GET | /config | Return helper config (permission level, screen capture) |
| POST | /screenshots | Upload screenshot (base64, 24h retention) |
Pre-flight Checks (per message)
- Session exists and belongs to device
- Session not expired (24h max age)
- Turn limit not exceeded
- Rate limit: 30 msg/60s per device (Redis)
- Org budget check (fail-closed)
- Input sanitization
Permission Levels
| Level | Description |
|---|
basic | Read-only tools |
standard | Default — read + some write tools |
extended | All tools including destructive operations |
Configured via helperToolFilter.ts → getHelperAllowedMcpToolNames(level).
Tool Approval Flow
- AI invokes a restricted tool →
approval_required SSE event
ToolApprovalPopup renders with description, device badge, parameters
- 5-minute auto-deny countdown timer
- User clicks Allow/Deny →
POST /chat/sessions/:id/approve/:executionId
- Hidden input keys filtered from display:
deviceId, orgId, siteId, sessionId
Agent IPC Integration
The Go agent spawns Helper in user sessions. Key IPC types from agent/internal/ipc/message.go:
| Type | Direction | Purpose |
|---|
auth_request/response | Helper → Agent | Authentication on connect |
capabilities | Helper → Agent | Report features (notify, tray, capture, clipboard) |
desktop_start/stop | Agent → Helper | WebRTC session lifecycle |
desktop_input | Agent → Helper | Keyboard/mouse relay |
clipboard_get/set/data | Bidirectional | Clipboard sync |
sas_request/response | Helper → Agent | Ctrl+Alt+Del (Windows) |
launch_process | Agent → Helper | Execute as logged-in user |
Helper roles: system (SYSTEM token, desktop capture) or user (logged-in user, script execution).
Windows SYSTEM Context
- Agent service (Session 0) spawns Helper via
CreateProcessAsUser with SYSTEM token + session ID override
- Helper detects SYSTEM context in
lib.rs setup: checks LOCALAPPDATA for "systemprofile"
- Redirects WebView2 data dir to
%ProgramData%\Breeze\helper-webview\ to avoid access issues
get_os_username() falls back to USERNAME env var when whoami returns "SYSTEM"
Tray Menu
Built dynamically from HelperConfig flags:
| Menu Item | Action |
|---|
| Request Support | Show chat window |
| Open Breeze Portal | Open portal URL in browser |
| Device Info | Emit show-device-info event, show device info view |
| Exit | app.exit(0) |
Left-click on tray icon → show chat window. Right-click → context menu.
Security
- SSRF prevention:
helper_fetch validates URL starts with configured API URL
- Auth header protection: Authorization always set by Rust, frontend can't override
- mTLS: Client certificate from secrets.yaml (optional, for Cloudflare mTLS)
- Token hashing: Agent token validated via SHA-256 hash comparison
- Device scoping: All session queries include
deviceId filter
- Input sanitization:
sanitizeUserMessage() applied to all chat input
- Rate limiting: 30 msg/min per device via Redis sliding window
- Budget enforcement: Org budget checked before every message (fail-closed)
Build & Development
cd apps/helper && pnpm tauri dev
cd apps/helper && pnpm tauri build
cd apps/helper && pnpm dev
Dev env vars (for browser-only development without Tauri):
VITE_API_URL=http://localhost:3001
VITE_AGENT_TOKEN=brz_<token>
VITE_AGENT_ID=<device-uuid>
Bundle Targets
| Platform | Format |
|---|
| macOS | DMG (/Applications/Breeze Helper.app) |
| Windows | MSI via WiX (C:\Program Files\Breeze Helper\breeze-helper.exe) |
| Linux | AppImage (/usr/local/bin/breeze-helper) |
Key Dependencies
Rust: tauri v2, reqwest (mTLS), tokio, serde/serde_yaml, chrono, whoami
TypeScript: React 18.3, Zustand 4.5, react-markdown 10, @tauri-apps/api 2.0
Data Flow: Chat Message
1. User types in chat input (App.tsx)
2. chatStore.sendMessage(content)
→ Creates session via POST /helper/chat/sessions (if needed)
→ Adds optimistic user message
→ helperStreamRequest() to POST /helper/chat/sessions/:id/messages
3. In Tauri: helper_fetch (Rust) → mTLS HTTP request → SSE response
→ Rust spawns tokio task → emits helper-fetch-stream events
→ Frontend listener → processSSELines() → Zustand state updates
4. API: helperAuth → runHelperPreFlight → streamingSessionManager
→ Claude API call with org-scoped tools
→ SSE events: message_start → content_delta → tool_use_start → tool_result → done
5. Tool approval (if Tier 3): approval_required event → ToolApprovalPopup
→ User Allow/Deny → POST /approve/:executionId → tool executes or rejects
Logging
Helper logs to helper.log in the config directory (alongside agent.yaml). In SYSTEM service context, stderr isn't connected to anything visible, so log_helper_error() appends timestamped lines to this file.
Gotchas
- WebView2 data dir: On Windows SYSTEM, must redirect to
%ProgramData%\Breeze\helper-webview\ or WebView2 init fails
- Stream listener race: Must register Tauri event listener BEFORE invoking
helper_fetch — reverse proxies (Caddy) can buffer entire SSE response and deliver at once
- Tray click events: Only match
Left + Up — matching all click variants steals focus from context menu on Windows
- 409 on concurrent messages:
streamingSessionManager.tryTransitionToProcessing prevents parallel processing; frontend removes optimistic message on 409
- Plan events:
plan_approval_required, plan_step_start, etc. received but not rendered yet