Bump MSRV to 1.92, add GCP deployment files (#40)

* Bump MSRV to 1.92 and add GCP deployment files

rig-core 0.30 uses let_chains (stabilized post-1.87), which breaks
builds on Rust 1.85. Bump rust-version in Cargo.toml and both
Dockerfiles to 1.92 (verified working).

Add cloud deployment scaffolding:
- Dockerfile: multi-stage build for the main agent container
- deploy/cloud-sql-proxy.service: systemd unit for Cloud SQL Auth Proxy
- deploy/ironclaw.service: systemd unit for the IronClaw container
- deploy/setup.sh: VM bootstrap script (Docker, proxy, services)
- deploy/env.example: reference environment configuration

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Address review feedback: harden deploy scaffolding

- Add comment explaining GATEWAY_HOST=0.0.0.0 and when to use 127.0.0.1
- Document /opt/ironclaw ownership model (root-owned, Docker reads as root)
- Switch cloud-sql-proxy service from User=root to DynamicUser=yes

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Resolve clippy lints (Rust 1.93) and fix CI test workflow

- Fix 97 collapsible_if warnings using let-chains syntax (auto-fixed)
- Fix ptr_arg: change &PathBuf to &Path in pairing store functions
- Fix suspicious_open_options: add .truncate(false) to OpenOptions
- Fix too_many_arguments: add clippy allow on execute_status
- Fix unnecessary_unwrap: use if-let in repository.rs hybrid_search
- Gate unused EchoTool with #[cfg(test)]
- Add PairingStore argument to ChannelStoreData::new() test call sites
- Add skip guard for bundled channel test when WASM artifacts unavailable
- Split CI test workflow to exclude PostgreSQL-dependent integration tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Address review feedback from ilblackdragon

- Add root check to setup.sh (exits with error if not root)
- Add warning comment to env.example about placeholder passwords
- Dockerfile.worker already uses rust:1.92 (no change needed)
- PR #41 overlap noted; will rebase after #41 merges

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: resolve 47 collapsible_if clippy warnings

Collapse nested if statements across the codebase to satisfy
clippy::collapsible_if on Rust 1.93.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Zaki Manian
2026-02-13 22:21:50 +04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent bbb68f7490
commit 5df0d13b59
53 changed files with 1055 additions and 909 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
name = "ironclaw" name = "ironclaw"
version = "0.1.3" version = "0.1.3"
edition = "2024" 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" description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
authors = ["NEAR AI <[email protected]>"] authors = ["NEAR AI <[email protected]>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+46
View File
@@ -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"]
+2 -2
View File
@@ -9,7 +9,7 @@
# The image includes common development tools so workers can build software, # The image includes common development tools so workers can build software,
# run tests, and execute shell commands. # run tests, and execute shell commands.
FROM rust:1.85-bookworm AS builder FROM rust:1.92-bookworm AS builder
WORKDIR /build WORKDIR /build
COPY . . COPY . .
@@ -40,7 +40,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV RUSTUP_HOME=/usr/local/rustup \ ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \ CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH 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 && chmod -R a+r /usr/local/rustup /usr/local/cargo
# Install Claude Code CLI (for claude-bridge mode) # Install Claude Code CLI (for claude-bridge mode)
+13
View File
@@ -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
+27
View File
@@ -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
+20
View File
@@ -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
+68
View File
@@ -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"
+41 -50
View File
@@ -654,9 +654,9 @@ impl Agent {
} }
// Restore response chain from conversation metadata // Restore response chain from conversation metadata
if let Some(store) = self.store() { if let Some(store) = self.store()
if let Ok(Some(metadata)) = store.get_conversation_metadata(thread_uuid).await { && let Ok(Some(metadata)) = store.get_conversation_metadata(thread_uuid).await
if let Some(rid) = metadata && let Some(rid) = metadata
.get("last_response_id") .get("last_response_id")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(String::from) .map(String::from)
@@ -666,8 +666,6 @@ impl Agent {
.seed_response_chain(&thread_uuid.to_string(), rid); .seed_response_chain(&thread_uuid.to_string(), rid);
tracing::debug!("Restored response chain for thread {}", thread_uuid); tracing::debug!("Restored response chain for thread {}", thread_uuid);
} }
}
}
// Insert into session and register with session manager // Insert into session and register with session manager
{ {
@@ -954,14 +952,13 @@ impl Agent {
return; return;
} }
if let Some(ref resp) = response { if let Some(ref resp) = response
if let Err(e) = store && let Err(e) = store
.add_conversation_message(thread_id, "assistant", resp) .add_conversation_message(thread_id, "assistant", resp)
.await .await
{ {
tracing::warn!("Failed to persist assistant message: {}", e); tracing::warn!("Failed to persist assistant message: {}", e);
} }
}
}); });
} }
@@ -1058,8 +1055,9 @@ impl Agent {
// Check if interrupted // Check if interrupted
{ {
let sess = session.lock().await; let sess = session.lock().await;
if let Some(thread) = sess.threads.get(&thread_id) { if let Some(thread) = sess.threads.get(&thread_id)
if thread.state == ThreadState::Interrupted { && thread.state == ThreadState::Interrupted
{
return Err(crate::error::JobError::ContextError { return Err(crate::error::JobError::ContextError {
id: thread_id, id: thread_id,
reason: "Interrupted".to_string(), reason: "Interrupted".to_string(),
@@ -1067,7 +1065,6 @@ impl Agent {
.into()); .into());
} }
} }
}
// Refresh tool definitions each iteration so newly built tools become visible // Refresh tool definitions each iteration so newly built tools become visible
let tool_defs = self.tools().tool_definitions().await; let tool_defs = self.tools().tool_definitions().await;
@@ -1140,20 +1137,21 @@ impl Agent {
// Record tool calls in the thread // Record tool calls in the thread
{ {
let mut sess = session.lock().await; let mut sess = session.lock().await;
if let Some(thread) = sess.threads.get_mut(&thread_id) { if let Some(thread) = sess.threads.get_mut(&thread_id)
if let Some(turn) = thread.last_turn_mut() { && let Some(turn) = thread.last_turn_mut()
{
for tc in &tool_calls { for tc in &tool_calls {
turn.record_tool_call(&tc.name, tc.arguments.clone()); turn.record_tool_call(&tc.name, tc.arguments.clone());
} }
} }
} }
}
// Execute each tool (with approval checking) // Execute each tool (with approval checking)
for tc in tool_calls { for tc in tool_calls {
// Check if tool requires approval // Check if tool requires approval
if let Some(tool) = self.tools().get(&tc.name).await { if let Some(tool) = self.tools().get(&tc.name).await
if tool.requires_approval() { && tool.requires_approval()
{
// Check if auto-approved for this session // Check if auto-approved for this session
let mut is_auto_approved = { let mut is_auto_approved = {
let sess = session.lock().await; let sess = session.lock().await;
@@ -1163,29 +1161,23 @@ impl Agent {
// For shell commands, override auto-approval for // For shell commands, override auto-approval for
// destructive patterns that should always require // destructive patterns that should always require
// explicit per-invocation approval. // explicit per-invocation approval.
if is_auto_approved && tc.name == "shell" { if is_auto_approved
if let Some(cmd) = tc && tc.name == "shell"
&& let Some(cmd) = tc
.arguments .arguments
.as_str() .as_str()
.and_then(|s| { .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
serde_json::from_str::<serde_json::Value>(s).ok()
})
.and_then(|v| { .and_then(|v| {
v.get("command") v.get("command").and_then(|c| c.as_str().map(String::from))
.and_then(|c| c.as_str().map(String::from))
}) })
&& crate::tools::builtin::shell::requires_explicit_approval(&cmd)
{ {
if crate::tools::builtin::shell::requires_explicit_approval(
&cmd,
) {
tracing::info!( tracing::info!(
"Shell command '{}' requires explicit approval despite auto-approve", "Shell command '{}' requires explicit approval despite auto-approve",
cmd.chars().take(80).collect::<String>() cmd.chars().take(80).collect::<String>()
); );
is_auto_approved = false; is_auto_approved = false;
} }
}
}
if !is_auto_approved { if !is_auto_approved {
// Need approval - store pending request and return // Need approval - store pending request and return
@@ -1201,7 +1193,6 @@ impl Agent {
return Ok(AgenticLoopResult::NeedApproval { pending }); return Ok(AgenticLoopResult::NeedApproval { pending });
} }
} }
}
let _ = self let _ = self
.channels .channels
@@ -1230,8 +1221,9 @@ impl Agent {
) )
.await; .await;
if let Ok(ref output) = tool_result { if let Ok(ref output) = tool_result
if !output.is_empty() { && !output.is_empty()
{
let _ = self let _ = self
.channels .channels
.send_status( .send_status(
@@ -1244,13 +1236,13 @@ impl Agent {
) )
.await; .await;
} }
}
// Record result in thread // Record result in thread
{ {
let mut sess = session.lock().await; let mut sess = session.lock().await;
if let Some(thread) = sess.threads.get_mut(&thread_id) { if let Some(thread) = sess.threads.get_mut(&thread_id)
if let Some(turn) = thread.last_turn_mut() { && let Some(turn) = thread.last_turn_mut()
{
match &tool_result { match &tool_result {
Ok(output) => { Ok(output) => {
turn.record_tool_result(serde_json::json!(output)); turn.record_tool_result(serde_json::json!(output));
@@ -1261,7 +1253,6 @@ impl Agent {
} }
} }
} }
}
// If tool_auth returned awaiting_token, enter auth mode // If tool_auth returned awaiting_token, enter auth mode
// and short-circuit: return the instructions directly so // and short-circuit: return the instructions directly so
@@ -1640,8 +1631,9 @@ impl Agent {
}; };
// Verify request ID if provided // Verify request ID if provided
if let Some(req_id) = request_id { if let Some(req_id) = request_id
if req_id != pending.request_id { && req_id != pending.request_id
{
// Put it back and return error // Put it back and return error
let mut sess = session.lock().await; let mut sess = session.lock().await;
if let Some(thread) = sess.threads.get_mut(&thread_id) { if let Some(thread) = sess.threads.get_mut(&thread_id) {
@@ -1651,7 +1643,6 @@ impl Agent {
"Request ID mismatch. Use the correct request ID.", "Request ID mismatch. Use the correct request ID.",
)); ));
} }
}
if approved { if approved {
// If always, add to auto-approved set // If always, add to auto-approved set
@@ -1704,8 +1695,9 @@ impl Agent {
) )
.await; .await;
if let Ok(ref output) = tool_result { if let Ok(ref output) = tool_result
if !output.is_empty() { && !output.is_empty()
{
let _ = self let _ = self
.channels .channels
.send_status( .send_status(
@@ -1718,7 +1710,6 @@ impl Agent {
) )
.await; .await;
} }
}
// Build context including the tool result // Build context including the tool result
let mut context_messages = pending.context_messages; let mut context_messages = pending.context_messages;
@@ -1726,8 +1717,9 @@ impl Agent {
// Record result in thread // Record result in thread
{ {
let mut sess = session.lock().await; let mut sess = session.lock().await;
if let Some(thread) = sess.threads.get_mut(&thread_id) { if let Some(thread) = sess.threads.get_mut(&thread_id)
if let Some(turn) = thread.last_turn_mut() { && let Some(turn) = thread.last_turn_mut()
{
match &tool_result { match &tool_result {
Ok(output) => { Ok(output) => {
turn.record_tool_result(serde_json::json!(output)); turn.record_tool_result(serde_json::json!(output));
@@ -1738,7 +1730,6 @@ impl Agent {
} }
} }
} }
}
// If tool_auth returned awaiting_token, enter auth mode and // If tool_auth returned awaiting_token, enter auth mode and
// return instructions directly (skip agentic loop continuation). // return instructions directly (skip agentic loop continuation).
@@ -2094,8 +2085,9 @@ impl Agent {
} }
// Persist new job to database (fire-and-forget) // Persist new job to database (fire-and-forget)
if let Some(store) = self.store() { if let Some(store) = self.store()
if let Ok(ctx) = self.context_manager.get_context(job_id).await { && let Ok(ctx) = self.context_manager.get_context(job_id).await
{
let store = store.clone(); let store = store.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = store.save_job(&ctx).await { if let Err(e) = store.save_job(&ctx).await {
@@ -2103,7 +2095,6 @@ impl Agent {
} }
}); });
} }
}
// Schedule for execution // Schedule for execution
self.scheduler.schedule(job_id).await?; self.scheduler.schedule(job_id).await?;
@@ -2182,12 +2173,12 @@ impl Agent {
let mut output = String::from("Jobs:\n"); let mut output = String::from("Jobs:\n");
for job_id in jobs { for job_id in jobs {
if let Ok(ctx) = self.context_manager.get_context(job_id).await { if let Ok(ctx) = self.context_manager.get_context(job_id).await
if ctx.user_id == user_id { && ctx.user_id == user_id
{
output.push_str(&format!(" {} - {} ({:?})\n", job_id, ctx.title, ctx.state)); output.push_str(&format!(" {} - {} ({:?})\n", job_id, ctx.title, ctx.state));
} }
} }
}
Ok(output) Ok(output)
} }
+1 -2
View File
@@ -103,11 +103,10 @@ impl RoutineEngine {
if let Trigger::Event { if let Trigger::Event {
channel: Some(ch), .. channel: Some(ch), ..
} = &routine.trigger } = &routine.trigger
&& ch != &message.channel
{ {
if ch != &message.channel {
continue; continue;
} }
}
// Regex match // Regex match
if !re.is_match(&message.content) { if !re.is_match(&message.content) {
+3 -3
View File
@@ -119,8 +119,9 @@ impl SelfRepair for DefaultSelfRepair {
let mut stuck_jobs = Vec::new(); let mut stuck_jobs = Vec::new();
for job_id in stuck_ids { for job_id in stuck_ids {
if let Ok(ctx) = self.context_manager.get_context(job_id).await { if let Ok(ctx) = self.context_manager.get_context(job_id).await
if ctx.state == JobState::Stuck { && ctx.state == JobState::Stuck
{
let stuck_duration = ctx let stuck_duration = ctx
.started_at .started_at
.map(|start| { .map(|start| {
@@ -139,7 +140,6 @@ impl SelfRepair for DefaultSelfRepair {
}); });
} }
} }
}
stuck_jobs stuck_jobs
} }
+3 -3
View File
@@ -346,12 +346,12 @@ impl Thread {
let mut turn = Turn::new(turn_number, &msg.content); let mut turn = Turn::new(turn_number, &msg.content);
// Check if next is assistant response // Check if next is assistant response
if let Some(next) = iter.peek() { if let Some(next) = iter.peek()
if next.role == crate::llm::Role::Assistant { && next.role == crate::llm::Role::Assistant
{
let response = iter.next().expect("peeked"); let response = iter.next().expect("peeked");
turn.complete(&response.content); turn.complete(&response.content);
} }
}
self.turns.push(turn); self.turns.push(turn);
turn_number += 1; turn_number += 1;
+3 -3
View File
@@ -199,13 +199,13 @@ impl SessionManager {
{ {
let sessions = self.sessions.read().await; let sessions = self.sessions.read().await;
for user_id in &stale_users { for user_id in &stale_users {
if let Some(session) = sessions.get(user_id) { if let Some(session) = sessions.get(user_id)
if let Ok(sess) = session.try_lock() { && let Ok(sess) = session.try_lock()
{
stale_thread_ids.extend(sess.threads.keys()); stale_thread_ids.extend(sess.threads.keys());
} }
} }
} }
}
// Remove sessions // Remove sessions
let count = { let count = {
+10 -11
View File
@@ -93,28 +93,27 @@ impl SubmissionParser {
// /thread <uuid> - switch thread // /thread <uuid> - switch thread
if let Some(rest) = lower.strip_prefix("/thread ") { if let Some(rest) = lower.strip_prefix("/thread ") {
let rest = rest.trim(); let rest = rest.trim();
if rest != "new" { if rest != "new"
if let Ok(id) = Uuid::parse_str(rest) { && let Ok(id) = Uuid::parse_str(rest)
{
return Submission::SwitchThread { thread_id: id }; return Submission::SwitchThread { thread_id: id };
} }
} }
}
// /resume <uuid> - resume from checkpoint // /resume <uuid> - resume from checkpoint
if let Some(rest) = lower.strip_prefix("/resume ") { if let Some(rest) = lower.strip_prefix("/resume ")
if let Ok(id) = Uuid::parse_str(rest.trim()) { && let Ok(id) = Uuid::parse_str(rest.trim())
{
return Submission::Resume { checkpoint_id: id }; return Submission::Resume { checkpoint_id: id };
} }
}
// Try structured JSON approval (from web gateway's /api/chat/approval endpoint) // Try structured JSON approval (from web gateway's /api/chat/approval endpoint)
if trimmed.starts_with('{') { if trimmed.starts_with('{')
if let Ok(submission) = serde_json::from_str::<Submission>(trimmed) { && let Ok(submission) = serde_json::from_str::<Submission>(trimmed)
if matches!(submission, Submission::ExecApproval { .. }) { && matches!(submission, Submission::ExecApproval { .. })
{
return submission; return submission;
} }
}
}
// Approval responses (simple yes/no/always for pending approvals) // Approval responses (simple yes/no/always for pending approvals)
// These are short enough to check explicitly // These are short enough to check explicitly
+3 -3
View File
@@ -227,12 +227,12 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
} }
// Check for cancellation // Check for cancellation
if let Ok(ctx) = self.context_manager().get_context(self.job_id).await { if let Ok(ctx) = self.context_manager().get_context(self.job_id).await
if ctx.state == JobState::Cancelled { && ctx.state == JobState::Cancelled
{
tracing::info!("Worker for job {} detected cancellation", self.job_id); tracing::info!("Worker for job {} detected cancellation", self.job_id);
return Ok(()); return Ok(());
} }
}
iteration += 1; iteration += 1;
if iteration > max_iterations { if iteration > max_iterations {
+11 -12
View File
@@ -134,15 +134,15 @@ impl ChannelStoreData {
if result.contains('{') && result.contains('}') { if result.contains('{') && result.contains('}') {
// Only warn if it looks like an unresolved placeholder (not JSON braces) // Only warn if it looks like an unresolved placeholder (not JSON braces)
let brace_pattern = regex::Regex::new(r"\{[A-Z_]+\}").ok(); let brace_pattern = regex::Regex::new(r"\{[A-Z_]+\}").ok();
if let Some(re) = brace_pattern { if let Some(re) = brace_pattern
if re.is_match(&result) { && re.is_match(&result)
{
tracing::warn!( tracing::warn!(
context = %context, context = %context,
"String may contain unresolved credential placeholders" "String may contain unresolved credential placeholders"
); );
} }
} }
}
result result
} }
@@ -338,14 +338,14 @@ impl near::agent::channel_host::Host for ChannelStoreData {
// Enforce max response body size to prevent memory exhaustion. // Enforce max response body size to prevent memory exhaustion.
let max_response = max_response_bytes; let max_response = max_response_bytes;
if let Some(cl) = response.content_length() { if let Some(cl) = response.content_length()
if cl as usize > max_response { && cl as usize > max_response
{
return Err(format!( return Err(format!(
"Response body too large: {} bytes exceeds limit of {} bytes", "Response body too large: {} bytes exceeds limit of {} bytes",
cl, max_response cl, max_response
)); ));
} }
}
let body = response let body = response
.bytes() .bytes()
.await .await
@@ -1495,8 +1495,8 @@ impl WasmChannel {
match result { match result {
Ok(emitted_messages) => { Ok(emitted_messages) => {
// Process any emitted messages // Process any emitted messages
if !emitted_messages.is_empty() { if !emitted_messages.is_empty()
if let Err(e) = Self::dispatch_emitted_messages( && let Err(e) = Self::dispatch_emitted_messages(
&channel_name, &channel_name,
emitted_messages, emitted_messages,
&message_tx, &message_tx,
@@ -1509,7 +1509,6 @@ impl WasmChannel {
); );
} }
} }
}
Err(e) => { Err(e) => {
tracing::warn!( tracing::warn!(
channel = %channel_name, channel = %channel_name,
@@ -1738,8 +1737,9 @@ impl Channel for WasmChannel {
*self.endpoints.write().await = endpoints; *self.endpoints.write().await = endpoints;
// Start polling if configured // Start polling if configured
if let Some(poll_config) = &config.poll { if let Some(poll_config) = &config.poll
if poll_config.enabled { && poll_config.enabled
{
let interval = self let interval = self
.capabilities .capabilities
.validate_poll_interval(poll_config.interval_ms) .validate_poll_interval(poll_config.interval_ms)
@@ -1754,7 +1754,6 @@ impl Channel for WasmChannel {
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!( tracing::info!(
channel = %self.name, channel = %self.name,
+8 -10
View File
@@ -25,26 +25,24 @@ pub async fn auth_middleware(
next: Next, next: Next,
) -> Response { ) -> Response {
// Try Authorization header first (constant-time comparison) // Try Authorization header first (constant-time comparison)
if let Some(auth_header) = headers.get("authorization") { if let Some(auth_header) = headers.get("authorization")
if let Ok(value) = auth_header.to_str() { && let Ok(value) = auth_header.to_str()
if let Some(token) = value.strip_prefix("Bearer ") { && let Some(token) = value.strip_prefix("Bearer ")
if bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) { && bool::from(token.as_bytes().ct_eq(auth.token.as_bytes()))
{
return next.run(request).await; return next.run(request).await;
} }
}
}
}
// Fall back to query parameter for SSE EventSource (constant-time comparison) // Fall back to query parameter for SSE EventSource (constant-time comparison)
if let Some(query) = request.uri().query() { if let Some(query) = request.uri().query() {
for pair in query.split('&') { for pair in query.split('&') {
if let Some(token) = pair.strip_prefix("token=") { if let Some(token) = pair.strip_prefix("token=")
if bool::from(token.as_bytes().ct_eq(auth.token.as_bytes())) { && bool::from(token.as_bytes().ct_eq(auth.token.as_bytes()))
{
return next.run(request).await; return next.run(request).await;
} }
} }
} }
}
(StatusCode::UNAUTHORIZED, "Invalid or missing auth token").into_response() (StatusCode::UNAUTHORIZED, "Invalid or missing auth token").into_response()
} }
+6 -6
View File
@@ -473,11 +473,11 @@ pub async fn chat_completions_handler(
if let Some(mt) = req.max_tokens { if let Some(mt) = req.max_tokens {
tool_req = tool_req.with_max_tokens(mt); tool_req = tool_req.with_max_tokens(mt);
} }
if let Some(ref tc) = req.tool_choice { if let Some(ref tc) = req.tool_choice
if let Some(choice) = normalize_tool_choice(tc) { && let Some(choice) = normalize_tool_choice(tc)
{
tool_req = tool_req.with_tool_choice(choice); tool_req = tool_req.with_tool_choice(choice);
} }
}
let resp = llm let resp = llm
.complete_with_tools(tool_req) .complete_with_tools(tool_req)
@@ -591,11 +591,11 @@ async fn handle_streaming(
if let Some(mt) = req.max_tokens { if let Some(mt) = req.max_tokens {
tool_req = tool_req.with_max_tokens(mt); tool_req = tool_req.with_max_tokens(mt);
} }
if let Some(ref tc) = req.tool_choice { if let Some(ref tc) = req.tool_choice
if let Some(choice) = normalize_tool_choice(tc) { && let Some(choice) = normalize_tool_choice(tc)
{
tool_req = tool_req.with_tool_choice(choice); tool_req = tool_req.with_tool_choice(choice);
} }
}
LlmResult::WithTools( LlmResult::WithTools(
llm.complete_with_tools(tool_req) llm.complete_with_tools(tool_req)
.await .await
+26 -27
View File
@@ -525,13 +525,13 @@ pub async fn clear_auth_mode(state: &GatewayState) {
if let Some(ref sm) = state.session_manager { if let Some(ref sm) = state.session_manager {
let session = sm.get_or_create_session(&state.user_id).await; let session = sm.get_or_create_session(&state.user_id).await;
let mut sess = session.lock().await; let mut sess = session.lock().await;
if let Some(thread_id) = sess.active_thread { if let Some(thread_id) = sess.active_thread
if let Some(thread) = sess.threads.get_mut(&thread_id) { && let Some(thread) = sess.threads.get_mut(&thread_id)
{
thread.pending_auth = None; thread.pending_auth = None;
} }
} }
} }
}
async fn chat_events_handler( async fn chat_events_handler(
State(state): State<Arc<GatewayState>>, State(state): State<Arc<GatewayState>>,
@@ -626,8 +626,9 @@ async fn chat_history_handler(
// Verify the thread belongs to the authenticated user before returning any data. // 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 // 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. // lookups could expose another user's conversation if the UUID is guessed.
if query.thread_id.is_some() { if query.thread_id.is_some()
if let Some(ref store) = state.store { && let Some(ref store) = state.store
{
let owned = store let owned = store
.conversation_belongs_to_user(thread_id, &state.user_id) .conversation_belongs_to_user(thread_id, &state.user_id)
.await .await
@@ -636,11 +637,11 @@ async fn chat_history_handler(
return Err((StatusCode::NOT_FOUND, "Thread not found".to_string())); return Err((StatusCode::NOT_FOUND, "Thread not found".to_string()));
} }
} }
}
// For paginated requests (before cursor set), always go to DB // For paginated requests (before cursor set), always go to DB
if before_cursor.is_some() { if before_cursor.is_some()
if let Some(ref store) = state.store { && let Some(ref store) = state.store
{
let (messages, has_more) = store let (messages, has_more) = store
.list_conversation_messages_paginated(thread_id, before_cursor, limit as i64) .list_conversation_messages_paginated(thread_id, before_cursor, limit as i64)
.await .await
@@ -655,11 +656,11 @@ async fn chat_history_handler(
oldest_timestamp, oldest_timestamp,
})); }));
} }
}
// Try in-memory first (freshest data for active threads) // Try in-memory first (freshest data for active threads)
if let Some(thread) = sess.threads.get(&thread_id) { if let Some(thread) = sess.threads.get(&thread_id)
if !thread.turns.is_empty() { && !thread.turns.is_empty()
{
let turns: Vec<TurnInfo> = thread let turns: Vec<TurnInfo> = thread
.turns .turns
.iter() .iter()
@@ -689,7 +690,6 @@ async fn chat_history_handler(
oldest_timestamp: None, oldest_timestamp: None,
})); }));
} }
}
// Fall back to DB for historical threads not in memory (paginated) // Fall back to DB for historical threads not in memory (paginated)
if let Some(ref store) = state.store { if let Some(ref store) = state.store {
@@ -738,13 +738,13 @@ fn build_turns_from_db_messages(messages: &[crate::history::ConversationMessage]
}; };
// Check if next message is an assistant response // Check if next message is an assistant response
if let Some(next) = iter.peek() { if let Some(next) = iter.peek()
if next.role == "assistant" { && next.role == "assistant"
{
let assistant_msg = iter.next().expect("peeked"); let assistant_msg = iter.next().expect("peeked");
turn.response = Some(assistant_msg.content.clone()); turn.response = Some(assistant_msg.content.clone());
turn.completed_at = Some(assistant_msg.created_at.to_rfc3339()); turn.completed_at = Some(assistant_msg.created_at.to_rfc3339());
} }
}
// Incomplete turn (user message without response) // Incomplete turn (user message without response)
if turn.response.is_none() { if turn.response.is_none() {
@@ -1126,8 +1126,9 @@ async fn jobs_detail_handler(
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?;
// Try sandbox job from DB first, scoped to the authenticated user. // Try sandbox job from DB first, scoped to the authenticated user.
if let Some(ref store) = state.store { if let Some(ref store) = state.store
if let Ok(Some(job)) = store.get_sandbox_job(job_id).await { && let Ok(Some(job)) = store.get_sandbox_job(job_id).await
{
if job.user_id != state.user_id { if job.user_id != state.user_id {
return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); return Err((StatusCode::NOT_FOUND, "Job not found".to_string()));
} }
@@ -1185,7 +1186,6 @@ async fn jobs_detail_handler(
transitions, transitions,
})); }));
} }
}
Err((StatusCode::NOT_FOUND, "Job not found".to_string())) Err((StatusCode::NOT_FOUND, "Job not found".to_string()))
} }
@@ -1198,18 +1198,19 @@ async fn jobs_cancel_handler(
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?;
// Try sandbox job cancellation, scoped to the authenticated user. // Try sandbox job cancellation, scoped to the authenticated user.
if let Some(ref store) = state.store { if let Some(ref store) = state.store
if let Ok(Some(job)) = store.get_sandbox_job(job_id).await { && let Ok(Some(job)) = store.get_sandbox_job(job_id).await
{
if job.user_id != state.user_id { if job.user_id != state.user_id {
return Err((StatusCode::NOT_FOUND, "Job not found".to_string())); return Err((StatusCode::NOT_FOUND, "Job not found".to_string()));
} }
if job.status == "running" || job.status == "creating" { if job.status == "running" || job.status == "creating" {
// Stop the container if we have a job manager. // Stop the container if we have a job manager.
if let Some(ref jm) = state.job_manager { if let Some(ref jm) = state.job_manager
if let Err(e) = jm.stop_job(job_id).await { && let Err(e) = jm.stop_job(job_id).await
{
tracing::warn!(job_id = %job_id, error = %e, "Failed to stop container during cancellation"); tracing::warn!(job_id = %job_id, error = %e, "Failed to stop container during cancellation");
} }
}
store store
.update_sandbox_job_status( .update_sandbox_job_status(
job_id, job_id,
@@ -1227,7 +1228,6 @@ async fn jobs_cancel_handler(
"job_id": job_id, "job_id": job_id,
}))); })));
} }
}
Err((StatusCode::NOT_FOUND, "Job not found".to_string())) Err((StatusCode::NOT_FOUND, "Job not found".to_string()))
} }
@@ -1334,15 +1334,14 @@ async fn jobs_prompt_handler(
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?; .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid job ID".to_string()))?;
// Verify user owns this job. // Verify user owns this job.
if let Some(ref store) = state.store { if let Some(ref store) = state.store
if !store && !store
.sandbox_job_belongs_to_user(job_id, &state.user_id) .sandbox_job_belongs_to_user(job_id, &state.user_id)
.await .await
.unwrap_or(false) .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 let content = body
.get("content") .get("content")
+3 -3
View File
@@ -107,11 +107,11 @@ async fn list_settings(
println!(); println!();
for (key, value) in all { for (key, value) in all {
if let Some(ref f) = filter { if let Some(ref f) = filter
if !key.starts_with(f) { && !key.starts_with(f)
{
continue; continue;
} }
}
let display_value = if value.len() > 60 { let display_value = if value.len() > 60 {
format!("{}...", &value[..57]) format!("{}...", &value[..57])
+28 -31
View File
@@ -420,13 +420,13 @@ async fn extract_crate_name(cargo_toml: &Path) -> anyhow::Result<String> {
// Simple TOML parsing for [package] name // Simple TOML parsing for [package] name
for line in content.lines() { for line in content.lines() {
let line = line.trim(); let line = line.trim();
if line.starts_with("name") { if line.starts_with("name")
if let Some((_, value)) = line.split_once('=') { && let Some((_, value)) = line.split_once('=')
{
let name = value.trim().trim_matches('"').trim_matches('\''); let name = value.trim().trim_matches('"').trim_matches('\'');
return Ok(name.to_string()); return Ok(name.to_string());
} }
} }
}
anyhow::bail!( anyhow::bail!(
"Could not extract package name from {}", "Could not extract package name from {}",
@@ -488,12 +488,12 @@ async fn list_tools(dir: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
if has_caps { if has_caps {
let caps_path = path.with_extension("capabilities.json"); let caps_path = path.with_extension("capabilities.json");
if let Ok(content) = fs::read_to_string(&caps_path).await { if let Ok(content) = fs::read_to_string(&caps_path).await
if let Ok(caps) = CapabilitiesFile::from_json(&content) { && let Ok(caps) = CapabilitiesFile::from_json(&content)
{
print_capabilities_summary(&caps); print_capabilities_summary(&caps);
} }
} }
}
println!(); println!();
} else { } else {
let caps_indicator = if has_caps { "" } else { "" }; let caps_indicator = if has_caps { "" } else { "" };
@@ -604,17 +604,17 @@ fn print_capabilities_summary(caps: &CapabilitiesFile) {
} }
} }
if let Some(ref secrets) = caps.secrets { if let Some(ref secrets) = caps.secrets
if !secrets.allowed_names.is_empty() { && !secrets.allowed_names.is_empty()
{
parts.push(format!("secrets: {}", secrets.allowed_names.len())); parts.push(format!("secrets: {}", secrets.allowed_names.len()));
} }
}
if let Some(ref ws) = caps.workspace { if let Some(ref ws) = caps.workspace
if !ws.allowed_prefixes.is_empty() { && !ws.allowed_prefixes.is_empty()
{
parts.push("workspace: read".to_string()); parts.push("workspace: read".to_string());
} }
}
if !parts.is_empty() { if !parts.is_empty() {
println!(" Perms: {}", parts.join(", ")); println!(" Perms: {}", parts.join(", "));
@@ -650,33 +650,33 @@ fn print_capabilities_detail(caps: &CapabilitiesFile) {
} }
} }
if let Some(ref secrets) = caps.secrets { if let Some(ref secrets) = caps.secrets
if !secrets.allowed_names.is_empty() { && !secrets.allowed_names.is_empty()
{
println!(" Secrets (existence check only):"); println!(" Secrets (existence check only):");
for name in &secrets.allowed_names { for name in &secrets.allowed_names {
println!(" {}", name); println!(" {}", name);
} }
} }
}
if let Some(ref tool_invoke) = caps.tool_invoke { if let Some(ref tool_invoke) = caps.tool_invoke
if !tool_invoke.aliases.is_empty() { && !tool_invoke.aliases.is_empty()
{
println!(" Tool aliases:"); println!(" Tool aliases:");
for (alias, real_name) in &tool_invoke.aliases { for (alias, real_name) in &tool_invoke.aliases {
println!(" {} -> {}", alias, real_name); println!(" {} -> {}", alias, real_name);
} }
} }
}
if let Some(ref ws) = caps.workspace { if let Some(ref ws) = caps.workspace
if !ws.allowed_prefixes.is_empty() { && !ws.allowed_prefixes.is_empty()
{
println!(" Workspace read prefixes:"); println!(" Workspace read prefixes:");
for prefix in &ws.allowed_prefixes { for prefix in &ws.allowed_prefixes {
println!(" {}", prefix); println!(" {}", prefix);
} }
} }
} }
}
/// Configure authentication for a tool. /// Configure authentication for a tool.
async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyhow::Result<()> { async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyhow::Result<()> {
@@ -752,9 +752,10 @@ async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyho
} }
// Check for environment variable // Check for environment variable
if let Some(ref env_var) = auth.env_var { if let Some(ref env_var) = auth.env_var
if let Ok(token) = std::env::var(env_var) { && let Ok(token) = std::env::var(env_var)
if !token.is_empty() { && !token.is_empty()
{
println!(" Found {} in environment.", env_var); println!(" Found {} in environment.", env_var);
println!(); println!();
@@ -782,8 +783,6 @@ async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyho
print_success(display_name); print_success(display_name);
return Ok(()); return Ok(());
} }
}
}
// Check for OAuth configuration // Check for OAuth configuration
if let Some(ref oauth) = auth.oauth { if let Some(ref oauth) = auth.oauth {
@@ -923,9 +922,9 @@ async fn auth_tool_oauth(
reader.read_line(&mut request_line).await?; reader.read_line(&mut request_line).await?;
// Parse GET /callback?code=xxx HTTP/1.1 // Parse GET /callback?code=xxx HTTP/1.1
if let Some(path) = request_line.split_whitespace().nth(1) { if let Some(path) = request_line.split_whitespace().nth(1)
if path.starts_with("/callback") { && path.starts_with("/callback")
if let Some(query) = path.split('?').nth(1) { && let Some(query) = path.split('?').nth(1) {
for param in query.split('&') { for param in query.split('&') {
let parts: Vec<&str> = param.splitn(2, '=').collect(); let parts: Vec<&str> = param.splitn(2, '=').collect();
if parts.len() == 2 && parts[0] == "code" { if parts.len() == 2 && parts[0] == "code" {
@@ -962,8 +961,6 @@ async fn auth_tool_oauth(
return Err(anyhow::anyhow!("Authorization denied by user")); return Err(anyhow::anyhow!("Authorization denied by user"));
} }
} }
}
}
let response = "HTTP/1.1 404 Not Found\r\n\r\n"; let response = "HTTP/1.1 404 Not Found\r\n\r\n";
let _ = socket.write_all(response.as_bytes()).await; let _ = socket.write_all(response.as_bytes()).await;
+6 -6
View File
@@ -107,14 +107,14 @@ impl TunnelConfig {
let public_url = optional_env("TUNNEL_URL")? let public_url = optional_env("TUNNEL_URL")?
.or_else(|| settings.tunnel.public_url.clone().filter(|s| !s.is_empty())); .or_else(|| settings.tunnel.public_url.clone().filter(|s| !s.is_empty()));
if let Some(ref url) = public_url { if let Some(ref url) = public_url
if !url.starts_with("https://") { && !url.starts_with("https://")
{
return Err(ConfigError::InvalidValue { return Err(ConfigError::InvalidValue {
key: "TUNNEL_URL".to_string(), key: "TUNNEL_URL".to_string(),
message: "must start with https:// (webhooks require HTTPS)".to_string(), message: "must start with https:// (webhooks require HTTPS)".to_string(),
}); });
} }
}
Ok(Self { public_url }) Ok(Self { public_url })
} }
@@ -806,14 +806,14 @@ impl SecretsConfig {
let enabled = master_key.is_some(); let enabled = master_key.is_some();
if let Some(ref key) = master_key { if let Some(ref key) = master_key
if key.expose_secret().len() < 32 { && key.expose_secret().len() < 32
{
return Err(ConfigError::InvalidValue { return Err(ConfigError::InvalidValue {
key: "SECRETS_MASTER_KEY".to_string(), key: "SECRETS_MASTER_KEY".to_string(),
message: "must be at least 32 bytes for AES-256-GCM".to_string(), message: "must be at least 32 bytes for AES-256-GCM".to_string(),
}); });
} }
}
Ok(Self { Ok(Self {
master_key, master_key,
+3 -4
View File
@@ -144,14 +144,13 @@ impl SuccessEvaluator for RuleBasedEvaluator {
// Check for critical errors // Check for critical errors
for action in actions.iter().filter(|a| !a.success) { for action in actions.iter().filter(|a| !a.success) {
if let Some(ref error) = action.error { if let Some(ref error) = action.error
if error.to_lowercase().contains("critical") && (error.to_lowercase().contains("critical")
|| error.to_lowercase().contains("fatal") || error.to_lowercase().contains("fatal"))
{ {
issues.push(format!("Critical error in {}: {}", action.tool_name, error)); issues.push(format!("Critical error in {}: {}", action.tool_name, error));
} }
} }
}
// Check job state // Check job state
if job.state != crate::context::JobState::Completed if job.state != crate::context::JobState::Completed
+8 -8
View File
@@ -490,14 +490,14 @@ impl ExtensionManager {
} }
// Check Content-Length header before downloading the full body // Check Content-Length header before downloading the full body
if let Some(len) = response.content_length() { if let Some(len) = response.content_length()
if len as usize > MAX_WASM_SIZE { && len as usize > MAX_WASM_SIZE
{
return Err(ExtensionError::InstallFailed(format!( return Err(ExtensionError::InstallFailed(format!(
"WASM binary too large ({} bytes, max {} bytes)", "WASM binary too large ({} bytes, max {} bytes)",
len, MAX_WASM_SIZE len, MAX_WASM_SIZE
))); )));
} }
}
let bytes = response let bytes = response
.bytes() .bytes()
@@ -766,11 +766,12 @@ impl ExtensionManager {
}; };
// Check env var first // Check env var first
if let Some(ref env_var) = auth.env_var { if let Some(ref env_var) = auth.env_var
if let Ok(value) = std::env::var(env_var) { && let Ok(value) = std::env::var(env_var)
{
// Store the env var value as a secret // Store the env var value as a secret
let params = CreateSecretParams::new(&auth.secret_name, &value) let params =
.with_provider(name.to_string()); CreateSecretParams::new(&auth.secret_name, &value).with_provider(name.to_string());
self.secrets self.secrets
.create(&self.user_id, params) .create(&self.user_id, params)
.await .await
@@ -787,7 +788,6 @@ impl ExtensionManager {
status: "authenticated".to_string(), status: "authenticated".to_string(),
}); });
} }
}
// Check if already authenticated // Check if already authenticated
if self if self
+6 -6
View File
@@ -209,8 +209,9 @@ impl NearAiProvider {
data: Option<Vec<ModelEntry>>, data: Option<Vec<ModelEntry>>,
} }
if let Ok(resp) = serde_json::from_str::<ModelsResponse>(&response_text) { if let Ok(resp) = serde_json::from_str::<ModelsResponse>(&response_text)
if let Some(entries) = resp.models.or(resp.data) { && let Some(entries) = resp.models.or(resp.data)
{
let models: Vec<ModelInfo> = entries let models: Vec<ModelInfo> = entries
.into_iter() .into_iter()
.filter_map(|e| { .filter_map(|e| {
@@ -224,7 +225,6 @@ impl NearAiProvider {
return Ok(models); return Ok(models);
} }
} }
}
// Try direct array format // Try direct array format
if let Ok(entries) = serde_json::from_str::<Vec<ModelEntry>>(&response_text) { if let Ok(entries) = serde_json::from_str::<Vec<ModelEntry>>(&response_text) {
@@ -694,8 +694,9 @@ impl LlmProvider for NearAiProvider {
} }
} }
} }
} else if item.item_type == "function_call" { } else if item.item_type == "function_call"
if let (Some(name), Some(call_id)) = (&item.name, &item.call_id) { && let (Some(name), Some(call_id)) = (&item.name, &item.call_id)
{
// Parse arguments JSON string into Value // Parse arguments JSON string into Value
let arguments = item let arguments = item
.arguments .arguments
@@ -710,7 +711,6 @@ impl LlmProvider for NearAiProvider {
}); });
} }
} }
}
let finish_reason = if tool_calls.is_empty() { let finish_reason = if tool_calls.is_empty() {
FinishReason::Stop FinishReason::Stop
+3 -3
View File
@@ -395,11 +395,11 @@ fn flatten_tool_messages(messages: Vec<ChatCompletionMessage>) -> Vec<ChatComple
if let (true, Some(calls)) = (msg.role == "assistant", &msg.tool_calls) { if let (true, Some(calls)) = (msg.role == "assistant", &msg.tool_calls) {
// Convert assistant tool_calls into descriptive text // Convert assistant tool_calls into descriptive text
let mut parts: Vec<String> = Vec::new(); let mut parts: Vec<String> = Vec::new();
if let Some(ref text) = msg.content { if let Some(ref text) = msg.content
if !text.is_empty() { && !text.is_empty()
{
parts.push(text.clone()); parts.push(text.clone());
} }
}
for tc in calls { for tc in calls {
parts.push(format!( parts.push(format!(
"[Called tool `{}` with arguments: {}]", "[Called tool `{}` with arguments: {}]",
+4 -5
View File
@@ -581,9 +581,10 @@ fn recover_tool_calls_from_content(
} }
// Try JSON first: {"name":"x","arguments":{}} // Try JSON first: {"name":"x","arguments":{}}
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(inner) { if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(inner)
if let Some(name) = parsed.get("name").and_then(|v| v.as_str()) { && let Some(name) = parsed.get("name").and_then(|v| v.as_str())
if tool_names.contains(name) { && tool_names.contains(name)
{
let arguments = parsed let arguments = parsed
.get("arguments") .get("arguments")
.cloned() .cloned()
@@ -595,8 +596,6 @@ fn recover_tool_calls_from_content(
}); });
continue; continue;
} }
}
}
// Bare tool name (e.g. "<tool_call>tool_list</tool_call>") // Bare tool name (e.g. "<tool_call>tool_list</tool_call>")
let name = inner.trim(); let name = inner.trim();
+9 -11
View File
@@ -83,8 +83,9 @@ impl SessionManager {
}; };
// Try to load existing session synchronously during construction // Try to load existing session synchronously during construction
if let Ok(data) = std::fs::read_to_string(&manager.config.session_path) { if let Ok(data) = std::fs::read_to_string(&manager.config.session_path)
if let Ok(session) = serde_json::from_str::<SessionData>(&data) { && let Ok(session) = serde_json::from_str::<SessionData>(&data)
{
// We can't await here, so we use try_write // We can't await here, so we use try_write
if let Ok(mut guard) = manager.token.try_write() { if let Ok(mut guard) = manager.token.try_write() {
*guard = Some(SecretString::from(session.session_token)); *guard = Some(SecretString::from(session.session_token));
@@ -94,7 +95,6 @@ impl SessionManager {
); );
} }
} }
}
manager manager
} }
@@ -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 // 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 let Some(path) = request_line.split_whitespace().nth(1)
if path.starts_with("/auth/callback") { && path.starts_with("/auth/callback") {
// Parse query parameters // Parse query parameters
if let Some(query) = path.split('?').nth(1) { if let Some(query) = path.split('?').nth(1) {
let mut token = None; let mut token = None;
@@ -453,7 +453,6 @@ impl SessionManager {
} }
} }
} }
}
// Not the callback we're looking for, send 404 // Not the callback we're looking for, send 404
let response = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"; let response = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n";
@@ -642,17 +641,16 @@ pub async fn create_session_manager(config: SessionConfig) -> Arc<SessionManager
let manager = SessionManager::new_async(config).await; let manager = SessionManager::new_async(config).await;
// Check for legacy env var and migrate if present and no file token // Check for legacy env var and migrate if present and no file token
if !manager.has_token().await { if !manager.has_token().await
if let Ok(token) = std::env::var("NEARAI_SESSION_TOKEN") { && let Ok(token) = std::env::var("NEARAI_SESSION_TOKEN")
if !token.is_empty() { && !token.is_empty()
{
tracing::info!("Migrating session token from NEARAI_SESSION_TOKEN env var to file"); tracing::info!("Migrating session token from NEARAI_SESSION_TOKEN env var to file");
manager.set_token(SecretString::from(token.clone())).await; manager.set_token(SecretString::from(token.clone())).await;
if let Err(e) = manager.save_session(&token, None).await { if let Err(e) = manager.save_session(&token, None).await {
tracing::warn!("Failed to save migrated session: {}", e); tracing::warn!("Failed to save migrated session: {}", e);
} }
} }
}
}
Arc::new(manager) Arc::new(manager)
} }
+9 -9
View File
@@ -255,14 +255,14 @@ async fn main() -> anyhow::Result<()> {
let _ = dotenvy::dotenv(); let _ = dotenvy::dotenv();
// Enhanced first-run detection // Enhanced first-run detection
if !cli.no_onboard { if !cli.no_onboard
if let Some(reason) = check_onboard_needed().await { && let Some(reason) = check_onboard_needed().await
{
println!("Onboarding needed: {}", reason); println!("Onboarding needed: {}", reason);
println!(); println!();
let mut wizard = SetupWizard::new(); let mut wizard = SetupWizard::new();
wizard.run().await?; wizard.run().await?;
} }
}
// Load bootstrap config (4 fields that must live on disk) // Load bootstrap config (4 fields that must live on disk)
let bootstrap = ironclaw::bootstrap::BootstrapConfig::load(); let bootstrap = ironclaw::bootstrap::BootstrapConfig::load();
@@ -801,14 +801,14 @@ async fn main() -> anyhow::Result<()> {
// Inject owner_id for Telegram so the bot only responds // Inject owner_id for Telegram so the bot only responds
// to the bound user account. // to the bound user account.
if channel_name == "telegram" { if channel_name == "telegram"
if let Some(owner_id) = config.channels.telegram_owner_id { && let Some(owner_id) = config.channels.telegram_owner_id
{
config_updates.insert( config_updates.insert(
"owner_id".to_string(), "owner_id".to_string(),
serde_json::json!(owner_id), serde_json::json!(owner_id),
); );
} }
}
if !config_updates.is_empty() { if !config_updates.is_empty() {
channel_arc.update_config(config_updates).await; channel_arc.update_config(config_updates).await;
@@ -898,8 +898,9 @@ async fn main() -> anyhow::Result<()> {
// Extract its routes for the unified server; the channel itself just // Extract its routes for the unified server; the channel itself just
// provides the mpsc stream. // provides the mpsc stream.
let mut webhook_server_addr: Option<std::net::SocketAddr> = None; let mut webhook_server_addr: Option<std::net::SocketAddr> = None;
if !cli.cli_only { if !cli.cli_only
if let Some(ref http_config) = config.channels.http { && let Some(ref http_config) = config.channels.http
{
let http_channel = HttpChannel::new(http_config.clone()); let http_channel = HttpChannel::new(http_config.clone());
webhook_routes.push(http_channel.routes()); webhook_routes.push(http_channel.routes());
let (host, port) = http_channel.addr(); let (host, port) = http_channel.addr();
@@ -915,7 +916,6 @@ async fn main() -> anyhow::Result<()> {
http_config.port http_config.port
); );
} }
}
// Start the unified webhook server if any routes were registered. // Start the unified webhook server if any routes were registered.
let mut webhook_server = if !webhook_routes.is_empty() { let mut webhook_server = if !webhook_routes.is_empty() {
+3 -3
View File
@@ -339,8 +339,9 @@ async fn get_prompt_handler(
Path(job_id): Path<Uuid>, Path(job_id): Path<Uuid>,
) -> Result<(StatusCode, Json<serde_json::Value>), StatusCode> { ) -> Result<(StatusCode, Json<serde_json::Value>), StatusCode> {
let mut queue = state.prompt_queue.lock().await; let mut queue = state.prompt_queue.lock().await;
if let Some(prompts) = queue.get_mut(&job_id) { if let Some(prompts) = queue.get_mut(&job_id)
if let Some(prompt) = prompts.pop_front() { && let Some(prompt) = prompts.pop_front()
{
return Ok(( return Ok((
StatusCode::OK, StatusCode::OK,
Json(serde_json::json!({ Json(serde_json::json!({
@@ -349,7 +350,6 @@ async fn get_prompt_handler(
})), })),
)); ));
} }
}
// Return 204 with an empty body. The Json wrapper requires some value // Return 204 with an empty body. The Json wrapper requires some value
// but the status code signals "nothing here". // but the status code signals "nothing here".
+6 -6
View File
@@ -229,8 +229,9 @@ impl ContainerJobManager {
.unwrap_or_else(|| PathBuf::from(".")) .unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw") .join(".ironclaw")
.join("projects"); .join("projects");
if let Ok(canonical_base) = projects_base.canonicalize() { if let Ok(canonical_base) = projects_base.canonicalize()
if !canonical.starts_with(&canonical_base) { && !canonical.starts_with(&canonical_base)
{
return Err(OrchestratorError::ContainerCreationFailed { return Err(OrchestratorError::ContainerCreationFailed {
job_id, job_id,
reason: format!( reason: format!(
@@ -240,7 +241,6 @@ impl ContainerJobManager {
), ),
}); });
} }
}
binds.push(format!("{}:/workspace:rw", canonical.display())); binds.push(format!("{}:/workspace:rw", canonical.display()));
env_vec.push("IRONCLAW_WORKSPACE=/workspace".to_string()); env_vec.push("IRONCLAW_WORKSPACE=/workspace".to_string());
} }
@@ -442,8 +442,9 @@ impl ContainerJobManager {
let containers = self.containers.read().await; let containers = self.containers.read().await;
containers.get(&job_id).map(|h| h.container_id.clone()) containers.get(&job_id).map(|h| h.container_id.clone())
}; };
if let Some(cid) = container_id { if let Some(cid) = container_id
if !cid.is_empty() { && !cid.is_empty()
{
match connect_docker().await { match connect_docker().await {
Ok(docker) => { Ok(docker) => {
if let Err(e) = docker if let Err(e) = docker
@@ -473,7 +474,6 @@ impl ContainerJobManager {
} }
} }
} }
}
self.token_store.revoke(job_id).await; self.token_store.revoke(job_id).await;
tracing::info!(job_id = %job_id, "Completed worker container"); tracing::info!(job_id = %job_id, "Completed worker container");
+3 -3
View File
@@ -147,12 +147,12 @@ impl LeakDetector {
// Build prefix matcher for patterns that start with a known prefix // Build prefix matcher for patterns that start with a known prefix
let mut prefixes = Vec::new(); let mut prefixes = Vec::new();
for (idx, pattern) in patterns.iter().enumerate() { for (idx, pattern) in patterns.iter().enumerate() {
if let Some(prefix) = extract_literal_prefix(pattern.regex.as_str()) { if let Some(prefix) = extract_literal_prefix(pattern.regex.as_str())
if prefix.len() >= 3 { && prefix.len() >= 3
{
prefixes.push((prefix, idx)); prefixes.push((prefix, idx));
} }
} }
}
let prefix_matcher = if !prefixes.is_empty() { let prefix_matcher = if !prefixes.is_empty() {
let prefix_strings: Vec<&str> = prefixes.iter().map(|(s, _)| s.as_str()).collect(); let prefix_strings: Vec<&str> = prefixes.iter().map(|(s, _)| s.as_str()).collect();
+4 -5
View File
@@ -494,11 +494,11 @@ impl ContainerRunner {
/// 3. `~/.docker/run/docker.sock` (Docker Desktop on macOS) /// 3. `~/.docker/run/docker.sock` (Docker Desktop on macOS)
pub async fn connect_docker() -> Result<Docker> { pub async fn connect_docker() -> Result<Docker> {
// First try bollard defaults (checks DOCKER_HOST, then /var/run/docker.sock) // First try bollard defaults (checks DOCKER_HOST, then /var/run/docker.sock)
if let Ok(docker) = Docker::connect_with_local_defaults() { if let Ok(docker) = Docker::connect_with_local_defaults()
if docker.ping().await.is_ok() { && docker.ping().await.is_ok()
{
return Ok(docker); return Ok(docker);
} }
}
// Try Docker Desktop socket (macOS) // Try Docker Desktop socket (macOS)
if let Some(home) = std::env::var_os("HOME") { if let Some(home) = std::env::var_os("HOME") {
@@ -507,13 +507,12 @@ pub async fn connect_docker() -> Result<Docker> {
let sock_str = desktop_sock.to_string_lossy(); let sock_str = desktop_sock.to_string_lossy();
if let Ok(docker) = if let Ok(docker) =
Docker::connect_with_socket(&sock_str, 120, bollard::API_DEFAULT_VERSION) 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);
} }
} }
} }
}
Err(SandboxError::DockerNotAvailable { Err(SandboxError::DockerNotAvailable {
reason: "Socket not found: /var/run/docker.sock".to_string(), reason: "Socket not found: /var/run/docker.sock".to_string(),
+6 -6
View File
@@ -259,12 +259,12 @@ async fn handle_connect(
let decision = state.decider.decide(&network_req).await; let decision = state.decider.decide(&network_req).await;
if !decision.is_allowed() { if !decision.is_allowed()
if let NetworkDecision::Deny { reason } = decision { && let NetworkDecision::Deny { reason } = decision
{
tracing::info!("Proxy: blocked CONNECT {} - {}", host, reason); tracing::info!("Proxy: blocked CONNECT {} - {}", host, reason);
return error_response(StatusCode::FORBIDDEN, reason); return error_response(StatusCode::FORBIDDEN, reason);
} }
}
tracing::debug!("Proxy: allowing CONNECT to {}", host); tracing::debug!("Proxy: allowing CONNECT to {}", host);
@@ -294,12 +294,12 @@ async fn forward_request(
// Copy headers (except hop-by-hop headers) // Copy headers (except hop-by-hop headers)
for (name, value) in req.headers() { for (name, value) in req.headers() {
if !is_hop_by_hop_header(name.as_str()) { if !is_hop_by_hop_header(name.as_str())
if let Ok(v) = value.to_str() { && let Ok(v) = value.to_str()
{
builder = builder.header(name.as_str(), v); builder = builder.header(name.as_str(), v);
} }
} }
}
// Inject credentials if needed // Inject credentials if needed
if let NetworkDecision::AllowWithCredentials { if let NetworkDecision::AllowWithCredentials {
+2 -3
View File
@@ -109,13 +109,12 @@ impl NetworkPolicyDecider for DefaultPolicyDecider {
async fn decide(&self, request: &NetworkRequest) -> NetworkDecision { async fn decide(&self, request: &NetworkRequest) -> NetworkDecision {
// First check if the domain is allowed // First check if the domain is allowed
let validation = self.allowlist.is_allowed(&request.host); let validation = self.allowlist.is_allowed(&request.host);
if !validation.is_allowed() { if !validation.is_allowed()
if let crate::sandbox::proxy::allowlist::DomainValidationResult::Denied(reason) = && let crate::sandbox::proxy::allowlist::DomainValidationResult::Denied(reason) =
validation validation
{ {
return NetworkDecision::Deny { reason }; return NetworkDecision::Deny { reason };
} }
}
// Check if we need to inject credentials // Check if we need to inject credentials
if let Some(mapping) = self.find_credential(&request.host) { if let Some(mapping) = self.find_credential(&request.host) {
+1 -1
View File
@@ -261,7 +261,7 @@ pub use platform::{delete_master_key, get_master_key, has_master_key, store_mast
/// Parse a hex string to bytes. /// Parse a hex string to bytes.
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, SecretError> { fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, SecretError> {
if hex.len() % 2 != 0 { if !hex.len().is_multiple_of(2) {
return Err(SecretError::KeychainError( return Err(SecretError::KeychainError(
"Invalid hex string length".to_string(), "Invalid hex string length".to_string(),
)); ));
+9 -9
View File
@@ -149,11 +149,11 @@ impl SecretsStore for PostgresSecretsStore {
let secret = row_to_secret(&r); let secret = row_to_secret(&r);
// Check expiration // Check expiration
if let Some(expires_at) = secret.expires_at { if let Some(expires_at) = secret.expires_at
if expires_at < Utc::now() { && expires_at < Utc::now()
{
return Err(SecretError::Expired); return Err(SecretError::Expired);
} }
}
Ok(secret) Ok(secret)
} }
@@ -272,12 +272,12 @@ impl SecretsStore for PostgresSecretsStore {
} }
// Simple glob: * matches any suffix // Simple glob: * matches any suffix
if let Some(prefix) = pattern.strip_suffix('*') { if let Some(prefix) = pattern.strip_suffix('*')
if secret_name.starts_with(prefix) { && secret_name.starts_with(prefix)
{
return Ok(true); return Ok(true);
} }
} }
}
Ok(false) Ok(false)
} }
@@ -430,12 +430,12 @@ pub mod testing {
if pattern == secret_name { if pattern == secret_name {
return Ok(true); return Ok(true);
} }
if let Some(prefix) = pattern.strip_suffix('*') { if let Some(prefix) = pattern.strip_suffix('*')
if secret_name.starts_with(prefix) { && secret_name.starts_with(prefix)
{
return Ok(true); return Ok(true);
} }
} }
}
Ok(false) Ok(false)
} }
} }
+3 -3
View File
@@ -252,8 +252,9 @@ async fn bind_telegram_owner(token: &SecretString) -> Result<Option<i64>, String
// Find the first message with a sender // Find the first message with a sender
for update in &body.result { for update in &body.result {
if let Some(ref msg) = update.message { if let Some(ref msg) = update.message
if let Some(ref from) = msg.from { && let Some(ref from) = msg.from
{
let display_name = from let display_name = from
.username .username
.as_ref() .as_ref()
@@ -280,7 +281,6 @@ async fn bind_telegram_owner(token: &SecretString) -> Result<Option<i64>, String
} }
} }
} }
}
print_error("Timed out waiting for a message. You can re-run setup to try again."); print_error("Timed out waiting for a message. You can re-run setup to try again.");
print_info("Bot will accept messages from all users until owner is bound."); print_info("Bot will accept messages from all users until owner is bound.");
+4 -3
View File
@@ -54,11 +54,12 @@ pub fn select_one(prompt: &str, options: &[&str]) -> io::Result<usize> {
} }
// Parse number // Parse number
if let Ok(num) = input.parse::<usize>() { if let Ok(num) = input.parse::<usize>()
if num >= 1 && num <= options.len() { && num >= 1
&& num <= options.len()
{
return Ok(num - 1); return Ok(num - 1);
} }
}
writeln!( writeln!(
stdout, stdout,
+4 -5
View File
@@ -344,8 +344,9 @@ impl SetupWizard {
/// Step 3: NEAR AI authentication. /// Step 3: NEAR AI authentication.
async fn step_authentication(&mut self) -> Result<(), SetupError> { async fn step_authentication(&mut self) -> Result<(), SetupError> {
// Check if we already have a session // Check if we already have a session
if let Some(ref session) = self.session_manager { if let Some(ref session) = self.session_manager
if session.has_token().await { && session.has_token().await
{
print_info("Existing session found. Validating..."); print_info("Existing session found. Validating...");
match session.ensure_authenticated().await { match session.ensure_authenticated().await {
Ok(()) => { Ok(()) => {
@@ -357,7 +358,6 @@ impl SetupWizard {
} }
} }
} }
}
// Create session manager if we don't have one // Create session manager if we don't have one
let session = if let Some(ref s) = self.session_manager { let session = if let Some(ref s) = self.session_manager {
@@ -642,12 +642,11 @@ impl SetupWizard {
&installed_names, &installed_names,
) )
.await? .await?
&& !installed.is_empty()
{ {
if !installed.is_empty() {
print_success(&format!("Installed channels: {}", installed.join(", "))); print_success(&format!("Installed channels: {}", installed.join(", ")));
discovered_channels = discover_wasm_channels(&channels_dir).await; discovered_channels = discover_wasm_channels(&channels_dir).await;
} }
}
// Determine if we need secrets context // Determine if we need secrets context
let needs_secrets = selected.contains(&1) || !selected_wasm_channels.is_empty(); let needs_secrets = selected.contains(&1) || !selected_wasm_channels.is_empty();
+6 -6
View File
@@ -326,8 +326,9 @@ impl TestHarness {
} }
// Verify expected output // Verify expected output
if let Some(ref expected) = test.expected_output { if let Some(ref expected) = test.expected_output
if &actual != expected { && &actual != expected
{
return TestResult { return TestResult {
name: test.name.clone(), name: test.name.clone(),
passed: false, passed: false,
@@ -340,7 +341,6 @@ impl TestHarness {
actual_output: Some(actual), actual_output: Some(actual),
}; };
} }
}
// Verify expected fields // Verify expected fields
if let Some(ref fields) = test.expected_fields { if let Some(ref fields) = test.expected_fields {
@@ -357,8 +357,9 @@ impl TestHarness {
}; };
} }
if let Some(ref expected_value) = field.value { if let Some(ref expected_value) = field.value
if field_value != Some(expected_value) { && field_value != Some(expected_value)
{
return TestResult { return TestResult {
name: test.name.clone(), name: test.name.clone(),
passed: false, passed: false,
@@ -372,7 +373,6 @@ impl TestHarness {
} }
} }
} }
}
TestResult { TestResult {
name: test.name.clone(), name: test.name.clone(),
+3 -3
View File
@@ -54,13 +54,13 @@ fn validate_url(url: &str) -> Result<reqwest::Url, ToolError> {
} }
// Check literal IP addresses // Check literal IP addresses
if let Ok(ip) = host.parse::<IpAddr>() { if let Ok(ip) = host.parse::<IpAddr>()
if is_disallowed_ip(&ip) { && is_disallowed_ip(&ip)
{
return Err(ToolError::NotAuthorized( return Err(ToolError::NotAuthorized(
"private or local IPs are not allowed".to_string(), "private or local IPs are not allowed".to_string(),
)); ));
} }
}
// Resolve hostname and check all resolved IPs against the blocklist. // Resolve hostname and check all resolved IPs against the blocklist.
// This prevents DNS rebinding where a hostname resolves to a private IP. // This prevents DNS rebinding where a hostname resolves to a private IP.
+3 -3
View File
@@ -157,8 +157,9 @@ impl CreateJobTool {
}); });
// Persist the job mode to DB // Persist the job mode to DB
if mode == JobMode::ClaudeCode { if mode == JobMode::ClaudeCode
if let Some(store) = self.store.clone() { && let Some(store) = self.store.clone()
{
let job_id_copy = job_id; let job_id_copy = job_id;
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = store if let Err(e) = store
@@ -169,7 +170,6 @@ impl CreateJobTool {
} }
}); });
} }
}
// Create the container job with the pre-determined job_id. // Create the container job with the pre-determined job_id.
let _token = jm let _token = jm
+3 -3
View File
@@ -343,13 +343,13 @@ impl ShellTool {
// Use sandbox if configured; fail-closed (never silently fall through // Use sandbox if configured; fail-closed (never silently fall through
// to unsandboxed execution when sandbox was intended). // to unsandboxed execution when sandbox was intended).
if let Some(ref sandbox) = self.sandbox { if let Some(ref sandbox) = self.sandbox
if sandbox.is_initialized() || sandbox.config().enabled { && (sandbox.is_initialized() || sandbox.config().enabled)
{
return self return self
.execute_sandboxed(sandbox, cmd, &cwd, timeout_duration) .execute_sandboxed(sandbox, cmd, &cwd, timeout_duration)
.await; .await;
} }
}
// Only execute directly when no sandbox was configured at all. // Only execute directly when no sandbox was configured at all.
let (output, code) = self.execute_direct(cmd, &cwd, timeout_duration).await?; let (output, code) = self.execute_direct(cmd, &cwd, timeout_duration).await?;
+3 -5
View File
@@ -539,9 +539,9 @@ pub async fn wait_for_authorization_callback(
.map_err(|e| AuthError::Http(e.to_string()))?; .map_err(|e| AuthError::Http(e.to_string()))?;
// Parse GET /callback?code=xxx HTTP/1.1 // Parse GET /callback?code=xxx HTTP/1.1
if let Some(path) = request_line.split_whitespace().nth(1) { if let Some(path) = request_line.split_whitespace().nth(1)
if path.starts_with("/callback") { && path.starts_with("/callback")
if let Some(query) = path.split('?').nth(1) { && let Some(query) = path.split('?').nth(1) {
// Check for error first // Check for error first
if query.contains("error=") { if query.contains("error=") {
let response = "HTTP/1.1 400 Bad Request\r\n\r\nAuthorization denied"; 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 response = "HTTP/1.1 404 Not Found\r\n\r\n";
let _ = socket.write_all(response.as_bytes()).await; let _ = socket.write_all(response.as_bytes()).await;
+12 -16
View File
@@ -184,11 +184,11 @@ impl McpClient {
} }
// Add Mcp-Session-Id header if we have a session // Add Mcp-Session-Id header if we have a session
if let Some(ref session_manager) = self.session_manager { if let Some(ref session_manager) = self.session_manager
if let Some(session_id) = session_manager.get_session_id(&self.server_name).await { && let Some(session_id) = session_manager.get_session_id(&self.server_name).await
{
req_builder = req_builder.header("Mcp-Session-Id", session_id); req_builder = req_builder.header("Mcp-Session-Id", session_id);
} }
}
let response = req_builder let response = req_builder
.send() .send()
@@ -199,18 +199,16 @@ impl McpClient {
if response.status() == reqwest::StatusCode::UNAUTHORIZED { if response.status() == reqwest::StatusCode::UNAUTHORIZED {
if attempt == 0 { if attempt == 0 {
// Try to refresh the token // Try to refresh the token
if let Some(ref secrets) = self.secrets { if let Some(ref secrets) = self.secrets
if let Some(ref config) = self.server_config { && let Some(ref config) = self.server_config
{
tracing::debug!( tracing::debug!(
"MCP token expired, attempting refresh for '{}'", "MCP token expired, attempting refresh for '{}'",
self.server_name self.server_name
); );
match refresh_access_token(config, secrets, &self.user_id).await { match refresh_access_token(config, secrets, &self.user_id).await {
Ok(_) => { Ok(_) => {
tracing::info!( tracing::info!("MCP token refreshed for '{}'", self.server_name);
"MCP token refreshed for '{}'",
self.server_name
);
// Continue to next iteration to retry with new token // Continue to next iteration to retry with new token
continue; continue;
} }
@@ -225,7 +223,6 @@ impl McpClient {
} }
} }
} }
}
return Err(ToolError::ExternalService(format!( return Err(ToolError::ExternalService(format!(
"MCP server '{}' requires authentication. Run: ironclaw mcp auth {}", "MCP server '{}' requires authentication. Run: ironclaw mcp auth {}",
self.server_name, self.server_name self.server_name, self.server_name
@@ -245,8 +242,8 @@ impl McpClient {
/// Parse the HTTP response into an MCP response. /// Parse the HTTP response into an MCP response.
async fn parse_response(&self, response: reqwest::Response) -> Result<McpResponse, ToolError> { async fn parse_response(&self, response: reqwest::Response) -> Result<McpResponse, ToolError> {
// Extract session ID from response header // Extract session ID from response header
if let Some(ref session_manager) = self.session_manager { if let Some(ref session_manager) = self.session_manager
if let Some(session_id) = response && let Some(session_id) = response
.headers() .headers()
.get("Mcp-Session-Id") .get("Mcp-Session-Id")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
@@ -255,7 +252,6 @@ impl McpClient {
.update_session_id(&self.server_name, Some(session_id.to_string())) .update_session_id(&self.server_name, Some(session_id.to_string()))
.await; .await;
} }
}
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status(); let status = response.status();
@@ -316,12 +312,12 @@ impl McpClient {
/// This should be called once per session to establish capabilities. /// This should be called once per session to establish capabilities.
pub async fn initialize(&self) -> Result<InitializeResult, ToolError> { pub async fn initialize(&self) -> Result<InitializeResult, ToolError> {
// Check if already initialized // Check if already initialized
if let Some(ref session_manager) = self.session_manager { if let Some(ref session_manager) = self.session_manager
if session_manager.is_initialized(&self.server_name).await { && session_manager.is_initialized(&self.server_name).await
{
// Return cached/default capabilities // Return cached/default capabilities
return Ok(InitializeResult::default()); return Ok(InitializeResult::default());
} }
}
// Ensure we have a session // Ensure we have a session
if let Some(ref session_manager) = self.session_manager { if let Some(ref session_manager) = self.session_manager {
+3 -3
View File
@@ -96,11 +96,11 @@ impl ToolRegistry {
if let Ok(mut tools) = self.tools.try_write() { if let Ok(mut tools) = self.tools.try_write() {
tools.insert(name.clone(), tool); tools.insert(name.clone(), tool);
// Mark as built-in so it can't be shadowed later // Mark as built-in so it can't be shadowed later
if PROTECTED_TOOL_NAMES.contains(&name.as_str()) { if PROTECTED_TOOL_NAMES.contains(&name.as_str())
if let Ok(mut builtins) = self.builtin_names.try_write() { && let Ok(mut builtins) = self.builtin_names.try_write()
{
builtins.insert(name.clone()); builtins.insert(name.clone());
} }
}
tracing::debug!("Registered tool: {}", name); tracing::debug!("Registered tool: {}", name);
} }
} }
+10 -9
View File
@@ -209,11 +209,11 @@ impl EndpointPattern {
} }
// Check path prefix // Check path prefix
if let Some(ref prefix) = self.path_prefix { if let Some(ref prefix) = self.path_prefix
if !url_path.starts_with(prefix) { && !url_path.starts_with(prefix)
{
return false; return false;
} }
}
// Check method // Check method
if !self.methods.is_empty() { if !self.methods.is_empty() {
@@ -237,15 +237,16 @@ impl EndpointPattern {
} }
// Support wildcard: *.example.com matches sub.example.com // Support wildcard: *.example.com matches sub.example.com
if let Some(suffix) = self.host.strip_prefix("*.") { if let Some(suffix) = self.host.strip_prefix("*.")
if url_host.ends_with(suffix) && url_host.len() > suffix.len() { && url_host.ends_with(suffix)
&& url_host.len() > suffix.len()
{
// Ensure there's a dot before the suffix (or it's the whole thing) // Ensure there's a dot before the suffix (or it's the whole thing)
let prefix = &url_host[..url_host.len() - suffix.len()]; let prefix = &url_host[..url_host.len() - suffix.len()];
if prefix.ends_with('.') || prefix.is_empty() { if prefix.ends_with('.') || prefix.is_empty() {
return true; return true;
} }
} }
}
false false
} }
@@ -291,12 +292,12 @@ impl SecretsCapability {
if pattern == name { if pattern == name {
return true; return true;
} }
if let Some(prefix) = pattern.strip_suffix('*') { if let Some(prefix) = pattern.strip_suffix('*')
if name.starts_with(prefix) { && name.starts_with(prefix)
{
return true; return true;
} }
} }
}
false false
} }
} }
+7 -6
View File
@@ -158,12 +158,12 @@ impl CredentialInjector {
if pattern == name { if pattern == name {
return true; return true;
} }
if let Some(prefix) = pattern.strip_suffix('*') { if let Some(prefix) = pattern.strip_suffix('*')
if name.starts_with(prefix) { && name.starts_with(prefix)
{
return true; return true;
} }
} }
}
false false
} }
} }
@@ -214,14 +214,15 @@ fn host_matches_pattern(host: &str, pattern: &str) -> bool {
} }
// Support wildcard: *.example.com matches sub.example.com // Support wildcard: *.example.com matches sub.example.com
if let Some(suffix) = pattern.strip_prefix("*.") { if let Some(suffix) = pattern.strip_prefix("*.")
if host.ends_with(suffix) && host.len() > suffix.len() { && host.ends_with(suffix)
&& host.len() > suffix.len()
{
let prefix = &host[..host.len() - suffix.len()]; let prefix = &host[..host.len() - suffix.len()];
if prefix.ends_with('.') || prefix.is_empty() { if prefix.ends_with('.') || prefix.is_empty() {
return true; return true;
} }
} }
}
false false
} }
+3 -3
View File
@@ -259,14 +259,14 @@ impl near::agent::host::Host for StoreData {
// Check Content-Length header for early rejection of oversized responses. // Check Content-Length header for early rejection of oversized responses.
let max_response = max_response_bytes; let max_response = max_response_bytes;
if let Some(cl) = response.content_length() { if let Some(cl) = response.content_length()
if cl as usize > max_response { && cl as usize > max_response
{
return Err(format!( return Err(format!(
"Response body too large: {} bytes exceeds limit of {} bytes", "Response body too large: {} bytes exceeds limit of {} bytes",
cl, max_response cl, max_response
)); ));
} }
}
// Read body with a size cap to prevent memory exhaustion. // Read body with a size cap to prevent memory exhaustion.
let body = response let body = response
+3 -3
View File
@@ -326,8 +326,9 @@ impl ClaudeBridgeRuntime {
match serde_json::from_str::<ClaudeStreamEvent>(&line) { match serde_json::from_str::<ClaudeStreamEvent>(&line) {
Ok(event) => { Ok(event) => {
// Capture session_id from system init // Capture session_id from system init
if event.event_type == "system" { if event.event_type == "system"
if let Some(ref sid) = event.session_id { && let Some(ref sid) = event.session_id
{
session_id = Some(sid.clone()); session_id = Some(sid.clone());
tracing::info!( tracing::info!(
job_id = %self.config.job_id, job_id = %self.config.job_id,
@@ -335,7 +336,6 @@ impl ClaudeBridgeRuntime {
"Captured Claude session ID" "Captured Claude session ID"
); );
} }
}
// Convert to our event payload and forward // Convert to our event payload and forward
let payloads = stream_event_to_payloads(&event); let payloads = stream_event_to_payloads(&event);
+6 -6
View File
@@ -333,20 +333,21 @@ impl Workspace {
]; ];
for (path, header) in identity_files { for (path, header) in identity_files {
if let Ok(doc) = self.read(path).await { if let Ok(doc) = self.read(path).await
if !doc.content.is_empty() { && !doc.content.is_empty()
{
parts.push(format!("{}\n\n{}", header, doc.content)); parts.push(format!("{}\n\n{}", header, doc.content));
} }
} }
}
// Add today's memory context (last 2 days of daily logs) // Add today's memory context (last 2 days of daily logs)
let today = Utc::now().date_naive(); let today = Utc::now().date_naive();
let yesterday = today.pred_opt().unwrap_or(today); let yesterday = today.pred_opt().unwrap_or(today);
for date in [today, yesterday] { for date in [today, yesterday] {
if let Ok(doc) = self.daily_log(date).await { if let Ok(doc) = self.daily_log(date).await
if !doc.content.is_empty() { && !doc.content.is_empty()
{
let header = if date == today { let header = if date == today {
"## Today's Notes" "## Today's Notes"
} else { } else {
@@ -355,7 +356,6 @@ impl Workspace {
parts.push(format!("{}\n\n{}", header, doc.content)); parts.push(format!("{}\n\n{}", header, doc.content));
} }
} }
}
Ok(parts.join("\n\n---\n\n")) Ok(parts.join("\n\n---\n\n"))
} }
+3 -3
View File
@@ -201,13 +201,13 @@ pub fn reciprocal_rank_fusion(
.collect(); .collect();
// Normalize scores to 0-1 range // Normalize scores to 0-1 range
if let Some(max_score) = results.iter().map(|r| r.score).reduce(f32::max) { if let Some(max_score) = results.iter().map(|r| r.score).reduce(f32::max)
if max_score > 0.0 { && max_score > 0.0
{
for result in &mut results { for result in &mut results {
result.score /= max_score; result.score /= max_score;
} }
} }
}
// Filter by minimum score // Filter by minimum score
if config.min_score > 0.0 { if config.min_score > 0.0 {
+3 -3
View File
@@ -302,13 +302,13 @@ async fn test_chat_completions_streaming() {
if data == "[DONE]" { if data == "[DONE]" {
continue; continue;
} }
if let Ok(chunk) = serde_json::from_str::<serde_json::Value>(data) { if let Ok(chunk) = serde_json::from_str::<serde_json::Value>(data)
if let Some(content) = chunk["choices"][0]["delta"]["content"].as_str() { && let Some(content) = chunk["choices"][0]["delta"]["content"].as_str()
{
full_content.push_str(content); full_content.push_str(content);
} }
} }
} }
}
assert!( assert!(
full_content.contains("Stream test"), full_content.contains("Stream test"),
"Expected reassembled content to contain 'Stream test', got: '{}'", "Expected reassembled content to contain 'Stream test', got: '{}'",