Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
127298f
feat(acp): implement permission policy (#4938)
Aug 6, 2026
d8be338
feat(desktop): permission policy config + actionable Allow/Deny card …
Aug 6, 2026
90e3292
fix(desktop): control_result is delivery confirmation, not terminal o…
Aug 6, 2026
4f72c3c
Merge remote-tracking branch 'origin/hayt/permission-policy' into dun…
Aug 6, 2026
6dbbc67
feat(acp): add PermissionMode::Auto + contradiction matrix rows (#4938)
Aug 6, 2026
b3b1b3b
fix(acp): address Thufir pass-1 CRITICAL and IMPORTANT findings (#4938)
Aug 6, 2026
96538e9
fix(acp): address Thufir pass-2 review findings (#4938)
Aug 6, 2026
6607eaf
fix(acp): structural round — one nonce-keyed permission record, finis…
Aug 7, 2026
7d277da
fix(acp): address Thufir pass-4 review findings
Aug 7, 2026
73f2ffe
fix(buzz-acp): thread single nonce through sync denial paths; upgrade…
Aug 7, 2026
fcd33ff
test(buzz-acp): tighten equality-deadline test to pinned future + exa…
Aug 7, 2026
bcd9bac
refactor(desktop): extract permission/transcript types to fix file-si…
Aug 7, 2026
fe901d8
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 7, 2026
2dd0706
refactor(desktop): extract RawManagedAgent/fromRawManagedAgent to man…
Aug 7, 2026
e87f265
fix(desktop): use struct literal init to satisfy clippy::field_reassi…
Aug 7, 2026
cef78d7
feat(desktop): render permission-request sentinel card in thread time…
Aug 8, 2026
d808c4b
feat(acp): publish kind-9/40003 permission sentinel cards into channe…
Aug 8, 2026
74c9200
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 8, 2026
1e0c5ba
fix(acp): restore correct merge resolution for buzz-acp post-main-revert
Aug 8, 2026
a16b059
fix(desktop): parse bare-JSON sentinel, unify expiry clock, tighten g…
Aug 8, 2026
fe7efd8
fix(acp): ACK-gated sentinel lifecycle, D7 admission, NIP-AO docs
Aug 8, 2026
3129900
test(desktop): add harness integration fixture test for kind-9 parser
Aug 8, 2026
ef67355
fix(acp): background task owns ACK-waiter expiry, add 3 missing named…
Aug 8, 2026
aeca230
refactor(acp): delete publish_event_acked, only register_publish_ack …
Aug 8, 2026
f858cab
fix(desktop): sentinel-only edit gate in formatTimelineMessages; rewo…
Aug 8, 2026
60c247e
test(e2e): align permission outcome assertion with label-based rendering
Aug 8, 2026
ff5b21f
chore: merge origin/main into duncan/permission-policy
Aug 10, 2026
3ad999a
fix(desktop): fail-closed PermissionDecisionButtons for allow_always …
Aug 10, 2026
bac3264
fix(acp): add permission_decision_tx: None to main's new test TaskMet…
Aug 10, 2026
00456e4
fix(desktop): persist applied permission policy for remote deploys
Aug 10, 2026
ca9acfa
fix(desktop): exact-allowlist permission kinds, disclose persistent deny
Aug 10, 2026
0bc5683
fix(desktop): enforce deploy-receipt invariant for applied permission…
Aug 10, 2026
f4efd99
feat(desktop): relocate permission-policy default to the agent defini…
Aug 12, 2026
f1ab873
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 12, 2026
3c0dfd7
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
1725f16
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
bf411ef
chore(desktop): reclaim file-size headroom via comment trims
Aug 25, 2026
9d42119
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
25ddb2f
refactor(desktop): extract spawn helpers and reshape permission card …
Aug 25, 2026
d9bed35
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
6aa3315
chore(desktop): reclaim file-size headroom for permission-policy fields
Aug 25, 2026
8672302
test(desktop): keep pending policy true in failed-deploy setup
Aug 25, 2026
d4417a9
test(desktop): expect seeded permissionPolicy in access-override edit…
Aug 25, 2026
e6014be
fix(acp): freeze permission card to a two-action contract
Aug 25, 2026
30bb1f8
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
6e7c697
docs(nip-ao): align sentinel spec with two-action card contract
Aug 25, 2026
926a021
fix(acp): unify sentinel byte bounds across producer and parser
Aug 25, 2026
a183f25
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 25, 2026
05566b9
fix(acp): gate sentinel total-content bound on raw content before trim
Aug 25, 2026
4a8305c
docs(acp): correct total-content gate rationale for control-char infl…
Aug 25, 2026
422e336
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 26, 2026
0b967b5
fix(acp): bound Publishing entries and require edit provenance for re…
Aug 26, 2026
8538058
test(acp): make publish-in-flight regression production-shaped
Aug 26, 2026
7bb2982
refactor(acp): bundle ask-only preflight gates into AskGates
Aug 26, 2026
1a19de3
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 27, 2026
018095e
fix(acp): retransmit resolved permission edit until relay accepts it
Aug 27, 2026
37103a6
fix(acp): bound resolved-edit retransmit per attempt, not per card
Aug 27, 2026
5916529
fix(acp): close inbound permission-decision lifecycle gap
Aug 28, 2026
111bc91
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 28, 2026
247f897
fix(desktop): survive rejected send in permission retransmit loop
Aug 28, 2026
77f7440
fix(desktop): propagate control_result status; failure re-enables car…
Aug 28, 2026
ecd441b
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 28, 2026
9301abf
test(desktop): add delivery-seam + component regression test for fail…
Aug 28, 2026
2787c55
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 28, 2026
2f95fca
fix(acp): correct verb derivation, add description field, fix forged-…
Aug 29, 2026
8101c31
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Aug 29, 2026
05246f6
fix(acp): extract description from real producer shapes; delegate pro…
Aug 29, 2026
05b496e
fix(acp): extend description extraction to codex v1.1.7; protect pros…
Aug 29, 2026
255ed62
fix(acp): clippy borrow and MessageRow memo gaps for permission gate
Aug 30, 2026
ed3a25c
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Sep 1, 2026
aa30ce6
fix(acp): implement Carl review findings F1–F4 with full behavioral t…
Sep 1, 2026
d70558f
fix(acp): implement expanded findings F1-F4 + routing hazard
Sep 1, 2026
242dbf2
fix(acp): close Thufir addendum gaps — unconditional first attempt, i…
Sep 1, 2026
3d925ef
chore(desktop): reclaim ratchet headroom — extract duplicate types, c…
Sep 1, 2026
d5343ef
fix(desktop): restore extract_applied_permission_policy lost in merge…
Sep 1, 2026
00a04c1
fix(desktop): correct display_name in content_matches_nip_ap_vector c…
Sep 1, 2026
e5fb508
test(acp): fix bounded-exit test — small-step advance for cross-task …
Sep 1, 2026
b144047
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Sep 1, 2026
c23daf8
fix(acp): correct wrapper-overhead and test structure for F1/F3 regre…
Sep 1, 2026
9628c1e
test(buzz-acp): strengthen pass-2 acceptance proofs for F1–F4 and rou…
Sep 1, 2026
96c9b7f
test(buzz-acp): close pass-3 gaps for F1/F3/F4 and MINOR desktop wording
Sep 1, 2026
fbeb9fa
test(buzz-acp): assert reject option ID in reject_once applied write
Sep 1, 2026
5c8ec7a
fix(acp): scope permission-card retirement to terminating turn; fix f…
Sep 1, 2026
a0309cd
fix(acp): treat channel_full as transient; fix legacy-key retirement …
Sep 2, 2026
9659800
fix(acp): classify channel_full as transient in transcript reducer
Sep 2, 2026
51486b2
Merge remote-tracking branch 'origin/main' into duncan/permission-policy
Sep 2, 2026
2e2f4df
docs(agents): enumerate authoritative delivery-failure statuses in st…
Sep 2, 2026
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
8,429 changes: 8,291 additions & 138 deletions crates/buzz-acp/src/acp.rs

Large diffs are not rendered by default.

293 changes: 252 additions & 41 deletions crates/buzz-acp/src/config.rs

Large diffs are not rendered by default.

788 changes: 784 additions & 4 deletions crates/buzz-acp/src/lib.rs

Large diffs are not rendered by default.

61 changes: 60 additions & 1 deletion crates/buzz-acp/src/observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ pub struct ObserverContext {
pub started_at: Option<String>,
}

/// Authorization envelope attached to permission-related observer events.
///
/// Present on the single `acp_read` emitted after a permission request passes
/// the admission preflight, and on the corresponding `acp_write` after the
/// response is confirmed written.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationEnvelope {
/// Single-use nonce bound to this request — delivered to the desktop and
/// consumed exactly once when the owner makes a decision.
pub request_nonce: String,
/// `true` when the owner can take action (policy=ask, preflight passed,
/// owner/observer available). `false` for auto-deny / fail-closed paths.
pub actionable: bool,
/// Human-readable reason when `actionable` is `false`.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
/// Wire card-expiry (unix seconds) for an actionable card — the same value
/// stored in the kind-9 sentinel. The desktop bounds its
/// retransmit-until-acked loop by this deadline, so a decision published
/// while the harness socket is down keeps being resent until the card
/// expires (never past it). `None` on non-actionable / already-resolved
/// frames, where no owner decision is awaited.
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
}

/// Handle used by the harness to publish local observer events.
#[derive(Clone)]
pub struct ObserverHandle {
Expand All @@ -54,7 +81,7 @@ fn new_observer_handle() -> ObserverHandle {
}

/// Event delivered through the in-process observer bus.
#[derive(Clone, Serialize)]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ObserverEvent {
/// Monotonic process-local sequence number.
Expand All @@ -74,6 +101,12 @@ pub struct ObserverEvent {
/// RFC3339 timestamp at which the current turn began, when known.
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
/// Authorization envelope — present only on permission `acp_read` /
/// `acp_write` frames, and on the observer-only `permission_terminal` frame
/// (which carries `reason = "uncertain"` and is never sent on the ACP wire).
/// `None` on all other event kinds.
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<AuthorizationEnvelope>,
/// Raw or semantic event payload.
pub payload: serde_json::Value,
}
Expand Down Expand Up @@ -107,6 +140,31 @@ impl ObserverHandle {
agent_index: Option<usize>,
context: &ObserverContext,
payload: serde_json::Value,
) {
self.emit_inner(kind, agent_index, context, None, payload);
}

/// Emit a local observer event with an authorization envelope.
///
/// Used for permission `acp_read` and `acp_write` frames.
pub fn emit_authorized(
&self,
kind: impl Into<String>,
agent_index: Option<usize>,
context: &ObserverContext,
authorization: AuthorizationEnvelope,
payload: serde_json::Value,
) {
self.emit_inner(kind, agent_index, context, Some(authorization), payload);
}

fn emit_inner(
&self,
kind: impl Into<String>,
agent_index: Option<usize>,
context: &ObserverContext,
authorization: Option<AuthorizationEnvelope>,
payload: serde_json::Value,
) {
let event = ObserverEvent {
seq: self.inner.seq.fetch_add(1, Ordering::Relaxed),
Expand All @@ -117,6 +175,7 @@ impl ObserverHandle {
session_id: context.session_id.clone(),
turn_id: context.turn_id.clone(),
started_at: context.started_at.clone(),
authorization,
payload,
};

Expand Down
188 changes: 170 additions & 18 deletions crates/buzz-acp/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ use crate::acp::{
model_in_catalog, resolve_model_switch_method, AcpClient, AcpError, EnvVar, McpServer,
ModelSwitchMethod, StopReason, SystemPromptTransport,
};
use crate::config::{compose_scoped_session_title, DedupMode, PermissionMode};
use crate::config::{
compose_scoped_session_title, DedupMode, PermissionMode, ResolvedPermissionConfig,
};
use crate::observer;
use crate::prompt_project::{pick_authoritative_project_home, PromptProjectInfo};
use crate::queue::{
Expand Down Expand Up @@ -79,6 +81,13 @@ pub struct TaskMeta {
/// tasks only — all prompt tasks install a steer channel regardless
/// of the agent's name.
pub steer_tx: Option<tokio::sync::mpsc::Sender<SteerRequest>>,
/// Permission decision channel — delivers `permission_decision` control
/// frames from the observer dispatch loop into the read loop's decision
/// arm. `None` until the first `ask`-policy permission request arrives
/// (installed per-session by the pool dispatch path). Cloned from the
/// sender end of the channel installed on `AcpClient` via
/// `install_permission_decision_rx`.
pub permission_decision_tx: Option<tokio::sync::mpsc::Sender<crate::acp::PermissionDecision>>,
/// Successful non-cancelling steers acknowledged while this task owned the
/// live session. The session ID prevents a late ack from contaminating a
/// replacement session after task return.
Expand Down Expand Up @@ -330,6 +339,15 @@ pub struct AgentPool {
result_rx: mpsc::UnboundedReceiver<PromptResult>,
pub join_set: JoinSet<()>,
task_map: HashMap<tokio::task::Id, TaskMeta>,
/// Nonces of permission decisions already forwarded to a read loop, with the
/// instant each was recorded. The desktop retransmits a decision until it
/// sees a `control_result`; a copy that arrives after the read loop applied
/// the decision and its task ended would otherwise get `no_active_turn` /
/// `channel_closed` and flip the resolved card to failed. Recording the
/// nonce on first delivery lets [`Self::was_recently_decided`] recognize
/// such a late duplicate and ack it success-shaped instead. Pruned to
/// [`DECIDED_NONCE_RETENTION`] (≥ the card's max expiry) on every write.
recently_decided: HashMap<String, tokio::time::Instant>,
/// Authoritative directory of which worker most recently owned each session
/// scope's provider session. Survives while a worker is checked out (its
/// `SessionState` is invisible to the pool then), so a busy owner does not
Expand All @@ -339,6 +357,11 @@ pub struct AgentPool {
session_owners: HashMap<SessionScope, usize>,
}

/// Retention for [`AgentPool::recently_decided`]. Matches the maximum card
/// expiry (`PERMISSION_ASK_TIMEOUT_SECS`) so a nonce stays recognized for as
/// long as the desktop could still be retransmitting it, then is reclaimed.
const DECIDED_NONCE_RETENTION: Duration = Duration::from_secs(300);

/// Result returned by a completed prompt task.
pub struct PromptResult {
pub agent: OwnedAgent,
Expand Down Expand Up @@ -785,8 +808,8 @@ pub struct PromptContext {
pub context_message_limit: u32,
/// Max turns per session before proactive rotation. 0 = disabled.
pub max_turns_per_session: u32,
/// Permission mode to apply after session creation. `Default` = skip.
pub permission_mode: PermissionMode,
/// Resolved permission configuration — policy, effective ACP mode, and how to transmit.
pub permission_config: ResolvedPermissionConfig,
/// Agent identity — used to derive the NIP-AE conversation key at
/// session creation for core injection.
pub agent_keys: nostr::Keys,
Expand All @@ -806,6 +829,11 @@ pub struct PromptContext {
/// the desktop keys per (agent, relay) pair, e.g. `session_config_captured`,
/// mirroring the `managed_agent_runtime_lifecycle` frames.
pub relay_url: String,
/// Publisher for kind-9 sentinel cards and kind-40003 edits.
/// When set, `run_prompt_task` wires it into `AcpClient` so permission
/// cards appear in the channel thread. `None` disables sentinel publishing
/// (observer feed path remains).
pub relay_event_publisher: Option<crate::relay::RelayEventPublisher>,
}

impl AgentPool {
Expand All @@ -823,10 +851,31 @@ impl AgentPool {
result_rx,
join_set: JoinSet::new(),
task_map: HashMap::new(),
recently_decided: HashMap::new(),
session_owners: HashMap::new(),
}
}

/// Record a permission-decision nonce as delivered to a read loop and prune
/// entries older than [`DECIDED_NONCE_RETENTION`]. Called when a decision is
/// first forwarded so a later retransmit of the same nonce is recognized.
pub fn record_permission_decision(&mut self, nonce: &str) {
let now = tokio::time::Instant::now();
self.recently_decided
.retain(|_, at| now.duration_since(*at) < DECIDED_NONCE_RETENTION);
self.recently_decided.insert(nonce.to_string(), now);
}

/// Whether `nonce` was recently forwarded to a read loop and is still within
/// the retention window. A late retransmit that matches is a duplicate the
/// harness has already forwarded — the caller acks it success-shaped rather
/// than failing the resolved card.
pub fn was_recently_decided(&self, nonce: &str) -> bool {
self.recently_decided.get(nonce).is_some_and(|at| {
tokio::time::Instant::now().duration_since(*at) < DECIDED_NONCE_RETENTION
})
}

/// Record which worker is handling `scope` so a later dispatch can detect a
/// busy owner and avoid opening a duplicate session on another worker.
pub fn record_scope_owner(&mut self, scope: SessionScope, agent_index: usize) {
Expand Down Expand Up @@ -1515,14 +1564,20 @@ async fn create_session_and_apply_model(
}),
);

// Apply permission mode if not the agent's built-in default AND the agent
// advertises the requested mode in session/new. Agents that don't support
// the mode (e.g., goose crashes on unrecognized set_config_option values)
// are safely skipped — the harness auto-approves via handle_permission_request.
if !ctx.permission_mode.is_default()
&& agent_supports_mode(&resp.raw, ctx.permission_mode.as_wire_str())
// Apply permission mode whenever the agent advertises it (including `default`).
// The `transmit_mode` flag handles any future cases where transmission should be skipped.
if ctx.permission_config.transmit_mode
&& agent_supports_mode(
&resp.raw,
ctx.permission_config.effective_mode.as_wire_str(),
)
{
apply_permission_mode(&mut agent.acp, &resp.session_id, &ctx.permission_mode).await?;
apply_permission_mode(
&mut agent.acp,
&resp.session_id,
&ctx.permission_config.effective_mode,
)
.await?;
}

Ok(resp.session_id)
Expand Down Expand Up @@ -1769,11 +1824,7 @@ fn patch_config_option_current_value(
}
}

/// Set the session permission mode via `session/set_config_option`.
///
/// Non-fatal for most errors: logs and proceeds. The agent falls back
/// to its default permission mode (`"default"`), which still works via
/// Check if the agent's `session/new` response advertises a given mode ID
/// Check whether the agent's `session/new` response advertises a given mode ID
/// in `result.modes.availableModes[].id`. Returns `false` if the modes
/// field is absent or the mode isn't listed.
fn agent_supports_mode(session_new_result: &serde_json::Value, mode_wire: &str) -> bool {
Expand All @@ -1789,7 +1840,11 @@ fn agent_supports_mode(session_new_result: &serde_json::Value, mode_wire: &str)
.unwrap_or(false)
}

/// per-tool auto-approval in `handle_permission_request`.
/// Set the session permission mode via `session/set_config_option`.
///
/// Non-fatal for most errors: logs and proceeds. The agent falls back to its
/// default mode, and any interactive permission request is rejected by
/// `handle_permission_request`.
///
/// **Fatal exception:** if the agent process exits (e.g., goose crashes on
/// unrecognized methods), returns `Err(AgentExited)` so the caller can respawn.
Expand Down Expand Up @@ -1829,7 +1884,7 @@ async fn apply_permission_mode(
Ok(Err(e)) => {
tracing::warn!(
target: "pool::permission",
"failed to set permission mode {wire:?}: {e} — falling back to per-tool auto-approval"
"failed to set permission mode {wire:?}: {e} — falling back to per-tool rejection"
);
}
Err(_) => {
Expand Down Expand Up @@ -2052,6 +2107,44 @@ pub async fn run_prompt_task(
turn_id.clone(),
turn_started_at.clone(),
));

// Wire permission configuration and owner-knowledge into the ACP client so
// `handle_permission_request` can evaluate the ask availability gate. These
// values come from `PromptContext` (resolved once at startup from CLI args and
// desktop-injected env vars) and are idempotent to re-apply across turns.
agent
.acp
.set_permission_config(ctx.permission_config.clone());
agent
.acp
.set_owner_pubkey_known(ctx.agent_owner_pubkey.is_some());

// Wire sentinel card publisher, agent signing keys, owner pubkey, and
// per-turn context for D7-final admission and kind-9/40003 publishing.
if let Some(publisher) = ctx.relay_event_publisher.clone() {
agent
.acp
.set_relay_publisher(publisher, ctx.agent_keys.clone());
}
agent
.acp
.set_agent_owner_pubkey_hex(ctx.agent_owner_pubkey.as_ref().map(|pk| pk.to_hex()));
// D7-final: record the turn initiator from the first event in the batch.
let turn_initiator = batch
.as_ref()
.and_then(|b| b.events.first())
.map(|be| be.event.pubkey);
agent.acp.set_turn_initiator_pubkey(turn_initiator);
// Sentinel routing: channel UUID and reply anchor from batch.
let batch_channel_id = batch.as_ref().map(|b| b.channel_id);
let thread_reply_event_id = batch
.as_ref()
.and_then(|b| b.events.first())
.map(|be| be.event.id.to_hex());
agent
.acp
.set_turn_channel_context(batch_channel_id, thread_reply_event_id);

let triggering_event_ids: Vec<String> = batch
.as_ref()
.map(|b| b.events.iter().map(|be| be.event.id.to_hex()).collect())
Expand Down Expand Up @@ -5147,6 +5240,60 @@ mod tests {
}
}

#[tokio::test(start_paused = true)]
async fn recently_decided_recognizes_a_nonce_within_retention() {
let mut pool = AgentPool::from_slots(vec![None]);
assert!(
!pool.was_recently_decided("n1"),
"unknown nonce is not recently decided"
);
pool.record_permission_decision("n1");
assert!(
pool.was_recently_decided("n1"),
"just-recorded nonce is recognized"
);
assert!(
!pool.was_recently_decided("n2"),
"a different nonce is not recognized"
);
}

#[tokio::test(start_paused = true)]
async fn recently_decided_expires_after_retention_window() {
let mut pool = AgentPool::from_slots(vec![None]);
pool.record_permission_decision("n1");
// Just inside the window: still recognized.
tokio::time::advance(DECIDED_NONCE_RETENTION - Duration::from_secs(1)).await;
assert!(
pool.was_recently_decided("n1"),
"nonce inside retention is still recognized"
);
// Past the window: no longer recognized (bounds the set's growth and
// stops acking retransmits for cards that have long since expired).
tokio::time::advance(Duration::from_secs(2)).await;
assert!(
!pool.was_recently_decided("n1"),
"nonce past retention is forgotten"
);
}

#[tokio::test(start_paused = true)]
async fn recording_prunes_entries_past_retention() {
let mut pool = AgentPool::from_slots(vec![None]);
pool.record_permission_decision("old");
tokio::time::advance(DECIDED_NONCE_RETENTION + Duration::from_secs(1)).await;
// Recording a new nonce prunes the stale one so the map cannot grow
// without bound over a long-lived process.
pool.record_permission_decision("new");
assert!(!pool.was_recently_decided("old"), "stale entry was pruned");
assert!(pool.was_recently_decided("new"), "fresh entry retained");
assert_eq!(
pool.recently_decided.len(),
1,
"only the fresh entry remains"
);
}

#[test]
fn delivery_receipt_line_sorts_event_ids() {
let channel_id = Uuid::nil();
Expand Down Expand Up @@ -8681,12 +8828,17 @@ printf '%s\n' '{{"jsonrpc":"2.0","id":0,"result":{{"stopReason":"end_turn"}}}}'"
),
context_message_limit: 0,
max_turns_per_session: 0,
permission_mode: PermissionMode::Default,
permission_config: ResolvedPermissionConfig::resolve(
crate::config::PermissionPolicy::Reject,
None,
)
.expect("test config"),
agent_keys: agent_keys.clone(),
agent_owner_pubkey: owner_pubkey,
memory_enabled: false,
harness_name: "goose".to_string(),
relay_url: "ws://127.0.0.1:3000".to_string(),
relay_event_publisher: None,
}
}

Expand Down
Loading
Loading