diff --git a/Cargo.toml b/Cargo.toml index bcb9b095..8ae00a3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ironclaw" version = "0.1.3" edition = "2024" -rust-version = "1.85" +rust-version = "1.92" description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly" authors = ["NEAR AI "] license = "MIT OR Apache-2.0" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..34d4d484 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# Multi-stage Dockerfile for the IronClaw agent (cloud deployment). +# +# Build: +# docker build --platform linux/amd64 -t ironclaw:latest . +# +# Run: +# docker run --env-file .env -p 3000:3000 ironclaw:latest + +# Stage 1: Build +FROM rust:1.92-slim-bookworm AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config libssl-dev cmake gcc g++ \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy manifests first for layer caching +COPY Cargo.toml Cargo.lock ./ + +# Copy source and build artifacts +COPY src/ src/ +COPY migrations/ migrations/ +COPY wit/ wit/ + +RUN cargo build --release --bin ironclaw + +# Stage 2: Runtime +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libssl3 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/ironclaw /usr/local/bin/ironclaw +COPY --from=builder /app/migrations /app/migrations + +# Non-root user +RUN useradd -m -u 1000 -s /bin/bash ironclaw +USER ironclaw + +EXPOSE 3000 + +ENV RUST_LOG=ironclaw=info + +ENTRYPOINT ["ironclaw"] diff --git a/Dockerfile.worker b/Dockerfile.worker index 3909abaa..6c58a5e5 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -9,7 +9,7 @@ # The image includes common development tools so workers can build software, # run tests, and execute shell commands. -FROM rust:1.85-bookworm AS builder +FROM rust:1.92-bookworm AS builder WORKDIR /build COPY . . @@ -40,7 +40,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.85.0 \ +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.92.0 \ && chmod -R a+r /usr/local/rustup /usr/local/cargo # Install Claude Code CLI (for claude-bridge mode) diff --git a/deploy/cloud-sql-proxy.service b/deploy/cloud-sql-proxy.service new file mode 100644 index 00000000..346a4e72 --- /dev/null +++ b/deploy/cloud-sql-proxy.service @@ -0,0 +1,13 @@ +[Unit] +Description=Cloud SQL Auth Proxy +After=network.target + +[Service] +Type=simple +DynamicUser=yes +ExecStart=/usr/local/bin/cloud-sql-proxy ironclaw-prod:us-central1:ironclaw-db --port=5432 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/env.example b/deploy/env.example new file mode 100644 index 00000000..1c7dac9e --- /dev/null +++ b/deploy/env.example @@ -0,0 +1,27 @@ +# WARNING: Replace all CHANGE_ME values before deploying. +# Do not use placeholder passwords in production. +DATABASE_URL=postgres://ironclaw:CHANGE_ME@localhost:5432/ironclaw + +# NEAR AI +NEARAI_SESSION_TOKEN=CHANGE_ME +NEARAI_MODEL=claude-3-5-sonnet-20241022 +NEARAI_BASE_URL=https://cloud-api.near.ai +NEARAI_AUTH_URL=https://private.near.ai +NEARAI_API_MODE=chat_completions + +# Agent +AGENT_NAME=ironclaw +CLI_ENABLED=false + +# Web Gateway +GATEWAY_ENABLED=true +# 0.0.0.0 binds to all interfaces (required for Docker --network=host). +# Use 127.0.0.1 if running outside Docker or for local-only access. +GATEWAY_HOST=0.0.0.0 +GATEWAY_PORT=3000 +GATEWAY_AUTH_TOKEN=CHANGE_ME + +# Disabled for initial deploy +SANDBOX_ENABLED=false +HEARTBEAT_ENABLED=false +EMBEDDING_ENABLED=false diff --git a/deploy/ironclaw.service b/deploy/ironclaw.service new file mode 100644 index 00000000..b5aa0a4e --- /dev/null +++ b/deploy/ironclaw.service @@ -0,0 +1,20 @@ +[Unit] +Description=IronClaw AI Assistant +After=cloud-sql-proxy.service docker.service +Requires=cloud-sql-proxy.service + +[Service] +Type=simple +ExecStartPre=/usr/bin/docker pull us-central1-docker.pkg.dev/ironclaw-prod/ironclaw/agent:latest +ExecStart=/usr/bin/docker run --rm \ + --name ironclaw \ + --env-file /opt/ironclaw/.env \ + --network=host \ + us-central1-docker.pkg.dev/ironclaw-prod/ironclaw/agent:latest \ + --no-onboard +ExecStop=/usr/bin/docker stop ironclaw +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100755 index 00000000..0bec03a0 --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# VM bootstrap script for IronClaw on GCP Compute Engine. +# +# Run on a fresh Debian 12 VM after SSH: +# sudo bash setup.sh +# +# Prerequisites: +# - VM has the ironclaw-vm service account attached +# - Cloud SQL Auth Proxy accessible via IAM +# - Artifact Registry image pushed + +set -euo pipefail + +# Must run as root +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: This script must be run as root (sudo bash setup.sh)" + exit 1 +fi + +echo "==> Installing Docker" +apt-get update +apt-get install -y docker.io +systemctl enable docker +systemctl start docker + +echo "==> Installing Cloud SQL Auth Proxy" +curl -fsSL -o /usr/local/bin/cloud-sql-proxy \ + https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.14.3/cloud-sql-proxy.linux.amd64 +chmod +x /usr/local/bin/cloud-sql-proxy + +echo "==> Installing systemd services" +cp /tmp/deploy/cloud-sql-proxy.service /etc/systemd/system/ +cp /tmp/deploy/ironclaw.service /etc/systemd/system/ +systemctl daemon-reload + +echo "==> Starting Cloud SQL Auth Proxy" +systemctl enable cloud-sql-proxy +systemctl start cloud-sql-proxy + +echo "==> Configuring Docker registry auth" +# The VM service account provides Artifact Registry access +gcloud auth configure-docker us-central1-docker.pkg.dev --quiet + +echo "==> Creating config directory" +# Owned by root, readable only by root. Docker reads --env-file as root +# before dropping to uid 1000 (ironclaw) inside the container. +mkdir -p /opt/ironclaw +chmod 700 /opt/ironclaw + +if [ ! -f /opt/ironclaw/.env ]; then + echo "WARNING: /opt/ironclaw/.env does not exist." + echo "Create it with your configuration before starting IronClaw." + echo "See deploy/env.example for the required variables." + echo "" + echo "Then run: systemctl enable ironclaw && systemctl start ironclaw" +else + chmod 600 /opt/ironclaw/.env + echo "==> Starting IronClaw" + systemctl enable ironclaw + systemctl start ironclaw +fi + +echo "==> Setup complete" +echo "" +echo "Verify with:" +echo " systemctl status cloud-sql-proxy" +echo " systemctl status ironclaw" +echo " docker logs ironclaw" diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 6c28d95d..9afd7bc2 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -654,19 +654,17 @@ impl Agent { } // Restore response chain from conversation metadata - if let Some(store) = self.store() { - if let Ok(Some(metadata)) = store.get_conversation_metadata(thread_uuid).await { - if let Some(rid) = metadata - .get("last_response_id") - .and_then(|v| v.as_str()) - .map(String::from) - { - thread.last_response_id = Some(rid.clone()); - self.llm() - .seed_response_chain(&thread_uuid.to_string(), rid); - tracing::debug!("Restored response chain for thread {}", thread_uuid); - } - } + if let Some(store) = self.store() + && let Ok(Some(metadata)) = store.get_conversation_metadata(thread_uuid).await + && let Some(rid) = metadata + .get("last_response_id") + .and_then(|v| v.as_str()) + .map(String::from) + { + thread.last_response_id = Some(rid.clone()); + self.llm() + .seed_response_chain(&thread_uuid.to_string(), rid); + tracing::debug!("Restored response chain for thread {}", thread_uuid); } // Insert into session and register with session manager @@ -954,13 +952,12 @@ impl Agent { return; } - if let Some(ref resp) = response { - if let Err(e) = store + if let Some(ref resp) = response + && let Err(e) = store .add_conversation_message(thread_id, "assistant", resp) .await - { - tracing::warn!("Failed to persist assistant message: {}", e); - } + { + tracing::warn!("Failed to persist assistant message: {}", e); } }); } @@ -1058,14 +1055,14 @@ impl Agent { // Check if interrupted { let sess = session.lock().await; - if let Some(thread) = sess.threads.get(&thread_id) { - if thread.state == ThreadState::Interrupted { - return Err(crate::error::JobError::ContextError { - id: thread_id, - reason: "Interrupted".to_string(), - } - .into()); + if let Some(thread) = sess.threads.get(&thread_id) + && thread.state == ThreadState::Interrupted + { + return Err(crate::error::JobError::ContextError { + id: thread_id, + reason: "Interrupted".to_string(), } + .into()); } } @@ -1140,11 +1137,11 @@ impl Agent { // Record tool calls in the thread { let mut sess = session.lock().await; - if let Some(thread) = sess.threads.get_mut(&thread_id) { - if let Some(turn) = thread.last_turn_mut() { - for tc in &tool_calls { - turn.record_tool_call(&tc.name, tc.arguments.clone()); - } + if let Some(thread) = sess.threads.get_mut(&thread_id) + && let Some(turn) = thread.last_turn_mut() + { + for tc in &tool_calls { + turn.record_tool_call(&tc.name, tc.arguments.clone()); } } } @@ -1152,54 +1149,48 @@ impl Agent { // Execute each tool (with approval checking) for tc in tool_calls { // Check if tool requires approval - if let Some(tool) = self.tools().get(&tc.name).await { - if tool.requires_approval() { - // Check if auto-approved for this session - let mut is_auto_approved = { - let sess = session.lock().await; - sess.is_tool_auto_approved(&tc.name) + if let Some(tool) = self.tools().get(&tc.name).await + && tool.requires_approval() + { + // Check if auto-approved for this session + let mut is_auto_approved = { + let sess = session.lock().await; + sess.is_tool_auto_approved(&tc.name) + }; + + // For shell commands, override auto-approval for + // destructive patterns that should always require + // explicit per-invocation approval. + if is_auto_approved + && tc.name == "shell" + && let Some(cmd) = tc + .arguments + .as_str() + .and_then(|s| serde_json::from_str::(s).ok()) + .and_then(|v| { + v.get("command").and_then(|c| c.as_str().map(String::from)) + }) + && crate::tools::builtin::shell::requires_explicit_approval(&cmd) + { + tracing::info!( + "Shell command '{}' requires explicit approval despite auto-approve", + cmd.chars().take(80).collect::() + ); + is_auto_approved = false; + } + + if !is_auto_approved { + // Need approval - store pending request and return + let pending = PendingApproval { + request_id: Uuid::new_v4(), + tool_name: tc.name.clone(), + parameters: tc.arguments.clone(), + description: tool.description().to_string(), + tool_call_id: tc.id.clone(), + context_messages: context_messages.clone(), }; - // For shell commands, override auto-approval for - // destructive patterns that should always require - // explicit per-invocation approval. - if is_auto_approved && tc.name == "shell" { - if let Some(cmd) = tc - .arguments - .as_str() - .and_then(|s| { - serde_json::from_str::(s).ok() - }) - .and_then(|v| { - v.get("command") - .and_then(|c| c.as_str().map(String::from)) - }) - { - if crate::tools::builtin::shell::requires_explicit_approval( - &cmd, - ) { - tracing::info!( - "Shell command '{}' requires explicit approval despite auto-approve", - cmd.chars().take(80).collect::() - ); - is_auto_approved = false; - } - } - } - - if !is_auto_approved { - // Need approval - store pending request and return - let pending = PendingApproval { - request_id: Uuid::new_v4(), - tool_name: tc.name.clone(), - parameters: tc.arguments.clone(), - description: tool.description().to_string(), - tool_call_id: tc.id.clone(), - context_messages: context_messages.clone(), - }; - - return Ok(AgenticLoopResult::NeedApproval { pending }); - } + return Ok(AgenticLoopResult::NeedApproval { pending }); } } @@ -1230,34 +1221,34 @@ impl Agent { ) .await; - if let Ok(ref output) = tool_result { - if !output.is_empty() { - let _ = self - .channels - .send_status( - &message.channel, - StatusUpdate::ToolResult { - name: tc.name.clone(), - preview: truncate_for_preview(output, 200), - }, - &message.metadata, - ) - .await; - } + if let Ok(ref output) = tool_result + && !output.is_empty() + { + let _ = self + .channels + .send_status( + &message.channel, + StatusUpdate::ToolResult { + name: tc.name.clone(), + preview: truncate_for_preview(output, 200), + }, + &message.metadata, + ) + .await; } // Record result in thread { let mut sess = session.lock().await; - if let Some(thread) = sess.threads.get_mut(&thread_id) { - if let Some(turn) = thread.last_turn_mut() { - match &tool_result { - Ok(output) => { - turn.record_tool_result(serde_json::json!(output)); - } - Err(e) => { - turn.record_tool_error(e.to_string()); - } + if let Some(thread) = sess.threads.get_mut(&thread_id) + && let Some(turn) = thread.last_turn_mut() + { + match &tool_result { + Ok(output) => { + turn.record_tool_result(serde_json::json!(output)); + } + Err(e) => { + turn.record_tool_error(e.to_string()); } } } @@ -1640,17 +1631,17 @@ impl Agent { }; // Verify request ID if provided - if let Some(req_id) = request_id { - if req_id != pending.request_id { - // Put it back and return error - let mut sess = session.lock().await; - if let Some(thread) = sess.threads.get_mut(&thread_id) { - thread.await_approval(pending); - } - return Ok(SubmissionResult::error( - "Request ID mismatch. Use the correct request ID.", - )); + if let Some(req_id) = request_id + && req_id != pending.request_id + { + // Put it back and return error + let mut sess = session.lock().await; + if let Some(thread) = sess.threads.get_mut(&thread_id) { + thread.await_approval(pending); } + return Ok(SubmissionResult::error( + "Request ID mismatch. Use the correct request ID.", + )); } if approved { @@ -1704,20 +1695,20 @@ impl Agent { ) .await; - if let Ok(ref output) = tool_result { - if !output.is_empty() { - let _ = self - .channels - .send_status( - &message.channel, - StatusUpdate::ToolResult { - name: pending.tool_name.clone(), - preview: truncate_for_preview(output, 200), - }, - &message.metadata, - ) - .await; - } + if let Ok(ref output) = tool_result + && !output.is_empty() + { + let _ = self + .channels + .send_status( + &message.channel, + StatusUpdate::ToolResult { + name: pending.tool_name.clone(), + preview: truncate_for_preview(output, 200), + }, + &message.metadata, + ) + .await; } // Build context including the tool result @@ -1726,15 +1717,15 @@ impl Agent { // Record result in thread { let mut sess = session.lock().await; - if let Some(thread) = sess.threads.get_mut(&thread_id) { - if let Some(turn) = thread.last_turn_mut() { - match &tool_result { - Ok(output) => { - turn.record_tool_result(serde_json::json!(output)); - } - Err(e) => { - turn.record_tool_error(e.to_string()); - } + if let Some(thread) = sess.threads.get_mut(&thread_id) + && let Some(turn) = thread.last_turn_mut() + { + match &tool_result { + Ok(output) => { + turn.record_tool_result(serde_json::json!(output)); + } + Err(e) => { + turn.record_tool_error(e.to_string()); } } } @@ -2094,15 +2085,15 @@ impl Agent { } // Persist new job to database (fire-and-forget) - if let Some(store) = self.store() { - if let Ok(ctx) = self.context_manager.get_context(job_id).await { - let store = store.clone(); - tokio::spawn(async move { - if let Err(e) = store.save_job(&ctx).await { - tracing::warn!("Failed to persist new job {}: {}", job_id, e); - } - }); - } + if let Some(store) = self.store() + && let Ok(ctx) = self.context_manager.get_context(job_id).await + { + let store = store.clone(); + tokio::spawn(async move { + if let Err(e) = store.save_job(&ctx).await { + tracing::warn!("Failed to persist new job {}: {}", job_id, e); + } + }); } // Schedule for execution @@ -2182,10 +2173,10 @@ impl Agent { let mut output = String::from("Jobs:\n"); for job_id in jobs { - if let Ok(ctx) = self.context_manager.get_context(job_id).await { - if ctx.user_id == user_id { - output.push_str(&format!(" {} - {} ({:?})\n", job_id, ctx.title, ctx.state)); - } + if let Ok(ctx) = self.context_manager.get_context(job_id).await + && ctx.user_id == user_id + { + output.push_str(&format!(" {} - {} ({:?})\n", job_id, ctx.title, ctx.state)); } } diff --git a/src/agent/routine_engine.rs b/src/agent/routine_engine.rs index ed4037c9..e88a0a9d 100644 --- a/src/agent/routine_engine.rs +++ b/src/agent/routine_engine.rs @@ -103,10 +103,9 @@ impl RoutineEngine { if let Trigger::Event { channel: Some(ch), .. } = &routine.trigger + && ch != &message.channel { - if ch != &message.channel { - continue; - } + continue; } // Regex match diff --git a/src/agent/self_repair.rs b/src/agent/self_repair.rs index 4d514405..ace75fe8 100644 --- a/src/agent/self_repair.rs +++ b/src/agent/self_repair.rs @@ -119,25 +119,25 @@ impl SelfRepair for DefaultSelfRepair { let mut stuck_jobs = Vec::new(); for job_id in stuck_ids { - if let Ok(ctx) = self.context_manager.get_context(job_id).await { - if ctx.state == JobState::Stuck { - let stuck_duration = ctx - .started_at - .map(|start| { - let now = Utc::now(); - let duration = now.signed_duration_since(start); - Duration::from_secs(duration.num_seconds().max(0) as u64) - }) - .unwrap_or_default(); + if let Ok(ctx) = self.context_manager.get_context(job_id).await + && ctx.state == JobState::Stuck + { + let stuck_duration = ctx + .started_at + .map(|start| { + let now = Utc::now(); + let duration = now.signed_duration_since(start); + Duration::from_secs(duration.num_seconds().max(0) as u64) + }) + .unwrap_or_default(); - stuck_jobs.push(StuckJob { - job_id, - last_activity: ctx.started_at.unwrap_or(ctx.created_at), - stuck_duration, - last_error: None, - repair_attempts: ctx.repair_attempts, - }); - } + stuck_jobs.push(StuckJob { + job_id, + last_activity: ctx.started_at.unwrap_or(ctx.created_at), + stuck_duration, + last_error: None, + repair_attempts: ctx.repair_attempts, + }); } } diff --git a/src/agent/session.rs b/src/agent/session.rs index fbfb4aa3..e77149ec 100644 --- a/src/agent/session.rs +++ b/src/agent/session.rs @@ -346,11 +346,11 @@ impl Thread { let mut turn = Turn::new(turn_number, &msg.content); // Check if next is assistant response - if let Some(next) = iter.peek() { - if next.role == crate::llm::Role::Assistant { - let response = iter.next().expect("peeked"); - turn.complete(&response.content); - } + if let Some(next) = iter.peek() + && next.role == crate::llm::Role::Assistant + { + let response = iter.next().expect("peeked"); + turn.complete(&response.content); } self.turns.push(turn); diff --git a/src/agent/session_manager.rs b/src/agent/session_manager.rs index 2ea6bfa1..db0be886 100644 --- a/src/agent/session_manager.rs +++ b/src/agent/session_manager.rs @@ -199,10 +199,10 @@ impl SessionManager { { let sessions = self.sessions.read().await; for user_id in &stale_users { - if let Some(session) = sessions.get(user_id) { - if let Ok(sess) = session.try_lock() { - stale_thread_ids.extend(sess.threads.keys()); - } + if let Some(session) = sessions.get(user_id) + && let Ok(sess) = session.try_lock() + { + stale_thread_ids.extend(sess.threads.keys()); } } } diff --git a/src/agent/submission.rs b/src/agent/submission.rs index 11f86263..a2b6b4d7 100644 --- a/src/agent/submission.rs +++ b/src/agent/submission.rs @@ -93,27 +93,26 @@ impl SubmissionParser { // /thread - switch thread if let Some(rest) = lower.strip_prefix("/thread ") { let rest = rest.trim(); - if rest != "new" { - if let Ok(id) = Uuid::parse_str(rest) { - return Submission::SwitchThread { thread_id: id }; - } + if rest != "new" + && let Ok(id) = Uuid::parse_str(rest) + { + return Submission::SwitchThread { thread_id: id }; } } // /resume - resume from checkpoint - if let Some(rest) = lower.strip_prefix("/resume ") { - if let Ok(id) = Uuid::parse_str(rest.trim()) { - return Submission::Resume { checkpoint_id: id }; - } + if let Some(rest) = lower.strip_prefix("/resume ") + && let Ok(id) = Uuid::parse_str(rest.trim()) + { + return Submission::Resume { checkpoint_id: id }; } // Try structured JSON approval (from web gateway's /api/chat/approval endpoint) - if trimmed.starts_with('{') { - if let Ok(submission) = serde_json::from_str::(trimmed) { - if matches!(submission, Submission::ExecApproval { .. }) { - return submission; - } - } + if trimmed.starts_with('{') + && let Ok(submission) = serde_json::from_str::(trimmed) + && matches!(submission, Submission::ExecApproval { .. }) + { + return submission; } // Approval responses (simple yes/no/always for pending approvals) diff --git a/src/agent/worker.rs b/src/agent/worker.rs index 65455839..bf8ec895 100644 --- a/src/agent/worker.rs +++ b/src/agent/worker.rs @@ -227,11 +227,11 @@ Report when the job is complete or if you encounter issues you cannot resolve."# } // Check for cancellation - if let Ok(ctx) = self.context_manager().get_context(self.job_id).await { - if ctx.state == JobState::Cancelled { - tracing::info!("Worker for job {} detected cancellation", self.job_id); - return Ok(()); - } + if let Ok(ctx) = self.context_manager().get_context(self.job_id).await + && ctx.state == JobState::Cancelled + { + tracing::info!("Worker for job {} detected cancellation", self.job_id); + return Ok(()); } iteration += 1; diff --git a/src/channels/wasm/wrapper.rs b/src/channels/wasm/wrapper.rs index da96255f..5d34e40c 100644 --- a/src/channels/wasm/wrapper.rs +++ b/src/channels/wasm/wrapper.rs @@ -134,13 +134,13 @@ impl ChannelStoreData { if result.contains('{') && result.contains('}') { // Only warn if it looks like an unresolved placeholder (not JSON braces) let brace_pattern = regex::Regex::new(r"\{[A-Z_]+\}").ok(); - if let Some(re) = brace_pattern { - if re.is_match(&result) { - tracing::warn!( - context = %context, - "String may contain unresolved credential placeholders" - ); - } + if let Some(re) = brace_pattern + && re.is_match(&result) + { + tracing::warn!( + context = %context, + "String may contain unresolved credential placeholders" + ); } } @@ -338,13 +338,13 @@ impl near::agent::channel_host::Host for ChannelStoreData { // Enforce max response body size to prevent memory exhaustion. let max_response = max_response_bytes; - if let Some(cl) = response.content_length() { - if cl as usize > max_response { - return Err(format!( - "Response body too large: {} bytes exceeds limit of {} bytes", - cl, max_response - )); - } + if let Some(cl) = response.content_length() + && cl as usize > max_response + { + return Err(format!( + "Response body too large: {} bytes exceeds limit of {} bytes", + cl, max_response + )); } let body = response .bytes() @@ -1495,8 +1495,8 @@ impl WasmChannel { match result { Ok(emitted_messages) => { // Process any emitted messages - if !emitted_messages.is_empty() { - if let Err(e) = Self::dispatch_emitted_messages( + if !emitted_messages.is_empty() + && let Err(e) = Self::dispatch_emitted_messages( &channel_name, emitted_messages, &message_tx, @@ -1508,7 +1508,6 @@ impl WasmChannel { "Failed to dispatch emitted messages from poll" ); } - } } Err(e) => { tracing::warn!( @@ -1738,22 +1737,22 @@ impl Channel for WasmChannel { *self.endpoints.write().await = endpoints; // Start polling if configured - if let Some(poll_config) = &config.poll { - if poll_config.enabled { - let interval = self - .capabilities - .validate_poll_interval(poll_config.interval_ms) - .map_err(|e| ChannelError::StartupFailed { - name: self.name.clone(), - reason: e, - })?; + if let Some(poll_config) = &config.poll + && poll_config.enabled + { + let interval = self + .capabilities + .validate_poll_interval(poll_config.interval_ms) + .map_err(|e| ChannelError::StartupFailed { + name: self.name.clone(), + reason: e, + })?; - // Create shutdown channel for polling and store the sender to keep it alive - let (poll_shutdown_tx, poll_shutdown_rx) = oneshot::channel(); - *self.poll_shutdown_tx.write().await = Some(poll_shutdown_tx); + // Create shutdown channel for polling and store the sender to keep it alive + let (poll_shutdown_tx, poll_shutdown_rx) = oneshot::channel(); + *self.poll_shutdown_tx.write().await = Some(poll_shutdown_tx); - self.start_polling(Duration::from_millis(interval as u64), poll_shutdown_rx); - } + self.start_polling(Duration::from_millis(interval as u64), poll_shutdown_rx); } tracing::info!( diff --git a/src/channels/web/auth.rs b/src/channels/web/auth.rs index 34fc9f07..23d1ddfc 100644 --- a/src/channels/web/auth.rs +++ b/src/channels/web/auth.rs @@ -25,23 +25,21 @@ pub async fn auth_middleware( next: Next, ) -> Response { // Try Authorization header first (constant-time comparison) - if let Some(auth_header) = headers.get("authorization") { - if let Ok(value) = auth_header.to_str() { - if let Some(token) = value.strip_prefix("Bearer ") { - if bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) { - return next.run(request).await; - } - } - } + if let Some(auth_header) = headers.get("authorization") + && let Ok(value) = auth_header.to_str() + && let Some(token) = value.strip_prefix("Bearer ") + && bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) + { + return next.run(request).await; } // Fall back to query parameter for SSE EventSource (constant-time comparison) if let Some(query) = request.uri().query() { for pair in query.split('&') { - if let Some(token) = pair.strip_prefix("token=") { - if bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) { - return next.run(request).await; - } + if let Some(token) = pair.strip_prefix("token=") + && bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) + { + return next.run(request).await; } } } diff --git a/src/channels/web/openai_compat.rs b/src/channels/web/openai_compat.rs index 7185ca7b..b2dfa007 100644 --- a/src/channels/web/openai_compat.rs +++ b/src/channels/web/openai_compat.rs @@ -473,10 +473,10 @@ pub async fn chat_completions_handler( if let Some(mt) = req.max_tokens { tool_req = tool_req.with_max_tokens(mt); } - if let Some(ref tc) = req.tool_choice { - if let Some(choice) = normalize_tool_choice(tc) { - tool_req = tool_req.with_tool_choice(choice); - } + if let Some(ref tc) = req.tool_choice + && let Some(choice) = normalize_tool_choice(tc) + { + tool_req = tool_req.with_tool_choice(choice); } let resp = llm @@ -591,10 +591,10 @@ async fn handle_streaming( if let Some(mt) = req.max_tokens { tool_req = tool_req.with_max_tokens(mt); } - if let Some(ref tc) = req.tool_choice { - if let Some(choice) = normalize_tool_choice(tc) { - tool_req = tool_req.with_tool_choice(choice); - } + if let Some(ref tc) = req.tool_choice + && let Some(choice) = normalize_tool_choice(tc) + { + tool_req = tool_req.with_tool_choice(choice); } LlmResult::WithTools( llm.complete_with_tools(tool_req) diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index 540b8174..68bf6edf 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -525,10 +525,10 @@ pub async fn clear_auth_mode(state: &GatewayState) { if let Some(ref sm) = state.session_manager { let session = sm.get_or_create_session(&state.user_id).await; let mut sess = session.lock().await; - if let Some(thread_id) = sess.active_thread { - if let Some(thread) = sess.threads.get_mut(&thread_id) { - thread.pending_auth = None; - } + if let Some(thread_id) = sess.active_thread + && let Some(thread) = sess.threads.get_mut(&thread_id) + { + thread.pending_auth = None; } } } @@ -626,69 +626,69 @@ async fn chat_history_handler( // Verify the thread belongs to the authenticated user before returning any data. // In-memory threads are already scoped by user via session_manager, but DB // lookups could expose another user's conversation if the UUID is guessed. - if query.thread_id.is_some() { - if let Some(ref store) = state.store { - let owned = store - .conversation_belongs_to_user(thread_id, &state.user_id) - .await - .unwrap_or(false); - if !owned && !sess.threads.contains_key(&thread_id) { - return Err((StatusCode::NOT_FOUND, "Thread not found".to_string())); - } + if query.thread_id.is_some() + && let Some(ref store) = state.store + { + let owned = store + .conversation_belongs_to_user(thread_id, &state.user_id) + .await + .unwrap_or(false); + if !owned && !sess.threads.contains_key(&thread_id) { + return Err((StatusCode::NOT_FOUND, "Thread not found".to_string())); } } // For paginated requests (before cursor set), always go to DB - if before_cursor.is_some() { - if let Some(ref store) = state.store { - let (messages, has_more) = store - .list_conversation_messages_paginated(thread_id, before_cursor, limit as i64) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + if before_cursor.is_some() + && let Some(ref store) = state.store + { + let (messages, has_more) = store + .list_conversation_messages_paginated(thread_id, before_cursor, limit as i64) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let oldest_timestamp = messages.first().map(|m| m.created_at.to_rfc3339()); - let turns = build_turns_from_db_messages(&messages); - return Ok(Json(HistoryResponse { - thread_id, - turns, - has_more, - oldest_timestamp, - })); - } + let oldest_timestamp = messages.first().map(|m| m.created_at.to_rfc3339()); + let turns = build_turns_from_db_messages(&messages); + return Ok(Json(HistoryResponse { + thread_id, + turns, + has_more, + oldest_timestamp, + })); } // Try in-memory first (freshest data for active threads) - if let Some(thread) = sess.threads.get(&thread_id) { - if !thread.turns.is_empty() { - let turns: Vec = thread - .turns - .iter() - .map(|t| TurnInfo { - turn_number: t.turn_number, - user_input: t.user_input.clone(), - response: t.response.clone(), - state: format!("{:?}", t.state), - started_at: t.started_at.to_rfc3339(), - completed_at: t.completed_at.map(|dt| dt.to_rfc3339()), - tool_calls: t - .tool_calls - .iter() - .map(|tc| ToolCallInfo { - name: tc.name.clone(), - has_result: tc.result.is_some(), - has_error: tc.error.is_some(), - }) - .collect(), - }) - .collect(); + if let Some(thread) = sess.threads.get(&thread_id) + && !thread.turns.is_empty() + { + let turns: Vec = thread + .turns + .iter() + .map(|t| TurnInfo { + turn_number: t.turn_number, + user_input: t.user_input.clone(), + response: t.response.clone(), + state: format!("{:?}", t.state), + started_at: t.started_at.to_rfc3339(), + completed_at: t.completed_at.map(|dt| dt.to_rfc3339()), + tool_calls: t + .tool_calls + .iter() + .map(|tc| ToolCallInfo { + name: tc.name.clone(), + has_result: tc.result.is_some(), + has_error: tc.error.is_some(), + }) + .collect(), + }) + .collect(); - return Ok(Json(HistoryResponse { - thread_id, - turns, - has_more: false, - oldest_timestamp: None, - })); - } + return Ok(Json(HistoryResponse { + thread_id, + turns, + has_more: false, + oldest_timestamp: None, + })); } // Fall back to DB for historical threads not in memory (paginated) @@ -738,12 +738,12 @@ fn build_turns_from_db_messages(messages: &[crate::history::ConversationMessage] }; // Check if next message is an assistant response - if let Some(next) = iter.peek() { - if next.role == "assistant" { - let assistant_msg = iter.next().expect("peeked"); - turn.response = Some(assistant_msg.content.clone()); - turn.completed_at = Some(assistant_msg.created_at.to_rfc3339()); - } + if let Some(next) = iter.peek() + && next.role == "assistant" + { + let assistant_msg = iter.next().expect("peeked"); + turn.response = Some(assistant_msg.content.clone()); + turn.completed_at = Some(assistant_msg.created_at.to_rfc3339()); } // Incomplete turn (user message without response) @@ -1126,65 +1126,65 @@ async fn jobs_detail_handler( .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; // Try sandbox job from DB first, scoped to the authenticated user. - if let Some(ref store) = state.store { - if let Ok(Some(job)) = store.get_sandbox_job(job_id).await { - if job.user_id != state.user_id { - return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); - } - let browse_id = std::path::Path::new(&job.project_dir) - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| job.id.to_string()); - - let ui_state = match job.status.as_str() { - "creating" => "pending", - "running" => "in_progress", - s => s, - }; - - let elapsed_secs = job.started_at.map(|start| { - let end = job.completed_at.unwrap_or_else(chrono::Utc::now); - (end - start).num_seconds().max(0) as u64 - }); - - // Synthesize transitions from timestamps. - let mut transitions = Vec::new(); - if let Some(started) = job.started_at { - transitions.push(TransitionInfo { - from: "creating".to_string(), - to: "running".to_string(), - timestamp: started.to_rfc3339(), - reason: None, - }); - } - if let Some(completed) = job.completed_at { - transitions.push(TransitionInfo { - from: "running".to_string(), - to: job.status.clone(), - timestamp: completed.to_rfc3339(), - reason: job.failure_reason.clone(), - }); - } - - return Ok(Json(JobDetailResponse { - id: job.id, - title: job.task.clone(), - description: String::new(), - state: ui_state.to_string(), - user_id: job.user_id.clone(), - created_at: job.created_at.to_rfc3339(), - started_at: job.started_at.map(|dt| dt.to_rfc3339()), - completed_at: job.completed_at.map(|dt| dt.to_rfc3339()), - elapsed_secs, - project_dir: Some(job.project_dir.clone()), - browse_url: Some(format!("/projects/{}/", browse_id)), - job_mode: { - let mode = store.get_sandbox_job_mode(job.id).await.ok().flatten(); - mode.filter(|m| m != "worker") - }, - transitions, - })); + if let Some(ref store) = state.store + && let Ok(Some(job)) = store.get_sandbox_job(job_id).await + { + if job.user_id != state.user_id { + return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); } + let browse_id = std::path::Path::new(&job.project_dir) + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| job.id.to_string()); + + let ui_state = match job.status.as_str() { + "creating" => "pending", + "running" => "in_progress", + s => s, + }; + + let elapsed_secs = job.started_at.map(|start| { + let end = job.completed_at.unwrap_or_else(chrono::Utc::now); + (end - start).num_seconds().max(0) as u64 + }); + + // Synthesize transitions from timestamps. + let mut transitions = Vec::new(); + if let Some(started) = job.started_at { + transitions.push(TransitionInfo { + from: "creating".to_string(), + to: "running".to_string(), + timestamp: started.to_rfc3339(), + reason: None, + }); + } + if let Some(completed) = job.completed_at { + transitions.push(TransitionInfo { + from: "running".to_string(), + to: job.status.clone(), + timestamp: completed.to_rfc3339(), + reason: job.failure_reason.clone(), + }); + } + + return Ok(Json(JobDetailResponse { + id: job.id, + title: job.task.clone(), + description: String::new(), + state: ui_state.to_string(), + user_id: job.user_id.clone(), + created_at: job.created_at.to_rfc3339(), + started_at: job.started_at.map(|dt| dt.to_rfc3339()), + completed_at: job.completed_at.map(|dt| dt.to_rfc3339()), + elapsed_secs, + project_dir: Some(job.project_dir.clone()), + browse_url: Some(format!("/projects/{}/", browse_id)), + job_mode: { + let mode = store.get_sandbox_job_mode(job.id).await.ok().flatten(); + mode.filter(|m| m != "worker") + }, + transitions, + })); } Err((StatusCode::NOT_FOUND, "Job not found".to_string())) @@ -1198,35 +1198,35 @@ async fn jobs_cancel_handler( .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; // Try sandbox job cancellation, scoped to the authenticated user. - if let Some(ref store) = state.store { - if let Ok(Some(job)) = store.get_sandbox_job(job_id).await { - if job.user_id != state.user_id { - return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); - } - if job.status == "running" || job.status == "creating" { - // Stop the container if we have a job manager. - if let Some(ref jm) = state.job_manager { - if let Err(e) = jm.stop_job(job_id).await { - tracing::warn!(job_id = %job_id, error = %e, "Failed to stop container during cancellation"); - } - } - store - .update_sandbox_job_status( - job_id, - "failed", - Some(false), - Some("Cancelled by user"), - None, - Some(chrono::Utc::now()), - ) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - } - return Ok(Json(serde_json::json!({ - "status": "cancelled", - "job_id": job_id, - }))); + if let Some(ref store) = state.store + && let Ok(Some(job)) = store.get_sandbox_job(job_id).await + { + if job.user_id != state.user_id { + return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); } + if job.status == "running" || job.status == "creating" { + // Stop the container if we have a job manager. + if let Some(ref jm) = state.job_manager + && let Err(e) = jm.stop_job(job_id).await + { + tracing::warn!(job_id = %job_id, error = %e, "Failed to stop container during cancellation"); + } + store + .update_sandbox_job_status( + job_id, + "failed", + Some(false), + Some("Cancelled by user"), + None, + Some(chrono::Utc::now()), + ) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + } + return Ok(Json(serde_json::json!({ + "status": "cancelled", + "job_id": job_id, + }))); } Err((StatusCode::NOT_FOUND, "Job not found".to_string())) @@ -1334,14 +1334,13 @@ async fn jobs_prompt_handler( .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; // Verify user owns this job. - if let Some(ref store) = state.store { - if !store + if let Some(ref store) = state.store + && !store .sandbox_job_belongs_to_user(job_id, &state.user_id) .await .unwrap_or(false) - { - return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); - } + { + return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); } let content = body diff --git a/src/cli/config.rs b/src/cli/config.rs index 894ea710..07788dca 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -107,10 +107,10 @@ async fn list_settings( println!(); for (key, value) in all { - if let Some(ref f) = filter { - if !key.starts_with(f) { - continue; - } + if let Some(ref f) = filter + && !key.starts_with(f) + { + continue; } let display_value = if value.len() > 60 { diff --git a/src/cli/tool.rs b/src/cli/tool.rs index 316ffa2c..f49c3a8d 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -420,11 +420,11 @@ async fn extract_crate_name(cargo_toml: &Path) -> anyhow::Result { // Simple TOML parsing for [package] name for line in content.lines() { let line = line.trim(); - if line.starts_with("name") { - if let Some((_, value)) = line.split_once('=') { - let name = value.trim().trim_matches('"').trim_matches('\''); - return Ok(name.to_string()); - } + if line.starts_with("name") + && let Some((_, value)) = line.split_once('=') + { + let name = value.trim().trim_matches('"').trim_matches('\''); + return Ok(name.to_string()); } } @@ -488,10 +488,10 @@ async fn list_tools(dir: Option, verbose: bool) -> anyhow::Result<()> { if has_caps { let caps_path = path.with_extension("capabilities.json"); - if let Ok(content) = fs::read_to_string(&caps_path).await { - if let Ok(caps) = CapabilitiesFile::from_json(&content) { - print_capabilities_summary(&caps); - } + if let Ok(content) = fs::read_to_string(&caps_path).await + && let Ok(caps) = CapabilitiesFile::from_json(&content) + { + print_capabilities_summary(&caps); } } println!(); @@ -604,16 +604,16 @@ fn print_capabilities_summary(caps: &CapabilitiesFile) { } } - if let Some(ref secrets) = caps.secrets { - if !secrets.allowed_names.is_empty() { - parts.push(format!("secrets: {}", secrets.allowed_names.len())); - } + if let Some(ref secrets) = caps.secrets + && !secrets.allowed_names.is_empty() + { + parts.push(format!("secrets: {}", secrets.allowed_names.len())); } - if let Some(ref ws) = caps.workspace { - if !ws.allowed_prefixes.is_empty() { - parts.push("workspace: read".to_string()); - } + if let Some(ref ws) = caps.workspace + && !ws.allowed_prefixes.is_empty() + { + parts.push("workspace: read".to_string()); } if !parts.is_empty() { @@ -650,30 +650,30 @@ fn print_capabilities_detail(caps: &CapabilitiesFile) { } } - if let Some(ref secrets) = caps.secrets { - if !secrets.allowed_names.is_empty() { - println!(" Secrets (existence check only):"); - for name in &secrets.allowed_names { - println!(" {}", name); - } + if let Some(ref secrets) = caps.secrets + && !secrets.allowed_names.is_empty() + { + println!(" Secrets (existence check only):"); + for name in &secrets.allowed_names { + println!(" {}", name); } } - if let Some(ref tool_invoke) = caps.tool_invoke { - if !tool_invoke.aliases.is_empty() { - println!(" Tool aliases:"); - for (alias, real_name) in &tool_invoke.aliases { - println!(" {} -> {}", alias, real_name); - } + if let Some(ref tool_invoke) = caps.tool_invoke + && !tool_invoke.aliases.is_empty() + { + println!(" Tool aliases:"); + for (alias, real_name) in &tool_invoke.aliases { + println!(" {} -> {}", alias, real_name); } } - if let Some(ref ws) = caps.workspace { - if !ws.allowed_prefixes.is_empty() { - println!(" Workspace read prefixes:"); - for prefix in &ws.allowed_prefixes { - println!(" {}", prefix); - } + if let Some(ref ws) = caps.workspace + && !ws.allowed_prefixes.is_empty() + { + println!(" Workspace read prefixes:"); + for prefix in &ws.allowed_prefixes { + println!(" {}", prefix); } } } @@ -752,37 +752,36 @@ async fn auth_tool(name: String, dir: Option, user_id: String) -> anyho } // Check for environment variable - if let Some(ref env_var) = auth.env_var { - if let Ok(token) = std::env::var(env_var) { - if !token.is_empty() { - println!(" Found {} in environment.", env_var); - println!(); + if let Some(ref env_var) = auth.env_var + && let Ok(token) = std::env::var(env_var) + && !token.is_empty() + { + println!(" Found {} in environment.", env_var); + println!(); - // Validate if endpoint is provided - if let Some(ref validation) = auth.validation_endpoint { - print!(" Validating token..."); - std::io::stdout().flush()?; + // Validate if endpoint is provided + if let Some(ref validation) = auth.validation_endpoint { + print!(" Validating token..."); + std::io::stdout().flush()?; - match validate_token(&token, validation, &auth.secret_name).await { - Ok(()) => { - println!(" ✓"); - } - Err(e) => { - println!(" ✗"); - println!(" Validation failed: {}", e); - println!(); - println!(" Falling back to manual entry..."); - return auth_tool_manual(&secrets_store, &user_id, &auth).await; - } - } + match validate_token(&token, validation, &auth.secret_name).await { + Ok(()) => { + println!(" ✓"); + } + Err(e) => { + println!(" ✗"); + println!(" Validation failed: {}", e); + println!(); + println!(" Falling back to manual entry..."); + return auth_tool_manual(&secrets_store, &user_id, &auth).await; } - - // Save the token - save_token(&secrets_store, &user_id, &auth, &token).await?; - print_success(display_name); - return Ok(()); } } + + // Save the token + save_token(&secrets_store, &user_id, &auth, &token).await?; + print_success(display_name); + return Ok(()); } // Check for OAuth configuration @@ -923,9 +922,9 @@ async fn auth_tool_oauth( reader.read_line(&mut request_line).await?; // Parse GET /callback?code=xxx HTTP/1.1 - if let Some(path) = request_line.split_whitespace().nth(1) { - if path.starts_with("/callback") { - if let Some(query) = path.split('?').nth(1) { + if let Some(path) = request_line.split_whitespace().nth(1) + && path.starts_with("/callback") + && let Some(query) = path.split('?').nth(1) { for param in query.split('&') { let parts: Vec<&str> = param.splitn(2, '=').collect(); if parts.len() == 2 && parts[0] == "code" { @@ -962,8 +961,6 @@ async fn auth_tool_oauth( return Err(anyhow::anyhow!("Authorization denied by user")); } } - } - } let response = "HTTP/1.1 404 Not Found\r\n\r\n"; let _ = socket.write_all(response.as_bytes()).await; diff --git a/src/config.rs b/src/config.rs index 6ecd7cc8..285a2914 100644 --- a/src/config.rs +++ b/src/config.rs @@ -107,13 +107,13 @@ impl TunnelConfig { let public_url = optional_env("TUNNEL_URL")? .or_else(|| settings.tunnel.public_url.clone().filter(|s| !s.is_empty())); - if let Some(ref url) = public_url { - if !url.starts_with("https://") { - return Err(ConfigError::InvalidValue { - key: "TUNNEL_URL".to_string(), - message: "must start with https:// (webhooks require HTTPS)".to_string(), - }); - } + if let Some(ref url) = public_url + && !url.starts_with("https://") + { + return Err(ConfigError::InvalidValue { + key: "TUNNEL_URL".to_string(), + message: "must start with https:// (webhooks require HTTPS)".to_string(), + }); } Ok(Self { public_url }) @@ -806,13 +806,13 @@ impl SecretsConfig { let enabled = master_key.is_some(); - if let Some(ref key) = master_key { - if key.expose_secret().len() < 32 { - return Err(ConfigError::InvalidValue { - key: "SECRETS_MASTER_KEY".to_string(), - message: "must be at least 32 bytes for AES-256-GCM".to_string(), - }); - } + if let Some(ref key) = master_key + && key.expose_secret().len() < 32 + { + return Err(ConfigError::InvalidValue { + key: "SECRETS_MASTER_KEY".to_string(), + message: "must be at least 32 bytes for AES-256-GCM".to_string(), + }); } Ok(Self { diff --git a/src/evaluation/success.rs b/src/evaluation/success.rs index 41e55bb8..31c78146 100644 --- a/src/evaluation/success.rs +++ b/src/evaluation/success.rs @@ -144,12 +144,11 @@ impl SuccessEvaluator for RuleBasedEvaluator { // Check for critical errors for action in actions.iter().filter(|a| !a.success) { - if let Some(ref error) = action.error { - if error.to_lowercase().contains("critical") - || error.to_lowercase().contains("fatal") - { - issues.push(format!("Critical error in {}: {}", action.tool_name, error)); - } + if let Some(ref error) = action.error + && (error.to_lowercase().contains("critical") + || error.to_lowercase().contains("fatal")) + { + issues.push(format!("Critical error in {}: {}", action.tool_name, error)); } } diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index d87b9605..d8ecefc6 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -490,13 +490,13 @@ impl ExtensionManager { } // Check Content-Length header before downloading the full body - if let Some(len) = response.content_length() { - if len as usize > MAX_WASM_SIZE { - return Err(ExtensionError::InstallFailed(format!( - "WASM binary too large ({} bytes, max {} bytes)", - len, MAX_WASM_SIZE - ))); - } + if let Some(len) = response.content_length() + && len as usize > MAX_WASM_SIZE + { + return Err(ExtensionError::InstallFailed(format!( + "WASM binary too large ({} bytes, max {} bytes)", + len, MAX_WASM_SIZE + ))); } let bytes = response @@ -766,27 +766,27 @@ impl ExtensionManager { }; // Check env var first - if let Some(ref env_var) = auth.env_var { - if let Ok(value) = std::env::var(env_var) { - // Store the env var value as a secret - let params = CreateSecretParams::new(&auth.secret_name, &value) - .with_provider(name.to_string()); - self.secrets - .create(&self.user_id, params) - .await - .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?; + if let Some(ref env_var) = auth.env_var + && let Ok(value) = std::env::var(env_var) + { + // Store the env var value as a secret + let params = + CreateSecretParams::new(&auth.secret_name, &value).with_provider(name.to_string()); + self.secrets + .create(&self.user_id, params) + .await + .map_err(|e| ExtensionError::AuthFailed(e.to_string()))?; - return Ok(AuthResult { - name: name.to_string(), - kind: ExtensionKind::WasmTool, - auth_url: None, - callback_type: None, - instructions: None, - setup_url: None, - awaiting_token: false, - status: "authenticated".to_string(), - }); - } + return Ok(AuthResult { + name: name.to_string(), + kind: ExtensionKind::WasmTool, + auth_url: None, + callback_type: None, + instructions: None, + setup_url: None, + awaiting_token: false, + status: "authenticated".to_string(), + }); } // Check if already authenticated diff --git a/src/llm/nearai.rs b/src/llm/nearai.rs index 193510bd..ed32d2fc 100644 --- a/src/llm/nearai.rs +++ b/src/llm/nearai.rs @@ -209,20 +209,20 @@ impl NearAiProvider { data: Option>, } - if let Ok(resp) = serde_json::from_str::(&response_text) { - if let Some(entries) = resp.models.or(resp.data) { - let models: Vec = entries - .into_iter() - .filter_map(|e| { - e.get_name().map(|name| ModelInfo { - name, - provider: None, - }) + if let Ok(resp) = serde_json::from_str::(&response_text) + && let Some(entries) = resp.models.or(resp.data) + { + let models: Vec = entries + .into_iter() + .filter_map(|e| { + e.get_name().map(|name| ModelInfo { + name, + provider: None, }) - .collect(); - if !models.is_empty() { - return Ok(models); - } + }) + .collect(); + if !models.is_empty() { + return Ok(models); } } @@ -694,21 +694,21 @@ impl LlmProvider for NearAiProvider { } } } - } else if item.item_type == "function_call" { - if let (Some(name), Some(call_id)) = (&item.name, &item.call_id) { - // Parse arguments JSON string into Value - let arguments = item - .arguments - .as_ref() - .and_then(|s| serde_json::from_str(s).ok()) - .unwrap_or(serde_json::Value::Object(Default::default())); + } else if item.item_type == "function_call" + && let (Some(name), Some(call_id)) = (&item.name, &item.call_id) + { + // Parse arguments JSON string into Value + let arguments = item + .arguments + .as_ref() + .and_then(|s| serde_json::from_str(s).ok()) + .unwrap_or(serde_json::Value::Object(Default::default())); - tool_calls.push(ToolCall { - id: call_id.clone(), - name: name.clone(), - arguments, - }); - } + tool_calls.push(ToolCall { + id: call_id.clone(), + name: name.clone(), + arguments, + }); } } diff --git a/src/llm/nearai_chat.rs b/src/llm/nearai_chat.rs index c51828bb..5ca5e95e 100644 --- a/src/llm/nearai_chat.rs +++ b/src/llm/nearai_chat.rs @@ -395,10 +395,10 @@ fn flatten_tool_messages(messages: Vec) -> Vec = Vec::new(); - if let Some(ref text) = msg.content { - if !text.is_empty() { - parts.push(text.clone()); - } + if let Some(ref text) = msg.content + && !text.is_empty() + { + parts.push(text.clone()); } for tc in calls { parts.push(format!( diff --git a/src/llm/reasoning.rs b/src/llm/reasoning.rs index 7cc511d8..41d4db1c 100644 --- a/src/llm/reasoning.rs +++ b/src/llm/reasoning.rs @@ -581,21 +581,20 @@ fn recover_tool_calls_from_content( } // Try JSON first: {"name":"x","arguments":{}} - if let Ok(parsed) = serde_json::from_str::(inner) { - if let Some(name) = parsed.get("name").and_then(|v| v.as_str()) { - if tool_names.contains(name) { - let arguments = parsed - .get("arguments") - .cloned() - .unwrap_or(serde_json::Value::Object(Default::default())); - calls.push(ToolCall { - id: format!("recovered_{}", calls.len()), - name: name.to_string(), - arguments, - }); - continue; - } - } + if let Ok(parsed) = serde_json::from_str::(inner) + && let Some(name) = parsed.get("name").and_then(|v| v.as_str()) + && tool_names.contains(name) + { + let arguments = parsed + .get("arguments") + .cloned() + .unwrap_or(serde_json::Value::Object(Default::default())); + calls.push(ToolCall { + id: format!("recovered_{}", calls.len()), + name: name.to_string(), + arguments, + }); + continue; } // Bare tool name (e.g. "tool_list") diff --git a/src/llm/session.rs b/src/llm/session.rs index 9b27b650..1350a5ac 100644 --- a/src/llm/session.rs +++ b/src/llm/session.rs @@ -83,16 +83,16 @@ impl SessionManager { }; // Try to load existing session synchronously during construction - if let Ok(data) = std::fs::read_to_string(&manager.config.session_path) { - if let Ok(session) = serde_json::from_str::(&data) { - // We can't await here, so we use try_write - if let Ok(mut guard) = manager.token.try_write() { - *guard = Some(SecretString::from(session.session_token)); - tracing::info!( - "Loaded session token from {}", - manager.config.session_path.display() - ); - } + if let Ok(data) = std::fs::read_to_string(&manager.config.session_path) + && let Ok(session) = serde_json::from_str::(&data) + { + // We can't await here, so we use try_write + if let Ok(mut guard) = manager.token.try_write() { + *guard = Some(SecretString::from(session.session_token)); + tracing::info!( + "Loaded session token from {}", + manager.config.session_path.display() + ); } } @@ -356,8 +356,8 @@ impl SessionManager { })?; // Parse GET /auth/callback?token=xxx&session_id=xxx&expires_at=xxx&is_new_user=xxx HTTP/1.1 - if let Some(path) = request_line.split_whitespace().nth(1) { - if path.starts_with("/auth/callback") { + if let Some(path) = request_line.split_whitespace().nth(1) + && path.starts_with("/auth/callback") { // Parse query parameters if let Some(query) = path.split('?').nth(1) { let mut token = None; @@ -453,7 +453,6 @@ impl SessionManager { } } } - } // Not the callback we're looking for, send 404 let response = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"; @@ -642,15 +641,14 @@ pub async fn create_session_manager(config: SessionConfig) -> Arc anyhow::Result<()> { let _ = dotenvy::dotenv(); // Enhanced first-run detection - if !cli.no_onboard { - if let Some(reason) = check_onboard_needed().await { - println!("Onboarding needed: {}", reason); - println!(); - let mut wizard = SetupWizard::new(); - wizard.run().await?; - } + if !cli.no_onboard + && let Some(reason) = check_onboard_needed().await + { + println!("Onboarding needed: {}", reason); + println!(); + let mut wizard = SetupWizard::new(); + wizard.run().await?; } // Load bootstrap config (4 fields that must live on disk) @@ -801,13 +801,13 @@ async fn main() -> anyhow::Result<()> { // Inject owner_id for Telegram so the bot only responds // to the bound user account. - if channel_name == "telegram" { - if let Some(owner_id) = config.channels.telegram_owner_id { - config_updates.insert( - "owner_id".to_string(), - serde_json::json!(owner_id), - ); - } + if channel_name == "telegram" + && let Some(owner_id) = config.channels.telegram_owner_id + { + config_updates.insert( + "owner_id".to_string(), + serde_json::json!(owner_id), + ); } if !config_updates.is_empty() { @@ -898,23 +898,23 @@ async fn main() -> anyhow::Result<()> { // Extract its routes for the unified server; the channel itself just // provides the mpsc stream. let mut webhook_server_addr: Option = None; - if !cli.cli_only { - if let Some(ref http_config) = config.channels.http { - let http_channel = HttpChannel::new(http_config.clone()); - webhook_routes.push(http_channel.routes()); - let (host, port) = http_channel.addr(); - webhook_server_addr = Some( - format!("{}:{}", host, port) - .parse() - .expect("HttpConfig host:port must be a valid SocketAddr"), - ); - channels.add(Box::new(http_channel)); - tracing::info!( - "HTTP channel enabled on {}:{}", - http_config.host, - http_config.port - ); - } + if !cli.cli_only + && let Some(ref http_config) = config.channels.http + { + let http_channel = HttpChannel::new(http_config.clone()); + webhook_routes.push(http_channel.routes()); + let (host, port) = http_channel.addr(); + webhook_server_addr = Some( + format!("{}:{}", host, port) + .parse() + .expect("HttpConfig host:port must be a valid SocketAddr"), + ); + channels.add(Box::new(http_channel)); + tracing::info!( + "HTTP channel enabled on {}:{}", + http_config.host, + http_config.port + ); } // Start the unified webhook server if any routes were registered. diff --git a/src/orchestrator/api.rs b/src/orchestrator/api.rs index b209403a..8923bc55 100644 --- a/src/orchestrator/api.rs +++ b/src/orchestrator/api.rs @@ -339,16 +339,16 @@ async fn get_prompt_handler( Path(job_id): Path, ) -> Result<(StatusCode, Json), StatusCode> { let mut queue = state.prompt_queue.lock().await; - if let Some(prompts) = queue.get_mut(&job_id) { - if let Some(prompt) = prompts.pop_front() { - return Ok(( - StatusCode::OK, - Json(serde_json::json!({ - "content": prompt.content, - "done": prompt.done, - })), - )); - } + if let Some(prompts) = queue.get_mut(&job_id) + && let Some(prompt) = prompts.pop_front() + { + return Ok(( + StatusCode::OK, + Json(serde_json::json!({ + "content": prompt.content, + "done": prompt.done, + })), + )); } // Return 204 with an empty body. The Json wrapper requires some value diff --git a/src/orchestrator/job_manager.rs b/src/orchestrator/job_manager.rs index 227af6a3..b1d20306 100644 --- a/src/orchestrator/job_manager.rs +++ b/src/orchestrator/job_manager.rs @@ -229,17 +229,17 @@ impl ContainerJobManager { .unwrap_or_else(|| PathBuf::from(".")) .join(".ironclaw") .join("projects"); - if let Ok(canonical_base) = projects_base.canonicalize() { - if !canonical.starts_with(&canonical_base) { - return Err(OrchestratorError::ContainerCreationFailed { - job_id, - reason: format!( - "project directory {} is outside allowed base {}", - canonical.display(), - canonical_base.display() - ), - }); - } + if let Ok(canonical_base) = projects_base.canonicalize() + && !canonical.starts_with(&canonical_base) + { + return Err(OrchestratorError::ContainerCreationFailed { + job_id, + reason: format!( + "project directory {} is outside allowed base {}", + canonical.display(), + canonical_base.display() + ), + }); } binds.push(format!("{}:/workspace:rw", canonical.display())); env_vec.push("IRONCLAW_WORKSPACE=/workspace".to_string()); @@ -442,36 +442,36 @@ impl ContainerJobManager { let containers = self.containers.read().await; containers.get(&job_id).map(|h| h.container_id.clone()) }; - if let Some(cid) = container_id { - if !cid.is_empty() { - match connect_docker().await { - Ok(docker) => { - if let Err(e) = docker - .stop_container( - &cid, - Some(bollard::container::StopContainerOptions { t: 5 }), - ) - .await - { - tracing::warn!(job_id = %job_id, error = %e, "Failed to stop completed container"); - } - if let Err(e) = docker - .remove_container( - &cid, - Some(bollard::container::RemoveContainerOptions { - force: true, - ..Default::default() - }), - ) - .await - { - tracing::warn!(job_id = %job_id, error = %e, "Failed to remove completed container"); - } + if let Some(cid) = container_id + && !cid.is_empty() + { + match connect_docker().await { + Ok(docker) => { + if let Err(e) = docker + .stop_container( + &cid, + Some(bollard::container::StopContainerOptions { t: 5 }), + ) + .await + { + tracing::warn!(job_id = %job_id, error = %e, "Failed to stop completed container"); } - Err(e) => { - tracing::warn!(job_id = %job_id, error = %e, "Failed to connect to Docker for container cleanup"); + if let Err(e) = docker + .remove_container( + &cid, + Some(bollard::container::RemoveContainerOptions { + force: true, + ..Default::default() + }), + ) + .await + { + tracing::warn!(job_id = %job_id, error = %e, "Failed to remove completed container"); } } + Err(e) => { + tracing::warn!(job_id = %job_id, error = %e, "Failed to connect to Docker for container cleanup"); + } } } self.token_store.revoke(job_id).await; diff --git a/src/safety/leak_detector.rs b/src/safety/leak_detector.rs index 85e83ad1..6ac6ae00 100644 --- a/src/safety/leak_detector.rs +++ b/src/safety/leak_detector.rs @@ -147,10 +147,10 @@ impl LeakDetector { // Build prefix matcher for patterns that start with a known prefix let mut prefixes = Vec::new(); for (idx, pattern) in patterns.iter().enumerate() { - if let Some(prefix) = extract_literal_prefix(pattern.regex.as_str()) { - if prefix.len() >= 3 { - prefixes.push((prefix, idx)); - } + if let Some(prefix) = extract_literal_prefix(pattern.regex.as_str()) + && prefix.len() >= 3 + { + prefixes.push((prefix, idx)); } } diff --git a/src/sandbox/container.rs b/src/sandbox/container.rs index da9e9145..1ef0fa0f 100644 --- a/src/sandbox/container.rs +++ b/src/sandbox/container.rs @@ -494,10 +494,10 @@ impl ContainerRunner { /// 3. `~/.docker/run/docker.sock` (Docker Desktop on macOS) pub async fn connect_docker() -> Result { // First try bollard defaults (checks DOCKER_HOST, then /var/run/docker.sock) - if let Ok(docker) = Docker::connect_with_local_defaults() { - if docker.ping().await.is_ok() { - return Ok(docker); - } + if let Ok(docker) = Docker::connect_with_local_defaults() + && docker.ping().await.is_ok() + { + return Ok(docker); } // Try Docker Desktop socket (macOS) @@ -507,10 +507,9 @@ pub async fn connect_docker() -> Result { let sock_str = desktop_sock.to_string_lossy(); if let Ok(docker) = Docker::connect_with_socket(&sock_str, 120, bollard::API_DEFAULT_VERSION) + && docker.ping().await.is_ok() { - if docker.ping().await.is_ok() { - return Ok(docker); - } + return Ok(docker); } } } diff --git a/src/sandbox/proxy/http.rs b/src/sandbox/proxy/http.rs index 6b0a3e82..3205841b 100644 --- a/src/sandbox/proxy/http.rs +++ b/src/sandbox/proxy/http.rs @@ -259,11 +259,11 @@ async fn handle_connect( let decision = state.decider.decide(&network_req).await; - if !decision.is_allowed() { - if let NetworkDecision::Deny { reason } = decision { - tracing::info!("Proxy: blocked CONNECT {} - {}", host, reason); - return error_response(StatusCode::FORBIDDEN, reason); - } + if !decision.is_allowed() + && let NetworkDecision::Deny { reason } = decision + { + tracing::info!("Proxy: blocked CONNECT {} - {}", host, reason); + return error_response(StatusCode::FORBIDDEN, reason); } tracing::debug!("Proxy: allowing CONNECT to {}", host); @@ -294,10 +294,10 @@ async fn forward_request( // Copy headers (except hop-by-hop headers) for (name, value) in req.headers() { - if !is_hop_by_hop_header(name.as_str()) { - if let Ok(v) = value.to_str() { - builder = builder.header(name.as_str(), v); - } + if !is_hop_by_hop_header(name.as_str()) + && let Ok(v) = value.to_str() + { + builder = builder.header(name.as_str(), v); } } diff --git a/src/sandbox/proxy/policy.rs b/src/sandbox/proxy/policy.rs index f3d967a0..2e2a45b9 100644 --- a/src/sandbox/proxy/policy.rs +++ b/src/sandbox/proxy/policy.rs @@ -109,12 +109,11 @@ impl NetworkPolicyDecider for DefaultPolicyDecider { async fn decide(&self, request: &NetworkRequest) -> NetworkDecision { // First check if the domain is allowed let validation = self.allowlist.is_allowed(&request.host); - if !validation.is_allowed() { - if let crate::sandbox::proxy::allowlist::DomainValidationResult::Denied(reason) = + if !validation.is_allowed() + && let crate::sandbox::proxy::allowlist::DomainValidationResult::Denied(reason) = validation - { - return NetworkDecision::Deny { reason }; - } + { + return NetworkDecision::Deny { reason }; } // Check if we need to inject credentials diff --git a/src/secrets/keychain.rs b/src/secrets/keychain.rs index 1b90a106..7dccc86a 100644 --- a/src/secrets/keychain.rs +++ b/src/secrets/keychain.rs @@ -261,7 +261,7 @@ pub use platform::{delete_master_key, get_master_key, has_master_key, store_mast /// Parse a hex string to bytes. fn hex_to_bytes(hex: &str) -> Result, SecretError> { - if hex.len() % 2 != 0 { + if !hex.len().is_multiple_of(2) { return Err(SecretError::KeychainError( "Invalid hex string length".to_string(), )); diff --git a/src/secrets/store.rs b/src/secrets/store.rs index 934b07bf..442bbcb9 100644 --- a/src/secrets/store.rs +++ b/src/secrets/store.rs @@ -149,10 +149,10 @@ impl SecretsStore for PostgresSecretsStore { let secret = row_to_secret(&r); // Check expiration - if let Some(expires_at) = secret.expires_at { - if expires_at < Utc::now() { - return Err(SecretError::Expired); - } + if let Some(expires_at) = secret.expires_at + && expires_at < Utc::now() + { + return Err(SecretError::Expired); } Ok(secret) @@ -272,10 +272,10 @@ impl SecretsStore for PostgresSecretsStore { } // Simple glob: * matches any suffix - if let Some(prefix) = pattern.strip_suffix('*') { - if secret_name.starts_with(prefix) { - return Ok(true); - } + if let Some(prefix) = pattern.strip_suffix('*') + && secret_name.starts_with(prefix) + { + return Ok(true); } } @@ -430,10 +430,10 @@ pub mod testing { if pattern == secret_name { return Ok(true); } - if let Some(prefix) = pattern.strip_suffix('*') { - if secret_name.starts_with(prefix) { - return Ok(true); - } + if let Some(prefix) = pattern.strip_suffix('*') + && secret_name.starts_with(prefix) + { + return Ok(true); } } Ok(false) diff --git a/src/setup/channels.rs b/src/setup/channels.rs index 7728ff0d..3de5cb23 100644 --- a/src/setup/channels.rs +++ b/src/setup/channels.rs @@ -252,32 +252,32 @@ async fn bind_telegram_owner(token: &SecretString) -> Result, String // Find the first message with a sender for update in &body.result { - if let Some(ref msg) = update.message { - if let Some(ref from) = msg.from { - let display_name = from - .username - .as_ref() - .map(|u| format!("@{}", u)) - .unwrap_or_else(|| from.first_name.clone()); + if let Some(ref msg) = update.message + && let Some(ref from) = msg.from + { + let display_name = from + .username + .as_ref() + .map(|u| format!("@{}", u)) + .unwrap_or_else(|| from.first_name.clone()); - print_success(&format!( - "Received message from {} (ID: {})", - display_name, from.id - )); + print_success(&format!( + "Received message from {} (ID: {})", + display_name, from.id + )); - // Acknowledge the update so it doesn't pile up - let ack_url = format!( - "https://api.telegram.org/bot{}/getUpdates", - token.expose_secret() - ); - let _ = client - .get(&ack_url) - .query(&[("offset", &(update.update_id + 1).to_string())]) - .send() - .await; + // Acknowledge the update so it doesn't pile up + let ack_url = format!( + "https://api.telegram.org/bot{}/getUpdates", + token.expose_secret() + ); + let _ = client + .get(&ack_url) + .query(&[("offset", &(update.update_id + 1).to_string())]) + .send() + .await; - return Ok(Some(from.id)); - } + return Ok(Some(from.id)); } } } diff --git a/src/setup/prompts.rs b/src/setup/prompts.rs index b07ae090..7d77c245 100644 --- a/src/setup/prompts.rs +++ b/src/setup/prompts.rs @@ -54,10 +54,11 @@ pub fn select_one(prompt: &str, options: &[&str]) -> io::Result { } // Parse number - if let Ok(num) = input.parse::() { - if num >= 1 && num <= options.len() { - return Ok(num - 1); - } + if let Ok(num) = input.parse::() + && num >= 1 + && num <= options.len() + { + return Ok(num - 1); } writeln!( diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index 8613992b..632d4272 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -344,17 +344,17 @@ impl SetupWizard { /// Step 3: NEAR AI authentication. async fn step_authentication(&mut self) -> Result<(), SetupError> { // Check if we already have a session - if let Some(ref session) = self.session_manager { - if session.has_token().await { - print_info("Existing session found. Validating..."); - match session.ensure_authenticated().await { - Ok(()) => { - print_success("Session valid"); - return Ok(()); - } - Err(e) => { - print_info(&format!("Session invalid: {}. Re-authenticating...", e)); - } + if let Some(ref session) = self.session_manager + && session.has_token().await + { + print_info("Existing session found. Validating..."); + match session.ensure_authenticated().await { + Ok(()) => { + print_success("Session valid"); + return Ok(()); + } + Err(e) => { + print_info(&format!("Session invalid: {}. Re-authenticating...", e)); } } } @@ -642,11 +642,10 @@ impl SetupWizard { &installed_names, ) .await? + && !installed.is_empty() { - if !installed.is_empty() { - print_success(&format!("Installed channels: {}", installed.join(", "))); - discovered_channels = discover_wasm_channels(&channels_dir).await; - } + print_success(&format!("Installed channels: {}", installed.join(", "))); + discovered_channels = discover_wasm_channels(&channels_dir).await; } // Determine if we need secrets context diff --git a/src/tools/builder/testing.rs b/src/tools/builder/testing.rs index 84fd4f3e..629e0b2d 100644 --- a/src/tools/builder/testing.rs +++ b/src/tools/builder/testing.rs @@ -326,20 +326,20 @@ impl TestHarness { } // Verify expected output - if let Some(ref expected) = test.expected_output { - if &actual != expected { - return TestResult { - name: test.name.clone(), - passed: false, - duration, - error: Some(format!( - "Output mismatch:\nExpected: {}\nActual: {}", - serde_json::to_string_pretty(expected).unwrap_or_default(), - serde_json::to_string_pretty(&actual).unwrap_or_default() - )), - actual_output: Some(actual), - }; - } + if let Some(ref expected) = test.expected_output + && &actual != expected + { + return TestResult { + name: test.name.clone(), + passed: false, + duration, + error: Some(format!( + "Output mismatch:\nExpected: {}\nActual: {}", + serde_json::to_string_pretty(expected).unwrap_or_default(), + serde_json::to_string_pretty(&actual).unwrap_or_default() + )), + actual_output: Some(actual), + }; } // Verify expected fields @@ -357,19 +357,19 @@ impl TestHarness { }; } - if let Some(ref expected_value) = field.value { - if field_value != Some(expected_value) { - return TestResult { - name: test.name.clone(), - passed: false, - duration, - error: Some(format!( - "Field '{}' mismatch: expected {:?}, got {:?}", - field.path, expected_value, field_value - )), - actual_output: Some(actual), - }; - } + if let Some(ref expected_value) = field.value + && field_value != Some(expected_value) + { + return TestResult { + name: test.name.clone(), + passed: false, + duration, + error: Some(format!( + "Field '{}' mismatch: expected {:?}, got {:?}", + field.path, expected_value, field_value + )), + actual_output: Some(actual), + }; } } } diff --git a/src/tools/builtin/http.rs b/src/tools/builtin/http.rs index c0ac14d0..3312eda5 100644 --- a/src/tools/builtin/http.rs +++ b/src/tools/builtin/http.rs @@ -54,12 +54,12 @@ fn validate_url(url: &str) -> Result { } // Check literal IP addresses - if let Ok(ip) = host.parse::() { - if is_disallowed_ip(&ip) { - return Err(ToolError::NotAuthorized( - "private or local IPs are not allowed".to_string(), - )); - } + if let Ok(ip) = host.parse::() + && is_disallowed_ip(&ip) + { + return Err(ToolError::NotAuthorized( + "private or local IPs are not allowed".to_string(), + )); } // Resolve hostname and check all resolved IPs against the blocklist. diff --git a/src/tools/builtin/job.rs b/src/tools/builtin/job.rs index 20fda2ad..86552c34 100644 --- a/src/tools/builtin/job.rs +++ b/src/tools/builtin/job.rs @@ -157,18 +157,18 @@ impl CreateJobTool { }); // Persist the job mode to DB - if mode == JobMode::ClaudeCode { - if let Some(store) = self.store.clone() { - let job_id_copy = job_id; - tokio::spawn(async move { - if let Err(e) = store - .update_sandbox_job_mode(job_id_copy, "claude_code") - .await - { - tracing::warn!(job_id = %job_id_copy, "Failed to set job mode: {}", e); - } - }); - } + if mode == JobMode::ClaudeCode + && let Some(store) = self.store.clone() + { + let job_id_copy = job_id; + tokio::spawn(async move { + if let Err(e) = store + .update_sandbox_job_mode(job_id_copy, "claude_code") + .await + { + tracing::warn!(job_id = %job_id_copy, "Failed to set job mode: {}", e); + } + }); } // Create the container job with the pre-determined job_id. diff --git a/src/tools/builtin/shell.rs b/src/tools/builtin/shell.rs index 9cd125d6..b8b948e4 100644 --- a/src/tools/builtin/shell.rs +++ b/src/tools/builtin/shell.rs @@ -343,12 +343,12 @@ impl ShellTool { // Use sandbox if configured; fail-closed (never silently fall through // to unsandboxed execution when sandbox was intended). - if let Some(ref sandbox) = self.sandbox { - if sandbox.is_initialized() || sandbox.config().enabled { - return self - .execute_sandboxed(sandbox, cmd, &cwd, timeout_duration) - .await; - } + if let Some(ref sandbox) = self.sandbox + && (sandbox.is_initialized() || sandbox.config().enabled) + { + return self + .execute_sandboxed(sandbox, cmd, &cwd, timeout_duration) + .await; } // Only execute directly when no sandbox was configured at all. diff --git a/src/tools/mcp/auth.rs b/src/tools/mcp/auth.rs index 20766dd1..5e3445b6 100644 --- a/src/tools/mcp/auth.rs +++ b/src/tools/mcp/auth.rs @@ -539,9 +539,9 @@ pub async fn wait_for_authorization_callback( .map_err(|e| AuthError::Http(e.to_string()))?; // Parse GET /callback?code=xxx HTTP/1.1 - if let Some(path) = request_line.split_whitespace().nth(1) { - if path.starts_with("/callback") { - if let Some(query) = path.split('?').nth(1) { + if let Some(path) = request_line.split_whitespace().nth(1) + && path.starts_with("/callback") + && let Some(query) = path.split('?').nth(1) { // Check for error first if query.contains("error=") { let response = "HTTP/1.1 400 Bad Request\r\n\r\nAuthorization denied"; @@ -578,8 +578,6 @@ pub async fn wait_for_authorization_callback( } } } - } - } let response = "HTTP/1.1 404 Not Found\r\n\r\n"; let _ = socket.write_all(response.as_bytes()).await; diff --git a/src/tools/mcp/client.rs b/src/tools/mcp/client.rs index e859f173..1da5879d 100644 --- a/src/tools/mcp/client.rs +++ b/src/tools/mcp/client.rs @@ -184,10 +184,10 @@ impl McpClient { } // Add Mcp-Session-Id header if we have a session - if let Some(ref session_manager) = self.session_manager { - if let Some(session_id) = session_manager.get_session_id(&self.server_name).await { - req_builder = req_builder.header("Mcp-Session-Id", session_id); - } + if let Some(ref session_manager) = self.session_manager + && let Some(session_id) = session_manager.get_session_id(&self.server_name).await + { + req_builder = req_builder.header("Mcp-Session-Id", session_id); } let response = req_builder @@ -199,29 +199,26 @@ impl McpClient { if response.status() == reqwest::StatusCode::UNAUTHORIZED { if attempt == 0 { // Try to refresh the token - if let Some(ref secrets) = self.secrets { - if let Some(ref config) = self.server_config { - tracing::debug!( - "MCP token expired, attempting refresh for '{}'", - self.server_name - ); - match refresh_access_token(config, secrets, &self.user_id).await { - Ok(_) => { - tracing::info!( - "MCP token refreshed for '{}'", - self.server_name - ); - // Continue to next iteration to retry with new token - continue; - } - Err(e) => { - tracing::debug!( - "Token refresh failed for '{}': {}", - self.server_name, - e - ); - // Fall through to return auth error - } + if let Some(ref secrets) = self.secrets + && let Some(ref config) = self.server_config + { + tracing::debug!( + "MCP token expired, attempting refresh for '{}'", + self.server_name + ); + match refresh_access_token(config, secrets, &self.user_id).await { + Ok(_) => { + tracing::info!("MCP token refreshed for '{}'", self.server_name); + // Continue to next iteration to retry with new token + continue; + } + Err(e) => { + tracing::debug!( + "Token refresh failed for '{}': {}", + self.server_name, + e + ); + // Fall through to return auth error } } } @@ -245,16 +242,15 @@ impl McpClient { /// Parse the HTTP response into an MCP response. async fn parse_response(&self, response: reqwest::Response) -> Result { // Extract session ID from response header - if let Some(ref session_manager) = self.session_manager { - if let Some(session_id) = response + if let Some(ref session_manager) = self.session_manager + && let Some(session_id) = response .headers() .get("Mcp-Session-Id") .and_then(|v| v.to_str().ok()) - { - session_manager - .update_session_id(&self.server_name, Some(session_id.to_string())) - .await; - } + { + session_manager + .update_session_id(&self.server_name, Some(session_id.to_string())) + .await; } if !response.status().is_success() { @@ -316,11 +312,11 @@ impl McpClient { /// This should be called once per session to establish capabilities. pub async fn initialize(&self) -> Result { // Check if already initialized - if let Some(ref session_manager) = self.session_manager { - if session_manager.is_initialized(&self.server_name).await { - // Return cached/default capabilities - return Ok(InitializeResult::default()); - } + if let Some(ref session_manager) = self.session_manager + && session_manager.is_initialized(&self.server_name).await + { + // Return cached/default capabilities + return Ok(InitializeResult::default()); } // Ensure we have a session diff --git a/src/tools/registry.rs b/src/tools/registry.rs index ffe84aa9..906e048d 100644 --- a/src/tools/registry.rs +++ b/src/tools/registry.rs @@ -96,10 +96,10 @@ impl ToolRegistry { if let Ok(mut tools) = self.tools.try_write() { tools.insert(name.clone(), tool); // Mark as built-in so it can't be shadowed later - if PROTECTED_TOOL_NAMES.contains(&name.as_str()) { - if let Ok(mut builtins) = self.builtin_names.try_write() { - builtins.insert(name.clone()); - } + if PROTECTED_TOOL_NAMES.contains(&name.as_str()) + && let Ok(mut builtins) = self.builtin_names.try_write() + { + builtins.insert(name.clone()); } tracing::debug!("Registered tool: {}", name); } diff --git a/src/tools/wasm/capabilities.rs b/src/tools/wasm/capabilities.rs index 0f4bd5a0..3b43d15c 100644 --- a/src/tools/wasm/capabilities.rs +++ b/src/tools/wasm/capabilities.rs @@ -209,10 +209,10 @@ impl EndpointPattern { } // Check path prefix - if let Some(ref prefix) = self.path_prefix { - if !url_path.starts_with(prefix) { - return false; - } + if let Some(ref prefix) = self.path_prefix + && !url_path.starts_with(prefix) + { + return false; } // Check method @@ -237,13 +237,14 @@ impl EndpointPattern { } // Support wildcard: *.example.com matches sub.example.com - if let Some(suffix) = self.host.strip_prefix("*.") { - if url_host.ends_with(suffix) && url_host.len() > suffix.len() { - // Ensure there's a dot before the suffix (or it's the whole thing) - let prefix = &url_host[..url_host.len() - suffix.len()]; - if prefix.ends_with('.') || prefix.is_empty() { - return true; - } + if let Some(suffix) = self.host.strip_prefix("*.") + && url_host.ends_with(suffix) + && url_host.len() > suffix.len() + { + // Ensure there's a dot before the suffix (or it's the whole thing) + let prefix = &url_host[..url_host.len() - suffix.len()]; + if prefix.ends_with('.') || prefix.is_empty() { + return true; } } @@ -291,10 +292,10 @@ impl SecretsCapability { if pattern == name { return true; } - if let Some(prefix) = pattern.strip_suffix('*') { - if name.starts_with(prefix) { - return true; - } + if let Some(prefix) = pattern.strip_suffix('*') + && name.starts_with(prefix) + { + return true; } } false diff --git a/src/tools/wasm/credential_injector.rs b/src/tools/wasm/credential_injector.rs index 0d878bbb..d2ffdaa8 100644 --- a/src/tools/wasm/credential_injector.rs +++ b/src/tools/wasm/credential_injector.rs @@ -158,10 +158,10 @@ impl CredentialInjector { if pattern == name { return true; } - if let Some(prefix) = pattern.strip_suffix('*') { - if name.starts_with(prefix) { - return true; - } + if let Some(prefix) = pattern.strip_suffix('*') + && name.starts_with(prefix) + { + return true; } } false @@ -214,12 +214,13 @@ fn host_matches_pattern(host: &str, pattern: &str) -> bool { } // Support wildcard: *.example.com matches sub.example.com - if let Some(suffix) = pattern.strip_prefix("*.") { - if host.ends_with(suffix) && host.len() > suffix.len() { - let prefix = &host[..host.len() - suffix.len()]; - if prefix.ends_with('.') || prefix.is_empty() { - return true; - } + if let Some(suffix) = pattern.strip_prefix("*.") + && host.ends_with(suffix) + && host.len() > suffix.len() + { + let prefix = &host[..host.len() - suffix.len()]; + if prefix.ends_with('.') || prefix.is_empty() { + return true; } } diff --git a/src/tools/wasm/wrapper.rs b/src/tools/wasm/wrapper.rs index 65050be0..8e516eb6 100644 --- a/src/tools/wasm/wrapper.rs +++ b/src/tools/wasm/wrapper.rs @@ -259,13 +259,13 @@ impl near::agent::host::Host for StoreData { // Check Content-Length header for early rejection of oversized responses. let max_response = max_response_bytes; - if let Some(cl) = response.content_length() { - if cl as usize > max_response { - return Err(format!( - "Response body too large: {} bytes exceeds limit of {} bytes", - cl, max_response - )); - } + if let Some(cl) = response.content_length() + && cl as usize > max_response + { + return Err(format!( + "Response body too large: {} bytes exceeds limit of {} bytes", + cl, max_response + )); } // Read body with a size cap to prevent memory exhaustion. diff --git a/src/worker/claude_bridge.rs b/src/worker/claude_bridge.rs index 102d43f1..1fbc1410 100644 --- a/src/worker/claude_bridge.rs +++ b/src/worker/claude_bridge.rs @@ -326,15 +326,15 @@ impl ClaudeBridgeRuntime { match serde_json::from_str::(&line) { Ok(event) => { // Capture session_id from system init - if event.event_type == "system" { - if let Some(ref sid) = event.session_id { - session_id = Some(sid.clone()); - tracing::info!( - job_id = %self.config.job_id, - session_id = %sid, - "Captured Claude session ID" - ); - } + if event.event_type == "system" + && let Some(ref sid) = event.session_id + { + session_id = Some(sid.clone()); + tracing::info!( + job_id = %self.config.job_id, + session_id = %sid, + "Captured Claude session ID" + ); } // Convert to our event payload and forward diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 548b5718..d0f05d9c 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -333,10 +333,10 @@ impl Workspace { ]; for (path, header) in identity_files { - if let Ok(doc) = self.read(path).await { - if !doc.content.is_empty() { - parts.push(format!("{}\n\n{}", header, doc.content)); - } + if let Ok(doc) = self.read(path).await + && !doc.content.is_empty() + { + parts.push(format!("{}\n\n{}", header, doc.content)); } } @@ -345,15 +345,15 @@ impl Workspace { let yesterday = today.pred_opt().unwrap_or(today); for date in [today, yesterday] { - if let Ok(doc) = self.daily_log(date).await { - if !doc.content.is_empty() { - let header = if date == today { - "## Today's Notes" - } else { - "## Yesterday's Notes" - }; - parts.push(format!("{}\n\n{}", header, doc.content)); - } + if let Ok(doc) = self.daily_log(date).await + && !doc.content.is_empty() + { + let header = if date == today { + "## Today's Notes" + } else { + "## Yesterday's Notes" + }; + parts.push(format!("{}\n\n{}", header, doc.content)); } } diff --git a/src/workspace/search.rs b/src/workspace/search.rs index 7cee5539..c9bf058d 100644 --- a/src/workspace/search.rs +++ b/src/workspace/search.rs @@ -201,11 +201,11 @@ pub fn reciprocal_rank_fusion( .collect(); // Normalize scores to 0-1 range - if let Some(max_score) = results.iter().map(|r| r.score).reduce(f32::max) { - if max_score > 0.0 { - for result in &mut results { - result.score /= max_score; - } + if let Some(max_score) = results.iter().map(|r| r.score).reduce(f32::max) + && max_score > 0.0 + { + for result in &mut results { + result.score /= max_score; } } diff --git a/tests/openai_compat_integration.rs b/tests/openai_compat_integration.rs index d0927a2c..f5d97fc5 100644 --- a/tests/openai_compat_integration.rs +++ b/tests/openai_compat_integration.rs @@ -302,10 +302,10 @@ async fn test_chat_completions_streaming() { if data == "[DONE]" { continue; } - if let Ok(chunk) = serde_json::from_str::(data) { - if let Some(content) = chunk["choices"][0]["delta"]["content"].as_str() { - full_content.push_str(content); - } + if let Ok(chunk) = serde_json::from_str::(data) + && let Some(content) = chunk["choices"][0]["delta"]["content"].as_str() + { + full_content.push_str(content); } } }