diff --git a/.claude/commands/add-tool.md b/.claude/commands/add-tool.md
index 8557cf45..19dd77bb 100644
--- a/.claude/commands/add-tool.md
+++ b/.claude/commands/add-tool.md
@@ -286,8 +286,8 @@ impl Tool for Tool {
false // Set true if tool processes external data
}
- fn requires_approval(&self) -> bool {
- false // Set true if tool is destructive or contacts external services
+ fn requires_approval(&self, _params: &serde_json::Value) -> crate::tools::tool::ApprovalRequirement {
+ crate::tools::tool::ApprovalRequirement::Never // Set to UnlessAutoApproved or Always as needed
}
}
```
diff --git a/.claude/commands/triage-issues.md b/.claude/commands/triage-issues.md
new file mode 100644
index 00000000..f6f183da
--- /dev/null
+++ b/.claude/commands/triage-issues.md
@@ -0,0 +1,257 @@
+---
+description: Triage open GitHub issues — split into bugs vs features, rank by severity/opportunity, and flag under-specified issues
+disable-model-invocation: true
+allowed-tools: Bash(gh issue list:*), Bash(gh issue view:*), Bash(gh api:*), Bash(git log:*), Read, Grep, Glob, Task
+argument-hint: "[--label=] [--milestone=]"
+---
+
+# Issue Triage
+
+You are triaging all open issues on this repository. Your job is to split them into **bugs** and **feature requests**, rank each group, assess how well-specified each issue is, and produce an actionable triage report.
+
+## Step 1: Fetch all open issues
+
+Fetch every open issue with metadata:
+
+```
+gh issue list --state open --limit 200 --json number,title,author,labels,assignees,createdAt,updatedAt,body,commentsCount,reactionGroups,milestone
+```
+
+If `$ARGUMENTS` contains `--label=`, append `--label ''` to the command. If it contains `--milestone=`, append `--milestone ''` to the command.
+
+Also fetch recently closed issues (last 14 days) to detect duplicates and already-resolved work:
+
+```
+gh issue list --state closed --search "closed:>=$(date -v-14d +%Y-%m-%d)" --limit 100 --json number,title,body,labels,closedAt
+```
+
+**Exclude pull requests** — `gh issue list` may include PRs. Fetch open PR numbers to filter them out:
+
+```
+gh pr list --state open --json number --jq '.[].number'
+```
+
+Remove any issue whose number appears in this list.
+
+## Step 2: Classify each issue as Bug or Feature
+
+Read each issue's title, body, and labels to classify it into one of these categories:
+
+### Bugs
+Issues that describe **broken existing behavior** — something that worked or should work but doesn't. Signals:
+- Labels: `bug`, `defect`, `regression`, `crash`, `error`
+- Title/body keywords: "broken", "fails", "crash", "panic", "error", "regression", "doesn't work", "unexpected behavior"
+- Includes reproduction steps or error output
+- References existing functionality not working as documented
+
+### Feature Requests
+Issues that describe **new or enhanced behavior** — something that doesn't exist yet. Signals:
+- Labels: `enhancement`, `feature`, `feature-request`, `improvement`, `proposal`
+- Title/body keywords: "add", "support", "implement", "would be nice", "proposal", "RFC", "new"
+- Describes a capability the project doesn't have
+- Proposes a design or API change
+
+### Ambiguous
+If an issue doesn't clearly fit either category (e.g., "improve X performance" could be a bug or a feature), classify it as **Ambiguous** and note why.
+
+## Step 3: Rate issue detail level
+
+For each issue, assess how well-specified it is on a 3-tier scale:
+
+| Detail Level | Criteria |
+|-------------|----------|
+| **Well-specified** | Has clear description of what/why, reproduction steps (bugs) or user story (features), acceptance criteria or expected behavior, and enough context to start working immediately |
+| **Adequate** | Describes the problem or request clearly, but missing some detail — no repro steps, vague acceptance criteria, or unclear scope. Needs 1-2 clarifying questions before work can start |
+| **Under-specified** | Vague title-only or single-sentence body, no context on why it matters, no clear definition of done. Needs significant discussion before it's actionable |
+
+Indicators of good specification:
+- Code snippets, error logs, or screenshots
+- Steps to reproduce (bugs)
+- Proposed API/behavior (features)
+- Links to related issues or discussions
+- Clear "done when" criteria
+
+## Step 4: Rank bugs by severity
+
+Score each bug on these dimensions and compute an overall severity rank:
+
+### Impact (1-4)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 4 | **Critical** | Data loss, security vulnerability, complete feature broken, crash in common path |
+| 3 | **High** | Major feature degraded, workaround exists but painful, affects many users |
+| 2 | **Medium** | Minor feature broken, easy workaround, affects subset of users |
+| 1 | **Low** | Cosmetic, edge case, documentation error, minor inconvenience |
+
+### Urgency (1-3)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 3 | **Urgent** | Security issue, regression in recent release, blocking other work |
+| 2 | **Normal** | Should be fixed in next release cycle |
+| 1 | **Low** | Fix when convenient, backlog-worthy |
+
+### Scope (1-3)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 3 | **Broad** | Affects core path, multiple modules, or all users |
+| 2 | **Moderate** | Affects one module or a specific configuration |
+| 1 | **Narrow** | Affects edge case or single obscure path |
+
+**Bug severity score** = Impact × 2 + Urgency + Scope (base max 14)
+
+Apply a one-time +2 boost if any of the following are true (max 16):
+- Has a linked PR already (someone is working on it — fast-track review)
+- Is labeled `security`
+- Is a regression (worked before, broken now)
+
+## Step 5: Rank features by opportunity
+
+Score each feature request on these dimensions:
+
+### Value (1-4)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 4 | **High** | Unlocks new use cases, frequently requested, strategic alignment |
+| 3 | **Medium-High** | Significant quality-of-life improvement, good user demand signals |
+| 2 | **Medium** | Nice to have, modest improvement to existing workflow |
+| 1 | **Low** | Marginal value, niche use case, unclear demand |
+
+Look for value signals in the issue:
+- Number of thumbs-up reactions or "+1" comments
+- Multiple people asking for the same thing
+- Alignment with project roadmap (check CLAUDE.md TODOs)
+- Unblocks other features or simplifies architecture
+
+### Effort estimate (1-3, inverted — lower effort = higher score)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 3 | **Small** | <1 day, isolated change, clear implementation path |
+| 2 | **Medium** | 1-3 days, touches a few modules, some design needed |
+| 1 | **Large** | 3+ days, cross-cutting, needs RFC or architectural discussion |
+
+### Readiness (1-3)
+| Score | Level | Description |
+|-------|-------|-------------|
+| 3 | **Ready** | Well-specified, implementation path clear, no blockers |
+| 2 | **Almost ready** | Needs minor clarification, but scope is understood |
+| 1 | **Not ready** | Needs design discussion, has open questions, blocked by other work |
+
+**Opportunity score** = Value × 2 + Effort + Readiness (base max 14)
+
+Apply a one-time +2 boost if any of the following are true (max 16):
+- A community member offered to implement it
+- It has a linked draft PR
+- It closes a gap listed in the project's "Current Limitations / TODOs"
+
+## Step 6: Detect duplicates and relationships
+
+Check for:
+- **Duplicates** — Issues describing the same bug or requesting the same feature (compare titles and bodies)
+- **Related clusters** — Groups of issues around the same area (e.g., multiple workspace issues, multiple CLI issues)
+- **Already fixed** — Open issues that may have been resolved by recently closed issues or merged PRs
+- **Blockers** — Issues that reference other issues as prerequisites ("depends on #N", "blocked by #N")
+- **Epic candidates** — Multiple small issues that could be grouped under a single tracking issue
+
+## Step 7: Produce the triage report
+
+Present the output in this format:
+
+### Quick Stats
+
+```
+Open: N | Bugs: N | Features: N | Ambiguous: N
+Well-specified: N | Adequate: N | Under-specified: N
+Unassigned: N | Stale (>30d): N
+```
+
+---
+
+### Critical Bugs (Severity 12+)
+
+Bugs that need immediate attention. For each:
+
+| # | Title | Severity | Impact | Detail | Age | Assignee |
+|---|-------|----------|--------|--------|-----|----------|
+
+Include a 1-line summary of the root cause if discernible from the issue.
+
+### High-Priority Bugs (Severity 8-12)
+
+Same table format. These should be addressed in the next release cycle.
+
+### Medium/Low Bugs (Severity <8)
+
+Compact table, sorted by severity descending.
+
+---
+
+### Quick Wins (Opportunity 12+ AND Effort = Small)
+
+Features that are high-value and low-effort — do these first. For each:
+
+| # | Title | Opportunity | Value | Effort | Detail | Age |
+|---|-------|-------------|-------|--------|--------|-----|
+
+### High-Opportunity Features (Opportunity 10+)
+
+Same table format. Worth investing in.
+
+### Backlog Features (Opportunity <10)
+
+Compact table, sorted by opportunity descending.
+
+---
+
+### Under-Specified Issues (Need Clarification)
+
+Issues rated "Under-specified" that can't be triaged effectively. For each, suggest 1-2 specific questions to ask the author to make it actionable.
+
+| # | Title | Type | What's missing |
+|---|-------|------|---------------|
+
+### Ambiguous Issues (Bug or Feature?)
+
+Issues that couldn't be clearly classified. For each, explain the ambiguity and suggest which category it likely belongs in.
+
+---
+
+### Duplicates & Overlaps
+
+Groups of issues that appear to be duplicates or closely related. Recommend which to keep and which to close.
+
+### Already Fixed?
+
+Open issues that may have been resolved by recently closed issues or merged PRs.
+
+### Stale Issues (>30 days, no activity)
+
+Issues with no updates in 30+ days. Recommend: close, ping author, or keep.
+
+---
+
+### By Area
+
+Group all issues by the area of the codebase they affect (infer from title/body/labels):
+
+| Area | Bugs | Features | Top Priority |
+|------|------|----------|-------------|
+
+### Suggested Next Actions
+
+Based on the triage, provide 3-5 concrete recommendations:
+1. Which bugs to fix first and why
+2. Which quick-win features to pick up
+3. Which under-specified issues to clarify
+4. Which stale issues to close
+5. Any clusters that suggest a larger initiative
+
+## Rules
+
+- Use `gh` CLI for all GitHub operations. Never guess issue state — always check.
+- For large issue lists (>20), use the Task tool to parallelize fetching issue details and comments.
+- Be concise in summaries. One line per issue in tables.
+- When scoring, be honest about uncertainty. If you can't tell severity from the description, say so and rate it conservatively.
+- Factor in issue age — older unresolved bugs may indicate they're less critical than they seem, or that they're hard to fix. Note this in your assessment.
+- Check comment threads for additional context that the original body may lack. An under-specified issue with rich discussion may actually be well-understood.
+- Do NOT post comments, close issues, or take any action. This skill is read-only analysis.
+- If the repo has >100 open issues, focus the detailed analysis on the top 30 by recency and engagement (comments + reactions), and provide a summary table for the rest.
diff --git a/.claude/commands/triage-prs.md b/.claude/commands/triage-prs.md
new file mode 100644
index 00000000..862719a9
--- /dev/null
+++ b/.claude/commands/triage-prs.md
@@ -0,0 +1,161 @@
+---
+description: Classify all open PRs by module, review state, scope, and architectural impact — produces a prioritized triage dashboard
+disable-model-invocation: true
+allowed-tools: Bash(gh pr list:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh api:*), Bash(gh pr checks:*), Bash(git log:*), Read, Grep, Glob, Task
+argument-hint: "[--label=] [--author=]"
+---
+
+# PR Triage Dashboard
+
+You are triaging all open PRs on this repository. Your job is to produce a prioritized, module-grouped dashboard that tells the maintainer exactly which PRs need attention and in what order.
+
+## Step 1: Fetch all open PRs
+
+Fetch every open PR with metadata:
+
+```
+gh pr list --state open --limit 100 --json number,title,author,labels,additions,deletions,headRefName,createdAt,updatedAt,isDraft,reviewRequests,reviews,files,body
+```
+
+If `$ARGUMENTS` contains `--label=`, append `--label ''` to the `gh pr list` command. If it contains `--author=`, append `--author ''` to the command.
+
+Also fetch recently merged PRs (last 7 days) to detect superseded/conflicting work:
+
+```
+gh pr list --state merged --search "merged:>=$(date -v-7d +%Y-%m-%d)" --limit 100 --json number,title,body,mergedAt
+```
+
+## Step 2: Classify each PR by module
+
+For each open PR, determine the primary module it touches by examining the `files` field. Classify into these categories based on the dominant `src/` subdirectory:
+
+| Category | Directories |
+|----------|------------|
+| **LLM & Inference** | `src/llm/` |
+| **Agent Core** | `src/agent/`, `src/skills/` |
+| **Tools** | `src/tools/`, `tools-src/` |
+| **Channels** | `src/channels/`, `channels-src/` |
+| **Storage & Memory** | `src/db/`, `src/workspace/`, `migrations/` |
+| **Security** | `src/safety/`, `src/secrets/` |
+| **Config & Setup** | `src/config.rs`, `src/setup/`, `src/cli/` |
+| **Sandbox & Orchestration** | `src/sandbox/`, `src/orchestrator/`, `src/worker/` |
+| **Hooks & Extensions** | `src/hooks/`, `src/extensions/` |
+| **Context & History** | `src/context/`, `src/history/`, `src/estimation/`, `src/evaluation/` |
+| **Web Gateway** | `src/channels/web/` |
+| **CI/CD & Docs** | `.github/`, `README.md`, `CLAUDE.md`, `*.md` (no src) |
+| **Other** | Anything else |
+
+If a PR touches multiple modules, assign it to the **primary** module (most files changed) but note the cross-cutting modules.
+
+## Step 3: Assess review state
+
+For each PR, determine its review status:
+
+- **Approved** — At least one human APPROVED review, no outstanding CHANGES_REQUESTED
+- **Changes requested** — At least one CHANGES_REQUESTED review still unresolved
+- **Reviewed (comments only)** — Human comments but no formal approve/reject
+- **Automated only** — Only bot reviews (gemini-code-assist, copilot, etc.)
+- **No review** — No reviews at all
+
+Also check:
+- CI status: `gh pr checks {number}` — PASS / FAIL / NONE
+- Draft status: is the PR marked as draft?
+- Staleness: how many days since `updatedAt`?
+
+## Step 4: Determine scope and risk
+
+Classify each PR by scope:
+
+| Scope | Criteria |
+|-------|----------|
+| **Tiny** | <50 lines changed (additions + deletions), 1-2 files |
+| **Small** | 50-200 lines, 1-5 files |
+| **Medium** | 200-500 lines, 3-10 files |
+| **Large** | 500-2000 lines, 5-20 files |
+| **XL** | 2000+ lines or 20+ files |
+
+## Step 5: Classify as fix vs. architectural
+
+For each PR, determine its nature:
+
+### Fixes (merge fast)
+- Bug fixes with clear root cause
+- Security patches
+- Crash/panic prevention
+- Typo/doc corrections
+- Code quality (removing .unwrap(), etc.)
+
+### Features (standard review)
+- New functionality within existing patterns
+- New tool implementations
+- Configuration additions
+- Test additions
+
+### Architectural (deep review needed)
+- New modules or subsystems
+- Changes to core traits or interfaces
+- New database backends or storage engines
+- New provider abstractions
+- Changes touching 5+ modules
+- Anything modifying the agent loop, session model, or security layer
+- New dependencies (check Cargo.toml changes)
+
+## Step 6: Detect conflicts and superseded PRs
+
+Check for:
+- Multiple PRs fixing the same issue (look at "Closes #N" / "Fixes #N" in PR bodies)
+- PRs touching the same files (potential merge conflicts)
+- PRs that are follow-ups to other open PRs (dependency chains)
+- PRs superseded by recently merged work
+
+## Step 7: Produce the dashboard
+
+Present the output in this format:
+
+### Quick Stats
+```
+Open: N | Draft: N | Needs review: N | Changes requested: N | Ready to merge: N
+```
+
+### Ready to Merge
+PRs that are approved, CI passing, and non-draft. List with one-line summary.
+
+### Needs Human Review (Fixes)
+Fixes that have no human review yet, sorted by severity (security > crash > bug > quality).
+
+### Needs Human Review (Features)
+Features with no human review, sorted by scope (smallest first).
+
+### Needs Deep Architectural Review
+Large/XL PRs, new modules, or cross-cutting changes. For each, include:
+- Which modules are affected
+- What new patterns or abstractions are introduced
+- Key risk areas to focus review on
+
+### Changes Requested (Waiting on Author)
+PRs where a reviewer asked for changes. Include who requested and a 1-line summary of what's needed.
+
+### Stale / Blocked
+PRs with no activity >7 days, or blocked by other PRs.
+
+### Conflicts & Overlaps
+Any detected conflicts, superseded PRs, or dependency chains.
+
+### By Module
+Group all PRs by their primary module in a compact table:
+
+| Module | PRs | Key PR to review first |
+|--------|-----|----------------------|
+
+### Superseded PRs (recommend closing)
+PRs that are clearly superseded by merged work. Include reasoning.
+
+## Rules
+
+- Use `gh` CLI for all GitHub operations. Never guess PR state — always check.
+- For large PR lists (>15), use the Task tool to parallelize fetching PR details and diffs.
+- Be concise in summaries. One line per PR in tables.
+- When assessing "ready to merge", be conservative. If there's any unresolved concern from a repo member, it's not ready.
+- Flag any PR that has been open >14 days with no review as needing attention.
+- If a PR description says "Closes #N" but #N was already closed by another merged PR, flag it as potentially superseded.
+- Do NOT post comments or take any action on PRs. This skill is read-only analysis.
diff --git a/.env.example b/.env.example
index 5e172630..acb96824 100644
--- a/.env.example
+++ b/.env.example
@@ -8,18 +8,38 @@ DATABASE_POOL_SIZE=10
# VECTOR_BACKEND=lancedb
# LANCEDB_PATH=~/.ironclaw/lancedb # path for LanceDB when VECTOR_BACKEND=lancedb
-# LLM Provider (NEAR AI)
-# NEAR AI provides a unified interface to all models with user authentication
-# Session token is stored in ~/.ironclaw/session.json and managed automatically.
-# On first run, the agent will open a browser for OAuth authentication.
-NEARAI_MODEL=claude-3-5-sonnet-20241022
+# LLM Provider
+# LLM_BACKEND=nearai # default
+# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil
+# LLM_REQUEST_TIMEOUT_SECS=120 # Increase for local LLMs (Ollama, vLLM, LM Studio)
+
+# === Anthropic Direct ===
+# Two auth modes:
+# 1. API key: Set ANTHROPIC_API_KEY (from console.anthropic.com/settings/keys)
+# 2. OAuth token: Set ANTHROPIC_OAUTH_TOKEN (from `claude login`)
+# OAuth tokens use Authorization: Bearer instead of x-api-key header.
+# ANTHROPIC_API_KEY=sk-ant-...
+# ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-... # from `claude login` credentials
+# ANTHROPIC_MODEL=claude-sonnet-4-20250514
+
+# === OpenAI Direct ===
+# OPENAI_API_KEY=sk-...
+
+# === NEAR AI (Chat Completions API) ===
+# Two auth modes:
+# 1. Session token (default): Uses browser OAuth (GitHub/Google) on first run.
+# Session token stored in ~/.ironclaw/session.json automatically.
+# Base URL defaults to https://private.near.ai
+# 2. API key: Set NEARAI_API_KEY to use API key auth from cloud.near.ai.
+# Base URL defaults to https://cloud-api.near.ai
+NEARAI_MODEL=zai-org/GLM-5-FP8
NEARAI_BASE_URL=https://private.near.ai
NEARAI_AUTH_URL=https://private.near.ai
-# NEARAI_SESSION_PATH=~/.ironclaw/session.json # optional, default shown
+# NEARAI_SESSION_TOKEN=sess_... # hosting providers: set this
+# NEARAI_SESSION_PATH=~/.ironclaw/session.json # optional, default shown
+# NEARAI_API_KEY=... # API key from cloud.near.ai
# Local LLM Providers (Ollama, LM Studio, vLLM, LiteLLM)
-# LLM_BACKEND=nearai # default
-# Possible values: nearai, ollama, openai_compatible, openai, anthropic
# === Ollama ===
# OLLAMA_MODEL=llama3.2
@@ -31,13 +51,43 @@ NEARAI_AUTH_URL=https://private.near.ai
# LLM_BACKEND=openai_compatible
# LLM_BASE_URL=http://localhost:1234/v1
# LLM_API_KEY=sk-... # optional for local servers
+# Custom HTTP headers for OpenAI-compatible providers
+# Format: comma-separated key:value pairs
+# LLM_EXTRA_HEADERS=HTTP-Referer:https://github.com/nearai/ironclaw,X-Title:ironclaw
-# === OpenRouter (via OpenAI-compatible) ===
-# LLM_MODEL=anthropic/claude-sonnet-4
+# === OpenRouter (300+ models via OpenAI-compatible) ===
+# LLM_MODEL=anthropic/claude-sonnet-4 # see openrouter.ai/models for IDs
# LLM_BACKEND=openai_compatible
# LLM_BASE_URL=https://openrouter.ai/api/v1
# LLM_API_KEY=sk-or-...
+# LLM_EXTRA_HEADERS=HTTP-Referer:https://myapp.com,X-Title:MyApp
+
+
+# === Together AI (via OpenAI-compatible) ===
+# LLM_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo
+# LLM_BACKEND=openai_compatible
+# LLM_BASE_URL=https://api.together.xyz/v1
+# LLM_API_KEY=...
+
+# === Fireworks AI (via OpenAI-compatible) ===
+# LLM_MODEL=accounts/fireworks/models/llama4-maverick-instruct-basic
+# LLM_BACKEND=openai_compatible
+# LLM_BASE_URL=https://api.fireworks.ai/inference/v1
+# LLM_API_KEY=fw_...
+
+# === Anthropic Direct ===
+# LLM_BACKEND=anthropic
+# ANTHROPIC_MODEL=claude-sonnet-4-6
+# ANTHROPIC_API_KEY=sk-ant-...
+# ANTHROPIC_BASE_URL=https://api.anthropic.com # default
+# Prompt cache retention — controls Anthropic server-side prompt caching:
+# none = disabled (no cache_control injected)
+# short = 5-minute TTL, 1.25× (125%) write surcharge (default)
+# long = 1-hour TTL, 2.0× (200%) write surcharge
+# ANTHROPIC_CACHE_RETENTION=short
+
+# For full provider setup guide see docs/LLM_PROVIDERS.md
# Channel Configuration
# CLI is always enabled
@@ -55,6 +105,17 @@ HTTP_HOST=0.0.0.0
HTTP_PORT=8080
HTTP_WEBHOOK_SECRET=your-webhook-secret
+# Signal Channel (optional, requires signal-cli daemon --http)
+# SIGNAL_HTTP_URL=http://127.0.0.1:8080
+# SIGNAL_ACCOUNT=+1234567890
+# SIGNAL_ALLOW_FROM=+1234567890,uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # comma-separated, * for all, empty = deny/require pairing
+# SIGNAL_ALLOW_FROM_GROUPS= # comma-separated group IDs, * for all, empty = deny all groups
+# SIGNAL_DM_POLICY=pairing # open | allowlist | pairing
+# SIGNAL_GROUP_POLICY=allowlist # allowlist | open | disabled
+# SIGNAL_GROUP_ALLOW_FROM= # comma-separated, empty = inherit from ALLOW_FROM
+# SIGNAL_IGNORE_ATTACHMENTS=false
+# SIGNAL_IGNORE_STORIES=true
+
# Agent Settings
AGENT_NAME=ironclaw
AGENT_MAX_PARALLEL_JOBS=5
@@ -74,9 +135,23 @@ HEARTBEAT_INTERVAL_SECS=1800
HEARTBEAT_NOTIFY_CHANNEL=cli
HEARTBEAT_NOTIFY_USER=default
+# Memory hygiene settings (automatic cleanup of stale workspace documents)
+# Runs on each heartbeat tick; identity files (IDENTITY.md, SOUL.md) are never deleted
+# MEMORY_HYGIENE_ENABLED=true
+# MEMORY_HYGIENE_DAILY_RETENTION_DAYS=30 # delete daily/ docs older than this many days
+# MEMORY_HYGIENE_CONVERSATION_RETENTION_DAYS=7 # delete conversations/ docs older than this many days
+# MEMORY_HYGIENE_CADENCE_HOURS=12 # minimum hours between cleanup passes
+
# Safety settings
SAFETY_MAX_OUTPUT_LENGTH=100000
SAFETY_INJECTION_CHECK_ENABLED=true
+# Restart Feature (Docker containers only)
+# Set IRONCLAW_IN_DOCKER=true in the container entrypoint to enable the restart feature.
+# Without this, the restart tool and /restart command will be disabled.
+# IRONCLAW_IN_DOCKER=false
+# IRONCLAW_RESTART_DELAY=5 # default wait before exit (seconds, range: 1-30)
+# IRONCLAW_MAX_FAILURES=10 # max consecutive failures before container exits
+
# Logging
RUST_LOG=ironclaw=debug,tower_http=debug
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..da476900
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+tests/test-pages/**/*.html linguist-generated=true
\ No newline at end of file
diff --git a/.githooks/commit-msg b/.githooks/commit-msg
new file mode 120000
index 00000000..2eb95be6
--- /dev/null
+++ b/.githooks/commit-msg
@@ -0,0 +1 @@
+../scripts/commit-msg-regression.sh
\ No newline at end of file
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 00000000..0abd640a
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Pre-commit hook: run version bump checks when WIT or extension sources change.
+# Install: git config core.hooksPath .githooks
+
+# Only run the check if relevant files are staged
+STAGED=$(git diff --cached --name-only)
+
+NEEDS_CHECK=false
+if echo "$STAGED" | grep -qE '^wit/|^channels-src/|^tools-src/'; then
+ NEEDS_CHECK=true
+fi
+
+if $NEEDS_CHECK; then
+ echo "pre-commit: checking version bumps..."
+ if ! ./scripts/check-version-bumps.sh; then
+ echo ""
+ echo "Commit blocked: version bump check failed."
+ echo "Bump versions in the relevant registry JSON and/or WIT package declaration."
+ echo "To bypass: git commit --no-verify"
+ exit 1
+ fi
+fi
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 00000000..fd7da0be
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,166 @@
+# Scope labels for actions/labeler@v6
+# Maps file path globs to scope labels. Multiple labels can apply per PR.
+
+"scope: agent":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/agent/**
+
+"scope: channel":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/channels/channel.rs
+ - src/channels/manager.rs
+ - src/channels/mod.rs
+
+"scope: channel/cli":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/channels/cli/**
+ - src/cli/**
+
+"scope: channel/web":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/channels/web/**
+
+"scope: channel/wasm":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/channels/wasm/**
+
+"scope: tool":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/tools/tool.rs
+ - src/tools/registry.rs
+ - src/tools/mod.rs
+ - src/tools/sandbox.rs
+
+"scope: tool/builtin":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/tools/builtin/**
+
+"scope: tool/wasm":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/tools/wasm/**
+
+"scope: tool/mcp":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/tools/mcp/**
+
+"scope: tool/builder":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/tools/builder/**
+
+"scope: db":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/db/mod.rs
+
+"scope: db/postgres":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/db/postgres.rs
+ - migrations/**
+
+"scope: db/libsql":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/db/libsql_backend.rs
+ - src/db/libsql_migrations.rs
+
+"scope: safety":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/safety/**
+
+"scope: llm":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/llm/**
+
+"scope: workspace":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/workspace/**
+
+"scope: orchestrator":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/orchestrator/**
+
+"scope: worker":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/worker/**
+
+"scope: secrets":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/secrets/**
+
+"scope: config":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/config.rs
+ - src/settings.rs
+
+"scope: extensions":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/extensions/**
+
+"scope: setup":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/setup/**
+
+"scope: evaluation":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/evaluation/**
+
+"scope: estimation":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/estimation/**
+
+"scope: sandbox":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/sandbox/**
+ - Dockerfile*
+
+"scope: hooks":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/hooks/**
+
+"scope: pairing":
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/pairing/**
+
+"scope: ci":
+ - changed-files:
+ - any-glob-to-any-file:
+ - .github/workflows/**
+ - .github/scripts/**
+
+"scope: docs":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.md"
+ - docs/**
+ - LICENSE*
+
+"scope: dependencies":
+ - changed-files:
+ - any-glob-to-any-file:
+ - Cargo.toml
+ - Cargo.lock
diff --git a/.github/scripts/create-labels.sh b/.github/scripts/create-labels.sh
new file mode 100755
index 00000000..66f07ea9
--- /dev/null
+++ b/.github/scripts/create-labels.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# Idempotent label bootstrap for IronClaw PR automation.
+# Uses `gh label create --force` so it can be re-run safely.
+#
+# Usage: bash .github/scripts/create-labels.sh
+# Requires: gh CLI authenticated with repo scope
+
+set -euo pipefail
+
+if ! command -v gh &>/dev/null; then
+ echo "Error: gh CLI is required. Install from https://cli.github.com" >&2
+ exit 1
+fi
+
+create() {
+ local name="$1" color="$2" description="$3"
+ gh label create "$name" --color "$color" --description "$description" --force
+}
+
+echo "==> Creating size labels..."
+create "size: XS" "F9D0C4" "< 10 changed lines (excluding docs)"
+create "size: S" "F5A3A3" "10-49 changed lines"
+create "size: M" "E57373" "50-199 changed lines"
+create "size: L" "D32F2F" "200-499 changed lines"
+create "size: XL" "B71C1C" "500+ changed lines"
+
+echo "==> Creating risk labels..."
+create "risk: low" "4CAF50" "Changes to docs, tests, or low-risk modules"
+create "risk: medium" "FFC107" "Business logic, config, or moderate-risk modules"
+create "risk: high" "F44336" "Safety, secrets, auth, or critical infrastructure"
+create "risk: manual" "9E9E9E" "Risk level set manually (sticky, not overwritten)"
+
+echo "==> Creating scope labels..."
+create "scope: agent" "006B75" "Agent core (agent loop, router, scheduler)"
+create "scope: channel" "00838F" "Channel infrastructure"
+create "scope: channel/cli" "00897B" "TUI / CLI channel"
+create "scope: channel/web" "00796B" "Web gateway channel"
+create "scope: channel/wasm" "00695C" "WASM channel runtime"
+create "scope: tool" "1565C0" "Tool infrastructure"
+create "scope: tool/builtin" "1976D2" "Built-in tools"
+create "scope: tool/wasm" "1E88E5" "WASM tool sandbox"
+create "scope: tool/mcp" "2196F3" "MCP client"
+create "scope: tool/builder" "42A5F5" "Dynamic tool builder"
+create "scope: db" "4A148C" "Database trait / abstraction"
+create "scope: db/postgres" "6A1B9A" "PostgreSQL backend"
+create "scope: db/libsql" "7B1FA2" "libSQL / Turso backend"
+create "scope: safety" "880E4F" "Prompt injection defense"
+create "scope: llm" "4527A0" "LLM integration"
+create "scope: workspace" "283593" "Persistent memory / workspace"
+create "scope: orchestrator" "0D47A1" "Container orchestrator"
+create "scope: worker" "01579B" "Container worker"
+create "scope: secrets" "BF360C" "Secrets management"
+create "scope: config" "E65100" "Configuration"
+create "scope: extensions" "33691E" "Extension management"
+create "scope: setup" "827717" "Onboarding / setup"
+create "scope: evaluation" "558B2F" "Success evaluation"
+create "scope: estimation" "9E9D24" "Cost/time estimation"
+create "scope: sandbox" "00BFA5" "Docker sandbox"
+create "scope: hooks" "6D4C41" "Git/event hooks"
+create "scope: pairing" "4E342E" "Pairing mode"
+create "scope: ci" "546E7A" "CI/CD workflows"
+create "scope: docs" "78909C" "Documentation"
+create "scope: dependencies" "90A4AE" "Dependency updates"
+
+echo "==> Creating workflow labels..."
+create "skip-regression-check" "9E9E9E" "Acknowledged: fix without regression test"
+
+echo "==> Creating contributor labels..."
+create "contributor: new" "FFF9C4" "First-time contributor"
+create "contributor: regular" "FFE082" "2-5 merged PRs"
+create "contributor: experienced" "FFB74D" "6-19 merged PRs"
+create "contributor: core" "FF8A65" "20+ merged PRs"
+
+echo "Done. All labels created/updated."
diff --git a/.github/scripts/pr-labeler.sh b/.github/scripts/pr-labeler.sh
new file mode 100755
index 00000000..96dc0fa7
--- /dev/null
+++ b/.github/scripts/pr-labeler.sh
@@ -0,0 +1,139 @@
+#!/usr/bin/env bash
+# Classify a PR by size, risk, and contributor tier.
+# Called by the pr-label-classify workflow.
+#
+# Inputs (env vars):
+# PR_NUMBER — pull request number
+# REPO — owner/repo (e.g. "user/ironclaw")
+#
+# Requires: gh CLI, jq
+
+set -euo pipefail
+
+PR_NUMBER="${PR_NUMBER:?PR_NUMBER is required}"
+REPO="${REPO:?REPO is required}"
+
+# ─── helpers ────────────────────────────────────────────────────────────────
+
+# Remove all labels in a dimension except the desired one.
+# Usage: set_exclusive_label "size" "size: M"
+set_exclusive_label() {
+ local prefix="$1" desired="$2"
+
+ # Fetch current labels on the PR
+ local current
+ current=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
+
+ # Remove any existing label with the same prefix
+ while IFS= read -r label; do
+ [[ -z "$label" ]] && continue
+ if [[ "$label" == "${prefix}:"* && "$label" != "$desired" ]]; then
+ gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$label" 2>/dev/null || true
+ fi
+ done <<< "$current"
+
+ # Add the desired label
+ gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$desired"
+}
+
+# ─── size ───────────────────────────────────────────────────────────────────
+
+classify_size() {
+ # Sum changed lines across non-doc files
+ local total
+ total=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" \
+ --paginate --jq '
+ [.[] | select(.filename | test("\\.(md|txt|rst|adoc)$") | not) | .changes]
+ | add // 0
+ ')
+
+ local label
+ if (( total < 10 )); then label="size: XS"
+ elif (( total < 50 )); then label="size: S"
+ elif (( total < 200 )); then label="size: M"
+ elif (( total < 500 )); then label="size: L"
+ else label="size: XL"
+ fi
+
+ echo "Size: ${total} changed lines -> ${label}"
+ set_exclusive_label "size" "$label"
+}
+
+# ─── risk ───────────────────────────────────────────────────────────────────
+
+classify_risk() {
+ # If "risk: manual" is present, skip — it's a sticky override
+ local current
+ current=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
+ if echo "$current" | grep -qx "risk: manual"; then
+ echo "Risk: skipped (manual override)"
+ return
+ fi
+
+ # Fetch changed file paths
+ local files
+ files=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" \
+ --paginate --jq '.[].filename')
+
+ local risk="low"
+
+ while IFS= read -r file; do
+ [[ -z "$file" ]] && continue
+
+ case "$file" in
+ # High risk: safety, secrets, auth, crypto, setup, orchestrator auth
+ src/safety/*|src/secrets/*|src/llm/session.rs|src/orchestrator/auth.rs|\
+ src/channels/web/auth.rs|src/setup/*)
+ risk="high"
+ break # can't go higher
+ ;;
+
+ # Medium risk: agent core, config, database, worker, tools, channels
+ src/agent/*|src/config.rs|src/settings.rs|src/db/*|src/worker/*|\
+ src/tools/*|src/channels/*|src/orchestrator/*|src/context/*|\
+ src/hooks/*|src/sandbox/*|src/extensions/*|Cargo.toml|\
+ .github/workflows/*)
+ # Only upgrade, never downgrade
+ [[ "$risk" != "high" ]] && risk="medium"
+ ;;
+
+ # Low risk: docs, tests, estimation, evaluation, history, etc.
+ *)
+ ;;
+ esac
+ done <<< "$files"
+
+ echo "Risk: ${risk}"
+ set_exclusive_label "risk" "risk: ${risk}"
+}
+
+# ─── contributor tier ───────────────────────────────────────────────────────
+
+classify_contributor() {
+ # Get PR author
+ local author
+ author=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json author --jq '.author.login')
+
+ # Count merged PRs by this author in this repo
+ local count
+ count=$(gh pr list --repo "$REPO" --state merged --author "$author" \
+ --limit 100 --json number --jq 'length')
+
+ local label
+ if (( count == 0 )); then label="contributor: new"
+ elif (( count < 6 )); then label="contributor: regular"
+ elif (( count < 20 )); then label="contributor: experienced"
+ else label="contributor: core"
+ fi
+
+ echo "Contributor: ${author} has ${count} merged PRs -> ${label}"
+ set_exclusive_label "contributor" "$label"
+}
+
+# ─── main ───────────────────────────────────────────────────────────────────
+
+echo "Classifying PR #${PR_NUMBER} in ${REPO}..."
+classify_size
+classify_risk
+classify_contributor
+echo "Done."
diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml
index 19f7d725..526c7740 100644
--- a/.github/workflows/code_style.yml
+++ b/.github/workflows/code_style.yml
@@ -3,8 +3,8 @@ on:
pull_request:
jobs:
- codestyle:
- name: Code Style (fmt + clippy)
+ format:
+ name: Formatting
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -12,11 +12,71 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
- components: rustfmt, clippy
- - uses: Swatinem/rust-cache@v2
+ components: rustfmt
- name: Check formatting
- run: |
- cargo fmt --all -- --check
- - name: Check lints (cargo clippy)
- run: cargo clippy -- -D warnings
+ run: cargo fmt --all -- --check
+
+ clippy:
+ name: Clippy (${{ matrix.name }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: all-features
+ flags: "--all-features"
+ - name: default
+ flags: ""
+ - name: libsql-only
+ flags: "--no-default-features --features libsql"
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: clippy-${{ matrix.name }}
+ - name: Check lints
+ run: cargo clippy --all --benches --tests --examples ${{ matrix.flags }} -- -D warnings
+
+ clippy-windows:
+ name: Clippy Windows (${{ matrix.name }})
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: all-features
+ flags: "--all-features"
+ - name: default
+ flags: ""
+ - name: libsql-only
+ flags: "--no-default-features --features libsql"
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: clippy-windows-${{ matrix.name }}
+ - name: Check lints
+ run: cargo clippy --all --benches --tests --examples ${{ matrix.flags }} -- -D warnings
+
+ # Roll-up job for branch protection
+ code-style:
+ name: Code Style (fmt + clippy)
+ runs-on: ubuntu-latest
+ if: always()
+ needs: [format, clippy, clippy-windows]
+ steps:
+ - run: |
+ if [[ "${{ needs.format.result }}" != "success" || "${{ needs.clippy.result }}" != "success" || "${{ needs.clippy-windows.result }}" != "success" ]]; then
+ echo "One or more jobs failed"
+ exit 1
+ fi
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 00000000..e7371677
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,226 @@
+# Code Coverage Workflow
+#
+# This workflow runs test coverage analysis and uploads reports to Codecov.
+# Coverage reports help identify untested code paths and maintain code quality.
+#
+# What it does:
+# - Runs unit and integration tests with coverage instrumentation
+# - Runs E2E tests with coverage instrumentation
+# - Uploads coverage reports to Codecov (https://codecov.io/gh/nearai/ironclaw)
+#
+# Viewing coverage reports:
+# - PRs automatically get coverage comments showing changes in coverage
+# - Visit https://codecov.io/gh/nearai/ironclaw for detailed coverage reports
+# - Coverage reports are generated for three configurations:
+# 1. all-features: Full feature set
+# 2. default: Default features
+# 3. libsql-only: Minimal libSQL-only configuration
+# - E2E coverage tracks end-to-end test coverage separately
+#
+# Coverage files:
+# - Unit/integration: lcov.info (uploaded to Codecov with "unit" flag)
+# - E2E: e2e-coverage.info (uploaded to Codecov with "e2e" flag)
+#
+# Requirements:
+# - Uses cargo-llvm-cov for coverage instrumentation
+# - Requires PostgreSQL for integration tests (pgvector/pgvector:pg16)
+# - E2E tests require Python 3.12 and Playwright
+
+name: Code Coverage
+on:
+ push:
+ branches: [main]
+
+permissions:
+ id-token: write
+ contents: read
+
+jobs:
+ coverage:
+ name: Coverage (${{ matrix.name }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: all-features
+ flags: "--all-features"
+ has_postgres: true
+ - name: default
+ flags: ""
+ has_postgres: true
+ - name: libsql-only
+ flags: "--no-default-features --features libsql"
+ has_postgres: false
+ services:
+ postgres:
+ image: pgvector/pgvector:pg16
+ env:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: ironclaw_test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U postgres"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: llvm-tools-preview
+ targets: wasm32-wasip2
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: coverage-${{ matrix.name }}
+
+ - name: Install cargo-llvm-cov
+ uses: taiki-e/install-action@cargo-llvm-cov
+
+ - name: Install cargo-component
+ run: |
+ if ! command -v cargo-component >/dev/null 2>&1; then
+ cargo install cargo-component --locked
+ fi
+
+ - name: Build WASM channels (for integration tests)
+ run: ./scripts/build-wasm-extensions.sh --channels
+
+ - name: Run database migrations
+ if: matrix.has_postgres
+ run: |
+ set -euo pipefail
+ readarray -t migration_files < <(printf '%s\n' migrations/V*.sql | sort -V)
+ for f in "${migration_files[@]}"; do
+ echo "Applying $f..."
+ psql -v ON_ERROR_STOP=1 -f "$f"
+ done
+ env:
+ PGHOST: localhost
+ PGUSER: postgres
+ PGPASSWORD: postgres
+ PGDATABASE: ironclaw_test
+
+ - name: Set DATABASE_URL for postgres configs
+ if: matrix.has_postgres
+ run: echo "DATABASE_URL=postgres://postgres:postgres@localhost/ironclaw_test" >> "$GITHUB_ENV"
+
+ - name: Generate coverage
+ run: cargo llvm-cov ${{ matrix.flags }} --workspace --lcov --output-path lcov.info
+
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: lcov.info
+ flags: ${{ matrix.name }}
+ disable_search: true
+ use_oidc: true
+ fail_ci_if_error: true
+
+ e2e-coverage:
+ name: E2E Coverage
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: llvm-tools-preview
+ targets: wasm32-wasip2
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: e2e-coverage
+
+ - name: Install cargo-llvm-cov
+ uses: taiki-e/install-action@cargo-llvm-cov
+
+ - name: Install cargo-component
+ run: |
+ if ! command -v cargo-component >/dev/null 2>&1; then
+ cargo install cargo-component --locked
+ fi
+
+ - name: Build WASM channels
+ run: ./scripts/build-wasm-extensions.sh --channels
+
+ - name: Set up coverage instrumentation
+ run: |
+ # show-env outputs shell-quoted values (KEY='value') but GITHUB_ENV
+ # expects unquoted KEY=value. Strip only the wrapping single quotes
+ # from KEY='value' lines without altering any internal characters.
+ cargo llvm-cov show-env | sed -E "s/^([A-Za-z_][A-Za-z0-9_]*)='(.*)'$/\1=\2/" >> "$GITHUB_ENV"
+
+ - name: Clean coverage workspace
+ run: cargo llvm-cov clean --workspace
+
+ - name: Build instrumented binary
+ run: cargo build --no-default-features --features libsql
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install E2E dependencies
+ run: |
+ cd tests/e2e
+ pip install -e .
+ playwright install --with-deps chromium
+
+ - name: Run E2E tests
+ run: |
+ pytest tests/e2e/ -v -x --timeout=120
+ env:
+ RUST_LOG: ironclaw=info
+ RUST_BACKTRACE: "1"
+
+ - name: Verify profraw files exist
+ if: always()
+ run: |
+ echo "LLVM_PROFILE_FILE=${LLVM_PROFILE_FILE}"
+ echo "CARGO_LLVM_COV_TARGET_DIR=${CARGO_LLVM_COV_TARGET_DIR}"
+ profraw_count=$(find target/ -name '*.profraw' 2>/dev/null | wc -l)
+ echo "Found ${profraw_count} .profraw files under target/"
+ find target/ -name '*.profraw' 2>/dev/null || true
+ if [ "$profraw_count" -eq 0 ]; then
+ echo "::warning::No .profraw files found — coverage report will fail"
+ fi
+
+ - name: Generate coverage report
+ if: always()
+ run: cargo llvm-cov report --lcov --output-path e2e-coverage.info
+
+ - name: Upload to Codecov
+ if: always()
+ uses: codecov/codecov-action@v5
+ with:
+ files: e2e-coverage.info
+ flags: e2e
+ disable_search: true
+ use_oidc: true
+ fail_ci_if_error: true
+
+ - name: Upload screenshots on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-screenshots
+ path: tests/e2e/screenshots/
+ if-no-files-found: ignore
+
+ coverage-gate:
+ name: Coverage
+ runs-on: ubuntu-latest
+ if: always()
+ needs: [coverage, e2e-coverage]
+ steps:
+ - run: |
+ if [[ "${{ needs.coverage.result }}" != "success" || "${{ needs.e2e-coverage.result }}" != "success" ]]; then
+ echo "One or more coverage jobs failed"
+ exit 1
+ fi
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 00000000..3dc95a2d
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,99 @@
+name: E2E Tests
+on:
+ schedule:
+ - cron: "0 6 * * 1" # Weekly Monday 6 AM UTC
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - "src/channels/web/**"
+ - "tests/e2e/**"
+
+jobs:
+ # ── Step 1: compile once ──────────────────────────────────────────────────
+ build:
+ name: Build ironclaw (libsql)
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - uses: actions/cache@v4
+ with:
+ path: |
+ target
+ ~/.cargo/registry
+ key: e2e-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
+
+ - name: Build
+ run: cargo build --no-default-features --features libsql
+
+ - name: Upload binary
+ uses: actions/upload-artifact@v4
+ with:
+ name: ironclaw-e2e-binary
+ path: target/debug/ironclaw
+ retention-days: 1
+
+ # ── Step 2: run test slices in parallel ───────────────────────────────────
+ test:
+ name: E2E (${{ matrix.group }})
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - group: core
+ files: "tests/e2e/scenarios/test_connection.py tests/e2e/scenarios/test_chat.py tests/e2e/scenarios/test_sse_reconnect.py tests/e2e/scenarios/test_html_injection.py"
+ - group: features
+ files: "tests/e2e/scenarios/test_skills.py tests/e2e/scenarios/test_tool_approval.py"
+ - group: extensions
+ files: "tests/e2e/scenarios/test_extensions.py"
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Download binary
+ uses: actions/download-artifact@v4
+ with:
+ name: ironclaw-e2e-binary
+ path: target/debug/
+
+ - name: Make binary executable
+ run: chmod +x target/debug/ironclaw
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install E2E dependencies
+ run: |
+ cd tests/e2e
+ pip install -e .
+ playwright install --with-deps chromium
+
+ - name: Run E2E tests (${{ matrix.group }})
+ run: pytest ${{ matrix.files }} -v --timeout=120
+
+ - name: Upload screenshots on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-screenshots-${{ matrix.group }}
+ path: tests/e2e/screenshots/
+ if-no-files-found: ignore
+
+ # ── Roll-up for branch protection ────────────────────────────────────────
+ e2e:
+ name: E2E Tests
+ runs-on: ubuntu-latest
+ if: always()
+ needs: [test]
+ steps:
+ - run: |
+ if [[ "${{ needs.test.result }}" != "success" ]]; then
+ echo "One or more E2E jobs failed"
+ exit 1
+ fi
diff --git a/.github/workflows/pr-label-classify.yml b/.github/workflows/pr-label-classify.yml
new file mode 100644
index 00000000..90f141de
--- /dev/null
+++ b/.github/workflows/pr-label-classify.yml
@@ -0,0 +1,26 @@
+name: "PR: Classify (Size, Risk, Contributor)"
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: read # needed for search/issues API (contributor count)
+
+jobs:
+ classify:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout base branch
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.ref }}
+
+ - name: Classify PR
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ REPO: ${{ github.repository }}
+ run: bash .github/scripts/pr-labeler.sh
diff --git a/.github/workflows/pr-label-scope.yml b/.github/workflows/pr-label-scope.yml
new file mode 100644
index 00000000..1c388561
--- /dev/null
+++ b/.github/workflows/pr-label-scope.yml
@@ -0,0 +1,18 @@
+name: "PR: Scope Labels"
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ scope:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ configuration-path: .github/labeler.yml
+ sync-labels: false # additive only — never remove scope labels
diff --git a/.github/workflows/regression-test-check.yml b/.github/workflows/regression-test-check.yml
new file mode 100644
index 00000000..18b8c76f
--- /dev/null
+++ b/.github/workflows/regression-test-check.yml
@@ -0,0 +1,107 @@
+name: Regression Test Check
+
+on:
+ pull_request:
+
+jobs:
+ regression-test:
+ name: Regression test enforcement
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for regression tests
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
+ run: |
+ set -euo pipefail
+
+ BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
+
+ # --- 1. Is this a fix PR? Check title first, then commit messages ---
+ IS_FIX=false
+
+ if grep -qiE '^(fix(\(.*\))?|hotfix|bugfix):' <<< "$PR_TITLE"; then
+ IS_FIX=true
+ fi
+
+ if [ "$IS_FIX" = false ]; then
+ COMMITS=$(git log --format='%s' "${BASE_REF}..HEAD")
+ if grep -qiE '^(fix(\(.*\))?|hotfix|bugfix):' <<< "$COMMITS"; then
+ IS_FIX=true
+ fi
+ fi
+
+ if [ "$IS_FIX" = false ]; then
+ echo "Not a fix PR — skipping regression test check."
+ exit 0
+ fi
+
+ echo "Fix PR detected."
+
+ # --- 2. Skip label or commit message marker ---
+ if grep -qF ',skip-regression-check,' <<< ",$PR_LABELS,"; then
+ echo "skip-regression-check label present — skipping."
+ exit 0
+ fi
+
+ COMMIT_BODIES=$(git log --format='%B' "${BASE_REF}..HEAD")
+ if grep -qF '[skip-regression-check]' <<< "$COMMIT_BODIES"; then
+ echo "[skip-regression-check] found in commit message — skipping."
+ exit 0
+ fi
+
+ # --- 3. Exempt static-only / docs-only changes ---
+ CHANGED_FILES=$(git diff --name-only "${BASE_REF}...HEAD")
+
+ if [ -z "$CHANGED_FILES" ]; then
+ echo "No changed files — skipping."
+ exit 0
+ fi
+
+ ALL_EXEMPT=true
+ while IFS= read -r file; do
+ case "$file" in
+ src/channels/web/static/*) ;;
+ *.md) ;;
+ *) ALL_EXEMPT=false; break ;;
+ esac
+ done <<< "$CHANGED_FILES"
+
+ if [ "$ALL_EXEMPT" = true ]; then
+ echo "All changes are static assets or docs — skipping."
+ exit 0
+ fi
+
+ # --- 4. Look for test changes ---
+
+ # Fast path: new test attributes or test modules in added lines.
+ if git diff "${BASE_REF}...HEAD" -U0 -- '*.rs' | grep -qE '^\+.*(#\[test\]|#\[tokio::test\]|#\[cfg\(test\)\]|mod tests)'; then
+ echo "Test changes found in .rs files."
+ exit 0
+ fi
+
+ # Whole-function context: detect edits inside existing test functions.
+ if git diff "${BASE_REF}...HEAD" -W -- '*.rs' | awk '
+ /^@@/ { if (has_test && has_add) { found=1; exit } has_test=0; has_add=0 }
+ /^ .*#\[test\]/ || /^ .*#\[tokio::test\]/ || /^ .*#\[cfg\(test\)\]/ || /^ .*mod tests/ { has_test=1 }
+ /^\+.*#\[test\]/ || /^\+.*#\[tokio::test\]/ || /^\+.*#\[cfg\(test\)\]/ || /^\+.*mod tests/ { has_test=1 }
+ /^\+[^+]/ { has_add=1 }
+ END { if (has_test && has_add) found=1; exit !found }
+ '; then
+ echo "Test changes found in existing test functions."
+ exit 0
+ fi
+
+ if grep -qE '^tests/' <<< "$CHANGED_FILES"; then
+ echo "Test file changes found under tests/."
+ exit 0
+ fi
+
+ # --- 5. No tests found ---
+ echo "::warning::This PR looks like a bug fix but contains no test changes. Every fix should include a regression test. Add a #[test] or #[tokio::test], or apply the 'skip-regression-check' label if not feasible."
+ exit 1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0be3140a..34eb554d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -89,10 +89,12 @@ jobs:
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
- # Let the initial task tell us to not run (currently very blunt)
+ # Wait for WASM extensions so we can patch manifests with SHA256 checksums
+ # before build.rs bakes them into the embedded catalog.
needs:
- plan
- if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
+ - build-wasm-extensions
+ if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') && (needs.build-wasm-extensions.result == 'skipped' || needs.build-wasm-extensions.result == 'success') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
@@ -139,6 +141,28 @@ jobs:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
+ - name: Patch manifests with WASM checksums
+ if: ${{ needs.plan.outputs.publishing == 'true' }}
+ shell: bash
+ run: |
+ CHECKSUMS="target/distrib/checksums.txt"
+ if [ ! -f "$CHECKSUMS" ]; then
+ echo "No checksums.txt found, skipping manifest patching"
+ exit 0
+ fi
+
+ while IFS= read -r line; do
+ sha256=$(echo "$line" | awk '{print $1}')
+ filename=$(echo "$line" | awk '{print $2}')
+ name=$(echo "$filename" | sed 's/-wasm32-wasip2\.tar\.gz$//')
+
+ for manifest in registry/tools/${name}.json registry/channels/${name}.json; do
+ if [ -f "$manifest" ]; then
+ jq --arg sha "$sha256" '.artifacts["wasm32-wasip2"].sha256 = $sha' "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
+ echo "Patched $manifest with sha256=$sha256"
+ fi
+ done
+ done < "$CHECKSUMS"
- name: Install dependencies
run: |
${{ matrix.packages_install }}
@@ -214,14 +238,113 @@ jobs:
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
+ # Build WASM extension bundles (tar.gz with .wasm + .capabilities.json)
+ build-wasm-extensions:
+ needs:
+ - plan
+ if: ${{ needs.plan.outputs.publishing == 'true' }}
+ runs-on: "ubuntu-22.04"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ submodules: recursive
+ - name: Install Rust toolchain + wasm target
+ run: |
+ rustup target add wasm32-wasip2
+ cargo install cargo-component --locked || true
+ - uses: swatinem/rust-cache@v2
+ with:
+ key: wasm-extensions
+ - name: Build and package WASM extensions
+ shell: bash
+ run: |
+ set -euo pipefail
+ mkdir -p target/wasm-bundles
+
+ # Process each manifest in registry/tools/ and registry/channels/
+ for manifest in registry/tools/*.json registry/channels/*.json; do
+ [ -f "$manifest" ] || continue
+
+ name=$(jq -r '.name' "$manifest")
+ source_dir=$(jq -r '.source.dir' "$manifest")
+ caps_file=$(jq -r '.source.capabilities' "$manifest")
+ crate_name=$(jq -r '.source.crate_name' "$manifest")
+
+ if [ ! -d "$source_dir" ]; then
+ echo "::warning::Source dir '$source_dir' not found for '$name', skipping"
+ continue
+ fi
+
+ echo "=== Building $name from $source_dir ==="
+
+ # Build WASM component
+ cargo component build --release --manifest-path "$source_dir/Cargo.toml" || {
+ echo "::warning::Build failed for '$name', skipping"
+ continue
+ }
+
+ # Find the built WASM file (Cargo uses underscores in artifact names)
+ wasm_artifact="${crate_name//-/_}"
+ wasm_path=""
+ for target_dir in wasm32-wasip2 wasm32-wasip1 wasm32-wasi; do
+ candidate="$source_dir/target/$target_dir/release/${wasm_artifact}.wasm"
+ if [ -f "$candidate" ]; then
+ wasm_path="$candidate"
+ break
+ fi
+ done
+
+ if [ -z "$wasm_path" ]; then
+ echo "::warning::No WASM output found for '$name', skipping"
+ continue
+ fi
+
+ # Copy files with standardized names for the archive
+ cp "$wasm_path" "target/wasm-bundles/${name}.wasm"
+
+ caps_path="$source_dir/$caps_file"
+ if [ -f "$caps_path" ]; then
+ cp "$caps_path" "target/wasm-bundles/${name}.capabilities.json"
+ else
+ echo "::warning::No capabilities file at '$caps_path' for '$name'"
+ fi
+
+ # Create tar.gz bundle
+ bundle="target/wasm-bundles/${name}-wasm32-wasip2.tar.gz"
+ (cd target/wasm-bundles && if [ -f "${name}.capabilities.json" ]; then tar czf "${name}-wasm32-wasip2.tar.gz" "${name}.wasm" "${name}.capabilities.json"; else tar czf "${name}-wasm32-wasip2.tar.gz" "${name}.wasm"; fi)
+
+ # Compute SHA256
+ sha256=$(sha256sum "$bundle" | cut -d' ' -f1)
+ echo "$sha256 ${name}-wasm32-wasip2.tar.gz" >> target/wasm-bundles/checksums.txt
+
+ # Clean up intermediate files
+ rm -f "target/wasm-bundles/${name}.wasm" "target/wasm-bundles/${name}.capabilities.json"
+
+ echo " -> $bundle ($sha256)"
+ done
+
+ echo "=== WASM bundles built ==="
+ ls -la target/wasm-bundles/
+ - name: "Upload WASM bundles"
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts-wasm-extensions
+ path: |
+ target/wasm-bundles/*.tar.gz
+ target/wasm-bundles/checksums.txt
+
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
- # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
- if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
+ - build-wasm-extensions
+ # Only run if we're "publishing", and only if plan, local, global, and wasm didn't fail (skipped is fine)
+ if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') && (needs.build-wasm-extensions.result == 'skipped' || needs.build-wasm-extensions.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
@@ -281,6 +404,69 @@ jobs:
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
+ # Commit patched manifest SHA256 checksums back to main so the repo
+ # stays in sync with the released artifacts.
+ update-registry-checksums:
+ needs:
+ - plan
+ - host
+ - build-wasm-extensions
+ if: ${{ always() && needs.host.result == 'success' && needs.build-wasm-extensions.result == 'success' }}
+ runs-on: "ubuntu-22.04"
+ permissions:
+ contents: write
+ pull-requests: write
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: main
+ - name: Fetch WASM checksums
+ uses: actions/download-artifact@v4
+ with:
+ name: artifacts-wasm-extensions
+ path: target/wasm-bundles/
+ - name: Patch manifests with SHA256
+ shell: bash
+ run: |
+ CHECKSUMS="target/wasm-bundles/checksums.txt"
+ if [ ! -f "$CHECKSUMS" ]; then
+ echo "No checksums.txt found"
+ exit 0
+ fi
+
+ while IFS= read -r line; do
+ sha256=$(echo "$line" | awk '{print $1}')
+ filename=$(echo "$line" | awk '{print $2}')
+ name=$(echo "$filename" | sed 's/-wasm32-wasip2\.tar\.gz$//')
+
+ for manifest in registry/tools/${name}.json registry/channels/${name}.json; do
+ if [ -f "$manifest" ]; then
+ jq --arg sha "$sha256" '.artifacts["wasm32-wasip2"].sha256 = $sha' "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
+ echo "Patched $manifest with sha256=$sha256"
+ fi
+ done
+ done < "$CHECKSUMS"
+ - name: Create PR with updated manifests
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add registry/
+ if git diff --cached --quiet; then
+ echo "No manifest changes to commit"
+ else
+ BRANCH="chore/update-checksums-$(date +%s)"
+ git checkout -b "$BRANCH"
+ git commit -m "chore: update WASM artifact SHA256 checksums [skip ci]"
+ git push origin "$BRANCH"
+ gh pr create \
+ --title "chore: update WASM artifact SHA256 checksums" \
+ --body "Auto-generated by release CI. Updates SHA256 checksums in registry manifests to match the released WASM artifacts." \
+ --base main \
+ --head "$BRANCH"
+ fi
+
announce:
needs:
- plan
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 13fc8410..8f0fd2bb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,7 +7,73 @@ on:
jobs:
tests:
- name: Run Tests
+ name: Tests (${{ matrix.name }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: all-features
+ flags: "--features postgres,libsql,html-to-markdown"
+ - name: default
+ flags: ""
+ - name: libsql-only
+ flags: "--no-default-features --features libsql"
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: wasm32-wasip2
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: ${{ matrix.name }}
+ - name: Install cargo-component
+ run: cargo install cargo-component --locked || true
+ - name: Build WASM channels (for integration tests)
+ run: ./scripts/build-wasm-extensions.sh --channels
+ - name: Run Tests
+ run: cargo test ${{ matrix.flags }} -- --nocapture
+
+ telegram-tests:
+ name: Telegram Channel Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ - uses: Swatinem/rust-cache@v2
+ - name: Run Telegram Channel Tests
+ run: cargo test --manifest-path channels-src/telegram/Cargo.toml -- --nocapture
+
+ windows-build:
+ name: Windows Build (${{ matrix.name }})
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: all-features
+ flags: "--all-features"
+ - name: default
+ flags: ""
+ - name: libsql-only
+ flags: "--no-default-features --features libsql"
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: windows-${{ matrix.name }}
+ - name: Check compilation
+ run: cargo check --all --benches --tests --examples ${{ matrix.flags }}
+
+ wasm-wit-compat:
+ name: WASM WIT Compatibility
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -15,7 +81,54 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
- profile: minimal
+ targets: wasm32-wasip2
- uses: Swatinem/rust-cache@v2
- - name: Run Tests
- run: cargo test --all-features -- --nocapture
+ with:
+ key: wasm-extensions
+ - name: Install cargo-component
+ run: cargo install cargo-component --locked || true
+ - name: Build all WASM extensions against current WIT
+ run: ./scripts/build-wasm-extensions.sh
+ - name: Instantiation test (host linker compatibility)
+ run: cargo test --all-features wit_compat -- --nocapture
+
+ docker-build:
+ name: Docker Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Build Docker image
+ run: docker build -t ironclaw-test:ci .
+
+ version-check:
+ name: Version Bump Check
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ - name: Check version bumps for changed extensions
+ env:
+ PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
+ run: ./scripts/check-version-bumps.sh
+
+ # Roll-up job for branch protection
+ run-tests:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ if: always()
+ needs: [tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check]
+ steps:
+ - run: |
+ if [[ "${{ needs.tests.result }}" != "success" || "${{ needs.telegram-tests.result }}" != "success" || "${{ needs.wasm-wit-compat.result }}" != "success" || "${{ needs.docker-build.result }}" != "success" || "${{ needs.windows-build.result }}" != "success" ]]; then
+ echo "One or more jobs failed"
+ exit 1
+ fi
+ # version-check only runs on PRs, so skip/success are both acceptable
+ if [[ "${{ needs.version-check.result }}" == "failure" ]]; then
+ echo "Version bump check failed"
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
index 8b12dcb8..17bdb86d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,9 @@
.env.*
!.env.example
-# Claude Code worktrees
+# Claude Code worktrees and lock files
.claude/worktrees/
+.claude/scheduled_tasks.lock
# Sidecar tool data
.sidecar/
@@ -16,6 +17,10 @@ target/
# Benchmark results (local runs, not committed)
bench-results/
+# Coverage reports (local runs, not committed)
+/coverage/
+
# WASM build artifacts (loaded from disk, not bundled)
*.wasm
+trace_*.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8744f15..5f51e62b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,321 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.16.1](https://github.com/nearai/ironclaw/compare/v0.16.0...v0.16.1) - 2026-03-06
+
+### Fixed
+
+- revert WASM artifact SHA256 checksums to null ([#627](https://github.com/nearai/ironclaw/pull/627))
+
+## [0.16.0](https://github.com/nearai/ironclaw/compare/v0.15.0...v0.16.0) - 2026-03-06
+
+### Added
+
+- *(e2e)* extensions tab tests, CI parallelization, and 3 production bug fixes ([#584](https://github.com/nearai/ironclaw/pull/584))
+- WASM extension versioning with WIT compat checks ([#592](https://github.com/nearai/ironclaw/pull/592))
+- Add HMAC-SHA256 webhook signature validation for Slack ([#588](https://github.com/nearai/ironclaw/pull/588))
+- restart ([#531](https://github.com/nearai/ironclaw/pull/531))
+- merge http/web_fetch tools, add tool output stash for large responses ([#578](https://github.com/nearai/ironclaw/pull/578))
+- integrate 13-dimension complexity scorer into smart routing ([#529](https://github.com/nearai/ironclaw/pull/529))
+
+### Fixed
+
+- *(llm)* fix reasoning model response parsing bugs ([#564](https://github.com/nearai/ironclaw/pull/564)) ([#580](https://github.com/nearai/ironclaw/pull/580))
+- *(ci)* fix three coverage workflow failures ([#597](https://github.com/nearai/ironclaw/pull/597))
+- Telegram channel accepts group messages from all users if owner_… ([#590](https://github.com/nearai/ironclaw/pull/590))
+- *(ci)* anchor coverage/ gitignore rule to repo root ([#591](https://github.com/nearai/ironclaw/pull/591))
+- *(security)* use OsRng for all security-critical key and token generation ([#519](https://github.com/nearai/ironclaw/pull/519))
+- prevent concurrent memory hygiene passes and Windows file lock errors ([#535](https://github.com/nearai/ironclaw/pull/535))
+- sort tool_definitions() for deterministic LLM tool ordering ([#582](https://github.com/nearai/ironclaw/pull/582))
+- *(ci)* persist all cargo-llvm-cov env vars for E2E coverage ([#559](https://github.com/nearai/ironclaw/pull/559))
+
+### Other
+
+- *(llm)* complete response cache — set_model invalidation, stats logging, sync mutex ([#290](https://github.com/nearai/ironclaw/pull/290))
+- add 29 E2E trace tests for issues #571-575 ([#593](https://github.com/nearai/ironclaw/pull/593))
+- add 26 tests for multi-thread safety, db CRUD, concurrency, errors ([#442](https://github.com/nearai/ironclaw/pull/442))
+- update WASM artifact SHA256 checksums [skip ci] ([#560](https://github.com/nearai/ironclaw/pull/560))
+- add WIT compatibility tests for WASM extensions ([#586](https://github.com/nearai/ironclaw/pull/586))
+- Trajectory benchmarks and e2e trace test rig ([#553](https://github.com/nearai/ironclaw/pull/553))
+
+## [0.15.0](https://github.com/nearai/ironclaw/compare/v0.14.0...v0.15.0) - 2026-03-04
+
+### Added
+
+- *(oauth)* route callbacks through web gateway for hosted instances ([#555](https://github.com/nearai/ironclaw/pull/555))
+- *(web)* show error details for failed tool calls ([#490](https://github.com/nearai/ironclaw/pull/490))
+- *(extensions)* improve auth UX and add load-time validation ([#536](https://github.com/nearai/ironclaw/pull/536))
+- add local-test skill and Dockerfile.test for web gateway testing ([#524](https://github.com/nearai/ironclaw/pull/524))
+
+### Fixed
+
+- *(security)* restrict query-token auth to SSE endpoints only ([#528](https://github.com/nearai/ironclaw/pull/528))
+- *(ci)* flush profraw coverage data in E2E teardown ([#550](https://github.com/nearai/ironclaw/pull/550))
+- *(wasm)* coerce string parameters to schema-declared types ([#498](https://github.com/nearai/ironclaw/pull/498))
+- *(agent)* strip leaked [Called tool ...] text from responses ([#497](https://github.com/nearai/ironclaw/pull/497))
+- *(web)* reset job list UI on restart failure ([#499](https://github.com/nearai/ironclaw/pull/499))
+- *(security)* replace .unwrap() panics in pairing store with proper error handling ([#515](https://github.com/nearai/ironclaw/pull/515))
+
+### Other
+
+- Fix UTF-8 unsafe truncation in sandbox log capture ([#359](https://github.com/nearai/ironclaw/pull/359))
+- enhance coverage with feature matrix, postgres, and E2E ([#523](https://github.com/nearai/ironclaw/pull/523))
+
+## [0.14.0](https://github.com/nearai/ironclaw/compare/v0.13.1...v0.14.0) - 2026-03-04
+
+### Added
+
+- remove the okta tool ([#506](https://github.com/nearai/ironclaw/pull/506))
+- add OAuth support for WASM tools in web gateway ([#489](https://github.com/nearai/ironclaw/pull/489))
+- *(web)* fix jobs UI parity for non-sandbox mode ([#491](https://github.com/nearai/ironclaw/pull/491))
+- *(workspace)* add TOOLS.md, BOOTSTRAP.md, and disk-to-DB import ([#477](https://github.com/nearai/ironclaw/pull/477))
+
+### Fixed
+
+- *(web)* mobile browser bar obscures chat input ([#508](https://github.com/nearai/ironclaw/pull/508))
+- *(web)* assign unique thread_id to manual routine triggers ([#500](https://github.com/nearai/ironclaw/pull/500))
+- *(web)* refresh routine UI after Run Now trigger ([#501](https://github.com/nearai/ironclaw/pull/501))
+- *(skills)* use slug for skill download URL from ClawHub ([#502](https://github.com/nearai/ironclaw/pull/502))
+- *(workspace)* thread document path through search results ([#503](https://github.com/nearai/ironclaw/pull/503))
+- *(workspace)* import custom templates before seeding defaults ([#505](https://github.com/nearai/ironclaw/pull/505))
+- use std::sync::RwLock in MessageTool to avoid runtime panic ([#411](https://github.com/nearai/ironclaw/pull/411))
+- wire secrets store into all WASM runtime activation paths ([#479](https://github.com/nearai/ironclaw/pull/479))
+
+### Other
+
+- enforce regression tests for fix commits ([#517](https://github.com/nearai/ironclaw/pull/517))
+- add code coverage with cargo-llvm-cov and Codecov ([#511](https://github.com/nearai/ironclaw/pull/511))
+- Remove restart infrastructure, generalize WASM channel setup ([#493](https://github.com/nearai/ironclaw/pull/493))
+
+## [0.13.1](https://github.com/nearai/ironclaw/compare/v0.13.0...v0.13.1) - 2026-03-02
+
+### Added
+
+- add Brave Web Search WASM tool ([#474](https://github.com/nearai/ironclaw/pull/474))
+
+### Fixed
+
+- *(web)* auto-scroll and Enter key completion for slash command autocomplete ([#475](https://github.com/nearai/ironclaw/pull/475))
+- correct download URLs for telegram-mtproto and slack-tool extensions ([#470](https://github.com/nearai/ironclaw/pull/470))
+
+## [0.13.0](https://github.com/nearai/ironclaw/compare/v0.12.0...v0.13.0) - 2026-03-02
+
+### Added
+
+- *(cli)* add tool setup command + GitHub setup schema ([#438](https://github.com/nearai/ironclaw/pull/438))
+- add web_fetch built-in tool ([#435](https://github.com/nearai/ironclaw/pull/435))
+- *(web)* DB-backed Jobs tab + scheduler-dispatched local jobs ([#436](https://github.com/nearai/ironclaw/pull/436))
+- *(extensions)* add OAuth setup UI for WASM tools + display name labels ([#437](https://github.com/nearai/ironclaw/pull/437))
+- *(bootstrap)* auto-detect libsql when ironclaw.db exists ([#399](https://github.com/nearai/ironclaw/pull/399))
+- *(web)* slash command autocomplete + /status /list + fix chat input locking ([#404](https://github.com/nearai/ironclaw/pull/404))
+- *(routines)* deliver notifications to all installed channels ([#398](https://github.com/nearai/ironclaw/pull/398))
+- *(web)* persist tool calls, restore approvals on thread switch, and UI fixes ([#382](https://github.com/nearai/ironclaw/pull/382))
+- add IRONCLAW_BASE_DIR env var with LazyLock caching ([#397](https://github.com/nearai/ironclaw/pull/397))
+- feat(signal) attachment upload + message tool ([#375](https://github.com/nearai/ironclaw/pull/375))
+
+### Fixed
+
+- *(channels)* add host-based credential injection to WASM channel wrapper ([#421](https://github.com/nearai/ironclaw/pull/421))
+- pre-validate Cloudflare tunnel token by spawning cloudflared ([#446](https://github.com/nearai/ironclaw/pull/446))
+- batch of quick fixes (#417, #338, #330, #358, #419, #344) ([#428](https://github.com/nearai/ironclaw/pull/428))
+- persist channel activation state across restarts ([#432](https://github.com/nearai/ironclaw/pull/432))
+- init WASM runtime eagerly regardless of tools directory existence ([#401](https://github.com/nearai/ironclaw/pull/401))
+- add TLS support for PostgreSQL connections ([#363](https://github.com/nearai/ironclaw/pull/363)) ([#427](https://github.com/nearai/ironclaw/pull/427))
+- scan inbound messages for leaked secrets ([#433](https://github.com/nearai/ironclaw/pull/433))
+- use tailscale funnel --bg for proper tunnel setup ([#430](https://github.com/nearai/ironclaw/pull/430))
+- normalize secret names to lowercase for case-insensitive matching ([#413](https://github.com/nearai/ironclaw/pull/413)) ([#431](https://github.com/nearai/ironclaw/pull/431))
+- persist model name to .env so dotted names survive restart ([#426](https://github.com/nearai/ironclaw/pull/426))
+- *(setup)* check cloudflared binary and validate tunnel token ([#424](https://github.com/nearai/ironclaw/pull/424))
+- *(setup)* validate PostgreSQL version and pgvector availability before migrations ([#423](https://github.com/nearai/ironclaw/pull/423))
+- guard zsh compdef call to prevent error before compinit ([#422](https://github.com/nearai/ironclaw/pull/422))
+- *(telegram)* remove restart button, validate token on setup ([#434](https://github.com/nearai/ironclaw/pull/434))
+- web UI routines tab shows all routines regardless of creating channel ([#391](https://github.com/nearai/ironclaw/pull/391))
+- Discord Ed25519 signature verification and capabilities header alias ([#148](https://github.com/nearai/ironclaw/pull/148)) ([#372](https://github.com/nearai/ironclaw/pull/372))
+- prevent duplicate WASM channel activation on startup ([#390](https://github.com/nearai/ironclaw/pull/390))
+
+### Other
+
+- rename WasmBuildable::repo_url to source_dir ([#445](https://github.com/nearai/ironclaw/pull/445))
+- Improve --help: add detailed about/examples/color, snapshot test (clo… ([#371](https://github.com/nearai/ironclaw/pull/371))
+- Add automated QA: schema validator, CI matrix, Docker build, and P1 test coverage ([#353](https://github.com/nearai/ironclaw/pull/353))
+
+## [0.12.0](https://github.com/nearai/ironclaw/compare/v0.11.1...v0.12.0) - 2026-02-26
+
+### Added
+
+- *(web)* improve WASM channel setup flow ([#380](https://github.com/nearai/ironclaw/pull/380))
+- *(web)* inline tool activity cards with auto-collapsing ([#376](https://github.com/nearai/ironclaw/pull/376))
+- *(web)* display logs newest-first in web gateway UI ([#369](https://github.com/nearai/ironclaw/pull/369))
+- *(signal)* tool approval workflow and status updates ([#350](https://github.com/nearai/ironclaw/pull/350))
+- add OpenRouter preset to setup wizard ([#270](https://github.com/nearai/ironclaw/pull/270))
+- *(channels)* add native Signal channel via signal-cli HTTP daemon ([#271](https://github.com/nearai/ironclaw/pull/271))
+
+### Fixed
+
+- correct MCP registry URLs and remove non-existent Google endpoints ([#370](https://github.com/nearai/ironclaw/pull/370))
+- resolve_thread adopts existing session threads by UUID ([#377](https://github.com/nearai/ironclaw/pull/377))
+- resolve telegram/slack name collision between tool and channel registries ([#346](https://github.com/nearai/ironclaw/pull/346))
+- make onboarding installs prefer release artifacts with source fallback ([#323](https://github.com/nearai/ironclaw/pull/323))
+- copy missing files in Dockerfile to fix build ([#322](https://github.com/nearai/ironclaw/pull/322))
+- fall back to build-from-source when extension download fails ([#312](https://github.com/nearai/ironclaw/pull/312))
+
+### Other
+
+- Add --version flag with clap built-in support and test ([#342](https://github.com/nearai/ironclaw/pull/342))
+- Update FEATURE_PARITY.md ([#337](https://github.com/nearai/ironclaw/pull/337))
+- add brew install ironclaw instructions ([#310](https://github.com/nearai/ironclaw/pull/310))
+- Fix skills system: enable by default, fix registry and install ([#300](https://github.com/nearai/ironclaw/pull/300))
+
+## [0.11.1](https://github.com/nearai/ironclaw/compare/v0.11.0...v0.11.1) - 2026-02-23
+
+### Other
+
+- Ignore out-of-date generated CI so custom release.yml jobs are allowed
+
+## [0.11.0](https://github.com/nearai/ironclaw/compare/v0.10.0...v0.11.0) - 2026-02-23
+
+### Fixed
+
+- auto-compact and retry on ContextLengthExceeded ([#315](https://github.com/nearai/ironclaw/pull/315))
+
+### Other
+
+- *(README)* Adding badges to readme ([#316](https://github.com/nearai/ironclaw/pull/316))
+- Feat/completion ([#240](https://github.com/nearai/ironclaw/pull/240))
+
+## [0.10.0](https://github.com/nearai/ironclaw/compare/v0.9.0...v0.10.0) - 2026-02-22
+
+### Added
+
+- update dashboard favicon ([#309](https://github.com/nearai/ironclaw/pull/309))
+- add web UI test skill for Chrome extension ([#302](https://github.com/nearai/ironclaw/pull/302))
+- implement FullJob routine mode with scheduler dispatch ([#288](https://github.com/nearai/ironclaw/pull/288))
+- hot-activate WASM channels, channel-first prompts, unified artifact resolution ([#297](https://github.com/nearai/ironclaw/pull/297))
+- add pairing/permission system to all WASM channels and fix extension registry ([#286](https://github.com/nearai/ironclaw/pull/286))
+- group chat privacy, channel-aware prompts, and safety hardening ([#285](https://github.com/nearai/ironclaw/pull/285))
+- embedded registry catalog and WASM bundle install pipeline ([#283](https://github.com/nearai/ironclaw/pull/283))
+- show token usage and cost tracker in gateway status popover ([#284](https://github.com/nearai/ironclaw/pull/284))
+- support custom HTTP headers for OpenAI-compatible provider ([#269](https://github.com/nearai/ironclaw/pull/269))
+- add smart routing provider for cost-optimized model selection ([#281](https://github.com/nearai/ironclaw/pull/281))
+
+### Fixed
+
+- persist user message at turn start before agentic loop ([#305](https://github.com/nearai/ironclaw/pull/305))
+- block send until thread is selected ([#306](https://github.com/nearai/ironclaw/pull/306))
+- reload chat history on SSE reconnect ([#307](https://github.com/nearai/ironclaw/pull/307))
+- map Esc to interrupt and Ctrl+C to graceful quit ([#267](https://github.com/nearai/ironclaw/pull/267))
+
+### Other
+
+- Fix tool schema OpenAI compatibility ([#301](https://github.com/nearai/ironclaw/pull/301))
+- simplify config resolution and consolidate main.rs init ([#287](https://github.com/nearai/ironclaw/pull/287))
+- Update image source in README.md
+- Add files via upload
+- remove ExtensionSource::Bundled, use download-only install for WASM channels ([#293](https://github.com/nearai/ironclaw/pull/293))
+- allow OAuth callback to work on remote servers (fixes #186) ([#212](https://github.com/nearai/ironclaw/pull/212))
+- add rate limiting for built-in tools (closes #171) ([#276](https://github.com/nearai/ironclaw/pull/276))
+- add LLM providers guide (OpenRouter, Together AI, Fireworks, Ollama, vLLM) ([#193](https://github.com/nearai/ironclaw/pull/193))
+- Feat/html to markdown #106 ([#115](https://github.com/nearai/ironclaw/pull/115))
+- adopt agent-market design language for web UI ([#282](https://github.com/nearai/ironclaw/pull/282))
+- speed up startup from ~15s to ~2s ([#280](https://github.com/nearai/ironclaw/pull/280))
+- consolidate tool approval into single param-aware method ([#274](https://github.com/nearai/ironclaw/pull/274))
+
+## [0.9.0](https://github.com/nearai/ironclaw/compare/v0.8.0...v0.9.0) - 2026-02-21
+
+### Added
+
+- add TEE attestation shield to web gateway UI ([#275](https://github.com/nearai/ironclaw/pull/275))
+- configurable tool iterations, auto-approve, and policy fix ([#251](https://github.com/nearai/ironclaw/pull/251))
+
+### Fixed
+
+- add X-Accel-Buffering header to SSE endpoints ([#277](https://github.com/nearai/ironclaw/pull/277))
+
+## [0.8.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.7.0...ironclaw-v0.8.0) - 2026-02-20
+
+### Added
+
+- extension registry with metadata catalog and onboarding integration ([#238](https://github.com/nearai/ironclaw/pull/238))
+- *(models)* add GPT-5.3 Codex, full GPT-5.x family, Claude 4.x series, o4-mini ([#197](https://github.com/nearai/ironclaw/pull/197))
+- wire memory hygiene into the heartbeat loop ([#195](https://github.com/nearai/ironclaw/pull/195))
+
+### Fixed
+
+- persist WASM channel workspace writes across callbacks ([#264](https://github.com/nearai/ironclaw/pull/264))
+- consolidate per-module ENV_MUTEX into crate-wide test lock ([#246](https://github.com/nearai/ironclaw/pull/246))
+- remove auto-proceed fake user message injection from agent loop ([#255](https://github.com/nearai/ironclaw/pull/255))
+- onboarding errors reset flow and remote server auth (#185, #186) ([#248](https://github.com/nearai/ironclaw/pull/248))
+- parallelize tool call execution via JoinSet ([#219](https://github.com/nearai/ironclaw/pull/219)) ([#252](https://github.com/nearai/ironclaw/pull/252))
+- prevent pipe deadlock in shell command execution ([#140](https://github.com/nearai/ironclaw/pull/140))
+- persist turns after approval and add agent-level tests ([#250](https://github.com/nearai/ironclaw/pull/250))
+
+### Other
+
+- add automated PR labeling system ([#253](https://github.com/nearai/ironclaw/pull/253))
+- update CLAUDE.md for recently merged features ([#183](https://github.com/nearai/ironclaw/pull/183))
+
+## [0.7.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.6.0...ironclaw-v0.7.0) - 2026-02-19
+
+### Added
+
+- extend lifecycle hooks with declarative bundles ([#176](https://github.com/nearai/ironclaw/pull/176))
+- support per-request model override in /v1/chat/completions ([#103](https://github.com/nearai/ironclaw/pull/103))
+
+### Fixed
+
+- harden openai-compatible provider, approval replay, and embeddings defaults ([#237](https://github.com/nearai/ironclaw/pull/237))
+- Network Security Findings ([#201](https://github.com/nearai/ironclaw/pull/201))
+
+### Added
+
+- Refactored OpenAI-compatible chat completion routing to use the rig adapter and `RetryProvider` composition for custom base URL usage.
+- Added Ollama embeddings provider support (`EMBEDDING_PROVIDER=ollama`, `OLLAMA_BASE_URL`) in workspace embeddings.
+- Added migration `V9__flexible_embedding_dimension.sql` for flexible embedding vector dimensions.
+
+### Changed
+
+- Changed default sandbox image to `ironclaw-worker:latest` in config/settings/sandbox defaults.
+- Improved tool-message sanitization and provider compatibility handling across NEAR AI, rig adapter, and shared LLM provider code.
+
+### Fixed
+
+- Fixed approval-input aliases (`a`, `/approve`, `/always`, `/deny`, etc.) in submission parsing.
+- Fixed multi-tool approval resume flow by preserving and replaying deferred tool calls so all prior `tool_use` IDs receive matching `tool_result` messages.
+- Fixed REPL quit/exit handling to route shutdown through the agent loop for graceful termination.
+
+## [0.6.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.5.0...ironclaw-v0.6.0) - 2026-02-19
+
+### Added
+
+- add issue triage skill ([#200](https://github.com/nearai/ironclaw/pull/200))
+- add PR triage dashboard skill ([#196](https://github.com/nearai/ironclaw/pull/196))
+- add OpenRouter usage examples ([#189](https://github.com/nearai/ironclaw/pull/189))
+- add Tinfoil private inference provider ([#62](https://github.com/nearai/ironclaw/pull/62))
+- shell env scrubbing and command injection detection ([#164](https://github.com/nearai/ironclaw/pull/164))
+- Add PR review tools, job monitor, and channel injection for E2E sandbox workflows ([#57](https://github.com/nearai/ironclaw/pull/57))
+- Secure prompt-based skills system (Phases 1-4) ([#51](https://github.com/nearai/ironclaw/pull/51))
+- Add benchmarking harness with spot suite ([#10](https://github.com/nearai/ironclaw/pull/10))
+- 10 infrastructure improvements from zeroclaw ([#126](https://github.com/nearai/ironclaw/pull/126))
+
+### Fixed
+
+- *(rig)* prevent OpenAI Responses API panic on tool call IDs ([#182](https://github.com/nearai/ironclaw/pull/182))
+- *(docs)* correct settings storage path in README ([#194](https://github.com/nearai/ironclaw/pull/194))
+- OpenAI tool calling — schema normalization, missing types, and Responses API panic ([#132](https://github.com/nearai/ironclaw/pull/132))
+- *(security)* prevent path traversal bypass in WASM HTTP allowlist ([#137](https://github.com/nearai/ironclaw/pull/137))
+- persist OpenAI-compatible provider and respect embeddings disable ([#177](https://github.com/nearai/ironclaw/pull/177))
+- remove .expect() calls in FailoverProvider::try_providers ([#156](https://github.com/nearai/ironclaw/pull/156))
+- sentinel value collision in FailoverProvider cooldown ([#125](https://github.com/nearai/ironclaw/pull/125)) ([#154](https://github.com/nearai/ironclaw/pull/154))
+- skills module audit cleanup ([#173](https://github.com/nearai/ironclaw/pull/173))
+
+### Other
+
+- Fix division by zero panic in ValueEstimator::is_profitable ([#139](https://github.com/nearai/ironclaw/pull/139))
+- audit feature parity matrix against codebase and recent commits ([#202](https://github.com/nearai/ironclaw/pull/202))
+- architecture improvements for contributor velocity ([#198](https://github.com/nearai/ironclaw/pull/198))
+- fix rustfmt formatting from PR #137
+- add .env.example examples for Ollama and OpenAI-compatible ([#110](https://github.com/nearai/ironclaw/pull/110))
+
## [0.5.0](https://github.com/nearai/ironclaw/compare/v0.4.0...v0.5.0) - 2026-02-17
### Added
@@ -61,6 +376,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump MSRV to 1.92, add GCP deployment files ([#40](https://github.com/nearai/ironclaw/pull/40))
- Add OpenAI-compatible HTTP API (/v1/chat/completions, /v1/models) ([#31](https://github.com/nearai/ironclaw/pull/31))
+
## [0.1.3](https://github.com/nearai/ironclaw/compare/v0.1.2...v0.1.3) - 2026-02-12
### Other
diff --git a/CLAUDE.md b/CLAUDE.md
index ff15ebbf..249bc903 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,14 +13,17 @@
### Features
- **Multi-channel input**: TUI (Ratatui), HTTP webhooks, WASM channels (Telegram, Slack), web gateway
- **Parallel job execution** with state machine and self-repair for stuck jobs
-- **Sandbox execution**: Docker container isolation with orchestrator/worker pattern
+- **Sandbox execution**: Docker container isolation with network proxy and credential injection
- **Claude Code mode**: Delegate jobs to Claude CLI inside containers
+- **Skills system**: SKILL.md prompt extensions with trust model, tool attenuation, and ClawHub registry
- **Routines**: Scheduled (cron) and reactive (event, webhook) task execution
- **Web gateway**: Browser UI with SSE/WebSocket real-time streaming
- **Extension management**: Install, auth, activate MCP/WASM extensions
- **Extensible tools**: Built-in tools, WASM sandbox, MCP client, dynamic builder
- **Persistent memory**: Workspace with hybrid search (FTS + vector via RRF)
-- **Prompt injection defense**: Sanitizer, validator, policy rules, leak detection
+- **Prompt injection defense**: Sanitizer, validator, policy rules, leak detection, shell env scrubbing
+- **Multi-provider LLM**: NEAR AI, OpenAI, Anthropic, Ollama, OpenAI-compatible, Tinfoil private inference
+- **Setup wizard**: 7-step interactive onboarding for first-run configuration
- **Heartbeat system**: Proactive periodic execution with checklist
## Build & Test
@@ -29,7 +32,7 @@
# Format code
cargo fmt
-# Lint (address warnings before committing)
+# Lint (fix ALL warnings before committing, including pre-existing ones)
cargo clippy --all --benches --tests --examples --all-features
# Run all tests
@@ -40,33 +43,53 @@ cargo test test_name
# Run with logging
RUST_LOG=ironclaw=debug cargo run
+
+# Run integration tests (may require running services/DB)
+cargo test --test workspace_integration
+cargo test --test ws_gateway_integration
+cargo test --test heartbeat_integration
+
+# Run E2E tests (Python/Playwright — requires a running ironclaw instance)
+# See tests/e2e/CLAUDE.md for full setup instructions
+cd tests/e2e
+python -m venv .venv && source .venv/bin/activate # On Windows: .venv\Scripts\activate
+pip install -e .
+playwright install chromium
+pytest scenarios/ # all scenarios
+pytest scenarios/test_chat.py # specific scenario
```
+### Test Tiers
+
+| Tier | Command | What runs | External deps |
+|------|---------|-----------|---------------|
+| Unit | `cargo test` | All `mod tests` + self-contained integration tests | None |
+| Integration | `cargo test --features integration` | + PostgreSQL-dependent tests | Running PostgreSQL |
+| Live | `cargo test --features integration -- --ignored` | + LLM-dependent tests | PostgreSQL + LLM API keys |
+
+Run `bash scripts/check-boundaries.sh` to verify test tier gating and other architecture rules.
+
## Project Structure
```
src/
├── lib.rs # Library root, module declarations
├── main.rs # Entry point, CLI args, startup
-├── config.rs # Configuration from env vars
+├── app.rs # App startup orchestration (channel wiring, DB init)
+├── bootstrap.rs # Base directory resolution (~/.ironclaw), early .env loading
+├── settings.rs # User settings persistence (~/.ironclaw/settings.json)
+├── service.rs # OS service management (launchd/systemd daemon install)
+├── tracing_fmt.rs # Custom tracing formatter
+├── util.rs # Shared utilities
+├── config/ # Configuration from env vars (split by subsystem)
+│ ├── mod.rs # Re-exports all config types; top-level Config struct
+│ ├── agent.rs, llm.rs, channels.rs, database.rs, sandbox.rs, skills.rs
+│ ├── heartbeat.rs, routines.rs, safety.rs, embeddings.rs, wasm.rs
+│ ├── tunnel.rs # Tunnel provider config (TUNNEL_PROVIDER, TUNNEL_URL, etc.)
+│ └── secrets.rs, hygiene.rs, builder.rs, helpers.rs
├── error.rs # Error types (thiserror)
│
-├── agent/ # Core agent logic
-│ ├── agent_loop.rs # Main Agent struct, message handling loop
-│ ├── router.rs # MessageIntent classification
-│ ├── scheduler.rs # Parallel job scheduling
-│ ├── worker.rs # Per-job execution with LLM reasoning
-│ ├── self_repair.rs # Stuck job detection and recovery
-│ ├── heartbeat.rs # Proactive periodic execution
-│ ├── session.rs # Session/thread/turn model with state machine
-│ ├── session_manager.rs # Thread/session lifecycle management
-│ ├── compaction.rs # Context window management with turn summarization
-│ ├── context_monitor.rs # Memory pressure detection
-│ ├── undo.rs # Turn-based undo/redo with checkpoints
-│ ├── submission.rs # Submission parsing (undo, redo, compact, clear, etc.)
-│ ├── task.rs # Sub-task execution framework
-│ ├── routine.rs # Routine types (Trigger, Action, Guardrails)
-│ └── routine_engine.rs # Routine execution (cron ticker, event matcher)
+├── agent/ # Core agent loop, dispatcher, scheduler, sessions — see src/agent/CLAUDE.md
│
├── channels/ # Multi-channel input
│ ├── channel.rs # Channel trait, IncomingMessage, OutgoingResponse
@@ -79,21 +102,60 @@ src/
│ │ ├── overlay.rs # Approval overlays
│ │ └── composer.rs # Message composition
│ ├── http.rs # HTTP webhook (axum) with secret validation
+│ ├── webhook_server.rs # Unified HTTP server composing all webhook routes
│ ├── repl.rs # Simple REPL (for testing)
-│ ├── web/ # Web gateway (browser UI)
-│ │ ├── mod.rs # Gateway builder, startup
-│ │ ├── server.rs # Axum router, 40+ API endpoints
-│ │ ├── sse.rs # SSE broadcast manager
-│ │ ├── ws.rs # WebSocket gateway + connection tracking
-│ │ ├── types.rs # Request/response types, SseEvent enum
-│ │ ├── auth.rs # Bearer token auth middleware
-│ │ ├── log_layer.rs # Tracing layer for log streaming
-│ │ └── static/ # HTML, CSS, JS (single-page app)
+│ ├── web/ # Web gateway (browser UI) — see src/channels/web/CLAUDE.md
│ └── wasm/ # WASM channel runtime
│ ├── mod.rs
│ ├── bundled.rs # Bundled channel discovery
+│ ├── capabilities.rs # Channel-specific capabilities (HTTP endpoint, emit rate)
+│ ├── error.rs # WASM channel error types
+│ ├── runtime.rs # WASM channel execution runtime
│ └── wrapper.rs # Channel trait wrapper for WASM modules
│
+├── cli/ # CLI subcommands (clap)
+│ ├── mod.rs # Cli struct, Command enum (run/onboard/config/tool/registry/mcp/memory/pairing/service/doctor/status/completion)
+│ ├── config.rs # config list/get/set subcommands
+│ ├── tool.rs # tool install/list/remove subcommands
+│ ├── registry.rs # registry list/install subcommands
+│ ├── mcp.rs # mcp add/auth/list/test subcommands
+│ ├── memory.rs # memory search/read/write subcommands
+│ ├── pairing.rs # pairing list/approve subcommands
+│ ├── service.rs # service install/start/stop subcommands
+│ ├── doctor.rs # Active health diagnostics
+│ ├── status.rs # System health/status display
+│ ├── completion.rs # Shell completion script generation
+│ └── oauth_defaults.rs # Default OAuth redirect URIs
+│
+├── registry/ # Extension registry catalog
+│ ├── mod.rs # Public API; re-exports RegistryCatalog, RegistryInstaller, manifest types
+│ ├── manifest.rs # ExtensionManifest, ArtifactSpec, BundleDefinition types
+│ ├── catalog.rs # RegistryCatalog: load from filesystem and embedded JSON
+│ ├── installer.rs # RegistryInstaller: download, verify, install WASM artifacts
+│ ├── artifacts.rs # Artifact download and caching
+│ └── embedded.rs # Catalog compiled into binary at build time (via build.rs)
+│
+├── hooks/ # Lifecycle hooks for intercepting agent operations
+│ ├── mod.rs # 6 HookPoints: BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse
+│ ├── hook.rs # Hook trait, HookContext, HookEvent, HookOutcome, HookFailureMode
+│ ├── registry.rs # HookRegistry: register, prioritize, execute hooks
+│ └── bundled.rs # Built-in hooks: rule-based filters, webhook forwarders, HookBundleConfig
+│
+├── tunnel/ # Tunnel abstraction for public internet exposure
+│ ├── mod.rs # Tunnel trait, TunnelProviderConfig, create_tunnel() factory
+│ ├── cloudflare.rs # CloudflareTunnel (cloudflared binary)
+│ ├── ngrok.rs # NgrokTunnel
+│ ├── tailscale.rs # TailscaleTunnel (serve/funnel modes)
+│ ├── custom.rs # CustomTunnel (arbitrary command with {host}/{port})
+│ └── none.rs # NoneTunnel (local-only, no exposure)
+│
+├── observability/ # Pluggable event/metric recording
+│ ├── mod.rs # create_observer() factory, ObservabilityConfig
+│ ├── traits.rs # Observer trait, ObserverEvent, ObserverMetric
+│ ├── noop.rs # NoopObserver (zero overhead, default)
+│ ├── log.rs # LogObserver (tracing-based)
+│ └── multi.rs # MultiObserver (fan-out to multiple backends)
+│
├── orchestrator/ # Internal HTTP API for sandbox containers
│ ├── mod.rs
│ ├── api.rs # Axum endpoints (LLM proxy, events, prompts)
@@ -111,26 +173,30 @@ src/
│ ├── sanitizer.rs # Pattern detection, content escaping
│ ├── validator.rs # Input validation (length, encoding, patterns)
│ ├── policy.rs # PolicyRule system with severity/actions
-│ └── leak_detector.rs # Secret detection (API keys, tokens, etc.)
+│ ├── leak_detector.rs # Secret detection (API keys, tokens, etc.)
+│ └── credential_detect.rs # HTTP request credential detection (headers, URL params)
│
-├── llm/ # LLM integration (NEAR AI only)
-│ ├── provider.rs # LlmProvider trait, message types
-│ ├── nearai.rs # NEAR AI chat-api implementation
-│ ├── reasoning.rs # Planning, tool selection, evaluation
-│ └── session.rs # Session token management with auto-renewal
+├── llm/ # Multi-provider LLM integration — see src/llm/CLAUDE.md
│
├── tools/ # Extensible tool system
│ ├── tool.rs # Tool trait, ToolOutput, ToolError
│ ├── registry.rs # ToolRegistry for discovery
│ ├── sandbox.rs # Process-based sandbox (stub, superseded by wasm/)
+│ ├── rate_limiter.rs # Shared sliding-window rate limiter for built-in and WASM tools
│ ├── builtin/ # Built-in tools
│ │ ├── echo.rs, time.rs, json.rs, http.rs
+│ │ ├── web_fetch.rs # GET URL → clean Markdown (readability + html-to-md conversion)
│ │ ├── file.rs # ReadFile, WriteFile, ListDir, ApplyPatch
│ │ ├── shell.rs # Shell command execution
│ │ ├── memory.rs # Memory tools (search, write, read, tree)
+│ │ ├── message.rs # MessageTool: agent proactively messages users on any channel
│ │ ├── job.rs # CreateJob, ListJobs, JobStatus, CancelJob
│ │ ├── routine.rs # routine_create/list/update/delete/history
│ │ ├── extension_tools.rs # Extension install/auth/activate/remove
+│ │ ├── skill_tools.rs # skill_list/search/install/remove tools
+│ │ ├── secrets_tools.rs # secret_list/secret_delete (zero-exposure: no values exposed)
+│ │ ├── html_converter.rs # HTML→Markdown via readability + html-to-markdown-rs
+│ │ ├── path_utils.rs # Shared path validation/canonicalization helpers
│ │ └── marketplace.rs, ecommerce.rs, taskrabbit.rs, restaurant.rs (stubs)
│ ├── builder/ # Dynamic tool building
│ │ ├── core.rs # BuildRequirement, SoftwareType, Language
@@ -139,7 +205,8 @@ src/
│ │ └── validation.rs # WASM validation
│ ├── mcp/ # Model Context Protocol
│ │ ├── client.rs # MCP client over HTTP
-│ │ └── protocol.rs # JSON-RPC types
+│ │ ├── protocol.rs # JSON-RPC types
+│ │ └── session.rs # MCP session management (Mcp-Session-Id header, per-server state)
│ └── wasm/ # Full WASM sandbox (wasmtime)
│ ├── runtime.rs # Module compilation and caching
│ ├── wrapper.rs # Tool trait wrapper for WASM modules
@@ -149,13 +216,10 @@ src/
│ ├── credential_injector.rs # Safe credential injection
│ ├── loader.rs # WASM tool discovery from filesystem
│ ├── rate_limiter.rs # Per-tool rate limiting
+│ ├── error.rs # WASM-specific error types
│ └── storage.rs # Linear memory persistence
│
-├── db/ # Database abstraction layer
-│ ├── mod.rs # Database trait (~60 async methods)
-│ ├── postgres.rs # PostgreSQL backend (delegates to Store + Repository)
-│ ├── libsql_backend.rs # libSQL/Turso backend (embedded SQLite)
-│ └── libsql_migrations.rs # SQLite-dialect schema (idempotent)
+├── db/ # Dual-backend persistence (PostgreSQL + libSQL) — see src/db/CLAUDE.md
│
├── workspace/ # Persistent memory system (OpenClaw-inspired)
│ ├── mod.rs # Workspace struct, memory operations
@@ -180,14 +244,48 @@ src/
│ ├── success.rs # SuccessEvaluator trait, RuleBasedEvaluator, LlmEvaluator
│ └── metrics.rs # MetricsCollector, QualityMetrics
│
+├── sandbox/ # Docker execution sandbox
+│ ├── mod.rs # Public API, default allowlist
+│ ├── config.rs # SandboxConfig, SandboxPolicy enum
+│ ├── manager.rs # SandboxManager orchestration
+│ ├── container.rs # ContainerRunner, Docker lifecycle
+│ ├── error.rs # SandboxError types
+│ └── proxy/ # Network proxy for containers
+│ ├── mod.rs # NetworkProxyBuilder
+│ ├── http.rs # HttpProxy, CredentialResolver trait
+│ ├── policy.rs # NetworkPolicyDecider trait
+│ └── allowlist.rs # DomainAllowlist validation
+│
├── secrets/ # Secrets management
+│ ├── mod.rs # SecretsStore trait, public API
+│ ├── types.rs # Core types (Secret, SecretRef, SecretMetadata)
│ ├── crypto.rs # AES-256-GCM encryption
-│ ├── store.rs # Secret storage
-│ └── types.rs # Credential types
+│ ├── keychain.rs # OS keychain integration (macOS Keychain, GNOME Keyring) for master key
+│ └── store.rs # Encrypted secret storage
+│
+├── setup/ # Onboarding wizard (spec: src/setup/README.md)
+│ ├── mod.rs # Entry point, check_onboard_needed()
+│ ├── wizard.rs # 7-step interactive wizard
+│ ├── channels.rs # Channel setup helpers
+│ └── prompts.rs # Terminal prompts (select, confirm, secret)
+│
+├── skills/ # SKILL.md prompt extension system
+│ ├── mod.rs # Core types (SkillTrust, LoadedSkill)
+│ ├── registry.rs # SkillRegistry: discover, install, remove
+│ ├── selector.rs # Deterministic scoring prefilter
+│ ├── attenuation.rs # Trust-based tool ceiling
+│ ├── gating.rs # Requirement checks (bins, env, config)
+│ ├── parser.rs # SKILL.md frontmatter + markdown parser
+│ └── catalog.rs # ClawHub registry client
│
└── history/ # Persistence
├── store.rs # PostgreSQL repositories
└── analytics.rs # Aggregation queries (JobStats, ToolStats)
+
+tests/
+├── *.rs # Integration tests (workspace, heartbeat, WS gateway, pairing, etc.)
+├── test-pages/ # HTML→Markdown conversion fixtures (CNN, Medium, Yahoo)
+└── e2e/ # Python/Playwright E2E scenarios (see tests/e2e/CLAUDE.md)
```
## Key Patterns
@@ -208,12 +306,16 @@ When designing new features or systems, always prefer generic/extensible archite
- Use `RwLock` for concurrent read/write access
### Traits for Extensibility
-- `Database` - Add new database backends (must implement all ~60 methods)
+- `Database` - Add new database backends (must implement all ~78 methods)
- `Channel` - Add new input sources
- `Tool` - Add new capabilities
- `LlmProvider` - Add new LLM backends
- `SuccessEvaluator` - Custom evaluation logic
- `EmbeddingProvider` - Add embedding backends (workspace search)
+- `NetworkPolicyDecider` - Custom network access policies for sandbox containers
+- `Hook` - Lifecycle hook at 6 interception points (BeforeInbound, BeforeToolCall, BeforeOutbound, OnSessionStart, OnSessionEnd, TransformResponse)
+- `Observer` - Observability backend (noop/log/multi; future: OpenTelemetry, Prometheus)
+- `Tunnel` - Tunnel provider for public internet exposure
### Tool Implementation
```rust
@@ -252,6 +354,61 @@ Pending -> InProgress -> Completed -> Submitted -> Accepted
\-> Failed
```
+### Code Style
+
+- Use `crate::` imports, not `super::`
+- No `pub use` re-exports unless exposing to downstream consumers
+- Prefer strong types over strings (enums, newtypes)
+- Keep functions focused, extract helpers when logic is reused
+- Comments for non-obvious logic only
+
+### Review & Fix Discipline
+
+Hard-won lessons from code review -- follow these when fixing bugs or addressing review feedback.
+
+**Fix the pattern, not just the instance:** When a reviewer flags a bug (e.g., TOCTOU race in INSERT + SELECT-back), search the entire codebase for all instances of that same pattern. A fix in `SecretsStore::create()` that doesn't also fix `WasmToolStore::store()` is half a fix.
+
+**Propagate architectural fixes to satellite types:** If a core type changes its concurrency model (e.g., `LibSqlBackend` switches to connection-per-operation), every type that was handed a resource from the old model (e.g., `LibSqlSecretsStore`, `LibSqlWasmToolStore` holding a single `Connection`) must also be updated. Grep for the old type across the codebase.
+
+**Schema translation is more than DDL:** When translating a database schema between backends (PostgreSQL to libSQL, etc.), check for:
+- **Indexes** -- diff `CREATE INDEX` statements between the two schemas
+- **Seed data** -- check for `INSERT INTO` in migrations (e.g., `leak_detection_patterns`)
+- **Semantic differences** -- document where SQL functions behave differently (e.g., `json_patch` vs `jsonb_set`)
+
+**Feature flag testing:** When adding feature-gated code, test compilation with each feature in isolation:
+```bash
+cargo check # default features
+cargo check --no-default-features --features libsql # libsql only
+cargo check --all-features # all features
+```
+Dead code behind the wrong `#[cfg]` gate will only show up when building with a single feature.
+
+**Regression test with every fix:** Every bug fix must include a test that would have caught the bug. Add a `#[test]` or `#[tokio::test]` that reproduces the original failure. Exempt: changes limited to `src/channels/web/static/` or `.md` files. Use `[skip-regression-check]` in commit message or PR label if genuinely not feasible. The `commit-msg` hook and CI workflow enforce this automatically.
+
+**Zero clippy warnings policy:** Fix ALL clippy warnings before committing, including pre-existing ones in files you didn't change. Never leave warnings behind — treat `cargo clippy` output as a zero-tolerance gate.
+
+**Transaction safety:** Multi-step database operations (INSERT+INSERT, UPDATE+DELETE, read-then-write) MUST be wrapped in a transaction. Never assume sequential calls are atomic. Before committing DB code, ask: "If this crashes between step N and N+1, is the database consistent?" If not, wrap in a transaction. This applies to both postgres and libsql backends.
+
+**UTF-8 string safety:** Never use byte-index slicing (`&s[..n]`) on user-supplied or external strings — it panics on multi-byte characters. Use `is_char_boundary()` to walk backwards from the desired length, or iterate with `char_indices()`. Grep for `[..` in changed files to catch violations.
+
+**Case-insensitive comparisons:** When comparing user-supplied strings (file paths, media types, extension names), always normalize to lowercase first with `.to_ascii_lowercase()`. On case-insensitive filesystems (macOS, Windows), path comparisons must be case-insensitive. File extension checks (`.png`, `.jpg`) and media type checks (`image/jpeg`) are common offenders.
+
+**Decorator/wrapper trait delegation:** When adding a new method to `LlmProvider` (or any trait with decorator wrappers), you MUST update ALL wrapper types to delegate to their inner provider. Grep for `impl LlmProvider for` to find all implementations. Add a test that exercises the method through the full provider chain (`build_provider_chain()`), not just the base impl.
+
+**Sensitive data in logs & events:** Tool parameters and outputs MUST be redacted before logging or broadcasting via SSE/WebSocket. Use `redact_params()` before any `tracing::info!`, `JobEvent`, or SSE emission that includes tool call data. Never log raw parameters from tool calls.
+
+**Test temporary files:** Use the `tempfile` crate for test directories/files. Never hardcode `/tmp/...` paths — they collide in parallel test runs and break on non-Unix platforms.
+
+**Trust boundaries in multi-process architecture:** Data from worker containers is untrusted. The orchestrator MUST validate: tool domain (never execute `Container`-domain tools on the host), nesting depth (server-side tracking, not client-supplied), and parameter sensitivity (redact before logging/broadcasting).
+
+**Mechanical verification before committing:** Run these checks on changed files before committing:
+- `cargo clippy --all --benches --tests --examples --all-features` -- zero warnings
+- `grep -rnE '\.unwrap\(|\.expect\(' ` -- no panics in production
+- `grep -rn 'super::' ` -- use `crate::` imports
+- If you fixed a pattern bug, `grep` for other instances of that pattern across `src/`
+- Fix commits must include regression tests (enforced by `commit-msg` hook; bypass with `[skip-regression-check]`)
+- Run `scripts/pre-commit-safety.sh` to catch UTF-8, case-sensitivity, hardcoded /tmp, and logging issues
+
## Configuration
Environment variables (see `.env.example`):
@@ -263,10 +420,14 @@ LIBSQL_PATH=~/.ironclaw/ironclaw.db # libSQL local path (default)
# LIBSQL_URL=libsql://xxx.turso.io # Turso cloud (optional)
# LIBSQL_AUTH_TOKEN=xxx # Required with LIBSQL_URL
-# NEAR AI (required)
-NEARAI_SESSION_TOKEN=sess_...
-NEARAI_MODEL=claude-3-5-sonnet-20241022
+# NEAR AI (when LLM_BACKEND=nearai, the default)
+# Two auth modes: session token (default) or API key
+# Session token auth (default): uses browser OAuth on first run
+NEARAI_SESSION_TOKEN=sess_... # hosting providers: set this
NEARAI_BASE_URL=https://private.near.ai
+# API key auth: set NEARAI_API_KEY, base URL defaults to cloud-api.near.ai
+# NEARAI_API_KEY=... # API key from cloud.near.ai
+NEARAI_MODEL=claude-3-5-sonnet-20241022
# Agent settings
AGENT_NAME=ironclaw
@@ -297,6 +458,10 @@ SANDBOX_ENABLED=true
SANDBOX_IMAGE=ironclaw-worker:latest
SANDBOX_MEMORY_LIMIT_MB=512
SANDBOX_TIMEOUT_SECS=1800
+SANDBOX_CPU_LIMIT=1.0 # CPU cores per container
+SANDBOX_NETWORK_PROXY=true # Enable network proxy for containers
+SANDBOX_PROXY_PORT=8080 # Proxy listener port
+SANDBOX_DEFAULT_POLICY=workspace_write # ReadOnly, WorkspaceWrite, FullAccess
# Claude Code mode (runs inside sandbox containers)
CLAUDE_CODE_ENABLED=false
@@ -308,116 +473,47 @@ CLAUDE_CODE_CONFIG_DIR=/home/worker/.claude
ROUTINES_ENABLED=true
ROUTINES_CRON_INTERVAL=60 # Tick interval in seconds
ROUTINES_MAX_CONCURRENT=3
+
+# Skills system
+SKILLS_ENABLED=true
+SKILLS_MAX_TOKENS=4000 # Max prompt budget per turn
+SKILLS_CATALOG_URL=https://clawhub.dev # ClawHub registry URL
+SKILLS_AUTO_DISCOVER=true # Scan skill directories on startup
+
+# Tinfoil private inference
+TINFOIL_API_KEY=... # Required when LLM_BACKEND=tinfoil
+TINFOIL_MODEL=kimi-k2-5 # Default model
+
+# Tunnel (public internet exposure for webhooks)
+TUNNEL_URL=https://abc123.ngrok.io # Static public URL (manual tunnel)
+# Or use a managed tunnel provider:
+TUNNEL_PROVIDER=none # none (default), cloudflare, tailscale, ngrok, custom
+TUNNEL_CF_TOKEN=... # Required for TUNNEL_PROVIDER=cloudflare
+TUNNEL_NGROK_TOKEN=... # Required for TUNNEL_PROVIDER=ngrok
+# TUNNEL_NGROK_DOMAIN=... # Custom domain (paid ngrok plan)
+# TUNNEL_TS_FUNNEL=true # Use tailscale funnel (public) vs serve (tailnet)
+TUNNEL_CUSTOM_COMMAND=... # Command with {host}/{port} for custom providers
+
+# Observability backend
+OBSERVABILITY_BACKEND=none # none/noop (default) or log
```
-### NEAR AI Provider
+### LLM Providers
-Uses the NEAR AI chat-api (`https://api.near.ai/v1/responses`) which provides:
-- Unified access to multiple models (OpenAI, Anthropic, etc.)
-- User authentication via session tokens
-- Usage tracking and billing through NEAR AI
-
-Session tokens have the format `sess_xxx` (37 characters). They are authenticated against the NEAR AI auth service.
+Backends: `nearai` (default), `openai`, `anthropic`, `ollama`, `openai_compatible`, `tinfoil` — set via `LLM_BACKEND`. See [src/llm/CLAUDE.md](src/llm/CLAUDE.md) for per-provider auth and configuration details.
## Database
-IronClaw supports two database backends, selected at compile time via Cargo feature flags and at runtime via the `DATABASE_BACKEND` environment variable.
-
-**IMPORTANT: All new features that touch persistence MUST support both backends.** Implement the operation as a method on the `Database` trait in `src/db/mod.rs`, then add the implementation in both `src/db/postgres.rs` (delegate to Store/Repository) and `src/db/libsql_backend.rs` (native SQL).
-
-### Backends
-
-| Backend | Feature Flag | Default | Use Case |
-|---------|-------------|---------|----------|
-| PostgreSQL | `postgres` (default) | Yes | Production, existing deployments |
-| libSQL/Turso | `libsql` | No | Zero-dependency local mode, edge, Turso cloud |
+Dual-backend persistence (PostgreSQL + libSQL/Turso). **All new persistence features must support both backends** — see [src/db/CLAUDE.md](src/db/CLAUDE.md) for schema, SQL dialect differences, adding operations, and libSQL limitations.
+Implement every new operation in both `src/db/postgres.rs` and `src/db/libsql/mod.rs`. Test in isolation:
```bash
-# Build with PostgreSQL only (default)
-cargo build
-
-# Build with libSQL only
-cargo build --no-default-features --features libsql
-
-# Build with both backends available
-cargo build --features "postgres,libsql"
+cargo check # postgres (default)
+cargo check --no-default-features --features libsql # libsql only
+cargo check --all-features # both
```
-### Database Trait
-
-The `Database` trait (`src/db/mod.rs`) defines ~60 async methods covering all persistence:
-- Conversations, messages, metadata
-- Jobs, actions, LLM calls, estimation snapshots
-- Sandbox jobs, job events
-- Routines, routine runs
-- Tool failures, settings
-- Workspace: documents, chunks, hybrid search
-
-Both backends implement this trait. PostgreSQL delegates to the existing `Store` + `Repository`. libSQL implements native SQLite-dialect SQL.
-
-### Schema
-
-**PostgreSQL:** `migrations/V1__initial.sql` (351 lines). Uses pgvector for embeddings, tsvector for FTS, PL/pgSQL functions. Managed by `refinery`.
-
-**libSQL:** `src/db/libsql_migrations.rs` (consolidated schema, ~480 lines). Translates PG types:
-- `UUID` -> `TEXT`, `TIMESTAMPTZ` -> `TEXT` (ISO-8601), `JSONB` -> `TEXT`
-- `VECTOR(1536)` -> `F32_BLOB(1536)` with `libsql_vector_idx`
-- `tsvector`/`ts_rank_cd` -> FTS5 virtual table with sync triggers
-- PL/pgSQL functions -> SQLite triggers
-
-**Tables (both backends):**
-
-**Core:**
-- `conversations` - Multi-channel conversation tracking
-- `agent_jobs` - Job metadata and status
-- `job_actions` - Event-sourced tool executions
-- `dynamic_tools` - Agent-built tools
-- `llm_calls` - Cost tracking
-- `estimation_snapshots` - Learning data
-
-**Workspace/Memory:**
-- `memory_documents` - Flexible path-based files (e.g., "context/vision.md", "daily/2024-01-15.md")
-- `memory_chunks` - Chunked content with FTS and vector indexes
-- `heartbeat_state` - Periodic execution tracking
-
-**Other:**
-- `routines`, `routine_runs` - Scheduled/reactive execution
-- `settings` - Per-user key-value settings
-- `tool_failures` - Self-repair tracking
-- `secrets`, `wasm_tools`, `tool_capabilities` - Extension infrastructure
-
-### Configuration
-
-```bash
-# Backend selection (default: postgres)
-DATABASE_BACKEND=libsql
-
-# PostgreSQL
-DATABASE_URL=postgres://user:pass@localhost/ironclaw
-
-# libSQL (embedded)
-LIBSQL_PATH=~/.ironclaw/ironclaw.db # Default path
-
-# libSQL (Turso cloud sync)
-LIBSQL_URL=libsql://your-db.turso.io
-LIBSQL_AUTH_TOKEN=your-token # Required when LIBSQL_URL is set
-
-# Vector store for workspace semantic search (optional)
-# When lancedb: uses LanceDB instead of pgvector/libsql for vector search
-VECTOR_BACKEND=builtin # default: use database built-in
-# VECTOR_BACKEND=lancedb # requires: cargo build --features lancedb
-# LANCEDB_PATH=~/.ironclaw/lancedb # default when VECTOR_BACKEND=lancedb
-```
-
-### Current Limitations (libSQL backend)
-
-- **Workspace/memory system** not yet wired through Database trait (requires Store migration)
-- **Secrets store** not yet available (still requires PostgresSecretsStore)
-- **Hybrid search** uses FTS5 only (vector search via libsql_vector_idx not yet implemented)
-- **Settings reload from DB** skipped (Config::from_db requires Store)
-- No incremental migration versioning (schema is CREATE IF NOT EXISTS, no ALTER TABLE support yet)
-- **No encryption at rest** -- The local SQLite database file stores conversation content, job data, workspace memory, and other application data in plaintext. Only secrets (API tokens, credentials) are encrypted via AES-256-GCM before storage. Users handling sensitive data should use full-disk encryption (FileVault, LUKS, BitLocker) or consider the PostgreSQL backend with TDE/encrypted storage.
-- **JSON merge patch vs path-targeted update** -- The libSQL backend uses RFC 7396 JSON Merge Patch (`json_patch`) for metadata updates, while PostgreSQL uses path-targeted `jsonb_set`. Merge patch replaces top-level keys entirely, which may drop nested keys not present in the patch. Callers should avoid relying on partial nested object updates in metadata fields.
+Database configuration: see Configuration section above.
## Safety Layer
@@ -425,6 +521,7 @@ All external tool output passes through `SafetyLayer`:
1. **Sanitizer** - Detects injection patterns, escapes dangerous content
2. **Validator** - Checks length, encoding, forbidden patterns
3. **Policy** - Rules with severity (Critical/High/Medium/Low) and actions (Block/Warn/Review/Sanitize)
+4. **Leak Detector** - Scans for 15+ secret patterns (API keys, tokens, private keys, connection strings) at two points: tool output before it reaches the LLM, and LLM responses before they reach the user. Actions per pattern: Block (reject entirely), Redact (mask the secret), or Warn (flag but allow)
Tool outputs are wrapped before reaching LLM:
```xml
@@ -433,6 +530,99 @@ Tool outputs are wrapped before reaching LLM:
```
+### Shell Environment Scrubbing
+
+The shell tool (`src/tools/builtin/shell.rs`) scrubs sensitive environment variables before executing commands, preventing secrets from leaking through `env`, `printenv`, or `$VAR` expansion. The sanitizer (`src/safety/sanitizer.rs`) also detects command injection patterns (chained commands, subshells, path traversal) and blocks or escapes them based on policy rules.
+
+## Skills System
+
+Skills are SKILL.md files that extend the agent's prompt with domain-specific instructions. Each skill is a YAML frontmatter block (metadata, activation criteria, required tools) followed by a markdown body that gets injected into the LLM context when the skill activates.
+
+### Trust Model
+
+| Trust Level | Source | Tool Access |
+|-------------|--------|-------------|
+| **Trusted** | User-placed in `~/.ironclaw/skills/` or workspace `skills/` | All tools available to the agent |
+| **Installed** | Downloaded from ClawHub registry | Read-only tools only (no shell, file write, HTTP) |
+
+### SKILL.md Format
+
+```yaml
+---
+name: my-skill
+version: 0.1.0
+description: Does something useful
+activation:
+ patterns:
+ - "deploy to.*production"
+ keywords:
+ - "deployment"
+ max_context_tokens: 2000
+metadata:
+ openclaw:
+ requires:
+ bins: [docker, kubectl]
+ env: [KUBECONFIG]
+---
+
+# Deployment Skill
+
+Instructions for the agent when this skill activates...
+```
+
+### Selection Pipeline
+
+1. **Gating** -- Check binary/env/config requirements; skip skills whose prerequisites are missing
+2. **Scoring** -- Deterministic scoring against message content using keywords, tags, and regex patterns
+3. **Budget** -- Select top-scoring skills that fit within `SKILLS_MAX_TOKENS` prompt budget
+4. **Attenuation** -- Apply trust-based tool ceiling; installed skills lose access to dangerous tools
+
+### Skill Tools
+
+Four built-in tools for managing skills at runtime:
+- **`skill_list`** -- List all discovered skills with trust level and status
+- **`skill_search`** -- Search ClawHub registry for available skills
+- **`skill_install`** -- Download and install a skill from ClawHub
+- **`skill_remove`** -- Remove an installed skill
+
+### Skill Directories
+
+- `~/.ironclaw/skills/` -- User's global skills (trusted)
+- `/skills/` -- Per-workspace skills (trusted)
+- `~/.ironclaw/installed_skills/` -- Registry-installed skills (installed trust)
+
+### Testing Skills
+
+- `skills/web-ui-test/` -- Manual test checklist for the web gateway UI via Claude for Chrome extension. Covers connection, chat, skills search/install/remove, and other tabs.
+
+Skills configuration: see Configuration section above.
+
+## Docker Sandbox
+
+The `src/sandbox/` module provides Docker-based isolation for job execution with a network proxy that controls outbound access and injects credentials.
+
+### Sandbox Policies
+
+| Policy | Filesystem | Network | Use Case |
+|--------|-----------|---------|----------|
+| **ReadOnly** | Read-only workspace mount | Allowlisted domains only | Analysis, code review |
+| **WorkspaceWrite** | Read-write workspace mount | Allowlisted domains only | Code generation, file edits |
+| **FullAccess** | Full filesystem | Unrestricted | Trusted admin tasks |
+
+### Network Proxy
+
+Containers route all HTTP/HTTPS traffic through a host-side proxy (`src/sandbox/proxy/`):
+- **Domain allowlist** -- Only allowlisted domains are reachable (default: package registries, docs sites, GitHub, common APIs)
+- **Credential injection** -- The `CredentialResolver` trait injects auth headers into proxied requests so secrets never enter the container environment
+- **CONNECT tunnel** -- HTTPS traffic uses CONNECT method; the proxy validates the target domain against the allowlist before establishing the tunnel
+- **Policy decisions** -- The `NetworkPolicyDecider` trait allows custom logic for allow/deny/inject decisions per request
+
+### Zero-Exposure Credential Model
+
+Secrets (API keys, tokens) are stored encrypted on the host and injected into HTTP requests by the proxy at transit time. Container processes never have access to raw credential values, preventing exfiltration even if container code is compromised.
+
+Sandbox configuration: see Configuration section above.
+
## Testing
Tests are in `mod tests {}` blocks at the bottom of each file. Run specific module tests:
@@ -454,174 +644,23 @@ Key test patterns:
4. **WIT bindgen integration** - Auto-extract tool description/schema from WASM modules (stubbed)
5. **Capability granting after tool build** - Built tools get empty capabilities; need UX for granting HTTP/secrets access
6. **Tool versioning workflow** - No version tracking or rollback for dynamically built tools
-7. **Webhook trigger endpoint** - Routines webhook trigger not yet exposed in web gateway
-8. **Full channel status view** - Gateway status widget exists, but no per-channel connection dashboard
+7. **Full channel status view** - Gateway status widget exists, but no per-channel connection dashboard
+8. **Observability backends** - Only `log` and `noop` implemented; OpenTelemetry/Prometheus not yet supported
-### Completed
+## Tool Architecture
-- ✅ **Workspace integration** - Memory tools registered, workspace passed to Agent and heartbeat
-- ✅ **WASM sandboxing** - Full implementation in `tools/wasm/` with fuel metering, memory limits, capabilities
-- ✅ **Dynamic tool building** - `tools/builder/` has LlmSoftwareBuilder with iterative build loop
-- ✅ **HTTP webhook security** - Secret validation implemented, proper error handling (no panics)
-- ✅ **Embeddings integration** - OpenAI and NEAR AI providers wired to workspace for semantic search
-- ✅ **Workspace system prompt** - Identity files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md) injected into LLM context
-- ✅ **Heartbeat notifications** - Route through channel manager (broadcast API) instead of logging-only
-- ✅ **Auto-context compaction** - Triggers automatically when context exceeds threshold
-- ✅ **Embedding backfill** - Runs on startup when embeddings provider is enabled
-- ✅ **Clippy clean** - All warnings addressed via config struct refactoring
-- ✅ **Tool approval enforcement** - Tools with `requires_approval()` (shell, http, file write/patch, build_software) now gate execution, track auto-approved tools per session
-- ✅ **Tool definition refresh** - Tool definitions refreshed each iteration so newly built tools become visible in same session
-- ✅ **Worker tool call handling** - Uses `respond_with_tools()` to properly execute tool calls when `select_tools()` returns empty
-- ✅ **Gateway control plane** - Web gateway with 40+ API endpoints, SSE/WebSocket
-- ✅ **Web Control UI** - Browser-based dashboard with chat, memory, jobs, logs, extensions, routines
-- ✅ **Slack/Telegram channels** - Implemented as WASM tools
-- ✅ **Docker sandbox** - Orchestrator/worker containers with per-job auth
-- ✅ **Claude Code mode** - Delegate jobs to Claude CLI inside containers
-- ✅ **Routines system** - Cron, event, webhook, and manual triggers with guardrails
-- ✅ **Extension management** - Install, auth, activate MCP/WASM extensions via CLI and web UI
-- ✅ **libSQL/Turso backend** - Database trait abstraction (`src/db/`), feature-gated dual backend support (postgres/libsql), embedded SQLite for zero-dependency local mode
+**Keep tool-specific logic out of the main agent codebase.** The main agent provides generic infrastructure; tools are self-contained units that declare their requirements through `capabilities.json` files (API endpoints, credentials, rate limits, auth setup). Service-specific auth flows, CLI commands, and configuration do not belong in the main agent.
-## Adding a New Tool
+Tools can be built as **WASM** (sandboxed, credential-injected, single binary) or **MCP servers** (ecosystem of pre-built servers, any language, but no sandbox). Both are first-class via `ironclaw tool install`. Auth is declared in capabilities files with OAuth and manual token entry support.
-### Built-in Tools (Rust)
-
-1. Create `src/tools/builtin/my_tool.rs`
-2. Implement the `Tool` trait
-3. Add `mod my_tool;` and `pub use` in `src/tools/builtin/mod.rs`
-4. Register in `ToolRegistry::register_builtin_tools()` in `registry.rs`
-5. Add tests
-
-### WASM Tools (Recommended)
-
-WASM tools are the preferred way to add new capabilities. They run in a sandboxed environment with explicit capabilities.
-
-1. Create a new crate in `tools-src//`
-2. Implement the WIT interface (`wit/tool.wit`)
-3. Create `.capabilities.json` declaring required permissions
-4. Build with `cargo build --target wasm32-wasip2 --release`
-5. Install with `ironclaw tool install path/to/tool.wasm`
-
-See `tools-src/` for examples.
-
-## Tool Architecture Principles
-
-**CRITICAL: Keep tool-specific logic out of the main agent codebase.**
-
-The main agent provides generic infrastructure; tools are self-contained units that declare their requirements through capabilities files.
-
-### What Goes in Tools (capabilities.json)
-
-- API endpoints the tool needs (HTTP allowlist)
-- Credentials required (secret names, injection locations)
-- Rate limits and timeouts
-- Auth setup instructions (see below)
-- Workspace paths the tool can read
-
-### What Does NOT Go in Main Agent
-
-- Service-specific auth flows (OAuth for Notion, Slack, etc.)
-- Service-specific CLI commands (`auth notion`, `auth slack`)
-- Service-specific configuration handling
-- Hardcoded API URLs or token formats
-
-### Tool Authentication
-
-Tools declare their auth requirements in `.capabilities.json` under the `auth` section. Two methods are supported:
-
-#### OAuth (Browser-based login)
-
-For services that support OAuth, users just click through browser login:
-
-```json
-{
- "auth": {
- "secret_name": "notion_api_token",
- "display_name": "Notion",
- "oauth": {
- "authorization_url": "https://api.notion.com/v1/oauth/authorize",
- "token_url": "https://api.notion.com/v1/oauth/token",
- "client_id_env": "NOTION_OAUTH_CLIENT_ID",
- "client_secret_env": "NOTION_OAUTH_CLIENT_SECRET",
- "scopes": [],
- "use_pkce": false,
- "extra_params": { "owner": "user" }
- },
- "env_var": "NOTION_TOKEN"
- }
-}
-```
-
-To enable OAuth for a tool:
-1. Register a public OAuth app with the service (e.g., notion.so/my-integrations)
-2. Configure redirect URIs: `http://localhost:9876/callback` through `http://localhost:9886/callback`
-3. Set environment variables for client_id and client_secret
-
-#### Manual Token Entry (Fallback)
-
-For services without OAuth or when OAuth isn't configured:
-
-```json
-{
- "auth": {
- "secret_name": "openai_api_key",
- "display_name": "OpenAI",
- "instructions": "Get your API key from platform.openai.com/api-keys",
- "setup_url": "https://platform.openai.com/api-keys",
- "token_hint": "Starts with 'sk-'",
- "env_var": "OPENAI_API_KEY"
- }
-}
-```
-
-#### Auth Flow Priority
-
-When running `ironclaw tool auth `:
-
-1. Check `env_var` - if set in environment, use it directly
-2. Check `oauth` - if configured, open browser for OAuth flow
-3. Fall back to `instructions` + manual token entry
-
-The agent reads auth config from the tool's capabilities file and provides the appropriate flow. No service-specific code in the main agent.
-
-### WASM Tools vs MCP Servers: When to Use Which
-
-Both are first-class in the extension system (`ironclaw tool install` handles both), but they have different strengths.
-
-**WASM Tools (IronClaw native)**
-
-- Sandboxed: fuel metering, memory limits, no access except what's allowlisted
-- Credentials injected by host runtime, tool code never sees the actual token
-- Output scanned for secret leakage before returning to the LLM
-- Auth (OAuth/manual) declared in `capabilities.json`, agent handles the flow
-- Single binary, no process management, works offline
-- Cost: must build yourself in Rust, no ecosystem, synchronous only
-
-**MCP Servers (Model Context Protocol)**
-
-- Growing ecosystem of pre-built servers (GitHub, Notion, Postgres, etc.)
-- Any language (TypeScript/Python most common)
-- Can do websockets, streaming, background polling
-- Cost: external process with full system access (no sandbox), manages own credentials, IronClaw can't prevent leaks
-
-**Decision guide:**
-
-| Scenario | Use |
-|----------|-----|
-| Good MCP server already exists | **MCP** |
-| Handles sensitive credentials (email send, banking) | **WASM** |
-| Quick prototype or one-off integration | **MCP** |
-| Core capability you'll maintain long-term | **WASM** |
-| Needs background connections (websockets, polling) | **MCP** |
-| Multiple tools share one OAuth token (e.g., Google suite) | **WASM** |
-
-The LLM-facing interface is identical for both (tool name, schema, execute), so swapping between them is transparent to the agent.
+See `src/tools/README.md` for full tool architecture, adding new tools (built-in Rust and WASM), auth JSON examples, and WASM vs MCP decision guide.
## Adding a New Channel
1. Create `src/channels/my_channel.rs`
2. Implement the `Channel` trait
-3. Add config in `src/config.rs`
-4. Wire up in `main.rs` channel setup section
+3. Add config in `src/config/channels.rs`
+4. Wire up in `src/app.rs` channel setup section
## Debugging
@@ -651,154 +690,20 @@ for that module's behavior. When modifying code in a module that has a spec:
| Module | Spec File |
|--------|-----------|
| `src/setup/` | `src/setup/README.md` |
-
-## Code Style
-
-- Use `crate::` imports, not `super::`
-- No `pub use` re-exports unless exposing to downstream consumers
-- Prefer strong types over strings (enums, newtypes)
-- Keep functions focused, extract helpers when logic is reused
-- Comments for non-obvious logic only
-
-## Review & Fix Discipline
-
-Hard-won lessons from code review -- follow these when fixing bugs or addressing review feedback.
-
-### Fix the pattern, not just the instance
-When a reviewer flags a bug (e.g., TOCTOU race in INSERT + SELECT-back), search the entire codebase for all instances of that same pattern. A fix in `SecretsStore::create()` that doesn't also fix `WasmToolStore::store()` is half a fix.
-
-### Propagate architectural fixes to satellite types
-If a core type changes its concurrency model (e.g., `LibSqlBackend` switches to connection-per-operation), every type that was handed a resource from the old model (e.g., `LibSqlSecretsStore`, `LibSqlWasmToolStore` holding a single `Connection`) must also be updated. Grep for the old type across the codebase.
-
-### Schema translation is more than DDL
-When translating a database schema between backends (PostgreSQL to libSQL, etc.), check for:
-- **Indexes** -- diff `CREATE INDEX` statements between the two schemas
-- **Seed data** -- check for `INSERT INTO` in migrations (e.g., `leak_detection_patterns`)
-- **Semantic differences** -- document where SQL functions behave differently (e.g., `json_patch` vs `jsonb_set`)
-
-### Feature flag testing
-When adding feature-gated code, test compilation with each feature in isolation:
-```bash
-cargo check # default features
-cargo check --no-default-features --features libsql # libsql only
-cargo check --all-features # all features
-```
-Dead code behind the wrong `#[cfg]` gate will only show up when building with a single feature.
-
-### Mechanical verification before committing
-Run these checks on changed files before committing:
-- `grep -rnE '\.unwrap\(|\.expect\(' ` -- no panics in production
-- `grep -rn 'super::' ` -- use `crate::` imports
-- If you fixed a pattern bug, `grep` for other instances of that pattern across `src/`
+| `src/workspace/` | `src/workspace/README.md` |
+| `src/tools/` | `src/tools/README.md` |
+| `src/agent/` | `src/agent/CLAUDE.md` |
+| `src/channels/web/` | `src/channels/web/CLAUDE.md` |
+| `src/db/` | `src/db/CLAUDE.md` |
+| `src/llm/` | `src/llm/CLAUDE.md` |
+| `tests/e2e/` | `tests/e2e/CLAUDE.md` |
## Workspace & Memory System
-Inspired by [OpenClaw](https://github.com/openclaw/openclaw), the workspace provides persistent memory for agents with a flexible filesystem-like structure.
+OpenClaw-inspired persistent memory with a flexible filesystem-like structure. Principle: "Memory is database, not RAM" -- if you want to remember something, write it explicitly. Uses hybrid search combining FTS (keyword) + vector (semantic) via Reciprocal Rank Fusion.
-### Key Principles
+Four memory tools for LLM use: `memory_search` (hybrid search -- call before answering questions about prior work), `memory_write`, `memory_read`, `memory_tree`. Identity files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md) are injected into the LLM system prompt.
-1. **"Memory is database, not RAM"** - If you want to remember something, write it explicitly
-2. **Flexible structure** - Create any directory/file hierarchy you need
-3. **Self-documenting** - Use README.md files to describe directory structure
-4. **Hybrid search** - Combines FTS (keyword) + vector (semantic) via Reciprocal Rank Fusion
+The heartbeat system runs proactive periodic execution (default: 30 minutes), reading `HEARTBEAT.md` and notifying via channel if findings are detected.
-### Filesystem Structure
-
-```
-workspace/
-├── README.md <- Root runbook/index
-├── MEMORY.md <- Long-term curated memory
-├── HEARTBEAT.md <- Periodic checklist
-├── IDENTITY.md <- Agent name, nature, vibe
-├── SOUL.md <- Core values
-├── AGENTS.md <- Behavior instructions
-├── USER.md <- User context
-├── context/ <- Identity-related docs
-│ ├── vision.md
-│ └── priorities.md
-├── daily/ <- Daily logs
-│ ├── 2024-01-15.md
-│ └── 2024-01-16.md
-├── projects/ <- Arbitrary structure
-│ └── alpha/
-│ ├── README.md
-│ └── notes.md
-└── ...
-```
-
-### Using the Workspace
-
-```rust
-use crate::workspace::{Workspace, OpenAiEmbeddings, paths};
-
-// Create workspace for a user
-let workspace = Workspace::new("user_123", pool)
- .with_embeddings(Arc::new(OpenAiEmbeddings::new(api_key)));
-
-// Read/write any path
-let doc = workspace.read("projects/alpha/notes.md").await?;
-workspace.write("context/priorities.md", "# Priorities\n\n1. Feature X").await?;
-workspace.append("daily/2024-01-15.md", "Completed task X").await?;
-
-// Convenience methods for well-known files
-workspace.append_memory("User prefers dark mode").await?;
-workspace.append_daily_log("Session note").await?;
-
-// List directory contents
-let entries = workspace.list("projects/").await?;
-
-// Search (hybrid FTS + vector)
-let results = workspace.search("dark mode preference", 5).await?;
-
-// Get system prompt from identity files
-let prompt = workspace.system_prompt().await?;
-```
-
-### Memory Tools
-
-Four tools for LLM use:
-
-- **`memory_search`** - Hybrid search, MUST be called before answering questions about prior work
-- **`memory_write`** - Write to any path (memory, daily_log, or custom paths)
-- **`memory_read`** - Read any file by path
-- **`memory_tree`** - View workspace structure as a tree (depth parameter, default 1)
-
-### Hybrid Search (RRF)
-
-Combines full-text search and vector similarity using Reciprocal Rank Fusion:
-
-```
-score(d) = Σ 1/(k + rank(d)) for each method where d appears
-```
-
-Default k=60. Results from both methods are combined, with documents appearing in both getting boosted scores.
-
-**Backend differences:**
-- **PostgreSQL:** `ts_rank_cd` for FTS, pgvector cosine distance for vectors, full RRF
-- **libSQL:** FTS5 for keyword search only (vector search via `libsql_vector_idx` not yet wired)
-
-### Heartbeat System
-
-Proactive periodic execution (default: 30 minutes):
-
-1. Reads `HEARTBEAT.md` checklist
-2. Runs agent turn with checklist prompt
-3. If findings, notifies via channel
-4. If nothing, agent replies "HEARTBEAT_OK" (no notification)
-
-```rust
-use crate::agent::{HeartbeatConfig, spawn_heartbeat};
-
-let config = HeartbeatConfig::default()
- .with_interval(Duration::from_secs(60 * 30))
- .with_notify("user_123", "telegram");
-
-spawn_heartbeat(config, workspace, llm, response_tx);
-```
-
-### Chunking Strategy
-
-Documents are chunked for search indexing:
-- Default: 800 words per chunk (roughly 800 tokens for English)
-- 15% overlap between chunks for context preservation
-- Minimum chunk size: 50 words (tiny trailing chunks merge with previous)
+See `src/workspace/README.md` for full API documentation, filesystem structure, hybrid search details, chunking strategy, and heartbeat system.
diff --git a/COVERAGE_PLAN.md b/COVERAGE_PLAN.md
new file mode 100644
index 00000000..c9d7d73b
--- /dev/null
+++ b/COVERAGE_PLAN.md
@@ -0,0 +1,862 @@
+# IronClaw Coverage Plan: 63.3% to 95%
+
+> Generated 2025-03-06 from [Codecov](https://app.codecov.io/gh/nearai/ironclaw/tree/main/src)
+
+## Current State
+
+| Metric | Value |
+|--------|-------|
+| **Current coverage** | 48,571 / 76,694 lines = **63.33%** |
+| **Target** | 72,859 / 76,694 lines = **95.0%** |
+| **Gap** | **24,288 lines** need coverage |
+| **Files >= 95%** | 43 / 239 |
+| **Files < 95%** | 196 (27,872 total misses) |
+
+## Module Summary
+
+Sorted by uncovered lines (descending):
+
+| Module | Lines | Hits | Miss | Coverage | Priority |
+|--------|------:|-----:|-----:|---------:|----------|
+| `channels/` | 14,079 | 8,677 | 5,402 | 61.6% | P0 |
+| `tools/` | 13,445 | 9,407 | 4,038 | 70.0% | P1 |
+| `agent/` | 9,152 | 6,096 | 3,056 | 66.6% | P0 |
+| `setup/` | 3,005 | 462 | 2,543 | 15.4% | P1 |
+| `extensions/` | 3,540 | 1,298 | 2,242 | 36.7% | P0 |
+| `cli/` | 2,834 | 697 | 2,137 | 24.6% | P1 |
+| `history/` | 1,626 | 0 | 1,626 | 0.0% | P0 |
+| `llm/` | 7,029 | 5,776 | 1,253 | 82.2% | P2 |
+| `(root)` | 4,122 | 3,121 | 1,001 | 75.7% | P2 |
+| `worker/` | 1,274 | 480 | 794 | 37.7% | P1 |
+| `sandbox/` | 1,615 | 897 | 718 | 55.5% | P2 |
+| `registry/` | 1,588 | 1,107 | 481 | 69.7% | P2 |
+| `db/` | 921 | 441 | 480 | 47.9% | P1 |
+| `workspace/` | 2,006 | 1,584 | 422 | 79.0% | P2 |
+| `orchestrator/` | 1,199 | 795 | 404 | 66.3% | P2 |
+| `config/` | 1,464 | 1,095 | 369 | 74.8% | P2 |
+| `hooks/` | 1,379 | 1,081 | 298 | 78.4% | P2 |
+| `secrets/` | 687 | 407 | 280 | 59.2% | P2 |
+| `skills/` | 1,714 | 1,585 | 129 | 92.5% | P3 |
+| `context/` | 693 | 586 | 107 | 84.6% | P3 |
+| `estimation/` | 467 | 369 | 98 | 79.0% | P3 |
+| `safety/` | 1,424 | 1,337 | 87 | 93.9% | P3 |
+| `evaluation/` | 226 | 152 | 74 | 67.3% | P3 |
+| `pairing/` | 498 | 446 | 52 | 89.6% | P3 |
+| `tunnel/` | 391 | 368 | 23 | 94.1% | P3 |
+| `observability/` | 316 | 307 | 9 | 97.2% | Done |
+
+## Top 40 Files by Uncovered Lines
+
+These files account for the vast majority of the coverage gap:
+
+| File | Lines | Miss | Coverage | Lines to 95% |
+|------|------:|-----:|---------:|--------------:|
+| `src/extensions/manager.rs` | 2,404 | 2,083 | 13.3% | 1,962 |
+| `src/setup/wizard.rs` | 2,150 | 1,789 | 16.8% | 1,681 |
+| `src/history/store.rs` | 1,486 | 1,486 | 0.0% | 1,411 |
+| `src/channels/web/server.rs` | 1,985 | 993 | 50.0% | 893 |
+| `src/channels/wasm/wrapper.rs` | 2,237 | 934 | 58.2% | 822 |
+| `src/agent/thread_ops.rs` | 1,044 | 763 | 26.9% | 710 |
+| `src/cli/tool.rs` | 757 | 735 | 2.9% | 697 |
+| `src/setup/channels.rs` | 645 | 596 | 7.6% | 563 |
+| `src/agent/commands.rs` | 587 | 587 | 0.0% | 557 |
+| `src/main.rs` | 740 | 522 | 29.4% | 485 |
+| `src/channels/web/handlers/jobs.rs` | 513 | 456 | 11.1% | 430 |
+| `src/tools/builder/core.rs` | 524 | 456 | 13.0% | 429 |
+| `src/agent/worker.rs` | 1,078 | 467 | 56.7% | 413 |
+| `src/channels/web/handlers/chat.rs` | 564 | 417 | 26.1% | 388 |
+| `src/tools/wasm/wrapper.rs` | 1,005 | 436 | 56.6% | 385 |
+| `src/channels/signal.rs` | 1,814 | 472 | 74.0% | 381 |
+| `src/tools/mcp/auth.rs` | 472 | 378 | 19.9% | 354 |
+| `src/worker/runtime.rs` | 350 | 330 | 5.7% | 312 |
+| `src/tools/builtin/job.rs` | 1,014 | 359 | 64.6% | 308 |
+| `src/cli/mcp.rs` | 322 | 319 | 0.9% | 302 |
+| `src/cli/oauth_defaults.rs` | 730 | 335 | 54.1% | 298 |
+| `src/llm/nearai_chat.rs` | 854 | 340 | 60.2% | 297 |
+| `src/sandbox/container.rs` | 407 | 317 | 22.1% | 296 |
+| `src/tools/mcp/client.rs` | 341 | 291 | 14.7% | 273 |
+| `src/registry/installer.rs` | 765 | 311 | 59.3% | 272 |
+| `src/orchestrator/job_manager.rs` | 405 | 270 | 33.3% | 249 |
+| `src/channels/web/handlers/routines.rs` | 249 | 249 | 0.0% | 236 |
+| `src/agent/scheduler.rs` | 559 | 263 | 53.0% | 235 |
+| `src/tools/wasm/storage.rs` | 296 | 243 | 17.9% | 228 |
+| `src/channels/repl.rs` | 233 | 233 | 0.0% | 221 |
+| `src/llm/session.rs` | 413 | 242 | 41.4% | 221 |
+| `src/worker/claude_bridge.rs` | 629 | 247 | 60.7% | 215 |
+| `src/agent/agent_loop.rs` | 523 | 234 | 55.2% | 207 |
+| `src/worker/api.rs` | 258 | 207 | 19.8% | 194 |
+| `src/sandbox/proxy/http.rs` | 307 | 192 | 37.5% | 176 |
+| `src/channels/wasm/storage.rs` | 182 | 182 | 0.0% | 172 |
+| `src/cli/registry.rs` | 177 | 177 | 0.0% | 168 |
+| `src/llm/reasoning.rs` | 1,163 | 219 | 81.2% | 160 |
+| `src/tools/builder/testing.rs` | 308 | 174 | 43.5% | 158 |
+| `src/db/postgres.rs` | 166 | 166 | 0.0% | 157 |
+
+---
+
+## Tier 1 -- High-Impact Unit Tests (~8,500 lines)
+
+Pure logic, serialization, and database queries testable in isolation without real
+infrastructure. Highest coverage gain per unit of effort.
+
+### `src/history/store.rs` -- 0% -> 95% (+1,411 lines)
+
+PostgreSQL repository layer (conversations, jobs, actions, LLM calls, estimation
+snapshots). Test query construction and result mapping. Can use the libSQL backend
+as a real in-memory database or test doubles for the `Database` trait.
+
+**Tests to write:**
+- `test_store_conversation_crud` -- create, read, update, delete conversations
+- `test_store_job_lifecycle` -- insert job, update status through state machine
+- `test_store_action_recording` -- record and query job actions
+- `test_store_llm_call_tracking` -- insert and aggregate LLM call records
+- `test_store_estimation_snapshots` -- save and retrieve estimation data
+
+### `src/history/analytics.rs` -- 0% -> 95% (+133 lines)
+
+Aggregation queries (JobStats, ToolStats). Test the query builders and result
+deserialization.
+
+**Tests to write:**
+- `test_job_stats_aggregation` -- verify counts, durations, success rates
+- `test_tool_stats_ranking` -- verify tool usage frequency sorting
+- `test_analytics_empty_db` -- graceful handling of no data
+
+### `src/extensions/manager.rs` -- 13.3% -> 95% (+1,962 lines)
+
+Largest single file gap. Extension lifecycle orchestration (install, auth,
+activate, remove), config parsing, and state transitions.
+
+**Tests to write:**
+- `test_extension_install_from_manifest` -- parse manifest, create extension record
+- `test_extension_auth_flow` -- OAuth token setup, credential storage
+- `test_extension_activate_deactivate` -- state transitions, tool registration
+- `test_extension_remove_cleanup` -- remove extension, clean up artifacts
+- `test_extension_config_validation` -- reject invalid configs, handle defaults
+- `test_extension_list_filtering` -- filter by status, type, search query
+- `test_extension_capability_check` -- verify required capabilities before activation
+
+### `src/extensions/discovery.rs` -- 27.8% -> 95% (+125 lines)
+
+Extension discovery from filesystem and registry.
+
+**Tests to write:**
+- `test_discover_local_extensions` -- scan directory, parse manifests
+- `test_discover_skip_invalid` -- gracefully skip malformed extension dirs
+- `test_discover_dedup` -- handle duplicate extensions across paths
+
+### `src/tools/builder/core.rs` -- 13% -> 95% (+429 lines)
+
+`BuildRequirement`, `SoftwareType`, `Language` types and project scaffolding.
+
+**Tests to write:**
+- `test_build_requirement_parsing` -- deserialize from JSON
+- `test_scaffold_project_structure` -- verify generated file tree
+- `test_language_detection` -- detect language from file extensions
+- `test_software_type_constraints` -- validate type-specific requirements
+
+### `src/tools/builder/testing.rs` -- 43.5% -> 95% (+158 lines)
+
+Test harness integration for built tools.
+
+**Tests to write:**
+- `test_harness_setup_teardown` -- lifecycle of test environment
+- `test_harness_run_tests` -- execute tests and capture results
+- `test_harness_failure_reporting` -- verify error details on test failure
+
+### `src/tools/mcp/auth.rs` -- 19.9% -> 95% (+354 lines)
+
+OAuth token management for MCP servers.
+
+**Tests to write:**
+- `test_token_refresh_on_expiry` -- auto-refresh when token expires
+- `test_token_header_injection` -- correct Authorization header format
+- `test_token_persistence` -- save/load tokens across restarts
+- `test_oauth_pkce_flow` -- code verifier/challenge generation
+- `test_auth_config_parsing` -- parse various auth config formats
+
+### `src/tools/mcp/client.rs` -- 14.7% -> 95% (+273 lines)
+
+JSON-RPC client for MCP protocol.
+
+**Tests to write:**
+- `test_jsonrpc_request_serialization` -- correct JSON-RPC 2.0 format
+- `test_jsonrpc_response_parsing` -- handle success, error, and batch responses
+- `test_jsonrpc_error_codes` -- map MCP error codes to ToolError
+- `test_tool_list_discovery` -- parse tools/list response
+- `test_tool_call_roundtrip` -- serialize call, parse result
+
+### `src/tools/wasm/storage.rs` -- 17.9% -> 95% (+228 lines)
+
+WASM tool persistence (store, load, delete, list).
+
+**Tests to write:**
+- `test_wasm_tool_store_roundtrip` -- store and retrieve tool binary + metadata
+- `test_wasm_tool_delete` -- remove tool and verify gone
+- `test_wasm_tool_list_filtering` -- filter by name, capability
+- `test_wasm_tool_update_metadata` -- update without re-uploading binary
+
+### `src/tools/wasm/wrapper.rs` -- 56.6% -> 95% (+385 lines)
+
+Tool trait wrapper for WASM modules.
+
+**Tests to write:**
+- `test_wasm_param_marshalling` -- JSON params to WASM component model types
+- `test_wasm_output_conversion` -- WASM return values to ToolOutput
+- `test_wasm_error_propagation` -- WASM traps to ToolError
+- `test_wasm_fuel_exhaustion` -- verify fuel limit enforcement
+- `test_wasm_memory_limit` -- verify memory ceiling
+
+### `src/tools/wasm/loader.rs` -- 62.4% -> 95% (+156 lines)
+
+WASM tool discovery from filesystem.
+
+**Tests to write:**
+- `test_loader_scan_directory` -- find .wasm files with capabilities.json
+- `test_loader_skip_invalid` -- skip files without valid WIT exports
+- `test_loader_cache_invalidation` -- reload when file changes
+
+### `src/tools/builtin/job.rs` -- 64.6% -> 95% (+308 lines)
+
+Job management tools (CreateJob, ListJobs, JobStatus, CancelJob).
+
+**Tests to write:**
+- `test_create_job_params` -- validate required/optional parameters
+- `test_list_jobs_formatting` -- verify output structure
+- `test_job_status_transitions` -- query status at each state
+- `test_cancel_job_running` -- cancel an in-progress job
+- `test_cancel_job_completed` -- error on already-completed job
+
+### `src/secrets/store.rs` -- 48.1% -> 95% (+145 lines)
+
+Encrypted secret storage.
+
+**Tests to write:**
+- `test_secret_store_roundtrip` -- store encrypted, retrieve decrypted
+- `test_secret_update` -- overwrite existing secret
+- `test_secret_delete` -- remove and verify inaccessible
+- `test_secret_list_redacted` -- list shows names but not values
+
+### `src/llm/session.rs` -- 41.4% -> 95% (+221 lines)
+
+Session token management with auto-renewal.
+
+**Tests to write:**
+- `test_session_token_parsing` -- parse `sess_xxx` format
+- `test_session_expiry_detection` -- detect expired tokens
+- `test_session_auto_renewal` -- trigger renewal before expiry
+- `test_session_concurrent_renewal` -- only one renewal in flight
+
+### `src/llm/nearai_chat.rs` -- 60.2% -> 95% (+297 lines)
+
+NEAR AI Chat Completions provider.
+
+**Tests to write:**
+- `test_nearai_request_building` -- correct endpoint, headers, body
+- `test_nearai_response_parsing` -- parse streaming and non-streaming responses
+- `test_nearai_tool_message_flattening` -- tool messages flattened to text
+- `test_nearai_auth_modes` -- session token vs API key auth
+- `test_nearai_error_handling` -- rate limits, auth failures, server errors
+
+### `src/llm/mod.rs` -- 53.7% -> 95% (+112 lines)
+
+Provider factory and backend selection.
+
+**Tests to write:**
+- `test_provider_factory_nearai` -- select NEAR AI from config
+- `test_provider_factory_openai` -- select OpenAI from config
+- `test_provider_factory_ollama` -- select Ollama from config
+- `test_provider_factory_invalid` -- error on unknown backend
+
+### `src/llm/reasoning.rs` -- 81.2% -> 95% (+160 lines)
+
+Planning, tool selection, evaluation logic.
+
+**Tests to write:**
+- `test_reasoning_step_parsing` -- parse planning steps from LLM output
+- `test_tool_selection_scoring` -- rank tools by relevance
+- `test_evaluation_rubric` -- score completions against criteria
+- `test_reasoning_with_no_tools` -- handle tool-less responses
+
+### `src/db/postgres.rs` -- 0% -> 95% (+157 lines)
+
+PostgreSQL backend delegation to Store + Repository.
+
+**Tests to write:**
+- `test_postgres_backend_delegates` -- verify delegation pattern (trait-level)
+- `test_postgres_connection_config` -- TLS, pool size, timeout parsing
+
+### `src/workspace/mod.rs` -- 75.9% -> 95% (+109 lines)
+
+Memory operations (write, read, search, tree).
+
+**Tests to write:**
+- `test_workspace_write_read` -- write document, read it back
+- `test_workspace_search_hybrid` -- FTS + vector search via RRF
+- `test_workspace_tree` -- directory listing of memory filesystem
+- `test_workspace_overwrite` -- update existing document
+
+### `src/workspace/embeddings.rs` -- 35.1% -> 95% (~100 lines)
+
+Embedding provider abstraction.
+
+**Tests to write:**
+- `test_embedding_dimension_handling` -- verify dimension config
+- `test_embedding_batch_processing` -- batch multiple chunks
+- `test_embedding_provider_fallback` -- graceful degradation when unavailable
+
+---
+
+## Tier 2 -- Trace Tests (~7,000 lines)
+
+End-to-end tests that exercise the agent loop, worker, scheduler, and dispatcher
+by replaying LLM traces through `TestRig` (see `tests/support/test_rig.rs`). Each
+trace test covers multiple modules simultaneously, making them high-leverage.
+
+Each trace test needs:
+1. A JSON fixture in `tests/fixtures/llm_traces/`
+2. A test file in `tests/` using `TestRigBuilder`
+
+### Trace: Thread Operations
+
+**Covers:** `agent/thread_ops.rs` (+710 lines)
+
+Test thread creation, listing, switching, and deletion via trace replay.
+
+**Fixture:** `thread_operations.json`
+**Tests:**
+- `test_thread_create_and_switch` -- create thread, switch to it, verify context
+- `test_thread_list` -- list all threads, verify metadata
+- `test_thread_delete` -- delete thread, verify removal
+- `test_thread_switch_nonexistent` -- error handling for missing thread
+
+### Trace: Agent Commands
+
+**Covers:** `agent/commands.rs` (+557 lines)
+
+Test slash commands through the agent loop.
+
+**Fixture:** `agent_commands.json`
+**Tests:**
+- `test_command_help` -- /help returns command list
+- `test_command_clear` -- /clear resets conversation
+- `test_command_compact` -- /compact triggers summarization
+- `test_command_undo_redo` -- /undo then /redo restores state
+- `test_command_status` -- /status shows agent state
+
+### Trace: Worker Multi-Turn Execution
+
+**Covers:** `agent/worker.rs` (+413 lines), `agent/agent_loop.rs` (+207 lines)
+
+Test multi-turn tool calling, error recovery, and completion flows.
+
+**Fixture:** `worker_multi_turn.json`
+**Tests:**
+- `test_worker_sequential_tools` -- call tool A, then tool B based on A's result
+- `test_worker_tool_error_recovery` -- tool fails, agent retries or adapts
+- `test_worker_max_turns` -- verify turn limit enforcement
+
+### Trace: Scheduler Parallel Jobs
+
+**Covers:** `agent/scheduler.rs` (+235 lines)
+
+Test parallel job dispatch and completion tracking.
+
+**Fixture:** `scheduler_parallel.json`
+**Tests:**
+- `test_scheduler_parallel_dispatch` -- dispatch 3 jobs, all complete
+- `test_scheduler_job_dependency` -- job B waits for job A
+- `test_scheduler_stuck_detection` -- detect and recover stuck job
+
+### Trace: Dispatcher Skill Selection
+
+**Covers:** `agent/dispatcher.rs` (+153 lines)
+
+Test skill-aware routing and tool attenuation.
+
+**Fixture:** `dispatcher_skills.json`
+**Tests:**
+- `test_dispatcher_skill_match` -- match message to skill, inject prompt
+- `test_dispatcher_tool_attenuation` -- installed skill loses dangerous tools
+- `test_dispatcher_no_skill` -- fallback when no skill matches
+
+### Trace: Routine Execution
+
+**Covers:** `agent/routine_engine.rs` (~80 lines), `agent/routine.rs` (~40 lines)
+
+Test cron tick and event-triggered routine execution.
+
+**Fixture:** `routine_execution.json`
+**Tests:**
+- `test_routine_cron_trigger` -- routine fires on schedule
+- `test_routine_event_trigger` -- routine fires on matching event
+- `test_routine_guardrails` -- routine respects policy constraints
+
+### Trace: Compaction and Context Pressure
+
+**Covers:** `agent/compaction.rs` (~50 lines), `agent/context_monitor.rs` (~30 lines)
+
+Test turn summarization and memory pressure detection.
+
+**Fixture:** `compaction_flow.json`
+**Tests:**
+- `test_compaction_triggers_at_threshold` -- summarize when context exceeds limit
+- `test_compaction_preserves_recent` -- keep recent turns intact
+- `test_context_pressure_warning` -- emit warning at high usage
+
+### Trace: Job Tool Coverage
+
+**Covers:** `tools/builtin/job.rs` (+308 lines), `tools/builtin/skill_tools.rs` (+110 lines)
+
+Test job and skill management tools through agent execution.
+
+**Fixture:** `job_and_skill_tools.json`
+**Tests:**
+- `test_create_and_list_jobs` -- create job, list shows it
+- `test_job_status_query` -- query status of running job
+- `test_skill_list_and_search` -- list local skills, search registry
+
+### Trace: Memory Tools
+
+**Covers:** `tools/builtin/memory.rs` (~20 lines), `workspace/` (+109 lines)
+
+Test memory operations through agent tool calls.
+
+**Fixture:** `memory_tools.json`
+**Tests:**
+- `test_memory_write_and_search` -- write doc, search finds it
+- `test_memory_read_by_path` -- read specific document
+- `test_memory_tree` -- list memory filesystem structure
+
+### Trace: Extension Management
+
+**Covers:** `tools/builtin/extension_tools.rs` (~40 lines)
+
+Test extension lifecycle via agent tool calls.
+
+**Fixture:** `extension_management.json`
+**Tests:**
+- `test_extension_install_via_tool` -- agent installs an extension
+- `test_extension_auth_via_tool` -- agent configures auth
+- `test_extension_activate_via_tool` -- agent activates extension
+
+### Trace: Self-Repair
+
+**Covers:** `agent/self_repair.rs` (~40 lines)
+
+Test stuck job detection and recovery.
+
+**Fixture:** `self_repair.json`
+**Tests:**
+- `test_stuck_job_detected` -- job stuck for > threshold triggers repair
+- `test_stuck_job_recovered` -- recovery restarts job successfully
+- `test_stuck_job_fails_permanently` -- recovery fails, job marked failed
+
+### Trace: Heartbeat
+
+**Covers:** `agent/heartbeat.rs` (+80 lines)
+
+Test periodic proactive execution.
+
+**Fixture:** `heartbeat.json`
+**Tests:**
+- `test_heartbeat_periodic_fire` -- heartbeat triggers at interval
+- `test_heartbeat_reads_checklist` -- reads HEARTBEAT.md, processes items
+- `test_heartbeat_notification` -- sends notification on findings
+
+---
+
+## Tier 3 -- Web/Channel Handler Tests (~4,500 lines)
+
+Test HTTP handlers and SSE/WS endpoints using `axum_test` or
+`tower::ServiceExt::oneshot` with a real router and in-memory database.
+
+### `src/channels/web/server.rs` -- 50% -> 95% (+893 lines)
+
+The single biggest web gap. 40+ API endpoints.
+
+**Tests to write:**
+- `test_api_health` -- GET /health returns 200
+- `test_api_chat_submit` -- POST /api/chat sends message
+- `test_api_jobs_list` -- GET /api/jobs returns job list
+- `test_api_jobs_create` -- POST /api/jobs creates job
+- `test_api_routines_crud` -- full CRUD cycle for routines
+- `test_api_settings_get_set` -- GET/PUT settings
+- `test_api_memory_search` -- POST /api/memory/search
+- `test_api_extensions_list` -- GET /api/extensions
+- `test_api_skills_list` -- GET /api/skills
+- `test_api_sse_connect` -- SSE stream connects and receives events
+- `test_api_auth_required` -- endpoints reject missing/bad tokens
+- `test_api_cors_headers` -- verify CORS configuration
+
+### `src/channels/web/handlers/chat.rs` -- 26.1% -> 95% (+388 lines)
+
+Chat message submission and SSE streaming.
+
+**Tests to write:**
+- `test_chat_submit_message` -- submit message, receive response
+- `test_chat_sse_stream` -- verify SSE event format
+- `test_chat_thread_context` -- messages scoped to thread
+- `test_chat_invalid_payload` -- reject malformed requests
+
+### `src/channels/web/handlers/jobs.rs` -- 11.1% -> 95% (+430 lines)
+
+Job CRUD endpoints.
+
+**Tests to write:**
+- `test_jobs_list_empty` -- empty list returns []
+- `test_jobs_create_and_get` -- create, then GET by ID
+- `test_jobs_cancel` -- cancel running job
+- `test_jobs_filter_by_status` -- filter by pending/running/completed
+- `test_jobs_pagination` -- limit/offset parameters
+
+### `src/channels/web/handlers/routines.rs` -- 0% -> 95% (+236 lines)
+
+Routine CRUD endpoints.
+
+**Tests to write:**
+- `test_routines_create` -- POST creates routine
+- `test_routines_list` -- GET lists all routines
+- `test_routines_update` -- PUT updates routine config
+- `test_routines_delete` -- DELETE removes routine
+- `test_routines_history` -- GET history for a routine
+
+### `src/channels/web/handlers/extensions.rs` -- 0% -> 95% (+129 lines)
+
+Extension management endpoints.
+
+**Tests to write:**
+- `test_extensions_list` -- list installed extensions
+- `test_extensions_install` -- install from manifest URL
+- `test_extensions_activate` -- activate/deactivate toggle
+- `test_extensions_remove` -- remove installed extension
+
+### `src/channels/web/handlers/memory.rs` -- 0% -> 95% (+110 lines)
+
+Memory/workspace endpoints.
+
+**Tests to write:**
+- `test_memory_search` -- search returns ranked results
+- `test_memory_write` -- write a document
+- `test_memory_read` -- read by path
+- `test_memory_tree` -- tree returns filesystem structure
+
+### `src/channels/web/handlers/settings.rs` -- 0% -> 95% (+103 lines)
+
+Settings endpoints.
+
+**Tests to write:**
+- `test_settings_get` -- retrieve current settings
+- `test_settings_update` -- update individual setting
+- `test_settings_validation` -- reject invalid setting values
+
+### `src/channels/web/handlers/static_files.rs` -- 0% -> 95% (+97 lines)
+
+Static file serving.
+
+**Tests to write:**
+- `test_static_index_html` -- GET / serves index.html
+- `test_static_css_js` -- serve CSS/JS with correct content types
+- `test_static_404` -- missing file returns 404
+
+### `src/channels/wasm/wrapper.rs` -- 58.2% -> 95% (+822 lines)
+
+WASM channel wrapper (message routing, lifecycle).
+
+**Tests to write:**
+- `test_wasm_channel_start` -- initialize WASM channel module
+- `test_wasm_channel_message_routing` -- route incoming message to WASM
+- `test_wasm_channel_response` -- return WASM response to caller
+- `test_wasm_channel_error_handling` -- handle WASM trap gracefully
+- `test_wasm_channel_lifecycle` -- start, process, shutdown
+
+### `src/channels/wasm/loader.rs` -- 38.1% -> 95% (+141 lines)
+
+WASM channel discovery.
+
+**Tests to write:**
+- `test_channel_loader_scan` -- find channel WASM modules
+- `test_channel_loader_validation` -- reject invalid modules
+- `test_channel_loader_manifest` -- parse channel capabilities
+
+### `src/channels/wasm/storage.rs` -- 0% -> 95% (+172 lines)
+
+WASM channel state persistence.
+
+**Tests to write:**
+- `test_channel_storage_save_load` -- persist and restore channel state
+- `test_channel_storage_isolation` -- per-channel state isolation
+- `test_channel_storage_cleanup` -- remove state on channel uninstall
+
+### `src/channels/signal.rs` -- 74% -> 95% (+381 lines)
+
+Signal protocol channel.
+
+**Tests to write:**
+- `test_signal_message_send` -- send encrypted message
+- `test_signal_message_receive` -- decrypt incoming message
+- `test_signal_attachment_handling` -- handle media attachments
+- `test_signal_group_message` -- group chat routing
+- `test_signal_error_handling` -- handle connection failures
+
+### `src/channels/repl.rs` -- 0% -> 95% (+221 lines)
+
+Simple REPL channel.
+
+**Tests to write:**
+- `test_repl_input_parsing` -- parse user input lines
+- `test_repl_output_formatting` -- format agent responses
+- `test_repl_multiline` -- handle multi-line input
+- `test_repl_special_commands` -- handle /quit, /help
+
+---
+
+## Tier 4 -- CLI Tests (~2,100 lines)
+
+CLI subcommands can be tested by invoking clap-parsed command structs directly
+or by calling the handler functions with constructed arguments.
+
+### `src/cli/tool.rs` -- 2.9% -> 95% (+697 lines)
+
+Tool CLI (install, list, remove, build).
+
+**Tests to write:**
+- `test_cli_tool_list` -- list installed tools
+- `test_cli_tool_install_local` -- install from local .wasm file
+- `test_cli_tool_install_registry` -- install from registry
+- `test_cli_tool_remove` -- remove installed tool
+- `test_cli_tool_build` -- scaffold and build tool project
+- `test_cli_tool_info` -- display tool details
+
+### `src/cli/mcp.rs` -- 0.9% -> 95% (+302 lines)
+
+MCP server management CLI.
+
+**Tests to write:**
+- `test_cli_mcp_list` -- list configured MCP servers
+- `test_cli_mcp_add` -- add MCP server config
+- `test_cli_mcp_remove` -- remove MCP server config
+- `test_cli_mcp_tools` -- list tools from MCP server
+- `test_cli_mcp_test_connection` -- verify MCP server reachable
+
+### `src/cli/oauth_defaults.rs` -- 54.1% -> 95% (+298 lines)
+
+OAuth default configurations.
+
+**Tests to write:**
+- `test_oauth_defaults_loading` -- load default OAuth configs
+- `test_oauth_url_construction` -- build auth/token URLs
+- `test_oauth_scope_merging` -- merge requested scopes with defaults
+- `test_oauth_provider_lookup` -- lookup by provider name
+
+### `src/cli/registry.rs` -- 0% -> 95% (+168 lines)
+
+Registry CLI commands.
+
+**Tests to write:**
+- `test_cli_registry_search` -- search for packages
+- `test_cli_registry_install` -- install package from registry
+- `test_cli_registry_info` -- display package details
+
+### `src/cli/status.rs` -- 0% -> 95% (+142 lines)
+
+Status display commands.
+
+**Tests to write:**
+- `test_cli_status_gathering` -- collect system status info
+- `test_cli_status_formatting` -- render status output
+- `test_cli_status_components` -- check individual components
+
+### `src/cli/memory.rs` -- 15.5% -> 95% (+138 lines)
+
+Memory CLI subcommands.
+
+**Tests to write:**
+- `test_cli_memory_search` -- search workspace from CLI
+- `test_cli_memory_write` -- write document from CLI
+- `test_cli_memory_read` -- read document from CLI
+- `test_cli_memory_tree` -- display memory tree
+
+### `src/cli/doctor.rs` -- 28.7% -> 95% (+115 lines)
+
+Diagnostic checks.
+
+**Tests to write:**
+- `test_doctor_check_database` -- verify DB connectivity check
+- `test_doctor_check_llm` -- verify LLM provider check
+- `test_doctor_check_tools` -- verify tool availability check
+- `test_doctor_report_format` -- verify output format
+
+### `src/cli/config.rs` -- 36.5% -> 95% (~100 lines)
+
+Config CLI subcommands.
+
+**Tests to write:**
+- `test_cli_config_get` -- read config value
+- `test_cli_config_set` -- write config value
+- `test_cli_config_list` -- list all config keys
+- `test_cli_config_reset` -- reset to defaults
+
+---
+
+## Tier 5 -- Setup/Infra Tests (~2,400 lines)
+
+Hardest to test: interactive wizards, Docker, process spawning. Strategy: extract
+pure logic into testable functions, test the interactive parts by injecting mock
+input.
+
+### `src/setup/wizard.rs` -- 16.8% -> 95% (+1,681 lines)
+
+7-step interactive onboarding wizard. Refactor to extract validation functions,
+step logic, and config generation into testable units.
+
+**Tests to write:**
+- `test_wizard_step_validation` -- each step validates input correctly
+- `test_wizard_config_generation` -- generate config from wizard answers
+- `test_wizard_default_values` -- verify sensible defaults
+- `test_wizard_skip_completed` -- skip already-configured steps
+- `test_wizard_llm_backend_selection` -- provider-specific config paths
+- `test_wizard_channel_setup` -- channel configuration logic
+
+### `src/setup/channels.rs` -- 7.6% -> 95% (+563 lines)
+
+Channel setup helpers.
+
+**Tests to write:**
+- `test_channel_setup_defaults` -- default channel configuration
+- `test_channel_setup_validation` -- reject invalid channel configs
+- `test_channel_setup_telegram` -- Telegram-specific setup logic
+- `test_channel_setup_signal` -- Signal-specific setup logic
+- `test_channel_setup_webhook` -- webhook URL validation
+
+### `src/setup/prompts.rs` -- 24.8% -> 95% (+147 lines)
+
+Terminal prompt utilities.
+
+**Tests to write:**
+- `test_prompt_select` -- selection from list
+- `test_prompt_confirm` -- yes/no confirmation
+- `test_prompt_secret` -- masked input
+- `test_prompt_validation` -- input validation rules
+
+### `src/sandbox/container.rs` -- 22.1% -> 95% (+296 lines)
+
+Docker container lifecycle. Test command construction without actual Docker.
+
+**Tests to write:**
+- `test_container_config_to_docker_args` -- generate correct docker run args
+- `test_container_volume_mounts` -- workspace mount configuration
+- `test_container_env_scrubbing` -- sensitive env vars removed
+- `test_container_resource_limits` -- CPU/memory limit args
+- `test_container_network_config` -- proxy network setup
+
+### `src/sandbox/manager.rs` -- 59% -> 95% (+114 lines)
+
+Sandbox orchestration.
+
+**Tests to write:**
+- `test_sandbox_policy_enforcement` -- policy to container config mapping
+- `test_sandbox_cleanup` -- cleanup on job completion
+- `test_sandbox_concurrent_limit` -- enforce max concurrent containers
+
+### `src/sandbox/proxy/http.rs` -- 37.5% -> 95% (+176 lines)
+
+HTTP proxy for container network access.
+
+**Tests to write:**
+- `test_proxy_allowlist_enforcement` -- block disallowed domains
+- `test_proxy_credential_injection` -- inject auth headers
+- `test_proxy_connect_tunnel` -- HTTPS CONNECT method handling
+- `test_proxy_logging` -- request/response logging
+
+### `src/worker/runtime.rs` -- 5.7% -> 95% (+312 lines)
+
+Worker execution loop (runs inside containers).
+
+**Tests to write:**
+- `test_worker_tool_dispatch` -- dispatch tool call, return result
+- `test_worker_llm_interaction` -- send prompt, receive response
+- `test_worker_turn_limit` -- enforce max turns
+- `test_worker_error_propagation` -- tool error surfaces to agent
+
+### `src/worker/claude_bridge.rs` -- 60.7% -> 95% (+215 lines)
+
+Claude CLI bridge.
+
+**Tests to write:**
+- `test_claude_command_construction` -- build claude CLI command
+- `test_claude_output_parsing` -- parse claude CLI JSON output
+- `test_claude_error_handling` -- handle CLI crashes gracefully
+- `test_claude_config_injection` -- inject config dir and model
+
+### `src/worker/api.rs` -- 19.8% -> 95% (+194 lines)
+
+Worker HTTP client to orchestrator.
+
+**Tests to write:**
+- `test_worker_api_request_building` -- correct endpoint URLs and headers
+- `test_worker_api_response_parsing` -- parse orchestrator responses
+- `test_worker_api_auth_token` -- bearer token injection
+- `test_worker_api_retry` -- retry on transient failures
+
+### `src/main.rs` -- 29.4% -> 95% (+485 lines)
+
+Entry point and startup. Extract startup logic into testable functions.
+
+**Tests to write:**
+- `test_cli_arg_parsing` -- verify clap argument parsing
+- `test_startup_config_loading` -- config from env + file
+- `test_startup_channel_selection` -- select channels from config
+- `test_startup_feature_flags` -- feature-gated code paths
+
+---
+
+## Tier 6 -- Remaining Files to 95% (~2,000 lines)
+
+Smaller files that each need a handful of additional tests.
+
+| File | Lines Needed | Test Focus |
+|------|-------------:|------------|
+| `src/tools/builtin/skill_tools.rs` | 110 | skill_list, skill_search, skill_install, skill_remove |
+| `src/hooks/bundled.rs` | 115 | bundled hook execution, hook discovery |
+| `src/registry/installer.rs` | 272 | package download, verification, installation |
+| `src/registry/artifacts.rs` | 72 | artifact packaging, checksums |
+| `src/orchestrator/job_manager.rs` | 249 | container lifecycle, job routing |
+| `src/orchestrator/api.rs` | 125 | LLM proxy, event dispatch endpoints |
+| `src/app.rs` | 137 | AppBuilder configuration, startup sequence |
+| `src/service.rs` | 120 | service lifecycle, signal handling |
+| `src/config/channels.rs` | 55 | channel config parsing |
+| `src/config/sandbox.rs` | 61 | sandbox config parsing |
+| `src/config/tunnel.rs` | 43 | tunnel config parsing |
+| `src/config/mod.rs` | 63 | config merging, env override |
+| `src/config/database.rs` | 38 | database URL parsing |
+| `src/evaluation/success.rs` | 34 | success evaluator logic |
+| `src/evaluation/metrics.rs` | 40 | metrics collection |
+| `src/context/manager.rs` | 57 | concurrent job context isolation |
+| `src/context/memory.rs` | 36 | action recording, conversation memory |
+
+---
+
+## Execution Priority
+
+Maximize coverage gain per unit of effort:
+
+| Order | Category | Lines Gained | Effort |
+|------:|----------|-------------:|--------|
+| 1 | Trace tests (Tier 2) | ~7,000 | Medium (high leverage, each test covers many modules) |
+| 2 | Unit tests for 0% files (Tier 1 subset) | ~3,500 | Low (pure logic, no infrastructure) |
+| 3 | Web handler tests (Tier 3) | ~4,500 | Medium (axum_test + in-memory DB) |
+| 4 | Extension/MCP/WASM unit tests (Tier 1 remainder) | ~3,500 | Medium |
+| 5 | CLI subcommand tests (Tier 4) | ~2,100 | Low-Medium |
+| 6 | Setup wizard extraction + tests (Tier 5) | ~2,400 | High (requires refactoring) |
+| 7 | LLM provider tests (Tier 1 subset) | ~800 | Medium |
+| 8 | Remaining small files (Tier 6) | ~2,000 | Low |
+
+## Notes
+
+- All trace tests require `--features libsql` and use `TestRigBuilder` from `tests/support/`
+- Web handler tests can use `axum::test` helpers or build the router directly
+- CLI tests should call handler functions directly, not shell out to the binary
+- Setup wizard tests require extracting pure logic from interactive prompts first
+- Sandbox/container tests should verify command construction, not run Docker
+- Worker tests can use `TraceLlm` for the LLM provider, same as trace tests
diff --git a/Cargo.lock b/Cargo.lock
index f35afcda..58bd543f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
[[package]]
name = "aead"
version = "0.5.2"
@@ -74,7 +83,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"version_check",
- "zerocopy 0.8.37",
+ "zerocopy 0.8.39",
]
[[package]]
@@ -174,9 +183,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.100"
+version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "approx"
@@ -201,6 +210,9 @@ name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
[[package]]
name = "arc-swap"
@@ -411,7 +423,7 @@ version = "57.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"serde_core",
"serde_json",
]
@@ -453,6 +465,15 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063"
+[[package]]
+name = "astral-tl"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d90933ffb0f97e2fc2e0de21da9d3f20597b804012d199843a6fe7c2810d28f3"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -479,9 +500,9 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.39"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
@@ -491,9 +512,9 @@ dependencies = [
[[package]]
name = "async-executor"
-version = "1.13.3"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
@@ -527,7 +548,7 @@ dependencies = [
"futures-lite",
"parking",
"polling",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"slab",
"windows-sys 0.61.2",
]
@@ -558,7 +579,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite",
- "rustix 1.1.3",
+ "rustix 1.1.4",
]
[[package]]
@@ -569,7 +590,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -584,7 +605,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
@@ -609,7 +630,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -626,7 +647,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -771,6 +792,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
[[package]]
name = "bigdecimal"
version = "0.4.10"
@@ -799,7 +826,7 @@ version = "0.66.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cexpr",
"clang-sys",
"lazy_static",
@@ -812,7 +839,7 @@ dependencies = [
"regex",
"rustc-hash 1.1.0",
"shlex",
- "syn 2.0.114",
+ "syn 2.0.117",
"which",
]
@@ -824,9 +851,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.10.0"
+version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bitpacking"
@@ -920,13 +947,13 @@ dependencies = [
"http-body-util",
"hyper 1.8.1",
"hyper-named-pipe",
- "hyper-rustls",
+ "hyper-rustls 0.27.7",
"hyper-util",
"hyperlocal",
"log",
"pin-project-lite",
- "rustls",
- "rustls-native-certs",
+ "rustls 0.23.37",
+ "rustls-native-certs 0.8.3",
"rustls-pemfile",
"rustls-pki-types",
"serde",
@@ -975,7 +1002,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -998,7 +1025,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -1024,9 +1051,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.19.1"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
dependencies = [
"allocator-api2",
]
@@ -1067,9 +1094,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
dependencies = [
"serde",
]
@@ -1094,7 +1121,7 @@ checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7"
dependencies = [
"cap-primitives",
"cap-std",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"smallvec",
]
@@ -1110,7 +1137,7 @@ dependencies = [
"io-lifetimes",
"ipnet",
"maybe-owned",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"rustix-linux-procfs",
"windows-sys 0.59.0",
"winx",
@@ -1135,7 +1162,7 @@ dependencies = [
"cap-primitives",
"io-extras",
"io-lifetimes",
- "rustix 1.1.3",
+ "rustix 1.1.4",
]
[[package]]
@@ -1148,7 +1175,7 @@ dependencies = [
"cap-primitives",
"iana-time-zone",
"once_cell",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"winx",
]
@@ -1163,9 +1190,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.55"
+version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1202,9 +1229,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
-version = "0.4.43"
+version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -1247,9 +1274,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.56"
+version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
+checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1257,9 +1284,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.56"
+version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
+checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
@@ -1267,6 +1294,15 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_complete"
+version = "4.5.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
+dependencies = [
+ "clap",
+]
+
[[package]]
name = "clap_derive"
version = "4.5.55"
@@ -1276,14 +1312,14 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
name = "clap_lex"
-version = "0.7.7"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
+checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clipboard-win"
@@ -1316,14 +1352,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
dependencies = [
"unicode-segmentation",
- "unicode-width 0.2.0",
+ "unicode-width 0.2.2",
]
[[package]]
name = "compression-codecs"
-version = "0.4.36"
+version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"compression-core",
"flate2",
@@ -1345,6 +1381,24 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
[[package]]
name = "const-random"
version = "0.1.18"
@@ -1540,6 +1594,21 @@ dependencies = [
"target-lexicon",
]
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1572,7 +1641,7 @@ dependencies = [
"proc-macro2",
"quote",
"strict",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -1648,7 +1717,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"crossterm_winapi",
"mio",
"parking_lot",
@@ -1664,13 +1733,13 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"crossterm_winapi",
"derive_more",
"document-features",
"mio",
"parking_lot",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -1702,6 +1771,29 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf 0.13.1",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "csv"
version = "1.4.0"
@@ -1732,6 +1824,33 @@ dependencies = [
"cipher",
]
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "darling"
version = "0.21.3"
@@ -1753,7 +1872,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -1764,7 +1883,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -2217,7 +2336,7 @@ checksum = "1063ad4c9e094b3f798acee16d9a47bd7372d9699be2de21b05c3bd3f34ab848"
dependencies = [
"datafusion-doc",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -2452,15 +2571,49 @@ dependencies = [
]
[[package]]
-name = "deranged"
-version = "0.5.5"
+name = "der"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "der_derive",
+ "flagset",
+ "zeroize",
+]
+
+[[package]]
+name = "der_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
"serde_core",
]
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -2480,7 +2633,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -2570,7 +2723,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -2605,6 +2758,21 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
[[package]]
name = "dyn-clone"
version = "1.0.20"
@@ -2621,6 +2789,36 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ego-tree"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
+
[[package]]
name = "either"
version = "1.15.0"
@@ -2639,6 +2837,12 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -2678,7 +2882,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -2720,6 +2924,15 @@ version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b"
+[[package]]
+name = "euclid"
+version = "0.20.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "event-listener"
version = "5.4.1"
@@ -2795,10 +3008,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"windows-sys 0.59.0",
]
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "filetime"
version = "0.2.27"
@@ -2822,13 +3041,19 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+[[package]]
+name = "flagset"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
+
[[package]]
name = "flatbuffers"
version = "25.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"rustc_version",
]
@@ -2860,6 +3085,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -2891,7 +3122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
dependencies = [
"io-lifetimes",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"windows-sys 0.59.0",
]
@@ -2941,10 +3172,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
-name = "futures"
-version = "0.3.31"
+name = "futf"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
@@ -2957,9 +3198,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
@@ -2967,15 +3208,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@@ -2984,9 +3225,9 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
@@ -3003,26 +3244,26 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
name = "futures-sink"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
@@ -3032,9 +3273,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
@@ -3044,7 +3285,6 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
- "pin-utils",
"slab",
]
@@ -3063,7 +3303,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"debugid",
"fxhash",
"serde",
@@ -3217,6 +3457,15 @@ dependencies = [
"libm",
]
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width 0.2.2",
+]
+
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -3244,6 +3493,19 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
[[package]]
name = "ghash"
version = "0.5.1"
@@ -3318,7 +3580,7 @@ dependencies = [
"cfg-if",
"crunchy",
"num-traits",
- "zerocopy 0.8.37",
+ "zerocopy 0.8.39",
]
[[package]]
@@ -3358,7 +3620,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
- "foldhash",
+ "foldhash 0.1.5",
"serde",
]
@@ -3367,6 +3629,11 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
[[package]]
name = "hashlink"
@@ -3425,11 +3692,59 @@ dependencies = [
[[package]]
name = "home"
-version = "0.5.11"
+version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "html-to-markdown-rs"
+version = "2.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05335c6bf406653110ad8447c84461c6d0cda5e0aff9d3d3518f87502d30abe"
+dependencies = [
+ "ahash 0.8.12",
+ "astral-tl",
+ "base64 0.22.1",
+ "html-escape",
+ "html5ever 0.38.0",
+ "lru 0.16.3",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e"
+dependencies = [
+ "log",
+ "markup5ever 0.36.1",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever 0.38.0",
]
[[package]]
@@ -3579,6 +3894,24 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070"
+dependencies = [
+ "futures-util",
+ "http 0.2.12",
+ "hyper 0.14.32",
+ "log",
+ "rustls 0.22.4",
+ "rustls-native-certs 0.7.3",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "webpki-roots 0.26.11",
+]
+
[[package]]
name = "hyper-rustls"
version = "0.27.7"
@@ -3588,11 +3921,11 @@ dependencies = [
"http 1.4.0",
"hyper 1.8.1",
"hyper-util",
- "rustls",
- "rustls-native-certs",
+ "rustls 0.23.37",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
- "tokio-rustls",
+ "tokio-rustls 0.26.4",
"tower-service",
]
@@ -3887,6 +4220,18 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "insta"
+version = "1.46.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4"
+dependencies = [
+ "console",
+ "once_cell",
+ "similar",
+ "tempfile",
+]
+
[[package]]
name = "io-extras"
version = "0.18.4"
@@ -3921,7 +4266,7 @@ dependencies = [
[[package]]
name = "ironclaw"
-version = "0.5.0"
+version = "0.16.1"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -3935,47 +4280,64 @@ dependencies = [
"bollard",
"bytes",
"chrono",
+ "chrono-tz",
"clap",
+ "clap_complete",
"cron",
"crossterm 0.28.1",
"deadpool-postgres",
"dirs 6.0.0",
"dotenvy",
+ "ed25519-dalek",
+ "flate2",
"fs4 0.6.6",
"futures",
+ "hex",
"hkdf",
+ "hmac",
+ "html-to-markdown-rs",
"http-body-util",
"hyper 1.8.1",
"hyper-util",
+ "iana-time-zone",
+ "insta",
"lancedb",
"libsql",
+ "lru 0.16.3",
"mime_guess",
"open",
+ "pdf-extract",
"pgvector",
"postgres-types",
"pretty_assertions",
"rand 0.8.5",
+ "readabilityrs",
"refinery",
"regex",
"reqwest",
"rig-core",
"rust_decimal",
"rust_decimal_macros",
+ "rustls 0.23.37",
+ "rustls-native-certs 0.8.3",
"rustyline",
"secrecy",
"secret-service",
- "security-framework 3.5.1",
+ "security-framework 3.7.0",
+ "semver",
"serde",
"serde_json",
"serde_yml",
"sha2",
"subtle",
+ "tar",
"tempfile",
"termimad",
"testcontainers-modules",
"thiserror 2.0.18",
"tokio",
"tokio-postgres",
+ "tokio-postgres-rustls",
"tokio-stream",
"tokio-test",
"tokio-tungstenite 0.26.2",
@@ -3984,6 +4346,7 @@ dependencies = [
"tower-http 0.6.8",
"tracing",
"tracing-subscriber",
+ "tracing-test",
"url",
"urlencoding",
"uuid",
@@ -3991,30 +4354,7 @@ dependencies = [
"wasmtime",
"wasmtime-wasi",
"zbus",
-]
-
-[[package]]
-name = "ironclaw-bench"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-trait",
- "chrono",
- "clap",
- "futures",
- "ironclaw",
- "regex",
- "rust_decimal",
- "serde",
- "serde_json",
- "tempfile",
- "thiserror 2.0.18",
- "tokio",
- "tokio-stream",
- "toml",
- "tracing",
- "tracing-subscriber",
- "uuid",
+ "zip",
]
[[package]]
@@ -4106,9 +4446,9 @@ dependencies = [
[[package]]
name = "jiff"
-version = "0.2.20"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
+checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -4121,20 +4461,20 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.20"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
+checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
name = "jiff-tzdb"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2"
+checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
[[package]]
name = "jiff-tzdb-platform"
@@ -4157,9 +4497,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.85"
+version = "0.3.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -4185,6 +4525,21 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "kuchikikiki"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73885c6a3cefdf7a1db0327cefbe4b9b72cac94cae4b19ede4fa492d8af02a0"
+dependencies = [
+ "bitflags 2.11.0",
+ "crc",
+ "cssparser",
+ "html5ever 0.38.0",
+ "indexmap 2.13.0",
+ "precomputed-hash",
+ "selectors 0.35.0",
+]
+
[[package]]
name = "lance"
version = "2.0.0"
@@ -4562,7 +4917,7 @@ dependencies = [
"prost 0.14.3",
"rand 0.9.2",
"serde",
- "shellexpand 3.1.1",
+ "shellexpand 3.1.2",
"snafu",
"tokio",
"tracing",
@@ -4755,9 +5110,9 @@ dependencies = [
[[package]]
name = "lazy-regex"
-version = "3.5.1"
+version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5c13b6857ade4c8ee05c3c3dc97d2ab5415d691213825b90d3211c425c1f907"
+checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
@@ -4766,14 +5121,14 @@ dependencies = [
[[package]]
name = "lazy-regex-proc_macros"
-version = "3.5.1"
+version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a95c68db5d41694cea563c86a4ba4dc02141c16ef64814108cb23def4d5438"
+checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358"
dependencies = [
"proc-macro2",
"quote",
"regex",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -4865,9 +5220,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.180"
+version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libloading"
@@ -4891,9 +5246,9 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"libc",
- "redox_syscall 0.7.0",
+ "redox_syscall 0.7.2",
]
[[package]]
@@ -4905,21 +5260,26 @@ dependencies = [
"anyhow",
"async-stream",
"async-trait",
+ "base64 0.21.7",
"bincode",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"bytes",
"fallible-iterator 0.3.0",
"futures",
"http 0.2.12",
"hyper 0.14.32",
+ "hyper-rustls 0.25.0",
+ "libsql-hrana",
"libsql-sqlite3-parser",
"libsql-sys",
"libsql_replication",
"parking_lot",
"serde",
+ "serde_json",
"thiserror 1.0.69",
"tokio",
"tokio-stream",
+ "tokio-util",
"tonic",
"tonic-web",
"tower 0.4.13",
@@ -4939,13 +5299,25 @@ dependencies = [
"cc",
]
+[[package]]
+name = "libsql-hrana"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeaf5d19e365465e1c23d687a28c805d7462531b3f619f0ba49d3cf369890a3e"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "prost 0.12.6",
+ "serde",
+]
+
[[package]]
name = "libsql-rusqlite"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae65c66088dcd309abbd5617ae046abac2a2ee0a7fdada5127353bd68e0a27ea"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"fallible-iterator 0.2.0",
"fallible-streaming-iterator",
"hashlink",
@@ -4959,14 +5331,14 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15a90128c708356af8f7d767c9ac2946692c9112b4f74f07b99a01a60680e413"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cc",
"fallible-iterator 0.3.0",
"indexmap 2.13.0",
"log",
"memchr",
"phf 0.11.3",
- "phf_codegen",
+ "phf_codegen 0.11.3",
"phf_shared 0.11.3",
"uncased",
]
@@ -5029,9 +5401,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
-version = "0.11.0"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
@@ -5073,6 +5445,24 @@ dependencies = [
"tracing-subscriber",
]
+[[package]]
+name = "lopdf"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5c8ecfc6c72051981c0459f75ccc585e7ff67c70829560cda8e647882a9abff"
+dependencies = [
+ "encoding_rs",
+ "flate2",
+ "indexmap 2.13.0",
+ "itoa",
+ "log",
+ "md-5",
+ "nom 7.1.3",
+ "rangemap",
+ "time",
+ "weezl",
+]
+
[[package]]
name = "lru"
version = "0.12.5"
@@ -5082,6 +5472,15 @@ dependencies = [
"hashbrown 0.15.5",
]
+[[package]]
+name = "lru"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+dependencies = [
+ "hashbrown 0.16.1",
+]
+
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -5122,6 +5521,12 @@ dependencies = [
"twox-hash",
]
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
[[package]]
name = "mach2"
version = "0.4.3"
@@ -5131,6 +5536,28 @@ dependencies = [
"libc",
]
+[[package]]
+name = "markup5ever"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c"
+dependencies = [
+ "log",
+ "tendril 0.4.3",
+ "web_atoms",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril 0.5.0",
+ "web_atoms",
+]
+
[[package]]
name = "matchers"
version = "0.2.0"
@@ -5192,9 +5619,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.7.6"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memfd"
@@ -5202,7 +5629,7 @@ version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227"
dependencies = [
- "rustix 1.1.3",
+ "rustix 1.1.4",
]
[[package]]
@@ -5284,9 +5711,9 @@ checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6"
[[package]]
name = "moka"
-version = "0.12.13"
+version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
+checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b"
dependencies = [
"async-lock",
"crossbeam-channel",
@@ -5325,17 +5752,17 @@ dependencies = [
[[package]]
name = "native-tls"
-version = "0.2.14"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
- "openssl-probe 0.1.6",
+ "openssl-probe 0.2.1",
"openssl-sys",
"schannel",
- "security-framework 2.11.1",
+ "security-framework 3.7.0",
"security-framework-sys",
"tempfile",
]
@@ -5355,6 +5782,12 @@ dependencies = [
"rawpointer",
]
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
[[package]]
name = "nibble_vec"
version = "0.1.0"
@@ -5370,7 +5803,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -5383,7 +5816,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -5452,9 +5885,9 @@ dependencies = [
[[package]]
name = "num-conv"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-integer"
@@ -5526,7 +5959,25 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
+name = "objc2-system-configuration"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
+dependencies = [
+ "objc2-core-foundation",
]
[[package]]
@@ -5615,7 +6066,7 @@ version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cfg-if",
"foreign-types",
"libc",
@@ -5632,7 +6083,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -5744,7 +6195,7 @@ dependencies = [
"regex",
"regex-syntax",
"structmeta",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -5771,6 +6222,21 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+[[package]]
+name = "pdf-extract"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbb3a5387b94b9053c1e69d8abfd4dd6dae7afda65a5c5279bc1f42ab39df575"
+dependencies = [
+ "adobe-cmap-parser",
+ "encoding_rs",
+ "euclid",
+ "lopdf",
+ "postscript",
+ "type1-encoding-parser",
+ "unicode-normalization",
+]
+
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@@ -5835,6 +6301,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
+ "phf_macros",
"phf_shared 0.13.1",
"serde",
]
@@ -5845,10 +6312,20 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
- "phf_generator",
+ "phf_generator 0.11.3",
"phf_shared 0.11.3",
]
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+]
+
[[package]]
name = "phf_generator"
version = "0.11.3"
@@ -5859,6 +6336,29 @@ dependencies = [
"rand 0.8.5",
]
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "phf_shared"
version = "0.11.3"
@@ -5904,7 +6404,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -5930,6 +6430,16 @@ dependencies = [
"futures-io",
]
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -5946,7 +6456,7 @@ dependencies = [
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"windows-sys 0.61.2",
]
@@ -5962,6 +6472,12 @@ dependencies = [
"universal-hash",
]
+[[package]]
+name = "pom"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
+
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -6022,6 +6538,12 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "postscript"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306"
+
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -6043,9 +6565,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "zerocopy 0.8.37",
+ "zerocopy 0.8.39",
]
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -6063,7 +6591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6119,7 +6647,7 @@ dependencies = [
"prost 0.14.3",
"prost-types",
"regex",
- "syn 2.0.114",
+ "syn 2.0.117",
"tempfile",
]
@@ -6133,7 +6661,7 @@ dependencies = [
"itertools 0.12.1",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6146,7 +6674,7 @@ dependencies = [
"itertools 0.12.1",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6160,9 +6688,9 @@ dependencies = [
[[package]]
name = "psm"
-version = "0.1.29"
+version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa96cb91275ed31d6da3e983447320c4eb219ac180fa1679a0889ff32861e2d"
+checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
dependencies = [
"ar_archive_writer",
"cc",
@@ -6211,7 +6739,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
- "rustls",
+ "rustls 0.23.37",
"socket2 0.6.2",
"thiserror 2.0.18",
"tokio",
@@ -6231,7 +6759,7 @@ dependencies = [
"rand 0.9.2",
"ring",
"rustc-hash 2.1.1",
- "rustls",
+ "rustls 0.23.37",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
@@ -6418,6 +6946,24 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "readabilityrs"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eb174b0af6c181a87d68b42800806657bfbdf88b566f819aaadb9d2a7b7699d"
+dependencies = [
+ "bitflags 2.11.0",
+ "kuchikikiki",
+ "once_cell",
+ "regex",
+ "scraper",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "url",
+ "v_htmlescape",
+]
+
[[package]]
name = "redox_syscall"
version = "0.3.5"
@@ -6433,16 +6979,16 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
]
[[package]]
name = "redox_syscall"
-version = "0.7.0"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
+checksum = "6d94dd2f7cd932d4dc02cc8b2b50dfd38bd079a4e5d79198b99743d7fcf9a4b4"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
]
[[package]]
@@ -6484,7 +7030,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6529,7 +7075,7 @@ dependencies = [
"quote",
"refinery-core",
"regex",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6548,9 +7094,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.12.2"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -6560,9 +7106,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -6571,9 +7117,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.8"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "relative-path"
@@ -6606,7 +7152,7 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
- "hyper-rustls",
+ "hyper-rustls 0.27.7",
"hyper-tls",
"hyper-util",
"js-sys",
@@ -6617,8 +7163,8 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
- "rustls",
- "rustls-native-certs",
+ "rustls 0.23.37",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"serde",
"serde_json",
@@ -6626,7 +7172,7 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-native-tls",
- "tokio-rustls",
+ "tokio-rustls 0.26.4",
"tokio-util",
"tower 0.5.3",
"tower-http 0.6.8",
@@ -6765,7 +7311,7 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
- "syn 2.0.114",
+ "syn 2.0.117",
"unicode-ident",
]
@@ -6803,7 +7349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
dependencies = [
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -6839,7 +7385,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -6848,14 +7394,14 @@ dependencies = [
[[package]]
name = "rustix"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"errno",
"libc",
- "linux-raw-sys 0.11.0",
+ "linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@@ -6866,23 +7412,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
dependencies = [
"once_cell",
- "rustix 1.1.3",
+ "rustix 1.1.4",
]
[[package]]
name = "rustls"
-version = "0.23.36"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
+checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
- "rustls-webpki",
+ "rustls-webpki 0.103.9",
"subtle",
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
+dependencies = [
+ "openssl-probe 0.1.6",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 2.11.1",
+]
+
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
@@ -6892,7 +7465,7 @@ dependencies = [
"openssl-probe 0.2.1",
"rustls-pki-types",
"schannel",
- "security-framework 3.5.1",
+ "security-framework 3.7.0",
]
[[package]]
@@ -6914,6 +7487,17 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-webpki"
+version = "0.102.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustls-webpki"
version = "0.103.9"
@@ -6937,7 +7521,7 @@ version = "17.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cfg-if",
"clipboard-win",
"fd-lock",
@@ -6949,7 +7533,7 @@ dependencies = [
"radix_trie",
"rustyline-derive",
"unicode-segmentation",
- "unicode-width 0.2.0",
+ "unicode-width 0.2.2",
"utf8parse",
"windows-sys 0.60.2",
]
@@ -6962,14 +7546,14 @@ checksum = "5d66de233f908aebf9cc30ac75ef9103185b4b715c6f2fb7a626aa5e5ede53ab"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
name = "ryu"
-version = "1.0.22"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
@@ -7023,7 +7607,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7038,6 +7622,21 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "scraper"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93cecd86d6259499c844440546d02f55f3e17bd286e529e48d1f9f67e92315cb"
+dependencies = [
+ "cssparser",
+ "ego-tree",
+ "getopts",
+ "html5ever 0.36.1",
+ "precomputed-hash",
+ "selectors 0.33.0",
+ "tendril 0.4.3",
+]
+
[[package]]
name = "seahash"
version = "4.1.0"
@@ -7079,7 +7678,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
@@ -7088,11 +7687,11 @@ dependencies = [
[[package]]
name = "security-framework"
-version = "3.5.1"
+version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -7101,14 +7700,52 @@ dependencies = [
[[package]]
name = "security-framework-sys"
-version = "2.15.0"
+version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
+[[package]]
+name = "selectors"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7"
+dependencies = [
+ "bitflags 2.11.0",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf 0.13.1",
+ "phf_codegen 0.13.1",
+ "precomputed-hash",
+ "rustc-hash 2.1.1",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "selectors"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fdfed56cd634f04fe8b9ddf947ae3dc493483e819593d2ba17df9ad05db8b2"
+dependencies = [
+ "bitflags 2.11.0",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf 0.13.1",
+ "phf_codegen 0.13.1",
+ "precomputed-hash",
+ "rustc-hash 2.1.1",
+ "servo_arc",
+ "smallvec",
+]
+
[[package]]
name = "semver"
version = "1.0.27"
@@ -7152,7 +7789,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7163,7 +7800,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7198,7 +7835,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7224,9 +7861,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.16.1"
+version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
+checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -7243,14 +7880,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.16.1"
+version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
+checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
dependencies = [
"darling",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7268,6 +7905,15 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
[[package]]
name = "sha1"
version = "0.10.6"
@@ -7279,6 +7925,12 @@ dependencies = [
"digest",
]
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -7310,9 +7962,9 @@ dependencies = [
[[package]]
name = "shellexpand"
-version = "3.1.1"
+version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
+checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
dependencies = [
"dirs 6.0.0",
]
@@ -7354,6 +8006,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "simd-adler32"
version = "0.3.8"
@@ -7366,6 +8027,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
[[package]]
name = "siphasher"
version = "1.0.2"
@@ -7374,9 +8041,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "sketches-ddsketch"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a"
+checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
dependencies = [
"serde",
]
@@ -7414,7 +8081,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7449,6 +8116,16 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
[[package]]
name = "sptr"
version = "0.3.2"
@@ -7473,7 +8150,7 @@ checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7506,6 +8183,30 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006"
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.13.1",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "stringprep"
version = "0.1.5"
@@ -7532,7 +8233,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7543,7 +8244,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7565,7 +8266,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7587,9 +8288,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.114"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -7619,7 +8320,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7628,7 +8329,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -7649,7 +8350,7 @@ version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"cap-fs-ext",
"cap-std",
"fd-lock",
@@ -7689,7 +8390,7 @@ dependencies = [
"itertools 0.14.0",
"levenshtein_automata",
"log",
- "lru",
+ "lru 0.12.5",
"lz4_flex 0.11.5",
"measure_time",
"memmap2",
@@ -7817,6 +8518,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -7825,17 +8537,38 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
-version = "3.24.0"
+version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.1",
"once_cell",
- "rustix 1.1.3",
+ "rustix 1.1.4",
"windows-sys 0.61.2",
]
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
+dependencies = [
+ "new_debug_unreachable",
+ "utf-8",
+]
+
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -7925,7 +8658,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7936,7 +8669,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -7959,9 +8692,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.45"
+version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
@@ -7974,15 +8707,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
-version = "0.2.25"
+version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
@@ -8022,6 +8755,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "tls_codec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
+dependencies = [
+ "tls_codec_derive",
+ "zeroize",
+]
+
+[[package]]
+name = "tls_codec_derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "tokio"
version = "1.49.0"
@@ -8058,7 +8812,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -8097,13 +8851,39 @@ dependencies = [
"whoami",
]
+[[package]]
+name = "tokio-postgres-rustls"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144"
+dependencies = [
+ "const-oid",
+ "ring",
+ "rustls 0.23.37",
+ "tokio",
+ "tokio-postgres",
+ "tokio-rustls 0.26.4",
+ "x509-cert",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
+dependencies = [
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+]
+
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "rustls",
+ "rustls 0.23.37",
"tokio",
]
@@ -8240,9 +9020,9 @@ dependencies = [
[[package]]
name = "toml_parser"
-version = "1.0.6+spec-1.1.0"
+version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
@@ -8342,7 +9122,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"bytes",
"futures-core",
"futures-util",
@@ -8363,7 +9143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"async-compression",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"bytes",
"futures-core",
"futures-util",
@@ -8412,7 +9192,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -8479,6 +9259,27 @@ dependencies = [
"tracing-serde",
]
+[[package]]
+name = "tracing-test"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051"
+dependencies = [
+ "tracing-core",
+ "tracing-subscriber",
+ "tracing-test-macro",
+]
+
+[[package]]
+name = "tracing-test-macro"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -8528,6 +9329,15 @@ dependencies = [
"rand 0.9.2",
]
+[[package]]
+name = "type1-encoding-parser"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3d6cc09e1a99c7e01f2afe4953789311a1c50baebbdac5b477ecf78e2e92a5b"
+dependencies = [
+ "pom",
+]
+
[[package]]
name = "typenum"
version = "1.19.0"
@@ -8568,9 +9378,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
-version = "1.0.22"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-normalization"
@@ -8601,9 +9411,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
-version = "0.2.0"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
@@ -8658,6 +9468,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -8672,16 +9488,23 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.20.0"
+version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
- "getrandom 0.3.4",
+ "getrandom 0.4.1",
"js-sys",
"serde_core",
+ "sha1_smol",
"wasm-bindgen",
]
+[[package]]
+name = "v_htmlescape"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
+
[[package]]
name = "valuable"
version = "0.1.1"
@@ -8736,9 +9559,18 @@ dependencies = [
[[package]]
name = "wasip2"
-version = "1.0.1+wasi-0.2.4"
+version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
@@ -8754,9 +9586,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.108"
+version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
+checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"
dependencies = [
"cfg-if",
"once_cell",
@@ -8767,9 +9599,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.58"
+version = "0.4.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a"
dependencies = [
"cfg-if",
"futures-util",
@@ -8781,9 +9613,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.108"
+version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
+checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -8791,22 +9623,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.108"
+version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
+checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.108"
+version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
+checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"
dependencies = [
"unicode-ident",
]
@@ -8831,6 +9663,28 @@ dependencies = [
"wasmparser 0.244.0",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.245.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.245.1",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.13.0",
+ "wasm-encoder 0.244.0",
+ "wasmparser 0.244.0",
+]
+
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -8851,7 +9705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25"
dependencies = [
"ahash 0.8.12",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"hashbrown 0.14.5",
"indexmap 2.13.0",
"semver",
@@ -8864,7 +9718,7 @@ version = "0.221.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"hashbrown 0.15.5",
"indexmap 2.13.0",
"semver",
@@ -8877,7 +9731,19 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.0",
+ "semver",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.245.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e"
+dependencies = [
+ "bitflags 2.11.0",
"indexmap 2.13.0",
"semver",
]
@@ -8902,7 +9768,7 @@ dependencies = [
"addr2line",
"anyhow",
"async-trait",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"bumpalo",
"cc",
"cfg-if",
@@ -8988,10 +9854,10 @@ dependencies = [
"anyhow",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"wasmtime-component-util",
"wasmtime-wit-bindgen",
- "wit-parser",
+ "wit-parser 0.221.3",
]
[[package]]
@@ -9104,7 +9970,7 @@ checksum = "1e91092e6cf77390eeccee273846a9327f3e8f91c3c6280f60f37809f0e62d29"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -9115,7 +9981,7 @@ checksum = "1a8e04b9a4c68ad018b330a4f4914b82b01dc3582d715ce21a93564c7f26b19f"
dependencies = [
"anyhow",
"async-trait",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"bytes",
"cap-fs-ext",
"cap-net-ext",
@@ -9163,7 +10029,7 @@ dependencies = [
"anyhow",
"heck",
"indexmap 2.13.0",
- "wit-parser",
+ "wit-parser 0.221.3",
]
[[package]]
@@ -9177,31 +10043,31 @@ dependencies = [
[[package]]
name = "wast"
-version = "244.0.0"
+version = "245.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2e7b9f9e23311275920e3d6b56d64137c160cf8af4f84a7283b36cfecbf4acb"
+checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf"
dependencies = [
"bumpalo",
"leb128fmt",
"memchr",
- "unicode-width 0.2.0",
- "wasm-encoder 0.244.0",
+ "unicode-width 0.2.2",
+ "wasm-encoder 0.245.1",
]
[[package]]
name = "wat"
-version = "1.244.0"
+version = "1.245.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbf35b87ed352f9ab6cd0732abde5a67dd6153dfd02c493e61459218b19456fa"
+checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be"
dependencies = [
- "wast 244.0.0",
+ "wast 245.0.1",
]
[[package]]
name = "web-sys"
-version = "0.3.85"
+version = "0.3.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -9217,6 +10083,42 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web_atoms"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576"
+dependencies = [
+ "phf 0.13.1",
+ "phf_codegen 0.13.1",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.6",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
[[package]]
name = "which"
version = "4.4.2"
@@ -9231,11 +10133,13 @@ dependencies = [
[[package]]
name = "whoami"
-version = "2.1.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fae98cf96deed1b7572272dfc777713c249ae40aa1cf8862e091e8b745f5361"
+checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d"
dependencies = [
+ "libc",
"libredox",
+ "objc2-system-configuration",
"wasite",
"web-sys",
]
@@ -9248,7 +10152,7 @@ checksum = "3b23e3dc273d1e35cab9f38a5f76487aeeedcfa6a3fb594e209ee7b6f8b41dcc"
dependencies = [
"anyhow",
"async-trait",
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"thiserror 1.0.69",
"tracing",
"wasmtime",
@@ -9266,7 +10170,7 @@ dependencies = [
"proc-macro2",
"quote",
"shellexpand 2.1.2",
- "syn 2.0.114",
+ "syn 2.0.117",
"witx",
]
@@ -9278,7 +10182,7 @@ checksum = "e882267ac583e013a38a5aaeb83a49b219456ba3aa6e6772440f7213b176e8ff"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"wiggle-generate",
]
@@ -9351,7 +10255,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -9362,7 +10266,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -9646,15 +10550,79 @@ version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags 2.11.0",
"windows-sys 0.59.0",
]
[[package]]
name = "wit-bindgen"
-version = "0.46.0"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser 0.244.0",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap 2.13.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.0",
+ "indexmap 2.13.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder 0.244.0",
+ "wasm-metadata",
+ "wasmparser 0.244.0",
+ "wit-parser 0.244.0",
+]
[[package]]
name = "wit-parser"
@@ -9674,6 +10642,24 @@ dependencies = [
"wasmparser 0.221.3",
]
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.244.0",
+]
+
[[package]]
name = "witx"
version = "0.9.1"
@@ -9726,6 +10712,18 @@ dependencies = [
"tap",
]
+[[package]]
+name = "x509-cert"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
+dependencies = [
+ "const-oid",
+ "der",
+ "spki",
+ "tls_codec",
+]
+
[[package]]
name = "xattr"
version = "1.6.1"
@@ -9733,7 +10731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
- "rustix 1.1.3",
+ "rustix 1.1.4",
]
[[package]]
@@ -9777,7 +10775,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"synstructure",
]
@@ -9829,7 +10827,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"zvariant_utils",
]
@@ -9856,11 +10854,11 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.37"
+version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
+checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
dependencies = [
- "zerocopy-derive 0.8.37",
+ "zerocopy-derive 0.8.39",
]
[[package]]
@@ -9871,18 +10869,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.37"
+version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
+checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
[[package]]
@@ -9902,7 +10900,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"synstructure",
]
@@ -9911,6 +10909,20 @@ name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
[[package]]
name = "zerotrie"
@@ -9942,14 +10954,43 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zip"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "crossbeam-utils",
+ "displaydoc",
+ "flate2",
+ "indexmap 2.13.0",
+ "memchr",
+ "thiserror 2.0.18",
+ "zopfli",
]
[[package]]
name = "zmij"
-version = "1.0.19"
+version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
[[package]]
name = "zstd"
@@ -10001,7 +11042,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
"zvariant_utils",
]
@@ -10013,5 +11054,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.114",
+ "syn 2.0.117",
]
diff --git a/Cargo.toml b/Cargo.toml
index 4e76bcf3..081ba4e4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,15 +1,24 @@
[workspace]
-members = [".", "benchmarks"]
+members = ["."]
exclude = [
+ "channels-src/discord",
"channels-src/telegram",
"channels-src/slack",
"channels-src/whatsapp",
+ "tools-src/github",
"tools-src/gmail",
+ "tools-src/google-calendar",
+ "tools-src/google-docs",
+ "tools-src/google-drive",
+ "tools-src/google-sheets",
+ "tools-src/google-slides",
+ "tools-src/slack",
+ "tools-src/telegram",
]
[package]
name = "ironclaw"
-version = "0.5.0"
+version = "0.16.1"
edition = "2024"
rust-version = "1.92"
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
@@ -31,7 +40,7 @@ tokio-stream = { version = "0.1", features = ["sync"] }
futures = "0.3"
# HTTP client
-reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots", "stream"] }
+reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls-native-roots", "stream"] }
# Serialization
serde = { version = "1", features = ["derive"] }
@@ -42,9 +51,12 @@ deadpool-postgres = { version = "0.14", optional = true }
tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-chrono-0_4", "with-serde_json-1"], optional = true }
postgres-types = { version = "0.2", features = ["with-serde_json-1"], optional = true }
refinery = { version = "0.8", features = ["tokio-postgres"], optional = true }
+tokio-postgres-rustls = { version = "0.13", optional = true }
+rustls = { version = "0.23", optional = true, default-features = false }
+rustls-native-certs = { version = "0.8", optional = true }
# Database - libSQL/Turso (optional embedded database)
-libsql = { version = "0.6", optional = true, default-features = false, features = ["core", "replication"] }
+libsql = { version = "0.6", optional = true, default-features = false, features = ["core", "replication", "remote", "tls"] }
# Error handling
thiserror = "2"
@@ -59,8 +71,10 @@ dotenvy = "0.15"
toml = "0.8"
# Core types
-uuid = { version = "1", features = ["v4", "serde"] }
+uuid = { version = "1", features = ["v4", "v5", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
+chrono-tz = "0.10"
+iana-time-zone = "0.1"
rust_decimal = { version = "1", features = ["serde", "serde-with-str", "maths"] }
rust_decimal_macros = "1"
@@ -72,13 +86,13 @@ clap = { version = "4", features = ["derive", "env"] }
# Terminal
crossterm = "0.28"
-rustyline = { version = "17", features = ["derive", "with-file-history"] }
+rustyline = { version = "17", features = ["custom-bindings", "derive", "with-file-history"] }
termimad = "0.34"
# Channel integrations
axum = { version = "0.8", features = ["ws"] }
tower = "0.5"
-tower-http = { version = "0.6", features = ["trace", "cors"] }
+tower-http = { version = "0.6", features = ["trace", "cors", "set-header"] }
# Cron scheduling for routines
cron = "0.13"
@@ -94,6 +108,9 @@ serde_yml = "0.0.12"
dirs = "6"
fs4 = "0.6"
+# Semantic versioning
+semver = "1"
+
# Secrecy for sensitive values
secrecy = { version = "0.10", features = ["serde"] }
@@ -121,6 +138,7 @@ wasmparser = "0.220" # WASM binary parsing for validation
# Cryptography for secrets management
aes-gcm = "0.10"
hkdf = "0.12"
+hmac = "0.12"
sha2 = "0.10"
blake3 = "1"
rand = "0.8"
@@ -132,6 +150,14 @@ rig-core = "0.30"
# Docker sandbox
bollard = "0.18"
+# Archive extraction for WASM extension bundles
+flate2 = "1"
+tar = "0.4"
+
+# Document text extraction
+pdf-extract = "0.7"
+zip = { version = "2", default-features = false, features = ["deflate"] }
+
# HTTP proxy for sandboxed network access
hyper = { version = "1.5", features = ["server", "http1", "http2"] }
hyper-util = { version = "0.1", features = ["server", "tokio", "http1", "http2"] }
@@ -139,6 +165,14 @@ http-body-util = "0.1"
bytes = "1"
base64 = "0.22.1"
mime_guess = "2.0.5"
+clap_complete = "4.5.0"
+lru = "0.16.3"
+
+# HTML to Markdown conversion (feature gated)
+html-to-markdown-rs = { version = "2.3", optional = true }
+readabilityrs = { version = "0.1.2", optional = true }
+ed25519-dalek = { version = "2.2.0", features = ["std"] }
+hex = "0.4.3"
# macOS keychain
[target.'cfg(target_os = "macos")'.dependencies]
@@ -151,17 +185,22 @@ zbus = "4"
[dev-dependencies]
tokio-test = "0.4"
+tracing-test = "0.2"
tokio-tungstenite = "0.26"
testcontainers-modules = { version = "0.11", features = ["postgres"] }
pretty_assertions = "1"
tempfile = "3"
+insta = "1.46.3"
[features]
-default = ["postgres", "libsql"]
+default = ["postgres", "libsql", "html-to-markdown"]
lancedb = ["dep:lancedb", "dep:arrow-array", "dep:arrow-schema"]
postgres = [
"dep:deadpool-postgres",
"dep:tokio-postgres",
+ "dep:tokio-postgres-rustls",
+ "dep:rustls",
+ "dep:rustls-native-certs",
"dep:postgres-types",
"dep:refinery",
"dep:pgvector",
@@ -169,10 +208,11 @@ postgres = [
]
libsql = ["dep:libsql"]
integration = []
+html-to-markdown = ["dep:html-to-markdown-rs", "dep:readabilityrs"]
-[[example]]
-name = "test_heartbeat"
-required-features = ["postgres"]
+[[test]]
+name = "html_to_markdown"
+required-features = ["html-to-markdown"]
# The profile that 'cargo dist' will build with
[profile.dist]
@@ -183,6 +223,8 @@ lto = "thin"
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.30.3"
+# Ignore out-of-date generated CI so custom release.yml jobs are allowed
+allow-dirty = ["ci"]
# CI backends to support
ci = "github"
# The installers to generate for each app
diff --git a/Dockerfile b/Dockerfile
index 34d4d484..0375e509 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,17 +11,24 @@ FROM rust:1.92-slim-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl-dev cmake gcc g++ \
- && rm -rf /var/lib/apt/lists/*
+ && rm -rf /var/lib/apt/lists/* \
+ && rustup target add wasm32-wasip2 \
+ && cargo install wasm-tools
WORKDIR /app
# Copy manifests first for layer caching
COPY Cargo.toml Cargo.lock ./
-# Copy source and build artifacts
+# Copy source, build script, tests, and supporting directories
+COPY build.rs build.rs
COPY src/ src/
+COPY tests/ tests/
COPY migrations/ migrations/
+COPY registry/ registry/
+COPY channels-src/ channels-src/
COPY wit/ wit/
+COPY providers.json providers.json
RUN cargo build --release --bin ironclaw
diff --git a/Dockerfile.test b/Dockerfile.test
new file mode 100644
index 00000000..202bd04d
--- /dev/null
+++ b/Dockerfile.test
@@ -0,0 +1,57 @@
+# Lightweight test Dockerfile for IronClaw web gateway testing.
+#
+# Build:
+# docker build --platform linux/amd64 -f Dockerfile.test -t ironclaw-test .
+#
+# Run (each on a different port):
+# docker run --rm -p 3003:3003 ironclaw-test
+# docker run --rm -p 3004:3003 ironclaw-test
+# docker run --rm -p 3005:3003 ironclaw-test
+
+# Stage 1: Build (libsql only — no PostgreSQL dependency)
+FROM rust:1.92-slim-bookworm AS builder
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev cmake gcc g++ \
+ && rm -rf /var/lib/apt/lists/* \
+ && rustup target add wasm32-wasip2 \
+ && cargo install wasm-tools
+
+WORKDIR /app
+
+COPY Cargo.toml Cargo.lock ./
+COPY build.rs build.rs
+COPY src/ src/
+COPY tests/ tests/
+COPY migrations/ migrations/
+COPY registry/ registry/
+COPY channels-src/ channels-src/
+COPY wit/ wit/
+
+RUN cargo build --release --no-default-features --features libsql --bin ironclaw
+
+# Stage 2: Runtime
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder /app/target/release/ironclaw /usr/local/bin/ironclaw
+
+RUN useradd -m -u 1000 -s /bin/bash ironclaw
+USER ironclaw
+WORKDIR /home/ironclaw
+
+EXPOSE 3003
+
+ENV RUST_LOG=ironclaw=info \
+ GATEWAY_ENABLED=true \
+ GATEWAY_HOST=0.0.0.0 \
+ GATEWAY_PORT=3003 \
+ GATEWAY_AUTH_TOKEN=test \
+ DATABASE_BACKEND=libsql \
+ LIBSQL_PATH=/home/ironclaw/test.db \
+ SANDBOX_ENABLED=false
+
+ENTRYPOINT ["ironclaw", "--no-onboard"]
diff --git a/FEATURE_PARITY.md b/FEATURE_PARITY.md
index 15a7b955..83d6d65e 100644
--- a/FEATURE_PARITY.md
+++ b/FEATURE_PARITY.md
@@ -37,7 +37,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Session management/routing | ✅ | ✅ | SessionManager exists |
| Configuration hot-reload | ✅ | ❌ | |
| Network modes (loopback/LAN/remote) | ✅ | 🚧 | HTTP only |
-| OpenAI-compatible HTTP API | ✅ | ✅ | /v1/chat/completions |
+| OpenAI-compatible HTTP API | ✅ | ✅ | /v1/chat/completions, per-request `model` override |
| Canvas hosting | ✅ | ❌ | Agent-driven UI |
| Gateway lock (PID-based) | ✅ | ❌ | |
| launchd/systemd integration | ✅ | ❌ | |
@@ -45,6 +45,13 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Tailscale integration | ✅ | ❌ | |
| Health check endpoints | ✅ | ✅ | /api/health + /api/gateway/status |
| `doctor` diagnostics | ✅ | ❌ | |
+| Agent event broadcast | ✅ | 🚧 | SSE broadcast manager exists (SseManager) but tool/job-state events not fully wired |
+| Channel health monitor | ✅ | ❌ | Auto-restart with configurable interval |
+| Presence system | ✅ | ❌ | Beacons on connect, system presence for agents |
+| Trusted-proxy auth mode | ✅ | ❌ | Header-based auth for reverse proxies |
+| APNs push pipeline | ✅ | ❌ | Wake disconnected iOS nodes via push |
+| Oversized payload guard | ✅ | 🚧 | HTTP webhook has 64KB body limit + Content-Length check; no chat.history cap |
+| Pre-prompt context diagnostics | ✅ | ❌ | Context size logging before prompt |
### Owner: _Unassigned_
@@ -58,23 +65,50 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| HTTP webhook | ✅ | ✅ | - | axum with secret validation |
| REPL (simple) | ✅ | ✅ | - | For testing |
| WASM channels | ❌ | ✅ | - | IronClaw innovation |
-| WhatsApp | ✅ | ❌ | P1 | Baileys (Web) |
+| WhatsApp | ✅ | ❌ | P1 | Baileys (Web), same-phone mode with echo detection |
| Telegram | ✅ | ✅ | - | WASM channel(MTProto), DM pairing, caption, /start, bot_username |
-| Discord | ✅ | ❌ | P2 | discord.js |
-| Signal | ✅ | ❌ | P2 | signal-cli |
+| Discord | ✅ | ❌ | P2 | discord.js, thread parent binding inheritance |
+| Signal | ✅ | ✅ | P2 | signal-cli daemonPC, SSE listener HTTP/JSON-R, user/group allowlists, DM pairing |
| Slack | ✅ | ✅ | - | WASM tool |
-| iMessage | ✅ | ❌ | P3 | BlueBubbles recommended |
-| Feishu/Lark | ✅ | ❌ | P3 | |
+| iMessage | ✅ | ❌ | P3 | BlueBubbles or Linq recommended |
+| Linq | ✅ | ❌ | P3 | Real iMessage via API, no Mac required |
+| Feishu/Lark | ✅ | ❌ | P3 | Bitable create app/field tools |
| LINE | ✅ | ❌ | P3 | |
| WebChat | ✅ | ✅ | - | Web gateway chat |
| Matrix | ✅ | ❌ | P3 | E2EE support |
-| Mattermost | ✅ | ❌ | P3 | |
+| Mattermost | ✅ | ❌ | P3 | Emoji reactions |
| Google Chat | ✅ | ❌ | P3 | |
| MS Teams | ✅ | ❌ | P3 | |
| Twitch | ✅ | ❌ | P3 | |
-| Voice Call | ✅ | ❌ | P3 | Twilio/Telnyx |
+| Voice Call | ✅ | ❌ | P3 | Twilio/Telnyx, stale call reaper, pre-cached greeting |
| Nostr | ✅ | ❌ | P3 | |
+### Telegram-Specific Features (since Feb 2025)
+
+| Feature | OpenClaw | IronClaw | Notes |
+|---------|----------|----------|-------|
+| Forum topic creation | ✅ | ❌ | Create topics in forum groups |
+| channel_post support | ✅ | ❌ | Bot-to-bot communication |
+| User message reactions | ✅ | ❌ | Surface inbound reactions |
+| sendPoll | ✅ | ❌ | Poll creation via agent |
+| Cron/heartbeat topic targeting | ✅ | ❌ | Messages land in correct topic |
+
+### Discord-Specific Features (since Feb 2025)
+
+| Feature | OpenClaw | IronClaw | Notes |
+|---------|----------|----------|-------|
+| Forwarded attachment downloads | ✅ | ❌ | Fetch media from forwarded messages |
+| Faster reaction state machine | ✅ | ❌ | Watchdog + debounce |
+| Thread parent binding inheritance | ✅ | ❌ | Threads inherit parent routing |
+
+### Slack-Specific Features (since Feb 2025)
+
+| Feature | OpenClaw | IronClaw | Notes |
+|---------|----------|----------|-------|
+| Streaming draft replies | ✅ | ❌ | Partial replies via draft message updates |
+| Configurable stream modes | ✅ | ❌ | Per-channel stream behavior |
+| Thread ownership | ✅ | ❌ | Thread-level ownership tracking |
+
### Channel Features
| Feature | OpenClaw | IronClaw | Notes |
@@ -85,8 +119,11 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Mention-based activation | ✅ | ✅ | bot_username + respond_to_all_group_messages |
| Per-group tool policies | ✅ | ❌ | Allow/deny specific tools |
| Thread isolation | ✅ | ✅ | Separate sessions per thread |
-| Per-channel media limits | ✅ | 🚧 | Caption support for media; no size limits |
-| Typing indicators | ✅ | 🚧 | TUI shows status |
+| Per-channel media limits | ✅ | ✅ | Attachment type in WIT; max 10 per msg, 20MB total, MIME allowlist |
+| Typing indicators | ✅ | 🚧 | TUI + Telegram typing/actionable status prompts; richer parity pending |
+| Per-channel ackReaction config | ✅ | ❌ | Customizable acknowledgement reactions |
+| Group session priming | ✅ | ❌ | Member roster injected for context |
+| Sender_id in trusted metadata | ✅ | ❌ | Exposed in system metadata |
### Owner: _Unassigned_
@@ -104,16 +141,16 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| `config` | ✅ | ✅ | - | Read/write config |
| `channels` | ✅ | ❌ | P2 | Channel management |
| `models` | ✅ | 🚧 | - | Model selector in TUI |
-| `status` | ✅ | ✅ | - | System status |
+| `status` | ✅ | ✅ | - | System status (enriched session details) |
| `agents` | ✅ | ❌ | P3 | Multi-agent management |
-| `sessions` | ✅ | ❌ | P3 | Session listing |
+| `sessions` | ✅ | ❌ | P3 | Session listing (shows subagent models) |
| `memory` | ✅ | ✅ | - | Memory search CLI |
-| `skills` | ✅ | ❌ | P3 | Agent skills |
-| `pairing` | ✅ | ✅ | - | list/approve for channel DM pairing |
-| `nodes` | ✅ | ❌ | P3 | Device management |
+| `skills` | ✅ | ✅ | - | Skills tools + web API endpoints (install, list, activate) |
+| `pairing` | ✅ | ✅ | - | list/approve, account selector |
+| `nodes` | ✅ | ❌ | P3 | Device management, remove/clear flows |
| `plugins` | ✅ | ❌ | P3 | Plugin management |
| `hooks` | ✅ | ✅ | P2 | Lifecycle hooks |
-| `cron` | ✅ | ❌ | P2 | Scheduled jobs |
+| `cron` | ✅ | ❌ | P2 | Scheduled jobs (model/thinking fields in edit) |
| `webhooks` | ✅ | ❌ | P3 | Webhook config |
| `message send` | ✅ | ❌ | P2 | Send to channels |
| `browser` | ✅ | ❌ | P3 | Browser automation |
@@ -121,7 +158,9 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| `doctor` | ✅ | ❌ | P2 | Diagnostics |
| `logs` | ✅ | ❌ | P3 | Query logs |
| `update` | ✅ | ❌ | P3 | Self-update |
-| `completion` | ✅ | ❌ | P3 | Shell completion |
+| `completion` | ✅ | ✅ | - | Shell completion |
+| `/subagents spawn` | ✅ | ❌ | P3 | Spawn subagents from chat |
+| `/export-session` | ✅ | ❌ | P3 | Export current session transcript |
### Owner: _Unassigned_
@@ -138,17 +177,32 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Global sessions | ✅ | ❌ | Optional shared context |
| Session pruning | ✅ | ❌ | Auto cleanup old sessions |
| Context compaction | ✅ | ✅ | Auto summarization |
-| Custom system prompts | ✅ | ✅ | Template variables |
-| Skills (modular capabilities) | ✅ | ❌ | Capability bundles |
+| Post-compaction read audit | ✅ | ❌ | Layer 3: workspace rules appended to summaries |
+| Post-compaction context injection | ✅ | ❌ | Workspace context as system event |
+| Custom system prompts | ✅ | ✅ | Template variables, safety guardrails |
+| Skills (modular capabilities) | ✅ | ✅ | Prompt-based skills with trust gating, attenuation, activation criteria, catalog, selector |
+| Skill routing blocks | ✅ | 🚧 | ActivationCriteria (keywords, patterns, tags) but no "Use when / Don't use when" blocks |
+| Skill path compaction | ✅ | ❌ | ~ prefix to reduce prompt tokens |
| Thinking modes (low/med/high) | ✅ | ❌ | Configurable reasoning depth |
+| Per-model thinkingDefault override | ✅ | ❌ | Override thinking level per model |
| Block-level streaming | ✅ | ❌ | |
| Tool-level streaming | ✅ | ❌ | |
+| Z.AI tool_stream | ✅ | ❌ | Real-time tool call streaming |
| Plugin tools | ✅ | ✅ | WASM tools |
| Tool policies (allow/deny) | ✅ | ✅ | |
| Exec approvals (`/approve`) | ✅ | ✅ | TUI approval overlay |
| Elevated mode | ✅ | ❌ | Privileged execution |
| Subagent support | ✅ | ✅ | Task framework |
+| `/subagents spawn` command | ✅ | ❌ | Spawn from chat |
| Auth profiles | ✅ | ❌ | Multiple auth strategies |
+| Generic API key rotation | ✅ | ❌ | Rotate keys across providers |
+| Stuck loop detection | ✅ | ❌ | Exponential backoff on stuck agent loops |
+| llms.txt discovery | ✅ | ❌ | Auto-discover site metadata |
+| Multiple images per tool call | ✅ | ❌ | Single tool call, multiple images |
+| URL allowlist (web_search/fetch) | ✅ | ❌ | Restrict web tool targets |
+| suppressToolErrors config | ✅ | ❌ | Hide tool errors from user |
+| Intent-first tool display | ✅ | ❌ | Details and exec summaries |
+| Transcript file size in status | ✅ | ❌ | Show size in session status |
### Owner: _Unassigned_
@@ -159,12 +213,22 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Provider | OpenClaw | IronClaw | Priority | Notes |
|----------|----------|----------|----------|-------|
| NEAR AI | ✅ | ✅ | - | Primary provider |
-| Anthropic (Claude) | ✅ | 🚧 | - | Via NEAR AI proxy |
+| Anthropic (Claude) | ✅ | 🚧 | - | Via NEAR AI proxy; Opus 4.5, Sonnet 4, Sonnet 4.6 |
| OpenAI | ✅ | 🚧 | - | Via NEAR AI proxy |
-| AWS Bedrock | ✅ | ❌ | P3 | |
-| Google Gemini | ✅ | ❌ | P3 | |
-| OpenRouter | ✅ | ❌ | P3 | |
+| AWS Bedrock | ✅ | ✅ | P3 | Via `openai_compatible` adapter (e.g. LiteLLM) |
+| Google Gemini | ✅ | ✅ | P3 | Via `gemini` adapter |
+| io.net | ✅ | ✅ | P3 | Via `ionet` adapter |
+| Mistral | ✅ | ✅ | P3 | Via `mistral` adapter |
+| Yandex AI Studio | ✅ | ✅ | P3 | Via `yandex` adapter |
+| Cloudflare Workers AI | ✅ | ✅ | P3 | Via `cloudflare` adapter |
+| NVIDIA API | ✅ | ✅ | P3 | Via `nvidia` adapter and `providers.json` |
+| OpenRouter | ✅ | ✅ | - | Via OpenAI-compatible provider (RigAdapter) |
+| Tinfoil | ❌ | ✅ | - | Private inference provider (IronClaw-only) |
+| OpenAI-compatible | ❌ | ✅ | - | Generic OpenAI-compatible endpoint (RigAdapter) |
| Ollama (local) | ✅ | ✅ | - | via `rig::providers::ollama` (full support) |
+| Perplexity | ✅ | ❌ | P3 | Freshness parameter for web_search |
+| MiniMax | ✅ | ❌ | P3 | Regional endpoint selection |
+| GLM-5 | ✅ | ❌ | P3 | |
| node-llama-cpp | ✅ | ➖ | - | N/A for Rust |
| llama.cpp (native) | ❌ | 🔮 | P3 | Rust bindings |
@@ -177,6 +241,8 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Cooldown management | ✅ | ✅ | Lock-free per-provider cooldown in `FailoverProvider` |
| Per-session model override | ✅ | ✅ | Model selector in TUI |
| Model selection UI | ✅ | ✅ | TUI keyboard shortcut |
+| Per-model thinkingDefault | ✅ | ❌ | Override thinking level per model in config |
+| 1M context beta header | ✅ | ❌ | Anthropic extended context support |
### Owner: _Unassigned_
@@ -186,16 +252,32 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Feature | OpenClaw | IronClaw | Priority | Notes |
|---------|----------|----------|----------|-------|
+| WIT inbound-attachment type | N/A | ✅ | P1 | `inbound-attachment` record in channel-host (id, mime_type, filename, size_bytes, source_url, storage_key, extracted_text) |
+| WIT outbound attachment type | N/A | ✅ | P1 | `attachment` record in channel (filename, mime_type, data) on `agent-response` |
+| WIT on-broadcast export | N/A | ✅ | P1 | Proactive message sending without prior incoming message |
+| IncomingMessage attachments | N/A | ✅ | P1 | `IncomingAttachment` struct on `IncomingMessage`, populated from WASM channels |
+| OutgoingResponse attachments | N/A | ✅ | P1 | File paths on `OutgoingResponse`, read from disk and sent as WIT attachments |
+| Attachment security (size/MIME) | N/A | ✅ | P1 | Inbound: max 10, 20MB total, MIME allowlist. Outbound: 50MB total |
+| Telegram media parsing | ✅ | ✅ | P1 | Photo, document, audio, video, voice, sticker parsed and emitted as attachments |
+| Telegram media sending | ✅ | ✅ | P1 | sendPhoto/sendDocument multipart upload, auto photo→document fallback >10MB |
+| Slack file parsing | ✅ | ✅ | P1 | `files` array from Events API parsed into attachments |
+| WhatsApp media parsing | ✅ | ✅ | P1 | Image, audio, video, document parsed with caption as extracted_text |
+| Discord attachment parsing | ✅ | ❌ | P2 | Discord interaction payloads don't include file attachments (needs message events) |
+| HTTP tool save_to | N/A | ✅ | P1 | Download binary files to /tmp/ for attachment sending (50MB limit, path traversal protection) |
+| Credential env var fallback | N/A | ✅ | P2 | Channels can use env vars (e.g., TELEGRAM_BOT_TOKEN) when secrets store not configured |
| Image processing (Sharp) | ✅ | ❌ | P2 | Resize, format convert |
+| Configurable image resize dims | ✅ | ❌ | P2 | Per-agent dimension config |
+| Multiple images per tool call | ✅ | ❌ | P2 | Single tool invocation, multiple images |
| Audio transcription | ✅ | ❌ | P2 | |
| Video support | ✅ | ❌ | P3 | |
| PDF parsing | ✅ | ❌ | P2 | pdfjs-dist |
-| MIME detection | ✅ | ❌ | P2 | |
+| MIME detection | ✅ | ✅ | P2 | MIME allowlist in host validates attachment types |
| Media caching | ✅ | ❌ | P3 | |
| Vision model integration | ✅ | ❌ | P2 | Image understanding |
| TTS (Edge TTS) | ✅ | ❌ | P3 | Text-to-speech |
| TTS (OpenAI) | ✅ | ❌ | P3 | |
-| Sticker-to-image | ✅ | ❌ | P3 | Telegram stickers |
+| Incremental TTS playback | ✅ | ❌ | P3 | iOS progressive playback |
+| Sticker-to-image | ✅ | ✅ | P3 | Telegram stickers emitted as image/webp attachments |
### Owner: _Unassigned_
@@ -213,10 +295,13 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Auth plugins | ✅ | ❌ | |
| Memory plugins | ✅ | ❌ | Custom backends |
| Tool plugins | ✅ | ✅ | WASM tools |
-| Hook plugins | ✅ | ❌ | |
+| Hook plugins | ✅ | ✅ | Declarative hooks from extension capabilities |
| Provider plugins | ✅ | ❌ | |
| Plugin CLI (`install`, `list`) | ✅ | ✅ | `tool` subcommand |
| ClawHub registry | ✅ | ❌ | Discovery |
+| `before_agent_start` hook | ✅ | ❌ | modelOverride/providerOverride support |
+| `before_message_write` hook | ✅ | ❌ | Pre-write message interception |
+| `llm_input`/`llm_output` hooks | ✅ | ❌ | LLM payload inspection |
### Owner: _Unassigned_
@@ -235,6 +320,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Legacy migration | ✅ | ➖ | |
| State directory | ✅ `~/.openclaw-state/` | ✅ `~/.ironclaw/` | |
| Credentials directory | ✅ | ✅ | Session files |
+| Full model compat fields in schema | ✅ | ❌ | pi-ai model compat exposed in config |
### Owner: _Unassigned_
@@ -247,16 +333,19 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Vector memory | ✅ | ✅ | pgvector |
| Session-based memory | ✅ | ✅ | |
| Hybrid search (BM25 + vector) | ✅ | ✅ | RRF algorithm |
+| Temporal decay (hybrid search) | ✅ | ❌ | Opt-in time-based scoring factor |
+| MMR re-ranking | ✅ | ❌ | Maximal marginal relevance for result diversity |
+| LLM-based query expansion | ✅ | ❌ | Expand FTS queries via LLM |
| OpenAI embeddings | ✅ | ✅ | |
| Gemini embeddings | ✅ | ❌ | |
| Local embeddings | ✅ | ❌ | |
| SQLite-vec backend | ✅ | ❌ | IronClaw uses PostgreSQL |
-| LanceDB backend | ✅ | ✅ | VectorStore + LanceDbVectorStore, DbWithLanceVectorStore wrapper, VECTOR_BACKEND=lancedb |
+| LanceDB backend | ✅ | ✅ | VectorStore trait + LanceDbVectorStore (configured via VECTOR_BACKEND=lancedb) |
| QMD backend | ✅ | ❌ | |
| Atomic reindexing | ✅ | ✅ | |
-| Embeddings batching | ✅ | ❌ | |
+| Embeddings batching | ✅ | ✅ | `embed_batch` on EmbeddingProvider trait |
| Citation support | ✅ | ❌ | |
-| Memory CLI commands | ✅ | ❌ | `memory search/index/status` |
+| Memory CLI commands | ✅ | ✅ | `memory search/read/write/tree/status` CLI subcommands |
| Flexible path structure | ✅ | ✅ | Filesystem-like API |
| Identity files (AGENTS.md, etc.) | ✅ | ✅ | |
| Daily logs | ✅ | ✅ | |
@@ -272,12 +361,16 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
|---------|----------|----------|----------|-------|
| iOS app (SwiftUI) | ✅ | 🚫 | - | Out of scope initially |
| Android app (Kotlin) | ✅ | 🚫 | - | Out of scope initially |
+| Apple Watch companion | ✅ | 🚫 | - | Send/receive messages MVP |
| Gateway WebSocket client | ✅ | 🚫 | - | |
| Camera/photo access | ✅ | 🚫 | - | |
| Voice input | ✅ | 🚫 | - | |
| Push-to-talk | ✅ | 🚫 | - | |
| Location sharing | ✅ | 🚫 | - | |
| Node pairing | ✅ | 🚫 | - | |
+| APNs push notifications | ✅ | 🚫 | - | Wake disconnected nodes before invoke |
+| Share to OpenClaw (iOS) | ✅ | 🚫 | - | iOS share sheet integration |
+| Background listening toggle | ✅ | 🚫 | - | iOS background audio |
### Owner: _Unassigned_ (if ever prioritized)
@@ -288,12 +381,17 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Feature | OpenClaw | IronClaw | Priority | Notes |
|---------|----------|----------|----------|-------|
| SwiftUI native app | ✅ | 🚫 | - | Out of scope |
-| Menu bar presence | ✅ | 🚫 | - | |
+| Menu bar presence | ✅ | 🚫 | - | Animated menubar icon |
| Bundled gateway | ✅ | 🚫 | - | |
-| Canvas hosting | ✅ | 🚫 | - | |
-| Voice wake | ✅ | 🚫 | - | |
+| Canvas hosting | ✅ | 🚫 | - | Agent-controlled panel with placement/resizing |
+| Voice wake | ✅ | 🚫 | - | Overlay, mic picker, language selection, live meter |
+| Voice wake overlay | ✅ | 🚫 | - | Partial transcripts, adaptive delays, dismiss animations |
+| Push-to-talk hotkey | ✅ | 🚫 | - | System-wide hotkey |
| Exec approval dialogs | ✅ | ✅ | - | TUI overlay |
| iMessage integration | ✅ | 🚫 | - | |
+| Instances tab | ✅ | 🚫 | - | Presence beacons across instances |
+| Agent events debug window | ✅ | 🚫 | - | Real-time event inspector |
+| Sparkle auto-updates | ✅ | 🚫 | - | Appcast distribution |
### Owner: _Unassigned_ (if ever prioritized)
@@ -310,7 +408,10 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Config editing | ✅ | ❌ | P3 | |
| Debug/logs viewer | ✅ | ✅ | - | Real-time log streaming with level/target filters |
| WebChat interface | ✅ | ✅ | - | Web gateway chat with SSE/WebSocket |
-| Canvas system (A2UI) | ✅ | ❌ | P3 | Agent-driven UI |
+| Canvas system (A2UI) | ✅ | ❌ | P3 | Agent-driven UI, improved asset resolution |
+| Control UI i18n | ✅ | ❌ | P3 | English, Chinese, Portuguese |
+| WebChat theme sync | ✅ | ❌ | P3 | Sync with system dark/light mode |
+| Partial output on abort | ✅ | ❌ | P2 | Preserve partial output when aborting |
### Owner: _Unassigned_
@@ -321,20 +422,26 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Feature | OpenClaw | IronClaw | Priority | Notes |
|---------|----------|----------|----------|-------|
| Cron jobs | ✅ | ✅ | - | Routines with cron trigger |
+| Cron stagger controls | ✅ | ❌ | P3 | Default stagger for scheduled jobs |
+| Cron finished-run webhook | ✅ | ❌ | P3 | Webhook on job completion |
| Timezone support | ✅ | ✅ | - | Via cron expressions |
| One-shot/recurring jobs | ✅ | ✅ | - | Manual + cron triggers |
+| Channel health monitor | ✅ | ❌ | P2 | Auto-restart with configurable interval |
| `beforeInbound` hook | ✅ | ✅ | P2 | |
| `beforeOutbound` hook | ✅ | ✅ | P2 | |
| `beforeToolCall` hook | ✅ | ✅ | P2 | |
+| `before_agent_start` hook | ✅ | ❌ | P2 | Model/provider override |
+| `before_message_write` hook | ✅ | ❌ | P2 | Pre-write interception |
| `onMessage` hook | ✅ | ✅ | - | Routines with event trigger |
| `onSessionStart` hook | ✅ | ✅ | P2 | |
| `onSessionEnd` hook | ✅ | ✅ | P2 | |
| `transcribeAudio` hook | ✅ | ❌ | P3 | |
| `transformResponse` hook | ✅ | ✅ | P2 | |
-| Bundled hooks | ✅ | ❌ | P2 | |
-| Plugin hooks | ✅ | ❌ | P3 | |
-| Workspace hooks | ✅ | ❌ | P2 | Inline code |
-| Outbound webhooks | ✅ | ❌ | P2 | |
+| `llm_input`/`llm_output` hooks | ✅ | ❌ | P3 | LLM payload inspection |
+| Bundled hooks | ✅ | ✅ | P2 | Audit + declarative rule/webhook hooks |
+| Plugin hooks | ✅ | ✅ | P3 | Registered from WASM `capabilities.json` |
+| Workspace hooks | ✅ | ✅ | P2 | `hooks/hooks.json` and `hooks/*.hook.json` |
+| Outbound webhooks | ✅ | ✅ | P2 | Fire-and-forget lifecycle event delivery |
| Heartbeat system | ✅ | ✅ | - | Periodic execution |
| Gmail pub/sub | ✅ | ❌ | P3 | |
@@ -349,6 +456,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Gateway token auth | ✅ | ✅ | Bearer token auth on web gateway |
| Device pairing | ✅ | ❌ | |
| Tailscale identity | ✅ | ❌ | |
+| Trusted-proxy auth | ✅ | ❌ | Header-based reverse proxy auth |
| OAuth flows | ✅ | 🚧 | NEAR AI OAuth |
| DM pairing verification | ✅ | ✅ | ironclaw pairing approve, host APIs |
| Allowlist/blocklist | ✅ | 🚧 | allow_from + pairing store |
@@ -356,18 +464,26 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Exec approvals | ✅ | ✅ | TUI overlay |
| TLS 1.3 minimum | ✅ | ✅ | reqwest rustls |
| SSRF protection | ✅ | ✅ | WASM allowlist |
+| SSRF IPv6 transition bypass block | ✅ | ❌ | Block IPv4-mapped IPv6 bypasses |
+| Cron webhook SSRF guard | ✅ | ❌ | SSRF checks on webhook delivery |
| Loopback-first | ✅ | 🚧 | HTTP binds 0.0.0.0 |
| Docker sandbox | ✅ | ✅ | Orchestrator/worker containers |
+| Podman support | ✅ | ❌ | Alternative to Docker |
| WASM sandbox | ❌ | ✅ | IronClaw innovation |
+| Sandbox env sanitization | ✅ | 🚧 | Shell tool scrubs env vars (secret detection); docker container env sanitization partial |
| Tool policies | ✅ | ✅ | |
| Elevated mode | ✅ | ❌ | |
-| Safe bins allowlist | ✅ | ❌ | |
+| Safe bins allowlist | ✅ | ❌ | Hardened path trust |
| LD*/DYLD* validation | ✅ | ❌ | |
-| Path traversal prevention | ✅ | ✅ | |
+| Path traversal prevention | ✅ | ✅ | Including config includes (OC-06) |
+| Credential theft via env injection | ✅ | 🚧 | Shell env scrubbing + command injection detection; no full OC-09 defense |
+| Session file permissions (0o600) | ✅ | ✅ | Session token file set to 0o600 in llm/session.rs |
+| Skill download path restriction | ✅ | ❌ | Prevent arbitrary write targets |
| Webhook signature verification | ✅ | ✅ | |
| Media URL validation | ✅ | ❌ | |
| Prompt injection defense | ✅ | ✅ | Pattern detection, sanitization |
| Leak detection | ✅ | ✅ | Secret exfiltration |
+| Dangerous tool re-enable warning | ✅ | ❌ | Warn when gateway.tools.allow re-enables HTTP tools |
### Owner: _Unassigned_
@@ -387,6 +503,9 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Coverage | V8 | tarpaulin/llvm-cov | |
| CI/CD | GitHub Actions | GitHub Actions | |
| Pre-commit hooks | prek | - | Consider adding |
+| Docker: Chromium + Xvfb | ✅ | ❌ | Optional browser in container |
+| Docker: init scripts | ✅ | ❌ | /openclaw-init.d/ support |
+| Browser: extraArgs config | ✅ | ❌ | Custom Chrome launch arguments |
### Owner: _Unassigned_
@@ -399,7 +518,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
- ✅ HTTP webhook channel
- ✅ DM pairing (ironclaw pairing list/approve, host APIs)
- ✅ WASM tool sandbox
-- ✅ Workspace/memory with hybrid search
+- ✅ Workspace/memory with hybrid search + embeddings batching
- ✅ Prompt injection defense
- ✅ Heartbeat system
- ✅ Session management
@@ -414,29 +533,40 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
- ✅ Cron job scheduling (routines)
- ✅ CLI subcommands (onboard, config, status, memory)
- ✅ Gateway token auth
+- ✅ Skills system (prompt-based with trust gating, attenuation, activation criteria)
+- ✅ Session file permissions (0o600)
+- ✅ Memory CLI commands (search, read, write, tree, status)
+- ✅ Shell env scrubbing + command injection detection
+- ✅ Tinfoil private inference provider
+- ✅ OpenAI-compatible / OpenRouter provider support
### P1 - High Priority
- ❌ Slack channel (real implementation)
- ✅ Telegram channel (WASM, DM pairing, caption, /start)
- ❌ WhatsApp channel
- ✅ Multi-provider failover (`FailoverProvider` with retryable error classification)
-- ✅ Hooks system (beforeInbound, beforeToolCall, beforeOutbound, onSessionStart, onSessionEnd, transformResponse)
+- ✅ Hooks system (core lifecycle hooks + bundled/plugin/workspace hooks + outbound webhooks)
### P2 - Medium Priority
- ❌ Media handling (images, PDFs)
-- ❌ Ollama/local model support
+- ✅ Ollama/local model support (via rig::providers::ollama)
- ❌ Configuration hot-reload
- ❌ Webhook trigger endpoint in web gateway
+- ❌ Channel health monitor with auto-restart
+- ❌ Partial output preservation on abort
### P3 - Lower Priority
- ❌ Discord channel
-- ❌ Signal channel
- ❌ Matrix channel
- ❌ Other messaging platforms
- ❌ TTS/audio features
- ❌ Video support
-- ❌ Skills system
+- 🚧 Skills routing blocks (activation criteria exist, but no "Use when / Don't use when")
- ❌ Plugin registry
+- ❌ Streaming (block/tool/Z.AI tool_stream)
+- ❌ Memory: temporal decay, MMR re-ranking, query expansion
+- ❌ Control UI i18n
+- ❌ Stuck loop detection
---
@@ -461,9 +591,12 @@ IronClaw intentionally differs from OpenClaw in these ways:
1. **Rust vs TypeScript**: Native performance, memory safety, single binary distribution
2. **WASM sandbox vs Docker**: Lighter weight, faster startup, capability-based security
-3. **PostgreSQL vs SQLite**: Better suited for production deployments
+3. **PostgreSQL + libSQL vs SQLite**: Dual-backend (production PG + embedded libSQL for zero-dep local mode)
4. **NEAR AI focus**: Primary provider with session-based auth
5. **No mobile/desktop apps**: Focus on server-side and CLI initially
6. **WASM channels**: Novel extension mechanism not in OpenClaw
+7. **Tinfoil private inference**: IronClaw-only provider for private/encrypted inference
+8. **GitHub WASM tool**: Native GitHub integration as WASM tool
+9. **Prompt-based skills**: Different approach than OpenClaw capability bundles (trust gating, attenuation)
These are intentional architectural choices, not gaps to be filled.
diff --git a/README.md b/README.md
index d8fc7a78..d19ae1e9 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
IronClaw
@@ -8,6 +8,12 @@
Your secure personal AI assistant, always on your side
+
+
+
+
+
+
Philosophy •
Features •
@@ -99,6 +105,15 @@ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/nearai/ironclaw/release
```
+
+ Install via Homebrew (macOS/Linux)
+
+```sh
+brew install ironclaw
+```
+
+
+
Compile the source code (Cargo on Windows, Linux, macOS)
@@ -139,8 +154,26 @@ ironclaw onboard
```
The wizard handles database connection, NEAR AI authentication (via browser OAuth),
-and secrets encryption (using your system keychain). All settings are saved to
-`~/.ironclaw/settings.toml`.
+and secrets encryption (using your system keychain). Settings are persisted in the
+connected database; bootstrap variables (e.g. `DATABASE_URL`, `LLM_BACKEND`) are
+written to `~/.ironclaw/.env` so they are available before the database connects.
+
+### Alternative LLM Providers
+
+IronClaw defaults to NEAR AI but works with any OpenAI-compatible endpoint.
+Popular options include **OpenRouter** (300+ models), **Together AI**, **Fireworks AI**,
+**Ollama** (local), and self-hosted servers like **vLLM** or **LiteLLM**.
+
+Select *"OpenAI-compatible"* in the wizard, or set environment variables directly:
+
+```env
+LLM_BACKEND=openai_compatible
+LLM_BASE_URL=https://openrouter.ai/api/v1
+LLM_API_KEY=sk-or-...
+LLM_MODEL=anthropic/claude-sonnet-4
+```
+
+See [docs/LLM_PROVIDERS.md](docs/LLM_PROVIDERS.md) for a full provider guide.
## Security
diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml
deleted file mode 100644
index acaeb64d..00000000
--- a/benchmarks/Cargo.toml
+++ /dev/null
@@ -1,50 +0,0 @@
-[package]
-name = "ironclaw-bench"
-version = "0.1.0"
-edition = "2024"
-rust-version = "1.85"
-description = "Benchmarking harness for IronClaw agent"
-license = "MIT OR Apache-2.0"
-publish = false
-
-[[bin]]
-name = "ironclaw-bench"
-path = "src/main.rs"
-
-[dependencies]
-ironclaw = { path = ".." }
-
-# Async runtime
-tokio = { version = "1", features = ["full"] }
-tokio-stream = { version = "0.1", features = ["sync"] }
-futures = "0.3"
-
-# Serialization
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-toml = "0.8"
-
-# CLI
-clap = { version = "4", features = ["derive"] }
-
-# Core types
-uuid = { version = "1", features = ["v4", "serde"] }
-chrono = { version = "0.4", features = ["serde"] }
-rust_decimal = { version = "1", features = ["serde", "serde-with-str"] }
-
-# Error handling
-thiserror = "2"
-anyhow = "1"
-
-# Async traits
-async-trait = "0.1"
-
-# Logging
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-
-# Scoring
-regex = "1"
-
-[dev-dependencies]
-tempfile = "3"
diff --git a/benchmarks/baselines/spot-gpt5.2-2c43b83/run.json b/benchmarks/baselines/spot-gpt5.2-2c43b83/run.json
deleted file mode 100644
index 01c0e9d7..00000000
--- a/benchmarks/baselines/spot-gpt5.2-2c43b83/run.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "run_id": "8a48de1f-09ce-4c35-9ad1-dad98fb83a1a",
- "suite_id": "spot",
- "config_label": "default",
- "model": "openai/gpt-5.2",
- "commit_hash": "2c43b83",
- "pass_rate": 1.0,
- "avg_score": 1.0,
- "total_tasks": 21,
- "completed_tasks": 21,
- "total_cost_usd": 0.307053,
- "total_wall_time_ms": 111009,
- "started_at": "2026-02-17T22:02:08.206112Z",
- "finished_at": "2026-02-17T22:03:59.270325Z"
-}
\ No newline at end of file
diff --git a/benchmarks/baselines/spot-gpt5.2-2c43b83/tasks.jsonl b/benchmarks/baselines/spot-gpt5.2-2c43b83/tasks.jsonl
deleted file mode 100644
index b19b421e..00000000
--- a/benchmarks/baselines/spot-gpt5.2-2c43b83/tasks.jsonl
+++ /dev/null
@@ -1,21 +0,0 @@
-{"task_id":"smoke-greeting","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":2037,"llm_calls":1,"input_tokens":1359,"output_tokens":65,"estimated_cost_usd":0.005052,"tool_calls":[],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"I’m NEAR AI Agent—an autonomous assistant that helps you plan, write, debug, and execute tasks using available tools (like reading/writing files, running shell commands, and making HTTP requests). Tell me what you’re working on and what outcome you want, and I’ll drive it to completion.","started_at":"2026-02-17T22:02:08.206165Z","finished_at":"2026-02-17T22:02:10.243552Z","config_label":"default","error":null}
-{"task_id":"smoke-math","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":2038,"llm_calls":1,"input_tokens":1366,"output_tokens":5,"estimated_cost_usd":0.004173,"tool_calls":[],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"1081","started_at":"2026-02-17T22:02:10.243933Z","finished_at":"2026-02-17T22:02:12.282133Z","config_label":"default","error":null}
-{"task_id":"tool-echo","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":2646,"llm_calls":2,"input_tokens":2786,"output_tokens":24,"estimated_cost_usd":0.008718,"tool_calls":[{"name":"echo","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Spot check passed","started_at":"2026-02-17T22:02:12.282294Z","finished_at":"2026-02-17T22:02:14.928540Z","config_label":"default","error":null}
-{"task_id":"tool-time","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":3460,"llm_calls":2,"input_tokens":2839,"output_tokens":78,"estimated_cost_usd":0.009687,"tool_calls":[{"name":"time","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Current date/time (UTC): **2026-02-17 22:02:16** \nISO 8601: **2026-02-17T22:02:16.045933+00:00**","started_at":"2026-02-17T22:02:14.928810Z","finished_at":"2026-02-17T22:02:18.389188Z","config_label":"default","error":null}
-{"task_id":"tool-json-query","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":2751,"llm_calls":2,"input_tokens":2839,"output_tokens":43,"estimated_cost_usd":0.009162,"tool_calls":[{"name":"json","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Bob","started_at":"2026-02-17T22:02:18.389812Z","finished_at":"2026-02-17T22:02:21.141146Z","config_label":"default","error":null}
-{"task_id":"tool-shell-echo","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":3460,"llm_calls":2,"input_tokens":2811,"output_tokens":25,"estimated_cost_usd":0.008808,"tool_calls":[{"name":"shell","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"benchmark test","started_at":"2026-02-17T22:02:21.141384Z","finished_at":"2026-02-17T22:02:24.601812Z","config_label":"default","error":null}
-{"task_id":"tool-list-dir","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":19146,"llm_calls":2,"input_tokens":4370,"output_tokens":1337,"estimated_cost_usd":0.033165,"tool_calls":[{"name":"list_dir","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"```\nclaude-501/\nclaude-505/\nclaude/\ncodex/\ncom.apple.launchd.HqYyNM7hoK/\ncom.apple.launchd.Z0bTtlsPEN/\ncom.apple.launchd.oY8y5gkzN4/\ncom.apple.launchd.sdo3WGu0S7/\ndata-gym-cache/\ndocker-desktop-privileged2203342862/\ngrammers-test/\nnear-agent-target/\nnear-market-bot-push/\nnear-market-bot/\nopenclaw/\npowerlog/\ntest_grpo/\ntest_grpo2/\ntest_grpo_entry/\ntest_grpo_mix/\ntest_grpo_singleton/\ntmpybtjiuc0/\n.d8bd37babaf2f1f7-00000000.node (5.9MB)\n.d8bd37fbfbb0f7ff-00000000.node (5.9MB)\n.d8bd3d9bdf6dd3f7-00000000.node (5.9MB)\n.d8bd3fbfbf21ddf7-00000000.node (5.9MB)\n.d8bd7dfebb38d5ff-00000000.node (5.9MB)\n.d8bdf5deba3bf1f7-00000000.node (5.9MB)\n.d8bdf7cfff76f3f7-00000000.node (5.9MB)\n.d8bdfd8ffe26d5ff-00000000.node (5.9MB)\n.d8bdfdeb9ba8dbff-00000000.node (5.9MB)\n.d8bdfffe9ab2ddff-00000000.node (5.9MB)\n.s.PGSQL.5432 (0B)\n.s.PGSQL.5432.lock (56B)\n__KMP_REGISTERED_LIB_75079 (1.0KB)\nagent_loop_new.rs (28.5KB)\nagent_mod.rs (1.6KB)\nauth_trace.md (10.8KB)\nbench-daily.md (72B)\nbench-log.md (109B)\nbench-meeting.md (160B)\nbench-monday.md (44B)\nbench-prefs.md (61B)\nbench-project.md (213B)\nbench-reminder.md (68B)\nbench-todo.md (153B)\nbench-tuesday.md (40B)\ncac-deck.html (151.5KB)\ncircuit_breaker.rs (22.1KB)\ncli_config.rs (9.1KB)\ncli_service.rs (1.1KB)\ncommands.rs (17.5KB)\nconfig.rs (58.4KB)\nconflicts_summary.md (21.4KB)\ncost_guard.rs (11.3KB)\ndebug_forc2.py (2.8KB)\ndebug_forc3.py (3.0KB)\ndebug_forc4.py (3.0KB)\ndebug_forc5.py (4.0KB)\ndebug_forc6.py (3.9KB)\ndebug_forc7.py (2.7KB)\ndebug_forc8.py (2.9KB)\ndebug_forc_prove.py (3.0KB)\ndispatcher.rs (26.2KB)\ndoctor.rs (8.4KB)\nhygiene.rs (7.3KB)\nironclaw_blog_test.png (21.4KB)\nironclaw_browser_test_viewport.png (156.1KB)\nironclaw_linkedin_debug.png (6.5KB)\nironclaw_spot_test.txt (19B)\nkeys_chain_signatures.rs (6.1KB)\nkeys_error.rs (1.6KB)\nkeys_intents.rs (5.8KB)\nkeys_mod.rs (31.6KB)\nkeys_policy.rs (31.4KB)\nkeys_rpc.rs (8.5KB)\nkeys_signer.rs (8.0KB)\nkeys_spending.rs (5.9KB)\nkeys_transaction.rs (13.7KB)\nkeys_types.rs (17.4KB)\nleak_detection_research_summary.md (13.0KB)\nleak_detector.rs (25.3KB)\nlib_new.rs (5.0KB)\nllm_mod.rs (10.0KB)\nmain.rs (56.8KB)\nmain_bootstrap.rs (11.0KB)\nmain_rs.txt (34.1KB)\nnear_resp.json (39B)\nobs_log.rs (5.9KB)\nobs_mod.rs (2.7KB)\nobs_multi.rs (4.0KB)\nobs_noop.rs (1.5KB)\nobs_traits.rs (4.0KB)\nour_bootstrap.rs (12.6KB)\nour_config.rs (45.2KB)\npost_final_review.py (6.4KB)\npost_review.py (4.6KB)\npost_review2.py (4.8KB)\npost_review3.py (2.2KB)\npr-35.diff (137.4KB)\npr115.diff (1.6MB)\npr126.diff (336.9KB)\npr14_full_diff.txt (193.4KB)\npr39_files.txt (243B)\npr51_files.txt (548B)\npr93_diff.txt (160.4KB)\nprs.json (4.6KB)\nresponse_cache.rs (15.1KB)\nrun_hard_proof_search.sh (545B)\nrun_hard_proof_search2.sh (515B)\nrun_proof_search_v3.sh (523B)\nrun_proof_search_v4.sh (524B)\nservice.rs (11.9KB)\nsettings.rs (34.7KB)\ntest_line.py (789B)\ntest_search.txt (1.8KB)\nthread_ops.rs (38.5KB)\ntool_wit.txt (6.0KB)\ntunnel_cf.rs (3.9KB)\ntunnel_custom.rs (6.2KB)\ntunnel_mod.rs (10.4KB)\ntunnel_ngrok.rs (4.2KB)\ntunnel_none.rs (1.3KB)\ntunnel_ts.rs (3.5KB)\nwasm_capabilities.rs (15.2KB)\nwasm_capabilities_schema.rs (26.2KB)\nwasm_host.rs (25.3KB)\nwasm_wrapper.rs (14.9KB)\n```","started_at":"2026-02-17T22:02:24.602248Z","finished_at":"2026-02-17T22:02:43.748463Z","config_label":"default","error":null}
-{"task_id":"chain-write-read","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":5712,"llm_calls":3,"input_tokens":4489,"output_tokens":73,"estimated_cost_usd":0.014562,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"It says: `ironclaw spot check`.","started_at":"2026-02-17T22:02:43.749933Z","finished_at":"2026-02-17T22:02:49.462007Z","config_label":"default","error":null}
-{"task_id":"chain-shell-json","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":4582,"llm_calls":3,"input_tokens":4433,"output_tokens":94,"estimated_cost_usd":0.014709,"tool_calls":[{"name":"shell","duration_ms":0,"success":true},{"name":"json","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"[Called tool `json` with arguments: {\"data\":{\"code\":200,\"status\":\"ok\"},\"operation\":\"query\",\"path\":\"status\"}]","started_at":"2026-02-17T22:02:49.463794Z","finished_at":"2026-02-17T22:02:54.046092Z","config_label":"default","error":null}
-{"task_id":"chain-time-echo","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":4583,"llm_calls":3,"input_tokens":4404,"output_tokens":76,"estimated_cost_usd":0.014352,"tool_calls":[{"name":"time","duration_ms":0,"success":true},{"name":"echo","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"2026-02-17T22:02:55.786456+00:00","started_at":"2026-02-17T22:02:54.046306Z","finished_at":"2026-02-17T22:02:58.629389Z","config_label":"default","error":null}
-{"task_id":"robust-no-tool","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":1020,"llm_calls":1,"input_tokens":1366,"output_tokens":5,"estimated_cost_usd":0.004173,"tool_calls":[],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Paris.","started_at":"2026-02-17T22:02:58.630502Z","finished_at":"2026-02-17T22:02:59.650655Z","config_label":"default","error":null}
-{"task_id":"robust-correct-tool","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":3980,"llm_calls":2,"input_tokens":2825,"output_tokens":50,"estimated_cost_usd":0.009225,"tool_calls":[{"name":"time","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"It’s **22:03:01 UTC** right now (**2026-02-17**).","started_at":"2026-02-17T22:02:59.650901Z","finished_at":"2026-02-17T22:03:03.631122Z","config_label":"default","error":null}
-{"task_id":"robust-json-validate","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":2954,"llm_calls":2,"input_tokens":2822,"output_tokens":43,"estimated_cost_usd":0.009111,"tool_calls":[{"name":"json","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"The JSON is valid (`valid: true`).","started_at":"2026-02-17T22:03:03.631527Z","finished_at":"2026-02-17T22:03:06.585971Z","config_label":"default","error":null}
-{"task_id":"memory-save-daily","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":4890,"llm_calls":3,"input_tokens":4547,"output_tokens":114,"estimated_cost_usd":0.015351,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"```md\n# Daily Tasks\n\n1. Review PR #42\n2. Update API docs\n3. Deploy to staging\n```","started_at":"2026-02-17T22:03:06.586556Z","finished_at":"2026-02-17T22:03:11.476648Z","config_label":"default","error":null}
-{"task_id":"memory-save-reminder","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":5607,"llm_calls":3,"input_tokens":4531,"output_tokens":95,"estimated_cost_usd":0.015018,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"The appointment is on **March 5th at 2pm**, with **Dr. Smith**.","started_at":"2026-02-17T22:03:11.476873Z","finished_at":"2026-02-17T22:03:17.084343Z","config_label":"default","error":null}
-{"task_id":"memory-save-meeting","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":6113,"llm_calls":3,"input_tokens":4750,"output_tokens":122,"estimated_cost_usd":0.01608,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Frontend owner: **Bob** \nLaunch date: **April 15th**","started_at":"2026-02-17T22:03:17.085432Z","finished_at":"2026-02-17T22:03:23.198905Z","config_label":"default","error":null}
-{"task_id":"memory-append-log","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":9880,"llm_calls":6,"input_tokens":9941,"output_tokens":208,"estimated_cost_usd":0.032943,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true},{"name":"shell","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Contents of `/tmp/bench-log.md`:\n1. Morning standup: discussed blockers on auth service \n2. Afternoon: resolved API timeout issue in payment gateway \n\nSummary: You discussed blockers affecting the auth service during the morning standup, and later in the afternoon you resolved an API timeout issue in the payment gateway.","started_at":"2026-02-17T22:03:23.199155Z","finished_at":"2026-02-17T22:03:33.079396Z","config_label":"default","error":null}
-{"task_id":"memory-recall-detail","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":4989,"llm_calls":3,"input_tokens":4721,"output_tokens":121,"estimated_cost_usd":0.015978,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"The project uses **PostgreSQL** as the database, and the PM is **James**.","started_at":"2026-02-17T22:03:33.080611Z","finished_at":"2026-02-17T22:03:38.069896Z","config_label":"default","error":null}
-{"task_id":"memory-todo-priority","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":5916,"llm_calls":3,"input_tokens":4676,"output_tokens":110,"estimated_cost_usd":0.015678,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"High priority tasks:\n- Fix login bug\n- Security audit","started_at":"2026-02-17T22:03:38.070162Z","finished_at":"2026-02-17T22:03:43.986831Z","config_label":"default","error":null}
-{"task_id":"memory-multifile","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":6081,"llm_calls":4,"input_tokens":6442,"output_tokens":196,"estimated_cost_usd":0.022266,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"On Tuesday you have **Sprint retro at 11am**, then **team lunch at noon**.","started_at":"2026-02-17T22:03:43.988013Z","finished_at":"2026-02-17T22:03:50.069452Z","config_label":"default","error":null}
-{"task_id":"memory-update-context","suite_id":"spot","score":{"value":1.0,"label":"pass","details":null},"trace":{"wall_time_ms":9164,"llm_calls":5,"input_tokens":8354,"output_tokens":252,"estimated_cost_usd":0.028842,"tool_calls":[{"name":"write_file","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true},{"name":"apply_patch","duration_ms":0,"success":true},{"name":"read_file","duration_ms":0,"success":true}],"turns":1,"hit_iteration_limit":false,"hit_timeout":false},"response":"Final read confirms the file now says `timezone: EST`.","started_at":"2026-02-17T22:03:50.070277Z","finished_at":"2026-02-17T22:03:59.235196Z","config_label":"default","error":null}
diff --git a/benchmarks/data/spot.jsonl b/benchmarks/data/spot.jsonl
deleted file mode 100644
index 5c8c927e..00000000
--- a/benchmarks/data/spot.jsonl
+++ /dev/null
@@ -1,21 +0,0 @@
-{"id": "smoke-greeting", "prompt": "Hello! Introduce yourself briefly.", "tags": ["smoke"], "assertions": {"response_matches": "(?i)(hello|hi|hey|assistant|agent|help)", "no_error": true, "max_tool_calls": 0}}
-{"id": "smoke-math", "prompt": "What is 47 * 23? Reply with just the number.", "tags": ["smoke"], "assertions": {"response_contains": ["1081"], "no_error": true, "max_tool_calls": 0}}
-{"id": "tool-echo", "prompt": "Use the echo tool to repeat the message: 'Spot check passed'", "tags": ["tool"], "assertions": {"tools_used": ["echo"], "response_contains": ["Spot check passed"], "no_error": true}}
-{"id": "tool-time", "prompt": "What is the current date and time? Use the time tool.", "tags": ["tool"], "assertions": {"tools_used": ["time"], "response_matches": "20\\d{2}", "no_error": true}}
-{"id": "tool-json-query", "prompt": "Given this JSON: {\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}, use the json tool to extract the second user's name.", "tags": ["tool"], "assertions": {"tools_used": ["json"], "response_contains": ["Bob"], "no_error": true}}
-{"id": "tool-shell-echo", "prompt": "Use the shell tool to run: echo 'benchmark test'", "tags": ["tool"], "assertions": {"tools_used": ["shell"], "response_contains": ["benchmark test"], "no_error": true}}
-{"id": "tool-list-dir", "prompt": "Use the list_dir tool to list the contents of the /tmp directory.", "tags": ["tool"], "assertions": {"tools_used": ["list_dir"], "no_error": true}}
-{"id": "chain-write-read", "prompt": "Write the text 'ironclaw spot check' to /tmp/ironclaw_spot_test.txt using the write_file tool, then read it back using the read_file tool and tell me what it says.", "tags": ["chain"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["ironclaw spot check"], "no_error": true}}
-{"id": "chain-shell-json", "prompt": "Run a shell command to output the JSON string '{\"status\": \"ok\", \"code\": 200}', then use the json tool to extract the status field.", "tags": ["chain"], "assertions": {"response_contains": ["ok"], "min_tool_calls": 1, "no_error": true}}
-{"id": "chain-time-echo", "prompt": "First get the current time using the time tool, then use the echo tool to repeat it back.", "tags": ["chain"], "assertions": {"tools_used": ["time", "echo"], "no_error": true}}
-{"id": "robust-no-tool", "prompt": "What is the capital of France? Answer directly without using any tools.", "tags": ["robust"], "assertions": {"response_contains": ["Paris"], "max_tool_calls": 0, "no_error": true}}
-{"id": "robust-correct-tool", "prompt": "What time is it right now?", "tags": ["robust"], "assertions": {"tools_used": ["time"], "tools_not_used": ["shell", "echo"], "no_error": true}}
-{"id": "robust-json-validate", "prompt": "Use the json tool to validate whether this is valid JSON: {\"key\": \"value\", \"num\": 42}", "tags": ["robust"], "assertions": {"tools_used": ["json"], "tools_not_used": ["shell"], "no_error": true}}
-{"id": "memory-save-daily", "prompt": "Save these daily tasks to /tmp/bench-daily.md:\n1. Review PR #42\n2. Update API docs\n3. Deploy to staging\nThen read the file back and confirm what was saved.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["PR #42", "docs", "staging"], "no_error": true}}
-{"id": "memory-save-reminder", "prompt": "Write a reminder to /tmp/bench-reminder.md: Dentist appointment on March 5th at 2pm with Dr. Smith. Then read the file back and tell me when the appointment is and with whom.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["March 5", "Smith"], "response_matches": "2(:00)?\\s*[Pp][Mm]", "no_error": true}}
-{"id": "memory-save-meeting", "prompt": "Save these meeting notes to /tmp/bench-meeting.md:\nMeeting: Project Phoenix sync\nAttendees: Alice, Bob, Carol\nDecisions:\n- Launch date: April 15th\n- Budget: $50k approved\n- Bob owns frontend, Carol owns backend\nThen read the file back and tell me who owns the frontend and what the launch date is.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["Bob", "frontend", "April 15"], "no_error": true}}
-{"id": "memory-append-log", "prompt": "Write 'Morning standup: discussed blockers on auth service' to /tmp/bench-log.md. Then append a new line 'Afternoon: resolved API timeout issue in payment gateway' to the same file. Finally read the full file and summarize what happened.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["auth", "timeout"], "min_tool_calls": 3, "no_error": true}}
-{"id": "memory-recall-detail", "prompt": "Save the following project context to /tmp/bench-project.md:\nProject Ironclad uses Rust for the backend, React for the frontend, and PostgreSQL for the database. The API is deployed on AWS ECS. The lead developer is Sarah and the PM is James. The sprint ends on March 20th.\nThen read it back and answer: What database does the project use, and who is the PM?", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["PostgreSQL", "James"], "no_error": true}}
-{"id": "memory-todo-priority", "prompt": "Write the following to /tmp/bench-todo.md:\n- [ ] Fix login bug (priority: HIGH)\n- [ ] Write unit tests (priority: medium)\n- [ ] Update README (priority: low)\n- [ ] Security audit (priority: HIGH)\nThen read it back and tell me which tasks are high priority.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["login bug", "security audit"], "no_error": true}}
-{"id": "memory-multifile", "prompt": "Save 'Team standup at 9am, then client demo at 2pm' to /tmp/bench-monday.md and 'Sprint retro at 11am, team lunch at noon' to /tmp/bench-tuesday.md. Then read both files and tell me what's happening on Tuesday.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["retro", "lunch"], "min_tool_calls": 3, "no_error": true}}
-{"id": "memory-update-context", "prompt": "Write 'User preference: dark mode, timezone: PST, language: English' to /tmp/bench-prefs.md. Then read it back, and rewrite the file changing the timezone to EST. Finally read it one more time and confirm the timezone is now EST.", "tags": ["memory"], "assertions": {"tools_used": ["write_file", "read_file"], "response_contains": ["EST"], "min_tool_calls": 4, "no_error": true}}
diff --git a/benchmarks/spot.toml b/benchmarks/spot.toml
deleted file mode 100644
index bf118e79..00000000
--- a/benchmarks/spot.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-task_timeout = "120s"
-parallelism = 1
-
-[[matrix]]
-label = "default"
-
-[suite_config]
-dataset_path = "benchmarks/data/spot.jsonl"
diff --git a/benchmarks/src/adapters/custom.rs b/benchmarks/src/adapters/custom.rs
deleted file mode 100644
index 31559002..00000000
--- a/benchmarks/src/adapters/custom.rs
+++ /dev/null
@@ -1,243 +0,0 @@
-use std::io::BufRead;
-use std::path::PathBuf;
-
-use async_trait::async_trait;
-use serde::Deserialize;
-
-use crate::error::BenchError;
-use crate::scoring;
-use crate::suite::{BenchScore, BenchSuite, BenchTask, TaskSubmission};
-
-/// A single entry in the custom JSONL format.
-#[derive(Debug, Deserialize)]
-struct CustomEntry {
- id: String,
- prompt: String,
- #[serde(default)]
- context: Option,
- #[serde(default)]
- tags: Vec,
- #[serde(default)]
- expected: Option,
- #[serde(default)]
- expected_contains: Option,
- #[serde(default)]
- expected_regex: Option,
- /// "exact", "contains", "regex", or "llm" (default: "exact")
- #[serde(default = "default_scorer")]
- scorer: String,
-}
-
-fn default_scorer() -> String {
- "exact".to_string()
-}
-
-/// Custom JSONL benchmark suite.
-///
-/// Each line of the JSONL file is a task with `id`, `prompt`, and scoring
-/// criteria (`expected`, `expected_contains`, `expected_regex`).
-pub struct CustomSuite {
- dataset_path: PathBuf,
-}
-
-impl CustomSuite {
- pub fn new(dataset_path: impl Into) -> Self {
- Self {
- dataset_path: dataset_path.into(),
- }
- }
-}
-
-#[async_trait]
-impl BenchSuite for CustomSuite {
- fn name(&self) -> &str {
- "Custom JSONL"
- }
-
- fn id(&self) -> &str {
- "custom"
- }
-
- async fn load_tasks(&self) -> Result, BenchError> {
- let file = std::fs::File::open(&self.dataset_path).map_err(BenchError::Io)?;
- let reader = std::io::BufReader::new(file);
- let mut tasks = Vec::new();
-
- for (line_num, line) in reader.lines().enumerate() {
- let line = line?;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- let entry: CustomEntry = serde_json::from_str(trimmed)
- .map_err(|e| BenchError::Config(format!("line {}: {}", line_num + 1, e)))?;
-
- let mut metadata = serde_json::json!({
- "scorer": entry.scorer,
- });
- if let Some(ref expected) = entry.expected {
- metadata["expected"] = serde_json::Value::String(expected.clone());
- }
- if let Some(ref expected_contains) = entry.expected_contains {
- metadata["expected_contains"] =
- serde_json::Value::String(expected_contains.clone());
- }
- if let Some(ref expected_regex) = entry.expected_regex {
- metadata["expected_regex"] = serde_json::Value::String(expected_regex.clone());
- }
-
- tasks.push(BenchTask {
- id: entry.id,
- prompt: entry.prompt,
- context: entry.context,
- resources: vec![],
- tags: entry.tags,
- expected_turns: None,
- timeout: None,
- metadata,
- });
- }
-
- Ok(tasks)
- }
-
- async fn score(
- &self,
- task: &BenchTask,
- submission: &TaskSubmission,
- ) -> Result {
- let scorer = task
- .metadata
- .get("scorer")
- .and_then(|v| v.as_str())
- .unwrap_or("exact");
-
- match scorer {
- "exact" => {
- if let Some(expected) = task.metadata.get("expected").and_then(|v| v.as_str()) {
- Ok(scoring::exact_match(expected, &submission.response))
- } else {
- Err(BenchError::Scoring {
- task_id: task.id.clone(),
- reason: "no 'expected' field for exact scoring".to_string(),
- })
- }
- }
- "contains" => {
- if let Some(expected) = task
- .metadata
- .get("expected_contains")
- .and_then(|v| v.as_str())
- {
- Ok(scoring::contains_match(expected, &submission.response))
- } else {
- Err(BenchError::Scoring {
- task_id: task.id.clone(),
- reason: "no 'expected_contains' field for contains scoring".to_string(),
- })
- }
- }
- "regex" => {
- if let Some(pattern) = task.metadata.get("expected_regex").and_then(|v| v.as_str())
- {
- Ok(scoring::regex_match(pattern, &submission.response))
- } else {
- Err(BenchError::Scoring {
- task_id: task.id.clone(),
- reason: "no 'expected_regex' field for regex scoring".to_string(),
- })
- }
- }
- "llm" => {
- // TODO: LLM-as-judge scoring
- tracing::warn!(
- task_id = %task.id,
- "LLM-as-judge scoring not implemented, returning placeholder 0.5"
- );
- Ok(BenchScore::partial(0.5, "LLM scoring not yet implemented"))
- }
- other => Err(BenchError::Scoring {
- task_id: task.id.clone(),
- reason: format!("unknown scorer: {other}"),
- }),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::io::Write;
-
- #[tokio::test]
- async fn test_custom_load_tasks() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("tasks.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"id": "t1", "prompt": "What is 2+2?", "expected": "4"}}"#
- )
- .unwrap();
- writeln!(
- file,
- r#"{{"id": "t2", "prompt": "Say hello", "expected_contains": "hello", "scorer": "contains"}}"#
- )
- .unwrap();
-
- let suite = CustomSuite::new(&path);
- let tasks = suite.load_tasks().await.unwrap();
- assert_eq!(tasks.len(), 2);
- assert_eq!(tasks[0].id, "t1");
- assert_eq!(tasks[1].id, "t2");
- }
-
- #[tokio::test]
- async fn test_custom_exact_scoring() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("tasks.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"id": "t1", "prompt": "What is 2+2?", "expected": "4"}}"#
- )
- .unwrap();
-
- let suite = CustomSuite::new(&path);
- let tasks = suite.load_tasks().await.unwrap();
-
- let submission = TaskSubmission {
- response: "4".to_string(),
- conversation: vec![],
- tool_calls: vec![],
- error: None,
- };
- let score = suite.score(&tasks[0], &submission).await.unwrap();
- assert_eq!(score.value, 1.0);
- assert_eq!(score.label, "pass");
- }
-
- #[tokio::test]
- async fn test_custom_contains_scoring() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("tasks.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"id": "t1", "prompt": "Greet me", "expected_contains": "hello", "scorer": "contains"}}"#
- )
- .unwrap();
-
- let suite = CustomSuite::new(&path);
- let tasks = suite.load_tasks().await.unwrap();
-
- let submission = TaskSubmission {
- response: "Hello there!".to_string(),
- conversation: vec![],
- tool_calls: vec![],
- error: None,
- };
- let score = suite.score(&tasks[0], &submission).await.unwrap();
- assert_eq!(score.value, 1.0);
- }
-}
diff --git a/benchmarks/src/adapters/gaia.rs b/benchmarks/src/adapters/gaia.rs
deleted file mode 100644
index 5142babf..00000000
--- a/benchmarks/src/adapters/gaia.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-use std::io::BufRead;
-use std::path::PathBuf;
-
-use async_trait::async_trait;
-use serde::Deserialize;
-
-use crate::error::BenchError;
-use crate::scoring;
-use crate::suite::{BenchScore, BenchSuite, BenchTask, TaskResource, TaskSubmission};
-
-/// GAIA dataset entry (Hugging Face JSONL format).
-#[derive(Debug, Deserialize)]
-struct GaiaEntry {
- task_id: String,
- #[serde(alias = "Question")]
- question: String,
- #[serde(alias = "Final answer", alias = "final_answer")]
- final_answer: String,
- #[serde(alias = "Level", default)]
- level: Option,
- #[serde(alias = "file_name", default)]
- file_name: Option,
-}
-
-/// GAIA benchmark suite.
-///
-/// Tasks are loaded from HuggingFace JSONL exports. Scoring uses normalized
-/// exact match against the `final_answer` field.
-pub struct GaiaSuite {
- dataset_path: PathBuf,
- attachments_dir: Option,
-}
-
-impl GaiaSuite {
- pub fn new(
- dataset_path: impl Into,
- attachments_dir: Option>,
- ) -> Self {
- Self {
- dataset_path: dataset_path.into(),
- attachments_dir: attachments_dir.map(|d| d.into()),
- }
- }
-}
-
-#[async_trait]
-impl BenchSuite for GaiaSuite {
- fn name(&self) -> &str {
- "GAIA"
- }
-
- fn id(&self) -> &str {
- "gaia"
- }
-
- async fn load_tasks(&self) -> Result, BenchError> {
- let file = std::fs::File::open(&self.dataset_path)?;
- let reader = std::io::BufReader::new(file);
- let mut tasks = Vec::new();
-
- for (line_num, line) in reader.lines().enumerate() {
- let line = line?;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- let entry: GaiaEntry = serde_json::from_str(trimmed)
- .map_err(|e| BenchError::Config(format!("GAIA line {}: {}", line_num + 1, e)))?;
-
- let mut resources = Vec::new();
- if let Some(ref file_name) = entry.file_name {
- if !file_name.is_empty() {
- if let Some(ref dir) = self.attachments_dir {
- resources.push(TaskResource {
- name: file_name.clone(),
- path: dir.join(file_name).to_string_lossy().to_string(),
- resource_type: crate::suite::ResourceType::File,
- });
- }
- }
- }
-
- let mut tags = Vec::new();
- if let Some(level) = entry.level {
- tags.push(format!("level-{level}"));
- }
-
- let metadata = serde_json::json!({
- "expected": entry.final_answer,
- "level": entry.level,
- });
-
- tasks.push(BenchTask {
- id: entry.task_id,
- prompt: entry.question,
- context: None,
- resources,
- tags,
- expected_turns: None,
- timeout: None,
- metadata,
- });
- }
-
- Ok(tasks)
- }
-
- async fn score(
- &self,
- task: &BenchTask,
- submission: &TaskSubmission,
- ) -> Result {
- let expected = task
- .metadata
- .get("expected")
- .and_then(|v| v.as_str())
- .ok_or_else(|| BenchError::Scoring {
- task_id: task.id.clone(),
- reason: "missing expected answer in metadata".to_string(),
- })?;
-
- Ok(scoring::exact_match(expected, &submission.response))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::io::Write;
-
- #[tokio::test]
- async fn test_gaia_load_tasks() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("gaia.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"task_id": "g1", "question": "What is the capital of France?", "final_answer": "Paris", "Level": 1}}"#
- )
- .unwrap();
-
- let suite = GaiaSuite::new(&path, None::);
- let tasks = suite.load_tasks().await.unwrap();
- assert_eq!(tasks.len(), 1);
- assert_eq!(tasks[0].id, "g1");
- assert!(tasks[0].tags.contains(&"level-1".to_string()));
- }
-
- #[tokio::test]
- async fn test_gaia_scoring() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("gaia.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"task_id": "g1", "question": "Capital of France?", "final_answer": "Paris"}}"#
- )
- .unwrap();
-
- let suite = GaiaSuite::new(&path, None::);
- let tasks = suite.load_tasks().await.unwrap();
-
- // Exact match (case insensitive)
- let submission = TaskSubmission {
- response: "paris".to_string(),
- conversation: vec![],
- tool_calls: vec![],
- error: None,
- };
- let score = suite.score(&tasks[0], &submission).await.unwrap();
- assert_eq!(score.value, 1.0);
-
- // Wrong answer
- let submission = TaskSubmission {
- response: "London".to_string(),
- conversation: vec![],
- tool_calls: vec![],
- error: None,
- };
- let score = suite.score(&tasks[0], &submission).await.unwrap();
- assert_eq!(score.value, 0.0);
- }
-}
diff --git a/benchmarks/src/adapters/mod.rs b/benchmarks/src/adapters/mod.rs
deleted file mode 100644
index 00f562df..00000000
--- a/benchmarks/src/adapters/mod.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-pub mod custom;
-pub mod gaia;
-pub mod spot;
-pub mod swe_bench;
-pub mod tau_bench;
-
-use crate::config::BenchConfig;
-use crate::error::BenchError;
-use crate::suite::BenchSuite;
-
-/// List of all known suite IDs.
-pub const KNOWN_SUITES: &[(&str, &str)] = &[
- ("custom", "Custom JSONL tasks"),
- ("gaia", "GAIA benchmark (knowledge & reasoning)"),
- ("spot", "Spot checks (end-to-end user workflows)"),
- ("tau_bench", "Tau-bench (multi-turn tool use)"),
- ("swe_bench", "SWE-bench Pro (software engineering)"),
-];
-
-/// Create a suite adapter by name.
-pub fn create_suite(name: &str, config: &BenchConfig) -> Result, BenchError> {
- let suite_map = config.suite_config_map();
- match name {
- "custom" => {
- let dataset_path = suite_map
- .get("dataset_path")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .ok_or_else(|| {
- BenchError::Config(
- "suite_config.dataset_path is required for 'custom' suite".to_string(),
- )
- })?;
- Ok(Box::new(custom::CustomSuite::new(dataset_path)))
- }
- "gaia" => {
- let dataset_path = suite_map
- .get("dataset_path")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .ok_or_else(|| {
- BenchError::Config(
- "suite_config.dataset_path is required for 'gaia' suite".to_string(),
- )
- })?;
- let attachments_dir = suite_map
- .get("attachments_dir")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- Ok(Box::new(gaia::GaiaSuite::new(
- dataset_path,
- attachments_dir,
- )))
- }
- "spot" => {
- let dataset_path = suite_map
- .get("dataset_path")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .ok_or_else(|| {
- BenchError::Config(
- "suite_config.dataset_path is required for 'spot' suite".to_string(),
- )
- })?;
- Ok(Box::new(spot::SpotSuite::new(dataset_path)))
- }
- "tau_bench" => {
- let dataset_path = suite_map
- .get("dataset_path")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .ok_or_else(|| {
- BenchError::Config(
- "suite_config.dataset_path is required for 'tau_bench' suite".to_string(),
- )
- })?;
- let domain = suite_map
- .get("domain")
- .and_then(|v| v.as_str())
- .unwrap_or("retail")
- .to_string();
- Ok(Box::new(tau_bench::TauBenchSuite::new(
- dataset_path,
- domain,
- )))
- }
- "swe_bench" => {
- let dataset_path = suite_map
- .get("dataset_path")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string())
- .ok_or_else(|| {
- BenchError::Config(
- "suite_config.dataset_path is required for 'swe_bench' suite".to_string(),
- )
- })?;
- let workspace_dir = suite_map
- .get("workspace_dir")
- .and_then(|v| v.as_str())
- .unwrap_or("/tmp/swe-bench")
- .to_string();
- let use_docker = suite_map
- .get("use_docker")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
- Ok(Box::new(swe_bench::SweBenchSuite::new(
- dataset_path,
- workspace_dir,
- use_docker,
- )))
- }
- _ => {
- let available = KNOWN_SUITES
- .iter()
- .map(|(id, _)| *id)
- .collect::>()
- .join(", ");
- Err(BenchError::SuiteNotFound {
- name: name.to_string(),
- available,
- })
- }
- }
-}
diff --git a/benchmarks/src/adapters/spot.rs b/benchmarks/src/adapters/spot.rs
deleted file mode 100644
index 77e8d0fa..00000000
--- a/benchmarks/src/adapters/spot.rs
+++ /dev/null
@@ -1,504 +0,0 @@
-use std::collections::HashSet;
-use std::io::BufRead;
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use async_trait::async_trait;
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-
-use crate::error::BenchError;
-use crate::suite::{BenchScore, BenchSuite, BenchTask, TaskSubmission};
-
-/// Multi-criterion assertions for a spot check scenario.
-///
-/// Each field generates one or more individual checks. The final score is
-/// `passed_checks / total_checks`, giving a value between 0.0 and 1.0.
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
-pub struct SpotAssertions {
- /// All must appear in the response (case-insensitive).
- #[serde(default)]
- pub response_contains: Vec,
-
- /// None may appear in the response (case-insensitive).
- #[serde(default)]
- pub response_not_contains: Vec,
-
- /// Each tool name must appear in the tool_calls list (checked by name,
- /// not by count; duplicates in tool_calls are collapsed).
- #[serde(default)]
- pub tools_used: Vec,
-
- /// None of these tool names may appear in the tool_calls list.
- #[serde(default)]
- pub tools_not_used: Vec,
-
- /// Regex pattern the response must match.
- #[serde(default)]
- pub response_matches: Option,
-
- /// Hard fail if the task produced an error.
- #[serde(default)]
- pub no_error: bool,
-
- /// Minimum number of tool calls expected (counts duplicates).
- #[serde(default)]
- pub min_tool_calls: Option,
-
- /// Maximum number of tool calls allowed (counts duplicates).
- #[serde(default)]
- pub max_tool_calls: Option,
-}
-
-impl SpotAssertions {
- /// Evaluate all assertions against a submission, returning (score, failure_details).
- pub fn evaluate(&self, submission: &TaskSubmission) -> (f64, Vec) {
- let mut passed: usize = 0;
- let mut total: usize = 0;
- let mut failures: Vec = Vec::new();
-
- // Hard fail: error check
- if self.no_error {
- total += 1;
- if let Some(ref err) = submission.error {
- failures.push(format!("no_error: task errored with: {err}"));
- // Hard fail: return 0.0 immediately
- return (0.0, failures);
- }
- passed += 1;
- }
-
- let response_lower = submission.response.to_lowercase();
-
- // response_contains: all must appear
- for needle in &self.response_contains {
- total += 1;
- if response_lower.contains(&needle.to_lowercase()) {
- passed += 1;
- } else {
- failures.push(format!("response_contains: missing \"{needle}\""));
- }
- }
-
- // response_not_contains: none may appear
- for needle in &self.response_not_contains {
- total += 1;
- if response_lower.contains(&needle.to_lowercase()) {
- failures.push(format!("response_not_contains: found \"{needle}\""));
- } else {
- passed += 1;
- }
- }
-
- let tool_set: HashSet<&str> = submission.tool_calls.iter().map(|s| s.as_str()).collect();
-
- // tools_used: each must appear
- for tool in &self.tools_used {
- total += 1;
- if tool_set.contains(tool.as_str()) {
- passed += 1;
- } else {
- failures.push(format!("tools_used: \"{tool}\" not called"));
- }
- }
-
- // tools_not_used: none may appear
- for tool in &self.tools_not_used {
- total += 1;
- if tool_set.contains(tool.as_str()) {
- failures.push(format!("tools_not_used: \"{tool}\" was called"));
- } else {
- passed += 1;
- }
- }
-
- // response_matches: regex pattern
- if let Some(ref pattern) = self.response_matches {
- total += 1;
- match Regex::new(pattern) {
- Ok(re) => {
- if re.is_match(&submission.response) {
- passed += 1;
- } else {
- failures.push(format!("response_matches: /{pattern}/ did not match"));
- }
- }
- Err(e) => {
- failures.push(format!("response_matches: bad regex: {e}"));
- }
- }
- }
-
- let call_count = submission.tool_calls.len();
-
- // min_tool_calls
- if let Some(min) = self.min_tool_calls {
- total += 1;
- if call_count >= min {
- passed += 1;
- } else {
- failures.push(format!(
- "min_tool_calls: expected >= {min}, got {call_count}"
- ));
- }
- }
-
- // max_tool_calls
- if let Some(max) = self.max_tool_calls {
- total += 1;
- if call_count <= max {
- passed += 1;
- } else {
- failures.push(format!(
- "max_tool_calls: expected <= {max}, got {call_count}"
- ));
- }
- }
-
- if total == 0 {
- return (1.0, failures);
- }
-
- let score = passed as f64 / total as f64;
- (score, failures)
- }
-}
-
-/// JSONL entry for a spot check scenario.
-#[derive(Debug, Deserialize)]
-struct SpotEntry {
- id: String,
- prompt: String,
- #[serde(default)]
- context: Option,
- #[serde(default)]
- tags: Vec,
- #[serde(default)]
- assertions: SpotAssertions,
-}
-
-/// Spot benchmark suite: end-to-end checks for real user workflows.
-///
-/// Tests conversation, individual tool use, multi-tool chaining, and robustness.
-/// Each task declares multi-criterion assertions scored as passed/total.
-pub struct SpotSuite {
- dataset_path: PathBuf,
-}
-
-impl SpotSuite {
- pub fn new(dataset_path: impl Into) -> Self {
- Self {
- dataset_path: dataset_path.into(),
- }
- }
-}
-
-#[async_trait]
-impl BenchSuite for SpotSuite {
- fn name(&self) -> &str {
- "Spot Checks"
- }
-
- fn id(&self) -> &str {
- "spot"
- }
-
- async fn load_tasks(&self) -> Result, BenchError> {
- let file = std::fs::File::open(&self.dataset_path).map_err(BenchError::Io)?;
- let reader = std::io::BufReader::new(file);
- let mut tasks = Vec::new();
-
- for (line_num, line) in reader.lines().enumerate() {
- let line = line?;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- let entry: SpotEntry = serde_json::from_str(trimmed)
- .map_err(|e| BenchError::Config(format!("spot line {}: {}", line_num + 1, e)))?;
-
- let metadata = serde_json::json!({
- "assertions": serde_json::to_value(&entry.assertions)
- .map_err(|e| BenchError::Config(format!("spot {}: {}", entry.id, e)))?,
- });
-
- tasks.push(BenchTask {
- id: entry.id,
- prompt: entry.prompt,
- context: entry.context,
- resources: vec![],
- tags: entry.tags,
- expected_turns: None,
- timeout: None,
- metadata,
- });
- }
-
- Ok(tasks)
- }
-
- async fn score(
- &self,
- task: &BenchTask,
- submission: &TaskSubmission,
- ) -> Result {
- let assertions: SpotAssertions = task
- .metadata
- .get("assertions")
- .ok_or_else(|| BenchError::Scoring {
- task_id: task.id.clone(),
- reason: "missing assertions in metadata".to_string(),
- })
- .and_then(|v| {
- serde_json::from_value(v.clone()).map_err(|e| BenchError::Scoring {
- task_id: task.id.clone(),
- reason: format!("bad assertions: {e}"),
- })
- })?;
-
- let (score, failures) = assertions.evaluate(submission);
-
- if score >= 1.0 {
- Ok(BenchScore::pass())
- } else if score <= 0.0 {
- Ok(BenchScore::fail(failures.join("; ")))
- } else {
- Ok(BenchScore::partial(score, failures.join("; ")))
- }
- }
-
- fn additional_tools(&self) -> Vec> {
- vec![
- Arc::new(ironclaw::tools::builtin::ShellTool::new()),
- Arc::new(ironclaw::tools::builtin::ReadFileTool::new()),
- Arc::new(ironclaw::tools::builtin::WriteFileTool::new()),
- Arc::new(ironclaw::tools::builtin::ListDirTool::new()),
- Arc::new(ironclaw::tools::builtin::ApplyPatchTool::new()),
- ]
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::io::Write;
-
- fn make_submission(
- response: &str,
- tool_calls: Vec<&str>,
- error: Option<&str>,
- ) -> TaskSubmission {
- TaskSubmission {
- response: response.to_string(),
- conversation: vec![],
- tool_calls: tool_calls.into_iter().map(|s| s.to_string()).collect(),
- error: error.map(|s| s.to_string()),
- }
- }
-
- #[test]
- fn test_all_pass() {
- let assertions = SpotAssertions {
- response_contains: vec!["hello".to_string()],
- tools_used: vec!["echo".to_string()],
- no_error: true,
- ..Default::default()
- };
- let sub = make_submission("Hello, world!", vec!["echo"], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
- assert!(failures.is_empty());
- }
-
- #[test]
- fn test_hard_fail_on_error() {
- let assertions = SpotAssertions {
- no_error: true,
- response_contains: vec!["hello".to_string()],
- ..Default::default()
- };
- let sub = make_submission("Hello!", vec![], Some("timeout after 60s"));
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 0.0);
- assert!(failures[0].contains("no_error"));
- }
-
- #[test]
- fn test_partial_score() {
- let assertions = SpotAssertions {
- response_contains: vec!["alpha".to_string(), "beta".to_string()],
- ..Default::default()
- };
- let sub = make_submission("alpha is here but not the other", vec![], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 0.5);
- assert_eq!(failures.len(), 1);
- assert!(failures[0].contains("beta"));
- }
-
- #[test]
- fn test_response_not_contains() {
- let assertions = SpotAssertions {
- response_not_contains: vec!["error".to_string(), "fail".to_string()],
- ..Default::default()
- };
- let sub = make_submission("This is an error message", vec![], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 0.5);
- assert_eq!(failures.len(), 1);
- assert!(failures[0].contains("error"));
- }
-
- #[test]
- fn test_tools_used_and_not_used() {
- let assertions = SpotAssertions {
- tools_used: vec!["time".to_string()],
- tools_not_used: vec!["shell".to_string(), "echo".to_string()],
- ..Default::default()
- };
- let sub = make_submission("The time is now", vec!["time"], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
- assert!(failures.is_empty());
- }
-
- #[test]
- fn test_tools_not_used_fails() {
- let assertions = SpotAssertions {
- tools_not_used: vec!["shell".to_string()],
- ..Default::default()
- };
- let sub = make_submission("result", vec!["shell", "time"], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 0.0);
- }
-
- #[test]
- fn test_response_matches_regex() {
- let assertions = SpotAssertions {
- response_matches: Some(r"\d{4}".to_string()),
- ..Default::default()
- };
- let sub = make_submission("The year is 2026", vec![], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
- assert!(failures.is_empty());
- }
-
- #[test]
- fn test_response_matches_regex_fail() {
- let assertions = SpotAssertions {
- response_matches: Some(r"^\d+$".to_string()),
- ..Default::default()
- };
- let sub = make_submission("not a number", vec![], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 0.0);
- }
-
- #[test]
- fn test_min_max_tool_calls() {
- let assertions = SpotAssertions {
- min_tool_calls: Some(2),
- max_tool_calls: Some(4),
- ..Default::default()
- };
-
- // Within range
- let sub = make_submission("ok", vec!["a", "b", "c"], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
-
- // Too few
- let sub = make_submission("ok", vec!["a"], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 0.5);
- assert!(failures[0].contains("min_tool_calls"));
-
- // Too many
- let sub = make_submission("ok", vec!["a", "b", "c", "d", "e"], None);
- let (score, failures) = assertions.evaluate(&sub);
- assert_eq!(score, 0.5);
- assert!(failures[0].contains("max_tool_calls"));
- }
-
- #[test]
- fn test_max_zero_tool_calls() {
- let assertions = SpotAssertions {
- max_tool_calls: Some(0),
- ..Default::default()
- };
- let sub = make_submission("just talking", vec![], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
-
- let sub = make_submission("oops", vec!["echo"], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 0.0);
- }
-
- #[test]
- fn test_empty_assertions() {
- let assertions = SpotAssertions::default();
- let sub = make_submission("anything", vec!["whatever"], None);
- let (score, _) = assertions.evaluate(&sub);
- assert_eq!(score, 1.0);
- }
-
- #[tokio::test]
- async fn test_spot_load_tasks() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("spot.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"id": "s1", "prompt": "Hello", "tags": ["smoke"], "assertions": {{"response_contains": ["hello"], "no_error": true}}}}"#
- )
- .unwrap();
- writeln!(
- file,
- r#"{{"id": "s2", "prompt": "Echo test", "assertions": {{"tools_used": ["echo"]}}}}"#
- )
- .unwrap();
-
- let suite = SpotSuite::new(&path);
- let tasks = suite.load_tasks().await.unwrap();
- assert_eq!(tasks.len(), 2);
- assert_eq!(tasks[0].id, "s1");
- assert_eq!(tasks[1].id, "s2");
- assert!(tasks[0].tags.contains(&"smoke".to_string()));
- }
-
- #[tokio::test]
- async fn test_spot_scoring() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("spot.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"id": "s1", "prompt": "Hello", "assertions": {{"response_contains": ["hello", "world"], "no_error": true}}}}"#
- )
- .unwrap();
-
- let suite = SpotSuite::new(&path);
- let tasks = suite.load_tasks().await.unwrap();
-
- // Full pass
- let sub = make_submission("Hello World!", vec![], None);
- let score = suite.score(&tasks[0], &sub).await.unwrap();
- assert_eq!(score.value, 1.0);
- assert_eq!(score.label, "pass");
-
- // Partial
- let sub = make_submission("Hello there", vec![], None);
- let score = suite.score(&tasks[0], &sub).await.unwrap();
- assert!(score.value > 0.0 && score.value < 1.0);
- assert_eq!(score.label, "partial");
-
- // Error hard fail
- let sub = make_submission("Hello World!", vec![], Some("boom"));
- let score = suite.score(&tasks[0], &sub).await.unwrap();
- assert_eq!(score.value, 0.0);
- assert_eq!(score.label, "fail");
- }
-}
diff --git a/benchmarks/src/adapters/swe_bench.rs b/benchmarks/src/adapters/swe_bench.rs
deleted file mode 100644
index 5f6c0b84..00000000
--- a/benchmarks/src/adapters/swe_bench.rs
+++ /dev/null
@@ -1,416 +0,0 @@
-use std::io::BufRead;
-use std::path::PathBuf;
-
-use async_trait::async_trait;
-use regex::Regex;
-use serde::Deserialize;
-
-use crate::error::BenchError;
-use crate::suite::{BenchScore, BenchSuite, BenchTask, TaskSubmission};
-
-/// Validate that a string is safe for use as a filesystem path component.
-/// Allows alphanumerics, hyphens, underscores, dots, and forward slashes (for nested paths).
-/// Rejects absolute paths, `..` traversal, and shell metacharacters.
-fn is_safe_path_component(s: &str) -> bool {
- !s.is_empty()
- && !s.starts_with('/')
- && !s.contains("..")
- && s.chars()
- .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/'))
-}
-
-/// Validate that a repo string matches the expected `owner/repo` GitHub format.
-fn is_valid_github_repo(repo: &str) -> bool {
- // Match "owner/repo" where both parts are alphanumeric with hyphens/underscores/dots
- static REPO_PATTERN: std::sync::LazyLock =
- std::sync::LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$").unwrap());
- REPO_PATTERN.is_match(repo)
-}
-
-/// Validate that a string looks like a git ref (hex SHA or valid ref name).
-fn is_valid_git_ref(s: &str) -> bool {
- !s.is_empty()
- && s.chars()
- .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/'))
- && !s.contains("..")
-}
-
-/// SWE-bench dataset entry.
-#[derive(Debug, Deserialize)]
-struct SweBenchEntry {
- instance_id: String,
- repo: String,
- base_commit: String,
- #[serde(default)]
- problem_statement: String,
- #[serde(default)]
- hints_text: Option,
- #[serde(default)]
- test_patch: Option,
- #[serde(default)]
- patch: Option,
-}
-
-/// SWE-bench Pro: real-world software engineering tasks.
-///
-/// Each task clones a repo at a specific commit, presents the problem statement,
-/// and expects the agent to produce a patch. Scoring runs the test suite.
-pub struct SweBenchSuite {
- dataset_path: PathBuf,
- workspace_dir: PathBuf,
- use_docker: bool,
-}
-
-impl SweBenchSuite {
- pub fn new(
- dataset_path: impl Into,
- workspace_dir: impl Into,
- use_docker: bool,
- ) -> Self {
- Self {
- dataset_path: dataset_path.into(),
- workspace_dir: workspace_dir.into(),
- use_docker,
- }
- }
-}
-
-#[async_trait]
-impl BenchSuite for SweBenchSuite {
- fn name(&self) -> &str {
- "SWE-bench Pro"
- }
-
- fn id(&self) -> &str {
- "swe_bench"
- }
-
- async fn load_tasks(&self) -> Result, BenchError> {
- let file = std::fs::File::open(&self.dataset_path)?;
- let reader = std::io::BufReader::new(file);
- let mut tasks = Vec::new();
-
- for (line_num, line) in reader.lines().enumerate() {
- let line = line?;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- let entry: SweBenchEntry = serde_json::from_str(trimmed).map_err(|e| {
- BenchError::Config(format!("swe_bench line {}: {}", line_num + 1, e))
- })?;
-
- if !is_safe_path_component(&entry.instance_id) {
- return Err(BenchError::Config(format!(
- "swe_bench line {}: unsafe instance_id \"{}\"",
- line_num + 1,
- entry.instance_id,
- )));
- }
- if !is_valid_github_repo(&entry.repo) {
- return Err(BenchError::Config(format!(
- "swe_bench line {}: invalid repo format \"{}\"",
- line_num + 1,
- entry.repo,
- )));
- }
- if !is_valid_git_ref(&entry.base_commit) {
- return Err(BenchError::Config(format!(
- "swe_bench line {}: invalid base_commit \"{}\"",
- line_num + 1,
- entry.base_commit,
- )));
- }
-
- let metadata = serde_json::json!({
- "repo": entry.repo,
- "base_commit": entry.base_commit,
- "test_patch": entry.test_patch,
- "gold_patch": entry.patch,
- "use_docker": self.use_docker,
- "workspace_dir": self.workspace_dir.to_string_lossy(),
- });
-
- let prompt = if let Some(ref hints) = entry.hints_text {
- format!("{}\n\nHints:\n{}", entry.problem_statement, hints)
- } else {
- entry.problem_statement
- };
-
- tasks.push(BenchTask {
- id: entry.instance_id,
- prompt,
- context: Some(format!(
- "Repository: {}, Commit: {}",
- entry.repo, entry.base_commit
- )),
- resources: vec![],
- tags: vec![format!("repo-{}", entry.repo.replace('/', "-"))],
- expected_turns: None,
- timeout: None,
- metadata,
- });
- }
-
- Ok(tasks)
- }
-
- async fn setup_task(&self, task: &BenchTask) -> Result<(), BenchError> {
- let repo = task
- .metadata
- .get("repo")
- .and_then(|v| v.as_str())
- .ok_or_else(|| BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: "missing repo in metadata".to_string(),
- })?;
- let base_commit = task
- .metadata
- .get("base_commit")
- .and_then(|v| v.as_str())
- .ok_or_else(|| BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: "missing base_commit in metadata".to_string(),
- })?;
-
- let task_dir = self.workspace_dir.join(&task.id);
-
- // Clone repo if not already present
- if !task_dir.exists() {
- let repo_url = format!("https://github.com/{}.git", repo);
- let output = tokio::process::Command::new("git")
- .args([
- "clone",
- "--depth",
- "1",
- &repo_url,
- &task_dir.to_string_lossy(),
- ])
- .output()
- .await
- .map_err(|e| BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: format!("git clone failed: {e}"),
- })?;
-
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: format!("git clone failed: {stderr}"),
- });
- }
- }
-
- // Checkout the base commit
- let output = tokio::process::Command::new("git")
- .args(["checkout", base_commit])
- .current_dir(&task_dir)
- .output()
- .await
- .map_err(|e| BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: format!("git checkout failed: {e}"),
- })?;
-
- if !output.status.success() {
- // Shallow clone might not have the commit; fetch more history
- let _ = tokio::process::Command::new("git")
- .args(["fetch", "--unshallow"])
- .current_dir(&task_dir)
- .output()
- .await;
-
- let output = tokio::process::Command::new("git")
- .args(["checkout", base_commit])
- .current_dir(&task_dir)
- .output()
- .await
- .map_err(|e| BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: format!("git checkout retry failed: {e}"),
- })?;
-
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(BenchError::TaskFailed {
- task_id: task.id.clone(),
- reason: format!("git checkout failed: {stderr}"),
- });
- }
- }
-
- Ok(())
- }
-
- async fn teardown_task(&self, task: &BenchTask) -> Result<(), BenchError> {
- let task_dir = self.workspace_dir.join(&task.id);
- if task_dir.exists() {
- // Reset any changes
- let _ = tokio::process::Command::new("git")
- .args(["checkout", "."])
- .current_dir(&task_dir)
- .output()
- .await;
- let _ = tokio::process::Command::new("git")
- .args(["clean", "-fdx"])
- .current_dir(&task_dir)
- .output()
- .await;
- }
- Ok(())
- }
-
- async fn score(
- &self,
- task: &BenchTask,
- submission: &TaskSubmission,
- ) -> Result {
- // For SWE-bench, scoring requires running the test patch against the agent's changes.
- // This is a simplified version that checks if the agent produced any code changes.
-
- let test_patch = task.metadata.get("test_patch").and_then(|v| v.as_str());
-
- if submission.response.is_empty() {
- return Ok(BenchScore::fail("no response from agent"));
- }
-
- // If we have a test patch, try to verify the submission
- if let Some(_test_patch) = test_patch {
- // TODO: Apply agent's patch, then apply test patch, then run tests.
- // For now, give partial credit if the agent produced some output.
- tracing::warn!(
- task_id = %task.id,
- "SWE-bench test execution not implemented, returning placeholder 0.25"
- );
- Ok(BenchScore::partial(
- 0.25,
- "test execution not yet implemented; partial credit for response",
- ))
- } else {
- tracing::warn!(
- task_id = %task.id,
- "no test_patch available, returning placeholder 0.25"
- );
- Ok(BenchScore::partial(
- 0.25,
- "no test_patch available for automated scoring",
- ))
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::io::Write;
-
- #[tokio::test]
- async fn test_swe_bench_load() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("swe.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"instance_id": "django__django-12345", "repo": "django/django", "base_commit": "abc123", "problem_statement": "Fix the ORM bug"}}"#
- )
- .unwrap();
-
- let suite = SweBenchSuite::new(&path, "/tmp/swe-test", false);
- let tasks = suite.load_tasks().await.unwrap();
- assert_eq!(tasks.len(), 1);
- assert_eq!(tasks[0].id, "django__django-12345");
- assert!(tasks[0].tags.contains(&"repo-django-django".to_string()));
- }
-
- #[tokio::test]
- async fn test_swe_bench_scoring_no_response() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("swe.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"instance_id": "s1", "repo": "org/repo", "base_commit": "abc", "problem_statement": "Fix bug"}}"#
- )
- .unwrap();
-
- let suite = SweBenchSuite::new(&path, "/tmp/swe-test", false);
- let tasks = suite.load_tasks().await.unwrap();
-
- let submission = TaskSubmission {
- response: String::new(),
- conversation: vec![],
- tool_calls: vec![],
- error: None,
- };
- let score = suite.score(&tasks[0], &submission).await.unwrap();
- assert_eq!(score.value, 0.0);
- }
-
- #[test]
- fn test_is_safe_path_component() {
- assert!(is_safe_path_component("django__django-12345"));
- assert!(is_safe_path_component("org/repo"));
- assert!(is_safe_path_component("abc123"));
- assert!(!is_safe_path_component(""));
- assert!(!is_safe_path_component("../../etc/passwd"));
- assert!(!is_safe_path_component("/etc/passwd"));
- assert!(!is_safe_path_component("foo;rm -rf /"));
- assert!(!is_safe_path_component("foo bar"));
- }
-
- #[test]
- fn test_is_valid_github_repo() {
- assert!(is_valid_github_repo("django/django"));
- assert!(is_valid_github_repo("org/repo-name"));
- assert!(is_valid_github_repo("Org.Name/Repo_v2"));
- assert!(!is_valid_github_repo(""));
- assert!(!is_valid_github_repo("no-slash"));
- assert!(!is_valid_github_repo("too/many/slashes"));
- assert!(!is_valid_github_repo("spa ce/repo"));
- }
-
- #[test]
- fn test_is_valid_git_ref() {
- assert!(is_valid_git_ref("abc123"));
- assert!(is_valid_git_ref("deadbeef0123456789abcdef0123456789abcdef"));
- assert!(is_valid_git_ref("v1.2.3"));
- assert!(is_valid_git_ref("main"));
- assert!(!is_valid_git_ref(""));
- assert!(!is_valid_git_ref("bad..ref"));
- assert!(!is_valid_git_ref("has space"));
- assert!(!is_valid_git_ref("semi;colon"));
- }
-
- #[tokio::test]
- async fn test_swe_bench_rejects_path_traversal() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("swe.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"instance_id": "../../etc/passwd", "repo": "org/repo", "base_commit": "abc", "problem_statement": "evil"}}"#
- )
- .unwrap();
-
- let suite = SweBenchSuite::new(&path, "/tmp/swe-test", false);
- let err = suite.load_tasks().await.unwrap_err();
- assert!(err.to_string().contains("unsafe instance_id"));
- }
-
- #[tokio::test]
- async fn test_swe_bench_rejects_bad_repo() {
- let dir = tempfile::tempdir().unwrap();
- let path = dir.path().join("swe.jsonl");
- let mut file = std::fs::File::create(&path).unwrap();
- writeln!(
- file,
- r#"{{"instance_id": "task1", "repo": "not-a-repo-format", "base_commit": "abc", "problem_statement": "bad"}}"#
- )
- .unwrap();
-
- let suite = SweBenchSuite::new(&path, "/tmp/swe-test", false);
- let err = suite.load_tasks().await.unwrap_err();
- assert!(err.to_string().contains("invalid repo format"));
- }
-}
diff --git a/benchmarks/src/adapters/tau_bench.rs b/benchmarks/src/adapters/tau_bench.rs
deleted file mode 100644
index 28c1c965..00000000
--- a/benchmarks/src/adapters/tau_bench.rs
+++ /dev/null
@@ -1,233 +0,0 @@
-use std::io::BufRead;
-use std::path::PathBuf;
-
-use async_trait::async_trait;
-use serde::Deserialize;
-
-use crate::error::BenchError;
-use crate::suite::{BenchScore, BenchSuite, BenchTask, ConversationTurn, TaskSubmission};
-
-/// Tau-bench task entry.
-#[derive(Debug, Deserialize)]
-struct TauBenchEntry {
- id: String,
- #[serde(default)]
- domain: String,
- instruction: String,
- #[serde(default)]
- user_persona: Option,
- #[serde(default)]
- expected_state: Option,
- #[serde(default)]
- expected_actions: Vec,
- #[serde(default)]
- max_turns: Option,
-}
-
-/// Tau-bench: multi-turn tool-calling dialog benchmark.
-///
-/// Tests agent ability to handle customer service scenarios with simulated
-/// domain APIs (retail, airline). Scoring compares final state against expected.
-pub struct TauBenchSuite {
- dataset_path: PathBuf,
- domain: String,
-}
-
-impl TauBenchSuite {
- pub fn new(dataset_path: impl Into, domain: impl Into) -> Self {
- Self {
- dataset_path: dataset_path.into(),
- domain: domain.into(),
- }
- }
-}
-
-#[async_trait]
-impl BenchSuite for TauBenchSuite {
- fn name(&self) -> &str {
- "Tau-bench"
- }
-
- fn id(&self) -> &str {
- "tau_bench"
- }
-
- async fn load_tasks(&self) -> Result, BenchError> {
- let file = std::fs::File::open(&self.dataset_path)?;
- let reader = std::io::BufReader::new(file);
- let mut tasks = Vec::new();
-
- for (line_num, line) in reader.lines().enumerate() {
- let line = line?;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- let entry: TauBenchEntry = serde_json::from_str(trimmed).map_err(|e| {
- BenchError::Config(format!("tau_bench line {}: {}", line_num + 1, e))
- })?;
-
- let domain = if entry.domain.is_empty() {
- self.domain.clone()
- } else {
- entry.domain.clone()
- };
-
- let metadata = serde_json::json!({
- "domain": domain,
- "user_persona": entry.user_persona,
- "expected_state": entry.expected_state,
- "expected_actions": entry.expected_actions,
- });
-
- tasks.push(BenchTask {
- id: entry.id,
- prompt: entry.instruction,
- context: entry.user_persona.clone(),
- resources: vec![],
- tags: vec![format!("domain-{domain}")],
- expected_turns: entry.max_turns,
- timeout: None,
- metadata,
- });
- }
-
- Ok(tasks)
- }
-
- async fn score(
- &self,
- task: &BenchTask,
- submission: &TaskSubmission,
- ) -> Result {
- // Score based on expected actions completion
- let expected_actions: Vec = task
- .metadata
- .get("expected_actions")
- .and_then(|v| serde_json::from_value(v.clone()).ok())
- .unwrap_or_default();
-
- if expected_actions.is_empty() {
- // No expected actions defined; score based on whether agent responded
- if submission.response.is_empty() {
- return Ok(BenchScore::fail("no response"));
- }
- return Ok(BenchScore::partial(
- 0.5,
- "no expected_actions to evaluate against",
- ));
- }
-
- // Check which expected actions were actually called
- let called: std::collections::HashSet<&str> =
- submission.tool_calls.iter().map(|s| s.as_str()).collect();
- let matched = expected_actions
- .iter()
- .filter(|a| called.contains(a.as_str()))
- .count();
-
- let ratio = matched as f64 / expected_actions.len() as f64;
- if ratio >= 1.0 {
- Ok(BenchScore::pass())
- } else if ratio > 0.0 {
- Ok(BenchScore::partial(
- ratio,
- format!(
- "{}/{} expected actions completed",
- matched,
- expected_actions.len()
- ),
- ))
- } else {
- Ok(BenchScore::fail(format!(
- "0/{} expected actions completed",
- expected_actions.len()
- )))
- }
- }
-
- async fn next_user_message(
- &self,
- task: &BenchTask,
- conversation: &[ConversationTurn],
- ) -> Result