From 2c87a4f7bdb975f6f9e3bce7bbaf4567b18b52df Mon Sep 17 00:00:00 2001 From: Srinivas Vaddi <38348871+vaddisrinivas@users.noreply.github.com> Date: Wed, 20 May 2026 07:00:53 -0400 Subject: [PATCH 1/3] Thread tool call context through policy --- src/openhuman/agent/harness/session/turn.rs | 18 +++++-- .../agent/harness/session/turn_tests.rs | 5 ++ src/openhuman/agent/tool_policy.rs | 49 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/openhuman/agent/harness/session/turn.rs b/src/openhuman/agent/harness/session/turn.rs index 88814f6aa6..526417ef91 100644 --- a/src/openhuman/agent/harness/session/turn.rs +++ b/src/openhuman/agent/harness/session/turn.rs @@ -25,7 +25,9 @@ use crate::openhuman::agent::harness; use crate::openhuman::agent::hooks::{self, ToolCallRecord, TurnContext}; use crate::openhuman::agent::memory_loader::collect_recall_citations; use crate::openhuman::agent::progress::AgentProgress; -use crate::openhuman::agent::tool_policy::{ToolPolicyDecision, ToolPolicyRequest}; +use crate::openhuman::agent::tool_policy::{ + ToolCallContext, ToolPolicyDecision, ToolPolicyRequest, +}; use crate::openhuman::agent_experience::{ prepend_experience_block, render_experience_hits, AgentExperienceStore, ExperienceQuery, }; @@ -1145,12 +1147,20 @@ impl Agent { false, ) } else if let Some(tool) = self.tools.iter().find(|t| t.name() == call.name) { + let context = ToolCallContext::session( + self.event_session_id(), + self.event_channel(), + self.agent_definition_id.to_string(), + call_id.clone(), + (iteration + 1) as u32, + ); let policy_request = ToolPolicyRequest { tool_name: call.name.clone(), arguments: call.arguments.clone(), - session_id: self.event_session_id().to_string(), - channel: self.event_channel().to_string(), - agent_definition_id: self.agent_definition_id.to_string(), + session_id: context.session_id.clone(), + channel: context.channel.clone(), + agent_definition_id: context.agent_definition_id.clone(), + context, }; if let ToolPolicyDecision::Deny { reason } = self.tool_policy.check(&policy_request).await diff --git a/src/openhuman/agent/harness/session/turn_tests.rs b/src/openhuman/agent/harness/session/turn_tests.rs index 91456c96fc..f512dbae4c 100644 --- a/src/openhuman/agent/harness/session/turn_tests.rs +++ b/src/openhuman/agent/harness/session/turn_tests.rs @@ -145,6 +145,11 @@ impl ToolPolicy for DenyCountingPolicy { assert_eq!(request.session_id, "turn-test-session"); assert_eq!(request.channel, "turn-test-channel"); assert_eq!(request.agent_definition_id, "main"); + assert_eq!(request.context.session_id, "turn-test-session"); + assert_eq!(request.context.channel, "turn-test-channel"); + assert_eq!(request.context.agent_definition_id, "main"); + assert_eq!(request.context.call_id, "policy-1"); + assert_eq!(request.context.iteration, 1); ToolPolicyDecision::deny("locked by test policy") } } diff --git a/src/openhuman/agent/tool_policy.rs b/src/openhuman/agent/tool_policy.rs index 3028ad65c0..0f43bdf44e 100644 --- a/src/openhuman/agent/tool_policy.rs +++ b/src/openhuman/agent/tool_policy.rs @@ -6,13 +6,59 @@ use async_trait::async_trait; +/// Structured context for a tool call before it reaches the tool +/// implementation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToolCallContext { + pub session_id: String, + pub channel: String, + pub agent_definition_id: String, + pub call_id: String, + pub iteration: u32, + pub source: ToolCallSource, +} + +impl ToolCallContext { + pub fn session( + session_id: impl Into, + channel: impl Into, + agent_definition_id: impl Into, + call_id: impl Into, + iteration: u32, + ) -> Self { + Self { + session_id: session_id.into(), + channel: channel.into(), + agent_definition_id: agent_definition_id.into(), + call_id: call_id.into(), + iteration, + source: ToolCallSource::Session, + } + } +} + +/// Entry point that produced a tool call. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToolCallSource { + Session, + Bus, + Channel, + Cron, + Webhook, + Unknown, +} + /// Snapshot of the tool call and session context a policy can inspect. #[derive(Debug, Clone)] pub struct ToolPolicyRequest { pub tool_name: String, pub arguments: serde_json::Value, + pub context: ToolCallContext, + /// Backward-compatible mirror of `context.session_id`. pub session_id: String, + /// Backward-compatible mirror of `context.channel`. pub channel: String, + /// Backward-compatible mirror of `context.agent_definition_id`. pub agent_definition_id: String, } @@ -66,11 +112,14 @@ mod tests { let request = ToolPolicyRequest { tool_name: "echo".into(), arguments: serde_json::json!({ "value": 1 }), + context: ToolCallContext::session("session", "chat", "orchestrator", "call-1", 1), session_id: "session".into(), channel: "chat".into(), agent_definition_id: "orchestrator".into(), }; assert_eq!(policy.check(&request).await, ToolPolicyDecision::Allow); + assert_eq!(request.context.source, ToolCallSource::Session); + assert_eq!(request.context.call_id, "call-1"); } } From 7a9d66d1d934cd2363e283e48836585501b7f038 Mon Sep 17 00:00:00 2001 From: Srinivas Vaddi <38348871+vaddisrinivas@users.noreply.github.com> Date: Wed, 20 May 2026 11:45:18 -0400 Subject: [PATCH 2/3] Add tool policy request constructor --- src/openhuman/agent/harness/session/turn.rs | 10 ++---- src/openhuman/agent/tool_policy.rs | 36 ++++++++++++++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/openhuman/agent/harness/session/turn.rs b/src/openhuman/agent/harness/session/turn.rs index 526417ef91..30ab9a7312 100644 --- a/src/openhuman/agent/harness/session/turn.rs +++ b/src/openhuman/agent/harness/session/turn.rs @@ -1154,14 +1154,8 @@ impl Agent { call_id.clone(), (iteration + 1) as u32, ); - let policy_request = ToolPolicyRequest { - tool_name: call.name.clone(), - arguments: call.arguments.clone(), - session_id: context.session_id.clone(), - channel: context.channel.clone(), - agent_definition_id: context.agent_definition_id.clone(), - context, - }; + let policy_request = + ToolPolicyRequest::new(call.name.clone(), call.arguments.clone(), context); if let ToolPolicyDecision::Deny { reason } = self.tool_policy.check(&policy_request).await { diff --git a/src/openhuman/agent/tool_policy.rs b/src/openhuman/agent/tool_policy.rs index 0f43bdf44e..e3286dbee7 100644 --- a/src/openhuman/agent/tool_policy.rs +++ b/src/openhuman/agent/tool_policy.rs @@ -62,6 +62,23 @@ pub struct ToolPolicyRequest { pub agent_definition_id: String, } +impl ToolPolicyRequest { + pub fn new( + tool_name: impl Into, + arguments: serde_json::Value, + context: ToolCallContext, + ) -> Self { + Self { + tool_name: tool_name.into(), + arguments, + session_id: context.session_id.clone(), + channel: context.channel.clone(), + agent_definition_id: context.agent_definition_id.clone(), + context, + } + } +} + /// Decision returned by a [`ToolPolicy`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ToolPolicyDecision { @@ -109,16 +126,19 @@ mod tests { #[tokio::test] async fn allow_all_policy_allows_every_call() { let policy = AllowAllToolPolicy; - let request = ToolPolicyRequest { - tool_name: "echo".into(), - arguments: serde_json::json!({ "value": 1 }), - context: ToolCallContext::session("session", "chat", "orchestrator", "call-1", 1), - session_id: "session".into(), - channel: "chat".into(), - agent_definition_id: "orchestrator".into(), - }; + let request = ToolPolicyRequest::new( + "echo", + serde_json::json!({ "value": 1 }), + ToolCallContext::session("session", "chat", "orchestrator", "call-1", 1), + ); assert_eq!(policy.check(&request).await, ToolPolicyDecision::Allow); + assert_eq!(request.session_id, request.context.session_id); + assert_eq!(request.channel, request.context.channel); + assert_eq!( + request.agent_definition_id, + request.context.agent_definition_id + ); assert_eq!(request.context.source, ToolCallSource::Session); assert_eq!(request.context.call_id, "call-1"); } From 9200fd2ac6745f51ecbad7fe7f203066f151c78a Mon Sep 17 00:00:00 2001 From: Srinivas Vaddi <38348871+vaddisrinivas@users.noreply.github.com> Date: Wed, 20 May 2026 15:03:41 -0400 Subject: [PATCH 3/3] Redact tool policy context debug output --- .../agent/harness/session/turn_tests.rs | 3 - src/openhuman/agent/tool_policy.rs | 101 +++++++++++++++--- 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/src/openhuman/agent/harness/session/turn_tests.rs b/src/openhuman/agent/harness/session/turn_tests.rs index f512dbae4c..eea9aedf74 100644 --- a/src/openhuman/agent/harness/session/turn_tests.rs +++ b/src/openhuman/agent/harness/session/turn_tests.rs @@ -142,9 +142,6 @@ impl ToolPolicy for DenyCountingPolicy { async fn check(&self, request: &ToolPolicyRequest) -> ToolPolicyDecision { assert_eq!(request.tool_name, "counting"); - assert_eq!(request.session_id, "turn-test-session"); - assert_eq!(request.channel, "turn-test-channel"); - assert_eq!(request.agent_definition_id, "main"); assert_eq!(request.context.session_id, "turn-test-session"); assert_eq!(request.context.channel, "turn-test-channel"); assert_eq!(request.context.agent_definition_id, "main"); diff --git a/src/openhuman/agent/tool_policy.rs b/src/openhuman/agent/tool_policy.rs index e3286dbee7..e9e26049ac 100644 --- a/src/openhuman/agent/tool_policy.rs +++ b/src/openhuman/agent/tool_policy.rs @@ -5,10 +5,11 @@ //! deny a tool before any side effect reaches the tool implementation. use async_trait::async_trait; +use std::fmt; /// Structured context for a tool call before it reaches the tool /// implementation. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct ToolCallContext { pub session_id: String, pub channel: String, @@ -37,8 +38,22 @@ impl ToolCallContext { } } +impl fmt::Debug for ToolCallContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ToolCallContext") + .field("session_id", &redact_for_debug(&self.session_id)) + .field("channel", &redact_for_debug(&self.channel)) + .field("agent_definition_id", &self.agent_definition_id) + .field("call_id", &self.call_id) + .field("iteration", &self.iteration) + .field("source", &self.source) + .finish() + } +} + /// Entry point that produced a tool call. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] // Reserved for non-session tool ingress paths wired in follow-up PRs. pub enum ToolCallSource { Session, Bus, @@ -49,36 +64,67 @@ pub enum ToolCallSource { } /// Snapshot of the tool call and session context a policy can inspect. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ToolPolicyRequest { pub tool_name: String, pub arguments: serde_json::Value, pub context: ToolCallContext, /// Backward-compatible mirror of `context.session_id`. + #[deprecated(note = "use context.session_id")] pub session_id: String, /// Backward-compatible mirror of `context.channel`. + #[deprecated(note = "use context.channel")] pub channel: String, /// Backward-compatible mirror of `context.agent_definition_id`. + #[deprecated(note = "use context.agent_definition_id")] pub agent_definition_id: String, } +impl fmt::Debug for ToolPolicyRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[allow(deprecated)] + { + f.debug_struct("ToolPolicyRequest") + .field("tool_name", &self.tool_name) + .field("arguments", &"") + .field("context", &self.context) + .field("session_id", &redact_for_debug(&self.session_id)) + .field("channel", &redact_for_debug(&self.channel)) + .field("agent_definition_id", &self.agent_definition_id) + .finish() + } + } +} + impl ToolPolicyRequest { pub fn new( tool_name: impl Into, arguments: serde_json::Value, context: ToolCallContext, ) -> Self { - Self { - tool_name: tool_name.into(), - arguments, - session_id: context.session_id.clone(), - channel: context.channel.clone(), - agent_definition_id: context.agent_definition_id.clone(), - context, + #[allow(deprecated)] + { + Self { + tool_name: tool_name.into(), + arguments, + session_id: context.session_id.clone(), + channel: context.channel.clone(), + agent_definition_id: context.agent_definition_id.clone(), + context, + } } } } +fn redact_for_debug(value: &str) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + return "".to_string(); + } + let prefix: String = trimmed.chars().take(4).collect(); + format!("{prefix}...") +} + /// Decision returned by a [`ToolPolicy`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ToolPolicyDecision { @@ -133,13 +179,38 @@ mod tests { ); assert_eq!(policy.check(&request).await, ToolPolicyDecision::Allow); - assert_eq!(request.session_id, request.context.session_id); - assert_eq!(request.channel, request.context.channel); - assert_eq!( - request.agent_definition_id, - request.context.agent_definition_id - ); + #[allow(deprecated)] + { + assert_eq!(request.session_id, request.context.session_id); + assert_eq!(request.channel, request.context.channel); + assert_eq!( + request.agent_definition_id, + request.context.agent_definition_id + ); + } assert_eq!(request.context.source, ToolCallSource::Session); assert_eq!(request.context.call_id, "call-1"); } + + #[test] + fn debug_redacts_sensitive_context_fields() { + let request = ToolPolicyRequest::new( + "secrets.lookup", + serde_json::json!({ "secret": "super-secret-token" }), + ToolCallContext::session( + "session-secret-123", + "private-channel", + "orchestrator", + "call-1", + 1, + ), + ); + + let rendered = format!("{request:?}"); + assert!(rendered.contains("sess...")); + assert!(rendered.contains("priv...")); + assert!(!rendered.contains("session-secret-123")); + assert!(!rendered.contains("private-channel")); + assert!(!rendered.contains("super-secret-token")); + } }