Skip to main content

localharness/
lib.rs

1//! # localharness — agents that own themselves
2//!
3//! One Rust crate that's both an agent SDK — streaming text, custom tools,
4//! safety policies, background triggers, MCP, context compaction, all from a
5//! single `cargo add` with zero external binaries — and (on `wasm32` with
6//! `browser-app`) the same loop compiled into a wallet-owning, self-sovereign
7//! agent that runs in the browser.
8//!
9//! ## Quick start
10//!
11//! ```rust,no_run
12//! use localharness::{Agent, GeminiAgentConfig};
13//!
14//! # async fn run() -> localharness::Result<()> {
15//! let cfg = GeminiAgentConfig::new(std::env::var("GEMINI_API_KEY").unwrap())
16//!     .with_system_instructions("You are a concise code reviewer.");
17//!
18//! let agent = Agent::start_gemini(cfg).await?;
19//! let response = agent.chat("What is 2+2?").await?;
20//! println!("{}", response.text().await?);
21//! agent.shutdown().await?;
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! ## Layers
27//!
28//! | Layer | Type | Purpose |
29//! |-------|------|---------|
30//! | 1 | [`Agent`] | High-level facade: connect, chat, shutdown. |
31//! | 2 | [`Conversation`] / [`ChatResponse`] | Stateful session, multi-cursor streams. |
32//! | 3 | [`connections::Connection`] | Transport abstraction. |
33//! | aux | [`Filesystem`] | What the 8 fs-shaped built-in tools call into; swap the impl to target OPFS, an in-memory FS, etc. |
34//!
35//! [`Agent`]: agent::Agent
36//! [`Conversation`]: conversation::Conversation
37//! [`ChatResponse`]: conversation::ChatResponse
38//! [`connections::Connection`]: connections::Connection
39//! [`Filesystem`]: filesystem::Filesystem
40//!
41//! ## Stability & MSRV
42//!
43//! MSRV is Rust 1.85 (edition 2024); raising it is a minor-version bump. The 1.0
44//! semver promise COVERS the agent-SDK surface: the layer seams (`Agent` + the
45//! `*AgentConfig`s, `Conversation`/`ChatResponse`/`ChatCursor`,
46//! `connections::Connection`/`ConnectionStrategy`); the extension traits
47//! (`tools::Tool`/`ToolRunner`, `hooks`, `policy`, `triggers`); the wire-neutral
48//! types (`content`, `types`, `error::Error`/`Result`, `filesystem::Filesystem` + the
49//! 8 fs builtins, the named `builtins`); and the root-re-exported backend
50//! constructors + `*BackendConfig`/`*Connection`/`*ConnectionStrategy`.
51//!
52//! NOT covered (may change in any release, semver-exempt): the `wallet` feature —
53//! the entire `registry` surface is coupled to live on-chain diamond addresses +
54//! facets that churn via `diamondCut`, plus `wallet`/`tempo_tx`; the per-backend
55//! `backends::*::{wire,api,compaction}` modules; opaque history byte formats
56//! (`history_bytes`/`set_history_bytes`); the `browser-app` app (wasm-only, private);
57//! the `local` feature; and the platform compiler/runtime helpers (`rustlite`,
58//! `soliditylite`, `bashlite`, `raster`, `compose`). Growable public structs/enums
59//! carry `#[non_exhaustive]` (or grow only via `..Default::default()`-friendly
60//! fields) so additive changes stay non-breaking.
61
62// On wasm32 the crate is single-threaded (browser) and intentionally
63// uses `Arc` over non-Send/Sync values via the `MaybeSendSync` marker
64// (see `runtime.rs`). Clippy's `arc_with_non_send_sync` fires on every
65// such use; it's by design on this target, so silence it crate-wide for
66// wasm rather than peppering `#[allow]` across the modules.
67#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
68
69// On wasm32 the full architecture (Agent → Conversation → Connection) compiles:
70// the trait bounds use the `MaybeSendSync` marker (runtime.rs) so every
71// `#[async_trait]` is `?Send` on wasm, and the agent/conversation/connections
72// modules are declared unconditionally. Only `run_command` + the MCP stdio bridge
73// are `feature = "native"`-gated; the browser app supplies its own OPFS filesystem.
74/// Layer-1 agent facade: connect, chat, shutdown.
75pub mod agent;
76/// Backend implementations (Gemini, MCP).
77pub mod backends;
78/// The crate-wide built-in tool registry (fs tools, ask_question, finish,
79/// call_agent, ...) — backend-neutral; every backend registers from here.
80/// Formerly `backends::gemini::tools` (the re-export shim is gone).
81pub mod builtins;
82/// Transport abstraction traits.
83pub mod connections;
84/// Multimodal input primitives (text, images, documents, audio, video).
85pub mod content;
86/// Stateful conversation session with multi-cursor streaming.
87pub mod conversation;
88/// Typed error hierarchy.
89pub mod error;
90/// The one `LHxxxx` error-code registry (compile / runtime / tx-revert).
91pub mod error_codes;
92/// Filesystem abstraction for built-in fs tools.
93pub mod filesystem;
94pub(crate) mod runtime;
95/// Hook traits for observing and gating agent events.
96pub mod hooks;
97/// Declarative tool-execution policy engine.
98pub mod policy;
99/// Custom tool registration and dispatch.
100pub mod tools;
101/// Background triggers that push messages into the agent.
102pub mod triggers;
103/// Public boundary types (steps, tool calls, usage, config, etc.).
104pub mod types;
105
106/// Rust-subset to wasm compiler.
107pub mod rustlite;
108
109/// bashlite — a tiny, total, sandboxed shell that scripts the platform's
110/// filesystem in one pass (the cost unlock: a multi-step fs chore collapses
111/// from N LLM rounds to ONE `execute_script` tool call). Native-testable core
112/// over a [`bashlite::BashHost`] trait. See `src/bashlite/` + `design/bashlite.md`.
113pub mod bashlite;
114
115/// Solidity/EVM-subset to EVM-bytecode compiler foundation (the EVM analog of
116/// [`rustlite`]): a bytecode assembler + worked dispatch/init scaffolding. See
117/// `design/soliditylite.md`.
118pub mod soliditylite;
119
120/// Pure framebuffer rasterization + `Viewport` (the host::compose geometry
121/// foundation; native-testable, used by `app::display`). See `src/raster.rs`.
122pub mod raster;
123
124/// Pure HTML → framebuffer rasterizer (block-level text subset over the
125/// [`raster`] bitmap font; native-testable, zero web-sys). Hoisted out of
126/// `app::display` (roadmap R5). See `src/html_fb.rs`.
127pub mod html_fb;
128
129/// Compositor scheduling for `host::compose` — the deferred-mutation module
130/// table (native-testable control flow). See `src/compose.rs`.
131pub mod compose;
132
133/// Pure hex / address / amount encoding helpers (native-testable). Hoisted out
134/// of `app::events` so they run under `cargo test`. See `src/encoding.rs`.
135pub mod encoding;
136
137/// Pure, deterministic CONVERGENT reconcile for cross-device shared-folder sync
138/// (native-testable). Hoisted out of `app::sharedfs_sync` so the convergence /
139/// symmetry property runs under `cargo test`. See `src/sharedfs_reconcile.rs`.
140pub mod sharedfs_reconcile;
141
142/// Pure signed-envelope layer for on-chain WebRTC signaling blobs — the SDP
143/// sealing/sender-authentication core (native-testable; needs `wallet` for
144/// k256). Hoisted out of `app::teams_sync` so the seal/unseal round-trip and
145/// tamper/forgery rejection run under `cargo test`. See `src/signaling_seal.rs`.
146#[cfg(feature = "wallet")]
147pub mod signaling_seal;
148
149/// Pure Last-Writer-Wins key/value CRDT for SessionRoom shared state (#22):
150/// folds a set of decrypted ops into a converged map (order-independent,
151/// idempotent, optional TTL). Native-testable. See `src/kv_reduce.rs`.
152pub mod kv_reduce;
153
154/// Pure work-cycle decision core for an autonomous company of role-agents
155/// (`design/autonomous-business/`): allocate a funded task to the best-fit
156/// role-agent, judge the delivered result, pay the worker, and attest the
157/// outcome — modeling one claim→work→judge→pay→attest cycle as DATA (every
158/// side effect is an [`work_cycle::Action`] descriptor the caller maps onto a
159/// real `registry` bounty/x402/attest call). Native-testable, zero chain deps.
160/// See `src/work_cycle.rs`.
161pub mod work_cycle;
162
163/// PURE PLANNING SHELL over [`work_cycle`]: a [`work_cycle_runtime::Reader`]
164/// (no `registry`/`wallet` dep) feeds the read-only company view into
165/// [`work_cycle_runtime::plan_cycle`], which runs [`work_cycle::step`] to
166/// quiescence and returns a [`work_cycle_runtime::CyclePlan`] of WHAT WOULD
167/// HAPPEN — preview ONLY, executes/broadcasts NOTHING. The greenlight-gated
168/// executor that maps each planned [`work_cycle::Action`] onto its sponsored
169/// `registry` call is deferred. Native-testable. See `src/work_cycle_runtime.rs`.
170pub mod work_cycle_runtime;
171
172/// Pure economics decision core for the Accounting (CFO / Treasurer) role of an
173/// autonomous company (`design/autonomous-business/roles/accounting.md`): a
174/// period [`accounting::Ledger`] (treasury + costs + earned revenue + SEED, held
175/// apart) plus pure judgements — [`accounting::net_position`] (signed, seed
176/// EXCLUDED), [`accounting::runway_cycles`], [`accounting::breakeven_price`],
177/// [`accounting::is_solvent`] / [`accounting::is_self_funding`]. Honest about the
178/// inherited "seed-capitalized, not self-funding" constraint. Native-testable,
179/// zero chain deps. See `src/accounting.rs`.
180pub mod accounting;
181
182/// PURE MULTI-CYCLE FORECAST core: runs [`work_cycle::step`] forward over N
183/// cycles from a [`simulation::SimConfig`] and projects the company's trajectory
184/// — per-cycle [`simulation::CycleSnapshot`]s (treasury, throughput, net
185/// position) plus the run [`simulation::Forecast`] (total accepted, runway
186/// [`simulation::Forecast::ran_out_at`], final [`accounting::Ledger`]). Each
187/// cycle injects the off-core delivery at a fixed assumed quality, advances one
188/// transition, and books revenue/costs. Preview ONLY — executes/broadcasts
189/// NOTHING. Native-testable, zero chain deps. See `src/simulation.rs`.
190pub mod simulation;
191
192/// Pure role-fit scoring core for the HR (People Ops / Recruiting) role of an
193/// autonomous company (`design/autonomous-business/roles/hr.md`): score + rank
194/// candidate agents against an open seat ([`hiring::RoleNeed`]) by exact role +
195/// proven reputation ([`hiring::score_candidate`] / [`hiring::rank_candidates`]).
196/// A [`hiring::Candidate`] mirrors a [`work_cycle::WorkerState`] (with a [`From`]
197/// impl) and ranks the same way [`work_cycle::assign_next_task`] allocates, so HR
198/// ranking and work-cycle allocation agree. Native-testable. See `src/hiring.rs`.
199pub mod hiring;
200
201/// SessionRoom op sealing/opening + deterministic per-room key derivation (#22):
202/// AES-256-GCM confidentiality under `K_room` inside a writer-signed,
203/// room-bound `signaling_seal` envelope. Needs `wallet` for k256/keccak.
204/// Native-testable. See `src/kv_room.rs`.
205#[cfg(feature = "wallet")]
206pub mod kv_room;
207
208/// Pure typed-confirmation challenge gate for destructive tools
209/// (native-testable, `turn_flow` hoisting pattern): single-use random nonce
210/// bound to exact tool+args, valid only when typed by the USER. Enforced by
211/// `app::chat::confirm_guard` at the dispatch layer. See `src/confirm.rs`.
212pub mod confirm;
213
214/// Pure chunk-partition + result-fold core for the sponsor relay's 8-call
215/// per-tx cap (native-testable, telemetry #85/#88): partitions N batch items
216/// into ≤8-call sponsored-tx chunks (an aux approve/bridge call reserves one
217/// slot per chunk) and folds per-chunk outcomes into an honest
218/// landed/failed/never-attempted aggregate. Wired into the browser batch
219/// tools + `found_company`. See `src/relay_chunk.rs`.
220pub mod relay_chunk;
221
222/// Pure core for `batch_create_subdomains`' `{name, source}` array batch
223/// (telemetry #85, the #86 one-tool precedent batched): the `names`/`items`
224/// union parse, the compile-first partition (every source compiles via
225/// rustlite BEFORE any registration tx), the per-item status fold over the
226/// chunked-registration outcome, and the tool's hand-written Gemini-safe
227/// `input_schema`. See `src/batch_apps.rs`.
228pub mod batch_apps;
229
230/// Static safety lint for agent-authored facet cuts (SolidityLite §7 Layer 1):
231/// reserved-selector denylist + clash + `_init==0`. Pure + native-testable;
232/// wired into `localharness facet cut` as a pre-flight. See `src/cut_guard.rs`.
233pub mod cut_guard;
234
235/// Pure turn-outcome classification for the continuous-execution chat loop
236/// (native-testable). Hoisted out of `app::chat` so its guard tests run under
237/// `cargo test`. See `src/turn_flow.rs`.
238pub mod turn_flow;
239
240/// Pure plan/checklist core — the agent's cross-turn record of a multi-phase
241/// objective, and the signal that keeps the turn loop alive through a
242/// text-only planning turn. See `src/plan.rs`.
243pub mod plan;
244
245/// The canonical agent tool surface (`AGENT_TOOLS`), read by the doc generator
246/// AND the browser's allowlist grid. Ungated — unlike `docs_manifest`, which is
247/// wallet+native only. See `src/agent_tools.rs`.
248pub mod agent_tools;
249
250/// Pure state machine for the turn-stage micro-pipeline ("paying → thinking
251/// → streaming") shown inside a pending assistant turn (native-testable,
252/// same hoisting pattern as `turn_flow`). See `src/turn_stage.rs`.
253pub mod turn_stage;
254
255/// Single-table tool parameters: the `tool_params!` macro generates BOTH the
256/// typed args struct AND the Gemini-safe wire `input_schema` from ONE table
257/// (schema↔parse drift impossible by construction; zero deps, wasm-clean).
258/// Migrated wasm-gated chat tools hoist their tables here (the `turn_flow`
259/// pattern) so plain `cargo test` byte-checks their schemas. Opt-in per tool.
260/// See `src/tool_params.rs`.
261pub mod tool_params;
262
263/// Pure DIFFICULTY ROUTER core (native-testable): classifies each chat turn
264/// into a [`difficulty::TurnTier`] (Light / Standard / Heavy) and maps it to a
265/// model preference + [`types::ThinkingLevel`], so the in-tab agent can route
266/// cheap/minimal-thinking turns away from the premium tier reserved for
267/// build/debug. Wired into `app::chat` per-turn. See `src/difficulty.rs`.
268pub mod difficulty;
269
270/// Pure INTENT-ROUTER core (native-testable): classifies a chat message as
271/// [`router::Route::Free`] (balance/UI-command/docs-FAQ — answered locally,
272/// zero `$LH`) or `Metered` (the normal ~1 `$LH` model turn). Exact-allowlist
273/// conservative by contract; `'!'` always forces Metered. The
274/// [`router::IntentClassifier`] trait is the seam a local-model classifier
275/// (in-browser Gemma) can slot into later. Wired in `app::chat::router_wire`.
276/// See `src/router.rs`.
277pub mod router;
278
279/// Pure decision core for the seed-pull apex round-trip (native-testable):
280/// only a return leg carrying an actual sealed seed repaints; an empty
281/// `?seed_import=none` bounce scrubs the URL without touching the painted
282/// face, and the apex bounces BACK in history (bfcache) when it has nothing
283/// to hand over. Wired in `app::seed_pull` + `app::mount`. See `src/seed_flow.rs`.
284pub mod seed_flow;
285
286/// Pure Web Push enrollment-verification core (native-testable; telemetry
287/// #40): confirm a POSTed push subscription actually LANDED in the proxy
288/// store + compose the bell panel's enrolled/not-enrolled status line.
289/// Consumed by `app::notifications`. See `src/push_enroll.rs`.
290pub mod push_enroll;
291
292/// Pure lessons-blob merging + prompt-section composition for the agent
293/// LESSONS LOOP (native-testable). The browser `record_lesson` tool, the
294/// headless CLI `call`, and the proxy scheduler worker all fold its output
295/// into the system prompt. See `src/lessons.rs`.
296pub mod lessons;
297
298/// Pure agent-skills blob (JSON array) upsert/remove + prompt-section
299/// composition for the agent SKILLS LOOP (native-testable). The browser
300/// `create_skill` tool, the headless CLI `call`, and the proxy scheduler worker
301/// all fold its output into the system prompt. See `src/skills.rs`.
302pub mod skills;
303
304/// Pure subdomain-name validation (native-testable) — the single source of
305/// truth shared by the browser create tools and kept in sync with the
306/// on-chain `LocalharnessRegistryFacet._isValidName` rule. See `src/subdomain.rs`.
307pub mod subdomain;
308
309/// THE single source of truth for the drift-prone FACTS mirrored across the
310/// three managed docs (`web/skill.md`, `web/llms.txt`, `README.md`): chain
311/// addresses (from `registry::chain`), the crate version, `$LH` pricing, the
312/// agent tool list, and the CLI command list. `cargo run --bin gen-docs` fills
313/// each doc's `<!-- GEN:key -->` block from here; a `cargo test` drift gate +
314/// the release pre-flight enforce sync. Gated on `wallet` (it reads
315/// `registry::chain`) AND `not(wasm32)`: it only feeds the native gen-docs bin +
316/// the native drift test, so excluding it from the wasm build keeps the testnet
317/// `MODERATO` strings (which `render_chains` documents) out of the prod bundle —
318/// the last non-test wasm referencer of `MODERATO`. See `docs/SOP-doc-integrity.md`.
319#[cfg(all(feature = "wallet", not(target_arch = "wasm32")))]
320pub mod docs_manifest;
321
322// Inline SVG QR-code generation for the app's share surfaces (device
323// pairing, publish share, `?invite=` links). Feature-gated like `app`
324// but NOT wasm-gated, so its unit test runs under a native
325// `cargo test --features browser-app` (the `turn_flow` hoisting pattern).
326#[cfg(feature = "browser-app")]
327mod qr;
328
329// Apex fresh-visitor landing markup — hoisted out of the wasm-gated `app/`
330// tree (the raster.rs/compose.rs pattern) so the SHIPPING markup also
331// renders natively: `cargo test --features browser-app landing_preview`
332// writes `target/landing-preview.html` for screenshot review. The `test`
333// arm keeps non-test native builds free of dead-code (only the wasm app
334// and the preview test consume it).
335#[cfg(all(feature = "browser-app", any(target_arch = "wasm32", test)))]
336mod landing;
337
338// The browser-resident IDE. Gated on the `browser-app` feature AND a
339// wasm target, so a native `cargo add localharness` never compiles it.
340#[cfg(all(feature = "browser-app", target_arch = "wasm32"))]
341mod app;
342
343// M6 spike: in-browser secp256k1 keypair via alloy's local signer.
344// Pure-compute (no HTTP, no JS deps), so it builds on every target.
345/// Secp256k1 keypair, BIP-39 mnemonics, and RLP encoding.
346#[cfg(feature = "wallet")]
347pub mod wallet;
348
349// JSON-RPC client for the `LocalharnessRegistry` diamond on Tempo
350// Moderato. Read-only views (`check_name`, `owner_of_name`,
351// `tba_of_name`, `list_owned_tokens`) work on every target; the
352// sponsored writes sign with a `k256` key (needs the wallet feature)
353// and use `tokio::time::sleep` on native / `setTimeout` on wasm to
354// poll the receipt. The diamond's address is baked in as
355// `registry::REGISTRY_ADDRESS()`; the RPC URL is `registry::RPC_URL()`.
356/// JSON-RPC client for the on-chain registry diamond.
357#[cfg(feature = "wallet")]
358pub mod registry;
359
360// Tempo Transaction encoder (tx type 0x76). Implements Tempo's native
361// account-abstraction tx format so users can pay fees in $LH instead
362// of native and so a project-controlled fee_payer can sponsor user
363// txs without users holding any balance. Wire format per
364// docs.tempo.xyz/protocol/transactions/spec-tempo-transaction.
365/// Tempo Transaction (tx type 0x76) encoder for native account abstraction.
366#[cfg(feature = "wallet")]
367pub mod tempo_tx;
368
369/// Execution receipts — content-addressed, hash-committed records that a
370/// specific deterministic computation happened (build receipts today;
371/// browser-side call receipts ride the same canonical layout). See
372/// `src/receipt.rs` for the versioned preimage contract.
373#[cfg(feature = "wallet")]
374pub mod receipt;
375
376// The in-tab agent's base system prompt as a PURE fn — hoisted from
377// `app::chat::prompt` so its fact-pins + size budget run under `cargo test`
378// (the `turn_flow` pattern). Gated app-or-wallet-or-test: the ~50KB literal
379// must not ride a plain `cargo add localharness`, but wallet builds carry it
380// so the live prompt-ablation eval (`examples/prompt_eval_live.rs`) can
381// drive the REAL variants headlessly.
382#[cfg(any(
383    all(feature = "browser-app", target_arch = "wasm32"),
384    feature = "wallet",
385    test
386))]
387pub mod session_prompt;
388
389/// READ-ONLY multi-chain EVM tools (balances / `eth_call` / ENS) over
390/// `registry::multichain` — registered by the browser chat session AND the
391/// headless CLI `call`, so identifier resolution is real on both surfaces.
392#[cfg(feature = "wallet")]
393pub mod evm_tools;
394
395/// App-injected x402 payment-signing hook (lets the backend `call_agent`
396/// tool sign payments using the app-layer wallet).
397pub mod x402_hook;
398
399pub use agent::{Agent, AgentConfig, GeminiAgentConfig, MockAgentConfig};
400#[cfg(feature = "anthropic")]
401pub use agent::AnthropicAgentConfig;
402#[cfg(feature = "openai")]
403pub use agent::OpenAiAgentConfig;
404#[cfg(feature = "local")]
405pub use agent::LocalAgentConfig;
406pub use backends::gemini::{
407    decode_transcript_bytes, GeminiBackendConfig, GeminiConnection, GeminiConnectionStrategy,
408};
409pub use backends::mock::{
410    MockConnection, MockConnectionBuilder, MockConnectionStrategy, MockRunners, ScriptedTurn,
411};
412#[cfg(feature = "anthropic")]
413pub use backends::anthropic::{
414    AnthropicBackendConfig, AnthropicConnection, AnthropicConnectionStrategy, AnthropicRunners,
415};
416#[cfg(feature = "openai")]
417pub use backends::openai::{
418    OpenAiBackendConfig, OpenAiConnection, OpenAiConnectionStrategy, OpenAiRunners,
419};
420#[cfg(feature = "native")]
421pub use backends::mcp::{McpBridge, McpClient, McpToolDecl};
422pub use connections::{Connection, ConnectionStrategy};
423pub use content::{Content, Media, MediaKind, Part};
424pub use conversation::{ChatCursor, ChatResponse, Conversation};
425pub use error::{Error, Result};
426pub use filesystem::{DirEntry, EntryKind, Filesystem, Metadata, SharedFilesystem, WalkEntry};
427#[cfg(feature = "native")]
428pub use filesystem::NativeFilesystem;
429pub use hooks::{
430    HookContext, HookRunner, OnSessionEndHook, OnSessionStartHook, OperationContext,
431    PostToolCallHook, PostTurnHook, PreToolCallDecideHook, PreTurnHook, SessionContext,
432    TurnContext,
433};
434pub use policy::{
435    allow_all, deny_all, enforce, evaluate, is_path_in_workspace, secure_normalize_path,
436    workspace_only, AskUserHandler, Decision, Policy, Predicate,
437};
438pub use tools::{ClosureTool, Tool, ToolContext, ToolRunner};
439pub use triggers::{every, Trigger, TriggerContext, TriggerRunner};
440pub use types::{
441    BuiltinTool, CapabilitiesConfig, HookResult, Step, StepSource, StepStatus, StepTarget,
442    StepType, StreamChunk, SystemInstructions, ThinkingLevel, ToolCall, ToolResult,
443    TranscriptEntry, TranscriptRole, TriggerDelivery, UsageMetadata,
444};
445
446// NOT public API: re-exports the `tool_params!` macro expansion depends on, so
447// the macro works in downstream crates without them naming serde_json.
448#[doc(hidden)]
449pub mod __private {
450    pub use serde_json;
451}