Skip to content
Closed
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
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Database Configuration
# For Docker: use host.docker.internal instead of localhost to access services on the host machine
DATABASE_URL=postgres://localhost/ironclaw
Comment on lines +2 to 3
Comment on lines +2 to 3
Comment on lines 1 to 3
DATABASE_POOL_SIZE=10

Expand Down Expand Up @@ -38,12 +39,14 @@ NEARAI_AUTH_URL=https://private.near.ai
# === Ollama ===
# OLLAMA_MODEL=llama3.2
# LLM_BACKEND=ollama
# OLLAMA_BASE_URL=http://localhost:11434 # default
# For Docker: use host.docker.internal:11434 to access Ollama on the host machine
# OLLAMA_BASE_URL=http://host.docker.internal:11434

# === OpenAI-compatible (LM Studio, vLLM, Anything-LLM) ===
# LLM_MODEL=llama-3.2-3b-instruct-q4_K_M
# LLM_BACKEND=openai_compatible
# LLM_BASE_URL=http://localhost:1234/v1
# For Docker: use host.docker.internal:1234 to access LM Studio on the host machine
# LLM_BASE_URL=http://host.docker.internal:1234/v1
# LLM_API_KEY=sk-... # optional for local servers
# Custom HTTP headers for OpenAI-compatible providers
# Format: comma-separated key:value pairs
Expand Down Expand Up @@ -99,6 +102,7 @@ SLACK_SIGNING_SECRET=...

# Telegram Bot (optional)
TELEGRAM_BOT_TOKEN=...
# TELEGRAM_POLLING_ENABLED=true # force Telegram to use getUpdates polling instead of webhooks

# HTTP Webhook Server (optional)
HTTP_HOST=0.0.0.0
Expand Down
29 changes: 1 addition & 28 deletions .github/workflows/code_style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,34 +89,7 @@ jobs:
- name: Check for .unwrap(), .expect(), assert!() in production code
run: |
BASE="${{ github.event.pull_request.base.sha }}"
# Get added lines in .rs files (production only, exclude tests/)
ADDED=$(git diff "$BASE"...HEAD -- 'src/**/*.rs' 'crates/**/*.rs' \
| grep -E '^\+[^+]' || true)

if [ -z "$ADDED" ]; then
echo "No production Rust changes detected."
exit 0
fi

# Match panic-inducing patterns, excluding test code and safety suppressions
VIOLATIONS=$(echo "$ADDED" \
| grep -E '\.(unwrap|expect)\(|[^_]assert(_eq|_ne)?!' \
| grep -Ev 'debug_assert|// safety:|#\[cfg\(test\)\]|#\[test\]|mod tests' \
|| true)

if [ -n "$VIOLATIONS" ]; then
echo "::error::Found .unwrap(), .expect(), or assert!() in production code."
echo "Production code must use proper error handling instead of panicking."
echo "Suppress false positives with an inline '// safety: <reason>' comment."
echo ""
echo "$VIOLATIONS" | head -20
echo ""
COUNT=$(echo "$VIOLATIONS" | wc -l | tr -d ' ')
echo "Total: $COUNT violation(s)"
exit 1
fi

echo "OK: No panic-inducing calls in changed production code."
python3 scripts/check_no_panics.py --base "$BASE" --head HEAD

# Roll-up job for branch protection
code-style:
Expand Down
24 changes: 12 additions & 12 deletions Cargo.lock

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

62 changes: 54 additions & 8 deletions channels-src/telegram/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1899,21 +1899,51 @@ fn clean_message_text(text: &str, bot_username: Option<&str>) -> String {
result
}

fn slash_command_token(text: &str) -> Option<&str> {
let trimmed = text.trim();
if !trimmed.starts_with('/') {
return None;
}

let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
Some(&trimmed[..end])
}

