All PostgreSQL connection sites hardcoded NoTls, preventing connections
to managed providers that require TLS (AWS RDS, Neon, Supabase, etc.).
- Add tokio-postgres-rustls with rustls + system root certificates
- Add SslMode enum (disable/prefer/require) via DATABASE_SSLMODE env var
- Replace NoTls at all 4 production call sites with TLS-aware pool creation
- Add SslMode::from_env() helper for lightweight CLI tools
- Log native cert loading errors and warn on empty root store
Default mode is Prefer (attempts TLS, matching most managed providers).
Co-authored-by: Claude Opus 4.6 <[email protected]>
* test: add failing tests for Discord signature validation and capabilities alias (Red phase)
TDD Red phase for #148. Adds 19 tests across 4 categories:
- Category 1: CredentialLocationSchema header_name alias (2 failing)
- Category 2: Ed25519 signature verification (3 failing)
- Category 3: Router signature key management (2 failing)
- Category 5: Discord capabilities public_key setup (1 failing)
All 8 failures are expected — stubs return false/None by design.
Implementation will follow in Green phase.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Discord Ed25519 signature verification and capabilities alias (#148)
Implement the Green phase for Discord channel security fixes:
- Add real Ed25519 signature verification in signature.rs using ed25519-dalek
- Add #[serde(alias = "header_name")] to CredentialLocationSchema::Header
for backward compatibility with external JSON files
- Add signature_keys storage to WasmChannelRouter (register/get/unregister)
- Add discord_public_key to discord.capabilities.json setup.required_secrets
- Add nested capabilities resolution to CapabilitiesFile for channel-level
JSON compatibility
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: address PR #372 review comments
- Fix invalid hex character in test fake_pub_key (router.rs)
- Simplify signature parsing with from_slice/try_from (signature.rs)
- Use idiomatic Option::or for nested capability merging (capabilities_schema.rs)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce signature verification, staleness check, key validation, recursive resolve
Address PR #372 review feedback:
- Wire verify_discord_signature() into webhook_handler with Ed25519
signature + timestamp staleness check (5s window via now_secs param)
- Validate Ed25519 keys in register_signature_key() (hex decode +
VerifyingKey::try_from) before storing, return Result<(), String>
- Recursively resolve nested capabilities in resolve_nested()
- Add 25 new tests: 8 staleness, 6 key validation, 7 webhook
integration (tower::oneshot), 4 resolve_nested edge cases
- Fix pre-existing clippy warning in signal.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: wire register_signature_key() into all channel loading paths
The Ed25519 signature key registration was implemented and tested but
never called from production code. All three channel loading paths
(setup_wasm_channels, activate_wasm_channel, refresh_active_channel)
now read the public key from the secrets store and register it with
the webhook router, enabling Discord signature verification.
Adds `signature_key_secret_name` field to WebhookSchema so channels
can declare which secret contains their Ed25519 public key.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(channels): add native Signal channel via signal-cli HTTP daemon
Implement a native Rust Signal channel that connects to a running
signal-cli daemon's HTTP endpoint, enabling Signal messaging without
WASM overhead.
Architecture:
- SSE listener at /api/v1/events for receiving messages with automatic
reconnection and exponential backoff
- JSON-RPC client at /api/v1/rpc for sending messages and typing
indicators
- Reply target tracking via Arc<RwLock<HashMap>> to route responses
back to the correct DM or group conversation
Features:
- User allowlisting supporting E.164 phone numbers, bare UUIDs, and
uuid:-prefixed identifiers (matching OpenClaw's format)
- Group allowlisting with wildcard (*) support
- Configurable story and attachment-only message filtering
- Health check via signal-cli /api/v1/check
- Broadcast support to all tracked reply targets
Configuration via environment variables:
- SIGNAL_HTTP_URL, SIGNAL_ACCOUNT (required)
- SIGNAL_ALLOWED_USERS, SIGNAL_ALLOWED_GROUPS
- SIGNAL_IGNORE_ATTACHMENTS (default: false)
- SIGNAL_IGNORE_STORIES (default: true)
Includes unit tests covering allowlist logic, envelope parsing,
recipient targeting, SSE deserialization, and edge cases.
* refactor(signal): remove expect|unwrap calls
- Change SignalChannel::new to return Result<Self, ChannelError>
- Replace .expect() on reqwest client build with proper error handling
- Replace .expect() on NonZeroUsize with compile-time const using unsafe new_unchecked
- Propagate errors through test helpers to avoid unwraps in tests
* fix(signal): prevent OOM from chunked response without Content-Length
Use bytes_stream() to check response size during download rather than
buffering entire body first. This closes the OOM vector where a
malicious signal-cli daemon could send unbounded chunked data.
* fix(signal): align is_e164 minimum digits with setup wizard
Both now require 7-15 digits after '+', preventing environment
variable bypass of the stricter onboarding validation.
* refactor(signal): extract from_parts constructor
Extract SignalChannel::from_parts() used by both new() and
sse_listener() to ensure consistent object construction.
* chore: remove redundant unused var
* refactor(signal): rename allowed_users to allow_from and add dm_policy/group_policy
- Rename allowed_users -> allow_from for consistency with other channels
- Rename allowed_groups -> allow_from_groups
- Add dm_policy field: 'open', 'allowlist', or 'pairing' (default: 'pairing')
- Add group_policy field: 'allowlist', 'open', or 'disabled' (default: 'allowlist')
- Add group_allow_from field that inherits from allow_from if empty
- Implement dm_policy and group_policy logic in message processing
- Add environment variable resolution: SIGNAL_ALLOW_FROM, SIGNAL_ALLOW_FROM_GROUPS,
SIGNAL_DM_POLICY, SIGNAL_GROUP_POLICY, SIGNAL_GROUP_ALLOW_FROM
- Add setup wizard prompts for new policy options
- Note: full pairing flow (PairingStore integration) marked as pending for future PR
* feat(signal): implement DM pairing workflow for unapproved senders
- Add PairingStore integration to check approved senders
- Handle pairing requests for unknown senders with dm_policy=pairing
- Send pairing reply message with approval instructions
- Update FEATURE_PARITY.md to reflect DM pairing support
* chore(ci): fix clippy warnings
* feat: add HTML-to-Markdown conversion for web content
- Add readabilityrs for content extraction
- Add html-to-markdown for conversion
- Feature-gated behind html-markdown flag
- Integrates with HTTP tool response handling
- Includes comprehensive tests and examples
Closes#106
* Update comments for is_html_response helper and fix tests to not fail silently in certain instances
---------
Co-authored-by: Zach Frederick <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* feat: embedded registry catalog and WASM bundle install pipeline
Embed registry manifests at compile time so the extension catalog is
available without network access. Add tar.gz bundle support for WASM
extension downloads (tools and channels), a /api/extensions/registry
endpoint, CI job to build and publish WASM bundles on release, and
ephemeral in-memory secrets fallback so the extension manager works
even without a persistent secrets store.
Key changes:
- build.rs: collect registry/*.json into embedded_catalog.json at compile time
- src/registry/embedded.rs + catalog.rs: load embedded or on-disk catalog
- src/extensions/manager.rs: download_and_install_wasm handles tar.gz bundles,
bare .wasm files, and separate capabilities downloads; wasm channel install
- src/channels/web/server.rs: /api/extensions/registry endpoint + no-cache headers
- src/app.rs: ephemeral InMemorySecretsStore fallback for extension manager
- registry/*.json: populate artifact download URLs for release bundles
- .github/workflows/release.yml: build-wasm-extensions CI job
- Simplified setup wizard and CLI registry commands
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — archive hardening, decompression bomb guard, test fix
- Add 100 MB decompressed entry size cap to tar.gz extraction in both
manager.rs and installer.rs to prevent decompression bombs
- Add archive.set_preserve_permissions(false) and set_unpack_xattrs(false)
for defense-in-depth against malicious archives
- Fix test assertion logic in catalog.rs (|| → || with correct negation)
- Replace silent tar fallback in CI with explicit if/else for capabilities
- Add warning when installing without SHA256 verification
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve clippy warning in settings.rs and enforce zero-warnings policy
Use struct initializer with ..Default::default() instead of field
reassignment. Update CLAUDE.md to codify zero clippy warnings policy —
all warnings must be fixed before committing, including pre-existing ones.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review round 2 — build reliability, caps validation, naming
- build.rs: emit per-file rerun-if-changed for reliable content tracking;
fix bundles fallback to match BundlesFile shape ({"bundles":{}})
- embedded.rs: parse catalog once via OnceLock instead of double-parsing
- manager.rs + installer.rs: add 1 MB size cap on capabilities_url downloads
with proper error surfacing
- secrets/store.rs: rename misleading `pub mod testing` to `pub mod in_memory`
- server.rs: track installed extensions by (name, kind) tuple to avoid
false positives across different extension kinds
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: direct agentic loop for SWE-bench benchmarks
Replace the full Agent-based runner with a purpose-built agentic loop
that directly calls the LLM with tools. The old path routed through
SafetyLayer (which blocked SWE-bench prompts), dispatcher (capped at
10 iterations), approval flow (wasted iterations), and 20+ irrelevant
builtin tools (diluted the model's focus).
New architecture:
- AgenticLoop: LLM call -> tool execution -> repeat (up to 30 iters)
- Per-task tool scoping via BenchSuite::task_tools() with working dirs
- Suite-provided system prompts via BenchSuite::system_prompt()
- No safety layer, no approval flow, no sessions/threads overhead
- Configurable max_iterations in BenchConfig and TOML
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: apply --model CLI override to LLM provider
The --model flag was updating matrix entry labels but not the actual
LLM provider, so requests were still sent using the model from .env.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: configurable tool iterations and auto-approve for benchmarks
Add max_tool_iterations and auto_approve_tools settings to AgentConfig,
replacing the hardcoded MAX_TOOL_ITERATIONS constant. Fix shell_injection
policy rule to not block markdown backtick code snippets.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address benchmarks crate audit findings
High:
- Fix truncate_output UTF-8 panic on multi-byte char boundaries
- Fix parallel results durability (write JSONL per-task, not after all)
Medium:
- Fix --sample to use random shuffle instead of first-N
- Delegate all LlmProvider methods in InstrumentedLlm
- Fix LLM-as-judge to return fail instead of misleading 0.5
- Remove unnecessary shallow clone (always gets unshallowed)
- Replace .unwrap() with .expect() in LazyLock regex init
Low:
- Remove dead code: unused error variants, trait methods, struct fields
- Remove BenchSuite::name() (redundant with id())
- Remove TaskSubmission::conversation, ConversationTurn, TurnRole
- Remove unused methods from BenchChannel, results, config
- Clean up ChannelCapture conversation tracking
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add SWE-bench dataset and Docker scoring infrastructure
Add the SWE-bench Lite dataset (300 tasks) and Docker files for
isolated test execution and scoring of SWE-bench patches.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: remove benchmarks (extracted to separate repo)
Benchmarks crate has been extracted to its own repository.
Remove the workspace member and all benchmarks/ files.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing AgentConfig fields in test initializer
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add extension registry with metadata catalog, CLI, and onboarding integration
Adds a central registry that catalogs all 14 available extensions (10 tools,
4 channels) with their capabilities, auth requirements, and artifact references.
The onboarding wizard now shows installable channels from the registry and
offers tool installation as a new Step 7.
- registry/ folder with per-extension JSON manifests and bundle definitions
- src/registry/ module: manifest structs, catalog loader, installer
- `ironclaw registry list|info|install|install-defaults` CLI commands
- Setup wizard enhanced: channels from registry, new extensions step (8 steps)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(setup): resolve workspace errors for tool crates and channels-only onboarding
Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during
onboard install because Cargo resolved them as part of the root workspace.
Add `[workspace]` table to each standalone crate and extend the root
`workspace.exclude` list so they build independently.
Channels-only mode (`onboard --channels-only`) failed with "Secrets not
configured" and "No database connection" because it skipped database and
security setup. Add `reconnect_existing_db()` to establish the DB connection
and load saved settings before running channel configuration.
Also improve the tunnel "already configured" display to show full provider
details (domain, mode, command) instead of just the provider name.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(registry): address PR review feedback on installer and catalog
- Use manifest.name (not crate_name) for installed filenames so
discovery, auth, and CLI commands all agree on the stem (#1)
- Add AlreadyInstalled error variant instead of misleading
ExtensionNotFound (#2)
- Add DownloadFailed error variant with URL context instead of
stuffing URLs into PathBuf (#3)
- Validate HTTP status with error_for_status() before reading
response bytes in artifact downloads (#4)
- Switch build_wasm_component to tokio::process::Command with
status() so build output streams to the terminal (#6)
- Find WASM artifact by crate_name specifically instead of picking
the first .wasm file in the release directory (#7)
- Add is_file() guard in catalog loader to skip directories (#8)
- Detect ambiguous bare-name lookups when both tools/<name> and
channels/<name> exist, with get_strict() returning an error (#9)
- Fix wizard step_extensions to check tool.name for installed
detection, consistent with the new naming (#11, #12)
- Fix redundant closures and map_or clippy warnings in changed files
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(setup): restore DB connection fields after settings reload
reconnect_postgres() and reconnect_libsql() called Settings::from_db_map()
which overwrote database_url / libsql_path / libsql_url set from env vars.
Also use get_strict() in cmd_info to surface ambiguous bare-name errors.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix clippy collapsible_if and print_literal warnings
Collapse nested if-let chains and inline string literals in format
macros to satisfy CI clippy lint checks (deny warnings).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(registry): prefer artifacts for install-defaults and improve dir lookup
- InstallDefaults now defaults to downloading pre-built artifacts
(matching `registry install` behavior), with --build flag for source builds.
- find_registry_dir() walks up 3 ancestor levels from the exe and adds
a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* docs(security): add network security reference for all listeners
Catalogs every network-facing surface (web gateway, webhook server,
orchestrator API, OAuth callback, sandbox proxy) with auth mechanisms,
bind addresses, egress controls, known findings, and a review checklist
for PRs that touch network-facing code.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(security): address three network security findings
- Use constant-time comparison (ct_eq) for webhook secret validation,
matching the pattern in web gateway and orchestrator auth
- Add X-Content-Type-Options and X-Frame-Options security headers to
the web gateway via SetResponseHeaderLayer
- Warn at startup when HTTP webhook server binds to 0.0.0.0
- Update NETWORK_SECURITY.md to mark findings 1, 4, 5 as resolved
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(security): address PR #201 review findings
- Reorder web gateway layers so security headers (X-Content-Type-Options,
X-Frame-Options) are outermost and apply to all responses including
DefaultBodyLimit 413 rejections
- Move 0.0.0.0 warning to final bind address resolution so it fires for
WASM-only webhook servers that fall back to the default address
- Add webhook handler auth tests: correct secret -> 200, wrong secret
-> 401, missing secret -> 401
- Rewrite NETWORK_SECURITY.md: replace brittle line-number references
with function/struct name anchors, add threat model section, document
graceful shutdown per listener, fill content gaps (health endpoint
responses, content-type validation, CSRF analysis, WS auth flow, MCP
trust boundary, orchestrator rate limiting), change findings F-4/F-5
from "Resolved" to "Mitigated" with caveats
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt and clippy warnings from main merge
Fix formatting in llm/mod.rs and llm/rig_adapter.rs introduced by
PR #132, and collapse nested if in rig_adapter.rs per clippy.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: split large files and consolidate test stubs for contributor velocity
- Extract 7 Database sub-traits (ConversationStore, JobStore, SandboxStore,
RoutineStore, ToolFailureStore, SettingsStore, WorkspaceStore) with Database
as a supertrait combining them all
- Split libsql_backend.rs (2769 lines) into src/db/libsql/ directory with
one file per sub-trait implementation
- Split config.rs (1753 lines) into src/config/ directory with 16 domain files
- Consolidate 3 duplicate test LLM stubs into shared StubLlm in src/testing.rs
- Split server.rs handlers into src/channels/web/handlers/ directory
- Extract main.rs init phases into AppBuilder (src/app.rs)
- Add developer setup script (scripts/dev-setup.sh)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: move heartbeat test from examples/ to tests/
Convert standalone example binary into a proper #[ignore] integration
test, matching the convention of the other integration tests.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting for CI
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review comments from Copilot
- tunnel.rs: replace .ok().flatten() with ? to propagate env var errors
- secrets.rs: remove misleading "process-wide cache" comment
- database.rs: use uppercase "DATABASE_URL" in error key
- testing.rs: gate harness tests with #[cfg(feature = "libsql")]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add secure prompt-based skills system (Phase 1 MVP)
Implement a skills system that extends the agent with prompt-level
instructions from local directories. Skills declare activation criteria,
tool permissions, and trust tiers that determine authority attenuation.
Core security model: the minimum trust level of any active skill
determines a tool ceiling -- tools above the ceiling are removed from
the LLM's tool list entirely at the API level, preventing prompt-based
manipulation.
New modules:
- skills/mod.rs: Core types (SkillTrust, SkillManifest, LoadedSkill)
- skills/scanner.rs: Content scanner for manipulation detection
- skills/registry.rs: Filesystem discovery and manifest parsing
- skills/selector.rs: Deterministic two-phase prefilter (no LLM)
- skills/attenuation.rs: Trust-based tool filtering
Integration:
- Agent loop selects skills per-turn and applies tool attenuation
- Reasoning engine injects skill context with structural isolation
- Config supports SKILLS_ENABLED, SKILLS_DIR, SKILLS_MAX_ACTIVE,
SKILLS_MAX_CONTEXT_TOKENS environment variables
- Disabled by default (SKILLS_ENABLED=false)
41 new tests covering all modules.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address all adversarial review findings for skills system
Security fixes:
- Escape skill name/version in XML attributes to prevent trust spoofing
- Escape prompt content to prevent </skill> tag breakout
- Require integrity hash for Verified/Community tier skills
- Validate skill names against [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}
- Add 64 KiB file size limit on prompt.md
Bug fixes:
- Use actual SkillsConfig from AgentDeps instead of SkillsConfig::default()
- Add skills_config field to AgentDeps, wired through from main.rs
Performance:
- Pre-compile regex patterns at load time (cached on LoadedSkill)
- Selector uses pre-compiled patterns instead of recompiling per message
- Switch all std::fs to tokio::fs for non-blocking async I/O
Hardening:
- Cap keyword score at 30 points to prevent keyword stuffing attacks
- Enforce max 20 keywords and 5 patterns per skill
- Normalize line endings (CRLF/CR to LF) before hashing
- Also includes cargo fmt formatting fixes for adjacent code
Tests: 54 skills tests pass (up from 41), zero new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address medium/low severity findings from adversarial review
Fixes all 18 medium/low severity findings identified by the security review:
- mod.rs: Add MAX_TAGS_PER_SKILL cap (10) in enforce_limits(); use
RegexBuilder with 64 KiB size_limit to prevent ReDoS; replace
case-enumerated escape_skill_content with regex matching all case
variants plus whitespace/null byte injection between </ and skill;
document allowed_patterns as unenforced until Phase 2; document
Marketplace URL validation as Phase 3 concern
- registry.rs: Add MAX_MANIFEST_FILE_SIZE (16 KiB) check before reading;
add symlink detection via symlink_metadata to reject symlinks in
discover_local; add MAX_DISCOVERED_SKILLS (100) cap; validate
prompt_hash format (sha256: + 64 hex chars); warn on name collision
before overwriting; accept SkillSource parameter in load_skill instead
of always using Local; add InvalidHashFormat, ManifestTooLarge,
SymlinkDetected error variants
- selector.rs: Add MAX_TAG_SCORE (15) cap parallel to keyword cap; warn
when declared max_context_tokens diverges >2x from actual prompt size
- scanner.rs: Add mixed-script homoglyph detection (Cyrillic, Greek,
Armenian unicode ranges); document token-boundary bypass and semantic
paraphrasing as known limitations
- attenuation.rs: Document READ_ONLY_TOOLS maintenance requirements
- agent_loop.rs: Surface scan warnings via structured tracing; add
structured audit events for skill activation and tool attenuation
61 tests pass, 0 new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address 12 findings from second adversarial security review
HIGH:
- Escape opening <skill tags in prompt content (prevents fake skill block injection)
- Scan manifest metadata fields (description, author, tags, reasons) not just prompt
- Block trust downgrade on name collision (existing Local can't be replaced by Community)
MEDIUM:
- Eliminate TOCTOU gap: read files then check size instead of metadata-then-read
- Reject file-level symlinks in load_skill (prompt.md, skill.toml)
- Truncate and filter manifest.skill.tags (prevent unlimited tag scoring)
- Cap regex pattern score at 40 (prevent 5x20=100 dominating keyword+tag)
- Add doc comment about skill_list tool exposing metadata (sanitization required)
- Move Community disclaimer inside <skill> tags (not outside structural boundary)
- Filter keywords/tags shorter than 3 chars (prevent broad matching)
LOW:
- Enforce minimum token_cost of 1 (max_context_tokens=0 can't bypass budget)
- Remove redundant try_exists checks in discover_local (let load_skill handle errors)
70 skills tests passing.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add HTTP endpoint scoping for skills (Phase 1)
Skills that declare an [http] section in skill.toml now have their HTTP
requests constrained to declared endpoints at runtime. This addresses
the gap where allowed_patterns was parsed but never enforced -- once the
http tool was visible via attenuation, the LLM could reach any URL.
Enforcement reuses EndpointPattern/AllowlistValidator from the WASM
capability system. Semantics: if no active skill declares [http], all
requests pass through (backward compat). If any skill declares [http],
URLs must match at least one skill's allowlist (union). Community skills'
[http] declarations are silently ignored (defense in depth).
Shell commands using curl/wget are also validated against scopes.
Scanner gains detection for known exfiltration domains (webhook.site,
ngrok.io, etc.), overly broad wildcards, and credential/host mismatches.
Closes#38
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Apply cargo fmt to http_scoping.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Apply cargo fmt across codebase
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add parameter-level permission enforcement for skills (Phase 2)
Activates enforcement of `allowed_patterns` in skill.toml permissions.
Previously these patterns were parsed but not enforced -- a Verified skill
declaring `permissions.shell` with `allowed_patterns = [{command = "cargo *"}]`
could still run any shell command. Now the enforcer validates tool parameters
against declared glob patterns before execution.
Key changes:
- New `enforcer.rs` module with `SkillPermissionEnforcer`, `glob_to_regex()`,
and `validate_tool_call()` with union semantics across active skills
- Typed pattern enums (`ShellPattern`, `FilePathPattern`, `MemoryTargetPattern`)
replace the previous `Vec<serde_json::Value>` in `ToolPermissionDeclaration`
- Scanner gains `scan_permission_patterns()` detecting dangerous patterns
(rm, sudo, curl, bare wildcards, command chaining, sensitive paths, identity files)
- Registry blocks non-Local skills with critical permission pattern warnings
- Agent loop threads enforcer into `execute_chat_tool` alongside HTTP scoping
Trust interaction: Community patterns ignored, Verified enforced, Local without
patterns unrestricted, Local with patterns enforced as guidance. Union semantics
across skills -- tool call allowed if ANY skill's patterns permit it.
34 new tests. All 818 library tests pass.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add worker permission enforcement and LLM behavioral analysis (Phase 3+4)
Phase 3 - Worker-side permission enforcement:
- Add SerializedToolPermission/SerializedPattern DTOs for HTTP boundary crossing
- Extend JobDescription, ContainerHandle, and orchestrator API to carry permissions
- CreateJobTool snapshots and forwards skill permissions to spawned workers
- Worker runtime builds SkillPermissionEnforcer and checks before tool execution
- Load-time token budget enforcement rejects prompts exceeding 2x declared budget
- Deduplicate enforcer construction: from_active_skills() delegates to from_serialized()
Phase 4 - LLM behavioral analysis:
- BehavioralAnalyzer with cached, LLM-based semantic content analysis
- Structured output parsing (FINDING|CATEGORY|SEVERITY|DESCRIPTION or CLEAN)
- Content-hash caching with bounded size (MAX_CACHE_ENTRIES=256)
- Graceful degradation when LLM unavailable
- Integrated into load_skill() for non-Local skills; critical findings block loading
Review fixes:
- Real cache tests with CountingLlm mock (test_cache_hit, test_cache_miss, test_cache_bounded)
- UTF-8-safe truncate() in worker runtime
- Few-shot examples in behavioral analysis prompt
- Documented max_context_tokens=0 opt-out and create_job() permission gap
848 tests passing, no new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address review feedback from serrrfirat on skills-phase2
- Fix truncate_cmd UTF-8 panic: use char-boundary-aware slicing
- Remove redundant effective_tools branching in reasoning.rs
- Document cache eviction as known limitation (arbitrary, not LRU)
- Add safety comment on SkillTrust enum ordering (security-critical)
- Simplify active_skills selection (prefilter_skills handles empty input)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining skills review feedback
* refactor: replace skills system with OpenClaw SKILL.md format + 2-state trust
Replace the 5-gate, 3-tier trust hierarchy (scanner, behavioral analyzer,
parameter-level enforcer, HTTP endpoint scoping) with a simplified 3-layer
security model: gating -> attenuation -> Docker confinement.
Key changes:
- SKILL.md format (YAML frontmatter + markdown prompt) replaces skill.toml + prompt.md
- 2-state trust (Installed/Trusted) replaces 3-tier (Community/Verified/Local)
- New parser.rs for SKILL.md parsing with serde_yaml
- New gating.rs for requirements checking (bins/env/config)
- Simplified registry with 2-location discovery (workspace + user dirs)
- Removed scanner, behavioral_analyzer, enforcer, http_scoping (~4,100 lines)
- Removed skill_permissions propagation through job/orchestrator/worker pipeline
- Added serde_yaml dependency for YAML frontmatter parsing
Net: -5,298 lines, 59 skills tests pass, 907 total tests pass.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add in-app skill management tools and ClawHub catalog integration
Add 4 chat-callable tools (skill_list, skill_search, skill_install,
skill_remove) plus matching web gateway endpoints for managing skills
at runtime. The catalog fetches from ClawHub's public registry API
at runtime rather than bundling entries at compile time.
Key changes:
- SkillRegistry gains mutation methods (install_skill, remove_skill,
reload, find_by_name) with Arc<RwLock> for concurrent access
- New catalog module queries ClawHub /api/v1/search with in-memory
caching (5-min TTL, configurable via CLAWHUB_REGISTRY env var)
- skill_list and skill_search added to READ_ONLY_TOOLS for safe use
under Installed trust ceiling
- Web gateway gets /api/skills, /api/skills/search, /api/skills/install,
and /api/skills/{name} DELETE endpoints
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #51 review feedback from ilblackdragon
Security:
- Add SSRF protection to fetch_skill_content: require HTTPS, reject
private/loopback/link-local IPs and internal hostnames, disable
redirects. Gateway install handler now reuses the same validation.
- URL-encode slug in skill_download_url to prevent query injection.
- Require X-Confirm-Action header on gateway skill install/remove
endpoints (equivalent to chat tool requires_approval gate).
Correctness:
- Eliminate all block_in_place/block_on usage in skill tools and
gateway handlers. Split install into prepare_install_to_disk (static
async, no lock) + commit_install (sync, brief write lock). Same
pattern for remove: validate_remove + delete_skill_files + commit_remove.
- Write normalized content to disk in install_skill (was writing
original un-normalized content, causing hash mismatch on re-read).
- Fix token estimation from 0.75 to 0.25 tokens/byte (~4 chars per
token) in registry.rs, selector.rs, and standalone loader.
Dependencies:
- Replace deprecated serde_yaml 0.9 with serde_yml 0.0.12.
- Remove unused toml dependency.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add benchmarking harness for agent evaluation
Introduces ironclaw-bench, a Rust-native benchmarking crate that drives the
real agent loop headlessly. Supports standard benchmarks (GAIA, Tau-bench,
SWE-bench Pro) and custom JSONL task sets with parallel execution, resume
support, and incremental JSONL output.
Key components:
- BenchChannel: headless Channel impl with auto-approval and response capture
- InstrumentedLlm: LlmProvider wrapper recording per-call token/cost metrics
- BenchRunner: task orchestration with parallel execution and JSONL resume
- Scoring utilities: exact match, contains, regex (all with normalization)
- CLI: run, results, compare, list subcommands via clap
- Four suite adapters: custom, gaia, tau_bench, swe_bench
Also fixes a pre-existing missing SseEvent::ToolResult match arm in the web
gateway and adds FinishReason to the LLM module's public re-exports.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add spot benchmark suite for end-to-end agent verification
Adds a "spot" suite with 13 scenarios across 4 categories (smoke,
tool use, multi-tool chaining, robustness) using multi-criterion
assertions instead of simple text matching. Also adds an `error`
field to TaskSubmission so suites can hard-fail on agent errors.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address audit findings in benchmarks crate
- Fix O(n²) scoring loop by indexing tasks in a HashMap (was re-parsing JSONL per result)
- Add UTF-8-safe truncation to prevent panic on multi-byte chars in channel capture
- Wire setup_task/teardown_task into both sequential and parallel runner paths
- Convert BenchRunner.suite from Box to Arc for parallel task setup/teardown
- Add tracing::warn for placeholder scores in custom, swe_bench, tau_bench adapters
- Add spot suite to CLI help text
- Add doc comment clarifying tools_used HashSet behavior in SpotAssertions
- Reorder match arms in create_suite to match KNOWN_SUITES alphabetical order
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: rewrite tasks.jsonl with scored results after scoring
The JSONL file was only written during execution (pre-scoring), so the
`results` command showed "pending" scores even after scoring completed.
Now the runner rewrites the JSONL with final scored results, keeping
task-level and aggregate data consistent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: prefix benchmark runs with model name and commit hash
Run logs and results table now show the base model and short git commit
hash, making it easy to correlate results with code versions. The commit
hash is also persisted in run.json for historical tracking.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add 8 memory benchmark scenarios to spot suite
Tests save-and-recall workflows using file tools:
- daily tasks, reminders, meeting notes, append logs
- detail extraction, todo priorities, multi-file ops
- context updates (write-read-rewrite-verify)
Total spot scenarios: 13 -> 21
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: fmt channel.rs and gitignore bench-results
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address critical and high findings from PR review
- Fix race condition: parallel mode now writes JSONL after all tasks
complete instead of concurrent unsynchronized appends
- Fix UTF-8 panic: use .chars().take(25) instead of byte slicing on
task_id which could panic on multi-byte characters
- Remove dead code: max_iterations (parsed but never used),
tool_whitelist() (declared but never called), MatrixEntry.tools
(declared but never applied)
- Eliminate double load_tasks(): cache task list on first load and
reuse the index for scoring instead of re-reading from disk
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: relax smoke-greeting assertion to not demand parrot greeting
The LLM often introduces itself without echoing "hello" back. Use a
regex that accepts any reasonable self-introduction (hello, hi, hey,
assistant, agent, help) instead of demanding a specific word.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: 100% spot baseline (GPT-5.2 @ 2c43b83, 21/21 pass)
Relax two brittle assertions:
- smoke-greeting: use regex for any reasonable self-intro instead of
demanding the model parrot "hello"
- memory-update-context: drop response_not_contains PST since the
model correctly says "not PST" which triggers the literal check
- memory-multifile: lower min_tool_calls from 4 to 3, the model can
batch two writes in one LLM turn
Baseline results committed to benchmarks/baselines/ for regression
tracking. Local runs stay in bench-results/ (gitignored).
Results: 100.0% pass, 1.000 avg, $0.31 cost, 111s wall time
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining PR review comments
- Replace .expect("semaphore closed") with proper error handling
- Derive PartialEq on BenchScore for cleaner test assertions
- Use ToPrimitive::to_f64() instead of string roundtrip in estimated_cost()
- Validate SWE-bench inputs: task_id (path traversal), repo (owner/repo format),
base_commit (valid git ref) with 5 new tests
- Skip "pending" (unscored) entries during resume so they get re-executed
- Use run.json mtime for find_latest_run (falls back to tasks.jsonl, then dir)
- Move additional_tools() outside parallel loop to share Arc<[Tool]> across tasks
- Add doc comments documenting known limitations (single-turn, resources, conversation)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: reject absolute paths in SWE-bench and validate matrix config
- is_safe_path_component now rejects paths starting with '/'
- BenchConfig::from_file validates matrix is non-empty
- Added tests for both validations
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: fail tasks on setup_task error and compute git hash once
- setup_task failure now records an error TaskResult instead of
continuing to run the task (both sequential and parallel paths)
- git_short_hash() computed once per run instead of twice
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: break up agent_loop.rs into four focused modules
Split the monolithic 2835-line agent_loop.rs into:
- agent_loop.rs (722L): Agent struct, event loop, message dispatch
- dispatcher.rs (635L): Agentic tool loop, tool execution, auth detection
- commands.rs (484L): System commands, job handlers, heartbeat, summarize
- thread_ops.rs (1059L): Thread lifecycle, approval, undo/redo, persistence
Each module gets its own impl Agent block. Agent fields changed to
pub(super) so sibling modules in the agent package can access them.
All 16 existing tests pass in their new locations.
Inspired by ZeroClaw's agent module split (agent.rs, loop_.rs,
dispatcher.rs, prompt.rs, memory_loader.rs).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add cost caps and guardrails for autonomous agent spending
Daily budget (MAX_COST_PER_DAY_CENTS) and hourly action rate
(MAX_ACTIONS_PER_HOUR) limits prevent runaway agents from burning
through API credits, especially in daemon/heartbeat modes.
- CostGuard with pre-flight check and post-call recording
- Sliding window for hourly rate, midnight-UTC daily reset
- 80% threshold warning, atomic fast-path for exceeded budget
- Wired into dispatcher loop (check before LLM call, record after)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add circuit breaker on LLM providers
Wraps LlmProvider with a Closed/Open/HalfOpen state machine that
trips after consecutive transient failures, preventing request storms
against a degraded backend. Automatically probes for recovery.
- CircuitBreakerProvider implements LlmProvider (drop-in wrapper)
- Transient error classification (server, rate-limit, network, auth infra)
- Client errors (wrong model, context overflow) don't trip the breaker
- Configurable via CIRCUIT_BREAKER_THRESHOLD and CIRCUIT_BREAKER_RECOVERY_SECS
- Composes with existing FailoverProvider (circuit breaker wraps failover)
- 12 tests covering full state machine and error classification
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add tunnel abstraction for remote access
Trait-based tunnel system with lifecycle management (start/stop/health)
for exposing the agent to the internet through external tunnel binaries.
Five providers:
- Cloudflare Tunnel (cloudflared, Zero Trust token auth)
- Tailscale (serve for tailnet, funnel for public)
- ngrok (with optional custom domain)
- Custom (arbitrary command with {host}/{port} placeholders)
- None (local-only, no external exposure)
Config via TUNNEL_PROVIDER + provider-specific env vars. Extends
existing TunnelConfig with optional managed provider alongside the
static TUNNEL_URL path. Factory, shared process management, and
37 tests covering all providers and edge cases.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add OS service management (launchd/systemd)
Adds `ironclaw service {install,start,stop,status,uninstall}` for
running the agent as a background daemon. macOS uses launchd plists
under ~/Library/LaunchAgents, Linux uses systemd user units.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add observability trait system with noop, log, and multi backends
Introduces an Observer trait for recording agent lifecycle events and
metrics, with pluggable backends. The noop backend compiles to zero
overhead, log backend uses tracing, and multi fans out to multiple
observers. Configured via OBSERVABILITY_BACKEND env var.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add in-memory LLM response cache with TTL and LRU eviction
CachedProvider wraps any LlmProvider and caches complete() responses
keyed by SHA-256(model + messages). Tool-calling requests are never
cached since they trigger side effects. Configurable via
RESPONSE_CACHE_ENABLED, RESPONSE_CACHE_TTL_SECS, and
RESPONSE_CACHE_MAX_ENTRIES env vars.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add memory hygiene with cadence-gated daily log cleanup
Adds workspace::hygiene module that automatically deletes daily log
documents older than a configurable retention period (default 30 days).
Runs on a 12-hour cadence tracked via a local state file to avoid
redundant passes. Best-effort design: failures are logged, never fatal.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add doctor diagnostics command for active health probing
Probes external dependencies (Docker, cloudflared, ngrok, tailscale),
validates NEAR AI session, checks database connectivity, and verifies
workspace directory. Complements the passive `status` command.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add structured TOML config file support
Adds ~/.ironclaw/config.toml as a configuration layer between env vars
and database settings. Priority: env var > TOML file > DB > defaults.
- `ironclaw config init` generates a commented config.toml from current settings
- `ironclaw --config path/to/config.toml` loads a custom config file
- Settings.merge_from() only overlays non-default values from the TOML file
- `ironclaw config path` now shows TOML file status
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address codex review findings
- apply_toml_overlay now returns Result and errors on explicit missing
or invalid config paths (was log-only, violating the documented
contract that explicit paths are fatal)
- custom tunnel url_pattern is now used to filter extracted URLs, not
just as a gate for scanning stdout
- systemd ExecStart path is now quoted to handle spaces in paths
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback
- Cache key now includes max_tokens, temperature, and stop_sequences
so different request parameters produce distinct keys
- to_cents() uses .trunc() + parse::<u64> instead of f64 intermediary,
avoiding precision loss for large values
- Tailscale public URL no longer includes local port (serve/funnel
expose on standard HTTPS port 443)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire up tunnel lifecycle and fix audit findings
Connect the tunnel module to the rest of the application so that
setting TUNNEL_PROVIDER actually starts a managed tunnel at boot and
stops it on shutdown. Previously create_tunnel() was never called
outside tests.
Changes:
- Expand TunnelSettings with provider credential fields (settings.rs)
- TunnelConfig::resolve() falls back to DB settings when env vars unset
- Start tunnel at boot, stop on shutdown, show URL in boot screen
- Setup wizard collects provider-specific credentials (ngrok, cloudflare,
tailscale, custom, static URL)
- Fix public_url() returning None under lock contention (SharedUrl)
- Fix local_host parameter ignored by cloudflare/ngrok/tailscale
- Fix tailscale silent fallback to "localhost" on bad JSON
- Fix ngrok globally mutating config via add-authtoken (use env var)
- Add 10s timeout to tailscale status --json
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review comments
- Document split_whitespace limitation in CustomTunnel doc comment
- Remove unnecessary quotes from systemd ExecStart directive
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback (round 3)
- doctor: missing libSQL DB on fresh install is Pass, not Fail
- service: quote ExecStart path for systemd space handling
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: correct cost guard doc comment (LLM calls, not LLM/tool)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add lifecycle hooks system with 6 interception points
Implement extensible hook infrastructure for intercepting and transforming
agent operations at well-defined points in the lifecycle:
- BeforeInbound: intercept/modify/reject incoming user messages
- BeforeToolCall: intercept/modify/reject tool executions (chat + job)
- BeforeOutbound: intercept/modify/suppress outgoing responses
- TransformResponse: transform final response before completing a turn
- OnSessionStart: fire-and-forget notification on new session creation
- OnSessionEnd: fire-and-forget notification on session pruning
Hooks execute in priority order with modification chaining, reject
short-circuits, configurable failure modes (FailOpen/FailClosed),
and per-hook timeouts. Empty registry is zero-cost (all hooks pass
through immediately).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce hook fail-closed semantics
* Merge upstream/main into feat/hooks-system-clean
Resolve merge conflicts:
- FEATURE_PARITY.md: Keep both upstream cron/routines status and hooks status
- src/error.rs: Keep both Hook and Orchestrator/Worker error variants
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve CI test failures in pairing store and wizard
- Fix pairing store truncate bug: record_failed_approve used
.truncate(true) which wiped the file before reading, causing rate
limiting to never accumulate past 1 attempt. Changed to
.truncate(false) to preserve existing data.
- Fix wizard test: skip test_install_missing_bundled_channels when
telegram WASM artifact specifically isn't available, not just when
all channels are empty (whatsapp may exist without telegram).
- Add workspace exclude for subcrate directories to prevent cargo
from discovering them as workspace members during builds.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #18 review comments
- Remove duplicate maybe_hydrate_thread call (rebase artifact)
- Fix RwLock held across async hook execution in HookRegistry::run()
- Add tracing::warn for silent JSON parse failures in hook modifications
- Refactor execute_tool_inner to accept &WorkerDeps instead of 8 Arc params
- Use real user_id from JobContext instead of job_id UUID in BeforeToolCall hook
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: cargo fmt + remove tracked worktree breaking CI
- Apply rustfmt formatting (method chain line breaks, match arm style)
- Remove .claude/worktrees/ from git tracking (caused submodule error in CI)
- Add .claude/worktrees/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Firat Sertgoz <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add interactive database backend selection during onboarding
Previously the onboarding wizard silently defaulted to PostgreSQL because
libsql wasn't in the default feature set. Now both backends ship by default
and the wizard presents a selection prompt when both are available.
DATABASE_BACKEND env var still bypasses the prompt for headless/CI use.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve libSQL onboarding crash, keychain double-prompt, and setup audit findings
Three bugs fixed:
1. libSQL onboarding crash ("Missing required setting 'database_url'"):
DatabaseConfig::resolve() only checked DATABASE_BACKEND env var, falling
back to Postgres default. Now reads settings.database_backend, plus
settings.libsql_path and settings.libsql_url as fallbacks.
2. OS keychain prompts twice during startup: Config::from_env() and
Config::from_db() both called get_master_key(). Now caches the key in
SECRETS_MASTER_KEY env var after first read so from_db() skips keychain.
3. "Path not found: nearai.session" warning: from_db_map() tried to apply
app-specific DB keys (nearai.session_token) to the Settings struct.
Now skips keys that don't map to known Settings fields. Also fixed
bootstrap migration key mismatch (nearai.session -> nearai.session_token).
Setup module audit fixes (14 findings):
- Replace unreachable!() with proper error in provider match
- Extract setup_api_key_provider() to deduplicate setup_anthropic/setup_openai
- Add SAFETY comments to all unsafe std::env::set_var blocks
- Fix .unwrap() calls with proper error handling
- Remove incorrect #[allow(dead_code)] on used TelegramUpdate::update_id
- Log warnings instead of silently discarding HTTP errors in Telegram binding
- Guard select_many against empty options, fix mask_api_key for non-ASCII
- Update stale doc comment in mod.rs, rename misleading variable
- Add 7 new tests (model fetcher fallbacks, channel discovery, secret gen)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback (set_var safety, parse warnings, db_map efficiency)
1. Replace unsafe set_var keychain caching with OnceLock<String> in
SecretsConfig::resolve(). Eliminates the env var write from main.rs
entirely, using a process-wide OnceLock cache instead.
2. Log tracing::warn when database_backend or llm_backend settings
fail to parse, instead of silently falling back to defaults.
3. Remove O(K*S) get() pre-check in from_db_map(). Instead, let set()
run and match on "Path not found" errors to skip unknown keys,
avoiding full Settings serialization per key.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address critical/high audit findings across WASM sub-crates
- Telegram: remove .unwrap() panic on workspace_read (owner_id check)
- WhatsApp: use configured api_version instead of hardcoded v18.0
- WhatsApp: log config parse errors before falling back to defaults
- Slack: log serialization errors in emit_message and json_response
- Google Docs: safe array access for batch update replies
- Google Sheets: safe array access for add_sheet replies
- Google Calendar: fix doc comment secret name mismatch
- Gmail: avoid unnecessary String allocation in UNREAD check
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second-round PR review feedback
- Validate custom model ID is non-empty (loop until valid input)
- Warn on unknown DATABASE_BACKEND env var before defaulting to Postgres
- Force re-selection when llm_backend contains unknown provider value
- Use ok_or_else for proper String error type in google-sheets
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: harden setup module error handling and secret safety
- Introduce ChannelSetupError typed enum replacing raw String errors
across all channel setup functions (setup_telegram, setup_http,
setup_tunnel, setup_wasm_channel, validate_telegram_token)
- Add From<ChannelSetupError> for SetupError to simplify call sites
- Convert setup_telegram retry from recursion to loop (unbounded stack)
- Stop printing HTTP webhook secret plaintext to terminal
- Use secret_input() for Turso auth token (was visible input())
- Replace dirs::home_dir().unwrap_or_default() with proper error
- Fix UTF-8 panic in model name truncation (byte-index to chars-based)
- Log warning in secret_exists() instead of silently swallowing errors
- Deduplicate generate_webhook_secret() to delegate to shared helper
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: replace unreachable!() with error return in setup wizard
The provider match in step_inference_provider was guarded by
is_known but used unreachable!() as the catch-all. If a new
provider is added to the is_known check without a corresponding
match arm, this would panic at runtime. Return a typed error
instead.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove unsafe set_var, use thread-safe overlay for injected secrets
Address PR #92 review comments:
- Replace all 5 unsafe `std::env::set_var()` calls with safe alternatives
- Add INJECTED_VARS OnceLock<HashMap> overlay in config.rs, checked by
optional_env() before falling back to std::env::var()
- Cache wizard API key in SetupWizard.llm_api_key field instead of env
- Pass explicit key param to fetch_anthropic_models/fetch_openai_models
- Persist env-provided API keys to secrets store during onboarding
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining PR review comments (clippy, TODO, secrets backend ordering)
- Fix empty line after doc comment (clippy: empty_line_after_doc_comments)
- Collapse nested if in optional_env overlay check (clippy: collapsible_if)
- Remove dangling TODO(#XX) placeholder issue ref in channels.rs
- Fix init_secrets_context to respect selected database_backend when both
postgres and libsql features are compiled, preventing wrong-backend
secrets storage when DATABASE_URL is set but libsql was chosen
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address latest PR review comments (SecretString, empty env, docs, embeddings)
- Change wizard llm_api_key from String to SecretString to prevent
accidental logging of API keys
- Fix inject_llm_keys_from_secrets skipping when env var is set but
empty, matching optional_env's treatment of empty as unset
- Fix inverted doc comment on INJECTED_VARS (env checked first, overlay
is the fallback, not the other way around)
- Update stale "env vars" comments in main.rs to reflect overlay pattern
- Fix step_embeddings not seeing cached OpenAI key from wizard session
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: OAuth callback listener binds IPv4 first to match redirect URLs
The listener was binding to [::1] (IPv6) first, but NEAR AI and other
OAuth flows redirect to http://127.0.0.1:9876/... (IPv4 explicit).
On macOS and most systems, [::1] and 127.0.0.1 are separate addresses,
so the browser's connection to 127.0.0.1 was refused when the listener
was on [::1]. Reversed the bind order: try 127.0.0.1 first, fall back
to [::1] if IPv4 is unavailable.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: cache keychain key eagerly to avoid redundant macOS password dialogs
Replace has_master_key() with get_master_key() in step_security() and
immediately build SecretsCrypto from the result. This eliminates redundant
keychain accesses later in init_secrets_context(), each of which triggers
macOS system dialogs (keychain unlock + app authorization).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: persist DATABASE_BACKEND to ~/.ironclaw/.env for libSQL startup
The wizard saved database_backend only to the database, but
Config::from_env() needs it BEFORE connecting to any database (to
decide which backend to use). Without it, the backend defaults to
Postgres and then fails with "Missing required setting database_url".
Now save all database bootstrap vars (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, LIBSQL_URL) to ~/.ironclaw/.env via save_bootstrap_env().
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: status command shows libSQL backend and skips keychain probe
The status command only checked DATABASE_URL (postgres), showing
"not configured" for libSQL users. Now detects the DATABASE_BACKEND
env var and reports libSQL path and Turso sync status.
Also remove the keychain probe from status. get_generic_password()
triggers macOS unlock+authorization dialogs which is terrible UX
for a read-only diagnostic command.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting in bootstrap test
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Move debug log truncation from agent loop to REPL channel
Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Flatten WASM tool schemas and fix host HTTP runtime contention
LLMs can't reliably follow oneOf + const discriminator patterns in JSON
Schema, causing tools like Google Calendar to receive malformed params
(e.g., {"operation":"list_events","data":{"calendarId":"primary"}} instead
of {"action":"list_events","calendar_id":"primary"}). Replace all 9 WASM
tool schemas with flat action enum + top-level properties. The serde
#[serde(tag = "action")] deserialization works identically.
Also fixes WASM host HTTP requests (channels and tools) stalling during
startup by replacing Handle::current().block_on() with a dedicated
single-threaded runtime per request, avoiding I/O driver contention.
Reduces verbose LLM debug logging (full request/response payloads) and
changes tower_http default from debug to warn.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Built-in OAuth credentials and combined Google scopes
Add infrastructure for shipping default OAuth credentials with the binary,
similar to how gcloud/rclone bake in their client_id. Credentials are set
at compile time via IRONCLAW_GOOGLE_CLIENT_ID / IRONCLAW_GOOGLE_CLIENT_SECRET
env vars, or can be hardcoded in src/cli/oauth_defaults.rs.
The fallback chain is: capabilities file > runtime env var > built-in defaults.
Also, when authing any Google tool, scopes from ALL installed Google tools
are now combined into a single OAuth request (they all share the same
google_oauth_token secret). One login covers Gmail, Calendar, Drive, etc.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Ship default Google OAuth credentials for zero-config auth
Google Desktop App credentials are not secret (per Google's own docs).
Hardcode them so `ironclaw tool auth <google-tool>` works out of the box
without requiring users to register their own OAuth app.
Credentials can still be overridden at compile time
(IRONCLAW_GOOGLE_CLIENT_ID) or runtime (GOOGLE_OAUTH_CLIENT_ID).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Consistent OAuth callback port and polished landing page
- Use fixed port 9876 instead of scanning 9876-9886 (one redirect URI
to register in provider OAuth apps, deterministic behavior)
- Replace broken unicode checkmark with SVG icons (charset was missing,
rendered as mojibake)
- Dark themed landing page with proper card layout for both success
and error states
- Add charset=utf-8 to Content-Type headers
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: Unify OAuth callback server across all auth flows
All three OAuth flows (WASM tool auth, MCP server auth, NEAR AI login)
now share the same code from cli::oauth_defaults:
- Fixed port 9876 (one redirect URI to register per provider)
- Shared landing page HTML (dark card with SVG icons, proper charset)
- Parameterized wait_for_callback(listener, path, param, display_name)
Removes ~120 lines of duplicated callback/HTML code.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Support for oauth token refresh
* refactor: Replace bootstrap.json with ~/.ironclaw/.env for DATABASE_URL
Kill the 4-field BootstrapConfig JSON file. Only DATABASE_URL actually
needs disk persistence (chicken-and-egg before DB connect). The other
three fields are now derived: pool_size defaults to 10 via env var,
secrets master key is auto-detected (env then keychain probe), and
onboard_completed is inferred from DATABASE_URL presence.
The new format is a standard .env file loaded via dotenvy early in
main, so DATABASE_URL is available as a regular env var everywhere.
Handles three upgrade paths:
- Clean start: wizard writes .env, reload after wizard completes
- Returning user: .env loaded at startup, business as usual
- Legacy upgrade: bootstrap.json auto-migrated to .env on first run
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review findings
- Fix UTF-8 panic in truncate_for_preview (byte-slice on char boundary)
- Cap WASM guest timeout_ms at 5 minutes to prevent resource exhaustion
- Fix localhost detection in requires_auth() to avoid substring matches
(e.g. "notlocalhost.com" no longer matches)
- Fix query param injection to insert before URL fragment
- Fix extract_host_from_url for IPv6 bracket notation
- Remove misleading schema defaults: Slack limit, Slides insertion_index,
Docs index (per-action defaults documented in descriptions instead)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Fix cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: IPv6 loopback support for OAuth listener and localhost detection
- bind_callback_listener: try [::1] first, fall back to 127.0.0.1,
so OAuth redirects work on systems where localhost resolves to ::1
- is_localhost_url: replace manual string parsing with url::Url for
correct handling of IPv6 brackets, ports, userinfo, etc.
- Add url crate as direct dependency (already a transitive dep)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review feedback on runtime reuse, onboard check, and OAuth binding
- Remove session file check from check_onboard_needed(); DATABASE_URL is sufficient
- Detect AddrInUse on IPv6 bind and fail immediately instead of falling through to IPv4
- Reuse dedicated tokio runtime across HTTP calls in both tool and channel WASM wrappers
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: HTML-escape provider name in OAuth landing page, simplify Slack limit description
- Add html_escape() to prevent XSS in landing_html() where provider_name
was interpolated directly into HTML (defense-in-depth, source is trusted
but escaping costs nothing)
- Remove per-action default numbers from Slack limit field description to
avoid confusing LLMs with conflicting defaults
Addresses review feedback from zmanian on PR #42.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Save all bootstrap fields from wizard, fix config module comment
- Wizard now saves secrets_master_key_source and database_pool_size to
bootstrap.json (was only saving database_url and onboard_completed,
which broke secrets after fresh onboard since SecretsConfig::resolve
reads key source from bootstrap)
- Update config.rs module doc to reflect bootstrap.json priority chain
instead of the removed ~/.ironclaw/.env approach
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: Replace BootstrapConfig with .env-based bootstrap
DATABASE_URL is the only setting that needs disk persistence before
the database is available. Instead of a custom bootstrap.json with 4
fields, use a standard ~/.ironclaw/.env file loaded via dotenvy.
- Remove BootstrapConfig struct entirely
- Restore ironclaw_env_path(), load_ironclaw_env(), save_database_url()
- SecretsConfig::resolve() now auto-detects (env var then keychain probe)
instead of reading a saved source from bootstrap.json
- DatabaseConfig::resolve() reads DATABASE_URL from env only (dotenvy
loads ~/.ironclaw/.env into the environment early in startup)
- check_onboard_needed() is now sync (just checks env vars)
- Wizard save_and_summarize() works for both postgres and libsql backends
- One-time migration from bootstrap.json to .env preserved
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Ensure load_ironclaw_env() runs in all Config paths, fix .env priority
- Config::from_env() and Config::from_db() now call load_ironclaw_env()
internally (after dotenvy::dotenv()), so CLI commands like `memory`
and `config` correctly load DATABASE_URL from ~/.ironclaw/.env
- Fix load order: standard ./.env first (higher priority), then
~/.ironclaw/.env, matching the documented priority chain
- Collapse nested if/if-let into let-chains (clippy::collapsible_if)
in oauth_defaults.rs, tool.rs, and secrets/store.rs
- Fix rename_to_migrated to take &Path instead of &PathBuf
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review comments (quoting, SSRF, error mapping)
- Quote DATABASE_URL in .env writes so `#` in passwords isn't treated
as a dotenv comment (e.g., `DATABASE_URL="postgres://..."`)
- Add SSRF defenses to refresh_oauth_token(): require HTTPS, reject
private/loopback IPs (with DNS resolution), disable redirects.
token_url comes from tool capabilities JSON, so a malicious tool
could otherwise exfiltrate refresh tokens.
- Fix IPv4 bind error mapping: only map AddrInUse to PortInUse,
use generic Io variant for other bind failures
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add libSQL/Turso database backend with full feature parity
Introduce a Database trait abstraction (~60 async methods) enabling
compile-time backend selection between PostgreSQL and libSQL/Turso.
Convert all modules from concrete Store to Arc<dyn Database>, add
LibSqlSecretsStore and LibSqlWasmToolStore implementations, wire
libsql stores throughout CLI and main entry points, and make the
setup wizard backend-agnostic.
Key changes:
- src/db/: Database trait, PostgresDatabase adapter, LibSqlBackend
with native SQLite-dialect SQL, and idempotent migration system
- src/secrets/store.rs: LibSqlSecretsStore (all 8 trait methods)
- src/tools/wasm/storage.rs: LibSqlWasmToolStore (all 7 trait methods)
- src/main.rs, cli/tool.rs, cli/mcp.rs: backend-conditional wiring
- src/setup/channels.rs: SecretsContext uses Arc<dyn SecretsStore>
- Feature-gate postgres-only tests and examples
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: enable onboarding wizard for libSQL builds
Refactor the setup wizard to work with both postgres and libsql feature
flags. Previously the wizard was gated behind #[cfg(feature = "postgres")]
only, so libsql-only builds would print an error on `ironclaw onboard`.
- Add libsql fields to Settings (database_backend, libsql_path, libsql_url)
- Split wizard database/migration/secrets methods into feature-gated variants
- Add step_database_libsql() with local path and Turso remote replica prompts
- Update setup/mod.rs and main.rs feature gates to any(postgres, libsql)
- Extend check_onboard_needed() to detect libsql database presence
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback for libSQL backend
- P0: Switch libsql_backend to connection-per-operation pattern to fix
shared Connection concurrency issue across tokio tasks
- P0: Wrap secrets store INSERT+SELECT in transaction to fix TOCTOU race
- P0: Document encryption-at-rest limitations and json_patch divergence
- P1: Fix get_opt_text removing .filter(|s| !s.is_empty()) that conflated
empty strings with NULL
- P1: Replace datetime('now') with fmt_ts(&Utc::now()) for consistent
RFC 3339 timestamps across all queries
- P2: Use explicit _rowid column in FTS5 triggers and joins for stability
across VACUUM operations
- P2: Add tracing::warn when embedding provided but vector search disabled
in hybrid_search
- Extract shared connect_from_config() helper to deduplicate DB connection
logic across main.rs, cli/config.rs, and cli/mcp.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing JobContext fields and resolve fmt/clippy warnings
Add total_tokens_used and max_tokens fields to JobContext in
libsql_backend.rs, apply cargo fmt, and fix clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: review fixes for libSQL backend (shared connections, panics, indexes)
- Replace .expect() with proper error propagation in 3 call sites
- Share Arc<Database> between backend and stores instead of single Connection
- Add connect-per-operation pattern to LibSqlSecretsStore and LibSqlWasmToolStore
- Wrap store() INSERT + SELECT-back in a transaction
- Add ~22 missing indexes for parity with PostgreSQL schema
- Add 18 leak_detection_patterns seed rows matching PostgreSQL V2 migration
- Fix super:: import to use crate:: style
- Gate mask_password_in_url behind #[cfg(feature = "postgres")]
- Rewrite secrets store init with or_else chain for runtime backend selection
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Resolve clippy lints (collapsible_if, too_many_arguments)
Collapse nested if blocks into let_chains to satisfy clippy's
collapsible_if lint (CI uses -D warnings). Suppress too_many_arguments
on libsql_row_to_tool_at since refactoring the positional index
pattern would be a larger change.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* Bump MSRV to 1.92 and add GCP deployment files
rig-core 0.30 uses let_chains (stabilized post-1.87), which breaks
builds on Rust 1.85. Bump rust-version in Cargo.toml and both
Dockerfiles to 1.92 (verified working).
Add cloud deployment scaffolding:
- Dockerfile: multi-stage build for the main agent container
- deploy/cloud-sql-proxy.service: systemd unit for Cloud SQL Auth Proxy
- deploy/ironclaw.service: systemd unit for the IronClaw container
- deploy/setup.sh: VM bootstrap script (Docker, proxy, services)
- deploy/env.example: reference environment configuration
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Address review feedback: harden deploy scaffolding
- Add comment explaining GATEWAY_HOST=0.0.0.0 and when to use 127.0.0.1
- Document /opt/ironclaw ownership model (root-owned, Docker reads as root)
- Switch cloud-sql-proxy service from User=root to DynamicUser=yes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Resolve clippy lints (Rust 1.93) and fix CI test workflow
- Fix 97 collapsible_if warnings using let-chains syntax (auto-fixed)
- Fix ptr_arg: change &PathBuf to &Path in pairing store functions
- Fix suspicious_open_options: add .truncate(false) to OpenOptions
- Fix too_many_arguments: add clippy allow on execute_status
- Fix unnecessary_unwrap: use if-let in repository.rs hybrid_search
- Gate unused EchoTool with #[cfg(test)]
- Add PairingStore argument to ChannelStoreData::new() test call sites
- Add skip guard for bundled channel test when WASM artifacts unavailable
- Split CI test workflow to exclude PostgreSQL-dependent integration tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address review feedback from ilblackdragon
- Add root check to setup.sh (exits with error if not root)
- Add warning comment to env.example about placeholder passwords
- Dockerfile.worker already uses rust:1.92 (no change needed)
- PR #41 overlap noted; will rebase after #41 merges
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve 47 collapsible_if clippy warnings
Collapse nested if statements across the codebase to satisfy
clippy::collapsible_if on Rust 1.93.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: comprehensive security hardening across all layers
Critical:
- Replace --dangerously-skip-permissions with explicit tool allowlist
via settings.json (Claude Code bridge)
- Constant-time token comparison (subtle crate) in web auth and
orchestrator auth to prevent timing attacks
High:
- Revoke tokens and clean up handles on container creation failure
- Drop SETUID/SETGID capabilities from containers (keep only CHOWN)
- Disable redirect following in HTTP tool and WASM wrapper (SSRF)
- Reject URL userinfo (@) in WASM allowlist parser (host confusion)
- Fix binary body bypassing leak detection (from_utf8 -> from_utf8_lossy)
- Protect identity files from LLM overwrites (prompt injection defense)
- Prevent tool shadowing: built-in tools cannot be replaced dynamically
- User-scoped job APIs: list/detail/cancel/restart/prompt/events/files
- CORS restricted to localhost origins, WebSocket origin validation
- Sandbox shell fail-closed: no silent fallback to unsandboxed execution
- Scrub secrets from log broadcaster before SSE broadcast
- XSS sanitization on rendered markdown in web UI
- WASM epoch ticker thread so timeout deadlines actually fire
Medium:
- Cap state transition history at 200 entries
- SSE/WebSocket connection limit (100 max)
- Request body size limit (1MB)
- Response body size limit enforcement in WASM HTTP
- UTF-8 safe string truncation (routine engine, shell tool)
- Fix PolicyAction::Sanitize to actually run the sanitizer
- TOCTOU fix in scheduler and context manager (hold write lock)
- Project file serving moved behind auth
- Path traversal guard on project_id
- Session file permissions set to 0600 on unix
- AtomicUsize for routine running_count (panic-safe)
- Completion detection hardened against false positives and tool injection
- Tool output no longer drives job completion (only LLM response)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address security review findings across all layers
- Fix path traversal sandbox bypass via lexical normalization (file.rs)
- Fix SSRF via DNS rebinding with pre-request hostname resolution (http.rs)
- Add token budget enforcement on LLM calls (reasoning.rs, state.rs)
- Fix cross-user chat history leak with ownership verification (store.rs, server.rs)
- Add sliding-window rate limiter on gateway chat endpoint (server.rs)
- Harden extension install: HTTPS-only, 50MB cap, WASM magic validation (manager.rs)
- Add destructive command blocklist that overrides shell auto-approval (shell.rs)
- Add 5MB response body size cap to HTTP tool (http.rs)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: deduplicate shared helpers and remove dead code
Extract floor_char_boundary and llm_signals_completion into src/util.rs,
unifying diverging phrase lists from agent/worker.rs and worker/runtime.rs.
Remove dead RespondResult::usage(), duplicate PROTECTED_IDENTITY_FILES
constant, double LeakDetector scanning in WebLogLayer, and invalid
0.0.0.0 origin from WebSocket allow list.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review findings and CI test failures
- Fix record_failed_approve: .truncate(true) wiped the attempts file
before reading, so failed pairing attempts never accumulated and
rate limiting never triggered.
- Guard wizard WASM test: skip gracefully when channel build artifacts
are absent (CI doesn't compile wasm32-wasip2 targets).
- Fix DNS rebinding check: use port 0 instead of hardcoded 443, since
the port is irrelevant for hostname resolution.
- Remove hardcoded CORS port 3001: the dynamic addr.port() entries
already cover the actual server port.
- Require WebSocket Origin header: reject connections that omit it
entirely, since browsers always send Origin for WS upgrades and a
missing header indicates a non-browser client bypassing the check.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second round of PR review findings
- store.rs: reintroduce file locking around read-modify-write in
record_failed_approve (concurrent callers could clobber each other).
- sse.rs: replace load+check+fetch_add with atomic fetch_update in both
subscribe_raw() and subscribe() to prevent overshooting max_connections.
- ws.rs: decrement WS tracker before early return when subscribe_raw()
returns None (connection limit reached), fixing a counter leak.
- server.rs: parse WS Origin host exactly instead of prefix matching,
preventing bypass via crafted origins like http://localhost.evil.com.
- workspace_integration.rs: skip tests gracefully when Postgres is
unreachable instead of panicking (fixes 10 CI failures).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Origin header to WS integration tests
The Origin header requirement added in a3b0190 broke the WS gateway
integration tests. Test clients now send Origin: http://127.0.0.1:{port}
to match the server's localhost validation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Implement DM pairing for channels
- Introduced a new pairing system to manage direct messages from unknown senders.
- Added `PairingStore` to handle pending requests and allowlist management.
- Implemented CLI commands for listing and approving pairing requests.
- Updated Telegram channel to utilize the new pairing logic, including workspace paths for storing pairing data.
- Enhanced WASM channel integration to support pairing functionality.
This feature enhances security by requiring approval for unknown senders before they can interact with the agent.
* Enhance Telegram channel support with media captioning and DM pairing features
- Added support for media captions in Telegram messages, allowing for richer content handling.
- Updated message processing to utilize either text or caption, improving message flexibility.
- Enhanced DM pairing functionality to include approval and listing capabilities for direct messages.
- Updated feature parity documentation to reflect new capabilities and improvements in Telegram integration.
* Update README and BUILDING_CHANNELS documentation for Telegram channel integration
- Enhanced README with instructions for building and running the Telegram channel, including a note on running `./scripts/build-all.sh` for full releases.
- Added detailed steps in BUILDING_CHANNELS.md for building and deploying the Telegram channel, emphasizing the need to run `./channels-src/telegram/build.sh` before building the main crate to ensure updated WASM is included.
- Updated CLI module to expose a new command for pairing with store functionality.
* Implement build script for Telegram channel WASM and enhance pairing error handling
- Added a new `build.rs` script to automate the compilation of the Telegram channel's WASM binary from source, ensuring reproducible builds and emphasizing supply chain security by preventing committed binaries.
- Updated `BUILDING_CHANNELS.md` to reflect the new build process and the importance of not committing compiled binaries.
- Enhanced error handling in the pairing approval process to include rate limiting for failed attempts, improving security and user feedback.
* Remove Telegram channel WASM binary file as part of the build process cleanup, ensuring no committed binaries are present in the repository.
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.
Co-authored-by: Claude Opus 4.6 <[email protected]>