Skip to content
Merged
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
45 changes: 30 additions & 15 deletions crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ pub use sensitive_data::{
use std::collections::BTreeSet;

use originweave_core::{
ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, ExtensionId,
InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode,
ActionRequest, ApprovalEvidence, ApprovalScope, BrowserSessionId, Capability, ExecutionPurpose,
ExtensionId, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery,
SessionMode,
};

/// Exact extension identities that may be present in one managed Agent Task profile.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentTaskExtensionPolicy {
browser_session: BrowserSessionId,
managed_extensions: BTreeSet<ExtensionId>,
valid_from: u64,
valid_until: u64,
Expand All @@ -34,17 +36,20 @@ pub struct AgentTaskExtensionPolicy {
impl AgentTaskExtensionPolicy {
/// Build one fail-closed Agent Task extension admission policy.
///
/// Duplicate identifiers collapse to one exact managed identity. An empty
/// iterator therefore represents the default policy that admits no extension.
/// `valid_from` is inclusive and `valid_until` is exclusive. `maximum_window`
/// is the reviewed local ceiling for that interval. All three values are opaque
/// timestamps or durations in the same caller-defined trusted time domain and
/// units supplied to [`evaluate_agent_task_extension`]. This constructor does
/// not authenticate policy provenance or attest a clock; an invalid, empty, or
/// overlong validity window is retained so evaluation can fail closed
/// deterministically.
/// `browser_session` binds this policy to one OriginWeave browser-session
/// authority. It does not prove which Chromium profile is attached to that
/// session or authenticate enterprise-policy provenance. Duplicate identifiers
/// collapse to one exact managed identity. An empty iterator therefore
/// represents the default policy that admits no extension. `valid_from` is
/// inclusive and `valid_until` is exclusive. `maximum_window` is the reviewed
/// local ceiling for that interval. The time values and duration use one
/// caller-defined trusted time domain and compatible units supplied to
/// [`evaluate_agent_task_extension`]. This constructor does not authenticate
/// policy provenance or attest a clock; invalid, empty, or overlong validity
/// windows are retained so evaluation can fail closed deterministically.
#[must_use]
pub fn new<I>(
browser_session: BrowserSessionId,
managed_extensions: I,
valid_from: u64,
valid_until: u64,
Expand All @@ -54,6 +59,7 @@ impl AgentTaskExtensionPolicy {
I: IntoIterator<Item = ExtensionId>,
{
Self {
browser_session,
managed_extensions: managed_extensions.into_iter().collect(),
valid_from,
valid_until,
Expand All @@ -77,25 +83,34 @@ pub enum AgentTaskExtensionDecision {
DenyPolicyNotYetValid,
/// The trusted evaluation time is at or beyond the policy expiry boundary.
DenyPolicyExpired,
/// The current OriginWeave browser session differs from the policy-bound session.
DenySessionMismatch,
}

/// Evaluate extension admission without minting OriginWeave Agent capability.
///
/// This pure boundary answers only whether the exact canonical extension may be
/// present in the caller's managed Agent Task profile at `trusted_time`.
/// present in the policy-bound Agent Task session at `trusted_time`.
/// `trusted_time`, [`AgentTaskExtensionPolicy::new`] `valid_from`, `valid_until`,
/// and `maximum_window` must use one caller-attested time domain and compatible
/// units; this function does not read or attest a clock. The validity window is
/// half-open (`valid_from <= trusted_time < valid_until`) and must not exceed the
/// reviewed local maximum. Chromium permissions, installation state, native
/// messaging, and [`originweave_core::ExtensionAgentGrant`] remain separate
/// authorities.
/// reviewed local maximum. The current session is checked before policy-window
/// or allow-list evaluation so a policy cannot be replayed across OriginWeave
/// browser sessions or used there as a policy-state or extension-membership
/// oracle. This does not attest Chromium profile identity. Chromium permissions,
/// installation state, native messaging, and [`originweave_core::ExtensionAgentGrant`]
/// remain separate authorities.
#[must_use]
pub fn evaluate_agent_task_extension(
extension_id: &ExtensionId,
policy: &AgentTaskExtensionPolicy,
current_session: BrowserSessionId,
trusted_time: u64,
) -> AgentTaskExtensionDecision {
if policy.browser_session != current_session {
return AgentTaskExtensionDecision::DenySessionMismatch;
}
if policy.valid_from >= policy.valid_until || policy.maximum_window == 0 {
return AgentTaskExtensionDecision::DenyInvalidPolicyWindow;
}
Expand Down
100 changes: 76 additions & 24 deletions crates/originweave-policy/tests/agent_task_extension_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ fn origin() -> Origin {

#[test]
fn empty_agent_task_extension_policy_denies_every_extension() {
let policy = AgentTaskExtensionPolicy::new([], 10, 20, 10);
let policy = AgentTaskExtensionPolicy::new(session(31), [], 10, 20, 10);
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 15),
evaluate_agent_task_extension(&extension, &policy, session(31), 15),
AgentTaskExtensionDecision::DenyNotManaged
);
}
Expand All @@ -39,96 +39,148 @@ fn empty_agent_task_extension_policy_denies_every_extension() {
fn managed_agent_task_extension_policy_allows_only_exact_identifiers() {
let allowed = extension_id("abcdefghijklmnopabcdefghijklmnop");
let other = extension_id("bcdefghijklmnopabcdefghijklmnopa");
let policy = AgentTaskExtensionPolicy::new([allowed.clone(), allowed.clone()], 10, 20, 10);
let policy =
AgentTaskExtensionPolicy::new(session(31), [allowed.clone(), allowed.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&allowed, &policy, 10),
evaluate_agent_task_extension(&allowed, &policy, session(31), 10),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&other, &policy, 19),
evaluate_agent_task_extension(&other, &policy, session(31), 19),
AgentTaskExtensionDecision::DenyNotManaged
);
}

#[test]
fn managed_agent_task_extension_policy_is_not_reusable_across_sessions() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, session(37), 15),
AgentTaskExtensionDecision::DenySessionMismatch
);
assert_eq!(
evaluate_agent_task_extension(
&extension_id("bcdefghijklmnopabcdefghijklmnopa"),
&policy,
session(37),
15,
),
AgentTaskExtensionDecision::DenySessionMismatch
);
}

#[test]
fn mismatched_session_cannot_probe_policy_window_state() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let current_session = session(37);

let invalid = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10, 10);
assert_eq!(
evaluate_agent_task_extension(&extension, &invalid, current_session, 15),
AgentTaskExtensionDecision::DenySessionMismatch
);

let overlong = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 21, 10);
assert_eq!(
evaluate_agent_task_extension(&extension, &overlong, current_session, 15),
AgentTaskExtensionDecision::DenySessionMismatch
);

let not_yet_valid = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);
assert_eq!(
evaluate_agent_task_extension(&extension, &not_yet_valid, current_session, 9),
AgentTaskExtensionDecision::DenySessionMismatch
);

let expired = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);
assert_eq!(
evaluate_agent_task_extension(&extension, &expired, current_session, 20),
AgentTaskExtensionDecision::DenySessionMismatch
);
}

