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
3 changes: 2 additions & 1 deletion 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

- 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.
- Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules.
- Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.
Expand Down Expand Up @@ -74,4 +75,4 @@ All notable changes to OriginWeave are documented in this file. The format follo
- The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it.
- The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels.

[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
53 changes: 52 additions & 1 deletion crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,62 @@ pub use sensitive_data::{
evaluate_handle_use,
};

use std::collections::BTreeSet;

use originweave_core::{
ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose,
ActionRequest, ApprovalEvidence, ApprovalScope, 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 {
managed_extensions: BTreeSet<ExtensionId>,
}

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.
#[must_use]
pub fn new<I>(managed_extensions: I) -> Self
where
I: IntoIterator<Item = ExtensionId>,
{
Self {
managed_extensions: managed_extensions.into_iter().collect(),
}
}
}

/// Result of evaluating one extension identity for an isolated Agent Task profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentTaskExtensionDecision {
/// The exact canonical extension identity appears in the managed allow-list.
AllowManagedExtension,
/// The extension identity is absent from the managed allow-list.
DenyNotManaged,
}

/// 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.
#[must_use]
pub fn evaluate_agent_task_extension(
extension_id: &ExtensionId,
policy: &AgentTaskExtensionPolicy,
) -> AgentTaskExtensionDecision {
if policy.managed_extensions.contains(extension_id) {
AgentTaskExtensionDecision::AllowManagedExtension
} else {
AgentTaskExtensionDecision::DenyNotManaged
}
}

/// The result of evaluating one typed action request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Expand Down
70 changes: 70 additions & 0 deletions crates/originweave-policy/tests/agent_task_extension_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#![allow(clippy::expect_used)]

use originweave_core::{
BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest,
ExtensionAgentCapability, ExtensionId, evaluate_extension_access,
};
use originweave_policy::{
AgentTaskExtensionDecision, AgentTaskExtensionPolicy, evaluate_agent_task_extension,
};

fn extension_id(value: &str) -> ExtensionId {
ExtensionId::parse(value).expect("valid extension identifier")
}

fn session(value: u64) -> BrowserSessionId {
BrowserSessionId::new(value).expect("nonzero browser session")
}

fn context(value: u64) -> BrowsingContextId {
BrowsingContextId::new(value).expect("nonzero browsing context")
}

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

assert_eq!(
evaluate_agent_task_extension(&extension, &policy),
AgentTaskExtensionDecision::DenyNotManaged
);
}

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

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

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

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

let request = ExtensionAccessRequest::new(
extension,
session(31),
context(37),
ExtensionAgentCapability::ProposeTypedAction,
);
assert_eq!(
evaluate_extension_access(&request, None),
ExtensionAccessDecision::DenyMissingGrant
);
}
Loading