From 00e50f047e839248fbb4cb3ea1ec8047bef63e6c Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:40:43 +0530 Subject: [PATCH 1/6] fix(security): allowlist run_shell commands and audit invocations run_shell previously gated only on a metacharacter denylist, so any allowlisted-metachar-free command could be invoked through the agent. Add SHELL_ALLOWLIST (pio, git, npm family, cargo family, python, make, cmake, ninja, plus a handful of read-only utilities) and reject anything else with a clear error. extract_program normalizes the program name (strips path prefix and Windows .exe/.cmd/.bat/.ps1 suffix) before the allowlist check. Every successful invocation is logged via log::info! so the user can audit what the AI ran. Covered by 8 new unit tests in filesystem.rs (allowlist accept/reject, metachar rejection, empty-input rejection, program extraction with paths and Windows extensions). Adds 6 more tests for the surrounding validators (leaf name traversal, lexical normalization, validate_path containment). Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/commands/filesystem.rs | 214 ++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/filesystem.rs b/src-tauri/src/commands/filesystem.rs index e3dbb2a..29dc1e3 100644 --- a/src-tauri/src/commands/filesystem.rs +++ b/src-tauri/src/commands/filesystem.rs @@ -570,11 +570,77 @@ pub fn stop_watch(app: tauri::AppHandle) -> Result<(), String> { Ok(()) } -#[command] -pub async fn run_shell(command: String, cwd: Option, root: Option) -> Result { - if command.contains(&['&', '|', ';', '\n', '\r', '$', '`', '(', ')', '{', '}', '<', '>', '!', '~', '*', '?', '[', ']'][..]) { +// Commands the agent is permitted to invoke through `run_shell`. The list is +// intentionally narrow: tools the embedded-development workflow legitimately +// needs, nothing else. Adding to this list is a security decision. +const SHELL_ALLOWLIST: &[&str] = &[ + "pio", "platformio", + "git", + "npm", "npx", "pnpm", "yarn", + "cargo", "rustc", "rustup", + "node", "python", "python3", "py", + "make", "cmake", "ninja", + "echo", "where", "which", "type", + "dir", "ls", "cd", "pwd", + "cat", "head", "tail", +]; + +const SHELL_METACHARS: &[char] = &[ + '&', '|', ';', '\n', '\r', '$', '`', '(', ')', '{', '}', '<', '>', '!', '~', '*', '?', '[', ']', +]; + +/// Returns the first whitespace-delimited token from `command`, stripped of +/// any leading path components and Windows `.exe`/`.cmd` suffix. Used to +/// match against `SHELL_ALLOWLIST`. +pub fn extract_program(command: &str) -> Option { + let first = command.split_whitespace().next()?; + // Strip drive/path prefix if any + let basename = std::path::Path::new(first) + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| first.to_string()); + // Strip trailing extension (Windows) + let stripped = basename + .rsplit_once('.') + .map(|(stem, ext)| { + let lower = ext.to_ascii_lowercase(); + if matches!(lower.as_str(), "exe" | "cmd" | "bat" | "ps1") { + stem.to_string() + } else { + basename.clone() + } + }) + .unwrap_or(basename); + Some(stripped.to_ascii_lowercase()) +} + +/// Validates a `run_shell` invocation against the metacharacter denylist and +/// command allowlist. Returns the canonical program name on success, or an +/// error string suitable for surfacing to the user. +pub fn validate_shell_command(command: &str) -> Result { + let trimmed = command.trim(); + if trimmed.is_empty() { + return Err("Empty shell command.".to_string()); + } + if trimmed.contains(SHELL_METACHARS) { return Err("Shell metacharacters are not allowed.".to_string()); } + let program = extract_program(trimmed) + .ok_or_else(|| "Could not parse shell command.".to_string())?; + if !SHELL_ALLOWLIST.contains(&program.as_str()) { + return Err(format!( + "Command '{}' is not on the allowlist. Allowed: {}", + program, + SHELL_ALLOWLIST.join(", ") + )); + } + Ok(program) +} + +#[command] +pub async fn run_shell(command: String, cwd: Option, root: Option) -> Result { + let program = validate_shell_command(&command)?; + log::info!("run_shell: program={} cwd={:?}", program, cwd); let mut builder = AsyncCommand::new(if cfg!(target_os = "windows") { "cmd" } else { "sh" }); if cfg!(target_os = "windows") { @@ -595,9 +661,151 @@ pub async fn run_shell(command: String, cwd: Option, root: Option out.txt").is_err()); + } + + #[test] + fn shell_rejects_empty_input() { + assert!(validate_shell_command("").is_err()); + assert!(validate_shell_command(" ").is_err()); + } + + // ---- validate_leaf_name ---- + #[test] + fn leaf_name_accepts_simple_names() { + assert!(validate_leaf_name("foo.txt", "file").is_ok()); + assert!(validate_leaf_name("module", "folder").is_ok()); + } + + #[test] + fn leaf_name_rejects_traversal() { + assert!(validate_leaf_name("../etc/passwd", "file").is_err()); + assert!(validate_leaf_name("foo/bar", "file").is_err()); + assert!(validate_leaf_name(".", "file").is_err()); + assert!(validate_leaf_name("..", "file").is_err()); + } + + #[test] + fn leaf_name_rejects_empty() { + assert!(validate_leaf_name("", "file").is_err()); + assert!(validate_leaf_name(" ", "file").is_err()); + } + + // ---- normalize_lexical ---- + #[test] + fn normalize_collapses_parent_dirs() { + let p = normalize_lexical(&PathBuf::from("a/b/../c")); + assert_eq!(p, PathBuf::from("a/c")); + } + + #[test] + fn normalize_drops_curdir() { + let p = normalize_lexical(&PathBuf::from("a/./b")); + assert_eq!(p, PathBuf::from("a/b")); + } + + // ---- validate_path: traversal containment ---- + #[test] + fn validate_path_blocks_traversal_outside_root() { + // Build a sandbox so the test runs anywhere + let tmp = std::env::temp_dir().join(format!( + "embedist_test_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let root = tmp.canonicalize().unwrap(); + let root_str = root.to_string_lossy().to_string(); + + // inside root - ok + let inside = root.join("foo.txt"); + assert!(validate_path(&inside.to_string_lossy(), &root_str).is_ok()); + + // .. escape - blocked + let escape = format!("{}/../../etc/passwd", root_str); + let err = validate_path(&escape, &root_str).unwrap_err(); + assert!(err.contains("outside project root"), "got: {}", err); + + std::fs::remove_dir_all(&root).ok(); + } +} From 4071e214e6dcb49b0ffecf287927dc80aecd35ca Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:40:54 +0530 Subject: [PATCH 2/6] fix(plan): replace fragile phase detection with explicit markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan-mode phase transitions used bare substring matches — content .includes('approve') for 'ready' and .includes('?') for 'clarify' — which produced false positives on any plan body that mentioned approval or contained a question mark anywhere. Extract detectPlanPhase() to module scope and have it prefer an explicit machine-readable marker emitted by the plan-mode prompt: [[phase: explore|design|review|clarify|ready]]. Fall back to anchored phrase matches restricted to the last 400 chars of the message body (e.g. "plan ready for approval", "awaiting clarification"), which is where the AI naturally puts the call to action. Both the plan-mode and agent-mode useEffect hooks now share the same detector. Update src/lib/prompts/modes/plan.md to document the marker contract so plan-mode responses emit one marker per response. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/AI/AIChatPanel.tsx | 29 +++++++++++++++++++++-------- src/lib/prompts/modes/plan.md | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/components/AI/AIChatPanel.tsx b/src/components/AI/AIChatPanel.tsx index c1bab6c..bb9be1c 100644 --- a/src/components/AI/AIChatPanel.tsx +++ b/src/components/AI/AIChatPanel.tsx @@ -42,6 +42,23 @@ const MODE_LABELS: Record = { agent: 'Agent', }; +// Phase detection: prefer explicit machine-readable markers, fall back to +// anchored phrase matches near the message end. Bare substring matches like +// "approve" or "?" anywhere in the body produced false positives. +type PlanPhase = 'explore' | 'design' | 'review' | 'clarify' | 'ready'; +const PHASE_MARKER_RE = /\[\[\s*phase\s*:\s*(explore|design|review|clarify|ready)\s*\]\]/i; +const READY_PHRASES = /(plan\s+ready\s+for\s+approval|awaiting\s+approval|ready\s+for\s+approval)\b/i; +const CLARIFY_PHRASES = /(awaiting\s+clarification|need(?:s|ed)?\s+clarification|please\s+clarify)\b/i; + +export function detectPlanPhase(content: string): PlanPhase | null { + const marker = PHASE_MARKER_RE.exec(content); + if (marker) return marker[1].toLowerCase() as PlanPhase; + const tail = content.slice(-400); + if (READY_PHRASES.test(tail)) return 'ready'; + if (CLARIFY_PHRASES.test(tail)) return 'clarify'; + return null; +} + function AIChatPanelContent() { const { mode, @@ -112,9 +129,8 @@ function AIChatPanelContent() { const lastMsg = messages[messages.length - 1]; if (lastMsg.role === 'assistant' && lastMsg.mode === 'plan' && lastMsg.content.trim()) { setPlanContent(lastMsg.content); - if (lastMsg.content.toLowerCase().includes('approve')) { - setPlanPhase('ready'); - } + const phase = detectPlanPhase(lastMsg.content); + if (phase) setPlanPhase(phase); } }, [mode, messages, setPlanContent, setPlanPhase]); @@ -146,11 +162,8 @@ function AIChatPanelContent() { const lastMsg = messages[messages.length - 1]; if (lastMsg.role === 'assistant' && lastMsg.mode === 'agent') { setPlanContent(lastMsg.content); - if (lastMsg.content.toLowerCase().includes('approve')) { - setPlanPhase('ready'); - } else if (lastMsg.content.includes('?')) { - setPlanPhase('clarify'); - } + const phase = detectPlanPhase(lastMsg.content); + if (phase) setPlanPhase(phase); } }, [messages, mode, setPlanContent, setPlanPhase]); diff --git a/src/lib/prompts/modes/plan.md b/src/lib/prompts/modes/plan.md index 8386c4d..1ee84ef 100644 --- a/src/lib/prompts/modes/plan.md +++ b/src/lib/prompts/modes/plan.md @@ -87,6 +87,20 @@ If you encounter ambiguities after research, ask the user specific questions: --- +## Phase Markers (REQUIRED) + +End every response with **exactly one** machine-readable phase marker on its own line. The UI uses this to drive the plan-phase indicator and reveal the Approve button. Do **not** wrap it in code fences. + +- `[[phase: explore]]` — still gathering information about the project/hardware +- `[[phase: design]]` — drafting architecture/milestones +- `[[phase: review]]` — presenting a complete plan, inviting feedback +- `[[phase: clarify]]` — waiting on user answers to questions +- `[[phase: ready]]` — plan is final and ready for the user to approve + +If you forget the marker, the UI falls back to phrase matching ("plan ready for approval", "please clarify") — but the marker is the contract. Prefer the marker. + +--- + ## Output Format Provide structured plans using: From 54a03bf924599cc172d31b775a0c78ddc295efe1 Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:40:56 +0530 Subject: [PATCH 3/6] chore(settings): add persist schema versioning settingsStore previously had no version field on its zustand persist config, so any future field reshape would silently corrupt persisted state for existing users. Declare version: 1 (baseline) and a migrate callback that is a pass-through today; future schema bumps should branch on _fromVersion and reshape `persisted` into the new schema. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/stores/settingsStore.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 134a258..51fca76 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -196,8 +196,15 @@ export const useSettingsStore = create()( toolPermissions: { ...state.toolPermissions, [tool]: permission }, })), }), - { + { name: 'embedist-settings', + // Bump this when the persisted shape changes and add a migration step below. + version: 1, + migrate: (persisted: unknown, _fromVersion: number) => { + // No prior versions exist; this is the baseline. Future bumps should + // branch on `_fromVersion` and reshape `persisted` into the new schema. + return persisted as Partial; + }, partialize: (state) => ({ providers: state.providers, customEndpoints: state.customEndpoints, From 210cd7d82f2514045a4a03cc5939835d388a4026 Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:41:00 +0530 Subject: [PATCH 4/6] chore(ci): add GitHub Actions workflow Add .github/workflows/ci.yml gating push and pull_request on: - npm ci + npm run build (tsc strict + Vite) - cargo fmt --check - cargo clippy --all-targets -- -D warnings - cargo test --all-targets Pinned to windows-latest since Embedist is Windows-only (NSIS bundle, winreg dep, ConPTY). Caches ~/.cargo and src-tauri/target keyed on Cargo.lock to keep wall-clock under ~15 minutes on warm runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e941ec6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + windows: + name: Build + Lint + Test (Windows) + runs-on: windows-latest + timeout-minutes: 25 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 24 + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache Rust target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install Tauri build prerequisites + # tauri-build needs the WebView2 runtime headers on Windows. The runner + # has them pre-installed; this step is a no-op placeholder for clarity. + run: echo "WebView2 runtime is preinstalled on windows-latest" + shell: pwsh + + - name: npm ci + run: npm ci + + - name: TypeScript + Vite build + run: npm run build + + - name: cargo fmt --check + working-directory: src-tauri + run: cargo fmt --check + + - name: cargo clippy + working-directory: src-tauri + run: cargo clippy --all-targets -- -D warnings + + - name: cargo test + working-directory: src-tauri + run: cargo test --all-targets From 10e625ce22f74cf8c6c38a7125e5f5da39705af6 Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:41:09 +0530 Subject: [PATCH 5/6] docs: track project CLAUDE.md, reconcile TODO-fix, add v0.38.0 spec - Commit the project-level CLAUDE.md that was sitting untracked in the working tree (AGENTS.md already references it as the sibling doc). - Reconcile TODO-fix.md against current source: of the 9 items previously marked open, 6 had already shipped in prior releases (defaultImplementationMode toggle, PlanPhaseIndicator, token-usage display, SerialConfig dead code, bottom-panel resize, keyboard-shortcuts modal). Remaining items (3.8, 7.3, 8.5) are now resolved by this branch and annotated FIXED (v0.38.0). - Add docs/superpowers/specs/2026-05-28-production-grade-design.md capturing the scope, goals, non-goals, and verification criteria for the production-grade work. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 52 ++++++++ TODO-fix.md | 87 ++++--------- .../2026-05-28-production-grade-design.md | 120 ++++++++++++++++++ 3 files changed, 198 insertions(+), 61 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/superpowers/specs/2026-05-28-production-grade-design.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e7e553e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,52 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repo layout note + +The app root is this directory (`embedist/embedist`). The parent folder `embedist/` is only a container — do not run npm/cargo from there. A separate Docusaurus package lives in `docs/` with its own `package.json`; only run its scripts when editing docs. + +`AGENTS.md` in this directory contains overlapping developer notes; keep both in sync when behavior changes. + +## Commands + +Frontend scripts (`package.json`): +- `npm run dev` — Vite dev server (port 1420, required by `tauri.conf.json`) +- `npm run build` — `tsc && vite build` (runs TypeScript strict-mode check) +- `npm run tauri dev` — run the full desktop app in development +- `npm run tauri build` — produce release artifacts + +There is **no** `lint` or `test` npm script. No automated test suite exists (`*.test.*`, `*.spec.*`, `src-tauri/tests` are all absent). Validation = `npm run build` + `cargo clippy --manifest-path src-tauri/Cargo.toml` + manual app checks. + +Release artifacts from `npm run tauri build`: +- Portable: `src-tauri/target/release/embedist.exe` +- Installer: `src-tauri/target/release/bundle/nsis/Embedist__x64-setup.exe` (NSIS only — set by `tauri.conf.json` `bundle.targets`) + +## Release version sync + +When bumping the version, update **all three** files — they're independent and drift silently: +- `package.json` +- `src-tauri/Cargo.toml` +- `src-tauri/tauri.conf.json` + +## Architecture + +Tauri 2 desktop app (Windows-only target). React 18 + TypeScript (strict) frontend talks to a Rust backend via Tauri `invoke` commands. + +**Frontend entry:** `src/main.tsx` → `src/App.tsx`. State lives in Zustand stores under `src/stores/` (`aiStore`, `fileStore`, `settingsStore`, `uiStore`) with `localStorage` persistence. Business logic is in hooks (`src/hooks/use*.ts`). + +**Backend entry:** `src-tauri/src/main.rs` → `src-tauri/src/lib.rs`. All Tauri commands are registered in the `invoke_handler!` macro in `lib.rs` and implemented in `src-tauri/src/commands/{ai,filesystem,platformio,pty,serial}.rs`. Shared async state (`SerialState`, `AIState`, `BuildState`, `PtyState`, `WatchState`) is attached via `.manage()` in `lib.rs` — add new state there. + +**AI modes:** chat / plan / agent / debug. Mode-specific system prompts are Markdown files at `src/lib/prompts/modes/*.md`, imported as `?raw` strings via `src/lib/prompts/index.ts`. `PROMPTS` in that file also defines each mode's RAG context categories and empty-state copy. + +**Tool calling:** Agent-mode tool execution runs client-side — `src/hooks/useAgent.ts` drives the loop (up to `MAX_ITERATIONS = 50`), dispatching to tools in `src/lib/agent-tools.ts`. Path safety checks live in `useAgent.ts` (`isPathSafe`, `PROJECT_SCOPED_TOOLS`). The Rust backend (`src-tauri/src/commands/ai.rs`) implements tool-enabled chat for OpenAI / Anthropic / DeepSeek / custom OpenAI-compatible endpoints. **Ollama and Google providers are text-only** in the backend — don't assume tool calling works there. + +**PlatformIO integration:** `src-tauri/src/commands/platformio.rs` shells out to `pio`. Build output streams via Tauri events; errors are parsed into a Problems panel. Build cancellation uses the shared `BuildState`. + +**CSP:** `tauri.conf.json` restricts `connect-src` to the specific AI provider hosts + `localhost:11434` (Ollama). Adding a new provider host means updating that CSP. + +## Rust gotchas (from prior pain) + +- For cloneable async shared state, use `Arc>` (see `BuildState` in `src-tauri/src/commands/platformio.rs`). +- Do **not** use `Option::is_none_or(...)` — prefer `is_some_and(...)` or explicit match for stable Rust. +- `run_shell` in `src-tauri/src/commands/filesystem.rs` rejects shell metacharacters; it won't run chained/piped expressions. diff --git a/TODO-fix.md b/TODO-fix.md index 58db09a..147a44f 100644 --- a/TODO-fix.md +++ b/TODO-fix.md @@ -1,6 +1,6 @@ # Embedist — TODO Fix List -Generated from comprehensive codebase analysis. Last updated: v0.34.0. +Generated from comprehensive codebase analysis. Last reconciled against source: **v0.38.0** (2026-05-28). ## Legend - **BROKEN**: Feature doesn't work at all @@ -84,26 +84,17 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 3.1 Agent Mode with Non-Tool Providers — FIXED - **Fixed in v0.33.0**: Warning banner shown in Agent mode when using DeepSeek/Ollama/Google -### 3.2 `defaultImplementationMode` Toggle — MISSING -- `settingsStore.ts:45` has `defaultImplementationMode: 'agent'` but no UI control -- **Fix**: Add "Default Mode After Plan Approval" toggle in `AISettings.tsx` with Chat/Agent options -- **Effort**: Low -- **Files**: `src/stores/settingsStore.ts`, `src/components/Settings/sections/AISettings.tsx` +### 3.2 `defaultImplementationMode` Toggle — FIXED +- Verified 2026-05-28: `settingsStore.ts:57` defines the field and setter; `AISettings.tsx:349-357` exposes the "After Plan Approval" Chat/Agent control. ### 3.3 AI Model Parameters — FIXED - **Fixed in v0.33.0**: Temperature, top_p, max_tokens controls added to AISettings, passed to Rust -### 3.4 `PlanPhaseIndicator` Stub — STUB -- `src/components/AI/PlanPanel/PlanPhaseIndicator.tsx` returns `null` -- **Fix**: Implement phase progress indicator (5 steps: explore→design→review→clarify→ready, current highlighted) -- **Effort**: Low -- **Files**: `src/components/AI/PlanPanel/PlanPhaseIndicator.tsx` +### 3.4 `PlanPhaseIndicator` Stub — FIXED +- Verified 2026-05-28: `PlanPhaseIndicator.tsx` renders the 5-phase indicator (explore→design→review→clarify→ready) with per-phase styling. -### 3.5 Token Usage Display — MISSING -- Backend returns `TokenUsage` but frontend ignores it -- **Fix**: Display token count in StatusBar or `MessageBubble.tsx`. Add "Show Tokens" toggle in AISettings. -- **Effort**: Low -- **Files**: `src/components/AI/MessageBubble.tsx`, `src/stores/aiStore.ts` +### 3.5 Token Usage Display — FIXED +- Verified 2026-05-28: `aiStore.ts:21-25` carries `usage` on each `AIMessage`; `MessageBubble.tsx:126-130` renders `total_tokens` when present. ### 3.6 Provider Model List Mismatch — FIXED - **Fixed in v0.33.0**: Model lists consolidated to shared constants @@ -111,11 +102,8 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 3.7 API Request Timeout — FIXED - **Fixed in v0.11.9**: 60s timeout added to `reqwest::Client` -### 3.8 Plan Phase Auto-Detection Fragile — FRAGILE -- Phase transitions use content substring matching (`'approve'`, `'?'`) — unreliable -- **Fix**: Use explicit phase markers in AI response format, or rely on explicit user/AI action buttons -- **Effort**: Medium -- **Files**: `src/components/AI/AIChatPanel.tsx` +### 3.8 Plan Phase Auto-Detection Fragile — FIXED (v0.38.0) +- Replaced bare-substring matching with explicit `[[phase: ready]]` / `[[phase: clarify]]` markers (emitted by the plan-mode prompt) plus a tightened phrase fallback anchored to the last 400 chars. `detectPlanPhase()` is exported from `AIChatPanel.tsx` for future unit testability. ### 3.9 Plan "Discard" — FIXED - **Fixed in v0.33.0**: Discard now clears plan-related system messages from store @@ -158,11 +146,8 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ## 5. SERIAL -### 5.1 `SerialConfig` Dead Code — RUST WARNING -- `serial.rs` has `SerialConfig` struct generating `dead_code` warning -- **Fix**: Use it (pass config from frontend) or remove it -- **Effort**: Trivial -- **Files**: `src-tauri/src/commands/serial.rs` +### 5.1 `SerialConfig` Dead Code — FIXED +- Verified 2026-05-28: `serial.rs` no longer contains a `SerialConfig` struct; clippy passes clean with `-D warnings`. ### 5.2 Serial Code Duplication — FIXED - **Fixed in v0.33.0**: Consolidated into `useSerial.ts`, `SerialMonitor.tsx` is a thin wrapper @@ -198,11 +183,8 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 6.5 Sidebar Width Persistence — FIXED - **Fixed in v0.33.0**: `sidebarWidth` persisted in `uiStore` -### 6.6 Bottom Panel Resize — PARTIAL -- `uiStore` has `bottomPanelHeight` but no drag handle exists -- **Fix**: Add drag handle at top of `BottomPanel.tsx` -- **Effort**: Low -- **Files**: `src/components/Layout/BottomPanel.tsx`, `src/stores/uiStore.ts` +### 6.6 Bottom Panel Resize — FIXED +- Verified 2026-05-28: `BottomPanel.tsx:76-80` renders a `bottom-panel-resize-handle` with `onMouseDown` wiring to the resize handler (`BottomPanel.tsx:22-54`). ### 6.7 App Version Mismatch — FIXED - **Fixed in v0.33.0**: Version synced across `package.json`, `Cargo.toml`, `tauri.conf.json` @@ -210,11 +192,8 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 6.8 Devtools — FIXED - **Fixed in v0.12.0**: Devtools enabled in `tauri.conf.json` -### 6.9 Keyboard Shortcut Help Modal — STUB -- MenuBar has "Keyboard Shortcuts" but it's a stub -- **Fix**: Create `KeyboardShortcutsModal.tsx` with all shortcuts grouped by category -- **Effort**: Low -- **Files**: `src/components/Layout/MenuBar.tsx`, new component +### 6.9 Keyboard Shortcut Help Modal — FIXED +- Verified 2026-05-28: `MenuBar.tsx:264` opens the modal; `MenuBar.tsx:458-507` renders the full shortcut listing grouped by File / Tabs / AI Modes / View / Build. ### 6.10 Unsaved Changes in Title Bar — FIXED - **Fixed in v0.11.9**: TitleBar shows `•` when any tab is modified @@ -232,11 +211,8 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 7.2 `chat_custom` Message Format — FIXED - **Fixed in v0.34.0**: Now uses standard OpenAI `tool_calls` format in assistant messages -### 7.3 `run_shell` Security — SECURITY -- `run_shell` in `filesystem.rs` executes arbitrary shell commands -- **Fix**: Review all callers (agent-tools.ts). Add confirmation dialog for destructive shell commands. Validate input strictly. -- **Effort**: Medium -- **Files**: `src-tauri/src/commands/filesystem.rs`, `src/lib/agent-tools.ts` +### 7.3 `run_shell` Security — FIXED (v0.38.0) +- Added `SHELL_ALLOWLIST` (`pio`, `git`, `npm`/`npx`/`pnpm`/`yarn`, `cargo`/`rustc`/`rustup`, `node`, `python`/`python3`/`py`, `make`/`cmake`/`ninja`, plus read-only utilities). `validate_shell_command()` enforces the allowlist on top of the existing metachar denylist, normalizes the program name (strips path prefix and Windows `.exe`/`.cmd`), and logs every invocation. Covered by 8 dedicated unit tests in `filesystem.rs`. Tool-permission prompt in `agent-tools.ts` is unchanged — the user can still gate `run_shell` per-call. ### 7.4 `save_plan_file` — FIXED - **Fixed in v0.33.0**: Purpose clarified — saves plan files to disk for persistence across sessions @@ -257,25 +233,14 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) ### 8.4 Cargo Clippy Warnings — FIXED - **Fixed in v0.34.0**: All clippy warnings resolved -### 8.5 Settings Store Schema Migration — ONGOING -- Adding fields to `settingsStore` changes persisted schema -- **Fix**: Ensure defaults in initial state. Consider versioned migrations for major changes. -- **Effort**: Ongoing +### 8.5 Settings Store Schema Migration — IN PLACE (v0.38.0) +- `settingsStore` now declares `version: 1` and a `migrate` callback in its zustand `persist` config. Baseline is a pass-through; future schema bumps should branch on `_fromVersion` to reshape the persisted state. --- ## QUICK WINS SUMMARY -| # | Issue | Category | Priority | Effort | -|---|-------|----------|----------|--------| -| 1 | `defaultImplementationMode` toggle | AI | P0 | Low | -| 2 | `PlanPhaseIndicator` implement | AI | P0 | Low | -| 3 | Token usage display | AI | P1 | Low | -| 4 | `SerialConfig` dead code | Rust | P1 | Trivial | -| 5 | DeepSeek tool calling support | AI | P1 | Low | -| 6 | Keyboard shortcut help modal | UI/UX | P1 | Low | -| 7 | Bottom panel resize drag handle | UI/UX | P2 | Low | -| 8 | `run_shell` security hardening | Rust | P2 | Medium | +All previously-listed quick wins have shipped. See per-section status above for verification details. --- @@ -285,13 +250,13 @@ Effort: Trivial (1 line) | Low (<1hr) | Medium (1-4hr) | High (4+hr) |----------|------|-------| | Editor | 0 | 8 | | File Explorer | 0 | 10 | -| AI / Agent | 4 | 8 | +| AI / Agent | 0 | 12 | | Build / PlatformIO | 0 | 6 | -| Serial | 1 | 5 | -| UI / UX | 2 | 9 | -| Rust Backend | 1 | 3 | -| Architecture / Debt | 1 | 4 | -| **Total** | **9** | **53** | +| Serial | 0 | 6 | +| UI / UX | 0 | 11 | +| Rust Backend | 0 | 4 | +| Architecture / Debt | 0 | 5 | +| **Total** | **0** | **62** | --- diff --git a/docs/superpowers/specs/2026-05-28-production-grade-design.md b/docs/superpowers/specs/2026-05-28-production-grade-design.md new file mode 100644 index 0000000..947073e --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-production-grade-design.md @@ -0,0 +1,120 @@ +# Production-Grade Embedist — Design Spec + +**Date:** 2026-05-28 +**Scope:** Define what "production grade" means for Embedist v0.37.0 → v0.38.0 and the work required to get there. +**Authored by:** Claude Code (autonomous, per `/goal` directive — no interactive brainstorm). + +## Problem + +Embedist ships as a single-developer Tauri 2 desktop app. As of v0.37.0 it has: + +- **No test suite** (`*.test.*`, `*.spec.*`, `src-tauri/tests` all absent). +- **No CI** (no `.github/workflows/`); every release relies on local `npm run build` + `cargo clippy`. +- **A stale TODO list** — `TODO-fix.md` claims 9 open items, but several have matching fix commits in `git log` (e.g. 3.5 token usage, 3.10 DeepSeek tool calling) suggesting drift. +- **A documented security concern** — `run_shell` (TODO 7.3) executes arbitrary shell commands subject only to a metacharacter denylist; no allowlist, no audit log. +- **Three small UX stubs** — `PlanPhaseIndicator` returns null, the keyboard-shortcuts modal is a stub, the bottom panel has no drag handle. + +Without tests and CI, regressions in any of the AI provider integrations or the Rust security surface land silently. Without TODO reconciliation, the project's published quality bar is inaccurate. + +## Goals + +1. **Truthful TODO list.** Every "open" item is genuinely open against current code. +2. **Closed quick wins.** Every Low/Trivial open item is fixed. +3. **Security-reviewed shell.** `run_shell` is either restricted to a small allowlist of safe operations, or its denylist is upgraded with rationale, audit logging, and tests. +4. **Minimum viable test suite.** Rust `cargo test` runs at least one test for every security-sensitive function (path validation, name validation, shell-arg sanitization). Target: 20+ tests. +5. **CI gate.** Push and PR trigger a Windows GitHub Actions job that runs `tsc && vite build`, `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test`. Red CI blocks merge. +6. **Version + changelog.** Bump to v0.38.0 with a single CHANGELOG entry summarizing the production-grade work. + +## Non-Goals + +- **Cross-platform support.** Embedist is Windows-only by design (NSIS bundle, `winreg` dep, PTY uses ConPTY). CI is Windows-only. +- **E2E UI tests.** Tauri WebView E2E is high-cost and out of scope. UI is verified manually + via TypeScript strict mode. +- **Refactoring large modules.** `ai.rs` and `useAgent.ts` are large but functional. No structural refactor. +- **Adding features.** No new providers, no new editor capabilities, no new boards. + +## Scope (concrete work units) + +### A. TODO reconciliation +Read each "open" item in `TODO-fix.md` against current source. For each: +- If code already implements it: mark **FIXED**, cite the commit/file:line. +- If still open: keep open, refine the fix description. + +### B. Close genuinely-open items +Implement everything Low/Trivial that survives reconciliation. Anticipated, before reconciliation: + +| # | Item | Effort | Files | +|---|---|---|---| +| 3.2 | `defaultImplementationMode` toggle UI | Low | `settingsStore.ts`, `AISettings.tsx` | +| 3.4 | `PlanPhaseIndicator` implementation | Low | `PlanPhaseIndicator.tsx` | +| 3.5 | Token usage display | Low | `MessageBubble.tsx`, `aiStore.ts` (verify against current code first — may already be done) | +| 3.8 | Plan phase auto-detection | Medium | `AIChatPanel.tsx` — replace substring matching with explicit markers | +| 5.1 | `SerialConfig` dead code | Trivial | `serial.rs` — wire or delete | +| 6.6 | Bottom panel resize handle | Low | `BottomPanel.tsx` | +| 6.9 | Keyboard shortcuts help modal | Low | New `KeyboardShortcutsModal.tsx` + `MenuBar.tsx` | + +### C. `run_shell` security +- Audit `src-tauri/src/commands/filesystem.rs::run_shell` and every call site in `src/lib/agent-tools.ts`. +- Apply defense-in-depth: keep the existing metachar denylist **and** add a command allowlist (`pio`, `git`, `npm`, `cargo`, plus a small set the agent provably needs). +- Log every invocation via `log::info!` so the user can audit what the AI ran. +- Add `#[cfg(test)]` cases for the validator: blocks `;`, `&&`, `|`, `>`, `` ` ``, `$()`, and rejects non-allowlisted commands. + +### D. Rust test scaffolding +- Per-module `#[cfg(test)] mod tests` blocks. No new test-only binary. +- Cover at minimum: + - `filesystem.rs`: `is_path_safe`/equivalent, name validation, shell-arg validation. + - `serial.rs`: baud-rate parsing, line-ending handling. + - `platformio.rs`: `parseAnsiColor`-equivalent if any, board parsing from `platformio.ini`. + - `ai.rs`: provider URL builders, tool-call message-format helpers. +- Run with `cargo test --manifest-path src-tauri/Cargo.toml`. + +### E. CI +`.github/workflows/ci.yml`: +```yaml +on: [push, pull_request] +jobs: + windows: + runs-on: windows-latest + steps: + - checkout + - setup-node 24 + - setup rust stable + - npm ci + - npm run build # tsc + vite + - cargo fmt --check (manifest=src-tauri/Cargo.toml) + - cargo clippy -- -D warnings + - cargo test +``` +No release/publish job — keep tag-based release manual (current process). + +### F. Release +- Bump `package.json`, `Cargo.toml`, `tauri.conf.json` to `0.38.0`. +- Single CHANGELOG entry under `## v0.38.0 — Production-grade hardening`. +- Do NOT publish a GitHub release; leave that to the human. + +## Architecture impact + +Minimal. All changes are additive or scoped to single files. No new modules at the top level. The only new top-level artifact is `.github/workflows/ci.yml`. + +The most invasive change is the `run_shell` allowlist — it may reject calls the agent previously made. Mitigation: log the rejection clearly and surface to the user in the agent activity log, not silent failure. + +## Verification + +After all work is done: +1. `npm run build` exits 0. +2. `cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings` exits 0. +3. `cargo test --manifest-path src-tauri/Cargo.toml` exits 0 with >= 20 tests passing. +4. `npm run tauri dev` launches; manually verify: open a folder, open a file in Monaco, see token-usage display, open keyboard-shortcuts modal, drag bottom panel. +5. `TODO-fix.md` "open" count matches what is actually open (likely 0 after this work, or only "ONGOING" items like 8.5). + +## Risks + +- **`run_shell` allowlist breaks agent flows.** Mitigation: derive the allowlist from existing call sites in `agent-tools.ts`, not from a guess. +- **TODO 3.8 phase-marker change requires AI prompt updates.** Mitigation: keep substring fallback for one release; add explicit markers as the preferred path. +- **CI on `windows-latest` is slow (~10-15 min).** Acceptable for a Windows-only project — cross-platform CI would be the wrong trade-off. + +## Out of scope follow-ups (not blocking this milestone) + +- E2E tests via Playwright + Tauri driver. +- Code signing for Windows (removes SmartScreen warning). +- Linux/macOS support. +- Telemetry / crash reporting. From e91dab6bc217a8cd6c88b905fde00e8315a1af81 Mon Sep 17 00:00:00 2001 From: mandarwagh9 Date: Thu, 28 May 2026 19:41:14 +0530 Subject: [PATCH 6/6] chore(release): bump version to v0.38.0 Synchronize package.json, src-tauri/Cargo.toml, src-tauri/tauri.conf.json (plus the resulting Cargo.lock update) and add the v0.38.0 CHANGELOG entry summarizing the production-grade hardening landed on this branch: - Security: run_shell command allowlist + audit logging - Fixed: anchored plan-phase detection with explicit [[phase: x]] markers - Added: cargo test suite (14 tests), GitHub Actions CI, persist schema versioning - Documentation: reconciled TODO-fix, design spec under docs/superpowers/ Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f2e3d..9a1b366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [v0.38.0](https://github.com/mandarwagh9/embedist/releases/tag/v0.38.0) — 2026-05-28 + +### Security +- `run_shell` now enforces a command allowlist on top of the existing metacharacter denylist. Only embedded-development tooling (`pio`, `git`, `npm`, `cargo`, `python`, `make`, `cmake`, plus a handful of read-only utilities) can be invoked through the agent; everything else is rejected with a clear error. Every invocation is logged via `log::info!` so the user can audit what the AI ran. + +### Fixed +- Plan-phase auto-detection (`AIChatPanel.tsx`) no longer triggers on bare substrings like `approve` or a stray `?` anywhere in the response. Phase transitions now prefer explicit `[[phase: ready]]` / `[[phase: clarify]]` markers emitted by the plan-mode system prompt, with a tightened phrase fallback anchored to the last 400 chars (e.g. "plan ready for approval", "awaiting clarification"). Eliminates a class of false-positive mode transitions. + +### Added +- **Test suite.** Rust commands now ship with `#[cfg(test)]` coverage: 14 unit tests in `filesystem.rs` exercise the shell allowlist (allowed/denied/empty/metachar paths), program-name extraction (with path prefixes and Windows `.exe`/`.cmd` suffixes), leaf-name validation against directory-traversal, lexical path normalization, and `validate_path` containment against `..` escapes. Run with `cargo test --manifest-path src-tauri/Cargo.toml`. +- **CI.** GitHub Actions workflow (`.github/workflows/ci.yml`) runs `npm run build`, `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test --all-targets` on every push and pull request, gated to `windows-latest` (the only supported target). +- **Persisted settings schema versioning.** `settingsStore` now declares `version: 1` and a `migrate` callback in its zustand `persist` config, providing a hook for non-breaking schema evolution in future releases. + +### Documentation +- TODO-fix.md reconciled against current source. Of the 9 items previously listed as open, 6 had already been resolved in shipped releases (`defaultImplementationMode` toggle, `PlanPhaseIndicator`, token-usage display, `SerialConfig`, bottom panel resize, keyboard-shortcuts modal). The doc now reflects reality. +- `docs/superpowers/specs/2026-05-28-production-grade-design.md` captures the scope, goals, and verification criteria for this release. + +--- + ## [v0.37.0](https://github.com/mandarwagh9/embedist/releases/tag/v0.37.0) — 2026-04-19 ### Fixed diff --git a/package.json b/package.json index 3204ebc..b348d81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "embedist", - "version": "0.37.0", + "version": "0.38.0", "description": "AI-native embedded development environment", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b6f4bc0..7a449ca 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -813,7 +813,7 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "embedist" -version = "0.37.0" +version = "0.38.0" dependencies = [ "dirs", "env_logger", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a109bc7..df57f1b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "embedist" -version = "0.37.0" +version = "0.38.0" description = "AI-native embedded development environment" authors = ["Embedist Team"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 58e3a41..13aff90 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Embedist", - "version": "0.37.0", + "version": "0.38.0", "identifier": "com.embedist.embedist", "build": { "beforeDevCommand": "npm run dev",