fix: handle Python None params in http tool, add params_summary to CodeAct dispatch

Two fixes from live testing:

1. http tool: treat null headers/body as empty (Python's None becomes
   JSON null via Monty). Previously headers=None errored with
   "'headers' must be an object or array of {name, value}".

2. scripting.rs: compute params_summary before dispatching actions in
   the CodeAct path (was always None). Now http calls show their URL
   in the CLI: ● http(https://api.github.com/repos/.../issues)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-27 23:45:17 -07:00
co-authored by Claude Opus 4.6
parent 6d6b7fab0e
commit a12188e231
2 changed files with 26 additions and 21 deletions
@@ -1035,6 +1035,8 @@ async fn dispatch_action(
)));
}
let ps = crate::types::event::summarize_params(action_name, &params);
match effects
.execute_action(action_name, params, &lease, context)
.await
@@ -1045,7 +1047,7 @@ async fn dispatch_action(
action_name: action_name.into(),
call_id: call_id.into(),
duration_ms: result.duration.as_millis() as u64,
params_summary: None,
params_summary: ps,
});
let monty_obj = json_to_monty(&result.output);
action_results.push(result);
@@ -1064,7 +1066,7 @@ async fn dispatch_action(
action_name: action_name.into(),
call_id: call_id.into(),
error: e.to_string(),
params_summary: None,
params_summary: ps,
});
DispatchResult::Ok(ExtFunctionResult::Error(MontyException::new(
ExcType::RuntimeError,
+22 -19
View File
@@ -10,10 +10,10 @@ use futures::StreamExt;
use reqwest::Client;
use crate::context::JobContext;
use crate::safety::LeakDetector;
use crate::secrets::SecretsStore;
use crate::tools::tool::{ApprovalRequirement, Tool, ToolError, ToolOutput, require_str};
use crate::tools::wasm::{InjectedCredentials, SharedCredentialRegistry, inject_credential};
use ironclaw_safety::LeakDetector;
#[cfg(feature = "html-to-markdown")]
use crate::tools::builtin::convert_html_to_markdown;
@@ -285,7 +285,7 @@ fn parse_headers_param(
}
match headers {
None => Ok(Vec::new()),
None | Some(serde_json::Value::Null) => Ok(Vec::new()),
Some(serde_json::Value::String(raw)) => {
let trimmed = raw.trim();
if trimmed.is_empty() {
@@ -472,12 +472,7 @@ impl Tool for HttpTool {
if let Some(registry) = self.credential_registry.as_ref() {
let cred_host = parsed_url.host_str().unwrap_or("");
if registry.has_credentials_for_host(cred_host) {
let forbidden: &[&str] = &[
"authorization",
"x-api-key",
"api-key",
"x-auth-token",
];
let forbidden: &[&str] = &["authorization", "x-api-key", "api-key", "x-auth-token"];
for (name, _) in &headers_vec {
if forbidden.iter().any(|f| name.eq_ignore_ascii_case(f)) {
return Err(ToolError::NotAuthorized(format!(
@@ -516,8 +511,10 @@ impl Tool for HttpTool {
request = request.header(key.as_str(), value.as_str());
}
// Add body if present
let body_bytes = if let Some(body) = params.get("body") {
// Add body if present (skip null — Python's None becomes JSON null)
let body_bytes = if let Some(body) = params.get("body")
&& !body.is_null()
{
if let Some(body_str) = body.as_str() {
if body_str.is_empty() {
None
@@ -908,7 +905,7 @@ impl Tool for HttpTool {
}
fn requires_approval(&self, params: &serde_json::Value) -> ApprovalRequirement {
let has_credentials = crate::safety::params_contain_manual_credentials(params)
let has_credentials = ironclaw_safety::params_contain_manual_credentials(params)
|| (self.credential_registry.as_ref().is_some_and(|registry| {
extract_host_from_params(params)
.is_some_and(|host| registry.has_credentials_for_host(&host))
@@ -1582,12 +1579,18 @@ mod tests {
assert!(registry.has_credentials_for_host(cred_host));
let forbidden: &[&str] = &["authorization", "x-api-key", "api-key", "x-auth-token"];
let llm_headers = [("Authorization".to_string(), "Bearer stolen_token".to_string())];
let llm_headers = [(
"Authorization".to_string(),
"Bearer stolen_token".to_string(),
)];
let blocked = llm_headers.iter().any(|(name, _)| {
forbidden.iter().any(|f| name.eq_ignore_ascii_case(f))
});
assert!(blocked, "LLM-provided Authorization header should be blocked");
let blocked = llm_headers
.iter()
.any(|(name, _)| forbidden.iter().any(|f| name.eq_ignore_ascii_case(f)));
assert!(
blocked,
"LLM-provided Authorization header should be blocked"
);
}
#[test]
@@ -1607,9 +1610,9 @@ mod tests {
("Content-Type".to_string(), "application/json".to_string()),
];
let blocked = llm_headers.iter().any(|(name, _)| {
forbidden.iter().any(|f| name.eq_ignore_ascii_case(f))
});
let blocked = llm_headers
.iter()
.any(|(name, _)| forbidden.iter().any(|f| name.eq_ignore_ascii_case(f)));
assert!(!blocked, "Non-auth headers should not be blocked");
}