From afabb68c58b132f27f13ceb47bfbbd947f0fdedf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:45:36 +0000 Subject: [PATCH 1/2] feat(core): bind extension grants to canonical origin Keep an extension-to-Agent grant from surviving same-session navigation or a port change. RFC 6454 treats scheme, host, and port as the origin tuple, so evaluate_extension_access now requires the request origin to match the grant. Co-authored-by: Seongho Bae --- CHANGELOG.md | 1 + crates/originweave-core/src/lib.rs | 17 +++++-- .../tests/extension_authority.rs | 51 +++++++++++++++++-- docs/TRD.md | 2 +- .../0013-manifest-v3-extension-authority.md | 2 +- docs/doctoring.md | 6 +++ .../extension-authority-security.md | 6 +++ 7 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39ca..3f1e88ef8 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 +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - 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. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 88dd2e586..5f862b862 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -967,16 +967,18 @@ pub struct ExtensionAgentGrant { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, capabilities: BTreeSet, } impl ExtensionAgentGrant { - /// Build an exact extension-to-Agent grant for one browser session and context. + /// Build an exact extension-to-Agent grant for one session, context, and origin. #[must_use] pub fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, capabilities: I, ) -> Self where @@ -986,6 +988,7 @@ impl ExtensionAgentGrant { extension_id, browser_session, browsing_context, + origin, capabilities: capabilities.into_iter().collect(), } } @@ -997,6 +1000,7 @@ pub struct ExtensionAccessRequest { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, capability: ExtensionAgentCapability, } @@ -1007,12 +1011,14 @@ impl ExtensionAccessRequest { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, capability: ExtensionAgentCapability, ) -> Self { Self { extension_id, browser_session, browsing_context, + origin, capability, } } @@ -1031,6 +1037,8 @@ pub enum ExtensionAccessDecision { DenyBrowserSessionMismatch, /// The request belongs to a different independently navigable browser context. DenyBrowsingContextMismatch, + /// The request belongs to a different canonical origin than the grant. + DenyOriginMismatch, /// The extension grant does not contain the requested OriginWeave capability. DenyCapabilityNotGranted, } @@ -1039,8 +1047,8 @@ pub enum ExtensionAccessDecision { /// /// A Chrome extension permission, installation state, or page capability is never /// consulted here. A future Chromium adapter must construct a host-originated -/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session/context -/// request at the boundary where Agent authority would otherwise cross. +/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session, context, +/// and canonical origin at the boundary where Agent authority would otherwise cross. #[must_use] pub fn evaluate_extension_access( request: &ExtensionAccessRequest, @@ -1058,6 +1066,9 @@ pub fn evaluate_extension_access( if request.browsing_context != grant.browsing_context { return ExtensionAccessDecision::DenyBrowsingContextMismatch; } + if request.origin != grant.origin { + return ExtensionAccessDecision::DenyOriginMismatch; + } if !grant.capabilities.contains(&request.capability) { return ExtensionAccessDecision::DenyCapabilityNotGranted; } diff --git a/crates/originweave-core/tests/extension_authority.rs b/crates/originweave-core/tests/extension_authority.rs index 82507a244..7c12ac0d9 100644 --- a/crates/originweave-core/tests/extension_authority.rs +++ b/crates/originweave-core/tests/extension_authority.rs @@ -2,7 +2,7 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, - ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, evaluate_extension_access, + ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, Origin, evaluate_extension_access, }; fn extension_id(value: &str) -> ExtensionId { @@ -17,6 +17,10 @@ fn context(value: u64) -> BrowsingContextId { BrowsingContextId::new(value).expect("nonzero browsing context") } +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("canonical origin") +} + #[test] fn extension_id_accepts_only_canonical_chromium_extension_ids() { let canonical = "abcdefghijklmnopabcdefghijklmnop"; @@ -43,10 +47,12 @@ fn extension_id_accepts_only_canonical_chromium_extension_ids() { fn extension_agent_access_requires_an_explicit_exact_grant() { let allowed_extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); let other_extension = extension_id("bcdefghijklmnopabcdefghijklmnopa"); + let granted_origin = origin("https://app.example"); let grant = ExtensionAgentGrant::new( allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -54,6 +60,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -68,6 +75,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { other_extension, session(7), context(11), + granted_origin.clone(), ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -79,6 +87,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(8), context(11), + granted_origin.clone(), ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -87,24 +96,51 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { ); let wrong_context = ExtensionAccessRequest::new( - allowed_extension, + allowed_extension.clone(), session(7), context(12), + granted_origin.clone(), ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( evaluate_extension_access(&wrong_context, Some(&grant)), ExtensionAccessDecision::DenyBrowsingContextMismatch ); + + let wrong_origin = ExtensionAccessRequest::new( + allowed_extension.clone(), + session(7), + context(11), + origin("https://other.example"), + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_origin, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); + + let wrong_port = ExtensionAccessRequest::new( + allowed_extension, + session(7), + context(11), + origin("https://app.example:8443"), + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_port, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); } #[test] fn chrome_permissions_never_imply_originweave_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://mail.example"); let grant = ExtensionAgentGrant::new( id.clone(), session(3), context(5), + granted_origin.clone(), [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -112,6 +148,7 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { id, session(3), context(5), + granted_origin, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!( @@ -123,10 +160,12 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { #[test] fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("http://127.0.0.1:8080"); let grant = ExtensionAgentGrant::new( id.clone(), session(13), context(17), + granted_origin.clone(), [ ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, @@ -137,7 +176,13 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, ] { - let request = ExtensionAccessRequest::new(id.clone(), session(13), context(17), capability); + let request = ExtensionAccessRequest::new( + id.clone(), + session(13), + context(17), + granted_origin.clone(), + capability, + ); assert_eq!( evaluate_extension_access(&request, Some(&grant)), ExtensionAccessDecision::Allow diff --git a/docs/TRD.md b/docs/TRD.md index 3e8030012..0caf7feb4 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -25,7 +25,7 @@ The current reusable Rust control plane is intentionally smaller than the final | Module / boundary | Current responsibility | Protected-main status | Active/non-shipped evidence | |---|---|---|---| -| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | PR #40 builds a protocol-ID registry on top of these values; it is not protected-main truth | +| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | Active origin-bound `ExtensionAgentGrant` evaluation adds canonical-origin matching to the existing extension/session/context grant; it is not protected-main truth until merge | | `originweave-policy` | Pure fail-closed action policy including purpose-bound sensitive-data authority. | **Implemented** | Trusted broker/runtime lifecycle remains separate planned work under issue #10 | | `originweave-destination` | Resolved-address classification, origin-bound snapshots, route authority, connection pinning, rebinding and redirect authority. | **Implemented** | PAC evaluation/proxy transport/CONNECT are still Planned | | `originweave-network` | Direct single-address TCP connection plan and exact operating-system peer verification. | **Implemented** | — | diff --git a/docs/adr/0013-manifest-v3-extension-authority.md b/docs/adr/0013-manifest-v3-extension-authority.md index e620edf9d..064f87f55 100644 --- a/docs/adr/0013-manifest-v3-extension-authority.md +++ b/docs/adr/0013-manifest-v3-extension-authority.md @@ -92,7 +92,7 @@ No persistent database migration is introduced. A release can roll back the Chro ## Open follow-ups -- Complete issue #27's compatibility matrix and production isolation acceptance. +- Complete issue #27's compatibility matrix and production isolation acceptance. Origin-bound `ExtensionAgentGrant` evaluation is the next protected-main candidate for the origin/resource-scope rule; expiry and task binding remain open. - Define managed-extension identity/update semantics. - Implement the native-messaging allow-list/process boundary before claiming support. - Integrate the complete Agent Task browser vertical slice under issue #28. diff --git a/docs/doctoring.md b/docs/doctoring.md index 75c107ef0..1d23fc4b6 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -14,6 +14,10 @@ The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal The exact Chromium regression evidence is pinned to revision `446d05d21720f0b3505ec21057b3e9f909784262`. A mutable `HEAD` reference is not sufficient for a reproducible security contract. +### Extension-to-Agent grant origin binding + +RFC 6454 defines a web origin as the scheme, host, and port tuple that browsers use to isolate authority. An OriginWeave `extension_grant` that is bound only to extension identity, session, and browsing context would remain valid after the same context navigates to another origin. OriginWeave therefore requires the grant and the request to carry the same canonical origin. A host change or a non-default port change is a different origin and cannot reuse the grant. This is grant-scope isolation only; it does not install an extension, parse Chrome messages, or mint Agent capabilities from Manifest V3 permissions. + ### Resolved destination and redirect safety Canonical origin identity is not a network-destination authorization. The IANA IPv4 and IPv6 Special-Purpose Address Space registries enumerate blocks whose source, destination, forwardability, globally reachable, and protocol-reserved properties differ. Both registries were last updated on 9 October 2025 and explicitly warn that registry presence does not guarantee routability in a particular local or global context. RFC 6890 established the common special-purpose registry fields, and RFC 8190 replaced the ambiguous `global` field with `globally reachable`. @@ -96,6 +100,8 @@ Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retriev Autio, C., Schwartz, R., Dunietz, J., Jain, S., Stanley, M., Tabassi, E., Hall, P., & Roberts, K. (2024). *Artificial intelligence risk management framework: Generative artificial intelligence profile* (NIST AI 600-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.600-1 +Barth, A. (2011). *The web origin concept* (RFC 6454). Internet Engineering Task Force. https://doi.org/10.17487/RFC6454 + Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the special-purpose IP address registries* (RFC 8190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8190 Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index a36380a31..62143e601 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -50,6 +50,12 @@ Exact head `e83749acd1cf5a0b778ba38eb9d6ed5a9bd1e68f` deliberately keeps only th The exact head has successful CI, exact owned production coverage, Security Scan, SAST and CodeRabbit status and is Ready for review. It has no raw secret bytes and does not create approval evidence, a broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or release claim. +### Origin-bound extension grant evaluation + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAccessRequest` to carry the same canonical origin. A same-session, same-context request for `https://other.example` or `https://app.example:8443` against a grant for `https://app.example` is `DenyOriginMismatch`. This does not install an extension, parse Chrome messages, bind expiry or task identity, or mint Agent capabilities from Manifest V3 permissions. + ## 4. Security interpretation The executable authority chain is intentionally non-transitive: From 16a872cddd0e336adfe0a686a75d057c05d8c74a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:51:21 +0000 Subject: [PATCH 2/2] feat(core): expire extension grants at exclusive trusted time Bind ExtensionAgentGrant to an exclusive expiry and require trusted evaluation time on ExtensionAccessRequest so a same-origin grant cannot be reused at or after the Agent Task deadline. Co-authored-by: Seongho Bae --- CHANGELOG.md | 1 + crates/originweave-core/src/lib.rs | 21 +++++- .../tests/extension_authority.rs | 68 +++++++++++++++++++ docs/TRD.md | 2 +- .../0013-manifest-v3-extension-authority.md | 2 +- docs/doctoring.md | 6 ++ .../extension-authority-security.md | 2 +- 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f1e88ef8..d17419927 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 +- 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. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - 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. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 5f862b862..b6ed55ff2 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -968,17 +968,19 @@ pub struct ExtensionAgentGrant { browser_session: BrowserSessionId, browsing_context: BrowsingContextId, origin: Origin, + expires_at_epoch_seconds: u64, capabilities: BTreeSet, } impl ExtensionAgentGrant { - /// Build an exact extension-to-Agent grant for one session, context, and origin. + /// Build an exact extension-to-Agent grant for one session, context, origin, and exclusive expiry. #[must_use] pub fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, origin: Origin, + expires_at_epoch_seconds: u64, capabilities: I, ) -> Self where @@ -989,6 +991,7 @@ impl ExtensionAgentGrant { browser_session, browsing_context, origin, + expires_at_epoch_seconds, capabilities: capabilities.into_iter().collect(), } } @@ -1001,17 +1004,22 @@ pub struct ExtensionAccessRequest { browser_session: BrowserSessionId, browsing_context: BrowsingContextId, origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, } impl ExtensionAccessRequest { /// Build one exact extension capability request without granting authority. + /// + /// `now_epoch_seconds` must be trusted evaluation time supplied by the host, + /// not a page, extension, or model clock. #[must_use] pub const fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, ) -> Self { Self { @@ -1019,6 +1027,7 @@ impl ExtensionAccessRequest { browser_session, browsing_context, origin, + now_epoch_seconds, capability, } } @@ -1027,7 +1036,7 @@ impl ExtensionAccessRequest { /// Result of evaluating an extension request against one explicit Agent grant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExtensionAccessDecision { - /// The exact extension, session, context, and capability are explicitly granted. + /// The exact extension, session, context, origin, unexpired grant, and capability are explicitly granted. Allow, /// No explicit extension-to-Agent grant was supplied. DenyMissingGrant, @@ -1039,6 +1048,8 @@ pub enum ExtensionAccessDecision { DenyBrowsingContextMismatch, /// The request belongs to a different canonical origin than the grant. DenyOriginMismatch, + /// Trusted evaluation time is at or after the grant's exclusive expiry. + DenyExpired, /// The extension grant does not contain the requested OriginWeave capability. DenyCapabilityNotGranted, } @@ -1048,7 +1059,8 @@ pub enum ExtensionAccessDecision { /// A Chrome extension permission, installation state, or page capability is never /// consulted here. A future Chromium adapter must construct a host-originated /// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session, context, -/// and canonical origin at the boundary where Agent authority would otherwise cross. +/// canonical origin, and exclusive expiry at the boundary where Agent authority +/// would otherwise cross. #[must_use] pub fn evaluate_extension_access( request: &ExtensionAccessRequest, @@ -1069,6 +1081,9 @@ pub fn evaluate_extension_access( if request.origin != grant.origin { return ExtensionAccessDecision::DenyOriginMismatch; } + if request.now_epoch_seconds >= grant.expires_at_epoch_seconds { + return ExtensionAccessDecision::DenyExpired; + } if !grant.capabilities.contains(&request.capability) { return ExtensionAccessDecision::DenyCapabilityNotGranted; } diff --git a/crates/originweave-core/tests/extension_authority.rs b/crates/originweave-core/tests/extension_authority.rs index 7c12ac0d9..f34c30e9b 100644 --- a/crates/originweave-core/tests/extension_authority.rs +++ b/crates/originweave-core/tests/extension_authority.rs @@ -21,6 +21,9 @@ fn origin(value: &str) -> Origin { Origin::parse(value).expect("canonical origin") } +const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; +const UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS: u64 = 1_700_000_600; + #[test] fn extension_id_accepts_only_canonical_chromium_extension_ids() { let canonical = "abcdefghijklmnopabcdefghijklmnop"; @@ -53,6 +56,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(11), granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -61,6 +65,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(11), granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -76,6 +81,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(11), granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -88,6 +94,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(8), context(11), granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -100,6 +107,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(12), granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -112,6 +120,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(11), origin("https://other.example"), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -124,6 +133,7 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { session(7), context(11), origin("https://app.example:8443"), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -141,6 +151,7 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { session(3), context(5), granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -149,6 +160,7 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { session(3), context(5), granted_origin, + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!( @@ -166,6 +178,7 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { session(13), context(17), granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, @@ -181,6 +194,7 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { session(13), context(17), granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, capability, ); assert_eq!( @@ -189,3 +203,57 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { ); } } + +#[test] +fn expired_origin_bound_grant_cannot_be_reused_after_exclusive_deadline() { + let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://billing.example"); + let expires_at_epoch_seconds = 1_700_000_100; + let grant = ExtensionAgentGrant::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + [ExtensionAgentCapability::ObserveCurrentContext], + ); + + let before_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds - 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&before_deadline, Some(&grant)), + ExtensionAccessDecision::Allow + ); + + let at_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&at_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); + + let after_deadline = ExtensionAccessRequest::new( + id, + session(19), + context(23), + granted_origin, + expires_at_epoch_seconds + 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&after_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); +} diff --git a/docs/TRD.md b/docs/TRD.md index 0caf7feb4..0e60e5ca5 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -25,7 +25,7 @@ The current reusable Rust control plane is intentionally smaller than the final | Module / boundary | Current responsibility | Protected-main status | Active/non-shipped evidence | |---|---|---|---| -| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | Active origin-bound `ExtensionAgentGrant` evaluation adds canonical-origin matching to the existing extension/session/context grant; it is not protected-main truth until merge | +| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | Active origin-bound `ExtensionAgentGrant` evaluation adds canonical-origin matching and exclusive trusted-time expiry; it is not protected-main truth until merge | | `originweave-policy` | Pure fail-closed action policy including purpose-bound sensitive-data authority. | **Implemented** | Trusted broker/runtime lifecycle remains separate planned work under issue #10 | | `originweave-destination` | Resolved-address classification, origin-bound snapshots, route authority, connection pinning, rebinding and redirect authority. | **Implemented** | PAC evaluation/proxy transport/CONNECT are still Planned | | `originweave-network` | Direct single-address TCP connection plan and exact operating-system peer verification. | **Implemented** | — | diff --git a/docs/adr/0013-manifest-v3-extension-authority.md b/docs/adr/0013-manifest-v3-extension-authority.md index 064f87f55..8feacbf27 100644 --- a/docs/adr/0013-manifest-v3-extension-authority.md +++ b/docs/adr/0013-manifest-v3-extension-authority.md @@ -92,7 +92,7 @@ No persistent database migration is introduced. A release can roll back the Chro ## Open follow-ups -- Complete issue #27's compatibility matrix and production isolation acceptance. Origin-bound `ExtensionAgentGrant` evaluation is the next protected-main candidate for the origin/resource-scope rule; expiry and task binding remain open. +- Complete issue #27's compatibility matrix and production isolation acceptance. Exclusive trusted-time expiry on origin-bound `ExtensionAgentGrant` evaluation is the next protected-main candidate; task identity binding remains open. - Define managed-extension identity/update semantics. - Implement the native-messaging allow-list/process boundary before claiming support. - Integrate the complete Agent Task browser vertical slice under issue #28. diff --git a/docs/doctoring.md b/docs/doctoring.md index 1d23fc4b6..f0133bb5d 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -18,6 +18,10 @@ The exact Chromium regression evidence is pinned to revision `446d05d21720f0b350 RFC 6454 defines a web origin as the scheme, host, and port tuple that browsers use to isolate authority. An OriginWeave `extension_grant` that is bound only to extension identity, session, and browsing context would remain valid after the same context navigates to another origin. OriginWeave therefore requires the grant and the request to carry the same canonical origin. A host change or a non-default port change is a different origin and cannot reuse the grant. This is grant-scope isolation only; it does not install an extension, parse Chrome messages, or mint Agent capabilities from Manifest V3 permissions. +### Extension-to-Agent grant exclusive expiry + +RFC 9700 is the current Best Current Practice for OAuth 2.0 security. It requires access tokens to be restricted in lifetime and treats long-lived bearer credentials as a standing authorization risk. An OriginWeave `extension_grant` that matches extension identity, session, browsing context, and canonical origin but has no exclusive expiry remains usable after the Agent Task window ends. OriginWeave therefore requires the grant to carry an exclusive `expires_at_epoch_seconds` deadline and the request to carry trusted `now_epoch_seconds`. Evaluation fails closed when `now >= expires_at`, matching the existing sensitive-handle exclusive-expiry rule. Page, extension, and model clocks are not trusted time. This slice does not bind task identity, install an extension, or mint Agent capabilities from Manifest V3 permissions. + ### Resolved destination and redirect safety Canonical origin identity is not a network-destination authorization. The IANA IPv4 and IPv6 Special-Purpose Address Space registries enumerate blocks whose source, destination, forwardability, globally reachable, and protocol-reserved properties differ. Both registries were last updated on 9 October 2025 and explicitly warn that registry presence does not guarantee routability in a particular local or global context. RFC 6890 established the common special-purpose registry fields, and RFC 8190 replaced the ambiguous `global` field with `globally reachable`. @@ -134,6 +138,8 @@ International Organization for Standardization. (2017). *Information and documen Koster, M., Illyes, G., Zeller, H., & Sassman, L. (2022). *Robots Exclusion Protocol* (RFC 9309). Internet Engineering Task Force. https://doi.org/10.17487/RFC9309 +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 security best current practice* (RFC 9700). Internet Engineering Task Force. https://doi.org/10.17487/RFC9700 + Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index 62143e601..1c211f83d 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -54,7 +54,7 @@ The exact head has successful CI, exact owned production coverage, Security Scan **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAccessRequest` to carry the same canonical origin. A same-session, same-context request for `https://other.example` or `https://app.example:8443` against a grant for `https://app.example` is `DenyOriginMismatch`. This does not install an extension, parse Chrome messages, bind expiry or task identity, or mint Agent capabilities from Manifest V3 permissions. +The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAccessRequest` to carry the same canonical origin. A same-session, same-context request for `https://other.example` or `https://app.example:8443` against a grant for `https://app.example` is `DenyOriginMismatch`. Exclusive trusted-time expiry is evaluated after that origin match: `now >= expires_at` is `DenyExpired`. This does not install an extension, parse Chrome messages, bind task identity, or mint Agent capabilities from Manifest V3 permissions. ## 4. Security interpretation