Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/lib/i18n/chunks/de-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ const de5: TranslationMap = {
'settings.mascot.colorGreen': 'Grün',
'settings.mascot.colorNavy': 'Marine',
'settings.mascot.colorYellow': 'Gelb',
'settings.mascot.customGifError':
'GIF konnte nicht geladen werden. Bitte überprüfe die URL und versuche es erneut.',
'settings.mascot.customGifHeading': 'Benutzerdefinierter GIF-Avatar',
'settings.mascot.customGifLabel': 'URL für benutzerdefinierten GIF-Avatar',
'settings.mascot.libraryUnavailable': 'OpenHuman Bibliothek nicht verfügbar',
'settings.mascot.title': 'OpenHuman',
};
Expand Down
4 changes: 4 additions & 0 deletions src/openhuman/agent/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ pub fn register_agent_handlers() {
// wired into the orchestrator session via Agent::turn,
// not the bus dispatcher.
None,
// Use the default (allow-all) tool policy. Custom
// policies can be wired in via AgentTurnRequest when
// per-channel policy configuration is added (#2134).
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
})
Expand Down
9 changes: 9 additions & 0 deletions src/openhuman/agent/harness/bughunt_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ async fn native_tool_call_decodes_json_encoded_arguments_string() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -162,6 +163,7 @@ async fn documents_silent_drop_of_non_json_arguments_string() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -218,6 +220,7 @@ async fn parallel_tool_calls_in_single_iteration_all_execute() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -260,6 +263,7 @@ async fn same_named_tool_in_registry_first_match_wins() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -312,6 +316,7 @@ async fn markdown_fenced_tool_call_block_is_parsed() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -365,6 +370,7 @@ async fn native_tool_calls_take_precedence_over_xml_in_text() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -424,6 +430,7 @@ async fn per_tool_max_result_size_caps_history_payload() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -475,6 +482,7 @@ async fn empty_response_with_no_tool_calls_terminates_with_empty_text() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -518,6 +526,7 @@ async fn progress_sink_emits_lifecycle_events_in_order() {
&[],
Some(tx),
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down
4 changes: 4 additions & 0 deletions src/openhuman/agent/harness/harness_gap_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ async fn full_turn_cycle_user_llm_tool_result_final() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("full turn cycle should succeed");
Expand Down Expand Up @@ -210,6 +211,7 @@ async fn max_iterations_exceeded_downcasts_to_typed_agent_error() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect_err("loop must fail when iterations exhausted");
Expand Down Expand Up @@ -285,6 +287,7 @@ async fn visible_tool_names_rejects_tool_outside_whitelist() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("loop should recover after whitelisted-out tool call");
Expand Down Expand Up @@ -342,6 +345,7 @@ async fn visible_tool_names_allows_tool_inside_whitelist() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("whitelisted tool should execute");
Expand Down
17 changes: 17 additions & 0 deletions src/openhuman/agent/harness/test_support_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ async fn keyword_provider_drives_prompt_guided_tool_loop_to_completion() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("loop should complete");
Expand Down Expand Up @@ -451,6 +452,7 @@ async fn keyword_provider_drives_native_tool_calls_path() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("loop should complete");
Expand Down Expand Up @@ -506,6 +508,7 @@ async fn keyword_provider_chains_multiple_tools_across_iterations() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -623,6 +626,7 @@ async fn crypto_wallet_send_flow_sequences_wallet_tools_and_confirmation_gate()
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("crypto wallet flow should complete");
Expand Down Expand Up @@ -735,6 +739,7 @@ async fn crypto_wallet_send_flow_does_not_execute_when_confirmation_is_not_grant
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("declined flow should still complete");
Expand Down Expand Up @@ -795,6 +800,7 @@ async fn keyword_provider_uses_latest_tool_result_to_drive_the_next_tool_call()
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("loop should complete");
Expand Down Expand Up @@ -868,6 +874,7 @@ async fn keyword_provider_executes_multiple_native_tool_calls_from_one_turn() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("loop should complete");
Expand Down Expand Up @@ -916,6 +923,7 @@ async fn keyword_provider_unknown_tool_surfaces_error_and_loop_continues() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -965,6 +973,7 @@ async fn run_tool_call_loop_returns_max_iterations_error() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect_err("should hit max iterations");
Expand Down Expand Up @@ -1034,6 +1043,7 @@ async fn agent_loop_refuses_clirpconly_tools() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1093,6 +1103,7 @@ async fn tool_error_result_is_surfaced_to_next_iteration() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1148,6 +1159,7 @@ async fn tool_anyhow_error_surfaces_in_history() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1192,6 +1204,7 @@ async fn visible_tool_names_whitelist_rejects_filtered_out_tools() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1237,6 +1250,7 @@ async fn extra_tools_are_invokable_alongside_registry() {
&extras,
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1391,6 +1405,7 @@ async fn harness_invokes_composio_action_tool_against_fake_backend() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.unwrap();
Expand Down Expand Up @@ -1537,6 +1552,7 @@ impl Tool for TestDelegationTool {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await?;

Expand Down Expand Up @@ -1679,6 +1695,7 @@ async fn orchestrator_prompt_drives_composio_call_via_delegation_chain() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("orchestrator loop should complete");
Expand Down
3 changes: 3 additions & 0 deletions src/openhuman/agent/harness/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async fn run_tool_call_loop_returns_structured_error_for_non_vision_provider() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect_err("provider without vision support should fail");
Expand Down Expand Up @@ -173,6 +174,7 @@ async fn run_tool_call_loop_rejects_oversized_image_payload() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect_err("oversized payload must fail");
Expand Down Expand Up @@ -212,6 +214,7 @@ async fn run_tool_call_loop_accepts_valid_multimodal_request_flow() {
&[],
None,
None,
&crate::openhuman::tools::policy::DefaultToolPolicy,
)
.await
.expect("valid multimodal payload should pass");
Expand Down
28 changes: 28 additions & 0 deletions src/openhuman/agent/harness/tool_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::openhuman::approval::{ApprovalManager, ApprovalRequest, ApprovalRespo
use crate::openhuman::inference::provider::{
ChatMessage, ChatRequest, Provider, ProviderCapabilityError, ProviderDelta,
};
use crate::openhuman::tools::policy::{DefaultToolPolicy, PolicyDecision, ToolPolicy};
use crate::openhuman::tools::traits::ToolScope;
use crate::openhuman::tools::Tool;
use anyhow::Result;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub(crate) async fn agent_turn(
max_tool_iterations: usize,
payload_summarizer: Option<&dyn PayloadSummarizer>,
) -> Result<String> {
let default_policy = DefaultToolPolicy;
run_tool_call_loop(
provider,
history,
Expand All @@ -66,6 +68,7 @@ pub(crate) async fn agent_turn(
&[],
None,
payload_summarizer,
&default_policy,
)
.await
}
Expand Down Expand Up @@ -117,6 +120,7 @@ pub(crate) async fn run_tool_call_loop(
extra_tools: &[Box<dyn Tool>],
on_progress: Option<tokio::sync::mpsc::Sender<AgentProgress>>,
payload_summarizer: Option<&dyn PayloadSummarizer>,
tool_policy: &dyn ToolPolicy,
) -> Result<String> {
let max_iterations = if max_tool_iterations == 0 {
DEFAULT_MAX_TOOL_ITERATIONS
Expand Down Expand Up @@ -609,6 +613,30 @@ pub(crate) async fn run_tool_call_loop(
}
};

// ── Tool policy check (#2131) ─────────────────
// Evaluate the pluggable ToolPolicy before any approval or
// execution. If the policy denies the call, skip everything
// (including approval side-effects) and return the denial
// reason as a tool error to the model.
if let PolicyDecision::Deny(reason) = tool_policy.evaluate(&call.name, &call.arguments)
{
tracing::debug!(
iteration,
tool = call.name.as_str(),
reason = %reason,
"[agent_loop] tool policy denied tool call"
);
let denied = format!("Tool '{}' denied by policy: {reason}", call.name);
emit_failed_completion(&denied).await;
individual_results.push(denied.clone());
let _ = writeln!(
tool_results,
"<tool_result name=\"{}\">\n{denied}\n</tool_result>",
call.name
);
continue;
}

// ── Approval hook ────────────────────────────────
if let Some(mgr) = approval {
if mgr.needs_approval(&call.name) {
Expand Down
Loading
Loading