diff --git a/CLAUDE.md b/CLAUDE.md
index 6ae73405..1c5d81eb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,9 +1,17 @@
-# NEAR Agent Development Guide
+# IronClaw Development Guide
## Project Overview
-LLM-powered autonomous agent for the NEAR AI marketplace. Features:
-- **Multi-channel input**: Full TUI (Ratatui), HTTP webhook with secret auth (Slack/Telegram stubs)
+**IronClaw** is a secure personal AI assistant that protects your data and expands its capabilities on the fly.
+
+### Core Philosophy
+- **User-first security** - Your data stays yours, encrypted and local
+- **Self-expanding** - Build new tools dynamically without vendor dependency
+- **Defense in depth** - Multiple security layers against prompt injection and data exfiltration
+- **Always available** - Multi-channel access with proactive background execution
+
+### Features
+- **Multi-channel input**: TUI (Ratatui), HTTP webhooks, Telegram, WhatsApp, Slack (WASM channels)
- **Parallel job execution** with state machine and self-repair for stuck jobs
- **Extensible tools**: Built-in tools, WASM sandbox, MCP client, dynamic builder
- **Persistent memory**: Workspace with hybrid search (FTS + vector via RRF)
@@ -26,7 +34,7 @@ cargo test
cargo test test_name
# Run with logging
-RUST_LOG=near_agent=debug cargo run
+RUST_LOG=ironclaw=debug cargo run
```
## Project Structure
@@ -201,7 +209,7 @@ Pending -> InProgress -> Completed -> Submitted -> Accepted
Environment variables (see `.env.example`):
```bash
-DATABASE_URL=postgres://user:pass@localhost/near_agent
+DATABASE_URL=postgres://user:pass@localhost/ironclaw
# NEAR AI (required)
NEARAI_SESSION_TOKEN=sess_...
@@ -209,7 +217,7 @@ NEARAI_MODEL=claude-3-5-sonnet-20241022
NEARAI_BASE_URL=https://private.near.ai
# Agent settings
-AGENT_NAME=near-agent
+AGENT_NAME=ironclaw
MAX_PARALLEL_JOBS=5
# Embeddings (for semantic memory search)
@@ -328,13 +336,13 @@ Key test patterns:
```bash
# Verbose logging
-RUST_LOG=near_agent=trace cargo run
+RUST_LOG=ironclaw=trace cargo run
# Just the agent module
-RUST_LOG=near_agent::agent=debug cargo run
+RUST_LOG=ironclaw::agent=debug cargo run
# With HTTP request logging
-RUST_LOG=near_agent=debug,tower_http=debug cargo run
+RUST_LOG=ironclaw=debug,tower_http=debug cargo run
```
## Code Style
diff --git a/Cargo.lock b/Cargo.lock
index 3f545e36..d8635375 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,6 +182,137 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.3",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.3",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.3",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -316,6 +447,28 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
[[package]]
name = "bollard"
version = "0.18.1"
@@ -525,6 +678,15 @@ dependencies = [
"rustversion",
]
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
[[package]]
name = "cc"
version = "1.2.55"
@@ -642,6 +804,15 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "constant_time_eq"
version = "0.4.2"
@@ -1133,6 +1304,33 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -1160,6 +1358,27 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@@ -1292,6 +1511,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
[[package]]
name = "futures-macro"
version = "0.3.31"
@@ -1828,6 +2060,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
+ "block-padding",
"generic-array",
]
@@ -1876,6 +2109,65 @@ dependencies = [
"serde",
]
+[[package]]
+name = "ironclaw"
+version = "0.1.0"
+dependencies = [
+ "aes-gcm",
+ "aho-corasick",
+ "anyhow",
+ "async-trait",
+ "axum",
+ "blake3",
+ "bollard",
+ "bytes",
+ "chrono",
+ "clap",
+ "crossterm",
+ "deadpool-postgres",
+ "dirs 6.0.0",
+ "dotenvy",
+ "futures",
+ "hkdf",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "open",
+ "pgvector",
+ "postgres-types",
+ "pretty_assertions",
+ "rand 0.8.5",
+ "ratatui",
+ "refinery",
+ "regex",
+ "reqwest",
+ "rust_decimal",
+ "rust_decimal_macros",
+ "secrecy",
+ "secret-service",
+ "security-framework",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "testcontainers-modules",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-postgres",
+ "tokio-stream",
+ "tokio-test",
+ "tower",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+ "urlencoding",
+ "uuid",
+ "wasmparser 0.220.1",
+ "wasmtime",
+ "wasmtime-wasi",
+ "zbus",
+]
+
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -2109,6 +2401,15 @@ dependencies = [
"rustix 1.1.3",
]
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -2128,59 +2429,16 @@ dependencies = [
]
[[package]]
-name = "near-agent"
-version = "0.1.0"
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
- "aes-gcm",
- "aho-corasick",
- "anyhow",
- "async-trait",
- "axum",
- "blake3",
- "bollard",
- "bytes",
- "chrono",
- "clap",
- "crossterm",
- "deadpool-postgres",
- "dirs 6.0.0",
- "dotenvy",
- "futures",
- "hkdf",
- "http-body-util",
- "hyper",
- "hyper-util",
- "open",
- "pgvector",
- "postgres-types",
- "pretty_assertions",
- "rand 0.8.5",
- "ratatui",
- "refinery",
- "regex",
- "reqwest",
- "rust_decimal",
- "rust_decimal_macros",
- "secrecy",
- "serde",
- "serde_json",
- "sha2",
- "tempfile",
- "testcontainers-modules",
- "thiserror 2.0.18",
- "tokio",
- "tokio-postgres",
- "tokio-stream",
- "tokio-test",
- "tower",
- "tower-http",
- "tracing",
- "tracing-subscriber",
- "urlencoding",
- "uuid",
- "wasmparser 0.220.1",
- "wasmtime",
- "wasmtime-wasi",
+ "bitflags 2.10.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
]
[[package]]
@@ -2192,12 +2450,76 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2279,6 +2601,22 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2386,12 +2724,37 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.3",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "polyval"
version = "0.6.2"
@@ -3172,6 +3535,25 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "secret-service"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "generic-array",
+ "hkdf",
+ "num",
+ "once_cell",
+ "rand 0.8.5",
+ "serde",
+ "sha2",
+ "zbus",
+]
+
[[package]]
name = "security-framework"
version = "3.5.1"
@@ -3322,6 +3704,17 @@ dependencies = [
"syn 2.0.114",
]
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -3760,6 +4153,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
+ "tracing",
"windows-sys 0.61.2",
]
@@ -4065,6 +4459,17 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -5182,6 +5587,16 @@ dependencies = [
"rustix 1.1.3",
]
+[[package]]
+name = "xdg-home"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "yansi"
version = "1.0.1"
@@ -5211,6 +5626,69 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zbus"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "rand 0.8.5",
+ "serde",
+ "serde_repr",
+ "sha1",
+ "static_assertions",
+ "tokio",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.52.0",
+ "xdg-home",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "zvariant",
+]
+
[[package]]
name = "zerocopy"
version = "0.8.37"
@@ -5324,3 +5802,40 @@ dependencies = [
"cc",
"pkg-config",
]
+
+[[package]]
+name = "zvariant"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zvariant_derive",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 8af7ba00..2dc13f28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,9 @@
[package]
-name = "near-agent"
+name = "ironclaw"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
-description = "LLM-powered autonomous agent for the NEAR AI marketplace"
+description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
license = "MIT OR Apache-2.0"
[dependencies]
@@ -98,6 +98,15 @@ hyper-util = { version = "0.1", features = ["server", "tokio", "http1", "http2"]
http-body-util = "0.1"
bytes = "1"
+# macOS keychain
+[target.'cfg(target_os = "macos")'.dependencies]
+security-framework = "3"
+
+# Linux secret-service (GNOME Keyring, KWallet)
+[target.'cfg(target_os = "linux")'.dependencies]
+secret-service = { version = "4", features = ["rt-tokio-crypto-rust"] }
+zbus = "4"
+
[dev-dependencies]
tokio-test = "0.4"
testcontainers-modules = { version = "0.11", features = ["postgres"] }
diff --git a/FEATURE_PARITY.md b/FEATURE_PARITY.md
index 3d44f86f..6f791052 100644
--- a/FEATURE_PARITY.md
+++ b/FEATURE_PARITY.md
@@ -208,7 +208,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Dynamic loading | ✅ | ✅ | WASM modules |
| Manifest validation | ✅ | ✅ | WASM metadata |
| HTTP path registration | ✅ | ❌ | Plugin routes |
-| Workspace-relative install | ✅ | ✅ | ~/.near-agent/tools/ |
+| Workspace-relative install | ✅ | ✅ | ~/.ironclaw/tools/ |
| Channel plugins | ✅ | ✅ | WASM channels |
| Auth plugins | ✅ | ❌ | |
| Memory plugins | ✅ | ❌ | Custom backends |
@@ -233,7 +233,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O
| Config validation/schema | ✅ | ✅ | Type-safe Config struct |
| Hot-reload | ✅ | ❌ | |
| Legacy migration | ✅ | ➖ | |
-| State directory | ✅ `~/.openclaw-state/` | ✅ `~/.near-agent/` | |
+| State directory | ✅ `~/.openclaw-state/` | ✅ `~/.ironclaw/` | |
| Credentials directory | ✅ | ✅ | Session files |
### Owner: _Unassigned_
diff --git a/README.md b/README.md
index 675b051a..d815c833 100644
--- a/README.md
+++ b/README.md
@@ -5,58 +5,60 @@
IronClaw
- LLM-powered autonomous agent for the NEAR AI marketplace
+ Your secure personal AI assistant, always on your side
+ Philosophy •
Features •
- Parity •
Installation •
Configuration •
- Architecture •
- Security
+ Security •
+ Architecture
---
+## Philosophy
+
+IronClaw is built on a simple principle: **your AI assistant should work for you, not against you**.
+
+In a world where AI systems are increasingly opaque about data handling and aligned with corporate interests, IronClaw takes a different approach:
+
+- **Your data stays yours** - All information is stored locally, encrypted, and never leaves your control
+- **Transparency by design** - Open source, auditable, no hidden telemetry or data harvesting
+- **Self-expanding capabilities** - Build new tools on the fly without waiting for vendor updates
+- **Defense in depth** - Multiple security layers protect against prompt injection and data exfiltration
+
+IronClaw is the AI assistant you can actually trust with your personal and professional life.
+
## Features
-- **Multi-channel input** - CLI, HTTP webhooks, Slack, Telegram
-- **Parallel job execution** - Concurrent task processing with isolated contexts
-- **Extensible tools** - Built-in tools + MCP protocol + WASM sandbox
-- **Persistent memory** - Hybrid search (FTS + vector) with chunked documents
-- **Prompt injection defense** - Pattern detection, content sanitization, policy enforcement
-- **Self-repair** - Automatic detection and recovery of stuck jobs
-- **Heartbeat system** - Proactive periodic execution for background tasks
+### Security First
-## OpenClaw Feature Parity
+- **WASM Sandbox** - Untrusted tools run in isolated WebAssembly containers with capability-based permissions
+- **Credential Protection** - Secrets are never exposed to tools; injected at the host boundary with leak detection
+- **Prompt Injection Defense** - Pattern detection, content sanitization, and policy enforcement
+- **Endpoint Allowlisting** - HTTP requests only to explicitly approved hosts and paths
-IronClaw is a Rust reimplementation inspired by [OpenClaw](https://github.com/openclaw/openclaw). See [FEATURE_PARITY.md](FEATURE_PARITY.md) for the complete tracking matrix.
+### Always Available
-### Status Summary
+- **Multi-channel** - Reach your assistant via CLI, Telegram, WhatsApp, Slack, or HTTP webhooks
+- **Heartbeat System** - Proactive background execution for monitoring and maintenance tasks
+- **Parallel Jobs** - Handle multiple requests concurrently with isolated contexts
+- **Self-repair** - Automatic detection and recovery of stuck operations
-| Category | Status | Notes |
-|----------|--------|-------|
-| **Core Agent** | ✅ Complete | Sessions, workers, routing, context compaction |
-| **Channels** | 🚧 Partial | TUI, HTTP, REPL, WASM channels done; messaging platforms pending |
-| **Tools** | ✅ Complete | Built-in, MCP, WASM sandbox, dynamic builder |
-| **Memory** | ✅ Complete | Hybrid search, embeddings, workspace filesystem |
-| **Security** | ✅ Complete | WASM sandbox, prompt injection, leak detection |
-| **Automation** | 🚧 Partial | Heartbeat done; cron, hooks pending |
-| **Gateway** | ❌ Pending | WebSocket control plane, service management |
-| **Web UI** | ❌ Pending | Control dashboard, WebChat |
-| **Mobile/Desktop** | 🚫 Out of scope | Focus on server-side initially |
+### Self-Expanding
-### Key Differences from OpenClaw
+- **Dynamic Tool Building** - Describe what you need, and IronClaw builds it as a WASM tool
+- **MCP Protocol** - Connect to Model Context Protocol servers for additional capabilities
+- **Plugin Architecture** - Drop in new WASM tools and channels without restarting
-- **Rust vs TypeScript** - Native performance, single binary
-- **WASM sandbox vs Docker** - Lightweight, capability-based security
-- **PostgreSQL vs SQLite** - Production-ready persistence
-- **NEAR AI primary** - Session-based auth with model proxy
+### Persistent Memory
-### Contributing
-
-Pick an unassigned feature area in [FEATURE_PARITY.md](FEATURE_PARITY.md) and claim it.
+- **Hybrid Search** - Full-text + vector search using Reciprocal Rank Fusion
+- **Workspace Filesystem** - Flexible path-based storage for notes, logs, and context
+- **Identity Files** - Maintain consistent personality and preferences across sessions
## Installation
@@ -64,14 +66,14 @@ Pick an unassigned feature area in [FEATURE_PARITY.md](FEATURE_PARITY.md) and cl
- Rust 1.85+
- PostgreSQL 15+ with pgvector extension
-- NEAR AI session token
+- NEAR AI session token (or other LLM provider)
### Build
```bash
# Clone the repository
-git clone https://github.com/nearai/near-agent.git
-cd near-agent
+git clone https://github.com/nearai/ironclaw.git
+cd ironclaw
# Build
cargo build --release
@@ -84,10 +86,10 @@ cargo test
```bash
# Create database
-createdb near_agent
+createdb ironclaw
# Enable pgvector
-psql near_agent -c "CREATE EXTENSION IF NOT EXISTS vector;"
+psql ironclaw -c "CREATE EXTENSION IF NOT EXISTS vector;"
# Run migrations
refinery migrate -c refinery.toml
@@ -99,12 +101,13 @@ Copy `.env.example` to `.env` and configure:
```bash
# Required
-DATABASE_URL=postgres://user:pass@localhost/near_agent
+DATABASE_URL=postgres://user:pass@localhost/ironclaw
NEARAI_SESSION_TOKEN=sess_...
# Optional: Enable channels
-SLACK_BOT_TOKEN=xoxb-...
TELEGRAM_BOT_TOKEN=...
+WHATSAPP_ACCESS_TOKEN=...
+SLACK_BOT_TOKEN=xoxb-...
HTTP_PORT=8080
```
@@ -118,15 +121,51 @@ HTTP_PORT=8080
| `AGENT_MAX_PARALLEL_JOBS` | Max concurrent jobs (default: 5) | No |
| `SECRETS_MASTER_KEY` | 32+ byte key for secret encryption | For secrets |
+## Security
+
+IronClaw implements defense in depth to protect your data and prevent misuse.
+
+### WASM Sandbox
+
+All untrusted tools run in isolated WebAssembly containers:
+
+- **Capability-based permissions** - Explicit opt-in for HTTP, secrets, tool invocation
+- **Endpoint allowlisting** - HTTP requests only to approved hosts/paths
+- **Credential injection** - Secrets injected at host boundary, never exposed to WASM code
+- **Leak detection** - Scans requests and responses for secret exfiltration attempts
+- **Rate limiting** - Per-tool request limits to prevent abuse
+- **Resource limits** - Memory, CPU, and execution time constraints
+
+```
+WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Execute ──► Leak Scan ──► WASM
+ Validator (request) Injector Request (response)
+```
+
+### Prompt Injection Defense
+
+External content passes through multiple security layers:
+
+- Pattern-based detection of injection attempts
+- Content sanitization and escaping
+- Policy rules with severity levels (Block/Warn/Review/Sanitize)
+- Tool output wrapping for safe LLM context injection
+
+### Data Protection
+
+- All data stored locally in your PostgreSQL database
+- Secrets encrypted with AES-256-GCM
+- No telemetry, analytics, or data sharing
+- Full audit log of all tool executions
+
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Channels │
-│ ┌─────┐ ┌──────┐ ┌───────┐ ┌──────────┐ │
-│ │ CLI │ │ HTTP │ │ Slack │ │ Telegram │ │
-│ └──┬──┘ └──┬───┘ └───┬───┘ └────┬─────┘ │
-│ └────────┴──────────┴───────────┘ │
+│ ┌─────┐ ┌──────────┐ ┌──────────┐ ┌───────┐ │
+│ │ CLI │ │ Telegram │ │ WhatsApp │ │ Slack │ │
+│ └──┬──┘ └────┬─────┘ └────┬─────┘ └───┬───┘ │
+│ └──────────┴─────────────┴────────────┘ │
│ │ │
│ ┌────▼────┐ │
│ │ Router │ Intent classification │
@@ -165,31 +204,6 @@ HTTP_PORT=8080
| **Workspace** | Persistent memory with hybrid search |
| **Safety Layer** | Prompt injection defense and content sanitization |
-## Security
-
-### WASM Sandbox
-
-Untrusted tools run in a sandboxed WASM environment with:
-
-- **Capability-based permissions** - Explicit opt-in for HTTP, secrets, tool invocation
-- **Endpoint allowlisting** - HTTP requests only to approved hosts/paths
-- **Credential injection** - Secrets injected at host boundary, never exposed to WASM
-- **Leak detection** - Scans requests and responses for secret exfiltration
-- **Rate limiting** - Per-tool request limits (per-minute and per-hour)
-- **Resource limits** - Memory, CPU, and execution time constraints
-
-```
-WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Execute ──► Leak Scan ──► WASM
- Validator (request) Injector Request (response)
-```
-
-### Prompt Injection Defense
-
-- Pattern-based detection of injection attempts
-- Content sanitization and escaping
-- Policy rules with severity levels (Block/Warn/Review/Sanitize)
-- Tool output wrapping for LLM context
-
## Usage
### CLI Mode
@@ -199,7 +213,7 @@ WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Exec
cargo run
# With debug logging
-RUST_LOG=near_agent=debug cargo run
+RUST_LOG=ironclaw=debug cargo run
```
### HTTP Server
@@ -211,7 +225,7 @@ HTTP_PORT=8080 cargo run
# Send a request
curl -X POST http://localhost:8080/webhook \
-H "Content-Type: application/json" \
- -d '{"message": "Hello, agent!"}'
+ -d '{"message": "Hello, IronClaw!"}'
```
## Development
@@ -230,6 +244,17 @@ cargo test
cargo test test_name
```
+## OpenClaw Heritage
+
+IronClaw is a Rust reimplementation inspired by [OpenClaw](https://github.com/openclaw/openclaw). See [FEATURE_PARITY.md](FEATURE_PARITY.md) for the complete tracking matrix.
+
+Key differences:
+
+- **Rust vs TypeScript** - Native performance, memory safety, single binary
+- **WASM sandbox vs Docker** - Lightweight, capability-based security
+- **PostgreSQL vs SQLite** - Production-ready persistence
+- **Security-first design** - Multiple defense layers, credential protection
+
## License
Licensed under either of:
diff --git a/channels-src/slack/Cargo.toml b/channels-src/slack/Cargo.toml
index c434519e..18d2fd39 100644
--- a/channels-src/slack/Cargo.toml
+++ b/channels-src/slack/Cargo.toml
@@ -2,7 +2,7 @@
name = "slack-channel"
version = "0.1.0"
edition = "2021"
-description = "Slack Events API channel for NEAR Agent"
+description = "Slack Events API channel for IronClaw"
license = "MIT OR Apache-2.0"
[lib]
diff --git a/channels-src/slack/src/lib.rs b/channels-src/slack/src/lib.rs
index 18748833..adb2c5aa 100644
--- a/channels-src/slack/src/lib.rs
+++ b/channels-src/slack/src/lib.rs
@@ -1,4 +1,4 @@
-//! Slack Events API channel for NEAR Agent.
+//! Slack Events API channel for IronClaw.
//!
//! This WASM component implements the channel interface for handling Slack
//! webhooks and sending messages back to Slack.
diff --git a/channels-src/telegram/Cargo.toml b/channels-src/telegram/Cargo.toml
index d33266e5..855aa8fa 100644
--- a/channels-src/telegram/Cargo.toml
+++ b/channels-src/telegram/Cargo.toml
@@ -2,7 +2,7 @@
name = "telegram-channel"
version = "0.1.0"
edition = "2021"
-description = "Telegram Bot API channel for NEAR Agent"
+description = "Telegram Bot API channel for IronClaw"
license = "MIT OR Apache-2.0"
[lib]
diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs
index 8b5c7517..146b1e11 100644
--- a/channels-src/telegram/src/lib.rs
+++ b/channels-src/telegram/src/lib.rs
@@ -1,7 +1,7 @@
// Telegram API types have fields reserved for future use (entities, reply threading, etc.)
#![allow(dead_code)]
-//! Telegram Bot API channel for NEAR Agent.
+//! Telegram Bot API channel for IronClaw.
//!
//! This WASM component implements the channel interface for handling Telegram
//! webhooks and sending messages back via the Bot API.
diff --git a/channels/whatsapp/Cargo.toml b/channels/whatsapp/Cargo.toml
index 6e91d1f3..8dd03499 100644
--- a/channels/whatsapp/Cargo.toml
+++ b/channels/whatsapp/Cargo.toml
@@ -2,7 +2,7 @@
name = "whatsapp-channel"
version = "0.1.0"
edition = "2021"
-description = "WhatsApp channel for near-agent using the Cloud API"
+description = "WhatsApp Cloud API channel for IronClaw"
[lib]
crate-type = ["cdylib"]
diff --git a/channels/whatsapp/src/lib.rs b/channels/whatsapp/src/lib.rs
index 6633ba4a..346c0f23 100644
--- a/channels/whatsapp/src/lib.rs
+++ b/channels/whatsapp/src/lib.rs
@@ -1,7 +1,7 @@
// WhatsApp API types have fields reserved for future use (contacts, statuses, etc.)
#![allow(dead_code)]
-//! WhatsApp Cloud API channel for NEAR Agent.
+//! WhatsApp Cloud API channel for IronClaw.
//!
//! This WASM component implements the channel interface for handling WhatsApp
//! webhooks and sending messages back via the Cloud API.
diff --git a/docs/BUILDING_CHANNELS.md b/docs/BUILDING_CHANNELS.md
index 7032bda3..a819bc01 100644
--- a/docs/BUILDING_CHANNELS.md
+++ b/docs/BUILDING_CHANNELS.md
@@ -1,6 +1,6 @@
# Building WASM Channels
-This guide covers how to build WASM channel modules for the NEAR Agent.
+This guide covers how to build WASM channel modules for IronClaw.
## Overview
@@ -19,7 +19,7 @@ channels/ # Or channels-src/
After building, deploy to:
```
-~/.near-agent/channels/
+~/.ironclaw/channels/
├── my-channel.wasm
└── my-channel.capabilities.json
```
@@ -31,7 +31,7 @@ After building, deploy to:
name = "my-channel"
version = "0.1.0"
edition = "2021"
-description = "My messaging platform channel for NEAR Agent"
+description = "My messaging platform channel for IronClaw"
[lib]
crate-type = ["cdylib"]
@@ -251,9 +251,9 @@ Create `my-channel.capabilities.json`:
cd channels/my-channel
cargo component build --release
-# Deploy to ~/.near-agent/channels/
-cp target/wasm32-wasip1/release/my_channel.wasm ~/.near-agent/channels/my-channel.wasm
-cp my-channel.capabilities.json ~/.near-agent/channels/
+# Deploy to ~/.ironclaw/channels/
+cp target/wasm32-wasip1/release/my_channel.wasm ~/.ironclaw/channels/my-channel.wasm
+cp my-channel.capabilities.json ~/.ironclaw/channels/
```
## Host Functions Available
diff --git a/examples/wasm-tools/slack/Cargo.toml b/examples/wasm-tools/slack/Cargo.toml
index 92e34820..83425bc8 100644
--- a/examples/wasm-tools/slack/Cargo.toml
+++ b/examples/wasm-tools/slack/Cargo.toml
@@ -2,7 +2,7 @@
name = "slack-tool"
version = "0.1.0"
edition = "2021"
-description = "Slack integration tool for NEAR Agent (WASM component)"
+description = "Slack integration tool for IronClaw (WASM component)"
license = "MIT OR Apache-2.0"
publish = false
diff --git a/examples/wasm-tools/slack/README.md b/examples/wasm-tools/slack/README.md
index 2836643a..aee92f18 100644
--- a/examples/wasm-tools/slack/README.md
+++ b/examples/wasm-tools/slack/README.md
@@ -1,6 +1,6 @@
# Slack WASM Tool
-A standalone WASM component that provides Slack integration for NEAR Agent. This serves as both a functional tool and a template for building custom WASM tools.
+A standalone WASM component that provides Slack integration for IronClaw. This serves as both a functional tool and a template for building custom WASM tools.
## Features
@@ -50,9 +50,9 @@ target/wasm32-wasip2/release/slack_tool.wasm
Copy the WASM and capabilities files to the agent's tools directory:
```bash
-mkdir -p ~/.near-agent/tools
-cp target/wasm32-wasip2/release/slack_tool.wasm ~/.near-agent/tools/slack.wasm
-cp slack.capabilities.json ~/.near-agent/tools/
+mkdir -p ~/.ironclaw/tools
+cp target/wasm32-wasip2/release/slack_tool.wasm ~/.ironclaw/tools/slack.wasm
+cp slack.capabilities.json ~/.ironclaw/tools/
```
### Option B: Database Storage (Production)
@@ -60,7 +60,7 @@ cp slack.capabilities.json ~/.near-agent/tools/
Use the agent CLI or API to store the tool:
```bash
-near-agent tool install \
+ironclaw tool install \
--name slack \
--wasm target/wasm32-wasip2/release/slack_tool.wasm \
--capabilities slack.capabilities.json
@@ -71,7 +71,7 @@ near-agent tool install \
Store your Slack bot token as a secret:
```bash
-near-agent secret set slack_bot_token "xoxb-your-token-here"
+ironclaw secret set slack_bot_token "xoxb-your-token-here"
```
Or via SQL:
@@ -88,7 +88,7 @@ VALUES ('your_user_id', 'slack_bot_token', ...);
{
"action": "send_message",
"channel": "#general",
- "text": "Hello from the NEAR Agent!"
+ "text": "Hello from IronClaw!"
}
```
@@ -214,7 +214,7 @@ world sandboxed-tool {
Ensure you've stored the secret:
```bash
-near-agent secret set slack_bot_token "xoxb-..."
+ironclaw secret set slack_bot_token "xoxb-..."
```
### "Endpoint not in allowlist"
diff --git a/examples/wasm-tools/slack/src/lib.rs b/examples/wasm-tools/slack/src/lib.rs
index 432ba31f..04cc2b70 100644
--- a/examples/wasm-tools/slack/src/lib.rs
+++ b/examples/wasm-tools/slack/src/lib.rs
@@ -1,4 +1,4 @@
-//! Slack WASM Tool for NEAR Agent.
+//! Slack WASM Tool for IronClaw.
//!
//! This is a standalone WASM component that provides Slack integration.
//! It demonstrates how to build external tools that can be dynamically
diff --git a/src/channels/cli/app.rs b/src/channels/cli/app.rs
index a0e76113..8bae51bb 100644
--- a/src/channels/cli/app.rs
+++ b/src/channels/cli/app.rs
@@ -147,7 +147,7 @@ impl AppState {
Self {
mode: InputMode::Editing,
messages: vec![ChatMessage::system(
- "Welcome to NEAR Agent. Type a message or /help for commands.",
+ "Welcome to IronClaw. Type a message or /help for commands.",
)],
composer: ChatComposer::new(),
approval: None,
diff --git a/src/channels/repl.rs b/src/channels/repl.rs
index c9c3f452..1046460e 100644
--- a/src/channels/repl.rs
+++ b/src/channels/repl.rs
@@ -64,7 +64,7 @@ impl Default for ReplChannel {
fn print_help() {
println!(
r#"
-NEAR Agent REPL - Interactive debugging mode
+IronClaw REPL - Interactive debugging mode
Commands:
/help Show this help message
@@ -116,7 +116,7 @@ impl Channel for ReplChannel {
let stdin = io::stdin();
let mut stdout = io::stdout();
- println!("NEAR Agent REPL - Type /help for commands, /quit to exit");
+ println!("IronClaw REPL - Type /help for commands, /quit to exit");
println!();
loop {
diff --git a/src/channels/wasm/loader.rs b/src/channels/wasm/loader.rs
index 5e94d38c..6ab145f2 100644
--- a/src/channels/wasm/loader.rs
+++ b/src/channels/wasm/loader.rs
@@ -1,6 +1,6 @@
//! WASM channel loader for loading channels from files or directories.
//!
-//! Loads WASM channel modules from the filesystem (default: ~/.near-agent/channels/).
+//! Loads WASM channel modules from the filesystem (default: ~/.ironclaw/channels/).
//! Each channel consists of:
//! - `.wasm` - The compiled WASM component
//! - `.capabilities.json` - Channel capabilities and configuration
@@ -329,12 +329,12 @@ pub struct DiscoveredChannel {
/// Get the default channels directory path.
///
-/// Returns ~/.near-agent/channels/
+/// Returns ~/.ironclaw/channels/
#[allow(dead_code)]
pub fn default_channels_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
- .join(".near-agent")
+ .join(".ironclaw")
.join("channels")
}
diff --git a/src/channels/wasm/mod.rs b/src/channels/wasm/mod.rs
index 24905fc0..7535d033 100644
--- a/src/channels/wasm/mod.rs
+++ b/src/channels/wasm/mod.rs
@@ -63,14 +63,14 @@
//! # Example Usage
//!
//! ```ignore
-//! use near_agent::channels::wasm::{WasmChannelLoader, WasmChannelRuntime};
+//! use ironclaw::channels::wasm::{WasmChannelLoader, WasmChannelRuntime};
//!
//! // Create runtime (can share engine with tool runtime)
//! let runtime = WasmChannelRuntime::new(config)?;
//!
//! // Load channels from directory
//! let loader = WasmChannelLoader::new(runtime);
-//! let channels = loader.load_from_dir(Path::new("~/.near-agent/channels/")).await?;
+//! let channels = loader.load_from_dir(Path::new("~/.ironclaw/channels/")).await?;
//!
//! // Add to channel manager
//! for channel in channels {
diff --git a/src/cli/config.rs b/src/cli/config.rs
new file mode 100644
index 00000000..080cccf1
--- /dev/null
+++ b/src/cli/config.rs
@@ -0,0 +1,195 @@
+//! Configuration management CLI commands.
+//!
+//! Commands for viewing and modifying settings.
+
+use clap::Subcommand;
+
+use crate::settings::Settings;
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum ConfigCommand {
+ /// List all settings and their current values
+ List {
+ /// Show only settings matching this prefix (e.g., "agent", "heartbeat")
+ #[arg(short, long)]
+ filter: Option,
+ },
+
+ /// Get a specific setting value
+ Get {
+ /// Setting path (e.g., "agent.max_parallel_jobs")
+ path: String,
+ },
+
+ /// Set a setting value
+ Set {
+ /// Setting path (e.g., "agent.max_parallel_jobs")
+ path: String,
+
+ /// Value to set
+ value: String,
+ },
+
+ /// Reset a setting to its default value
+ Reset {
+ /// Setting path (e.g., "agent.max_parallel_jobs")
+ path: String,
+ },
+
+ /// Show the settings file path
+ Path,
+}
+
+/// Run a config command.
+pub fn run_config_command(cmd: ConfigCommand) -> anyhow::Result<()> {
+ match cmd {
+ ConfigCommand::List { filter } => list_settings(filter),
+ ConfigCommand::Get { path } => get_setting(&path),
+ ConfigCommand::Set { path, value } => set_setting(&path, &value),
+ ConfigCommand::Reset { path } => reset_setting(&path),
+ ConfigCommand::Path => show_path(),
+ }
+}
+
+/// List all settings.
+fn list_settings(filter: Option) -> anyhow::Result<()> {
+ let settings = Settings::load();
+ let all = settings.list();
+
+ // Find the longest key for alignment
+ let max_key_len = all.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
+
+ println!("Settings:");
+ println!();
+
+ for (key, value) in all {
+ // Skip if filter is set and doesn't match
+ if let Some(ref f) = filter {
+ if !key.starts_with(f) {
+ continue;
+ }
+ }
+
+ // Truncate long values for display
+ let display_value = if value.len() > 60 {
+ format!("{}...", &value[..57])
+ } else {
+ value
+ };
+
+ println!(" {:width$} {}", key, display_value, width = max_key_len);
+ }
+
+ Ok(())
+}
+
+/// Get a specific setting.
+fn get_setting(path: &str) -> anyhow::Result<()> {
+ let settings = Settings::load();
+
+ match settings.get(path) {
+ Some(value) => {
+ println!("{}", value);
+ Ok(())
+ }
+ None => {
+ anyhow::bail!("Setting not found: {}", path);
+ }
+ }
+}
+
+/// Set a setting value.
+fn set_setting(path: &str, value: &str) -> anyhow::Result<()> {
+ let mut settings = Settings::load();
+
+ // Try to set the value
+ settings
+ .set(path, value)
+ .map_err(|e| anyhow::anyhow!("{}", e))?;
+
+ // Save to disk
+ settings.save()?;
+
+ println!("Set {} = {}", path, value);
+ Ok(())
+}
+
+/// Reset a setting to default.
+fn reset_setting(path: &str) -> anyhow::Result<()> {
+ let mut settings = Settings::load();
+
+ // Get the default value for display
+ let default = Settings::default();
+ let default_value = default
+ .get(path)
+ .ok_or_else(|| anyhow::anyhow!("Unknown setting: {}", path))?;
+
+ // Reset it
+ settings.reset(path).map_err(|e| anyhow::anyhow!("{}", e))?;
+
+ // Save to disk
+ settings.save()?;
+
+ println!("Reset {} to default: {}", path, default_value);
+ Ok(())
+}
+
+/// Show the settings file path.
+fn show_path() -> anyhow::Result<()> {
+ let path = Settings::default_path();
+ println!("{}", path.display());
+
+ if path.exists() {
+ let metadata = std::fs::metadata(&path)?;
+ println!(" Size: {} bytes", metadata.len());
+ if let Ok(modified) = metadata.modified() {
+ use std::time::SystemTime;
+ let duration = SystemTime::now()
+ .duration_since(modified)
+ .unwrap_or_default();
+ let secs = duration.as_secs();
+ if secs < 60 {
+ println!(" Modified: {} seconds ago", secs);
+ } else if secs < 3600 {
+ println!(" Modified: {} minutes ago", secs / 60);
+ } else if secs < 86400 {
+ println!(" Modified: {} hours ago", secs / 3600);
+ } else {
+ println!(" Modified: {} days ago", secs / 86400);
+ }
+ }
+ } else {
+ println!(" (does not exist, using defaults)");
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::tempdir;
+
+ #[test]
+ fn test_list_settings() {
+ // Just verify it doesn't panic
+ let settings = Settings::default();
+ let list = settings.list();
+ assert!(!list.is_empty());
+ }
+
+ #[test]
+ fn test_get_set_reset() {
+ let _dir = tempdir().unwrap();
+
+ let mut settings = Settings::default();
+
+ // Set a value
+ settings.set("agent.name", "testbot").unwrap();
+ assert_eq!(settings.agent.name, "testbot");
+
+ // Reset to default
+ settings.reset("agent.name").unwrap();
+ assert_eq!(settings.agent.name, "ironclaw");
+ }
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index adc0a009..631a4770 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -3,18 +3,22 @@
//! Provides subcommands for:
//! - Running the agent (`run`)
//! - Interactive setup wizard (`setup`)
+//! - Managing configuration (`config list`, `config get`, `config set`)
//! - Managing WASM tools (`tool install`, `tool list`, `tool remove`)
-//! - Managing secrets (`secret set`, `secret list`, `secret remove`)
+mod config;
mod tool;
+pub use config::{ConfigCommand, run_config_command};
pub use tool::{ToolCommand, run_tool_command};
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
-#[command(name = "near-agent")]
-#[command(about = "LLM-powered autonomous agent for the NEAR AI marketplace")]
+#[command(name = "ironclaw")]
+#[command(
+ about = "Secure personal AI assistant that protects your data and expands its capabilities"
+)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
@@ -61,12 +65,13 @@ pub enum Command {
channels_only: bool,
},
+ /// Manage configuration settings
+ #[command(subcommand)]
+ Config(ConfigCommand),
+
/// Manage WASM tools
#[command(subcommand)]
Tool(ToolCommand),
- // Future: Secret management
- // #[command(subcommand)]
- // Secret(SecretCommand),
}
impl Cli {
diff --git a/src/cli/tool.rs b/src/cli/tool.rs
index bee90001..1fa2df89 100644
--- a/src/cli/tool.rs
+++ b/src/cli/tool.rs
@@ -13,8 +13,8 @@ use crate::tools::wasm::{CapabilitiesFile, compute_binary_hash};
/// Default tools directory.
fn default_tools_dir() -> PathBuf {
dirs::home_dir()
- .map(|h| h.join(".near-agent").join("tools"))
- .unwrap_or_else(|| PathBuf::from(".near-agent/tools"))
+ .map(|h| h.join(".ironclaw").join("tools"))
+ .unwrap_or_else(|| PathBuf::from(".ironclaw/tools"))
}
#[derive(Subcommand, Debug, Clone)]
@@ -32,7 +32,7 @@ pub enum ToolCommand {
#[arg(long)]
capabilities: Option,
- /// Target directory for installation (default: ~/.near-agent/tools/)
+ /// Target directory for installation (default: ~/.ironclaw/tools/)
#[arg(short, long)]
target: Option,
@@ -51,7 +51,7 @@ pub enum ToolCommand {
/// List installed tools
List {
- /// Directory to list tools from (default: ~/.near-agent/tools/)
+ /// Directory to list tools from (default: ~/.ironclaw/tools/)
#[arg(short, long)]
dir: Option,
@@ -65,7 +65,7 @@ pub enum ToolCommand {
/// Name of the tool to remove
name: String,
- /// Directory to remove tool from (default: ~/.near-agent/tools/)
+ /// Directory to remove tool from (default: ~/.ironclaw/tools/)
#[arg(short, long)]
dir: Option,
},
@@ -75,7 +75,7 @@ pub enum ToolCommand {
/// Name of the tool or path to .wasm file
name_or_path: String,
- /// Directory to look for tool (default: ~/.near-agent/tools/)
+ /// Directory to look for tool (default: ~/.ironclaw/tools/)
#[arg(short, long)]
dir: Option,
},
@@ -420,7 +420,7 @@ async fn list_tools(dir: Option, verbose: bool) -> anyhow::Result<()> {
if !tools_dir.exists() {
println!("No tools directory found at {}", tools_dir.display());
- println!("Install a tool with: near-agent tool install ");
+ println!("Install a tool with: ironclaw tool install ");
return Ok(());
}
@@ -674,7 +674,7 @@ mod tests {
#[test]
fn test_default_tools_dir() {
let dir = default_tools_dir();
- assert!(dir.to_string_lossy().contains(".near-agent"));
+ assert!(dir.to_string_lossy().contains(".ironclaw"));
assert!(dir.to_string_lossy().contains("tools"));
}
}
diff --git a/src/config.rs b/src/config.rs
index ee5d6668..0ead0078 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-//! Configuration for the NEAR Agent.
+//! Configuration for IronClaw.
use std::path::PathBuf;
use std::time::Duration;
@@ -131,16 +131,30 @@ pub struct DatabaseConfig {
impl DatabaseConfig {
fn from_env() -> Result {
+ let settings = crate::settings::Settings::load();
+
+ // Priority: env var > settings > error (required)
+ let url = optional_env("DATABASE_URL")?
+ .or(settings.database_url.clone())
+ .ok_or_else(|| ConfigError::MissingRequired {
+ key: "database_url".to_string(),
+ hint: "Run 'ironclaw setup' or set DATABASE_URL environment variable".to_string(),
+ })?;
+
+ // Priority: env var > settings > default
+ let pool_size = optional_env("DATABASE_POOL_SIZE")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "DATABASE_POOL_SIZE".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .or(settings.database_pool_size)
+ .unwrap_or(10);
+
Ok(Self {
- url: SecretString::from(required_env("DATABASE_URL")?),
- pool_size: optional_env("DATABASE_POOL_SIZE")?
- .map(|s| s.parse())
- .transpose()
- .map_err(|e| ConfigError::InvalidValue {
- key: "DATABASE_POOL_SIZE".to_string(),
- message: format!("must be a positive integer: {e}"),
- })?
- .unwrap_or(10),
+ url: SecretString::from(url),
+ pool_size,
})
}
@@ -192,7 +206,7 @@ pub struct NearAiConfig {
pub base_url: String,
/// Base URL for auth/refresh endpoints (default: https://private.near.ai)
pub auth_base_url: String,
- /// Path to session file (default: ~/.near-agent/session.json)
+ /// Path to session file (default: ~/.ironclaw/session.json)
pub session_path: PathBuf,
/// API mode: "responses" (chat-api) or "chat_completions" (cloud-api)
pub api_mode: NearAiApiMode,
@@ -269,10 +283,17 @@ impl Default for EmbeddingsConfig {
impl EmbeddingsConfig {
fn from_env() -> Result {
+ let settings = crate::settings::Settings::load();
let openai_api_key = optional_env("OPENAI_API_KEY")?.map(SecretString::from);
- let provider = optional_env("EMBEDDING_PROVIDER")?.unwrap_or_else(|| "openai".to_string());
- // Auto-enable if we have an API key
+ // Priority: env var > settings > default
+ let provider = optional_env("EMBEDDING_PROVIDER")?
+ .unwrap_or_else(|| settings.embeddings.provider.clone());
+
+ let model =
+ optional_env("EMBEDDING_MODEL")?.unwrap_or_else(|| settings.embeddings.model.clone());
+
+ // Priority: env var > settings > auto-detect from API key
let enabled = optional_env("EMBEDDING_ENABLED")?
.map(|s| s.parse())
.transpose()
@@ -280,14 +301,16 @@ impl EmbeddingsConfig {
key: "EMBEDDING_ENABLED".to_string(),
message: format!("must be 'true' or 'false': {e}"),
})?
- .unwrap_or(openai_api_key.is_some());
+ .unwrap_or_else(|| {
+ // Check settings, or auto-enable if API key present
+ settings.embeddings.enabled || openai_api_key.is_some()
+ });
Ok(Self {
enabled,
provider,
openai_api_key,
- model: optional_env("EMBEDDING_MODEL")?
- .unwrap_or_else(|| "text-embedding-3-small".to_string()),
+ model,
})
}
@@ -297,11 +320,11 @@ impl EmbeddingsConfig {
}
}
-/// Get the default session file path (~/.near-agent/session.json).
+/// Get the default session file path (~/.ironclaw/session.json).
fn default_session_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
- .join(".near-agent")
+ .join(".ironclaw")
.join("session.json")
}
@@ -310,7 +333,7 @@ fn default_session_path() -> PathBuf {
pub struct ChannelsConfig {
pub cli: CliConfig,
pub http: Option,
- /// Directory containing WASM channel modules (default: ~/.near-agent/channels/).
+ /// Directory containing WASM channel modules (default: ~/.ironclaw/channels/).
pub wasm_channels_dir: std::path::PathBuf,
/// Whether WASM channels are enabled.
pub wasm_channels_enabled: bool,
@@ -371,11 +394,11 @@ impl ChannelsConfig {
}
}
-/// Get the default channels directory (~/.near-agent/channels/).
+/// Get the default channels directory (~/.ironclaw/channels/).
fn default_channels_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
- .join(".near-agent")
+ .join(".ironclaw")
.join("channels")
}
@@ -394,19 +417,57 @@ pub struct AgentConfig {
impl AgentConfig {
fn from_env() -> Result {
+ let settings = crate::settings::Settings::load();
+
Ok(Self {
- name: optional_env("AGENT_NAME")?.unwrap_or_else(|| "near-agent".to_string()),
- max_parallel_jobs: parse_optional_env("AGENT_MAX_PARALLEL_JOBS", 5)?,
- job_timeout: Duration::from_secs(parse_optional_env("AGENT_JOB_TIMEOUT_SECS", 3600)?),
- stuck_threshold: Duration::from_secs(parse_optional_env(
- "AGENT_STUCK_THRESHOLD_SECS",
- 300,
- )?),
- repair_check_interval: Duration::from_secs(parse_optional_env(
- "SELF_REPAIR_CHECK_INTERVAL_SECS",
- 60,
- )?),
- max_repair_attempts: parse_optional_env("SELF_REPAIR_MAX_ATTEMPTS", 3)?,
+ // Priority: env var > settings > default
+ name: optional_env("AGENT_NAME")?.unwrap_or_else(|| settings.agent.name.clone()),
+ max_parallel_jobs: optional_env("AGENT_MAX_PARALLEL_JOBS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "AGENT_MAX_PARALLEL_JOBS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.agent.max_parallel_jobs as usize),
+ job_timeout: Duration::from_secs(
+ optional_env("AGENT_JOB_TIMEOUT_SECS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "AGENT_JOB_TIMEOUT_SECS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.agent.job_timeout_secs),
+ ),
+ stuck_threshold: Duration::from_secs(
+ optional_env("AGENT_STUCK_THRESHOLD_SECS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "AGENT_STUCK_THRESHOLD_SECS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.agent.stuck_threshold_secs),
+ ),
+ repair_check_interval: Duration::from_secs(
+ optional_env("SELF_REPAIR_CHECK_INTERVAL_SECS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "SELF_REPAIR_CHECK_INTERVAL_SECS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.agent.repair_check_interval_secs),
+ ),
+ max_repair_attempts: optional_env("SELF_REPAIR_MAX_ATTEMPTS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "SELF_REPAIR_MAX_ATTEMPTS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.agent.max_repair_attempts),
use_planning: optional_env("AGENT_USE_PLANNING")?
.map(|s| s.parse())
.transpose()
@@ -414,7 +475,7 @@ impl AgentConfig {
key: "AGENT_USE_PLANNING".to_string(),
message: format!("must be 'true' or 'false': {e}"),
})?
- .unwrap_or(true), // Default to planning enabled
+ .unwrap_or(settings.agent.use_planning),
})
}
}
@@ -447,7 +508,7 @@ impl SafetyConfig {
pub struct WasmConfig {
/// Whether WASM tool execution is enabled.
pub enabled: bool,
- /// Directory containing installed WASM tools (default: ~/.near-agent/tools/).
+ /// Directory containing installed WASM tools (default: ~/.ironclaw/tools/).
pub tools_dir: PathBuf,
/// Default memory limit in bytes (default: 10 MB).
pub default_memory_limit: u64,
@@ -464,11 +525,13 @@ pub struct WasmConfig {
/// Secrets management configuration.
#[derive(Clone, Default)]
pub struct SecretsConfig {
- /// Master key for encrypting secrets (loaded from SECRETS_MASTER_KEY env var).
- /// Must be at least 32 bytes for AES-256-GCM.
+ /// Master key for encrypting secrets.
+ /// Source determined by KeySource in settings.
pub master_key: Option,
/// Whether secrets management is enabled.
pub enabled: bool,
+ /// Source of the master key.
+ pub source: crate::settings::KeySource,
}
impl std::fmt::Debug for SecretsConfig {
@@ -476,13 +539,53 @@ impl std::fmt::Debug for SecretsConfig {
f.debug_struct("SecretsConfig")
.field("master_key", &self.master_key.is_some())
.field("enabled", &self.enabled)
+ .field("source", &self.source)
.finish()
}
}
impl SecretsConfig {
fn from_env() -> Result {
- let master_key = optional_env("SECRETS_MASTER_KEY")?.map(SecretString::from);
+ use crate::settings::KeySource;
+
+ let settings = crate::settings::Settings::load();
+
+ // Priority: env var > keychain (based on settings) > disabled
+ let (master_key, source) = if let Some(env_key) = optional_env("SECRETS_MASTER_KEY")? {
+ // Env var takes priority (for CI/Docker)
+ (Some(SecretString::from(env_key)), KeySource::Env)
+ } else {
+ match settings.secrets_master_key_source {
+ KeySource::Keychain => {
+ // Try to load from OS keychain
+ match crate::secrets::keychain::get_master_key() {
+ Ok(key_bytes) => {
+ let key_hex: String =
+ key_bytes.iter().map(|b| format!("{:02x}", b)).collect();
+ (Some(SecretString::from(key_hex)), KeySource::Keychain)
+ }
+ Err(_) => {
+ // Keychain configured but key not found
+ // This might happen if keychain was cleared
+ tracing::warn!(
+ "Secrets configured for keychain but key not found. \
+ Run 'ironclaw setup' to reconfigure."
+ );
+ (None, KeySource::None)
+ }
+ }
+ }
+ KeySource::Env => {
+ // Settings say env, but no env var found
+ tracing::warn!(
+ "Secrets configured for env var but SECRETS_MASTER_KEY not set."
+ );
+ (None, KeySource::None)
+ }
+ KeySource::None => (None, KeySource::None),
+ }
+ };
+
let enabled = master_key.is_some();
// Validate master key length if provided
@@ -498,6 +601,7 @@ impl SecretsConfig {
Ok(Self {
master_key,
enabled,
+ source,
})
}
@@ -521,11 +625,11 @@ impl Default for WasmConfig {
}
}
-/// Get the default tools directory (~/.near-agent/tools/).
+/// Get the default tools directory (~/.ironclaw/tools/).
fn default_tools_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
- .join(".near-agent")
+ .join(".ironclaw")
.join("tools")
}
@@ -676,7 +780,10 @@ impl Default for HeartbeatConfig {
impl HeartbeatConfig {
fn from_env() -> Result {
+ let settings = crate::settings::Settings::load();
+
Ok(Self {
+ // Priority: env var > settings > default
enabled: optional_env("HEARTBEAT_ENABLED")?
.map(|s| s.parse())
.transpose()
@@ -684,10 +791,19 @@ impl HeartbeatConfig {
key: "HEARTBEAT_ENABLED".to_string(),
message: format!("must be 'true' or 'false': {e}"),
})?
- .unwrap_or(false),
- interval_secs: parse_optional_env("HEARTBEAT_INTERVAL_SECS", 1800)?,
- notify_channel: optional_env("HEARTBEAT_NOTIFY_CHANNEL")?,
- notify_user: optional_env("HEARTBEAT_NOTIFY_USER")?,
+ .unwrap_or(settings.heartbeat.enabled),
+ interval_secs: optional_env("HEARTBEAT_INTERVAL_SECS")?
+ .map(|s| s.parse())
+ .transpose()
+ .map_err(|e| ConfigError::InvalidValue {
+ key: "HEARTBEAT_INTERVAL_SECS".to_string(),
+ message: format!("must be a positive integer: {e}"),
+ })?
+ .unwrap_or(settings.heartbeat.interval_secs),
+ notify_channel: optional_env("HEARTBEAT_NOTIFY_CHANNEL")?
+ .or(settings.heartbeat.notify_channel.clone()),
+ notify_user: optional_env("HEARTBEAT_NOTIFY_USER")?
+ .or(settings.heartbeat.notify_user.clone()),
})
}
}
@@ -787,10 +903,6 @@ impl SandboxModeConfig {
// Helper functions
-fn required_env(key: &str) -> Result {
- std::env::var(key).map_err(|_| ConfigError::MissingEnvVar(key.to_string()))
-}
-
fn optional_env(key: &str) -> Result