Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
**/target/
*.wasm
.env
.claude/
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tower-http = { version = "0.6", features = ["cors", "trace"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rmp-serde = "1" # MessagePack
toml = "0.8" # Config file parsing

# WASM plugin host
extism = "1"
Expand All @@ -38,6 +39,12 @@ clap = { version = "4", features = ["derive", "env"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Trait async support
async-trait = "0.1"

# URL parsing (for webhook proxy host validation)
url = "2"

# Utilities
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
Expand Down
60 changes: 46 additions & 14 deletions examples/config.toml
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
# exoclaw configuration
# Copy to ~/.exoclaw/config.toml or pass via --config flag.
# Exoclaw Configuration Reference
# Copy to ~/.exoclaw/config.toml and customize.
# All sections are optional — exoclaw works with zero config for local dev.

[gateway]
port = 7200
bind = "127.0.0.1"
# token = "set-via-EXOCLAW_TOKEN-env-var"
# Auth token: set via EXOCLAW_TOKEN env var (never put tokens in config files)

[agent]
provider = "anthropic"
model = "claude-sonnet-4-5-20250929"
max_tokens = 4096
id = "personal"
provider = "anthropic" # "anthropic" | "openai"
model = "claude-sonnet-4-5-20250929" # model identifier
max_tokens = 4096 # max tokens per LLM response
# api_key: set via ANTHROPIC_API_KEY or OPENAI_API_KEY env var
# system_prompt = "You are a helpful assistant."
# soul_path = "~/.exoclaw/soul.md" # personality document (~500 tokens)
# tools = ["echo", "web-search"] # plugin names this agent can use

# Optional fallback provider (used if primary fails)
[agent.fallback]
id = "fallback"
provider = "openai"
model = "gpt-4o"
max_tokens = 4096

# Optional: NATS message bus for persistence/replay
# [bus]
# url = "nats://localhost:4222"
# Token budgets — omit for unlimited
[budgets]
session = 50000 # 50K tokens per session
daily = 500000 # 500K tokens per day
monthly = 5000000 # 5M tokens per month

# WASM plugins — each plugin runs in an isolated sandbox
[[plugins]]
name = "echo"
path = "examples/echo-plugin/target/wasm32-unknown-unknown/release/echo_plugin.wasm"
capabilities = [] # echo needs no external access

# Plugin declarations — each channel is a WASM module
[[plugins]]
name = "telegram"
path = "plugins/telegram.wasm"
# Capabilities granted to this plugin:
capabilities = ["http:api.telegram.org", "store:sessions"]
capabilities = [
"http:api.telegram.org", # HTTP access to Telegram API
"store:sessions", # host storage access
]

[[plugins]]
name = "whatsapp"
path = "plugins/whatsapp.wasm"
capabilities = ["http:web.whatsapp.com", "store:sessions"]
capabilities = [
"http:web.whatsapp.com",
"store:sessions",
]

# Session routing bindings
# Priority: peer > guild > team > account > channel > default
[[bindings]]
channel = "websocket"
agent_id = "personal"

# Session routing bindings (priority: peer > guild > team > account > channel > default)
[[bindings]]
channel = "telegram"
agent_id = "personal"
Expand All @@ -40,3 +66,9 @@ agent_id = "personal"
channel = "whatsapp"
peer_id = "work-group-123"
agent_id = "work"

# Example: route a specific Discord guild to a work agent
# [[bindings]]
# channel = "discord"
# guild_id = "my-server-id"
# agent_id = "work"
53 changes: 53 additions & 0 deletions examples/echo-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ struct OutgoingMessage {
text: String,
}

/// Tool call input (generic JSON).
#[derive(Deserialize)]
struct ToolInput {
message: Option<String>,
}

/// Tool call result.
#[derive(Serialize)]
struct ToolResult {
content: String,
is_error: bool,
}

/// Main entry point called by the exoclaw plugin host.
///
/// Receives a JSON-encoded IncomingMessage and returns a JSON-encoded
Expand All @@ -40,3 +53,43 @@ pub fn handle_message(input: String) -> FnResult<String> {

Ok(output)
}

/// Tool call entry point. Takes JSON input, returns JSON result.
#[plugin_fn]
pub fn handle_tool_call(input: String) -> FnResult<String> {
let tool_input: ToolInput = serde_json::from_str(&input)
.map_err(|e| Error::msg(format!("bad tool input: {e}")))?;

let message = tool_input.message.unwrap_or_else(|| "no message".into());

let result = ToolResult {
content: format!("echo: {message}"),
is_error: false,
};

let output = serde_json::to_string(&result)
.map_err(|e| Error::msg(format!("serialize failed: {e}")))?;

Ok(output)
}

/// Describe the plugin's tool schema.
#[plugin_fn]
pub fn describe(_input: String) -> FnResult<String> {
let schema = serde_json::json!({
"name": "echo",
"description": "Echoes the input message back. Useful for testing.",
"input_schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message to echo back"
}
},
"required": ["message"]
}
});

Ok(serde_json::to_string(&schema).unwrap())
}
Loading