--- description: "Moltis engineering guide for Claude/Codex agents: Rust architecture, testing, security, and release workflows" alwaysApply: true --- # CLAUDE.md Rust version of openclaw ([docs](https://docs.openclaw.ai), [code](https://github.com/openclaw/openclaw)). All code must have tests with high coverage. Always check for security. ## Cargo Features Enable new feature flags **by default** in `crates/cli/Cargo.toml` (opt-out, not opt-in): ```toml [features] default = ["foo", ...] foo = ["moltis-gateway/foo"] ``` ## Workspace Dependencies Add new crates to `[workspace.dependencies]` in root `Cargo.toml`, reference with `{ workspace = true }`. Never add versions directly in crate `Cargo.toml`. Use latest stable crates.io version. ## Config Schema and Validation When adding/renaming fields in `MoltisConfig` (`crates/config/src/schema.rs`), also update `build_schema_map()` in `crates/config/src/validate.rs`. New enum variants for string-typed fields need updates in `check_semantic_warnings()`. ## Rust Style and Idioms - **File size limit: 1,500 lines.** CI enforces this via `scripts/check-file-size.sh`. Split large files into modules by domain. Existing oversize files are allowlisted for incremental decomposition. - Do not add implementation code to `mod.rs` or `lib.rs`. Keep those files for module wiring, exports, and crate setup, move real logic into dedicated sibling modules. - Use traits for behaviour boundaries. Prefer generics for hot paths, `dyn Trait` for heterogeneous/runtime dispatch. - Derive `Default` when all fields have sensible defaults. - Use concrete types (`struct`/`enum`) over `serde_json::Value` wherever shape is known. - **Match on types, never strings.** Only convert to strings at serialization/display boundaries. - Prefer `From`/`Into`/`TryFrom`/`TryInto` over manual conversions. Ask before adding manual conversion paths. - **DRY cross-crate types:** When two crates need the same enum/struct, define it once in the lower-level crate and re-export via `pub type Alias = other_crate::Type` from the higher-level one. Never duplicate enums across crates or round-trip through strings (`parse(&id.to_string())`) to convert between mirror types. - Prefer streaming over non-streaming API calls. - Run independent async work concurrently (`tokio::join!`, `futures::join_all`). - Never use `block_on` inside async context. - **Forbidden:** `Mutex<()>` / `Arc>` — mutex must guard actual state. - Use `anyhow::Result` for app errors, `thiserror` for library errors. Propagate with `?`. - **Never `.unwrap()`/`.expect()` in production.** Workspace lints deny these. Use `?`, `ok_or_else`, `unwrap_or_default`, `unwrap_or_else(|e| e.into_inner())` for locks. - Use `time` crate (workspace dep) for date/time — no manual epoch math or magic constants like `86400`. - Prefer `chrono` only if already imported in the crate; default to `time` for new code. - Prefer crates over subprocesses (`std::process::Command`). Use subprocesses only when no mature crate exists. - Prefer guard clauses (early returns) over nested `if` blocks. - Prefer iterators/combinators over manual loops. Use `Cow<'_, str>` when allocation is conditional. - Keep public API surfaces small. Use `#[must_use]` where return values matter. ### Tracing and Metrics All crates must have `tracing` and `metrics` features, gated with `#[cfg(feature = "...")]`. Use `tracing::instrument` on async functions. Record metrics at key points (counts, durations, errors). See `docs/metrics-and-tracing.md`. ## Build Commands ```bash cargo build # Debug build cargo build --release # Release build cargo run / cargo run --release ``` ## Web UI (TypeScript + Preact + Vite) TypeScript/TSX source in `crates/web/ui/src/`, built with Vite to `crates/web/src/assets/dist/`. CSS and static assets in `crates/web/src/assets/`. Release mode embeds via `include_dir!`. Generated assets (`dist/`, `css/style.css`, `style.css`, `sw.js`) are gitignored. Run `just build-web-assets` to generate them (requires Node.js). A `build.rs` check warns (debug) or fails (release/embedded-assets) if they are missing. See `docs/src/frontend.md` for the full architecture guide. ### Build Commands ```bash cd crates/web/ui npm run build # Vite: TS/TSX → dist/ npm run build:css # Tailwind: input.css → ../src/assets/css/style.css npm run build:sw # esbuild: src/sw.ts → ../src/assets/sw.js npm run build:all # All three above npm run dev # Vite watch mode (rebuilds on save) npx tsc --noEmit # Type check (strict, must be 0 errors) ``` **After changing TS/TSX files**, always: 1. `biome check --write crates/web/ui/src/` 2. `cd crates/web/ui && npm run build` 3. `cd crates/web/ui && npx tsc --noEmit` ### TypeScript Rules - **File size limit: 1,500 lines** (same rule as Rust). Split large files into modules by domain. - Pages: extract sections/modals into `pages/sections/`, `pages/channels/`, `pages/chat/`, etc. - Utilities: extract sub-modules into sibling directories (`providers/`, `sessions/`, `ws/`). - Keep shared signals, types, and re-exports in the main file; move logic into sub-modules. - All UI code is **TypeScript** with **JSX** (Preact). No HTM tagged templates. - Add typed Props interfaces for all Preact components. - Use `@preact/signals` with generic type parameters: `signal([])`. - Prefer typed interfaces over `Record` — define concrete shapes where property access is known. - Use `targetValue(e)` / `targetChecked(e)` from `typed-events.ts` for form event handlers. - No `any` types — use `unknown` with type guards or specific interfaces. - Use shared components from `components/forms/` (TextField, SaveButton, ListItem, Badge, TabBar, etc.). ### CSS Rules - **Always use Tailwind classes** instead of inline `style="..."`. - Reuse CSS classes from `components.css`: `provider-btn`, `provider-btn-secondary`, `provider-btn-danger`. - Match button heights/text sizes when elements sit together. - **Rebuild Tailwind** after adding new classes: `cd crates/web/ui && npm run build:css`. ### Adding Settings Nav Icons Settings sidebar icons use `::before` pseudo-elements in `components.css`, **not** the `icon` JSX property in the `sections` array. When adding a new settings section: 1. Create the SVG mask in `crates/web/src/assets/icons/masks/` with `fill="black"` (not `currentColor`) 2. Add `.icon-` class in `crates/web/ui/input.css` under the mask-image icons section 3. **Also add** `.settings-nav-item[data-section=""]::before` in `crates/web/src/assets/css/components.css` pointing to the SVG — without this the icon renders as a black square 4. The `icon: ` in `SettingsPage.tsx` is a fallback only ### E2E Test Shims E2E tests dynamically import individual JS modules (`js/state.js`, `js/helpers.js`, etc.). With Vite bundling, these don't exist as standalone files. Shim files in `src/assets/js/` proxy to `window.__moltis_modules` (populated by `app.tsx`). When adding new modules that tests import, add a shim file and expose the module in `app.tsx`. ### Selection Cards Use clickable cards (`.model-card`, `.backend-card` in `input.css`) instead of dropdowns for option selection. States: `.selected`, `.disabled`, default. Badges: `.recommended-badge`, `.tier-badge`. ### Provider Config Storage Provider keys in `~/.config/moltis/provider_keys.json` via `KeyStore` in `provider_setup.rs`. When adding fields, update: `ProviderConfig` struct, `available()` response, `save_key()`. ### Server-Injected Data (gon pattern) For server data needed at page load: add to `GonData` in `server.rs` / `build_gon_data()`. TS side: `import * as gon from "./gon"` — use `gon.get()`, `gon.onChange()`, `gon.refresh()`. Types in `crates/web/ui/src/types/gon.ts` mirror the Rust `GonData` struct. Never inject inline `