Skip to content
22 changes: 0 additions & 22 deletions app/src/lib/i18n/chunks/de-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,28 +523,6 @@ const de5: TranslationMap = {
'settings.mascot.colorYellow': 'Gelb',
'settings.mascot.libraryUnavailable': 'OpenHuman Bibliothek nicht verfügbar',
'settings.mascot.title': 'OpenHuman',
'settings.developerMenu.mcpServer.title': 'MCP-Server',
'settings.developerMenu.mcpServer.desc':
'Externe MCP-Clients zur Verbindung mit OpenHuman konfigurieren',
'settings.mcpServer.title': 'MCP-Server',
'settings.mcpServer.toolsSectionTitle': 'Verfügbare Tools',
'settings.mcpServer.toolsSectionDesc':
'Tools, die über den MCP-Stdio-Server bereitgestellt werden, wenn openhuman-core mcp ausgeführt wird',
'settings.mcpServer.configSectionTitle': 'Client-Konfiguration',
'settings.mcpServer.configSectionDesc':
'Wählen Sie Ihren MCP-Client aus, um den passenden Konfigurations-Schnipsel zu erzeugen',
'settings.mcpServer.copySnippet': 'In Zwischenablage kopieren',
'settings.mcpServer.copied': 'Kopiert!',
'settings.mcpServer.openConfigFile': 'Konfigurationsdatei öffnen',
'settings.mcpServer.binaryPathNotFound':
'OpenHuman-Binary nicht gefunden. Wenn Sie aus dem Quellcode arbeiten, bauen Sie mit: cargo build --bin openhuman-core',
'settings.mcpServer.openConfigError': 'Konfigurationsdatei konnte nicht geöffnet werden',
'settings.mcpServer.clientClaudeDesktop': 'Claude Desktop',
'settings.mcpServer.clientCursor': 'Cursor',
'settings.mcpServer.clientCodex': 'Codex',
'settings.mcpServer.clientZed': 'Zed',
'settings.mcpServer.configFilePath': 'Konfigurationsdatei',
'settings.mcpServer.clientSelectorAriaLabel': 'MCP-Client-Auswahl',
};

