mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 00:29:24 +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
@@ -326,20 +326,20 @@ impl TestHarness {
|
||||
}
|
||||
|
||||
// Verify expected output
|
||||
if let Some(ref expected) = test.expected_output {
|
||||
if &actual != expected {
|
||||
return TestResult {
|
||||
name: test.name.clone(),
|
||||
passed: false,
|
||||
duration,
|
||||
error: Some(format!(
|
||||
"Output mismatch:\nExpected: {}\nActual: {}",
|
||||
serde_json::to_string_pretty(expected).unwrap_or_default(),
|
||||
serde_json::to_string_pretty(&actual).unwrap_or_default()
|
||||
)),
|
||||
actual_output: Some(actual),
|
||||
};
|
||||
}
|
||||
if let Some(ref expected) = test.expected_output
|
||||
&& &actual != expected
|
||||
{
|
||||
return TestResult {
|
||||
name: test.name.clone(),
|
||||
passed: false,
|
||||
duration,
|
||||
error: Some(format!(
|
||||
"Output mismatch:\nExpected: {}\nActual: {}",
|
||||
serde_json::to_string_pretty(expected).unwrap_or_default(),
|
||||
serde_json::to_string_pretty(&actual).unwrap_or_default()
|
||||
)),
|
||||
actual_output: Some(actual),
|
||||
};
|
||||
}
|
||||
|
||||
// Verify expected fields
|
||||
@@ -357,19 +357,19 @@ impl TestHarness {
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(ref expected_value) = field.value {
|
||||
if field_value != Some(expected_value) {
|
||||
return TestResult {
|
||||
name: test.name.clone(),
|
||||
passed: false,
|
||||
duration,
|
||||
error: Some(format!(
|
||||
"Field '{}' mismatch: expected {:?}, got {:?}",
|
||||
field.path, expected_value, field_value
|
||||
)),
|
||||
actual_output: Some(actual),
|
||||
};
|
||||
}
|
||||
if let Some(ref expected_value) = field.value
|
||||
&& field_value != Some(expected_value)
|
||||
{
|
||||
return TestResult {
|
||||
name: test.name.clone(),
|
||||
passed: false,
|
||||
duration,
|
||||
error: Some(format!(
|
||||
"Field '{}' mismatch: expected {:?}, got {:?}",
|
||||
field.path, expected_value, field_value
|
||||
)),
|
||||
actual_output: Some(actual),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,12 @@ fn validate_url(url: &str) -> Result<reqwest::Url, ToolError> {
|
||||
}
|
||||
|
||||
// Check literal IP addresses
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
if is_disallowed_ip(&ip) {
|
||||
return Err(ToolError::NotAuthorized(
|
||||
"private or local IPs are not allowed".to_string(),
|
||||
));
|
||||
}
|
||||
if let Ok(ip) = host.parse::<IpAddr>()
|
||||
&& is_disallowed_ip(&ip)
|
||||
{
|
||||
return Err(ToolError::NotAuthorized(
|
||||
"private or local IPs are not allowed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Resolve hostname and check all resolved IPs against the blocklist.
|
||||
|
||||
+12
-12
@@ -157,18 +157,18 @@ impl CreateJobTool {
|
||||
});
|
||||
|
||||
// Persist the job mode to DB
|
||||
if mode == JobMode::ClaudeCode {
|
||||
if let Some(store) = self.store.clone() {
|
||||
let job_id_copy = job_id;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = store
|
||||
.update_sandbox_job_mode(job_id_copy, "claude_code")
|
||||
.await
|
||||
{
|
||||
tracing::warn!(job_id = %job_id_copy, "Failed to set job mode: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
if mode == JobMode::ClaudeCode
|
||||
&& let Some(store) = self.store.clone()
|
||||
{
|
||||
let job_id_copy = job_id;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = store
|
||||
.update_sandbox_job_mode(job_id_copy, "claude_code")
|
||||
.await
|
||||
{
|
||||
tracing::warn!(job_id = %job_id_copy, "Failed to set job mode: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create the container job with the pre-determined job_id.
|
||||
|
||||
@@ -343,12 +343,12 @@ impl ShellTool {
|
||||
|
||||
// Use sandbox if configured; fail-closed (never silently fall through
|
||||
// to unsandboxed execution when sandbox was intended).
|
||||
if let Some(ref sandbox) = self.sandbox {
|
||||
if sandbox.is_initialized() || sandbox.config().enabled {
|
||||
return self
|
||||
.execute_sandboxed(sandbox, cmd, &cwd, timeout_duration)
|
||||
.await;
|
||||
}
|
||||
if let Some(ref sandbox) = self.sandbox
|
||||
&& (sandbox.is_initialized() || sandbox.config().enabled)
|
||||
{
|
||||
return self
|
||||
.execute_sandboxed(sandbox, cmd, &cwd, timeout_duration)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Only execute directly when no sandbox was configured at all.
|
||||
|
||||
@@ -539,9 +539,9 @@ pub async fn wait_for_authorization_callback(
|
||||
.map_err(|e| AuthError::Http(e.to_string()))?;
|
||||
|
||||
// Parse GET /callback?code=xxx HTTP/1.1
|
||||
if let Some(path) = request_line.split_whitespace().nth(1) {
|
||||
if path.starts_with("/callback") {
|
||||
if let Some(query) = path.split('?').nth(1) {
|
||||
if let Some(path) = request_line.split_whitespace().nth(1)
|
||||
&& path.starts_with("/callback")
|
||||
&& let Some(query) = path.split('?').nth(1) {
|
||||
// Check for error first
|
||||
if query.contains("error=") {
|
||||
let response = "HTTP/1.1 400 Bad Request\r\n\r\nAuthorization denied";
|
||||
@@ -578,8 +578,6 @@ pub async fn wait_for_authorization_callback(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = "HTTP/1.1 404 Not Found\r\n\r\n";
|
||||
let _ = socket.write_all(response.as_bytes()).await;
|
||||
|
||||
+35
-39
@@ -184,10 +184,10 @@ impl McpClient {
|
||||
}
|
||||
|
||||
// Add Mcp-Session-Id header if we have a session
|
||||
if let Some(ref session_manager) = self.session_manager {
|
||||
if let Some(session_id) = session_manager.get_session_id(&self.server_name).await {
|
||||
req_builder = req_builder.header("Mcp-Session-Id", session_id);
|
||||
}
|
||||
if let Some(ref session_manager) = self.session_manager
|
||||
&& let Some(session_id) = session_manager.get_session_id(&self.server_name).await
|
||||
{
|
||||
req_builder = req_builder.header("Mcp-Session-Id", session_id);
|
||||
}
|
||||
|
||||
let response = req_builder
|
||||
@@ -199,29 +199,26 @@ impl McpClient {
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
if attempt == 0 {
|
||||
// Try to refresh the token
|
||||
if let Some(ref secrets) = self.secrets {
|
||||
if let Some(ref config) = self.server_config {
|
||||
tracing::debug!(
|
||||
"MCP token expired, attempting refresh for '{}'",
|
||||
self.server_name
|
||||
);
|
||||
match refresh_access_token(config, secrets, &self.user_id).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
"MCP token refreshed for '{}'",
|
||||
self.server_name
|
||||
);
|
||||
// Continue to next iteration to retry with new token
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
"Token refresh failed for '{}': {}",
|
||||
self.server_name,
|
||||
e
|
||||
);
|
||||
// Fall through to return auth error
|
||||
}
|
||||
if let Some(ref secrets) = self.secrets
|
||||
&& let Some(ref config) = self.server_config
|
||||
{
|
||||
tracing::debug!(
|
||||
"MCP token expired, attempting refresh for '{}'",
|
||||
self.server_name
|
||||
);
|
||||
match refresh_access_token(config, secrets, &self.user_id).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("MCP token refreshed for '{}'", self.server_name);
|
||||
// Continue to next iteration to retry with new token
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
"Token refresh failed for '{}': {}",
|
||||
self.server_name,
|
||||
e
|
||||
);
|
||||
// Fall through to return auth error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,16 +242,15 @@ impl McpClient {
|
||||
/// Parse the HTTP response into an MCP response.
|
||||
async fn parse_response(&self, response: reqwest::Response) -> Result<McpResponse, ToolError> {
|
||||
// Extract session ID from response header
|
||||
if let Some(ref session_manager) = self.session_manager {
|
||||
if let Some(session_id) = response
|
||||
if let Some(ref session_manager) = self.session_manager
|
||||
&& let Some(session_id) = response
|
||||
.headers()
|
||||
.get("Mcp-Session-Id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
session_manager
|
||||
.update_session_id(&self.server_name, Some(session_id.to_string()))
|
||||
.await;
|
||||
}
|
||||
{
|
||||
session_manager
|
||||
.update_session_id(&self.server_name, Some(session_id.to_string()))
|
||||
.await;
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
@@ -316,11 +312,11 @@ impl McpClient {
|
||||
/// This should be called once per session to establish capabilities.
|
||||
pub async fn initialize(&self) -> Result<InitializeResult, ToolError> {
|
||||
// Check if already initialized
|
||||
if let Some(ref session_manager) = self.session_manager {
|
||||
if session_manager.is_initialized(&self.server_name).await {
|
||||
// Return cached/default capabilities
|
||||
return Ok(InitializeResult::default());
|
||||
}
|
||||
if let Some(ref session_manager) = self.session_manager
|
||||
&& session_manager.is_initialized(&self.server_name).await
|
||||
{
|
||||
// Return cached/default capabilities
|
||||
return Ok(InitializeResult::default());
|
||||
}
|
||||
|
||||
// Ensure we have a session
|
||||
|
||||
@@ -96,10 +96,10 @@ impl ToolRegistry {
|
||||
if let Ok(mut tools) = self.tools.try_write() {
|
||||
tools.insert(name.clone(), tool);
|
||||
// Mark as built-in so it can't be shadowed later
|
||||
if PROTECTED_TOOL_NAMES.contains(&name.as_str()) {
|
||||
if let Ok(mut builtins) = self.builtin_names.try_write() {
|
||||
builtins.insert(name.clone());
|
||||
}
|
||||
if PROTECTED_TOOL_NAMES.contains(&name.as_str())
|
||||
&& let Ok(mut builtins) = self.builtin_names.try_write()
|
||||
{
|
||||
builtins.insert(name.clone());
|
||||
}
|
||||
tracing::debug!("Registered tool: {}", name);
|
||||
}
|
||||
|
||||
@@ -209,10 +209,10 @@ impl EndpointPattern {
|
||||
}
|
||||
|
||||
// Check path prefix
|
||||
if let Some(ref prefix) = self.path_prefix {
|
||||
if !url_path.starts_with(prefix) {
|
||||
return false;
|
||||
}
|
||||
if let Some(ref prefix) = self.path_prefix
|
||||
&& !url_path.starts_with(prefix)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check method
|
||||
@@ -237,13 +237,14 @@ impl EndpointPattern {
|
||||
}
|
||||
|
||||
// Support wildcard: *.example.com matches sub.example.com
|
||||
if let Some(suffix) = self.host.strip_prefix("*.") {
|
||||
if url_host.ends_with(suffix) && url_host.len() > suffix.len() {
|
||||
// Ensure there's a dot before the suffix (or it's the whole thing)
|
||||
let prefix = &url_host[..url_host.len() - suffix.len()];
|
||||
if prefix.ends_with('.') || prefix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if let Some(suffix) = self.host.strip_prefix("*.")
|
||||
&& url_host.ends_with(suffix)
|
||||
&& url_host.len() > suffix.len()
|
||||
{
|
||||
// Ensure there's a dot before the suffix (or it's the whole thing)
|
||||
let prefix = &url_host[..url_host.len() - suffix.len()];
|
||||
if prefix.ends_with('.') || prefix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,10 +292,10 @@ impl SecretsCapability {
|
||||
if pattern == name {
|
||||
return true;
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*') {
|
||||
if name.starts_with(prefix) {
|
||||
return true;
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& name.starts_with(prefix)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
@@ -158,10 +158,10 @@ impl CredentialInjector {
|
||||
if pattern == name {
|
||||
return true;
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*') {
|
||||
if name.starts_with(prefix) {
|
||||
return true;
|
||||
}
|
||||
if let Some(prefix) = pattern.strip_suffix('*')
|
||||
&& name.starts_with(prefix)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -214,12 +214,13 @@ fn host_matches_pattern(host: &str, pattern: &str) -> bool {
|
||||
}
|
||||
|
||||
// Support wildcard: *.example.com matches sub.example.com
|
||||
if let Some(suffix) = pattern.strip_prefix("*.") {
|
||||
if host.ends_with(suffix) && host.len() > suffix.len() {
|
||||
let prefix = &host[..host.len() - suffix.len()];
|
||||
if prefix.ends_with('.') || prefix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if let Some(suffix) = pattern.strip_prefix("*.")
|
||||
&& host.ends_with(suffix)
|
||||
&& host.len() > suffix.len()
|
||||
{
|
||||
let prefix = &host[..host.len() - suffix.len()];
|
||||
if prefix.ends_with('.') || prefix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -259,13 +259,13 @@ impl near::agent::host::Host for StoreData {
|
||||
|
||||
// Check Content-Length header for early rejection of oversized responses.
|
||||
let max_response = max_response_bytes;
|
||||
if let Some(cl) = response.content_length() {
|
||||
if cl as usize > max_response {
|
||||
return Err(format!(
|
||||
"Response body too large: {} bytes exceeds limit of {} bytes",
|
||||
cl, max_response
|
||||
));
|
||||
}
|
||||
if let Some(cl) = response.content_length()
|
||||
&& cl as usize > max_response
|
||||
{
|
||||
return Err(format!(
|
||||
"Response body too large: {} bytes exceeds limit of {} bytes",
|
||||
cl, max_response
|
||||
));
|
||||
}
|
||||
|
||||
// Read body with a size cap to prevent memory exhaustion.
|
||||
|
||||
Reference in New Issue
Block a user