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

- 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.
- Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline.
Expand Down
52 changes: 48 additions & 4 deletions crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,38 @@ use originweave_core::{
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentTaskExtensionPolicy {
managed_extensions: BTreeSet<ExtensionId>,
valid_from: u64,
valid_until: u64,
maximum_window: u64,
}

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.
#[must_use]
pub fn new<I>(managed_extensions: I) -> Self
pub fn new<I>(
managed_extensions: I,
valid_from: u64,
valid_until: u64,
maximum_window: u64,
) -> Self
where
I: IntoIterator<Item = ExtensionId>,
{
Self {
managed_extensions: managed_extensions.into_iter().collect(),
valid_from,
valid_until,
maximum_window,
}
}
}
Expand All @@ -51,19 +69,45 @@ pub enum AgentTaskExtensionDecision {
AllowManagedExtension,
/// The extension identity is absent from the managed allow-list.
DenyNotManaged,
/// The configured validity window is empty/reversed or its local ceiling is zero.
DenyInvalidPolicyWindow,
/// The configured policy validity interval exceeds the reviewed local maximum.
DenyPolicyWindowExceedsMaximum,
/// The trusted evaluation time precedes the policy validity window.
DenyPolicyNotYetValid,
/// The trusted evaluation time is at or beyond the policy expiry boundary.
DenyPolicyExpired,
}

/// 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. Chromium permissions,
/// installation state, native messaging, and [`originweave_core::ExtensionAgentGrant`]
/// remain separate authorities.
/// present in the caller's managed Agent Task profile 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.
#[must_use]
pub fn evaluate_agent_task_extension(
extension_id: &ExtensionId,
policy: &AgentTaskExtensionPolicy,
trusted_time: u64,
) -> AgentTaskExtensionDecision {
if policy.valid_from >= policy.valid_until || policy.maximum_window == 0 {
return AgentTaskExtensionDecision::DenyInvalidPolicyWindow;
}
if policy.valid_until - policy.valid_from > policy.maximum_window {
return AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum;
}
if trusted_time < policy.valid_from {
return AgentTaskExtensionDecision::DenyPolicyNotYetValid;
}
if trusted_time >= policy.valid_until {
return AgentTaskExtensionDecision::DenyPolicyExpired;
}
if policy.managed_extensions.contains(extension_id) {
AgentTaskExtensionDecision::AllowManagedExtension
} else {
Expand Down
85 changes: 78 additions & 7 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([]);
let policy = AgentTaskExtensionPolicy::new([], 10, 20, 10);
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");

assert_eq!(
evaluate_agent_task_extension(&extension, &policy),
evaluate_agent_task_extension(&extension, &policy, 15),
AgentTaskExtensionDecision::DenyNotManaged
);
}
Expand All @@ -39,25 +39,96 @@ 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()]);
let policy = AgentTaskExtensionPolicy::new([allowed.clone(), allowed.clone()], 10, 20, 10);

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

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

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 9),
AgentTaskExtensionDecision::DenyPolicyNotYetValid
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 20),
AgentTaskExtensionDecision::DenyPolicyExpired
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 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);

assert_eq!(
evaluate_agent_task_extension(&extension, &reversed, 15),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &empty, 20),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &zero_maximum, 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);

assert_eq!(
evaluate_agent_task_extension(&extension, &exact, 19),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &overlong, 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);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 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()]);
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);

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

Expand Down
Loading