From 5c0e5be959fabe53367315a574840bdb1c4d175c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:06:39 +0900 Subject: [PATCH 1/5] test(policy): require extension proposal policy composition --- .../tests/extension_action_proposal.rs | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 crates/originweave-policy/tests/extension_action_proposal.rs diff --git a/crates/originweave-policy/tests/extension_action_proposal.rs b/crates/originweave-policy/tests/extension_action_proposal.rs new file mode 100644 index 00000000..c38fef52 --- /dev/null +++ b/crates/originweave-policy/tests/extension_action_proposal.rs @@ -0,0 +1,182 @@ +#![allow(clippy::expect_used)] + +//! Compose exact extension proposal authority with ordinary typed-action policy. +//! +//! An extension grant can authorize only the right to propose one typed action for independent +//! OriginWeave policy evaluation. It must not manufacture instruction trust, action capability, +//! origin authority, secret authority, approval, or action success. + +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, BrowserSessionId, + BrowsingContextId, Capability, ExecutionPurpose, ExtensionAccessDecision, + ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, InstructionSource, Origin, + PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, +}; +use originweave_policy::{ + Decision, DenialReason, ExtensionProposalDecision, evaluate_extension_action_proposal, +}; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn extension_id() -> ExtensionId { + ExtensionId::parse("abcdefghijklmnopabcdefghijklmnop").expect("valid extension id") +} + +fn browser_session() -> BrowserSessionId { + BrowserSessionId::new(17).expect("nonzero browser session") +} + +fn browsing_context() -> BrowsingContextId { + BrowsingContextId::new(23).expect("nonzero browsing context") +} + +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("valid test origin") +} + +fn intent() -> ActionIntentDigest { + ActionIntentDigest::parse(VALID_INTENT).expect("valid intent digest") +} + +fn proposal_grant() -> ExtensionAgentGrant { + ExtensionAgentGrant::new( + extension_id(), + browser_session(), + browsing_context(), + [ExtensionAgentCapability::ProposeTypedAction], + ) +} + +fn observe_request(source: InstructionSource) -> ActionRequest { + let site = origin("https://app.example"); + ActionRequest::new( + ActionKind::Observe, + site.clone(), + site, + source, + SecretDelivery::None, + intent(), + ) +} + +fn observe_context() -> PolicyContext { + PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::Observe]), + BTreeSet::from([origin("https://app.example")]), + BTreeSet::new(), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ) +} + +#[test] +fn missing_extension_grant_stops_before_action_policy() { + assert_eq!( + evaluate_extension_action_proposal( + &extension_id(), + browser_session(), + browsing_context(), + None, + &observe_request(InstructionSource::User), + &observe_context(), + ), + ExtensionProposalDecision::ExtensionAccessDenied( + ExtensionAccessDecision::DenyMissingGrant + ) + ); +} + +#[test] +fn non_proposal_extension_capability_cannot_submit_an_action() { + let grant = ExtensionAgentGrant::new( + extension_id(), + browser_session(), + browsing_context(), + [ExtensionAgentCapability::ObserveCurrentContext], + ); + + assert_eq!( + evaluate_extension_action_proposal( + &extension_id(), + browser_session(), + browsing_context(), + Some(&grant), + &observe_request(InstructionSource::User), + &observe_context(), + ), + ExtensionProposalDecision::ExtensionAccessDenied( + ExtensionAccessDecision::DenyCapabilityNotGranted + ) + ); +} + +#[test] +fn exact_proposal_grant_reaches_ordinary_action_policy() { + assert_eq!( + evaluate_extension_action_proposal( + &extension_id(), + browser_session(), + browsing_context(), + Some(&proposal_grant()), + &observe_request(InstructionSource::User), + &observe_context(), + ), + ExtensionProposalDecision::ActionPolicy(Decision::Allow) + ); +} + +#[test] +fn extension_transport_does_not_promote_web_content_to_instruction_authority() { + assert_eq!( + evaluate_extension_action_proposal( + &extension_id(), + browser_session(), + browsing_context(), + Some(&proposal_grant()), + &observe_request(InstructionSource::WebContent), + &observe_context(), + ), + ExtensionProposalDecision::ActionPolicy(Decision::Deny( + DenialReason::UntrustedInstructionSource + )) + ); +} + +#[test] +fn extension_proposal_grant_cannot_manufacture_secret_fill_approval() { + let site = origin("https://app.example"); + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([Capability::FillSecret]), + BTreeSet::from([site.clone()]), + BTreeSet::from([site.clone()]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + let request = ActionRequest::new( + ActionKind::FillSecret, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::BrokerHandle, + intent(), + ); + + assert_eq!( + evaluate_extension_action_proposal( + &extension_id(), + browser_session(), + browsing_context(), + Some(&proposal_grant()), + &request, + &context, + ), + ExtensionProposalDecision::ActionPolicy(Decision::RequireApproval(RiskClass::R3)) + ); +} From af629d3aebadb67041ba28b97dfab0cc01dcf283 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:08:49 +0900 Subject: [PATCH 2/5] test(policy): format extension proposal RED regression --- crates/originweave-policy/tests/extension_action_proposal.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-policy/tests/extension_action_proposal.rs b/crates/originweave-policy/tests/extension_action_proposal.rs index c38fef52..e1b8b157 100644 --- a/crates/originweave-policy/tests/extension_action_proposal.rs +++ b/crates/originweave-policy/tests/extension_action_proposal.rs @@ -85,9 +85,7 @@ fn missing_extension_grant_stops_before_action_policy() { &observe_request(InstructionSource::User), &observe_context(), ), - ExtensionProposalDecision::ExtensionAccessDenied( - ExtensionAccessDecision::DenyMissingGrant - ) + ExtensionProposalDecision::ExtensionAccessDenied(ExtensionAccessDecision::DenyMissingGrant) ); } From dfb411c219a8d935cf0a99dab3f9585e2400e3d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:13:08 +0900 Subject: [PATCH 3/5] feat(policy): compose extension proposal authority --- crates/originweave-policy/src/lib.rs | 45 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 243ae8ce..c32cf69c 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -16,8 +16,10 @@ pub use sensitive_data::{ }; use originweave_core::{ - ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, - InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, + ActionRequest, ApprovalEvidence, ApprovalScope, BrowserSessionId, BrowsingContextId, Capability, + ExecutionPurpose, ExtensionAccessDecision, ExtensionAccessRequest, ExtensionAgentCapability, + ExtensionAgentGrant, ExtensionId, InstructionSource, PolicyContext, RiskClass, RobotsDecision, + SecretDelivery, SessionMode, evaluate_extension_access, }; /// The result of evaluating one typed action request. @@ -31,6 +33,15 @@ pub enum Decision { RequireApproval(RiskClass), } +/// Result of composing exact extension proposal authority with ordinary action policy. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExtensionProposalDecision { + /// The extension/session/context lacks exact typed-action proposal authority. + ExtensionAccessDenied(ExtensionAccessDecision), + /// Proposal authority was present; this is the unchanged ordinary action-policy result. + ActionPolicy(Decision), +} + /// A stable reason that policy denied an action. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DenialReason { @@ -66,6 +77,36 @@ pub enum DenialReason { ApprovalScopeMismatch, } +/// Evaluate one extension-originated typed-action proposal without promoting transport authority. +/// +/// This function checks the exact extension, browser-session, browsing-context and +/// [`ExtensionAgentCapability::ProposeTypedAction`] grant before ordinary action policy. When +/// extension access is allowed, the caller-supplied [`ActionRequest`] is evaluated unchanged, so +/// its instruction source, capability, origin, secret-delivery and approval requirements cannot be +/// minted or rewritten by extension transport. This function does not parse extension messages, +/// execute browser input, resolve secrets, or claim an action post-condition. +#[must_use] +pub fn evaluate_extension_action_proposal( + extension_id: &ExtensionId, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + grant: Option<&ExtensionAgentGrant>, + request: &ActionRequest, + context: &PolicyContext, +) -> ExtensionProposalDecision { + let access_request = ExtensionAccessRequest::new( + extension_id.clone(), + browser_session, + browsing_context, + ExtensionAgentCapability::ProposeTypedAction, + ); + let access = evaluate_extension_access(&access_request, grant); + if access != ExtensionAccessDecision::Allow { + return ExtensionProposalDecision::ExtensionAccessDenied(access); + } + ExtensionProposalDecision::ActionPolicy(evaluate(request, context)) +} + /// Evaluate a typed browser action against one explicit policy context. #[must_use] pub fn evaluate(request: &ActionRequest, context: &PolicyContext) -> Decision { From c65a7c14036e052dee26b7fa6298a7d4e8353dbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:15:11 +0900 Subject: [PATCH 4/5] style(policy): apply canonical rustfmt --- crates/originweave-policy/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index c32cf69c..007f1548 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -16,10 +16,10 @@ pub use sensitive_data::{ }; use originweave_core::{ - ActionRequest, ApprovalEvidence, ApprovalScope, BrowserSessionId, BrowsingContextId, Capability, - ExecutionPurpose, ExtensionAccessDecision, ExtensionAccessRequest, ExtensionAgentCapability, - ExtensionAgentGrant, ExtensionId, InstructionSource, PolicyContext, RiskClass, RobotsDecision, - SecretDelivery, SessionMode, evaluate_extension_access, + ActionRequest, ApprovalEvidence, ApprovalScope, BrowserSessionId, BrowsingContextId, + Capability, ExecutionPurpose, ExtensionAccessDecision, ExtensionAccessRequest, + ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, InstructionSource, PolicyContext, + RiskClass, RobotsDecision, SecretDelivery, SessionMode, evaluate_extension_access, }; /// The result of evaluating one typed action request. From 3d2fff3daa766e5e6d7f25e7727a18e01ff52a2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:19:17 +0900 Subject: [PATCH 5/5] docs(changelog): record extension proposal composition --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39c..d5224b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Exact extension typed-action proposal composition that requires the matching extension/session/context `ProposeTypedAction` grant before ordinary unchanged action-policy evaluation; extension transport cannot manufacture instruction trust, Agent capability or origin authority, secret approval, browser execution, or verified success. - 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. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. @@ -73,4 +74,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 \ No newline at end of file