Skip to content
Draft
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 @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Added

- Session-bound managed extension admission for isolated Agent Task profiles: policy is tied to one exact OriginWeave `BrowserSessionId`, mismatched sessions fail closed before allow-list membership is considered, and the boundary remains distinct from Chromium profile attestation and Agent capability grants.
- Time-bounded managed extension admission for isolated Agent Task profiles with an explicit half-open validity window and caller-supplied trusted evaluation time; invalid, not-yet-valid, and expired policy states fail closed before exact extension allow-list membership is considered.
- Fail-closed managed extension admission for isolated Agent Task profiles: an empty policy admits no extension, only exact canonical `ExtensionId` allow-list membership is accepted, duplicate entries cannot widen authority, and successful profile admission remains separate from `ExtensionAgentGrant` capability.
- Explicit reduced-assurance classification for attached human tabs when trusted adapter evidence says an existing extension can influence page state; the narrow rule does not detect extensions, prove extension absence, grant Agent authority, or turn an unclassified context into high-assurance evidence.
Expand Down
44 changes: 32 additions & 12 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 @@ -33,19 +35,28 @@ 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. Both values are
/// opaque timestamps in the same caller-defined trusted time domain that will
/// be supplied to [`evaluate_agent_task_extension`]. This constructor does not
/// authenticate policy provenance or attest a clock; an invalid or empty
/// 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. Both values are opaque timestamps
/// in the same caller-defined trusted time domain supplied to
/// [`evaluate_agent_task_extension`]. This constructor does not authenticate
/// policy provenance or attest a clock; an invalid or empty validity window is
/// retained so evaluation can fail closed deterministically.
#[must_use]
pub fn new<I>(managed_extensions: I, valid_from: u64, valid_until: u64) -> Self
pub fn new<I>(
browser_session: BrowserSessionId,
managed_extensions: I,
valid_from: u64,
valid_until: u64,
) -> Self
where
I: IntoIterator<Item = ExtensionId>,
{
Self {
browser_session,
managed_extensions: managed_extensions.into_iter().collect(),
valid_from,
valid_until,
Expand All @@ -66,21 +77,27 @@ 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`, and `valid_until`
/// must use the same caller-attested time domain; this function does not read or
/// attest a clock. The validity window is half-open: `valid_from <= trusted_time <
/// valid_until`. Chromium permissions, installation state, native messaging, and
/// valid_until`. The current session is checked before allow-list membership so a
/// policy cannot be replayed across OriginWeave browser sessions or used there as
/// an 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.valid_from >= policy.valid_until {
Expand All @@ -92,6 +109,9 @@ pub fn evaluate_agent_task_extension(
if trusted_time >= policy.valid_until {
return AgentTaskExtensionDecision::DenyPolicyExpired;
}
if policy.browser_session != current_session {
return AgentTaskExtensionDecision::DenySessionMismatch;
}
if policy.managed_extensions.contains(extension_id) {
AgentTaskExtensionDecision::AllowManagedExtension
} else {
Expand Down
58 changes: 40 additions & 18 deletions crates/originweave-policy/tests/agent_task_extension_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ fn context(value: u64) -> BrowsingContextId {

#[test]
fn empty_agent_task_extension_policy_denies_every_extension() {
let policy = AgentTaskExtensionPolicy::new([], 10, 20);
let policy = AgentTaskExtensionPolicy::new(session(31), [], 10, 20);
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 @@ -35,75 +35,97 @@ 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);
let policy =
AgentTaskExtensionPolicy::new(session(31), [allowed.clone(), allowed.clone()], 10, 20);

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);

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 managed_agent_task_extension_policy_fails_closed_outside_its_validity_window() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20);
let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20);

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);
let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20);
let reversed = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 10);
let empty = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 20, 20);

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
);
}

#[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);
let policy =
AgentTaskExtensionPolicy::new(session(31), [extension.clone()], u64::MAX - 1, u64::MAX);

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);
let policy = AgentTaskExtensionPolicy::new(session(31), [extension.clone()], 10, 20);

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

Expand Down
Loading