mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 00:59:33 +00:00
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:
co-authored by
Claude Opus 4.6
parent
bbb68f7490
commit
5df0d13b59
+136
-145
@@ -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::<serde_json::Value>(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::<String>()
|
||||
);
|
||||
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::<serde_json::Value>(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::<String>()
|
||||
);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-18
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -93,27 +93,26 @@ impl SubmissionParser {
|
||||
// /thread <uuid> - 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 <uuid> - 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::<Submission>(trimmed) {
|
||||
if matches!(submission, Submission::ExecApproval { .. }) {
|
||||
return submission;
|
||||
}
|
||||
}
|
||||
if trimmed.starts_with('{')
|
||||
&& let Ok(submission) = serde_json::from_str::<Submission>(trimmed)
|
||||
&& matches!(submission, Submission::ExecApproval { .. })
|
||||
{
|
||||
return submission;
|
||||
}
|
||||
|
||||
// Approval responses (simple yes/no/always for pending approvals)
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user