Skip to content
Closed
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

- 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.
Expand Down
17 changes: 14 additions & 3 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,16 +967,18 @@ pub struct ExtensionAgentGrant {
extension_id: ExtensionId,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: Origin,
capabilities: BTreeSet<ExtensionAgentCapability>,
}

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<I>(
extension_id: ExtensionId,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: Origin,
capabilities: I,
) -> Self
where
Expand All @@ -986,6 +988,7 @@ impl ExtensionAgentGrant {
extension_id,
browser_session,
browsing_context,
origin,
capabilities: capabilities.into_iter().collect(),
}
}
Expand All @@ -997,6 +1000,7 @@ pub struct ExtensionAccessRequest {
extension_id: ExtensionId,
browser_session: BrowserSessionId,
browsing_context: BrowsingContextId,
origin: Origin,
capability: ExtensionAgentCapability,
}

Expand All @@ -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,
}
}
Expand All @@ -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,
}
Expand All @@ -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,
Expand All @@ -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;
}
Comment on lines +1069 to +1071

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Origin equality is the right check. Origin::parse already drops default HTTPS 443 / HTTP 80, so https://app.example and https://app.example:443 stay the same grant. Host change and :8443 are covered in the test. This is not a defect.

When the next grant-dimension slice touches this function, also update ExtensionAccessDecision::Allow rustdoc. It still says the grant is only extension, session, context, and capability.

if !grant.capabilities.contains(&request.capability) {
return ExtensionAccessDecision::DenyCapabilityNotGranted;
}
Expand Down
51 changes: 48 additions & 3 deletions crates/originweave-core/tests/extension_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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";
Expand All @@ -43,17 +47,20 @@ 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],
);

let exact = ExtensionAccessRequest::new(
allowed_extension.clone(),
session(7),
context(11),
granted_origin.clone(),
ExtensionAgentCapability::ObserveCurrentContext,
);
assert_eq!(
Expand All @@ -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!(
Expand All @@ -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!(
Expand All @@ -87,31 +96,59 @@ 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],
);

let propose_action = ExtensionAccessRequest::new(
id,
session(3),
context(5),
granted_origin,
ExtensionAgentCapability::ProposeTypedAction,
);
assert_eq!(
Expand All @@ -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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/TRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** | — |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0013-manifest-v3-extension-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions docs/doctoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/traceability/extension-authority-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section correctly keeps the slice at IMPLEMENTED_ON_ACTIVE_PR and does not close #27.

Section 4 below still draws explicit extension/session/context grant without origin. Leave that diagram until this head is on protected main; do not widen this PR to a docs-only rewrite.


## 4. Security interpretation

The executable authority chain is intentionally non-transitive:
Expand Down
Loading