huskies: merge 1218 story Remembered/sticky permission approvals so constrained agents stop re-prompting for the same action class
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
//! part of the server.
|
||||
|
||||
use crate::http::context::{PermissionDecision, PermissionForward};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -241,6 +241,57 @@ impl PendingPermReplies {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remembered permissions ───────────────────────────────────────────────
|
||||
|
||||
/// In-memory "don't ask again for this tool + target-pattern" store, scoped
|
||||
/// per requesting agent session (story 1218).
|
||||
///
|
||||
/// Keyed by session id (see `http::mcp::session`) — for a per-story coding
|
||||
/// agent this is the story id, stable across a `--resume` of the same story
|
||||
/// since the worktree (and its `.mcp.json` header) doesn't change. Rules
|
||||
/// remembered here are visible only within the session that recorded them,
|
||||
/// so one agent's "don't ask again" never widens what another agent or
|
||||
/// story is auto-approved for.
|
||||
///
|
||||
/// Deliberately **not** persisted to disk: this store lives only as long as
|
||||
/// the server process does. Restarting the server clears every remembered
|
||||
/// rule (agents will be prompted again once), which is an explicit tradeoff
|
||||
/// — durability across restarts would require writing into each worktree's
|
||||
/// own `.claude/settings.json`, which adds a second persistence path for a
|
||||
/// case (server restarts mid-story) rare enough not to justify the added
|
||||
/// complexity.
|
||||
pub struct RememberedPermissions {
|
||||
inner: StdMutex<HashMap<String, HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl RememberedPermissions {
|
||||
/// Create an empty store.
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
inner: StdMutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Record that `rule` is approved for `session_id` going forward.
|
||||
pub fn remember(&self, session_id: &str, rule: &str) {
|
||||
self.inner
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(session_id.to_string())
|
||||
.or_default()
|
||||
.insert(rule.to_string());
|
||||
}
|
||||
|
||||
/// `true` if `rule` was previously remembered for `session_id`.
|
||||
pub fn is_remembered(&self, session_id: &str, rule: &str) -> bool {
|
||||
self.inner
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(session_id)
|
||||
.is_some_and(|rules| rules.contains(rule))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -394,4 +445,36 @@ mod tests {
|
||||
let pending = PendingPermReplies::new();
|
||||
assert!(pending.resolve_oldest("no-such-room").await.is_none());
|
||||
}
|
||||
|
||||
// ── RememberedPermissions ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn remembered_permissions_starts_empty() {
|
||||
let store = RememberedPermissions::new();
|
||||
assert!(!store.is_remembered("1218", "Bash(git *)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remembered_permissions_recalls_after_remember() {
|
||||
let store = RememberedPermissions::new();
|
||||
store.remember("1218", "Bash(git *)");
|
||||
assert!(store.is_remembered("1218", "Bash(git *)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remembered_permissions_scoped_per_session() {
|
||||
let store = RememberedPermissions::new();
|
||||
store.remember("1218", "Bash(git *)");
|
||||
assert!(
|
||||
!store.is_remembered("1216", "Bash(git *)"),
|
||||
"a rule remembered for one story must not apply to another"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remembered_permissions_scoped_per_rule() {
|
||||
let store = RememberedPermissions::new();
|
||||
store.remember("1218", "Bash(git *)");
|
||||
assert!(!store.is_remembered("1218", "Write"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user