export default de5;
68 changes: 55 additions & 13 deletions src/openhuman/agent/harness/subagent_runner/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1529,16 +1529,33 @@ async fn run_inner_loop(
{
let args = parse_tool_arguments(&call.arguments);
let timeout = crate::openhuman::tool_timeout::tool_execution_timeout_duration();
// ── External-effect approval gate (#1339) ────
// ── External-effect approval gate (#1339, #2135)
// Subagents share the same gate as the parent loop;
// see `tool_loop.rs` for the rationale.
//
// When the call is allowed and persisted, we keep
// hold of the `request_id` so we can stamp the
// terminal execution outcome onto the same audit
// row (issue #2135).
let mut approval_request_id: Option<String> = None;
let mut approval_gate_for_audit: Option<
std::sync::Arc<crate::openhuman::approval::ApprovalGate>,
> = None;
let gate_denial: Option<String> = if tool.external_effect_with_args(&args) {
if let Some(gate) = crate::openhuman::approval::ApprovalGate::try_global() {
let summary =
crate::openhuman::approval::summarize_action(&call.name, &args);
let redacted = crate::openhuman::approval::redact_args(&args);
match gate.intercept(&call.name, &summary, redacted).await {
crate::openhuman::approval::GateOutcome::Allow => None,
let (outcome, request_id) =
gate.intercept_audited(&call.name, &summary, redacted).await;
match outcome {
crate::openhuman::approval::GateOutcome::Allow => {
approval_request_id = request_id;
if approval_request_id.is_some() {
approval_gate_for_audit = Some(gate);
}
None
}
crate::openhuman::approval::GateOutcome::Deny { reason } => {
tracing::warn!(
tool = call.name.as_str(),
Expand All @@ -1563,18 +1580,43 @@ async fn run_inner_loop(
// (CodeRabbit review on PR #2149.)
format!("Error: {reason}")
} else {
match tokio::time::timeout(timeout, tool.execute(args)).await {
Ok(Ok(result)) => {
let raw = result.output();
if result.is_error {
format!("Error: {raw}")
} else {
raw
let (raw, exec_success) =
match tokio::time::timeout(timeout, tool.execute(args)).await {
Ok(Ok(result)) => {
let raw = result.output();
if result.is_error {
(format!("Error: {raw}"), false)
} else {
(raw, true)
}
}
}
Ok(Err(err)) => format!("Error executing {}: {err}", call.name),
Err(_) => format!("Error: tool '{}' timed out", call.name),
Ok(Err(err)) => {
(format!("Error executing {}: {err}", call.name), false)
}
Err(_) => (format!("Error: tool '{}' timed out", call.name), false),
};
// Stamp the terminal status onto the
// pending_approvals audit row — best-effort,
// failures don't propagate to the agent (#2135).
// Success comes from the structured execute result,
// not from parsing `raw.starts_with("Error")` — a
// legitimate success payload can start with "Error"
// (search hits, copied logs), which would otherwise
// persist a false Failure (CodeRabbit review on #2367).
if let (Some(gate), Some(req_id)) = (
approval_gate_for_audit.as_ref(),
approval_request_id.as_ref(),
) {
let success = exec_success;
let exec_outcome = if success {
crate::openhuman::approval::ExecutionOutcome::Success
} else {
crate::openhuman::approval::ExecutionOutcome::Failure
};
let err_text = if success { None } else { Some(raw.as_str()) };
gate.record_execution(req_id, exec_outcome, err_text);
}
raw
}
} else {
format!("Unknown tool: {}", call.name)
Expand Down
49 changes: 46 additions & 3 deletions src/openhuman/agent/harness/tool_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,12 +680,25 @@ pub(crate) async fn run_tool_call_loop(
}
}

// ── External-effect approval gate (#1339) ─────────
// ── External-effect approval gate (#1339, #2135) ──
// Tools whose `external_effect()` returns true route
// through the process-global `ApprovalGate` so the UI
// can prompt the user before `execute()` runs. The gate
// is `None` when supervised mode is disabled or in test
// envs — behavior matches the pre-#1339 path.
//
// `approval_request_id` carries the persisted row id
// forward so we can stamp the terminal execution
// outcome onto the same `pending_approvals` row after
// the tool finishes (issue #2135). `None` means the
// tool was either not gated (no supervised gate, not
// external-effect), was session-allowlist-shortcutted,
// or was denied — none of which produce an audit row
// that needs an "after" entry.
let mut approval_request_id: Option<String> = None;
let mut approval_gate_for_audit: Option<
std::sync::Arc<crate::openhuman::approval::ApprovalGate>,
> = None;
if let Some(tool) = tool_opt {
if tool.external_effect_with_args(&call.arguments) {
if let Some(gate) = crate::openhuman::approval::ApprovalGate::try_global() {
Expand All @@ -694,8 +707,15 @@ pub(crate) async fn run_tool_call_loop(
&call.arguments,
);
let redacted = crate::openhuman::approval::redact_args(&call.arguments);
match gate.intercept(&call.name, &summary, redacted).await {
crate::openhuman::approval::GateOutcome::Allow => {}
let (outcome, request_id) =
gate.intercept_audited(&call.name, &summary, redacted).await;
match outcome {
crate::openhuman::approval::GateOutcome::Allow => {
approval_request_id = request_id;
if approval_request_id.is_some() {
approval_gate_for_audit = Some(gate);
}
}
crate::openhuman::approval::GateOutcome::Deny { reason } => {
tracing::warn!(
iteration,
Expand Down Expand Up @@ -890,6 +910,29 @@ pub(crate) async fn run_tool_call_loop(
log::warn!("[agent_loop] progress sink closed while emitting ToolCallCompleted: {e}");
}
}
// ── Approval audit after-action row (#2135) ────
// Stamp the terminal status onto the same
// `pending_approvals` row the gate created before
// execution, so the audit trail carries both the
// before (approval) and after (executed_at +
// outcome). Best-effort: a write failure here is
// logged but not propagated to the agent.
if let (Some(gate), Some(req_id)) = (
approval_gate_for_audit.as_ref(),
approval_request_id.as_ref(),
) {
let exec_outcome = if success {
crate::openhuman::approval::ExecutionOutcome::Success
} else {
crate::openhuman::approval::ExecutionOutcome::Failure
};
let err_text = if success {
None
} else {
Some(result_text.as_str())
};
gate.record_execution(req_id, exec_outcome, err_text);
}
result_text
} else {
tracing::warn!(
Expand Down
34 changes: 31 additions & 3 deletions src/openhuman/agent/triage/escalation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ pub async fn apply_decision(run: TriageRun, envelope: &TriggerEnvelope) -> anyho
// still applies — defense in depth, not duplication
// (each gate is short-circuited by the session
// allowlist after the first approval).
let mut approval_request_id: Option<String> = None;
let mut approval_gate_for_audit: Option<
std::sync::Arc<crate::openhuman::approval::ApprovalGate>,
> = None;
if let Some(gate) = crate::openhuman::approval::ApprovalGate::try_global() {
let summary = format!(
"triage::{} target={} prompt_chars={}",
Expand All @@ -109,8 +113,15 @@ pub async fn apply_decision(run: TriageRun, envelope: &TriggerEnvelope) -> anyho
"prompt_chars": prompt.chars().count(),
});
let tool_key = format!("triage.{}", run.decision.action.as_str());
match gate.intercept(&tool_key, &summary, redacted).await {
crate::openhuman::approval::GateOutcome::Allow => {}
let (outcome, request_id) =
gate.intercept_audited(&tool_key, &summary, redacted).await;
match outcome {
crate::openhuman::approval::GateOutcome::Allow => {
approval_request_id = request_id;
if approval_request_id.is_some() {
approval_gate_for_audit = Some(gate);
}
}
crate::openhuman::approval::GateOutcome::Deny { reason } => {
tracing::warn!(
action = %action_str,
Expand All @@ -128,7 +139,24 @@ pub async fn apply_decision(run: TriageRun, envelope: &TriggerEnvelope) -> anyho
}
}

match dispatch_target_agent(target, prompt).await {
let dispatch_result = dispatch_target_agent(target, prompt).await;
// Record terminal status on the approval audit row
// (#2135). Best-effort: write errors are logged inside
// record_execution and never propagate to the caller.
if let (Some(gate), Some(req_id)) = (
approval_gate_for_audit.as_ref(),
approval_request_id.as_ref(),
) {
let (exec_outcome, err_text) = match &dispatch_result {
Ok(_) => (crate::openhuman::approval::ExecutionOutcome::Success, None),
Err(e) => (
crate::openhuman::approval::ExecutionOutcome::Failure,
Some(e.to_string()),
),
};
gate.record_execution(req_id, exec_outcome, err_text.as_deref());
}
match dispatch_result {
Ok(output) => {
tracing::info!(
target_agent = %target,
Expand Down
Loading
Loading