Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- Orchestration: prevent duplicate secret prompt after 120 s timeout. When a sub-agent re-requests a secret that was already denied (via timeout or explicit rejection) in the current plan execution, the orchestrator now silently re-denies it without showing the user another confirmation dialog. Denied `(handle_id, secret_key)` pairs are tracked for the duration of the plan run and cleared on each new `/plan confirm` (#1455)
- Anomaly detector now classifies `[stderr]` tool output as `AnomalyOutcome::Error`. Previously the condition checked for dead-code pattern `[exit code` (never emitted by `ShellExecutor`), causing all shell stderr output to be silently classified as `Success` (#1453)
- Shell audit logger (`ShellExecutor`) now classifies `[stderr]` output as `AuditResult::Error`, matching the anomaly detector fix (#1453)
- Add `protocolVersion` field to A2A agent card (`/.well-known/agent.json`); value is set to `A2A_PROTOCOL_VERSION` constant (`"0.2.1"`) and emitted by the default `AgentCardBuilder` (#1442)
Expand Down
27 changes: 27 additions & 0 deletions crates/zeph-core/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ pub struct Agent<C: Channel> {
/// URLs extracted from untrusted tool outputs that had injection flags.
/// Cleared at the start of each `process_response` call (per-turn strategy — see S3).
pub(super) flagged_urls: std::collections::HashSet<String>,
/// Tracks `(handle_id, secret_key)` pairs denied during the current plan execution
/// (IC-CRIT-05 / issue #1455). Prevents re-prompting the user for the same secret
/// within a single plan run. Cleared at the start of each new `handle_plan_confirm`.
denied_secrets: std::collections::HashSet<(String, String)>,
/// Image parts staged by `/image` commands, attached to the next user message.
pending_image_parts: Vec<zeph_llm::provider::MessagePart>,
/// Graph waiting for `/plan confirm` before execution starts.
Expand Down Expand Up @@ -444,6 +448,7 @@ impl<C: Channel> Agent<C> {
crate::sanitizer::exfiltration::ExfiltrationGuardConfig::default(),
),
flagged_urls: std::collections::HashSet::new(),
denied_secrets: std::collections::HashSet::new(),
pending_image_parts: Vec::new(),
pending_graph: None,
debug_dumper: None,
Expand Down Expand Up @@ -659,6 +664,10 @@ impl<C: Channel> Agent<C> {
))
.await?;

// Clear per-plan denial set so stale denials from a previous run don't carry over
// into this execution (IC-CRIT-05 / issue #1455).
self.denied_secrets.clear();

let final_status = self.run_scheduler_loop(&mut scheduler, task_count).await?;

let completed_graph = scheduler.into_graph();
Expand Down Expand Up @@ -808,6 +817,10 @@ impl<C: Channel> Agent<C> {
///
/// SEC-P1-02: explicit user confirmation is required before granting any secret to a
/// sub-agent. Denial is the default on timeout or channel error.
///
/// IC-CRIT-05 / issue #1455: secrets denied in this plan execution are tracked in
/// `self.denied_secrets` and silently re-denied on subsequent sub-agent requests,
/// preventing duplicate prompts after a 120 s timeout.
async fn process_pending_secret_requests(&mut self) {
loop {
let pending = self
Expand All @@ -817,6 +830,18 @@ impl<C: Channel> Agent<C> {
let Some((req_handle_id, req)) = pending else {
break;
};

// IC-CRIT-05: skip prompting if this (agent, key) pair was already denied.
if self
.denied_secrets
.contains(&(req_handle_id.clone(), req.secret_key.clone()))
{
if let Some(mgr) = self.subagent_manager.as_mut() {
let _ = mgr.deny_secret(&req_handle_id);
}
continue;
}

let prompt = format!(
"Sub-agent requests secret '{}'. Allow?{}",
req.secret_key,
Expand All @@ -841,6 +866,8 @@ impl<C: Channel> Agent<C> {
let _ = mgr.deliver_secret(&req_handle_id, key);
}
} else {
self.denied_secrets
.insert((req_handle_id.clone(), req.secret_key.clone()));
let _ = mgr.deny_secret(&req_handle_id);
}
}
Expand Down
Loading