build(rust): pin MSRV to 1.88 + add rust-toolchain.toml (#469)

The Rust workspace fails to build on stable 1.86/1.87 with cryptic E0658
errors deep in dependencies: rig-core uses let-chains and
openjarvis-skills uses `is_multiple_of`, both stabilized in 1.88 (#252).

Add a rust-toolchain.toml pinning channel 1.88 (rustup then auto-selects
a working toolchain instead of erroring mid-build) and declare
rust-version = "1.88" in [workspace.package] to self-document it.

Because the toolchain pin makes CI's `cargo clippy -D warnings` run under
1.88 — whose clippy enables `uninlined_format_args` — also apply the
mechanical `format!("{}", x)` -> `format!("{x}")` rewrites across the
workspace (via `clippy --fix`; string output is identical, no logic
change). Verified clippy + fmt + `cargo test --workspace` clean on BOTH
1.88 and current stable.

Verified locally: cargo +1.86 and +1.87 fail (E0658), +1.88 builds and
tests cleanly. Supporting true 1.86 is infeasible without downgrading
rig-core below the versions exposing the token-usage symbols we use —
deferred.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-06-01 13:17:33 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2b14315f3c
commit d1ad3316b6
30 changed files with 76 additions and 72 deletions
+8
View File
@@ -20,6 +20,14 @@ members = [
"crates/openjarvis-scheduler",
]
# Minimum supported Rust version. The workspace uses let-chains (via rig-core)
# and `is_multiple_of` (openjarvis-skills), both stabilized in Rust 1.88 —
# building on 1.86/1.87 fails with cryptic E0658 errors deep in dependencies
# (see #252). Declared here and pinned in rust-toolchain.toml so users get a
# clear "requires rustc 1.88" signal instead.
[workspace.package]
rust-version = "1.88"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -32,8 +32,7 @@ impl LoopGuard {
// Check identical calls
if self.seen_hashes.contains(&hash) {
return Some(format!(
"Loop detected: identical call to '{}' with same arguments",
tool_name
"Loop detected: identical call to '{tool_name}' with same arguments"
));
}
self.seen_hashes.insert(hash);
@@ -355,7 +355,7 @@ impl<M: CompletionModel + 'static> OjAgent for MonitorOperativeAgent<M> {
// Loop guard check
if let Some(loop_msg) = guard.check(&action, &action_input) {
return Ok(AgentResult {
content: format!("Agent stopped: {}", loop_msg),
content: format!("Agent stopped: {loop_msg}"),
tool_results: all_tool_results,
turns: turn,
metadata: self.strategy_metadata(),
@@ -379,7 +379,7 @@ impl<M: CompletionModel + 'static> OjAgent for MonitorOperativeAgent<M> {
let compressed = self.compress_observation(&tool_result.content);
history.push(RigMessage::assistant(&text));
current_input = format!("Observation: {}", compressed);
current_input = format!("Observation: {compressed}");
all_tool_results.push(tool_result);
} else {
@@ -202,7 +202,7 @@ impl<M: CompletionModel + 'static> OjAgent for NativeOpenHandsAgent<M> {
if let Some(loop_msg) = guard.check(tool_name, &args_str) {
return Ok(AgentResult {
content: format!("Agent stopped: {}", loop_msg),
content: format!("Agent stopped: {loop_msg}"),
tool_results: all_tool_results,
turns: turn,
metadata: HashMap::new(),
@@ -221,7 +221,7 @@ impl<M: CompletionModel + 'static> OjAgent for NativeOpenHandsAgent<M> {
let obs = Self::truncate_observation(&tool_result.content, 4000);
history.push(RigMessage::assistant(&text));
current_input = format!("Output:\n{}", obs);
current_input = format!("Output:\n{obs}");
all_tool_results.push(tool_result);
continue;
@@ -231,7 +231,7 @@ impl<M: CompletionModel + 'static> OjAgent for NativeOpenHandsAgent<M> {
if let Some((action, action_input)) = Self::parse_action(&text) {
if let Some(loop_msg) = guard.check(&action, &action_input) {
return Ok(AgentResult {
content: format!("Agent stopped: {}", loop_msg),
content: format!("Agent stopped: {loop_msg}"),
tool_results: all_tool_results,
turns: turn,
metadata: HashMap::new(),
@@ -253,7 +253,7 @@ impl<M: CompletionModel + 'static> OjAgent for NativeOpenHandsAgent<M> {
let obs = Self::truncate_observation(&tool_result.content, 4000);
history.push(RigMessage::assistant(&text));
current_input = format!("Result: {}", obs);
current_input = format!("Result: {obs}");
all_tool_results.push(tool_result);
continue;
@@ -143,7 +143,7 @@ impl<M: CompletionModel + 'static> OjAgent for NativeReActAgent<M> {
if let Some((action, action_input)) = Self::parse_action(&text) {
if let Some(loop_msg) = guard.check(&action, &action_input) {
return Ok(AgentResult {
content: format!("Agent stopped: {}", loop_msg),
content: format!("Agent stopped: {loop_msg}"),
tool_results: all_tool_results,
turns: turn,
metadata: HashMap::new(),
@@ -104,8 +104,7 @@ pub fn get_engine_static(
))),
other => Err(OpenJarvisError::Engine(
openjarvis_core::error::EngineError::ModelNotFound(format!(
"Unknown engine: {}",
other
"Unknown engine: {other}"
)),
)),
}
@@ -28,7 +28,7 @@ impl LlamaCppEngine {
let host = if host.starts_with("http") {
host
} else {
format!("http://{}", host)
format!("http://{host}")
};
let host = host.trim_end_matches('/').to_string();
let timeout = std::time::Duration::from_secs_f64(timeout_secs);
@@ -120,8 +120,7 @@ impl InferenceEngine for LlamaCppEngine {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(OpenJarvisError::Engine(EngineError::Http(format!(
"llama.cpp returned {}: {}",
status, body
"llama.cpp returned {status}: {body}"
))));
}
+1 -1
View File
@@ -119,7 +119,7 @@ impl InferenceEngine for OllamaEngine {
ToolCall {
id: tc["id"]
.as_str()
.unwrap_or(&format!("call_{}", i))
.unwrap_or(&format!("call_{i}"))
.to_string(),
name: func["name"].as_str().unwrap_or("").to_string(),
arguments: args,
@@ -84,7 +84,7 @@ impl OpenAICompatEngine {
if let Some(ref key) = self.api_key {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", key).parse().unwrap(),
format!("Bearer {key}").parse().unwrap(),
);
}
headers
@@ -239,7 +239,7 @@ impl InferenceEngine for OpenAICompatEngine {
if let Some(ref key) = self.api_key {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", key).parse().unwrap(),
format!("Bearer {key}").parse().unwrap(),
);
}
@@ -107,8 +107,7 @@ fn rig_request_to_oj_messages(request: &CompletionRequest) -> Vec<Message> {
.collect::<Vec<_>>()
.join("\n\n");
messages.push(Message::system(format!(
"Relevant context:\n{}",
doc_context
"Relevant context:\n{doc_context}"
)));
}
+2 -3
View File
@@ -24,7 +24,7 @@ impl SGLangEngine {
let host = if host.starts_with("http") {
host
} else {
format!("http://{}", host)
format!("http://{host}")
};
let host = host.trim_end_matches('/').to_string();
let timeout = std::time::Duration::from_secs_f64(timeout_secs);
@@ -115,8 +115,7 @@ impl InferenceEngine for SGLangEngine {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(OpenJarvisError::Engine(EngineError::Http(format!(
"SGLang returned {}: {}",
status, body
"SGLang returned {status}: {body}"
))));
}
+4 -5
View File
@@ -26,7 +26,7 @@ impl VLLMEngine {
let host = if host.starts_with("http") {
host
} else {
format!("http://{}", host)
format!("http://{host}")
};
let host = host.trim_end_matches('/').to_string();
let timeout = std::time::Duration::from_secs_f64(timeout_secs);
@@ -70,7 +70,7 @@ impl VLLMEngine {
if let Some(ref key) = self.api_key {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", key).parse().unwrap(),
format!("Bearer {key}").parse().unwrap(),
);
}
headers
@@ -135,8 +135,7 @@ impl InferenceEngine for VLLMEngine {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(OpenJarvisError::Engine(EngineError::Http(format!(
"vLLM returned {}: {}",
status, body
"vLLM returned {status}: {body}"
))));
}
@@ -231,7 +230,7 @@ impl InferenceEngine for VLLMEngine {
if let Some(ref key) = self.api_key {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", key).parse().unwrap(),
format!("Bearer {key}").parse().unwrap(),
);
}
@@ -78,8 +78,7 @@ impl AgentAdvisorPolicy {
recs.push(Recommendation {
rec_type: "routing".to_string(),
suggestion: format!(
"Query class '{}' has {} failures — consider different model or agent",
qclass, count,
"Query class '{qclass}' has {count} failures — consider different model or agent",
),
severity: "high".to_string(),
});
@@ -160,7 +159,7 @@ mod tests {
let traces: Vec<TraceInfo> = (0..5)
.map(|i| TraceInfo {
outcome: "failure".into(),
query: format!("query {}", i),
query: format!("query {i}"),
tool_call_count: 8,
total_latency_seconds: 2.0,
})
@@ -178,7 +177,7 @@ mod tests {
let traces: Vec<TraceInfo> = (0..5)
.map(|i| TraceInfo {
outcome: "failure".into(),
query: format!("def func_{}():", i),
query: format!("def func_{i}():"),
tool_call_count: 2,
total_latency_seconds: 1.0,
})
@@ -209,7 +209,7 @@ mod tests {
fn test_max_examples_trim() {
let mut policy = ICLUpdaterPolicy::new(0.0, 3, 3);
for i in 0..5 {
policy.add_example(format!("q{}", i), format!("r{}", i), 0.5, HashMap::new());
policy.add_example(format!("q{i}"), format!("r{i}"), 0.5, HashMap::new());
}
assert_eq!(policy.example_db().len(), 3);
assert_eq!(policy.example_db()[0].query, "q2");
+1 -1
View File
@@ -97,7 +97,7 @@ impl McpServer {
let resp = McpResponse::error(
Value::Null,
-32700,
&format!("Parse error: {}", e),
&format!("Parse error: {e}"),
);
serde_json::to_string(&resp).unwrap_or_default()
}
+1 -1
View File
@@ -93,7 +93,7 @@ impl PyEngine {
),
other => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Unknown engine: {}", other),
format!("Unknown engine: {other}"),
));
}
};
@@ -20,8 +20,7 @@ impl PySchedulerStore {
fn create_task(&self, name: &str, schedule_type: &str, schedule_value: &str) -> PyResult<String> {
let st = openjarvis_scheduler::ScheduleType::parse(schedule_type).ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"invalid schedule_type '{}', expected cron/interval/once",
schedule_type
"invalid schedule_type '{schedule_type}', expected cron/interval/once"
))
})?;
let task = self.inner.create_task(name, st, schedule_value);
@@ -41,8 +40,7 @@ impl PySchedulerStore {
fn update_status(&self, id: &str, status: &str) -> PyResult<bool> {
let s = openjarvis_scheduler::TaskStatus::parse(status).ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"invalid status '{}', expected active/paused/cancelled/completed",
status
"invalid status '{status}', expected active/paused/cancelled/completed"
))
})?;
Ok(self.inner.update_status(id, s))
+1 -2
View File
@@ -266,8 +266,7 @@ impl PyHybridMemory {
}
other => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Unknown backend key: {}. Supported: sqlite, bm25, faiss, colbert",
other
"Unknown backend key: {other}. Supported: sqlite, bm25, faiss, colbert"
)));
}
};
+1 -1
View File
@@ -303,7 +303,7 @@ mod tests {
event_type: SecurityEventType::SecretDetected,
timestamp: 1000.0 + i as f64,
findings: vec![],
content_preview: format!("event {}", i),
content_preview: format!("event {i}"),
action_taken: "warn".into(),
};
logger.log(&event).unwrap();
+5 -6
View File
@@ -135,14 +135,13 @@ pub fn check_ssrf(url_str: &str) -> Option<String> {
if BLOCKED_HOSTS.contains(canonical_host.as_str()) {
return Some(format!(
"Blocked host: {} (cloud metadata endpoint)",
canonical_host
"Blocked host: {canonical_host} (cloud metadata endpoint)"
));
}
if let Some(ip) = literal_ip {
if is_private_ip(&ip) {
return Some(format!("URL resolves to private IP: {}", ip));
return Some(format!("URL resolves to private IP: {ip}"));
}
return None;
}
@@ -153,7 +152,7 @@ pub fn check_ssrf(url_str: &str) -> Option<String> {
_ => 80,
});
let addr_str = format!("{}:{}", canonical_host, port);
let addr_str = format!("{canonical_host}:{port}");
if let Ok(addrs) = addr_str.to_socket_addrs() {
for addr in addrs {
if is_private_ip(&addr.ip()) {
@@ -207,7 +206,7 @@ mod tests {
fn test_ipv4_mapped_ipv6_rfc1918_is_private() {
for s in ["::ffff:10.0.0.1", "::ffff:172.16.0.1", "::ffff:192.168.1.1"] {
let v6: Ipv6Addr = s.parse().unwrap();
assert!(is_private_ip(&IpAddr::V6(v6)), "{} should be private", s);
assert!(is_private_ip(&IpAddr::V6(v6)), "{s} should be private");
}
}
@@ -244,7 +243,7 @@ mod tests {
"http://[::ffff:172.16.0.1]/",
] {
let result = check_ssrf(url);
assert!(result.is_some(), "{} must be blocked", url);
assert!(result.is_some(), "{url} must be blocked");
}
}
+6 -8
View File
@@ -230,7 +230,7 @@ impl SessionStore {
params![session_id, remove_up_to as i64],
);
let summary = format!("[consolidated {} earlier messages]", remove_up_to);
let summary = format!("[consolidated {remove_up_to} earlier messages]");
let now = now_secs();
let _ = self.conn.execute(
"INSERT INTO session_messages (session_id, role, content, channel, timestamp)
@@ -292,15 +292,13 @@ impl SessionStore {
let cutoff = now_secs() - self.max_age_hours * 3600.0;
format!(
"SELECT session_id FROM sessions
WHERE last_activity >= {}
ORDER BY last_activity DESC LIMIT {}",
cutoff, limit
WHERE last_activity >= {cutoff}
ORDER BY last_activity DESC LIMIT {limit}"
)
} else {
format!(
"SELECT session_id FROM sessions
ORDER BY last_activity DESC LIMIT {}",
limit
ORDER BY last_activity DESC LIMIT {limit}"
)
};
@@ -356,7 +354,7 @@ impl SessionStore {
params![checkpoint_id, session_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.map_err(|e| format!("Checkpoint not found: {}", e))?;
.map_err(|e| format!("Checkpoint not found: {e}"))?;
let deleted: usize = self
.conn
@@ -573,7 +571,7 @@ mod tests {
for i in 0..10 {
store
.save_message(&s.session_id, "user", &format!("msg {}", i), "irc")
.save_message(&s.session_id, "user", &format!("msg {i}"), "irc")
.unwrap();
}
@@ -48,7 +48,7 @@ impl BaseTool for CalculatorTool {
Ok(result) => Ok(ToolResult::success("calculator", result.to_string())),
Err(e) => Ok(ToolResult::failure(
"calculator",
format!("Error evaluating '{}': {}", expression, e),
format!("Error evaluating '{expression}': {e}"),
)),
}
}
@@ -63,7 +63,7 @@ impl BaseTool for FileReadTool {
if is_sensitive_file(path) {
return Ok(ToolResult::failure(
"file_read",
format!("Access denied: '{}' is a sensitive file", path_str),
format!("Access denied: '{path_str}' is a sensitive file"),
));
}
@@ -71,7 +71,7 @@ impl BaseTool for FileReadTool {
Ok(content) => Ok(ToolResult::success("file_read", content)),
Err(e) => Ok(ToolResult::failure(
"file_read",
format!("Error reading '{}': {}", path_str, e),
format!("Error reading '{path_str}': {e}"),
)),
}
}
@@ -94,7 +94,7 @@ impl BaseTool for FileWriteTool {
if is_sensitive_file(path) {
return Ok(ToolResult::failure(
"file_write",
format!("Access denied: '{}' is a sensitive file", path_str),
format!("Access denied: '{path_str}' is a sensitive file"),
));
}
@@ -103,7 +103,7 @@ impl BaseTool for FileWriteTool {
if let Err(e) = std::fs::create_dir_all(parent) {
return Ok(ToolResult::failure(
"file_write",
format!("Error creating directory: {}", e),
format!("Error creating directory: {e}"),
));
}
}
@@ -116,7 +116,7 @@ impl BaseTool for FileWriteTool {
)),
Err(e) => Ok(ToolResult::failure(
"file_write",
format!("Error writing '{}': {}", path_str, e),
format!("Error writing '{path_str}': {e}"),
)),
}
}
@@ -21,7 +21,7 @@ fn run_git(args: &[&str], cwd: Option<&str>) -> Result<String, String> {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
Err(e) => Err(format!("Failed to run git: {}", e)),
Err(e) => Err(format!("Failed to run git: {e}")),
}
}
@@ -84,7 +84,7 @@ impl BaseTool for GitLogTool {
fn execute(&self, params: &Value) -> Result<ToolResult, OpenJarvisError> {
let cwd = params["cwd"].as_str();
let n = params["n"].as_i64().unwrap_or(10);
match run_git(&["log", "--oneline", &format!("-{}", n)], cwd) {
match run_git(&["log", "--oneline", &format!("-{n}")], cwd) {
Ok(output) => Ok(ToolResult::success("git_log", output)),
Err(e) => Ok(ToolResult::failure("git_log", e)),
}
@@ -83,7 +83,7 @@ impl BaseTool for HttpRequestTool {
} else {
body
};
let content = format!("Status: {}\n{}", status, truncated);
let content = format!("Status: {status}\n{truncated}");
if status < 400 {
Ok(ToolResult::success("http_request", content))
} else {
@@ -92,7 +92,7 @@ impl BaseTool for HttpRequestTool {
}
Err(e) => Ok(ToolResult::failure(
"http_request",
format!("Request failed: {}", e),
format!("Request failed: {e}"),
)),
}
}
@@ -61,8 +61,7 @@ impl BaseTool for ShellExecTool {
let exit_code = output.status.code().unwrap_or(-1);
let content = format!(
"Exit code: {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
exit_code, stdout, stderr
"Exit code: {exit_code}\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
if output.status.success() {
@@ -73,7 +72,7 @@ impl BaseTool for ShellExecTool {
}
Err(e) => Ok(ToolResult::failure(
"shell_exec",
format!("Failed to execute: {}", e),
format!("Failed to execute: {e}"),
)),
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ impl ToolExecutor {
if !policy.check(aid, cap, "") {
return Err(OpenJarvisError::Tool(ToolError::CapabilityDenied(
aid.to_string(),
format!("{} (tool: {})", cap, tool_name),
format!("{cap} (tool: {tool_name})"),
)));
}
}
@@ -170,7 +170,7 @@ impl MemoryBackend for KnowledgeGraphMemory {
top_k: usize,
) -> Result<Vec<RetrievalResult>, OpenJarvisError> {
let conn = self.conn.lock();
let pattern = format!("%{}%", query);
let pattern = format!("%{query}%");
let mut stmt = conn
.prepare(
"SELECT name, entity_type, properties
+2 -2
View File
@@ -233,8 +233,8 @@ mod tests {
let store = TraceStore::in_memory().unwrap();
for i in 0..5 {
let trace = Trace {
trace_id: format!("t{}", i),
query: format!("query {}", i),
trace_id: format!("t{i}"),
query: format!("query {i}"),
..Default::default()
};
store.save(&trace).unwrap();
+10
View File
@@ -0,0 +1,10 @@
# Pin the Rust toolchain for the openjarvis_rust workspace.
#
# The workspace requires Rust >= 1.88: rig-core uses let-chains and
# openjarvis-skills uses `is_multiple_of`, both stabilized in 1.88. On older
# stable toolchains the build fails with cryptic E0658 errors deep inside a
# transitive dependency (see #252). Pinning here makes rustup install/select a
# compatible toolchain automatically and gives a clear requirement up front.
[toolchain]
channel = "1.88"
components = ["rustfmt", "clippy"]