fn is_start_command_token(command: &str, bot_username: Option<&str>) -> bool {
if command.eq_ignore_ascii_case("/start") {
return true;
}

if let Some(bot) = bot_username {
let expected = format!("/start@{}", bot);
command.eq_ignore_ascii_case(&expected)
} else {
command.len() > "/start@".len() && command[..7].eq_ignore_ascii_case("/start@")
}
}

/// Decide which user content should be emitted to the agent loop.
///
/// - `/start` emits a placeholder so the agent can greet the user
/// - bare slash commands are passed through for Submission parsing
/// - slash commands are passed through for Submission parsing
/// - `/start <payload>` keeps legacy behavior and emits only the payload
/// - empty/mention-only messages are ignored
/// - otherwise cleaned text is emitted
fn content_to_emit_for_agent(content: &str, bot_username: Option<&str>) -> Option<String> {
let cleaned_text = clean_message_text(content, bot_username);
let trimmed_content = content.trim();
let cleaned_text = clean_message_text(content, bot_username);

if trimmed_content.eq_ignore_ascii_case("/start") {
return Some("[User started the bot]".to_string());
}
if let Some(command) = slash_command_token(trimmed_content) {
if is_start_command_token(command, bot_username) {
if trimmed_content.eq_ignore_ascii_case(command) {
return Some("[User started the bot]".to_string());
}

if !cleaned_text.is_empty() {
return Some(cleaned_text);
}
}

if cleaned_text.is_empty() && trimmed_content.starts_with('/') {
return Some(trimmed_content.to_string());
}

Expand Down Expand Up @@ -2007,12 +2037,20 @@ mod tests {
content_to_emit_for_agent(" /start ", None),
Some("[User started the bot]".to_string())
);
assert_eq!(
content_to_emit_for_agent("/start@MyBot", Some("MyBot")),
Some("[User started the bot]".to_string())
);

// /start with args → pass args through
assert_eq!(
content_to_emit_for_agent("/start hello", None),
Some("hello".to_string())
);
assert_eq!(
content_to_emit_for_agent("/start@MyBot hello", Some("MyBot")),
Some("hello".to_string())
);

// Control commands → pass through raw so Submission::parse() can match
assert_eq!(
Expand Down Expand Up @@ -2076,10 +2114,18 @@ mod tests {
Some("/no".to_string())
);

// Commands with args → cleaned text (command stripped)
// Commands with args must pass through raw so Submission::parse() can match.
assert_eq!(
content_to_emit_for_agent("/help me please", None),
Some("me please".to_string())
Some("/help me please".to_string())
);
assert_eq!(
content_to_emit_for_agent("/model claude-opus-4-6", None),
Some("/model claude-opus-4-6".to_string())
);
assert_eq!(
content_to_emit_for_agent("/model@MyBot qwen3.5:9b-q8_0", Some("MyBot")),
Some("/model@MyBot qwen3.5:9b-q8_0".to_string())
);

// Plain text → pass through
Expand Down
2 changes: 1 addition & 1 deletion channels-src/whatsapp/Cargo.lock

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

22 changes: 21 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
postgres:
image: pgvector/pgvector:pg16
ports:
- "127.0.0.1:5432:5432"
- "5432:5432"
environment:
POSTGRES_DB: ironclaw
POSTGRES_USER: ironclaw
Expand All @@ -16,5 +16,25 @@ services:
timeout: 3s
retries: 5

app:
image: ironclaw:latest
depends_on:
postgres:
condition: service_healthy
ports:
- "3231:3000"
- "8281:8080"
env_file:
- .env
environment:
# Override DB connection to use service name (DNS resolution)
DATABASE_URL: postgres://ironclaw:ironclaw@postgres:5432/ironclaw
volumes:
- ./extensions/ironclaw-home:/home/ironclaw/.ironclaw
- ./tools-src:/app/tools-src:ro
- ./channels-src:/app/channels-src:ro
Comment on lines +33 to +35
networks:
- default

volumes:
pgdata:
Loading
Loading