#[test]
fn managed_agent_task_extension_policy_fails_closed_outside_its_validity_window() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);
let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 9),
evaluate_agent_task_extension(&extension, &policy, session(31), 9),
AgentTaskExtensionDecision::DenyPolicyNotYetValid
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 20),
evaluate_agent_task_extension(&extension, &policy, session(31), 20),
AgentTaskExtensionDecision::DenyPolicyExpired
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX),
evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX),
AgentTaskExtensionDecision::DenyPolicyExpired
);
}

#[test]
fn invalid_managed_extension_policy_window_fails_closed_before_membership() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let reversed = AgentTaskExtensionPolicy::new([extension.clone()], 20, 10, 10);
let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20, 10);
let zero_maximum = AgentTaskExtensionPolicy::new([extension.clone()], 20, 21, 0);
let reversed = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10, 10);
let empty = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 20, 10);
let zero_maximum = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 21, 0);

assert_eq!(
evaluate_agent_task_extension(&extension, &reversed, 15),
evaluate_agent_task_extension(&extension, &reversed, session(31), 15),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &empty, 20),
evaluate_agent_task_extension(&extension, &empty, session(31), 20),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &zero_maximum, 20),
evaluate_agent_task_extension(&extension, &zero_maximum, session(31), 20),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
}

#[test]
fn managed_extension_policy_window_cannot_exceed_local_maximum() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let exact = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);
let overlong = AgentTaskExtensionPolicy::new([extension.clone()], 10, 21, 10);
let exact = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);
let overlong = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 21, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &exact, 19),
evaluate_agent_task_extension(&extension, &exact, session(31), 19),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &overlong, 19),
evaluate_agent_task_extension(&extension, &overlong, session(31), 19),
AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum
);
}

#[test]
fn maximum_timestamp_window_remains_half_open_without_overflow() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], u64::MAX - 1, u64::MAX, 1);
let policy =
AgentTaskExtensionPolicy::new(session(31), [extension.clone()], u64::MAX - 1, u64::MAX, 1);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1),
evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX - 1),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX),
evaluate_agent_task_extension(&extension, &policy, session(31), u64::MAX),
AgentTaskExtensionDecision::DenyPolicyExpired
);
}

#[test]
fn managed_agent_task_extension_admission_does_not_mint_agent_capability() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);
let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 15),
evaluate_agent_task_extension(&extension, &policy, session(31), 15),
AgentTaskExtensionDecision::AllowManagedExtension
);

Expand Down
Loading