Enforce single-agent tenancy to prevent LRU session-state cross-contamination#176
Conversation
| revocation: Arc<dyn RevocationStore + Send + Sync>, | ||
| verifier: Box<dyn TokenVerifier + Send + Sync>, | ||
| tenancy_mode: TenancyMode, | ||
| first_agent_id: Mutex<Option<AgentId>>, |
There was a problem hiding this comment.
Since this is a "set once, never change" value, let's use https://doc.rust-lang.org/std/sync/struct.OnceLock.html rather than a Mutex.
There was a problem hiding this comment.
We could also store the OnceLock inside TenancyMode::SingleAgent, if we prefer.
There was a problem hiding this comment.
Updated to OnceLock
| envelope: None, | ||
| identity: None, |
There was a problem hiding this comment.
Is there a reason we don't capture the envelope (envelope: Some(envelope.clone()))? Same for the identity (identity: Some(DenyIdentity::from_claims(&claims)) should do it, no?).
There was a problem hiding this comment.
The TenantMismatch deny now carries envelope: Some(envelope.clone()) and identity: Some(DenyIdentity::from_claims(&claims)), matching the convention used in the rest of the enforcement code.
| pub fn new( | ||
| capability_map: CapabilityMap, | ||
| verifier: Box<dyn TokenVerifier + Send + Sync>, | ||
| revocation: Arc<dyn RevocationStore + Send + Sync>, | ||
| clock_skew_tolerance: Duration, | ||
| tenancy_mode: TenancyMode, | ||
| ) -> Self { |
There was a problem hiding this comment.
Since we have de-facto only one tenancy mode at the moment, we could also consider renaming this from new to single_tenant. The explicit parameter doesn't buy us much anyway.
(Feel free to keep it as is, not a problem)
There was a problem hiding this comment.
I'll keep it for now because of some of the refactor but updated it when we go to multi.
| let claims = self.validate(&entry.raw_token)?; | ||
|
|
||
| // Step 3: Enforce single-agent tenancy (V1 ADR §2) | ||
| if self.tenancy_mode == TenancyMode::SingleAgent { |
There was a problem hiding this comment.
should we add a test for this scenario?
There was a problem hiding this comment.
Updated
| /// # Errors | ||
| /// | ||
| /// Returns an error if the configuration is invalid. | ||
| pub fn validate(&self) -> Result<(), String> { |
There was a problem hiding this comment.
should we add this later?
There was a problem hiding this comment.
Removed.
| /// Build a pipeline where Stage 1 denies every request: the | ||
| /// `CapabilityMap` is empty, so token selection fails before any | ||
| /// runtime-state bookkeeping. | ||
| #[expect(dead_code, reason = "test helper not currently used")] |
There was a problem hiding this comment.
I wonder why we cannot run this test.
There was a problem hiding this comment.
The helper was written for the "Stage 1 DENY short-circuits before session counter" invariant. The test that motivates it was never written. Fixed it.
| /// serves one agent, preventing session-state cross-contamination in the LRU. | ||
| #[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum TenancyMode { |
There was a problem hiding this comment.
Not a blocker from my side, but should we wait until we have the other variants before introducing this configuration? We could add the enforcement now, but avoid making it configurable until we have at least a second option.
If we decide to expose it already, should we also update the documentation to explain the purpose and expected behavior of this new field for users?
There was a problem hiding this comment.
Updated the rustdoc
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…mination - Add [tenancy] mode = "single_agent" as default config - Track first observed agent_id in CapabilityValidator - Deny requests with different agent_id with TenantMismatch reason - Add TenantMismatch to DenyReason enum in firma-core - Update all test files to pass TenancyMode parameter
Replaces Mutex<Option<AgentId>> with OnceLock<AgentId> since the value is set on first observation and never changes. get_or_init combines initialization and comparison in a single atomic operation, removing the lock/unwrap and the associated poison-panic surface.
The deny branch had envelope: None and identity: None, inconsistent with the convention across constraint_enforcement.rs and pipeline.rs where both are attached when available. Here both are in scope: - envelope: &NormalizedEnvelope is a parameter to enforce() - claims is the freshly verified CapabilityClaims from validate() Attaching identity is especially important for audit: TenantMismatch denies must be filterable via 'firma monitor --agent <id>' (FIR-208), and the presenting agent is what should be attributable.
Adds MutableVerifier — a clone-able TokenVerifier whose returned claims can be swapped mid-test. This lets a single CapabilityValidator instance be exercised across multiple enforce() calls, which is required to observe the OnceLock behavior (first call establishes the agent_id, subsequent calls compare against it). The test asserts: - first call with agent_first populates the OnceLock and succeeds - second call with agent_second on the same validator denies with DenyReason::TenantMismatch at the TokenValidation stage - the deny carries the verified identity (agent_id == agent_second) and the envelope, matching the convention in constraint_enforcement.rs
The method only returns Ok(()) and is the only such stub among the config types. Other configs (audit, local_exec, etc.) have real validation; tenancy currently has nothing to check, so the call site in Config::validate just adds noise. Re-add when a future tenancy mode actually requires validation.
The previous rustdoc was a single sentence that referenced a non-existent 'V1 ADR §2' — the only explanation for why this field exists, what it does at runtime, or how to configure it lived in the pipeline.rs comment about the LRU counter. Expands the TenancyMode rustdoc to cover: - what 'tenancy' means in this context (one-sidecar-one-agent) - the concrete threat it prevents (agent A's LRU counters visible to agent B's Cedar evaluation) - runtime behavior: first request binds, mismatches deny with TenantMismatch at TokenValidation, restart resets the bound - TOML configuration example - removes the dangling ADR reference Also adds a module-level overview so the file is self-explanatory.
3566bdf to
bcd37f3
Compare
Summary
Enforces single-agent tenancy in code to prevent session-state cross-contamination in the LRU cache.
Changes
Behavior
from_thetoken is storedTesting
All 516 firma-sidecar tests pass, plus full workspace test suite passes.