From 3ccbfdb0e819e844e6d4ddee3ad0bb8ccf816aa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:13:49 +0900 Subject: [PATCH 001/632] test(browser-session): expose raw lifecycle port side door --- .../tests/lifecycle_port_authority.rs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 crates/originweave-browser-session/tests/lifecycle_port_authority.rs diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs new file mode 100644 index 000000000..78f924e6e --- /dev/null +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -0,0 +1,73 @@ +use originweave_browser_session::{ + BrowserSession, BrowserSessionIncarnation, DisposableContextCreateError, + DisposableContextDestroyError, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug)] +struct RecordingPort { + create_calls: usize, + destroy_calls: usize, +} + +impl DisposableContextPort for RecordingPort { + fn create_disposable_context( + &mut self, + _browser_session: BrowserSessionId, + _incarnation: BrowserSessionIncarnation, + ) -> Result { + self.create_calls += 1; + Ok(DisposableContextHandle::new( + DisposableIsolationId::parse("raw-port-side-door").expect("valid isolation id"), + BrowsingContextId::new(41).expect("valid browsing context"), + )) + } + + fn destroy_disposable_context( + &mut self, + _browser_session: BrowserSessionId, + _incarnation: BrowserSessionIncarnation, + _context: &DisposableContextHandle, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls += 1; + Ok(()) + } +} + +#[test] +fn raw_session_identity_cannot_directly_authorize_create_or_destroy() { + let session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) + .expect("incarnation capacity"); + let mut port = RecordingPort { + create_calls: 0, + destroy_calls: 0, + }; + + let direct_create = DisposableContextPort::create_disposable_context( + &mut port, + session.id(), + session.incarnation(), + ); + assert!( + direct_create.is_err(), + "raw session/incarnation values must not be sufficient lifecycle authority" + ); + assert_eq!(port.create_calls, 0, "unauthorized create reached adapter I/O"); + + let forged_handle = DisposableContextHandle::new( + DisposableIsolationId::parse("raw-port-side-door").expect("valid isolation id"), + BrowsingContextId::new(41).expect("valid browsing context"), + ); + let direct_destroy = DisposableContextPort::destroy_disposable_context( + &mut port, + session.id(), + session.incarnation(), + &forged_handle, + ); + assert!( + direct_destroy.is_err(), + "raw lifecycle tuple must not be sufficient destruction authority" + ); + assert_eq!(port.destroy_calls, 0, "unauthorized destroy reached adapter I/O"); +} From 2fde15ed66c10cddf511a82ca11959ed591ba2b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:17:44 +0900 Subject: [PATCH 002/632] test(browser-session): format lifecycle port hostile RED --- .../tests/lifecycle_port_authority.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs index 78f924e6e..6fcfd84dd 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -53,7 +53,10 @@ fn raw_session_identity_cannot_directly_authorize_create_or_destroy() { direct_create.is_err(), "raw session/incarnation values must not be sufficient lifecycle authority" ); - assert_eq!(port.create_calls, 0, "unauthorized create reached adapter I/O"); + assert_eq!( + port.create_calls, 0, + "unauthorized create reached adapter I/O" + ); let forged_handle = DisposableContextHandle::new( DisposableIsolationId::parse("raw-port-side-door").expect("valid isolation id"), @@ -69,5 +72,8 @@ fn raw_session_identity_cannot_directly_authorize_create_or_destroy() { direct_destroy.is_err(), "raw lifecycle tuple must not be sufficient destruction authority" ); - assert_eq!(port.destroy_calls, 0, "unauthorized destroy reached adapter I/O"); + assert_eq!( + port.destroy_calls, 0, + "unauthorized destroy reached adapter I/O" + ); } From d4537bc63c91d0f8ddfd3e8aa63fae04929f0fad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:26:48 +0900 Subject: [PATCH 003/632] fix(browser-session): bind lifecycle I/O to aggregate-issued requests --- crates/originweave-browser-session/src/lib.rs | 234 ++++++++++++++++-- 1 file changed, 210 insertions(+), 24 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 66f5753c5..17cd60cf8 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -44,6 +44,8 @@ pub enum BrowserSessionError { DuplicateBrowsingContext, /// The port returned an isolation identity already known to this aggregate. DuplicateDisposableIsolation, + /// A different lifecycle-port instance was supplied after this Browser Session bound its port. + LifecyclePortMismatch, /// The requested context is not currently owned and active in this session. ContextNotOwned, /// The supplied authority belongs to another incarnation, isolation boundary, session, context, or epoch. @@ -130,6 +132,33 @@ impl BrowserSessionIncarnation { } } +/// Stable non-zero identity for one live disposable-context lifecycle-port instance. +/// +/// A reviewed adapter assigns this identity when the adapter instance is created and keeps it stable +/// for that instance's lifetime. Browser Session binds the first port identity it uses and rejects a +/// different identity before lifecycle I/O. This value identifies an adapter instance; it grants no +/// lifecycle authority by itself. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DisposableContextPortId(u64); + +impl DisposableContextPortId { + /// Create a non-zero lifecycle-port instance identity. + #[must_use] + pub const fn new(value: u64) -> Option { + if value == 0 { + None + } else { + Some(Self(value)) + } + } + + /// Return the adapter-defined non-zero identity value. + #[must_use] + pub const fn value(self) -> u64 { + self.0 + } +} + /// Adapter result for one newly created disposable browser context. /// /// The isolation identity scopes the lifecycle boundary used for destruction; the browsing-context @@ -178,12 +207,87 @@ pub enum BrowserSessionRecoveryEvidence { UnprovenDestruction(DisposableContextHandle), } +/// Opaque Browser Session-issued request for one disposable-context creation attempt. +/// +/// There is deliberately no public constructor. Raw session, incarnation, or port identifiers are +/// insufficient to call the lifecycle port; Browser Session creates this request only after it has +/// validated aggregate state and bound the lifecycle-port instance. +#[derive(Debug)] +pub struct DisposableContextCreateRequest { + port_id: DisposableContextPortId, + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, +} + +impl DisposableContextCreateRequest { + /// Return the lifecycle-port instance this request is bound to. + #[must_use] + pub const fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } +} + +/// Opaque Browser Session-issued request for destruction of one exact owned disposable context. +/// +/// There is deliberately no public constructor. The request is created only after Browser Session +/// validates the supplied presentation authority against current aggregate ownership and the bound +/// lifecycle-port instance. +#[derive(Debug)] +pub struct DisposableContextDestroyRequest { + port_id: DisposableContextPortId, + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + context: DisposableContextHandle, +} + +impl DisposableContextDestroyRequest { + /// Return the lifecycle-port instance this request is bound to. + #[must_use] + pub const fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the exact domain handle whose remote isolation boundary must be destroyed. + #[must_use] + pub const fn context(&self) -> &DisposableContextHandle { + &self.context + } +} + /// Port implemented by a reviewed browser adapter for disposable context lifecycle operations. /// -/// `incarnation` is domain-issued and must participate in the adapter's lifecycle mapping; ignoring it -/// would reintroduce sequential ABA aliasing. `create_disposable_context` must create a fresh isolation -/// boundary and context owned exclusively by the supplied Browser Session incarnation. For WebDriver -/// BiDi the isolation identity maps one-to-one to the user-context identifier returned by +/// `port_id` must be side-effect-free, stable for one live adapter instance, and distinct from other +/// simultaneously usable instances. Browser Session binds the first port id used by an aggregate and +/// rejects a different id before create or destroy I/O. This closes the raw port side door and prevents +/// a second adapter instance from becoming an alternate lifecycle target after the aggregate is bound. +/// +/// The create/destroy requests have private construction paths. A caller that merely knows a browser +/// session id, incarnation, context id, isolation id, or port id cannot issue lifecycle I/O directly. +/// For WebDriver BiDi the isolation identity maps one-to-one to the user-context identifier returned by /// `browser.createUserContext`. /// /// [`DisposableContextCreateError::CreateFailedClean`] is allowed only when the adapter proves that no @@ -191,23 +295,23 @@ pub enum BrowserSessionRecoveryEvidence { /// verification becomes uncertain, the adapter must return it inside /// [`DisposableContextCreateError::CreateFailedUncertain`]. /// -/// `destroy_disposable_context` must destroy the exact boundary carried by the supplied handle and +/// `destroy_disposable_context` must destroy the exact boundary carried by the supplied request and /// return success only after destruction is proven. Reconstructing cleanup authority from raw driver /// identifiers is forbidden, and a command acknowledgement alone is insufficient evidence. pub trait DisposableContextPort { - /// Create one fresh disposable isolation boundary and browsing context for this incarnation. + /// Return this live adapter instance's stable lifecycle-port identity without browser I/O. + fn port_id(&self) -> DisposableContextPortId; + + /// Create one fresh disposable isolation boundary and browsing context for this authorized request. fn create_disposable_context( &mut self, - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, + request: &DisposableContextCreateRequest, ) -> Result; - /// Destroy the exact disposable isolation boundary represented by this handle and incarnation. + /// Destroy the exact disposable isolation boundary represented by this authorized request. fn destroy_disposable_context( &mut self, - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - context: &DisposableContextHandle, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError>; } @@ -291,6 +395,7 @@ pub struct BrowserSession { state: BrowserSessionState, transport_lost: bool, next_epoch: u64, + lifecycle_port_id: Option, contexts: BTreeMap, recovery_evidence: Vec, } @@ -315,6 +420,7 @@ impl BrowserSession { state: BrowserSessionState::Active, transport_lost: false, next_epoch: 1, + lifecycle_port_id: None, contexts: BTreeMap::new(), recovery_evidence: Vec::new(), }) @@ -356,8 +462,14 @@ impl BrowserSession { port: &mut P, ) -> Result { self.require_active()?; + let port_id = self.bind_lifecycle_port(port)?; let epoch = reserve_epoch(&mut self.next_epoch)?; - let handle = match port.create_disposable_context(self.id, self.incarnation) { + let request = DisposableContextCreateRequest { + port_id, + browser_session: self.id, + incarnation: self.incarnation, + }; + let handle = match port.create_disposable_context(&request) { Ok(handle) => handle, Err(DisposableContextCreateError::CreateFailedClean) => { return Err(BrowserSessionError::ContextCreationFailed); @@ -455,11 +567,17 @@ impl BrowserSession { authority: &PresentationMutationAuthority, port: &mut P, ) -> Result<(), BrowserSessionError> { + let port_id = self.require_bound_lifecycle_port(port)?; let browser_session = self.id; let incarnation = self.incarnation; let record = self.context_for_authority_mut(authority)?; - let handle = record.handle.clone(); - match port.destroy_disposable_context(browser_session, incarnation, &handle) { + let request = DisposableContextDestroyRequest { + port_id, + browser_session, + incarnation, + context: record.handle.clone(), + }; + match port.destroy_disposable_context(&request) { Ok(()) => { record.state = OwnedContextState::Destroyed; Ok(()) @@ -467,7 +585,9 @@ impl BrowserSession { Err(DisposableContextDestroyError::DestroyFailed) => { record.state = OwnedContextState::Uncertain; self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnprovenDestruction(handle)); + .push(BrowserSessionRecoveryEvidence::UnprovenDestruction( + request.context, + )); self.enter_recovery_required(); Err(BrowserSessionError::ContextDestructionFailed) } @@ -504,6 +624,31 @@ impl BrowserSession { Ok(()) } + fn bind_lifecycle_port( + &mut self, + port: &P, + ) -> Result { + let supplied = port.port_id(); + match self.lifecycle_port_id { + None => { + self.lifecycle_port_id = Some(supplied); + Ok(supplied) + } + Some(bound) if bound == supplied => Ok(bound), + Some(_) => Err(BrowserSessionError::LifecyclePortMismatch), + } + } + + fn require_bound_lifecycle_port( + &self, + port: &P, + ) -> Result { + match self.lifecycle_port_id { + Some(bound) if bound == port.port_id() => Ok(bound), + _ => Err(BrowserSessionError::LifecyclePortMismatch), + } + } + fn require_active(&self) -> Result<(), BrowserSessionError> { if self.state == BrowserSessionState::Active { Ok(()) @@ -587,6 +732,7 @@ mod tests { #[derive(Debug)] struct TestPort { + port_id: DisposableContextPortId, next_handle: DisposableContextHandle, create_error: Option, fail_destroy: bool, @@ -599,7 +745,12 @@ mod tests { impl TestPort { fn new(context: u64, isolation: &str) -> Self { + Self::with_port_id(context, isolation, 1) + } + + fn with_port_id(context: u64, isolation: &str, port_id: u64) -> Self { Self { + port_id: DisposableContextPortId::new(port_id).expect("valid port id"), next_handle: DisposableContextHandle::new( isolation_id(isolation), context_id(context), @@ -616,13 +767,17 @@ mod tests { } impl DisposableContextPort for TestPort { + fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + fn create_disposable_context( &mut self, - _browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, + request: &DisposableContextCreateRequest, ) -> Result { + assert_eq!(request.port_id(), self.port_id); self.create_calls += 1; - self.create_incarnations.push(incarnation); + self.create_incarnations.push(request.incarnation()); match self.create_error.clone() { Some(error) => Err(error), None => Ok(self.next_handle.clone()), @@ -631,13 +786,13 @@ mod tests { fn destroy_disposable_context( &mut self, - _browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - context: &DisposableContextHandle, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { + assert_eq!(request.port_id(), self.port_id); self.destroy_calls += 1; - self.destroy_incarnations.push(incarnation); - self.destroyed_isolations.push(context.isolation.clone()); + self.destroy_incarnations.push(request.incarnation()); + self.destroyed_isolations + .push(request.context().isolation.clone()); if self.fail_destroy { Err(DisposableContextDestroyError::DestroyFailed) } else { @@ -685,6 +840,11 @@ mod tests { let handle = DisposableContextHandle::new(valid.clone(), context_id(10)); assert_eq!(handle.isolation(), &valid); assert_eq!(handle.browsing_context(), context_id(10)); + assert_eq!(DisposableContextPortId::new(0), None); + assert_eq!( + DisposableContextPortId::new(17).expect("valid port id").value(), + 17 + ); } #[test] @@ -798,6 +958,32 @@ mod tests { ); } + #[test] + fn lifecycle_port_binding_rejects_other_adapter_before_io() { + let mut session = session(32); + let mut first_port = TestPort::with_port_id(320, "isolation-320", 11); + let authority = session + .create_disposable_context(&mut first_port) + .expect("first lifecycle port is bound"); + + let mut other_port = TestPort::with_port_id(321, "isolation-321", 12); + assert_eq!( + session.create_disposable_context(&mut other_port), + Err(BrowserSessionError::LifecyclePortMismatch) + ); + assert_eq!(other_port.create_calls, 0); + assert_eq!( + session.destroy_disposable_context(&authority, &mut other_port), + Err(BrowserSessionError::LifecyclePortMismatch) + ); + assert_eq!(other_port.destroy_calls, 0); + + session + .destroy_disposable_context(&authority, &mut first_port) + .expect("bound lifecycle port remains authorized"); + assert_eq!(first_port.destroy_calls, 1); + } + #[test] fn epoch_exhaustion_prevents_creation_io() { let mut exhausted_session = session(4); From 97e0a4d875166ad733e78c1d3f213454ee615f01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:27:10 +0900 Subject: [PATCH 004/632] test(browser-session): verify aggregate-issued lifecycle requests --- .../tests/lifecycle_port_authority.rs | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs index 6fcfd84dd..5c69cecfe 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -1,79 +1,81 @@ use originweave_browser_session::{ - BrowserSession, BrowserSessionIncarnation, DisposableContextCreateError, - DisposableContextDestroyError, DisposableContextHandle, DisposableContextPort, - DisposableIsolationId, + BrowserSession, BrowserSessionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct RecordingPort { + port_id: DisposableContextPortId, create_calls: usize, destroy_calls: usize, } +impl RecordingPort { + fn new(port_id: u64) -> Self { + Self { + port_id: DisposableContextPortId::new(port_id).expect("valid port id"), + create_calls: 0, + destroy_calls: 0, + } + } +} + impl DisposableContextPort for RecordingPort { + fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + fn create_disposable_context( &mut self, - _browser_session: BrowserSessionId, - _incarnation: BrowserSessionIncarnation, + request: &DisposableContextCreateRequest, ) -> Result { + assert_eq!(request.port_id(), self.port_id); + assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); self.create_calls += 1; Ok(DisposableContextHandle::new( - DisposableIsolationId::parse("raw-port-side-door").expect("valid isolation id"), + DisposableIsolationId::parse("aggregate-issued-request").expect("valid isolation id"), BrowsingContextId::new(41).expect("valid browsing context"), )) } fn destroy_disposable_context( &mut self, - _browser_session: BrowserSessionId, - _incarnation: BrowserSessionIncarnation, - _context: &DisposableContextHandle, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { + assert_eq!(request.port_id(), self.port_id); + assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); + assert_eq!(request.context().browsing_context(), BrowsingContextId::new(41).unwrap()); self.destroy_calls += 1; Ok(()) } } #[test] -fn raw_session_identity_cannot_directly_authorize_create_or_destroy() { - let session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) +fn aggregate_issued_request_binds_lifecycle_io_to_one_port() { + let mut session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) .expect("incarnation capacity"); - let mut port = RecordingPort { - create_calls: 0, - destroy_calls: 0, - }; + let mut bound_port = RecordingPort::new(101); + let authority = session + .create_disposable_context(&mut bound_port) + .expect("Browser Session-issued create request"); + assert_eq!(bound_port.create_calls, 1); - let direct_create = DisposableContextPort::create_disposable_context( - &mut port, - session.id(), - session.incarnation(), - ); - assert!( - direct_create.is_err(), - "raw session/incarnation values must not be sufficient lifecycle authority" - ); + let mut other_port = RecordingPort::new(102); assert_eq!( - port.create_calls, 0, - "unauthorized create reached adapter I/O" - ); - - let forged_handle = DisposableContextHandle::new( - DisposableIsolationId::parse("raw-port-side-door").expect("valid isolation id"), - BrowsingContextId::new(41).expect("valid browsing context"), - ); - let direct_destroy = DisposableContextPort::destroy_disposable_context( - &mut port, - session.id(), - session.incarnation(), - &forged_handle, - ); - assert!( - direct_destroy.is_err(), - "raw lifecycle tuple must not be sufficient destruction authority" + session.create_disposable_context(&mut other_port), + Err(BrowserSessionError::LifecyclePortMismatch) ); + assert_eq!(other_port.create_calls, 0, "wrong port reached create I/O"); assert_eq!( - port.destroy_calls, 0, - "unauthorized destroy reached adapter I/O" + session.destroy_disposable_context(&authority, &mut other_port), + Err(BrowserSessionError::LifecyclePortMismatch) ); + assert_eq!(other_port.destroy_calls, 0, "wrong port reached destroy I/O"); + + session + .destroy_disposable_context(&authority, &mut bound_port) + .expect("Browser Session-issued destroy request"); + assert_eq!(bound_port.destroy_calls, 1); } From f4b6faad15042c40c90db781055d1c87d0ff79ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:37:28 +0900 Subject: [PATCH 005/632] test(browser-session): expose same-id lifecycle port spoof --- .../tests/lifecycle_port_same_id_spoof.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs diff --git a/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs new file mode 100644 index 000000000..bf437dc20 --- /dev/null +++ b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs @@ -0,0 +1,94 @@ +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug)] +struct RecordingPort { + port_id: DisposableContextPortId, + context: BrowsingContextId, + isolation: &'static str, + create_calls: usize, + destroy_calls: usize, +} + +impl RecordingPort { + fn new(port_id: u64, context: u64, isolation: &'static str) -> Self { + Self { + port_id: DisposableContextPortId::new(port_id).expect("valid port id"), + context: BrowsingContextId::new(context).expect("valid browsing context"), + isolation, + create_calls: 0, + destroy_calls: 0, + } + } +} + +impl DisposableContextPort for RecordingPort { + fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + + fn create_disposable_context( + &mut self, + request: &DisposableContextCreateRequest, + ) -> Result { + assert_eq!(request.port_id(), self.port_id); + self.create_calls += 1; + Ok(DisposableContextHandle::new( + DisposableIsolationId::parse(self.isolation).expect("valid isolation id"), + self.context, + )) + } + + fn destroy_disposable_context( + &mut self, + request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + assert_eq!(request.port_id(), self.port_id); + self.destroy_calls += 1; + Ok(()) + } +} + +#[test] +fn distinct_port_with_same_claimed_id_cannot_create() { + let mut session = BrowserSession::start(BrowserSessionId::new(17).expect("valid session id")) + .expect("incarnation capacity"); + let mut approved_port = RecordingPort::new(101, 41, "approved-isolation"); + session + .create_disposable_context(&mut approved_port) + .expect("bind approved port"); + + let mut spoofing_port = RecordingPort::new(101, 42, "spoofed-isolation"); + assert_eq!( + session.create_disposable_context(&mut spoofing_port), + Err(BrowserSessionError::LifecyclePortMismatch) + ); + assert_eq!( + spoofing_port.create_calls, 0, + "distinct adapter with the same self-reported id reached create I/O" + ); +} + +#[test] +fn distinct_port_with_same_claimed_id_cannot_destroy() { + let mut session = BrowserSession::start(BrowserSessionId::new(18).expect("valid session id")) + .expect("incarnation capacity"); + let mut approved_port = RecordingPort::new(101, 51, "approved-isolation"); + let authority = session + .create_disposable_context(&mut approved_port) + .expect("bind approved port"); + + let mut spoofing_port = RecordingPort::new(101, 52, "spoofed-isolation"); + assert_eq!( + session.destroy_disposable_context(&authority, &mut spoofing_port), + Err(BrowserSessionError::LifecyclePortMismatch) + ); + assert_eq!( + spoofing_port.destroy_calls, 0, + "distinct adapter with the same self-reported id reached destroy I/O" + ); +} From d8388560d0c3bbebf8496812687d92139f6bb6f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:40:28 +0900 Subject: [PATCH 006/632] test(browser-session): update recovery port contract --- .../destroy_failure_requires_recovery.rs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs index 669f0723c..5833504a5 100644 --- a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs +++ b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs @@ -1,12 +1,14 @@ use originweave_browser_session::{ - BrowserSession, BrowserSessionError, BrowserSessionIncarnation, BrowserSessionRecoveryEvidence, - BrowserSessionState, DisposableContextCreateError, DisposableContextDestroyError, - DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableContextPortId, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct FailingDestroyPort { + port_id: DisposableContextPortId, next_handle: DisposableContextHandle, create_calls: usize, destroy_calls: usize, @@ -18,7 +20,10 @@ impl FailingDestroyPort { .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; + let port_id = DisposableContextPortId::new(context) + .ok_or("static fixture lifecycle port id must be non-zero")?; Ok(Self { + port_id, next_handle: DisposableContextHandle::new(isolation, browsing_context), create_calls: 0, destroy_calls: 0, @@ -27,21 +32,24 @@ impl FailingDestroyPort { } impl DisposableContextPort for FailingDestroyPort { + fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + fn create_disposable_context( &mut self, - _browser_session: BrowserSessionId, - _incarnation: BrowserSessionIncarnation, + request: &DisposableContextCreateRequest, ) -> Result { + assert_eq!(request.port_id(), self.port_id); self.create_calls += 1; Ok(self.next_handle.clone()) } fn destroy_disposable_context( &mut self, - _browser_session: BrowserSessionId, - _incarnation: BrowserSessionIncarnation, - _context: &DisposableContextHandle, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { + assert_eq!(request.port_id(), self.port_id); self.destroy_calls += 1; Err(DisposableContextDestroyError::DestroyFailed) } From b6432ab4944098aa2c36288985b1d8ffa323cb82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:40:46 +0900 Subject: [PATCH 007/632] test(browser-session): update incarnation reuse port contract --- .../tests/sequential_incarnation_reuse.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs index 355201280..a3e49f1b6 100644 --- a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs +++ b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs @@ -1,12 +1,13 @@ use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionIncarnation, DisposableContextCreateError, - DisposableContextDestroyError, DisposableContextHandle, DisposableContextPort, - DisposableIsolationId, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct ReusingPort { + port_id: DisposableContextPortId, handle: DisposableContextHandle, create_incarnations: Vec, destroy_incarnations: Vec, @@ -18,7 +19,10 @@ impl ReusingPort { .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; + let port_id = DisposableContextPortId::new(context) + .ok_or("static fixture lifecycle port id must be non-zero")?; Ok(Self { + port_id, handle: DisposableContextHandle::new(isolation, browsing_context), create_incarnations: Vec::new(), destroy_incarnations: Vec::new(), @@ -27,22 +31,25 @@ impl ReusingPort { } impl DisposableContextPort for ReusingPort { + fn port_id(&self) -> DisposableContextPortId { + self.port_id + } + fn create_disposable_context( &mut self, - _browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, + request: &DisposableContextCreateRequest, ) -> Result { - self.create_incarnations.push(incarnation); + assert_eq!(request.port_id(), self.port_id); + self.create_incarnations.push(request.incarnation()); Ok(self.handle.clone()) } fn destroy_disposable_context( &mut self, - _browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - _context: &DisposableContextHandle, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - self.destroy_incarnations.push(incarnation); + assert_eq!(request.port_id(), self.port_id); + self.destroy_incarnations.push(request.incarnation()); Ok(()) } } From 9caf9bbe4228c443b7d5a4279831765a6a38765a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:43:42 +0900 Subject: [PATCH 008/632] style(browser-session): apply canonical rustfmt --- .../tests/lifecycle_port_authority.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs index 5c69cecfe..b2b23f541 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -46,7 +46,10 @@ impl DisposableContextPort for RecordingPort { ) -> Result<(), DisposableContextDestroyError> { assert_eq!(request.port_id(), self.port_id); assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); - assert_eq!(request.context().browsing_context(), BrowsingContextId::new(41).unwrap()); + assert_eq!( + request.context().browsing_context(), + BrowsingContextId::new(41).unwrap() + ); self.destroy_calls += 1; Ok(()) } @@ -72,7 +75,10 @@ fn aggregate_issued_request_binds_lifecycle_io_to_one_port() { session.destroy_disposable_context(&authority, &mut other_port), Err(BrowserSessionError::LifecyclePortMismatch) ); - assert_eq!(other_port.destroy_calls, 0, "wrong port reached destroy I/O"); + assert_eq!( + other_port.destroy_calls, 0, + "wrong port reached destroy I/O" + ); session .destroy_disposable_context(&authority, &mut bound_port) From d43a4d86c8487ebdb9db9f1c4650fb7ee6225afc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:08:26 +0900 Subject: [PATCH 009/632] test(browser-session): expose lifecycle preflight side effect --- .../lifecycle_port_preflight_side_effect.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs diff --git a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs new file mode 100644 index 000000000..9a5baafa1 --- /dev/null +++ b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs @@ -0,0 +1,67 @@ +use std::cell::Cell; + +use originweave_browser_session::{ + BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, + DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, + DisposableContextPort, DisposableContextPortId, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug)] +struct SideEffectingIdentityPort { + identity_callbacks: Cell, + create_calls: usize, +} + +impl SideEffectingIdentityPort { + fn new() -> Self { + Self { + identity_callbacks: Cell::new(0), + create_calls: 0, + } + } +} + +impl DisposableContextPort for SideEffectingIdentityPort { + fn port_id(&self) -> DisposableContextPortId { + self.identity_callbacks + .set(self.identity_callbacks.get().saturating_add(1)); + DisposableContextPortId::new(401).expect("valid port id") + } + + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_calls += 1; + Ok(DisposableContextHandle::new( + DisposableIsolationId::parse("preflight-user-context").expect("valid isolation id"), + BrowsingContextId::new(401).expect("valid browsing context"), + )) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +#[test] +fn lifecycle_authority_does_not_depend_on_side_effecting_identity_preflight() { + let mut session = BrowserSession::start(BrowserSessionId::new(401).expect("valid session id")) + .expect("incarnation capacity"); + let mut port = SideEffectingIdentityPort::new(); + + session + .create_disposable_context(&mut port) + .expect("authorized create"); + + assert_eq!( + port.identity_callbacks.get(), + 0, + "Browser Session invoked an arbitrary adapter callback before lifecycle authority was established" + ); + assert_eq!(port.create_calls, 1); +} From 9cde981899950b900698a17e7fa739af59f6bb4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 06:12:36 +0900 Subject: [PATCH 010/632] fix(browser-session): bind lifecycle port structurally --- crates/originweave-browser-session/src/lib.rs | 708 +++++++++--------- .../destroy_failure_requires_recovery.rs | 61 +- .../tests/lifecycle_port_authority.rs | 53 +- .../lifecycle_port_preflight_side_effect.rs | 34 +- .../tests/lifecycle_port_same_id_spoof.rs | 73 +- .../tests/sequential_incarnation_reuse.rs | 68 +- ...er-session-disposable-context-authority.md | 107 ++- .../browser-session-lifecycle-authority.md | 66 +- .../browser-session-lifecycle-authority.md | 62 +- ...test_browser_session_lifecycle_contract.py | 63 +- 10 files changed, 659 insertions(+), 636 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 17cd60cf8..f020f1c9c 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -44,8 +44,6 @@ pub enum BrowserSessionError { DuplicateBrowsingContext, /// The port returned an isolation identity already known to this aggregate. DuplicateDisposableIsolation, - /// A different lifecycle-port instance was supplied after this Browser Session bound its port. - LifecyclePortMismatch, /// The requested context is not currently owned and active in this session. ContextNotOwned, /// The supplied authority belongs to another incarnation, isolation boundary, session, context, or epoch. @@ -132,33 +130,6 @@ impl BrowserSessionIncarnation { } } -/// Stable non-zero identity for one live disposable-context lifecycle-port instance. -/// -/// A reviewed adapter assigns this identity when the adapter instance is created and keeps it stable -/// for that instance's lifetime. Browser Session binds the first port identity it uses and rejects a -/// different identity before lifecycle I/O. This value identifies an adapter instance; it grants no -/// lifecycle authority by itself. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DisposableContextPortId(u64); - -impl DisposableContextPortId { - /// Create a non-zero lifecycle-port instance identity. - #[must_use] - pub const fn new(value: u64) -> Option { - if value == 0 { - None - } else { - Some(Self(value)) - } - } - - /// Return the adapter-defined non-zero identity value. - #[must_use] - pub const fn value(self) -> u64 { - self.0 - } -} - /// Adapter result for one newly created disposable browser context. /// /// The isolation identity scopes the lifecycle boundary used for destruction; the browsing-context @@ -209,23 +180,16 @@ pub enum BrowserSessionRecoveryEvidence { /// Opaque Browser Session-issued request for one disposable-context creation attempt. /// -/// There is deliberately no public constructor. Raw session, incarnation, or port identifiers are -/// insufficient to call the lifecycle port; Browser Session creates this request only after it has -/// validated aggregate state and bound the lifecycle-port instance. +/// There is deliberately no public constructor. A request is created only inside a +/// [`BoundBrowserSession`], after Browser Session has validated that the aggregate is active. Raw +/// session, incarnation, context, isolation, or adapter-selected identifiers cannot recreate it. #[derive(Debug)] pub struct DisposableContextCreateRequest { - port_id: DisposableContextPortId, browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, } impl DisposableContextCreateRequest { - /// Return the lifecycle-port instance this request is bound to. - #[must_use] - pub const fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - /// Return the Browser Session transport identity for adapter addressability. #[must_use] pub const fn browser_session(&self) -> BrowserSessionId { @@ -241,24 +205,17 @@ impl DisposableContextCreateRequest { /// Opaque Browser Session-issued request for destruction of one exact owned disposable context. /// -/// There is deliberately no public constructor. The request is created only after Browser Session -/// validates the supplied presentation authority against current aggregate ownership and the bound -/// lifecycle-port instance. +/// There is deliberately no public constructor. The bound aggregate creates this request only after +/// validating the supplied presentation authority against current ownership. A caller cannot rebuild +/// cleanup authority from raw browser identifiers. #[derive(Debug)] pub struct DisposableContextDestroyRequest { - port_id: DisposableContextPortId, browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, context: DisposableContextHandle, } impl DisposableContextDestroyRequest { - /// Return the lifecycle-port instance this request is bound to. - #[must_use] - pub const fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - /// Return the Browser Session transport identity for adapter addressability. #[must_use] pub const fn browser_session(&self) -> BrowserSessionId { @@ -280,28 +237,21 @@ impl DisposableContextDestroyRequest { /// Port implemented by a reviewed browser adapter for disposable context lifecycle operations. /// -/// `port_id` must be side-effect-free, stable for one live adapter instance, and distinct from other -/// simultaneously usable instances. Browser Session binds the first port id used by an aggregate and -/// rejects a different id before create or destroy I/O. This closes the raw port side door and prevents -/// a second adapter instance from becoming an alternate lifecycle target after the aggregate is bound. -/// -/// The create/destroy requests have private construction paths. A caller that merely knows a browser -/// session id, incarnation, context id, isolation id, or port id cannot issue lifecycle I/O directly. -/// For WebDriver BiDi the isolation identity maps one-to-one to the user-context identifier returned by -/// `browser.createUserContext`. +/// The port never self-asserts an instance identifier. Instead, Browser Session consumes one concrete +/// port value into [`BoundBrowserSession`]. Public lifecycle methods then use only that owned port, so a +/// caller cannot swap a second adapter instance into create or destroy after binding. The port receives +/// only aggregate-issued request values with private construction paths. /// -/// [`DisposableContextCreateError::CreateFailedClean`] is allowed only when the adapter proves that no -/// disposable state was created. If a user-context identity is already known when later creation or -/// verification becomes uncertain, the adapter must return it inside -/// [`DisposableContextCreateError::CreateFailedUncertain`]. +/// For WebDriver BiDi, creation should map the isolation identity one-to-one to the user-context +/// identifier returned by `browser.createUserContext`. [`DisposableContextCreateError::CreateFailedClean`] +/// is allowed only when the adapter proves that no disposable state was created. If a user-context +/// identity is already known when later creation or verification becomes uncertain, the adapter must +/// return it inside [`DisposableContextCreateError::CreateFailedUncertain`]. /// /// `destroy_disposable_context` must destroy the exact boundary carried by the supplied request and /// return success only after destruction is proven. Reconstructing cleanup authority from raw driver /// identifiers is forbidden, and a command acknowledgement alone is insufficient evidence. pub trait DisposableContextPort { - /// Return this live adapter instance's stable lifecycle-port identity without browser I/O. - fn port_id(&self) -> DisposableContextPortId; - /// Create one fresh disposable isolation boundary and browsing context for this authorized request. fn create_disposable_context( &mut self, @@ -330,8 +280,9 @@ impl BrowserContextEpoch { /// Opaque proof that Browser Session currently owns presentation mutation for one context epoch. /// /// The fields are private and no public constructor exists. A caller obtains this value only after -/// Browser Session has created a disposable boundary through its lifecycle port. Session incarnation, -/// isolation identity, context identity, and epoch must all still match before adapter I/O is allowed. +/// Browser Session has created a disposable boundary through its bound lifecycle port. Session +/// incarnation, isolation identity, context identity, and epoch must all still match before adapter I/O +/// is allowed. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PresentationMutationAuthority { browser_session: BrowserSessionId, @@ -395,11 +346,21 @@ pub struct BrowserSession { state: BrowserSessionState, transport_lost: bool, next_epoch: u64, - lifecycle_port_id: Option, contexts: BTreeMap, recovery_evidence: Vec, } +/// Browser Session composed with the one lifecycle-port instance allowed to mutate its remote state. +/// +/// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and +/// no public Browser Session lifecycle method accepts an arbitrary port parameter. This makes adapter +/// ownership structural rather than dependent on a caller-selected scalar or an adapter callback. +#[derive(Debug)] +pub struct BoundBrowserSession

{ + session: BrowserSession, + port: P, +} + impl BrowserSession { /// Start an active Browser Session around an already validated transport session identity. /// @@ -420,12 +381,23 @@ impl BrowserSession { state: BrowserSessionState::Active, transport_lost: false, next_epoch: 1, - lifecycle_port_id: None, contexts: BTreeMap::new(), recovery_evidence: Vec::new(), }) } + /// Consume this aggregate and one concrete lifecycle port into a linear bound session. + /// + /// Binding invokes no adapter method. All subsequent create/destroy I/O is reachable only through + /// the owned port inside the returned wrapper. + #[must_use] + pub fn bind_lifecycle_port(self, port: P) -> BoundBrowserSession

{ + BoundBrowserSession { + session: self, + port, + } + } + /// Return this aggregate's browser-session transport identity. #[must_use] pub const fn id(&self) -> BrowserSessionId { @@ -456,16 +428,85 @@ impl BrowserSession { &self.recovery_evidence } - /// Create and register one disposable context, then mint authority for its first epoch. - pub fn create_disposable_context( + /// Return current presentation authority for an already-owned active context. + pub fn presentation_authority( + &self, + browsing_context: BrowsingContextId, + ) -> Result { + self.require_active()?; + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + Ok(Self::authority_for( + self.id, + self.incarnation, + &record.handle, + record.epoch, + )) + } + + /// Advance one active owned context to a new authority epoch. + pub fn advance_context_epoch( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.require_active()?; + let browser_session = self.id; + let incarnation = self.incarnation; + let record = self + .contexts + .get_mut(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + let next = reserve_epoch(&mut self.next_epoch)?; + record.epoch = next; + Ok(Self::authority_for( + browser_session, + incarnation, + &record.handle, + next, + )) + } + + /// Record browser transport loss independently from ownership-recovery state. + /// + /// Returns `true` only for the first observed transport loss. If ownership was already uncertain, + /// `RecoveryRequired` remains the lifecycle state while the transport-loss fact is retained. + pub fn record_transport_loss(&mut self) -> bool { + if self.transport_lost || self.state == BrowserSessionState::Ended { + return false; + } + self.transport_lost = true; + if self.state == BrowserSessionState::Active { + self.state = BrowserSessionState::TransportLost; + self.mark_active_contexts_uncertain(); + } + true + } + + /// End the Browser Session only after every owned context has proven destruction. + pub fn end(&mut self) -> Result<(), BrowserSessionError> { + self.require_active()?; + if self + .contexts + .values() + .any(|record| record.state != OwnedContextState::Destroyed) + { + return Err(BrowserSessionError::ActiveContextRemains); + } + self.state = BrowserSessionState::Ended; + Ok(()) + } + + fn create_disposable_context_with_port( &mut self, port: &mut P, ) -> Result { self.require_active()?; - let port_id = self.bind_lifecycle_port(port)?; let epoch = reserve_epoch(&mut self.next_epoch)?; let request = DisposableContextCreateRequest { - port_id, browser_session: self.id, incarnation: self.incarnation, }; @@ -519,60 +560,15 @@ impl BrowserSession { Ok(authority) } - /// Return current presentation authority for an already-owned active context. - pub fn presentation_authority( - &self, - browsing_context: BrowsingContextId, - ) -> Result { - self.require_active()?; - let record = self - .contexts - .get(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - Ok(Self::authority_for( - self.id, - self.incarnation, - &record.handle, - record.epoch, - )) - } - - /// Advance one active owned context to a new authority epoch. - pub fn advance_context_epoch( - &mut self, - browsing_context: BrowsingContextId, - ) -> Result { - self.require_active()?; - let browser_session = self.id; - let incarnation = self.incarnation; - let record = self - .contexts - .get_mut(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - let next = reserve_epoch(&mut self.next_epoch)?; - record.epoch = next; - Ok(Self::authority_for( - browser_session, - incarnation, - &record.handle, - next, - )) - } - - /// Destroy the disposable isolation boundary covered by the supplied exact-epoch authority. - pub fn destroy_disposable_context( + fn destroy_disposable_context_with_port( &mut self, authority: &PresentationMutationAuthority, port: &mut P, ) -> Result<(), BrowserSessionError> { - let port_id = self.require_bound_lifecycle_port(port)?; let browser_session = self.id; let incarnation = self.incarnation; let record = self.context_for_authority_mut(authority)?; let request = DisposableContextDestroyRequest { - port_id, browser_session, incarnation, context: record.handle.clone(), @@ -594,61 +590,6 @@ impl BrowserSession { } } - /// Record browser transport loss independently from ownership-recovery state. - /// - /// Returns `true` only for the first observed transport loss. If ownership was already uncertain, - /// `RecoveryRequired` remains the lifecycle state while the transport-loss fact is retained. - pub fn record_transport_loss(&mut self) -> bool { - if self.transport_lost || self.state == BrowserSessionState::Ended { - return false; - } - self.transport_lost = true; - if self.state == BrowserSessionState::Active { - self.state = BrowserSessionState::TransportLost; - self.mark_active_contexts_uncertain(); - } - true - } - - /// End the Browser Session only after every owned context has proven destruction. - pub fn end(&mut self) -> Result<(), BrowserSessionError> { - self.require_active()?; - if self - .contexts - .values() - .any(|record| record.state != OwnedContextState::Destroyed) - { - return Err(BrowserSessionError::ActiveContextRemains); - } - self.state = BrowserSessionState::Ended; - Ok(()) - } - - fn bind_lifecycle_port( - &mut self, - port: &P, - ) -> Result { - let supplied = port.port_id(); - match self.lifecycle_port_id { - None => { - self.lifecycle_port_id = Some(supplied); - Ok(supplied) - } - Some(bound) if bound == supplied => Ok(bound), - Some(_) => Err(BrowserSessionError::LifecyclePortMismatch), - } - } - - fn require_bound_lifecycle_port( - &self, - port: &P, - ) -> Result { - match self.lifecycle_port_id { - Some(bound) if bound == port.port_id() => Ok(bound), - _ => Err(BrowserSessionError::LifecyclePortMismatch), - } - } - fn require_active(&self) -> Result<(), BrowserSessionError> { if self.state == BrowserSessionState::Active { Ok(()) @@ -706,6 +647,63 @@ impl BrowserSession { } } +impl BoundBrowserSession

{ + /// Return the bound Browser Session for read-only policy and ACL validation. + #[must_use] + pub const fn browser_session(&self) -> &BrowserSession { + &self.session + } + + /// Return the bound lifecycle port for read-only diagnostics and adapter-local planning. + #[must_use] + pub const fn lifecycle_port(&self) -> &P { + &self.port + } + + /// Create one disposable context through the exact port consumed when this session was bound. + pub fn create_disposable_context( + &mut self, + ) -> Result { + self.session + .create_disposable_context_with_port(&mut self.port) + } + + /// Return current presentation authority for an already-owned active context. + pub fn presentation_authority( + &self, + browsing_context: BrowsingContextId, + ) -> Result { + self.session.presentation_authority(browsing_context) + } + + /// Advance one active owned context to a new authority epoch. + pub fn advance_context_epoch( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.session.advance_context_epoch(browsing_context) + } + + /// Destroy the exact owned disposable boundary through the bound lifecycle port. + pub fn destroy_disposable_context( + &mut self, + authority: &PresentationMutationAuthority, + ) -> Result<(), BrowserSessionError> { + self.session + .destroy_disposable_context_with_port(authority, &mut self.port) + } + + /// Record browser transport loss without exposing mutable lifecycle-port access. + pub fn record_transport_loss(&mut self) -> bool { + self.session.record_transport_loss() + } + + /// End the Browser Session only after every owned context has proven destruction. + pub fn end(&mut self) -> Result<(), BrowserSessionError> { + self.session.end() + } +} + fn reserve_epoch(next_epoch: &mut u64) -> Result { let epoch = BrowserContextEpoch(*next_epoch); *next_epoch = next_epoch @@ -729,37 +727,40 @@ fn allocate_incarnation( #[allow(clippy::expect_used)] mod tests { use super::*; + use std::collections::VecDeque; #[derive(Debug)] struct TestPort { - port_id: DisposableContextPortId, - next_handle: DisposableContextHandle, + handles: VecDeque, create_error: Option, fail_destroy: bool, create_calls: usize, destroy_calls: usize, + create_sessions: Vec, create_incarnations: Vec, + destroy_sessions: Vec, destroy_incarnations: Vec, destroyed_isolations: Vec, } impl TestPort { fn new(context: u64, isolation: &str) -> Self { - Self::with_port_id(context, isolation, 1) + Self::with_handles(vec![DisposableContextHandle::new( + isolation_id(isolation), + context_id(context), + )]) } - fn with_port_id(context: u64, isolation: &str, port_id: u64) -> Self { + fn with_handles(handles: Vec) -> Self { Self { - port_id: DisposableContextPortId::new(port_id).expect("valid port id"), - next_handle: DisposableContextHandle::new( - isolation_id(isolation), - context_id(context), - ), + handles: handles.into(), create_error: None, fail_destroy: false, create_calls: 0, destroy_calls: 0, + create_sessions: Vec::new(), create_incarnations: Vec::new(), + destroy_sessions: Vec::new(), destroy_incarnations: Vec::new(), destroyed_isolations: Vec::new(), } @@ -767,20 +768,19 @@ mod tests { } impl DisposableContextPort for TestPort { - fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - fn create_disposable_context( &mut self, request: &DisposableContextCreateRequest, ) -> Result { - assert_eq!(request.port_id(), self.port_id); self.create_calls += 1; + self.create_sessions.push(request.browser_session()); self.create_incarnations.push(request.incarnation()); match self.create_error.clone() { Some(error) => Err(error), - None => Ok(self.next_handle.clone()), + None => Ok(self + .handles + .pop_front() + .expect("test must provide one handle per successful creation")), } } @@ -788,8 +788,8 @@ mod tests { &mut self, request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - assert_eq!(request.port_id(), self.port_id); self.destroy_calls += 1; + self.destroy_sessions.push(request.browser_session()); self.destroy_incarnations.push(request.incarnation()); self.destroyed_isolations .push(request.context().isolation.clone()); @@ -840,73 +840,72 @@ mod tests { let handle = DisposableContextHandle::new(valid.clone(), context_id(10)); assert_eq!(handle.isolation(), &valid); assert_eq!(handle.browsing_context(), context_id(10)); - assert_eq!(DisposableContextPortId::new(0), None); - assert_eq!( - DisposableContextPortId::new(17).expect("valid port id").value(), - 17 - ); } #[test] - fn disposable_creation_is_the_only_raw_context_entry_to_authority() { - let mut session = session(1); - let mut port = TestPort::new(10, "isolation-10"); - assert_eq!(session.id(), session_id(1)); - assert_ne!(session.incarnation().value(), 0); - assert!(!session.transport_is_lost()); - assert!(session.recovery_evidence().is_empty()); + fn bound_creation_is_the_only_raw_context_entry_to_authority() { + let raw_session = session(1); + assert_eq!(raw_session.id(), session_id(1)); + assert_ne!(raw_session.incarnation().value(), 0); + assert!(!raw_session.transport_is_lost()); + assert!(raw_session.recovery_evidence().is_empty()); assert_eq!( - session.presentation_authority(context_id(10)), + raw_session.presentation_authority(context_id(10)), Err(BrowserSessionError::ContextNotOwned) ); - let authority = session - .create_disposable_context(&mut port) + let mut bound = raw_session.bind_lifecycle_port(TestPort::new(10, "isolation-10")); + let authority = bound + .create_disposable_context() .expect("owned disposable context"); - assert_eq!(port.create_incarnations, vec![session.incarnation()]); + assert_eq!( + bound.lifecycle_port().create_sessions, + vec![session_id(1)] + ); + assert_eq!( + bound.lifecycle_port().create_incarnations, + vec![bound.browser_session().incarnation()] + ); assert_eq!(authority.browser_session(), session_id(1)); - assert_eq!(authority.incarnation(), session.incarnation()); + assert_eq!(authority.incarnation(), bound.browser_session().incarnation()); assert_eq!(authority.isolation().as_str(), "isolation-10"); assert_eq!(authority.browsing_context(), context_id(10)); assert_eq!(authority.context_epoch().value(), 1); - assert_eq!( - session.presentation_authority(context_id(10)), - Ok(authority) - ); + assert_eq!(bound.presentation_authority(context_id(10)), Ok(authority)); } #[test] fn creation_failure_preserves_known_recovery_identity() { - let mut clean_session = session(2); let mut clean_port = TestPort::new(20, "isolation-20"); clean_port.create_error = Some(DisposableContextCreateError::CreateFailedClean); + let mut clean = session(2).bind_lifecycle_port(clean_port); assert_eq!( - clean_session.create_disposable_context(&mut clean_port), + clean.create_disposable_context(), Err(BrowserSessionError::ContextCreationFailed) ); - assert_eq!(clean_session.state(), BrowserSessionState::Active); - clean_session.end().expect("clean failure can end"); + assert_eq!(clean.browser_session().state(), BrowserSessionState::Active); + clean.end().expect("clean failure can end"); - let mut unknown_session = session(21); let mut unknown_port = TestPort::new(210, "isolation-210"); unknown_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(None)); + let mut unknown = session(21).bind_lifecycle_port(unknown_port); assert_eq!( - unknown_session.create_disposable_context(&mut unknown_port), + unknown.create_disposable_context(), Err(BrowserSessionError::ContextCreationUncertain) ); - assert!(unknown_session.recovery_evidence().is_empty()); + assert!(unknown.browser_session().recovery_evidence().is_empty()); let known = isolation_id("partial-user-context-211"); - let mut known_session = session(22); let mut known_port = TestPort::new(211, "unused"); known_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(Some( known.clone(), ))); + let mut known_session = session(22).bind_lifecycle_port(known_port); assert_eq!( - known_session.create_disposable_context(&mut known_port), + known_session.create_disposable_context(), Err(BrowserSessionError::ContextCreationUncertain) ); assert_eq!( - known_session.recovery_evidence(), + known_session.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::PartialCreationIsolation( known )] @@ -919,39 +918,43 @@ mod tests { #[test] fn duplicate_adapter_output_preserves_offending_handle() { - let mut duplicate_context_session = session(3); - let mut first_context_port = TestPort::new(30, "isolation-30-a"); - duplicate_context_session - .create_disposable_context(&mut first_context_port) - .expect("first owned context"); let duplicate_context_handle = DisposableContextHandle::new(isolation_id("isolation-30-b"), context_id(30)); - let mut duplicate_context_port = TestPort::new(30, "isolation-30-b"); + let context_port = TestPort::with_handles(vec![ + DisposableContextHandle::new(isolation_id("isolation-30-a"), context_id(30)), + duplicate_context_handle.clone(), + ]); + let mut duplicate_context = session(3).bind_lifecycle_port(context_port); + duplicate_context + .create_disposable_context() + .expect("first owned context"); assert_eq!( - duplicate_context_session.create_disposable_context(&mut duplicate_context_port), + duplicate_context.create_disposable_context(), Err(BrowserSessionError::DuplicateBrowsingContext) ); assert_eq!( - duplicate_context_session.recovery_evidence(), + duplicate_context.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( duplicate_context_handle )] ); - let mut duplicate_isolation_session = session(31); - let mut first_isolation_port = TestPort::new(310, "isolation-31"); - duplicate_isolation_session - .create_disposable_context(&mut first_isolation_port) - .expect("first owned isolation"); let duplicate_isolation_handle = DisposableContextHandle::new(isolation_id("isolation-31"), context_id(311)); - let mut duplicate_isolation_port = TestPort::new(311, "isolation-31"); + let isolation_port = TestPort::with_handles(vec![ + DisposableContextHandle::new(isolation_id("isolation-31"), context_id(310)), + duplicate_isolation_handle.clone(), + ]); + let mut duplicate_isolation = session(31).bind_lifecycle_port(isolation_port); + duplicate_isolation + .create_disposable_context() + .expect("first owned isolation"); assert_eq!( - duplicate_isolation_session.create_disposable_context(&mut duplicate_isolation_port), + duplicate_isolation.create_disposable_context(), Err(BrowserSessionError::DuplicateDisposableIsolation) ); assert_eq!( - duplicate_isolation_session.recovery_evidence(), + duplicate_isolation.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( duplicate_isolation_handle )] @@ -959,234 +962,237 @@ mod tests { } #[test] - fn lifecycle_port_binding_rejects_other_adapter_before_io() { - let mut session = session(32); - let mut first_port = TestPort::with_port_id(320, "isolation-320", 11); - let authority = session - .create_disposable_context(&mut first_port) - .expect("first lifecycle port is bound"); - - let mut other_port = TestPort::with_port_id(321, "isolation-321", 12); - assert_eq!( - session.create_disposable_context(&mut other_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!(other_port.create_calls, 0); - assert_eq!( - session.destroy_disposable_context(&authority, &mut other_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!(other_port.destroy_calls, 0); - - session - .destroy_disposable_context(&authority, &mut first_port) - .expect("bound lifecycle port remains authorized"); - assert_eq!(first_port.destroy_calls, 1); + fn bound_port_is_structural_and_not_swappable() { + let approved = TestPort::new(320, "isolation-320"); + let other = TestPort::new(321, "isolation-321"); + let mut bound = session(32).bind_lifecycle_port(approved); + let authority = bound + .create_disposable_context() + .expect("owned context uses consumed port"); + assert_eq!(other.create_calls, 0); + assert_eq!(other.destroy_calls, 0); + bound + .destroy_disposable_context(&authority) + .expect("same structurally bound port destroys context"); + assert_eq!(bound.lifecycle_port().create_calls, 1); + assert_eq!(bound.lifecycle_port().destroy_calls, 1); } #[test] fn epoch_exhaustion_prevents_creation_io() { - let mut exhausted_session = session(4); - exhausted_session.next_epoch = u64::MAX; - let mut unused_port = TestPort::new(40, "isolation-40"); + let mut bound = session(4).bind_lifecycle_port(TestPort::new(40, "isolation-40")); + bound.session.next_epoch = u64::MAX; assert_eq!( - exhausted_session.create_disposable_context(&mut unused_port), + bound.create_disposable_context(), Err(BrowserSessionError::EpochExhausted) ); - assert_eq!(unused_port.create_calls, 0); + assert_eq!(bound.lifecycle_port().create_calls, 0); } #[test] fn epoch_exhaustion_prevents_advance_mutation() { - let mut exhausted_session = session(41); - let mut port = TestPort::new(410, "isolation-410"); - let authority = exhausted_session - .create_disposable_context(&mut port) + let mut bound = session(41).bind_lifecycle_port(TestPort::new(410, "isolation-410")); + let authority = bound + .create_disposable_context() .expect("owned context"); - exhausted_session.next_epoch = u64::MAX; + bound.session.next_epoch = u64::MAX; assert_eq!( - exhausted_session.advance_context_epoch(context_id(410)), + bound.advance_context_epoch(context_id(410)), Err(BrowserSessionError::EpochExhausted) ); - assert_eq!( - exhausted_session.presentation_authority(context_id(410)), - Ok(authority) - ); + assert_eq!(bound.presentation_authority(context_id(410)), Ok(authority)); } #[test] fn epoch_advance_invalidates_old_and_unknown_authority() { - let mut session = session(5); - let mut port = TestPort::new(50, "isolation-50"); - let old = session - .create_disposable_context(&mut port) + let mut bound = session(5).bind_lifecycle_port(TestPort::new(50, "isolation-50")); + let old = bound + .create_disposable_context() .expect("owned context"); assert_eq!( - session.advance_context_epoch(context_id(51)), + bound.advance_context_epoch(context_id(51)), Err(BrowserSessionError::ContextNotOwned) ); - let new = session + let new = bound .advance_context_epoch(context_id(50)) .expect("advanced epoch"); assert_eq!(new.context_epoch().value(), 2); assert_eq!( - session.destroy_disposable_context(&old, &mut port), + bound.destroy_disposable_context(&old), Err(BrowserSessionError::AuthorityMismatch) ); - session - .destroy_disposable_context(&new, &mut port) + bound + .destroy_disposable_context(&new) .expect("destroy current epoch"); - assert_eq!(port.destroy_incarnations, vec![session.incarnation()]); assert_eq!( - session.presentation_authority(context_id(50)), + bound.lifecycle_port().destroy_incarnations, + vec![bound.browser_session().incarnation()] + ); + assert_eq!( + bound.presentation_authority(context_id(50)), Err(BrowserSessionError::ContextNotOwned) ); assert_eq!( - session.destroy_disposable_context(&new, &mut port), + bound.destroy_disposable_context(&new), Err(BrowserSessionError::ContextNotOwned) ); } #[test] fn cross_session_and_foreign_isolation_authority_fail_before_io() { - let mut owner = session(6); - let mut owner_port = TestPort::new(60, "isolation-60"); + let mut owner = session(6).bind_lifecycle_port(TestPort::new(60, "isolation-60")); let authority = owner - .create_disposable_context(&mut owner_port) + .create_disposable_context() .expect("owner context"); - let mut foreign = session(7); - let mut foreign_port = TestPort::new(60, "isolation-60"); + let mut foreign = session(7).bind_lifecycle_port(TestPort::new(60, "isolation-60")); foreign - .create_disposable_context(&mut foreign_port) + .create_disposable_context() .expect("foreign context"); assert_eq!( - foreign.destroy_disposable_context(&authority, &mut foreign_port), + foreign.destroy_disposable_context(&authority), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(foreign_port.destroy_calls, 0); + assert_eq!(foreign.lifecycle_port().destroy_calls, 0); let forged = PresentationMutationAuthority { - browser_session: owner.id(), - incarnation: owner.incarnation(), + browser_session: owner.browser_session().id(), + incarnation: owner.browser_session().incarnation(), isolation: isolation_id("foreign-isolation"), browsing_context: authority.browsing_context(), context_epoch: authority.context_epoch(), }; assert_eq!( - owner.destroy_disposable_context(&forged, &mut owner_port), + owner.destroy_disposable_context(&forged), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(owner_port.destroy_calls, 0); + assert_eq!(owner.lifecycle_port().destroy_calls, 0); } #[test] fn sequential_incarnation_reuse_rejects_stale_authority() { let shared_id = session_id(8); - let mut session_a = BrowserSession::start(shared_id).expect("A incarnation"); - let mut port_a = TestPort::new(80, "reused-user-context"); + let mut session_a = BrowserSession::start(shared_id) + .expect("A incarnation") + .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); let authority_a = session_a - .create_disposable_context(&mut port_a) + .create_disposable_context() .expect("A context"); session_a - .destroy_disposable_context(&authority_a, &mut port_a) + .destroy_disposable_context(&authority_a) .expect("A destroy"); session_a.end().expect("A end"); - let mut session_b = BrowserSession::start(shared_id).expect("B incarnation"); - let mut port_b = TestPort::new(80, "reused-user-context"); + let mut session_b = BrowserSession::start(shared_id) + .expect("B incarnation") + .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); let authority_b = session_b - .create_disposable_context(&mut port_b) + .create_disposable_context() .expect("B context"); - assert_ne!(session_a.incarnation(), session_b.incarnation()); + assert_ne!( + session_a.browser_session().incarnation(), + session_b.browser_session().incarnation() + ); assert_eq!( - session_b.destroy_disposable_context(&authority_a, &mut port_b), + session_b.destroy_disposable_context(&authority_a), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(port_b.destroy_calls, 0); + assert_eq!(session_b.lifecycle_port().destroy_calls, 0); session_b - .destroy_disposable_context(&authority_b, &mut port_b) + .destroy_disposable_context(&authority_b) .expect("B destroy"); - assert_eq!(port_b.destroy_calls, 1); + assert_eq!(session_b.lifecycle_port().destroy_calls, 1); } #[test] fn destroy_failure_retains_handle_and_transport_loss_orthogonally() { - let mut session = session(9); - let mut port = TestPort::new(90, "isolation-90"); - let authority = session - .create_disposable_context(&mut port) - .expect("owned context"); let expected_handle = DisposableContextHandle::new(isolation_id("isolation-90"), context_id(90)); + let mut port = TestPort::new(90, "isolation-90"); port.fail_destroy = true; + let mut bound = session(9).bind_lifecycle_port(port); + let authority = bound + .create_disposable_context() + .expect("owned context"); assert_eq!( - session.destroy_disposable_context(&authority, &mut port), + bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) ); - assert_eq!(session.state(), BrowserSessionState::RecoveryRequired); assert_eq!( - session.recovery_evidence(), + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert_eq!( + bound.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::UnprovenDestruction( expected_handle )] ); - assert!(!session.transport_is_lost()); - assert!(session.record_transport_loss()); - assert!(session.transport_is_lost()); - assert_eq!(session.state(), BrowserSessionState::RecoveryRequired); - assert!(!session.record_transport_loss()); + assert!(!bound.browser_session().transport_is_lost()); + assert!(bound.record_transport_loss()); + assert!(bound.browser_session().transport_is_lost()); assert_eq!( - session.create_disposable_context(&mut port), + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert!(!bound.record_transport_loss()); + assert_eq!( + bound.create_disposable_context(), Err(BrowserSessionError::SessionNotActive) ); assert_eq!( - session.presentation_authority(context_id(90)), + bound.presentation_authority(context_id(90)), Err(BrowserSessionError::SessionNotActive) ); assert_eq!( - session.advance_context_epoch(context_id(90)), + bound.advance_context_epoch(context_id(90)), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(session.end(), Err(BrowserSessionError::SessionNotActive)); + assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); } #[test] fn transport_loss_invalidates_active_contexts_and_is_idempotent() { - let mut session = session(10); - let mut port = TestPort::new(100, "isolation-100"); - let authority = session - .create_disposable_context(&mut port) + let mut bound = session(10).bind_lifecycle_port(TestPort::new(100, "isolation-100")); + let authority = bound + .create_disposable_context() .expect("owned context"); - assert!(session.record_transport_loss()); - assert_eq!(session.state(), BrowserSessionState::TransportLost); - assert!(session.transport_is_lost()); - assert!(!session.record_transport_loss()); + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert!(bound.browser_session().transport_is_lost()); + assert!(!bound.record_transport_loss()); assert_eq!( - session.destroy_disposable_context(&authority, &mut port), + bound.destroy_disposable_context(&authority), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(port.destroy_calls, 0); + assert_eq!(bound.lifecycle_port().destroy_calls, 0); } #[test] fn normal_end_requires_proven_destruction_and_ignores_late_transport_report() { - let mut session = session(11); - let mut port = TestPort::new(110, "isolation-110"); - let authority = session - .create_disposable_context(&mut port) + let mut bound = session(11).bind_lifecycle_port(TestPort::new(110, "isolation-110")); + let authority = bound + .create_disposable_context() .expect("owned context"); + assert_eq!(bound.end(), Err(BrowserSessionError::ActiveContextRemains)); + bound + .destroy_disposable_context(&authority) + .expect("proven destruction"); assert_eq!( - session.end(), - Err(BrowserSessionError::ActiveContextRemains) + bound.lifecycle_port().destroy_sessions, + vec![session_id(11)] ); - session - .destroy_disposable_context(&authority, &mut port) - .expect("proven destruction"); - session.end().expect("normal end"); - assert_eq!(session.state(), BrowserSessionState::Ended); - assert!(!session.record_transport_loss()); - assert_eq!(session.end(), Err(BrowserSessionError::SessionNotActive)); + assert_eq!( + bound.lifecycle_port().destroyed_isolations, + vec![isolation_id("isolation-110")] + ); + bound.end().expect("normal end"); + assert_eq!(bound.browser_session().state(), BrowserSessionState::Ended); + assert!(!bound.record_transport_loss()); + assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); } #[test] diff --git a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs index 5833504a5..a93e692f9 100644 --- a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs +++ b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs @@ -2,13 +2,12 @@ use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, - DisposableContextPortId, DisposableIsolationId, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct FailingDestroyPort { - port_id: DisposableContextPortId, next_handle: DisposableContextHandle, create_calls: usize, destroy_calls: usize, @@ -20,10 +19,7 @@ impl FailingDestroyPort { .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; - let port_id = DisposableContextPortId::new(context) - .ok_or("static fixture lifecycle port id must be non-zero")?; Ok(Self { - port_id, next_handle: DisposableContextHandle::new(isolation, browsing_context), create_calls: 0, destroy_calls: 0, @@ -32,24 +28,18 @@ impl FailingDestroyPort { } impl DisposableContextPort for FailingDestroyPort { - fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - fn create_disposable_context( &mut self, - request: &DisposableContextCreateRequest, + _request: &DisposableContextCreateRequest, ) -> Result { - assert_eq!(request.port_id(), self.port_id); self.create_calls += 1; Ok(self.next_handle.clone()) } fn destroy_disposable_context( &mut self, - request: &DisposableContextDestroyRequest, + _request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - assert_eq!(request.port_id(), self.port_id); self.destroy_calls += 1; Err(DisposableContextDestroyError::DestroyFailed) } @@ -65,46 +55,51 @@ fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &' let expected_isolation = DisposableIsolationId::parse("user-context-501") .map_err(|_| "static fixture recovery isolation id must be valid")?; let expected_handle = DisposableContextHandle::new(expected_isolation, context_id); - let mut session = BrowserSession::start(session_id) + let session = BrowserSession::start(session_id) .map_err(|_| "browser session incarnation must be available")?; - let mut failing_port = FailingDestroyPort::new(5010, "user-context-501")?; + let failing_port = FailingDestroyPort::new(5010, "user-context-501")?; + let mut bound = session.bind_lifecycle_port(failing_port); - let authority = session - .create_disposable_context(&mut failing_port) + let authority = bound + .create_disposable_context() .map_err(|_| "fixture disposable context creation must succeed")?; assert_eq!( - session.destroy_disposable_context(&authority, &mut failing_port), + bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) ); - assert_eq!(failing_port.destroy_calls, 1); - assert_eq!(session.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(bound.lifecycle_port().destroy_calls, 1); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); assert_eq!( - session.recovery_evidence(), + bound.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::UnprovenDestruction( expected_handle )] ); - assert!(!session.transport_is_lost()); + assert!(!bound.browser_session().transport_is_lost()); - assert!(session.record_transport_loss()); - assert!(session.transport_is_lost()); - assert_eq!(session.state(), BrowserSessionState::RecoveryRequired); - assert!(!session.record_transport_loss()); - - let mut later_port = FailingDestroyPort::new(5011, "user-context-501-later")?; + assert!(bound.record_transport_loss()); + assert!(bound.browser_session().transport_is_lost()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert!(!bound.record_transport_loss()); assert_eq!( - session.create_disposable_context(&mut later_port), + bound.create_disposable_context(), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(later_port.create_calls, 0); + assert_eq!(bound.lifecycle_port().create_calls, 1); assert_eq!( - session.presentation_authority(context_id), + bound.presentation_authority(context_id), Err(BrowserSessionError::SessionNotActive) ); assert_eq!( - session.advance_context_epoch(context_id), + bound.advance_context_epoch(context_id), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(session.end(), Err(BrowserSessionError::SessionNotActive)); + assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); Ok(()) } diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs index b2b23f541..424e75ab2 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -1,21 +1,19 @@ use originweave_browser_session::{ - BrowserSession, BrowserSessionError, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, + BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, + DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, + DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct RecordingPort { - port_id: DisposableContextPortId, create_calls: usize, destroy_calls: usize, } impl RecordingPort { - fn new(port_id: u64) -> Self { + fn new() -> Self { Self { - port_id: DisposableContextPortId::new(port_id).expect("valid port id"), create_calls: 0, destroy_calls: 0, } @@ -23,15 +21,10 @@ impl RecordingPort { } impl DisposableContextPort for RecordingPort { - fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - fn create_disposable_context( &mut self, request: &DisposableContextCreateRequest, ) -> Result { - assert_eq!(request.port_id(), self.port_id); assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); self.create_calls += 1; Ok(DisposableContextHandle::new( @@ -44,7 +37,6 @@ impl DisposableContextPort for RecordingPort { &mut self, request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - assert_eq!(request.port_id(), self.port_id); assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); assert_eq!( request.context().browsing_context(), @@ -56,32 +48,21 @@ impl DisposableContextPort for RecordingPort { } #[test] -fn aggregate_issued_request_binds_lifecycle_io_to_one_port() { - let mut session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) +fn aggregate_issued_request_is_reachable_only_through_owned_port_binding() { + let session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) .expect("incarnation capacity"); - let mut bound_port = RecordingPort::new(101); - let authority = session - .create_disposable_context(&mut bound_port) - .expect("Browser Session-issued create request"); - assert_eq!(bound_port.create_calls, 1); + let unbound_other_port = RecordingPort::new(); + let mut bound = session.bind_lifecycle_port(RecordingPort::new()); - let mut other_port = RecordingPort::new(102); - assert_eq!( - session.create_disposable_context(&mut other_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!(other_port.create_calls, 0, "wrong port reached create I/O"); - assert_eq!( - session.destroy_disposable_context(&authority, &mut other_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!( - other_port.destroy_calls, 0, - "wrong port reached destroy I/O" - ); + let authority = bound + .create_disposable_context() + .expect("Browser Session-issued create request"); + assert_eq!(bound.lifecycle_port().create_calls, 1); + assert_eq!(unbound_other_port.create_calls, 0); - session - .destroy_disposable_context(&authority, &mut bound_port) + bound + .destroy_disposable_context(&authority) .expect("Browser Session-issued destroy request"); - assert_eq!(bound_port.destroy_calls, 1); + assert_eq!(bound.lifecycle_port().destroy_calls, 1); + assert_eq!(unbound_other_port.destroy_calls, 0); } diff --git a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs index 9a5baafa1..63532a37f 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use originweave_browser_session::{ BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, - DisposableContextPort, DisposableContextPortId, DisposableIsolationId, + DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -20,15 +20,14 @@ impl SideEffectingIdentityPort { create_calls: 0, } } -} -impl DisposableContextPort for SideEffectingIdentityPort { - fn port_id(&self) -> DisposableContextPortId { + fn identity_probe(&self) { self.identity_callbacks .set(self.identity_callbacks.get().saturating_add(1)); - DisposableContextPortId::new(401).expect("valid port id") } +} +impl DisposableContextPort for SideEffectingIdentityPort { fn create_disposable_context( &mut self, _request: &DisposableContextCreateRequest, @@ -49,19 +48,24 @@ impl DisposableContextPort for SideEffectingIdentityPort { } #[test] -fn lifecycle_authority_does_not_depend_on_side_effecting_identity_preflight() { - let mut session = BrowserSession::start(BrowserSessionId::new(401).expect("valid session id")) +fn lifecycle_binding_invokes_no_adapter_callback_before_authorized_create() { + let session = BrowserSession::start(BrowserSessionId::new(401).expect("valid session id")) .expect("incarnation capacity"); - let mut port = SideEffectingIdentityPort::new(); - - session - .create_disposable_context(&mut port) - .expect("authorized create"); + let port = SideEffectingIdentityPort::new(); + let mut bound = session.bind_lifecycle_port(port); assert_eq!( - port.identity_callbacks.get(), + bound.lifecycle_port().identity_callbacks.get(), 0, - "Browser Session invoked an arbitrary adapter callback before lifecycle authority was established" + "binding invoked adapter code before aggregate-issued lifecycle authority existed" ); - assert_eq!(port.create_calls, 1); + bound + .create_disposable_context() + .expect("authorized create"); + assert_eq!(bound.lifecycle_port().identity_callbacks.get(), 0); + assert_eq!(bound.lifecycle_port().create_calls, 1); + + // Prove the fixture would detect an identity callback if production code invoked one. + bound.lifecycle_port().identity_probe(); + assert_eq!(bound.lifecycle_port().identity_callbacks.get(), 1); } diff --git a/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs index bf437dc20..ae16efd9b 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs @@ -1,13 +1,12 @@ use originweave_browser_session::{ - BrowserSession, BrowserSessionError, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, + BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, + DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, + DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct RecordingPort { - port_id: DisposableContextPortId, context: BrowsingContextId, isolation: &'static str, create_calls: usize, @@ -15,9 +14,8 @@ struct RecordingPort { } impl RecordingPort { - fn new(port_id: u64, context: u64, isolation: &'static str) -> Self { + fn new(context: u64, isolation: &'static str) -> Self { Self { - port_id: DisposableContextPortId::new(port_id).expect("valid port id"), context: BrowsingContextId::new(context).expect("valid browsing context"), isolation, create_calls: 0, @@ -27,15 +25,10 @@ impl RecordingPort { } impl DisposableContextPort for RecordingPort { - fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - fn create_disposable_context( &mut self, - request: &DisposableContextCreateRequest, + _request: &DisposableContextCreateRequest, ) -> Result { - assert_eq!(request.port_id(), self.port_id); self.create_calls += 1; Ok(DisposableContextHandle::new( DisposableIsolationId::parse(self.isolation).expect("valid isolation id"), @@ -45,50 +38,42 @@ impl DisposableContextPort for RecordingPort { fn destroy_disposable_context( &mut self, - request: &DisposableContextDestroyRequest, + _request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - assert_eq!(request.port_id(), self.port_id); self.destroy_calls += 1; Ok(()) } } #[test] -fn distinct_port_with_same_claimed_id_cannot_create() { - let mut session = BrowserSession::start(BrowserSessionId::new(17).expect("valid session id")) +fn distinct_adapter_cannot_be_substituted_for_create_after_binding() { + let session = BrowserSession::start(BrowserSessionId::new(17).expect("valid session id")) .expect("incarnation capacity"); - let mut approved_port = RecordingPort::new(101, 41, "approved-isolation"); - session - .create_disposable_context(&mut approved_port) - .expect("bind approved port"); + let approved_port = RecordingPort::new(41, "approved-isolation"); + let spoofing_port = RecordingPort::new(42, "spoofed-isolation"); + let mut bound = session.bind_lifecycle_port(approved_port); - let mut spoofing_port = RecordingPort::new(101, 42, "spoofed-isolation"); - assert_eq!( - session.create_disposable_context(&mut spoofing_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!( - spoofing_port.create_calls, 0, - "distinct adapter with the same self-reported id reached create I/O" - ); + bound + .create_disposable_context() + .expect("bound adapter creates context"); + assert_eq!(bound.lifecycle_port().create_calls, 1); + assert_eq!(spoofing_port.create_calls, 0); } #[test] -fn distinct_port_with_same_claimed_id_cannot_destroy() { - let mut session = BrowserSession::start(BrowserSessionId::new(18).expect("valid session id")) +fn distinct_adapter_cannot_be_substituted_for_destroy_after_binding() { + let session = BrowserSession::start(BrowserSessionId::new(18).expect("valid session id")) .expect("incarnation capacity"); - let mut approved_port = RecordingPort::new(101, 51, "approved-isolation"); - let authority = session - .create_disposable_context(&mut approved_port) - .expect("bind approved port"); + let approved_port = RecordingPort::new(51, "approved-isolation"); + let spoofing_port = RecordingPort::new(52, "spoofed-isolation"); + let mut bound = session.bind_lifecycle_port(approved_port); + let authority = bound + .create_disposable_context() + .expect("bound adapter creates context"); - let mut spoofing_port = RecordingPort::new(101, 52, "spoofed-isolation"); - assert_eq!( - session.destroy_disposable_context(&authority, &mut spoofing_port), - Err(BrowserSessionError::LifecyclePortMismatch) - ); - assert_eq!( - spoofing_port.destroy_calls, 0, - "distinct adapter with the same self-reported id reached destroy I/O" - ); + bound + .destroy_disposable_context(&authority) + .expect("bound adapter destroys context"); + assert_eq!(bound.lifecycle_port().destroy_calls, 1); + assert_eq!(spoofing_port.destroy_calls, 0); } diff --git a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs index a3e49f1b6..9232da729 100644 --- a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs +++ b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs @@ -1,13 +1,12 @@ use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionIncarnation, DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableContextPortId, DisposableIsolationId, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct ReusingPort { - port_id: DisposableContextPortId, handle: DisposableContextHandle, create_incarnations: Vec, destroy_incarnations: Vec, @@ -19,10 +18,7 @@ impl ReusingPort { .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; - let port_id = DisposableContextPortId::new(context) - .ok_or("static fixture lifecycle port id must be non-zero")?; Ok(Self { - port_id, handle: DisposableContextHandle::new(isolation, browsing_context), create_incarnations: Vec::new(), destroy_incarnations: Vec::new(), @@ -31,15 +27,10 @@ impl ReusingPort { } impl DisposableContextPort for ReusingPort { - fn port_id(&self) -> DisposableContextPortId { - self.port_id - } - fn create_disposable_context( &mut self, request: &DisposableContextCreateRequest, ) -> Result { - assert_eq!(request.port_id(), self.port_id); self.create_incarnations.push(request.incarnation()); Ok(self.handle.clone()) } @@ -48,7 +39,6 @@ impl DisposableContextPort for ReusingPort { &mut self, request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - assert_eq!(request.port_id(), self.port_id); self.destroy_incarnations.push(request.incarnation()); Ok(()) } @@ -60,38 +50,56 @@ fn stale_authority_cannot_cross_sequential_session_incarnations() -> Result<(), let session_id = BrowserSessionId::new(701) .map_err(|_| "static fixture browser session id must be valid")?; - let mut port_a = ReusingPort::new(7010, "user-context-reused")?; - let mut session_a = BrowserSession::start(session_id) + let session_a = BrowserSession::start(session_id) .map_err(|_| "first browser session incarnation must be available")?; - let authority_a = session_a - .create_disposable_context(&mut port_a) + let mut bound_a = session_a.bind_lifecycle_port(ReusingPort::new( + 7010, + "user-context-reused", + )?); + let authority_a = bound_a + .create_disposable_context() .map_err(|_| "first disposable context creation must succeed")?; - session_a - .destroy_disposable_context(&authority_a, &mut port_a) + bound_a + .destroy_disposable_context(&authority_a) .map_err(|_| "first disposable context destruction must succeed")?; - session_a + bound_a .end() .map_err(|_| "first browser session must end normally")?; - let mut port_b = ReusingPort::new(7010, "user-context-reused")?; - let mut session_b = BrowserSession::start(session_id) + let session_b = BrowserSession::start(session_id) .map_err(|_| "second browser session incarnation must be available")?; - let authority_b = session_b - .create_disposable_context(&mut port_b) + let mut bound_b = session_b.bind_lifecycle_port(ReusingPort::new( + 7010, + "user-context-reused", + )?); + let authority_b = bound_b + .create_disposable_context() .map_err(|_| "second disposable context creation must succeed")?; - assert_ne!(session_a.incarnation(), session_b.incarnation()); - assert_eq!(port_a.create_incarnations, vec![session_a.incarnation()]); - assert_eq!(port_b.create_incarnations, vec![session_b.incarnation()]); + assert_ne!( + bound_a.browser_session().incarnation(), + bound_b.browser_session().incarnation() + ); + assert_eq!( + bound_a.lifecycle_port().create_incarnations, + vec![bound_a.browser_session().incarnation()] + ); assert_eq!( - session_b.destroy_disposable_context(&authority_a, &mut port_b), + bound_b.lifecycle_port().create_incarnations, + vec![bound_b.browser_session().incarnation()] + ); + assert_eq!( + bound_b.destroy_disposable_context(&authority_a), Err(BrowserSessionError::AuthorityMismatch) ); - assert!(port_b.destroy_incarnations.is_empty()); + assert!(bound_b.lifecycle_port().destroy_incarnations.is_empty()); - session_b - .destroy_disposable_context(&authority_b, &mut port_b) + bound_b + .destroy_disposable_context(&authority_b) .map_err(|_| "current incarnation authority must remain valid")?; - assert_eq!(port_b.destroy_incarnations, vec![session_b.incarnation()]); + assert_eq!( + bound_b.lifecycle_port().destroy_incarnations, + vec![bound_b.browser_session().incarnation()] + ); Ok(()) } diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 345071fd6..b30fb85e3 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -7,119 +7,110 @@ OriginWeave's WebDriver BiDi presentation adapter requires opaque ownership witnesses before viewport/device-pixel-ratio, timezone, or screen-area mutation can be planned. A caller that merely knows a browser-session or browsing-context identifier therefore cannot overwrite another owner's presentation state and later clear it to an implementation default. -The Browser Session boundary must establish why a context is exclusively OriginWeave-owned before presentation authority exists. External browser-session, user-context/isolation, and browsing-context identifiers are protocol addressability. They may be reused after a prior lifecycle ends, so `(BrowserSessionId, DisposableIsolationId, BrowsingContextId, local epoch)` is not by itself a durable capability generation. +Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are protocol or implementation addressability. They are not Browser Session authority. A previous repair introduced opaque `DisposableContextCreateRequest` and `DisposableContextDestroyRequest`, but also asked each adapter to self-report a public numeric port id. That left two defects: a second adapter could select the same id, and Browser Session had to invoke arbitrary adapter code to read that id before lifecycle authority existed. Rust `&self` does not make such a callback pure. Lifecycle failures also need lossless evidence. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. These outcomes require recovery quarantine while retaining every exact browser-issued identity that is already known. -Transport liveness is independent from ownership certainty. A session already in `RecoveryRequired` can subsequently lose its transport; that new fact must be recorded without erasing the recovery evidence. Conversely, merely entering recovery does not prove the transport is dead. +Transport liveness is independent from ownership certainty. A session already in `RecoveryRequired` can subsequently lose its transport; that new fact must be recorded without erasing recovery evidence. Conversely, merely entering recovery does not prove the transport is dead. The 9 September 2026 WebDriver BiDi Working Draft defines user-context identifiers and the `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext` lifecycle. Those commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. ## Decision drivers -- Raw WebDriver/BiDi identifiers are addressability, not mutation or cleanup authority. +- Raw WebDriver/BiDi identifiers and adapter-chosen ids are addressability, not mutation or cleanup authority. +- No arbitrary adapter callback may be required to establish lifecycle-port ownership. +- A caller must not be able to substitute a second adapter instance after Browser Session lifecycle binding. +- Create/destroy requests must remain non-caller-constructible and usable only through the bound aggregate composition. - Sequential aggregate recreation must not make a retained stale authority valid again. -- The lifecycle adapter must receive the same non-reused session incarnation used by authority validation; an aggregate-only nonce is insufficient. - Known remote identities from partial creation, duplicate output, or unproven destruction must be retained as recovery evidence without becoming command authority. -- Ownership recovery and transport liveness must remain orthogonal. -- Duplicate or uncertain outcomes fail closed and must not permit false normal completion. -- Destruction I/O must use the exact stored handle and session incarnation rather than reconstructing authority from raw identifiers. +- Ownership recovery and transport liveness remain orthogonal. - Browser Session remains the domain authority; WebDriver BiDi, CDP, MCP, and LLMs remain adapters or consumers. ## Decision -Introduce `originweave-browser-session` as an independent Rust bounded context and retain ADR status `Proposed` until protected-main and real-browser acceptance exist. - -1. `BrowserSession` is the aggregate root. `BrowserSession::start` allocates a process-local, monotonically non-reused `BrowserSessionIncarnation` before browser I/O. Allocation fails closed before `u64` wrap. -2. Presentation authority is intentionally non-serializable. A process restart destroys every outstanding in-memory authority. Within one process, `BrowserSessionIncarnation` prevents sequential ABA when a later aggregate reuses the same external session, isolation, context, and local epoch values. -3. The same `BrowserSessionIncarnation` is passed through `DisposableContextPort` create and destroy calls. Adapters must scope their remote ownership mapping to that incarnation. Ignoring it violates the port contract. -4. A context enters the owned set only after `DisposableContextPort::create_disposable_context` returns a `DisposableContextHandle`. Raw `BrowsingContextId` input never creates ownership. -5. `PresentationMutationAuthority` is opaque and binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. -6. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `DisposableContextCreateError::CreateFailedUncertain(Option)` enters `RecoveryRequired`; when the browser-issued isolation/user-context identity is known, it is preserved exactly. -7. Duplicate browsing-context or isolation output enters `RecoveryRequired` and stores the complete offending `DisposableContextHandle` as recovery evidence. OriginWeave does not auto-destroy it because the adapter may have returned foreign state. -8. `BrowserSessionRecoveryEvidence` records only reconciliation evidence: `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnprovenDestruction`. It grants no browser command authority. -9. Destruction validates exact authority before I/O, passes the current incarnation and stored handle to the port, and succeeds only after the adapter proves the exact boundary is gone. `DisposableContextDestroyError` moves the record and aggregate into recovery and retains the exact failed handle. -10. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; later duplicate reports are idempotent. If transport is lost while the aggregate is `Active`, the lifecycle state becomes `TransportLost` and active contexts become uncertain. If ownership was already uncertain, `RecoveryRequired` remains the lifecycle state and the transport-loss fact is retained alongside it. -11. `RecoveryRequired`, `TransportLost`, and `Ended` reject active-only creation, authority issuance/advance, destruction, and normal end. Reconciliation is a later, separately authorized design. -12. Context epochs remain monotonic authority identities within one aggregate. They invalidate older authority after navigation or another lifecycle boundary but are not a substitute for session incarnation. +Introduce and retain `originweave-browser-session` as an independent Rust bounded context. ADR status remains `Proposed` until protected-main and real-browser acceptance exist. + +1. `BrowserSession::start` allocates a process-local, monotonically non-reused `BrowserSessionIncarnation` before browser I/O. Allocation fails closed before `u64` wrap. +2. Presentation authority is intentionally non-serializable. Within one process, `BrowserSessionIncarnation` prevents sequential ABA when a later aggregate reuses the same external session, isolation, context, and local epoch values. +3. Browser Session uses a **linear lifecycle-port binding**. `BrowserSession::bind_lifecycle_port` consumes both the aggregate and one concrete adapter value into `BoundBrowserSession

`. Binding performs no adapter callback. +4. `BoundBrowserSession

` does not expose mutable port access and its public create/destroy methods accept no alternate port argument. The exact adapter instance is therefore structural composition rather than a caller-selected or self-asserted scalar identity. +5. `DisposableContextPort` has no `port_id()` preflight method. `DisposableContextPortId` is removed. A second adapter cannot claim equality by choosing the same scalar. +6. `DisposableContextCreateRequest` and `DisposableContextDestroyRequest` remain opaque, have no public constructor, and are created only inside the bound Browser Session path after aggregate state or exact presentation authority has been validated. They carry Browser Session addressability and incarnation; the destroy request additionally carries the exact stored handle. +7. The adapter is part of the reviewed lifecycle anti-corruption boundary. A malicious adapter implementation that internally delegates an authorized request is outside what a Rust trait can prevent without inverting the dependency boundary; protocol-specific pending/accepted/quarantine ownership remains the responsibility of the separately reviewed BiDi ACL adapter in ADR 0115. +8. A context enters the owned set only after the bound port returns a `DisposableContextHandle`. Raw `BrowsingContextId` input never creates ownership. +9. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before destruction I/O. +10. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; a known browser-issued isolation identity is preserved exactly. +11. Duplicate browsing-context or isolation output enters `RecoveryRequired` and stores the complete offending `DisposableContextHandle` as recovery evidence. OriginWeave does not auto-destroy ambiguous output. +12. `BrowserSessionRecoveryEvidence` records only reconciliation evidence: `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnprovenDestruction`. It grants no browser command authority. +13. Destruction validates exact authority before I/O and passes the current incarnation and stored handle in `DisposableContextDestroyRequest`. `DisposableContextDestroyError` moves the record and aggregate into recovery and retains the exact failed handle. +14. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; repeated reports are idempotent. +15. `RecoveryRequired`, `TransportLost`, and `Ended` reject active-only creation, authority issuance/advance, destruction, and normal end. Reconciliation is a later, separately authorized design. +16. Context epochs remain monotonic authority identities within one aggregate. They invalidate older authority after navigation or another lifecycle boundary but are not a substitute for session incarnation. ## Alternatives considered -### Treat any known context as owned +### Adapter-supplied numeric port id -Rejected. It restores the authority-confusion defect and allows one task to clear another task's state. +Rejected. A public scalar is caller-selectable and replayable by a distinct adapter. Making the callback side-effect-free by documentation is also insufficient because Rust `&self` permits interior mutation and delegated effects. -### Depend only on browser-issued isolation identity +### Pointer-address identity -Rejected. The WebDriver BiDi user-context identifier is suitable lifecycle addressability, but this ADR does not assume a historical non-reuse guarantee after removal. A later aggregate therefore needs a separate OriginWeave lifecycle generation. +Rejected. Object addresses are implementation details, can change when values move, and can be reused after destruction. Pointer equality would replace one ABA surface with another. -### Add an aggregate-only random or monotonic nonce +### Session-owned wrapper with the concrete port -Rejected if it does not reach the lifecycle adapter. It would stop one aggregate from accepting another aggregate's token while still allowing a valid current token to address a remote boundary through aliasable adapter keys. The selected `BrowserSessionIncarnation` participates in both authority validation and port calls. +Selected. Ownership is represented by Rust move semantics and private fields. No identity probe is required, the caller cannot swap a second adapter into public lifecycle methods, and opaque requests remain confined to the bound call path. ### Persist authority generations globally -Deferred and unnecessary for the current in-process authority model. Presentation authority is not durable across process restart; recovery across restart belongs to evidence/reconciliation design, not silent authority resurrection. - -### Treat every uncertain lifecycle failure as transport loss - -Rejected. Ownership uncertainty and transport liveness answer different operational questions. Collapsing them loses information needed for safe reconciliation. +Deferred. Presentation authority is not durable across process restart; restart reconciliation belongs to evidence and browser observation, not silent authority resurrection. ### Automatically clean duplicate or partial state -Rejected. When ownership is ambiguous, cleanup itself can become a cross-owner destructive action. Exact recovery evidence is retained while normal authority stays blocked. - -### Snapshot and restore every predecessor presentation override - -Deferred. OriginWeave does not yet have a complete queryable predecessor-state contract for every governed presentation surface. Disposable ownership remains the stronger first implementation. +Rejected. When ownership is ambiguous, cleanup itself can become a cross-owner destructive action. ## Consequences -The Browser Session aggregate now carries an explicit lifecycle generation through the anti-corruption boundary instead of treating protocol identifiers as durable capabilities. A retained token from aggregate A cannot validate against aggregate B solely because the browser or adapter later reused the same external identifiers and local epoch. +Browser Session no longer asks an adapter to prove its own identity before authority. The aggregate and exact lifecycle port become one composed runtime object, while adapter-specific remote identifiers remain outside the Browser Session domain model. -Recovery is also diagnosable rather than merely terminal. Known partial user-context identities, duplicate returned handles, and exact handles whose destruction could not be proven remain available as `BrowserSessionRecoveryEvidence`. This evidence is purpose-bound to later reconciliation; it is not a cleanup credential. +The API change is intentionally breaking on the active stack: consumers must call `BrowserSession::bind_lifecycle_port(port)` and then perform lifecycle operations through `BoundBrowserSession`. ADR 0115/#316 must be non-force restacked and adapt its WebDriver BiDi lifecycle adapter to this composition before adoption. -Transport failure can now be observed after ownership has already become uncertain without replacing or erasing that uncertainty. This supports later recovery planning that distinguishes “ownership uncertain but transport still live” from “ownership uncertain and transport lost.” - -The selected process-local incarnation has a deliberate scope. It prevents ABA only for outstanding in-memory authority within the running process. Durable restart reconciliation must use separately persisted evidence and browser observation; this ADR does not serialize or resurrect authority across restart. +This binding closes ordinary caller substitution and self-selected-id replay. It does not claim that an adversarial implementation of the trusted `DisposableContextPort` trait cannot internally forward calls; such an implementation already executes inside the reviewed adapter TCB. The BiDi ACL still must prove pending → accepted/quarantined remote ownership, complete recovery tuples, and live-target validation independently. ## Security and governance impact -No page-controlled value, raw browser-session id, raw browsing-context id, user-context string, provider/model decision, or LLM output can mint presentation authority. The adapter receives domain-issued incarnation information only as a lifecycle-scoping input and cannot manufacture Browser Session policy authority. +No page-controlled value, raw browser-session id, raw browsing-context id, user-context string, adapter-selected scalar, provider/model decision, or LLM output can mint lifecycle requests or presentation authority. Browser Session performs no arbitrary adapter callback while establishing the lifecycle-port binding. -Unknown or duplicate remote state is quarantined rather than destroyed speculatively. This reduces the risk that recovery logic removes another owner's user context. It does not replace Chromium sandboxing, egress policy, Keyverse secret handling, Wardnet controls, or central workflow security. +Unknown or duplicate remote state is quarantined rather than destroyed speculatively. This does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. ## Tests and exact evidence -The test suite covers raw-context rejection, bounded isolation identity parsing, typed clean/uncertain creation, retained partial identity, duplicate-handle evidence, epoch exhaustion, stale epoch rejection, foreign-session/isolation rejection, destruction failure, transport loss, normal end, and incarnation-allocation exhaustion. - -A dedicated hostile test, `stale_authority_cannot_cross_sequential_session_incarnations`, creates aggregate A, destroys and ends it, creates aggregate B with the same external session/user-context/browsing-context values and local epoch, and requires A's retained authority to fail before B adapter I/O while B's current authority succeeds. The port records incarnation values so the test also proves that the lifecycle mapping receives the new generation. +The suite retains recovery, sequential ABA, epoch, foreign-authority, destruction, transport-loss, and normal-end coverage. `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` proves that binding performs no adapter callback before the aggregate-issued create request. `distinct_adapter_cannot_be_substituted_for_create_after_binding` and `distinct_adapter_cannot_be_substituted_for_destroy_after_binding`, together with repository source contracts, require lifecycle methods to use only the consumed port and prohibit reintroduction of public `DisposableContextPortId`/`port_id()` or arbitrary-port Browser Session lifecycle methods. -`destroy_failure_requires_recovery_before_any_new_authority` requires an unproven destruction to retain the exact failed handle, enter `RecoveryRequired`, then record a later real transport loss without erasing ownership evidence; repeated loss reports are idempotent. +The prior hostile RED was captured on exact `d43a4d86c8487ebdb9db9f1c4650fb7ee6225afc` in CI `34524654914`: the pre-authority callback fixture observed one identity callback where zero was required. This decision replaces that self-asserted identity design rather than suppressing the test. -The RED for the sequential ABA defect was captured on exact `ec145963ad8fe19c9416f2b3856b94660082dbf7` in CI `34469580144`: repository contracts and formatting passed, and Rust `Run tests` failed at the new hostile test before Clippy/rustdoc. The production fix and subsequent documentation/test updates must earn a new exact-head GREEN; predecessor evidence does not transfer. - -Repository contracts, canonical formatting, locked Rust tests, strict Clippy, rustdoc/API docs, exact function/line/region/branch coverage, independent review, and applicable central checks remain required before ordinary adoption into #313. +Repository contracts, canonical formatting, locked Rust tests, strict Clippy, rustdoc/API docs, exact function/line/region/branch coverage, current review findings, and applicable central checks remain required on the successor exact head. Predecessor GREEN never transfers. ## Buyer acceptance still open -This slice does not yet prove real WebDriver BiDi `browser.createUserContext`/`browsingContext.create`/`browser.removeUserContext` integration, browser-observed destruction, recovery reconciliation, Browser Session→BiDi private-witness conversion, pinned Chromium presentation post-conditions, crash/restart cleanup, #299 3/3 Agent Task replay, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not yet prove real WebDriver BiDi `browser.createUserContext`/`browsingContext.create`/`browser.removeUserContext` integration, pending/accepted/quarantined remote binding, browser-observed destruction, Browser Session→BiDi private-witness conversion, pinned Chromium presentation post-conditions, crash/restart cleanup, #299 3/3 Agent Task replay, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Migration and rollback -The change remains additive on the active stacked branch. Consumers must adopt the new `BrowserSession::start` result and incarnation-aware `DisposableContextPort` contract. Until a reviewed adapter bridge exists, presentation mutation remains fail closed behind private ownership witnesses. Rollback removes this active-PR bounded-context slice without weakening protected Chromium or central security policy. +Consumers on the active stack replace `session.create_disposable_context(&mut port)` / `session.destroy_disposable_context(..., &mut port)` with one `let mut bound = session.bind_lifecycle_port(port)` followed by bound lifecycle calls. The wrapper exposes read-only access to the aggregate and adapter for policy validation and diagnostics but does not return mutable adapter access or an unbound session. + +Rollback returns to the predecessor active-PR API only if the lifecycle-port authority finding is rejected with stronger evidence; it must not restore self-reported scalar identity as a security boundary. ## Open follow-ups -- Implement the WebDriver BiDi disposable-user-context adapter with incarnation-scoped mapping and observed destruction post-condition. -- Define the Browser Session→BiDi ACL without exposing public ownership constructors. -- Design separately authorized reconciliation for `BrowserSessionRecoveryEvidence`, including browser/process restart. +- Restack #316 onto the verified Browser Session successor and adapt the WebDriver BiDi lifecycle ACL to `BoundBrowserSession` without exposing a second lifecycle side door. +- Implement protocol-specific pending → accepted/quarantined creation and complete recovery tuples in the BiDi ACL owner. +- Define separately authorized reconciliation for `BrowserSessionRecoveryEvidence`, including browser/process restart. - Replay #299 historical pinned Chromium evidence after the canonical sandbox/runtime repair, then run a separate current-Stable qualification. -- Revisit predecessor capture/restore only if reusable attached contexts become a buyer requirement. ## Supersession / reversal conditions -Supersede this ADR if the browser platform provides a complete, queryable, generation-safe ownership primitive with exact destruction evidence, or if OriginWeave adopts another isolation primitive with equivalent guarantees. Do not regress to raw context identity as authority. +Supersede this ADR if the browser platform provides a complete, queryable, generation-safe ownership primitive with exact destruction evidence, or if OriginWeave adopts another isolation primitive with equivalent guarantees. Do not regress to raw context identity or adapter-selected identity as authority. ## References diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 66336ac88..1f77f4afa 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -4,48 +4,52 @@ - Owning bounded context: `originweave-browser-session` - Governing proposal: ADR 0114 - Requirement owner: issue #312 -- Integration prerequisites: #229 presentation-ownership witnesses; canonical browser/sandbox owner path under #212/#148 +- Integration prerequisites: #229 presentation-ownership witnesses; #314/#316 WebDriver BiDi ACL after this foundation is exact-head GREEN ## Problem and invariant -Browser-session, user-context/isolation, and browsing-context identifiers are addresses. They are not evidence that the current Browser Session aggregate exclusively owns presentation mutation or cleanup. A retained authority must not regain meaning if a later aggregate reuses the same remote identifiers and local epoch. +Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are addresses. They are not evidence that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. A retained authority must not regain meaning if a later aggregate reuses the same remote identifiers and local epoch, and a caller must not be able to redirect a valid lifecycle request into a second adapter instance by choosing or replaying an adapter id. -The active implementation now establishes this chain: +The active implementation establishes this chain: ```text validated BrowserSessionId → BrowserSession::start allocates non-reused BrowserSessionIncarnation -→ DisposableContextPort receives session id + incarnation -→ adapter creates fresh task-owned isolation boundary + browsing context -→ adapter returns DisposableIsolationId + BrowsingContextId -→ aggregate records exact handle + monotonic context epoch +→ BrowserSession::bind_lifecycle_port consumes one concrete DisposableContextPort +→ BoundBrowserSession

owns aggregate + exact port; binding invokes no adapter callback +→ aggregate validates Active + reserves monotonic context epoch +→ aggregate privately constructs DisposableContextCreateRequest(session, incarnation) +→ exact owned port creates task-owned isolation boundary + browsing context +→ aggregate records exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) -→ exact authority validation before adapter I/O -→ destruction receives the same incarnation + stored handle -→ adapter proves exact disposable boundary destruction +→ exact authority validation before destroy I/O +→ aggregate privately constructs DisposableContextDestroyRequest(session, incarnation, stored handle) +→ exact owned port proves destruction → context Destroyed -→ normal BrowserSession::end admitted +→ normal BrowserSession end admitted ``` -`BrowserSessionIncarnation` is process-local and monotonic. Presentation authority is not persisted across process restart, so restart invalidates outstanding authority rather than requiring a durable counter. Within one running process, the incarnation is checked by the aggregate and passed through the lifecycle port; an adapter that ignores it does not satisfy the ACL contract. +`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, no mutable port accessor is exposed, and `DisposableContextPort` has no identity-preflight callback. The previous public `DisposableContextPortId`/`port_id()` design was removed because the value was self-asserted and the callback itself could have side effects before authority. + +`DisposableContextCreateRequest` and `DisposableContextDestroyRequest` have private construction paths. Their getters expose only addressability needed by a reviewed adapter. A caller that knows those values cannot reconstruct the request. ## Lossless recovery evidence `DisposableContextCreateError::CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known user-context/isolation identity as `BrowserSessionRecoveryEvidence::PartialCreationIsolation`; `None` remains representable when no identity was obtained. Both uncertain cases enter `RecoveryRequired` and mint no authority. -Duplicate browsing-context or isolation output stores the complete offending `DisposableContextHandle` as `DuplicateAdapterHandle` before recovery quarantine. OriginWeave deliberately does not auto-destroy duplicate output because ownership may be foreign. Failed or unproven destruction records `UnprovenDestruction` with the exact owned handle. Recovery evidence authorizes no browser command; it exists only for a later reviewed reconciliation path. +Duplicate browsing-context or isolation output stores the complete offending `DisposableContextHandle` as `DuplicateAdapterHandle` before recovery quarantine. Failed or unproven destruction records `UnprovenDestruction` with the exact owned handle. Recovery evidence authorizes no browser command. + +Protocol-specific complete BiDi tuples, pending → accepted/quarantined mapping, and remote target liveness remain #314/#316 responsibilities; they are not copied into Browser Session domain truth. ## Orthogonal transport liveness Transport liveness is tracked independently from ownership recovery. If transport loss occurs after `RecoveryRequired`, the aggregate keeps `RecoveryRequired`, preserves all recovery evidence, and separately records `transport_lost = true`. The first loss report is observable; repeated reports are idempotent. If loss occurs while `Active`, the lifecycle state becomes `TransportLost` and active context records become uncertain. -This avoids conflating “ownership uncertain while transport may still be usable for separately authorized reconciliation” with “ownership uncertain and the transport is gone.” - ## Sequential ABA safety -The sequential ABA hostile case is explicit: aggregate A creates `(S,U,C,epoch=1)`, proves destruction, and ends. Aggregate B later starts with the same external `S`; the adapter may return the same `U/C`, and B also begins at local epoch 1. A's retained authority must still fail before any B adapter I/O. B receives a different `BrowserSessionIncarnation`, and only B's newly minted authority is accepted. +Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external `S`; the browser may return the same `U/C`, and B also begins at local epoch 1. A's retained authority still fails before B adapter I/O because B has a different `BrowserSessionIncarnation`. -The port also receives the incarnation on create/destroy. This closes the prior gap where an aggregate-only nonce could protect token comparison while the browser adapter still keyed destruction by aliasable raw identifiers. +The bound port receives the incarnation inside aggregate-issued create/destroy requests. The caller cannot replace the bound adapter after creation to reinterpret that current incarnation against a different adapter-local map. ## Standards trace @@ -53,42 +57,32 @@ The design dossier references the 9 September 2026 WebDriver BiDi Working Draft. OriginWeave does not turn that protocol identifier into policy authority or assume historical non-reuse after removal. `DisposableIsolationId` remains lifecycle addressability. A successful command ACK is insufficient evidence that the disposable boundary is actually gone. -The active `originweave-bidi` adapter remains separately runtime-qualified against its documented 3 September 2026 revision. Tracking the 9 September publication here does not silently repin that runtime contract. - ## Source and executable evidence | Invariant | Source / test | |---|---| | independent Browser Session bounded context | `crates/originweave-browser-session/`; `tests/test_browser_session_lifecycle_contract.py` | -| raw context cannot mint authority | `BrowserSession::presentation_authority`; `disposable_creation_is_the_only_raw_context_entry_to_authority` | +| lifecycle port ownership is structural | `BoundBrowserSession`; `bound_port_is_structural_and_not_swappable` | +| binding performs no arbitrary adapter callback | `BrowserSession::bind_lifecycle_port`; `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` | +| no self-asserted adapter id authority | absence of `DisposableContextPortId` / `port_id()`; repository contract | +| create/destroy requests are aggregate-issued | `DisposableContextCreateRequest`; `DisposableContextDestroyRequest`; `aggregate_issued_request_is_reachable_only_through_owned_port_binding` | +| raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | | authority includes non-reused BrowserSessionIncarnation | `PresentationMutationAuthority`; `sequential_incarnation_reuse_rejects_stale_authority` | -| lifecycle port receives the same incarnation | `DisposableContextPort`; `stale_authority_cannot_cross_sequential_session_incarnations` | | lossless recovery evidence for known partial identity | `BrowserSessionRecoveryEvidence`; `creation_failure_preserves_known_recovery_identity` | -| duplicate adapter handle retained without speculative cleanup | `BrowserSession::create_disposable_context`; `duplicate_adapter_output_preserves_offending_handle` | -| unproven destruction retains exact handle | `BrowserSession::destroy_disposable_context`; `destroy_failure_requires_recovery_before_any_new_authority` | +| duplicate adapter handle retained without speculative cleanup | `create_disposable_context_with_port`; `duplicate_adapter_output_preserves_offending_handle` | +| unproven destruction retains exact handle | `destroy_disposable_context_with_port`; `destroy_failure_requires_recovery_before_any_new_authority` | | transport liveness remains orthogonal to recovery | `BrowserSession::record_transport_loss`; `destroy_failure_retains_handle_and_transport_loss_orthogonally` | | sequential ABA authority is rejected before I/O | `BrowserSession::context_for_authority_mut`; `stale_authority_cannot_cross_sequential_session_incarnations` | | normal end requires proved destruction | `BrowserSession::end`; `normal_end_requires_proven_destruction_and_ignores_late_transport_report` | | incarnation exhaustion fails closed | `allocate_incarnation`; `incarnation_allocator_fails_closed_before_wrap` | -Earlier exact-head evidence remains historical only. Exact `ab04f9522e97e1ecd6d914c48cb6f77f087eac3b` was repository GREEN in CI `34463908909` after repairing repository-contract drift, but it still contained the three Browser Session defects above. - -The sequential ABA RED was then captured on exact `ec145963ad8fe19c9416f2b3856b94660082dbf7` in CI `34469580144`: Python repository contracts and canonical formatting passed; the Rust `Run tests` step failed at the newly added hostile sequential-incarnation test. That RED is the causal predecessor for the incarnation-aware domain/port repair. No earlier GREEN transfers to the repaired successor. +The pre-authority adapter-callback RED was captured on exact `d43a4d86c8487ebdb9db9f1c4650fb7ee6225afc` in CI `34524654914`: the hostile fixture observed one identity callback where zero was required. The same predecessor also retained the self-selected scalar identity defect. The bound-session successor must earn fresh exact-head formatting, tests, Clippy, rustdoc, and function/line/region/branch 100% evidence; historical GREEN does not transfer. Protected-main integration is required before capability maturity can be promoted beyond `IMPLEMENTED_ON_ACTIVE_PR`. ## Buyer acceptance still open -This slice does not yet prove: - -- actual WebDriver BiDi `browser.createUserContext`/`browsingContext.create` integration and incarnation-scoped mapping; -- observed `browser.removeUserContext` post-condition for the exact owned boundary; -- a separately authorized reconciliation service consuming `BrowserSessionRecoveryEvidence`; -- Browser Session authority conversion into BiDi presentation/screen-area private witnesses; -- pinned Chromium post-condition observation after presentation mutation; -- crash/process-restart reconciliation of uncertain disposable contexts; -- 3/3 complete #299 Agent Task browser trials; -- protected-main release, SBOM, provenance, reproducibility, or rollback evidence. +This slice does not yet prove actual WebDriver BiDi `browser.createUserContext`/`browsingContext.create` integration, observed `browser.removeUserContext` post-condition, protocol-specific pending/accepted/quarantined binding, separately authorized recovery reconciliation, Browser Session authority conversion into BiDi presentation private witnesses, pinned Chromium post-condition observation, crash/process-restart reconciliation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Reference diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 5b171cfbf..54bd12ead 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -7,41 +7,50 @@ sequenceDiagram autonumber participant C as Application service participant S as BrowserSession aggregate + participant BS as BoundBrowserSession participant P as DisposableContextPort participant B as Browser adapter (planned) C->>S: start(valid BrowserSessionId) S->>S: allocate BrowserSessionIncarnation - C->>S: create_disposable_context(port) - S->>S: reserve monotonic context epoch - S->>P: create_disposable_context(session_id, incarnation) + C->>S: bind_lifecycle_port(port by value) + S-->>C: BoundBrowserSession owns aggregate + exact port + Note over S,P: binding invokes no adapter callback + + C->>BS: create_disposable_context() + BS->>S: require Active + reserve monotonic epoch + S->>S: mint DisposableContextCreateRequest + S->>P: create_disposable_context(request) P->>B: create fresh isolation boundary + browsing context B-->>P: unique isolation id + BrowsingContextId or typed create error P-->>S: DisposableContextHandle S->>S: register exact handle + Active epoch S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) - Note over C,S: Raw BrowserSessionId/BrowsingContextId/user-context id cannot mint authority. + Note over C,S: Raw ids and adapter-selected scalar identities cannot mint lifecycle or presentation authority. - C->>S: advance_context_epoch(context_id) - S->>S: replace epoch; old authority becomes stale + C->>BS: advance_context_epoch(context_id) + BS->>S: replace epoch; old authority becomes stale S-->>C: new opaque authority carrying same incarnation + isolation - C->>S: destroy_disposable_context(authority, port) - S->>S: validate exact session/incarnation/isolation/context/epoch before I/O - S->>P: destroy_disposable_context(session_id, incarnation, stored handle) + C->>BS: destroy_disposable_context(authority) + BS->>S: validate exact session/incarnation/isolation/context/epoch before I/O + S->>S: mint DisposableContextDestroyRequest with exact stored handle + S->>P: destroy_disposable_context(request) P->>B: remove exact owned isolation boundary B-->>P: observed destruction post-condition or DisposableContextDestroyError P-->>S: success S->>S: context = Destroyed - C->>S: end() - S->>S: require every owned context Destroyed + C->>BS: end() + BS->>S: require every owned context Destroyed S-->>C: Ended ``` -`BrowserSessionIncarnation` separates two sequential aggregate lifecycles even when the browser or adapter later reuses the same external session, user-context/isolation, browsing-context, and local epoch values. The incarnation is checked by authority validation and reaches the lifecycle port. It is therefore not merely an aggregate-local nonce that the adapter can ignore. +`BoundBrowserSession` is a linear lifecycle-port binding: it consumes one concrete port and exposes no public lifecycle method that accepts a replacement port. `DisposableContextPort` has no identity callback, so Browser Session does not execute arbitrary adapter code merely to establish adapter ownership. `DisposableContextCreateRequest` and `DisposableContextDestroyRequest` are non-caller-constructible capabilities created inside the bound path. -For a WebDriver BiDi adapter, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. That protocol id remains lifecycle addressability rather than OriginWeave policy authority. Creation and destruction expose distinct typed errors. +`BrowserSessionIncarnation` separates sequential aggregate lifecycles even when the browser later reuses the same external session, user-context/isolation, browsing-context, and local epoch values. The incarnation is checked by authority validation and reaches the lifecycle port inside the opaque request. + +For a WebDriver BiDi adapter, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. That protocol id remains lifecycle addressability rather than OriginWeave policy authority. Protocol-specific pending/accepted/quarantined remote tuples remain in the BiDi ACL boundary rather than this domain model. ## Recovery and transport state @@ -79,23 +88,24 @@ stateDiagram-v2 ```mermaid sequenceDiagram autonumber - participant A as BrowserSession A - participant B as BrowserSession B - participant P as Lifecycle port - - A->>A: start(S) => incarnation A - A->>P: create(S, incarnation A) - P-->>A: U, C - A->>P: destroy(S, incarnation A, U/C) + participant A as BoundBrowserSession A + participant B as BoundBrowserSession B + participant PA as Lifecycle port A + participant PB as Lifecycle port B + + A->>A: start(S) => incarnation A; bind PA + A->>PA: create(request S, incarnation A) + PA-->>A: U, C + A->>PA: destroy(request S, incarnation A, U/C) A->>A: end() - B->>B: start(S) => incarnation B - B->>P: create(S, incarnation B) - P-->>B: same U, same C + B->>B: start(S) => incarnation B; bind PB + B->>PB: create(request S, incarnation B) + PB-->>B: same U, same C Note over A,B: both local context epochs may equal 1 B->>B: validate retained authority A - B-->>A: AuthorityMismatch before adapter I/O - B->>P: destroy with authority B + incarnation B + B-->>A: AuthorityMismatch before PB destroy I/O + B->>PB: destroy with authority B + incarnation B ``` `RecoveryRequired` and `TransportLost` remain terminal for normal authority in this slice. A later reconciliation design may inspect `BrowserSessionRecoveryEvidence`, but it must not reconstruct cleanup authority from raw identifiers or treat command ACK as proof of destruction. diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 6e4487988..60c099ece 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -32,27 +32,34 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: source = (CRATE / "src/lib.rs").read_text(encoding="utf-8") self.assertIn("pub struct BrowserSession", source) + self.assertIn("pub struct BoundBrowserSession", source) self.assertIn("pub trait DisposableContextPort", source) self.assertIn("pub struct DisposableIsolationId", source) self.assertIn("pub struct DisposableContextHandle", source) self.assertIn("pub struct BrowserSessionIncarnation", source) self.assertIn("pub struct PresentationMutationAuthority", source) + self.assertIn("pub struct DisposableContextCreateRequest", source) + self.assertIn("pub struct DisposableContextDestroyRequest", source) self.assertIn("pub enum BrowserSessionRecoveryEvidence", source) self.assertIn("BrowserSessionState::RecoveryRequired", source) self.assertIn("pub enum DisposableContextCreateError", source) self.assertIn("pub enum DisposableContextDestroyError", source) self.assertNotIn("pub enum DisposableContextPortError", source) + self.assertNotIn("DisposableContextPortId", source) + self.assertNotIn("fn port_id(&self)", source) + self.assertNotIn("pub fn create_disposable_context", source) + self.assertIn("pub fn bind_lifecycle_port", source) self.assertIn("CreateFailedClean", source) self.assertIn("CreateFailedUncertain", source) self.assertIn("PartialCreationIsolation", source) self.assertIn("DuplicateAdapterHandle", source) self.assertIn("UnprovenDestruction", source) - self.assertIn("create_disposable_context", source) + self.assertIn("create_disposable_context_with_port", source) self.assertIn("advance_context_epoch", source) self.assertIn("record_transport_loss", source) self.assertIn("transport_is_lost", source) self.assertIn("recovery_evidence", source) - self.assertIn("user-context identifier", source) + self.assertIn("user-context", source) self.assertIn("Reconstructing cleanup authority", source) self.assertIn("sequential_incarnation_reuse_rejects_stale_authority", source) @@ -62,8 +69,19 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertNotIn("pub fn new", authority_impl) self.assertNotIn("pub const fn new", authority_impl) + create_request_impl = source.split("impl DisposableContextCreateRequest", 1)[1].split( + "pub struct DisposableContextDestroyRequest", 1 + )[0] + destroy_request_impl = source.split("impl DisposableContextDestroyRequest", 1)[1].split( + "pub trait DisposableContextPort", 1 + )[0] + self.assertNotIn("pub fn new", create_request_impl) + self.assertNotIn("pub const fn new", create_request_impl) + self.assertNotIn("pub fn new", destroy_request_impl) + self.assertNotIn("pub const fn new", destroy_request_impl) + def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> None: - """Recovery and sequential reuse invariants must be executable outside crate internals.""" + """Recovery, binding, and sequential reuse invariants must execute outside crate internals.""" destroy_hostile = ( CRATE / "tests/destroy_failure_requires_recovery.rs" @@ -71,19 +89,44 @@ def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> No reincarnation_hostile = ( CRATE / "tests/sequential_incarnation_reuse.rs" ).read_text(encoding="utf-8") + preflight_hostile = ( + CRATE / "tests/lifecycle_port_preflight_side_effect.rs" + ).read_text(encoding="utf-8") + substitution_hostile = ( + CRATE / "tests/lifecycle_port_same_id_spoof.rs" + ).read_text(encoding="utf-8") self.assertIn( "destroy_failure_requires_recovery_before_any_new_authority", destroy_hostile, ) self.assertIn("BrowserSessionRecoveryEvidence::UnprovenDestruction", destroy_hostile) - self.assertIn("assert!(session.record_transport_loss());", destroy_hostile) - self.assertIn("assert!(!session.record_transport_loss());", destroy_hostile) + self.assertIn("assert!(bound.record_transport_loss());", destroy_hostile) + self.assertIn("assert!(!bound.record_transport_loss());", destroy_hostile) self.assertIn( "stale_authority_cannot_cross_sequential_session_incarnations", reincarnation_hostile, ) - self.assertIn("assert_ne!(session_a.incarnation(), session_b.incarnation());", reincarnation_hostile) - self.assertIn("assert!(port_b.destroy_incarnations.is_empty());", reincarnation_hostile) + self.assertIn( + "assert_ne!(\n bound_a.browser_session().incarnation(),", + reincarnation_hostile, + ) + self.assertIn( + "assert!(bound_b.lifecycle_port().destroy_incarnations.is_empty());", + reincarnation_hostile, + ) + self.assertIn( + "lifecycle_binding_invokes_no_adapter_callback_before_authorized_create", + preflight_hostile, + ) + self.assertIn("identity_callbacks", preflight_hostile) + self.assertIn( + "distinct_adapter_cannot_be_substituted_for_create_after_binding", + substitution_hostile, + ) + self.assertIn( + "distinct_adapter_cannot_be_substituted_for_destroy_after_binding", + substitution_hostile, + ) def test_architecture_decision_and_traceability_are_explicit(self) -> None: """Disposable ownership must remain a Proposed, standards-traced active-PR claim.""" @@ -102,6 +145,10 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("RecoveryRequired", adr) self.assertIn("BrowserSessionIncarnation", adr) self.assertIn("BrowserSessionRecoveryEvidence", adr) + self.assertIn("DisposableContextCreateRequest", adr) + self.assertIn("DisposableContextDestroyRequest", adr) + self.assertIn("BoundBrowserSession", adr) + self.assertIn("linear lifecycle-port binding", adr) self.assertIn("DisposableContextCreateError", adr) self.assertIn("DisposableContextDestroyError", adr) self.assertIn("CreateFailedClean", adr) @@ -110,6 +157,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("sequential", adr) self.assertIn("unproven destruction", adr) self.assertIn("IMPLEMENTED_ON_ACTIVE_PR", trace) + self.assertIn("BoundBrowserSession", trace) self.assertIn("RecoveryRequired", trace) self.assertIn("BrowserSessionIncarnation", trace) self.assertIn("lossless recovery evidence", trace) @@ -117,6 +165,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("sequential ABA", trace) self.assertIn("command ACK", trace) self.assertIn("PresentationMutationAuthority", uml) + self.assertIn("BoundBrowserSession", uml) self.assertIn("BrowserSessionIncarnation", uml) self.assertIn("RecoveryRequired", uml) self.assertIn("transport_lost", uml) From db09d3b3c476ecdd06d2a57e422aff2b815f930a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:11:27 +0900 Subject: [PATCH 011/632] test(browser-session): add create transaction RED --- .../tests/creation_transaction_completion.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 crates/originweave-browser-session/tests/creation_transaction_completion.rs diff --git a/crates/originweave-browser-session/tests/creation_transaction_completion.rs b/crates/originweave-browser-session/tests/creation_transaction_completion.rs new file mode 100644 index 000000000..2c6f17592 --- /dev/null +++ b/crates/originweave-browser-session/tests/creation_transaction_completion.rs @@ -0,0 +1,120 @@ +use std::cell::RefCell; +use std::collections::{BTreeMap, VecDeque}; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionIncarnation, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateDisposition, DisposableContextCreateError, DisposableContextCreateRequest, + DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, + DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug, Default)] +struct CreationLedger { + session: Option, + incarnation: Option, + pending: BTreeMap, + accepted: Vec, + rejected: Vec, +} + +#[derive(Debug)] +struct TransactionalPort { + handles: VecDeque, + ledger: Rc>, +} + +impl TransactionalPort { + fn new(handles: Vec, ledger: Rc>) -> Self { + Self { + handles: handles.into(), + ledger, + } + } +} + +impl DisposableContextPort for TransactionalPort { + fn create_disposable_context( + &mut self, + request: &DisposableContextCreateRequest, + ) -> Result { + let handle = self + .handles + .pop_front() + .expect("fixture supplies one handle per create"); + let mut ledger = self.ledger.borrow_mut(); + ledger.session.get_or_insert(request.browser_session()); + ledger.incarnation.get_or_insert(request.incarnation()); + let prior = ledger + .pending + .insert(request.attempt_epoch().value(), handle.clone()); + assert!(prior.is_none(), "create attempts must not collide"); + Ok(handle) + } + + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + let mut ledger = self.ledger.borrow_mut(); + if ledger.session != Some(completion.browser_session()) + || ledger.incarnation != Some(completion.incarnation()) + { + return Err(DisposableContextCreateCompletionError::CompletionFailed); + } + let attempt = completion.attempt_epoch().value(); + if ledger.pending.remove(&attempt).is_none() { + return Err(DisposableContextCreateCompletionError::CompletionFailed); + } + match completion.disposition() { + DisposableContextCreateDisposition::Accepted => ledger.accepted.push(attempt), + DisposableContextCreateDisposition::Rejected => ledger.rejected.push(attempt), + } + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +#[test] +fn accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt() { + let context = BrowsingContextId::new(8010).expect("valid browsing context"); + let first = DisposableContextHandle::new( + DisposableIsolationId::parse("transaction-user-context-a").expect("valid isolation"), + context, + ); + let duplicate_context = DisposableContextHandle::new( + DisposableIsolationId::parse("transaction-user-context-b").expect("valid isolation"), + context, + ); + let ledger = Rc::new(RefCell::new(CreationLedger::default())); + let port = TransactionalPort::new(vec![first, duplicate_context], Rc::clone(&ledger)); + let session = BrowserSession::start(BrowserSessionId::new(801).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port); + + let accepted = bound + .create_disposable_context() + .expect("first candidate accepted"); + assert_eq!(accepted.context_epoch().value(), 1); + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::DuplicateBrowsingContext) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + + let ledger = ledger.borrow(); + assert!(ledger.pending.is_empty()); + assert_eq!(ledger.accepted, vec![1]); + assert_eq!(ledger.rejected, vec![2]); +} From a2e5b277e55d121f75f367f50f572b81486cc485 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:13:18 +0900 Subject: [PATCH 012/632] test(browser-session): enforce transaction and port encapsulation RED --- ...test_browser_session_lifecycle_contract.py | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 60c099ece..0dd8afb50 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -39,6 +39,9 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertIn("pub struct BrowserSessionIncarnation", source) self.assertIn("pub struct PresentationMutationAuthority", source) self.assertIn("pub struct DisposableContextCreateRequest", source) + self.assertIn("pub struct DisposableContextCreateCompletion", source) + self.assertIn("pub enum DisposableContextCreateDisposition", source) + self.assertIn("pub enum DisposableContextCreateCompletionError", source) self.assertIn("pub struct DisposableContextDestroyRequest", source) self.assertIn("pub enum BrowserSessionRecoveryEvidence", source) self.assertIn("BrowserSessionState::RecoveryRequired", source) @@ -47,12 +50,19 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertNotIn("pub enum DisposableContextPortError", source) self.assertNotIn("DisposableContextPortId", source) self.assertNotIn("fn port_id(&self)", source) + self.assertNotIn("pub const fn lifecycle_port", source) + self.assertNotIn("pub fn lifecycle_port", source) self.assertNotIn("pub fn create_disposable_context", source) self.assertIn("pub fn bind_lifecycle_port", source) + self.assertIn("attempt_epoch: BrowserContextEpoch", source) + self.assertIn("fn complete_disposable_context_creation(", source) + self.assertIn("DisposableContextCreateDisposition::Accepted", source) + self.assertIn("DisposableContextCreateDisposition::Rejected", source) self.assertIn("CreateFailedClean", source) self.assertIn("CreateFailedUncertain", source) self.assertIn("PartialCreationIsolation", source) self.assertIn("DuplicateAdapterHandle", source) + self.assertIn("UnsettledAdapterHandle", source) self.assertIn("UnprovenDestruction", source) self.assertIn("create_disposable_context_with_port", source) self.assertIn("advance_context_epoch", source) @@ -70,18 +80,23 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertNotIn("pub const fn new", authority_impl) create_request_impl = source.split("impl DisposableContextCreateRequest", 1)[1].split( - "pub struct DisposableContextDestroyRequest", 1 + "pub enum DisposableContextCreateDisposition", 1 + )[0] + completion_impl = source.split("impl DisposableContextCreateCompletion", 1)[1].split( + "pub enum DisposableContextCreateCompletionError", 1 )[0] destroy_request_impl = source.split("impl DisposableContextDestroyRequest", 1)[1].split( "pub trait DisposableContextPort", 1 )[0] self.assertNotIn("pub fn new", create_request_impl) self.assertNotIn("pub const fn new", create_request_impl) + self.assertNotIn("pub fn new", completion_impl) + self.assertNotIn("pub const fn new", completion_impl) self.assertNotIn("pub fn new", destroy_request_impl) self.assertNotIn("pub const fn new", destroy_request_impl) def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> None: - """Recovery, binding, and sequential reuse invariants must execute outside crate internals.""" + """Recovery, binding, transaction, and sequential reuse invariants execute externally.""" destroy_hostile = ( CRATE / "tests/destroy_failure_requires_recovery.rs" @@ -95,6 +110,10 @@ def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> No substitution_hostile = ( CRATE / "tests/lifecycle_port_same_id_spoof.rs" ).read_text(encoding="utf-8") + transaction_hostile = ( + CRATE / "tests/creation_transaction_completion.rs" + ).read_text(encoding="utf-8") + self.assertIn( "destroy_failure_requires_recovery_before_any_new_authority", destroy_hostile, @@ -102,6 +121,8 @@ def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> No self.assertIn("BrowserSessionRecoveryEvidence::UnprovenDestruction", destroy_hostile) self.assertIn("assert!(bound.record_transport_loss());", destroy_hostile) self.assertIn("assert!(!bound.record_transport_loss());", destroy_hostile) + self.assertNotIn("lifecycle_port()", destroy_hostile) + self.assertIn( "stale_authority_cannot_cross_sequential_session_incarnations", reincarnation_hostile, @@ -110,15 +131,16 @@ def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> No "assert_ne!(\n bound_a.browser_session().incarnation(),", reincarnation_hostile, ) - self.assertIn( - "assert!(bound_b.lifecycle_port().destroy_incarnations.is_empty());", - reincarnation_hostile, - ) + self.assertIn("assert!(destroy_b.borrow().is_empty());", reincarnation_hostile) + self.assertNotIn("lifecycle_port()", reincarnation_hostile) + self.assertIn( "lifecycle_binding_invokes_no_adapter_callback_before_authorized_create", preflight_hostile, ) self.assertIn("identity_callbacks", preflight_hostile) + self.assertNotIn("bound.lifecycle_port()", preflight_hostile) + self.assertIn( "distinct_adapter_cannot_be_substituted_for_create_after_binding", substitution_hostile, @@ -127,6 +149,16 @@ def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> No "distinct_adapter_cannot_be_substituted_for_destroy_after_binding", substitution_hostile, ) + self.assertNotIn("bound.lifecycle_port()", substitution_hostile) + + self.assertIn( + "accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt", + transaction_hostile, + ) + self.assertIn("request.attempt_epoch().value()", transaction_hostile) + self.assertIn("DisposableContextCreateDisposition::Accepted", transaction_hostile) + self.assertIn("DisposableContextCreateDisposition::Rejected", transaction_hostile) + self.assertIn("assert!(ledger.pending.is_empty());", transaction_hostile) def test_architecture_decision_and_traceability_are_explicit(self) -> None: """Disposable ownership must remain a Proposed, standards-traced active-PR claim.""" @@ -146,9 +178,12 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("BrowserSessionIncarnation", adr) self.assertIn("BrowserSessionRecoveryEvidence", adr) self.assertIn("DisposableContextCreateRequest", adr) + self.assertIn("DisposableContextCreateCompletion", adr) self.assertIn("DisposableContextDestroyRequest", adr) self.assertIn("BoundBrowserSession", adr) self.assertIn("linear lifecycle-port binding", adr) + self.assertIn("no public raw port accessor", adr) + self.assertIn("per-create transaction", adr) self.assertIn("DisposableContextCreateError", adr) self.assertIn("DisposableContextDestroyError", adr) self.assertIn("CreateFailedClean", adr) @@ -158,6 +193,9 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("unproven destruction", adr) self.assertIn("IMPLEMENTED_ON_ACTIVE_PR", trace) self.assertIn("BoundBrowserSession", trace) + self.assertIn("DisposableContextCreateCompletion", trace) + self.assertIn("per-create transaction", trace) + self.assertIn("no public raw port accessor", trace) self.assertIn("RecoveryRequired", trace) self.assertIn("BrowserSessionIncarnation", trace) self.assertIn("lossless recovery evidence", trace) @@ -166,6 +204,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn("command ACK", trace) self.assertIn("PresentationMutationAuthority", uml) self.assertIn("BoundBrowserSession", uml) + self.assertIn("DisposableContextCreateCompletion", uml) self.assertIn("BrowserSessionIncarnation", uml) self.assertIn("RecoveryRequired", uml) self.assertIn("transport_lost", uml) From 32f87d6f598685a8b37ab8a269eb48b233a5b92d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:15:02 +0900 Subject: [PATCH 013/632] test(browser-session): keep adapter observations outside bound capability --- .../destroy_failure_requires_recovery.rs | 43 +++++++--- .../tests/lifecycle_port_authority.rs | 53 ++++++++---- .../lifecycle_port_preflight_side_effect.rs | 52 +++++++----- .../tests/lifecycle_port_same_id_spoof.rs | 80 ++++++++++++++----- .../tests/sequential_incarnation_reuse.rs | 63 +++++++++++---- 5 files changed, 212 insertions(+), 79 deletions(-) diff --git a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs index a93e692f9..e88a1b964 100644 --- a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs +++ b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs @@ -1,5 +1,9 @@ +use std::cell::Cell; +use std::rc::Rc; + use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, DisposableIsolationId, @@ -9,20 +13,25 @@ use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct FailingDestroyPort { next_handle: DisposableContextHandle, - create_calls: usize, - destroy_calls: usize, + create_calls: Rc>, + destroy_calls: Rc>, } impl FailingDestroyPort { - fn new(context: u64, isolation: &str) -> Result { + fn new( + context: u64, + isolation: &str, + create_calls: Rc>, + destroy_calls: Rc>, + ) -> Result { let isolation = DisposableIsolationId::parse(isolation) .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; Ok(Self { next_handle: DisposableContextHandle::new(isolation, browsing_context), - create_calls: 0, - destroy_calls: 0, + create_calls, + destroy_calls, }) } } @@ -32,15 +41,22 @@ impl DisposableContextPort for FailingDestroyPort { &mut self, _request: &DisposableContextCreateRequest, ) -> Result { - self.create_calls += 1; + self.create_calls.set(self.create_calls.get() + 1); Ok(self.next_handle.clone()) } + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + fn destroy_disposable_context( &mut self, _request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - self.destroy_calls += 1; + self.destroy_calls.set(self.destroy_calls.get() + 1); Err(DisposableContextDestroyError::DestroyFailed) } } @@ -57,7 +73,14 @@ fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &' let expected_handle = DisposableContextHandle::new(expected_isolation, context_id); let session = BrowserSession::start(session_id) .map_err(|_| "browser session incarnation must be available")?; - let failing_port = FailingDestroyPort::new(5010, "user-context-501")?; + let create_calls = Rc::new(Cell::new(0)); + let destroy_calls = Rc::new(Cell::new(0)); + let failing_port = FailingDestroyPort::new( + 5010, + "user-context-501", + Rc::clone(&create_calls), + Rc::clone(&destroy_calls), + )?; let mut bound = session.bind_lifecycle_port(failing_port); let authority = bound @@ -67,7 +90,7 @@ fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &' bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) ); - assert_eq!(bound.lifecycle_port().destroy_calls, 1); + assert_eq!(destroy_calls.get(), 1); assert_eq!( bound.browser_session().state(), BrowserSessionState::RecoveryRequired @@ -91,7 +114,7 @@ fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &' bound.create_disposable_context(), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(bound.lifecycle_port().create_calls, 1); + assert_eq!(create_calls.get(), 1); assert_eq!( bound.presentation_authority(context_id), Err(BrowserSessionError::SessionNotActive) diff --git a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs index 424e75ab2..ea5432fcc 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_authority.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_authority.rs @@ -1,21 +1,25 @@ +use std::cell::Cell; +use std::rc::Rc; + use originweave_browser_session::{ - BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, - DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, - DisposableContextPort, DisposableIsolationId, + BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct RecordingPort { - create_calls: usize, - destroy_calls: usize, + create_calls: Rc>, + destroy_calls: Rc>, } impl RecordingPort { - fn new() -> Self { + fn new(create_calls: Rc>, destroy_calls: Rc>) -> Self { Self { - create_calls: 0, - destroy_calls: 0, + create_calls, + destroy_calls, } } } @@ -26,13 +30,20 @@ impl DisposableContextPort for RecordingPort { request: &DisposableContextCreateRequest, ) -> Result { assert_eq!(request.browser_session(), BrowserSessionId::new(7).unwrap()); - self.create_calls += 1; + self.create_calls.set(self.create_calls.get() + 1); Ok(DisposableContextHandle::new( DisposableIsolationId::parse("aggregate-issued-request").expect("valid isolation id"), BrowsingContextId::new(41).expect("valid browsing context"), )) } + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + fn destroy_disposable_context( &mut self, request: &DisposableContextDestroyRequest, @@ -42,7 +53,7 @@ impl DisposableContextPort for RecordingPort { request.context().browsing_context(), BrowsingContextId::new(41).unwrap() ); - self.destroy_calls += 1; + self.destroy_calls.set(self.destroy_calls.get() + 1); Ok(()) } } @@ -51,18 +62,28 @@ impl DisposableContextPort for RecordingPort { fn aggregate_issued_request_is_reachable_only_through_owned_port_binding() { let session = BrowserSession::start(BrowserSessionId::new(7).expect("valid session id")) .expect("incarnation capacity"); - let unbound_other_port = RecordingPort::new(); - let mut bound = session.bind_lifecycle_port(RecordingPort::new()); + let approved_create_calls = Rc::new(Cell::new(0)); + let approved_destroy_calls = Rc::new(Cell::new(0)); + let other_create_calls = Rc::new(Cell::new(0)); + let other_destroy_calls = Rc::new(Cell::new(0)); + let _unbound_other_port = RecordingPort::new( + Rc::clone(&other_create_calls), + Rc::clone(&other_destroy_calls), + ); + let mut bound = session.bind_lifecycle_port(RecordingPort::new( + Rc::clone(&approved_create_calls), + Rc::clone(&approved_destroy_calls), + )); let authority = bound .create_disposable_context() .expect("Browser Session-issued create request"); - assert_eq!(bound.lifecycle_port().create_calls, 1); - assert_eq!(unbound_other_port.create_calls, 0); + assert_eq!(approved_create_calls.get(), 1); + assert_eq!(other_create_calls.get(), 0); bound .destroy_disposable_context(&authority) .expect("Browser Session-issued destroy request"); - assert_eq!(bound.lifecycle_port().destroy_calls, 1); - assert_eq!(unbound_other_port.destroy_calls, 0); + assert_eq!(approved_destroy_calls.get(), 1); + assert_eq!(other_destroy_calls.get(), 0); } diff --git a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs index 63532a37f..01c1d658b 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs @@ -1,23 +1,25 @@ use std::cell::Cell; +use std::rc::Rc; use originweave_browser_session::{ - BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, - DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, - DisposableContextPort, DisposableIsolationId, + BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct SideEffectingIdentityPort { - identity_callbacks: Cell, - create_calls: usize, + identity_callbacks: Rc>, + create_calls: Rc>, } impl SideEffectingIdentityPort { - fn new() -> Self { + fn new(identity_callbacks: Rc>, create_calls: Rc>) -> Self { Self { - identity_callbacks: Cell::new(0), - create_calls: 0, + identity_callbacks, + create_calls, } } @@ -32,13 +34,21 @@ impl DisposableContextPort for SideEffectingIdentityPort { &mut self, _request: &DisposableContextCreateRequest, ) -> Result { - self.create_calls += 1; + self.create_calls + .set(self.create_calls.get().saturating_add(1)); Ok(DisposableContextHandle::new( DisposableIsolationId::parse("preflight-user-context").expect("valid isolation id"), BrowsingContextId::new(401).expect("valid browsing context"), )) } + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + fn destroy_disposable_context( &mut self, _request: &DisposableContextDestroyRequest, @@ -51,21 +61,27 @@ impl DisposableContextPort for SideEffectingIdentityPort { fn lifecycle_binding_invokes_no_adapter_callback_before_authorized_create() { let session = BrowserSession::start(BrowserSessionId::new(401).expect("valid session id")) .expect("incarnation capacity"); - let port = SideEffectingIdentityPort::new(); - let mut bound = session.bind_lifecycle_port(port); + let identity_callbacks = Rc::new(Cell::new(0)); + let create_calls = Rc::new(Cell::new(0)); + let port = SideEffectingIdentityPort::new( + Rc::clone(&identity_callbacks), + Rc::clone(&create_calls), + ); + + // Prove the fixture observes a shared-reference callback without retaining adapter access after bind. + port.identity_probe(); + assert_eq!(identity_callbacks.get(), 1); + identity_callbacks.set(0); + let mut bound = session.bind_lifecycle_port(port); assert_eq!( - bound.lifecycle_port().identity_callbacks.get(), + identity_callbacks.get(), 0, "binding invoked adapter code before aggregate-issued lifecycle authority existed" ); bound .create_disposable_context() .expect("authorized create"); - assert_eq!(bound.lifecycle_port().identity_callbacks.get(), 0); - assert_eq!(bound.lifecycle_port().create_calls, 1); - - // Prove the fixture would detect an identity callback if production code invoked one. - bound.lifecycle_port().identity_probe(); - assert_eq!(bound.lifecycle_port().identity_callbacks.get(), 1); + assert_eq!(identity_callbacks.get(), 0); + assert_eq!(create_calls.get(), 1); } diff --git a/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs index ae16efd9b..bc692f31d 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_same_id_spoof.rs @@ -1,7 +1,11 @@ +use std::cell::Cell; +use std::rc::Rc; + use originweave_browser_session::{ - BrowserSession, DisposableContextCreateError, DisposableContextCreateRequest, - DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, - DisposableContextPort, DisposableIsolationId, + BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -9,17 +13,22 @@ use originweave_core::{BrowserSessionId, BrowsingContextId}; struct RecordingPort { context: BrowsingContextId, isolation: &'static str, - create_calls: usize, - destroy_calls: usize, + create_calls: Rc>, + destroy_calls: Rc>, } impl RecordingPort { - fn new(context: u64, isolation: &'static str) -> Self { + fn new( + context: u64, + isolation: &'static str, + create_calls: Rc>, + destroy_calls: Rc>, + ) -> Self { Self { context: BrowsingContextId::new(context).expect("valid browsing context"), isolation, - create_calls: 0, - destroy_calls: 0, + create_calls, + destroy_calls, } } } @@ -29,18 +38,25 @@ impl DisposableContextPort for RecordingPort { &mut self, _request: &DisposableContextCreateRequest, ) -> Result { - self.create_calls += 1; + self.create_calls.set(self.create_calls.get() + 1); Ok(DisposableContextHandle::new( DisposableIsolationId::parse(self.isolation).expect("valid isolation id"), self.context, )) } + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + fn destroy_disposable_context( &mut self, _request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - self.destroy_calls += 1; + self.destroy_calls.set(self.destroy_calls.get() + 1); Ok(()) } } @@ -49,23 +65,51 @@ impl DisposableContextPort for RecordingPort { fn distinct_adapter_cannot_be_substituted_for_create_after_binding() { let session = BrowserSession::start(BrowserSessionId::new(17).expect("valid session id")) .expect("incarnation capacity"); - let approved_port = RecordingPort::new(41, "approved-isolation"); - let spoofing_port = RecordingPort::new(42, "spoofed-isolation"); + let approved_create_calls = Rc::new(Cell::new(0)); + let approved_destroy_calls = Rc::new(Cell::new(0)); + let spoof_create_calls = Rc::new(Cell::new(0)); + let spoof_destroy_calls = Rc::new(Cell::new(0)); + let approved_port = RecordingPort::new( + 41, + "approved-isolation", + Rc::clone(&approved_create_calls), + Rc::clone(&approved_destroy_calls), + ); + let _spoofing_port = RecordingPort::new( + 42, + "spoofed-isolation", + Rc::clone(&spoof_create_calls), + Rc::clone(&spoof_destroy_calls), + ); let mut bound = session.bind_lifecycle_port(approved_port); bound .create_disposable_context() .expect("bound adapter creates context"); - assert_eq!(bound.lifecycle_port().create_calls, 1); - assert_eq!(spoofing_port.create_calls, 0); + assert_eq!(approved_create_calls.get(), 1); + assert_eq!(spoof_create_calls.get(), 0); } #[test] fn distinct_adapter_cannot_be_substituted_for_destroy_after_binding() { let session = BrowserSession::start(BrowserSessionId::new(18).expect("valid session id")) .expect("incarnation capacity"); - let approved_port = RecordingPort::new(51, "approved-isolation"); - let spoofing_port = RecordingPort::new(52, "spoofed-isolation"); + let approved_create_calls = Rc::new(Cell::new(0)); + let approved_destroy_calls = Rc::new(Cell::new(0)); + let spoof_create_calls = Rc::new(Cell::new(0)); + let spoof_destroy_calls = Rc::new(Cell::new(0)); + let approved_port = RecordingPort::new( + 51, + "approved-isolation", + Rc::clone(&approved_create_calls), + Rc::clone(&approved_destroy_calls), + ); + let _spoofing_port = RecordingPort::new( + 52, + "spoofed-isolation", + Rc::clone(&spoof_create_calls), + Rc::clone(&spoof_destroy_calls), + ); let mut bound = session.bind_lifecycle_port(approved_port); let authority = bound .create_disposable_context() @@ -74,6 +118,6 @@ fn distinct_adapter_cannot_be_substituted_for_destroy_after_binding() { bound .destroy_disposable_context(&authority) .expect("bound adapter destroys context"); - assert_eq!(bound.lifecycle_port().destroy_calls, 1); - assert_eq!(spoofing_port.destroy_calls, 0); + assert_eq!(approved_destroy_calls.get(), 1); + assert_eq!(spoof_destroy_calls.get(), 0); } diff --git a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs index 9232da729..81eac3a9e 100644 --- a/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs +++ b/crates/originweave-browser-session/tests/sequential_incarnation_reuse.rs @@ -1,27 +1,37 @@ +use std::cell::RefCell; +use std::rc::Rc; + use originweave_browser_session::{ - BrowserSession, BrowserSessionError, BrowserSessionIncarnation, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + BrowserSession, BrowserSessionError, BrowserSessionIncarnation, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; #[derive(Debug)] struct ReusingPort { handle: DisposableContextHandle, - create_incarnations: Vec, - destroy_incarnations: Vec, + create_incarnations: Rc>>, + destroy_incarnations: Rc>>, } impl ReusingPort { - fn new(context: u64, isolation: &str) -> Result { + fn new( + context: u64, + isolation: &str, + create_incarnations: Rc>>, + destroy_incarnations: Rc>>, + ) -> Result { let isolation = DisposableIsolationId::parse(isolation) .map_err(|_| "static fixture isolation id must be valid")?; let browsing_context = BrowsingContextId::new(context) .map_err(|_| "static fixture browsing context id must be valid")?; Ok(Self { handle: DisposableContextHandle::new(isolation, browsing_context), - create_incarnations: Vec::new(), - destroy_incarnations: Vec::new(), + create_incarnations, + destroy_incarnations, }) } } @@ -31,15 +41,26 @@ impl DisposableContextPort for ReusingPort { &mut self, request: &DisposableContextCreateRequest, ) -> Result { - self.create_incarnations.push(request.incarnation()); + self.create_incarnations + .borrow_mut() + .push(request.incarnation()); Ok(self.handle.clone()) } + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + fn destroy_disposable_context( &mut self, request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { - self.destroy_incarnations.push(request.incarnation()); + self.destroy_incarnations + .borrow_mut() + .push(request.incarnation()); Ok(()) } } @@ -50,11 +71,15 @@ fn stale_authority_cannot_cross_sequential_session_incarnations() -> Result<(), let session_id = BrowserSessionId::new(701) .map_err(|_| "static fixture browser session id must be valid")?; + let create_a = Rc::new(RefCell::new(Vec::new())); + let destroy_a = Rc::new(RefCell::new(Vec::new())); let session_a = BrowserSession::start(session_id) .map_err(|_| "first browser session incarnation must be available")?; let mut bound_a = session_a.bind_lifecycle_port(ReusingPort::new( 7010, "user-context-reused", + Rc::clone(&create_a), + Rc::clone(&destroy_a), )?); let authority_a = bound_a .create_disposable_context() @@ -66,11 +91,15 @@ fn stale_authority_cannot_cross_sequential_session_incarnations() -> Result<(), .end() .map_err(|_| "first browser session must end normally")?; + let create_b = Rc::new(RefCell::new(Vec::new())); + let destroy_b = Rc::new(RefCell::new(Vec::new())); let session_b = BrowserSession::start(session_id) .map_err(|_| "second browser session incarnation must be available")?; let mut bound_b = session_b.bind_lifecycle_port(ReusingPort::new( 7010, "user-context-reused", + Rc::clone(&create_b), + Rc::clone(&destroy_b), )?); let authority_b = bound_b .create_disposable_context() @@ -81,25 +110,25 @@ fn stale_authority_cannot_cross_sequential_session_incarnations() -> Result<(), bound_b.browser_session().incarnation() ); assert_eq!( - bound_a.lifecycle_port().create_incarnations, - vec![bound_a.browser_session().incarnation()] + create_a.borrow().as_slice(), + &[bound_a.browser_session().incarnation()] ); assert_eq!( - bound_b.lifecycle_port().create_incarnations, - vec![bound_b.browser_session().incarnation()] + create_b.borrow().as_slice(), + &[bound_b.browser_session().incarnation()] ); assert_eq!( bound_b.destroy_disposable_context(&authority_a), Err(BrowserSessionError::AuthorityMismatch) ); - assert!(bound_b.lifecycle_port().destroy_incarnations.is_empty()); + assert!(destroy_b.borrow().is_empty()); bound_b .destroy_disposable_context(&authority_b) .map_err(|_| "current incarnation authority must remain valid")?; assert_eq!( - bound_b.lifecycle_port().destroy_incarnations, - vec![bound_b.browser_session().incarnation()] + destroy_b.borrow().as_slice(), + &[bound_b.browser_session().incarnation()] ); Ok(()) } From b3ff343b48c1201f42d5dc6bae07bc606a42c7b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:20:57 +0900 Subject: [PATCH 014/632] fix(browser-session): settle exact create transactions --- crates/originweave-browser-session/src/lib.rs | 297 ++++++++++++++---- 1 file changed, 242 insertions(+), 55 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index f020f1c9c..7c8632a7d 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -174,6 +174,8 @@ pub enum BrowserSessionRecoveryEvidence { PartialCreationIsolation(DisposableIsolationId), /// A create call returned a complete handle that aliased an already-owned context or isolation. DuplicateAdapterHandle(DisposableContextHandle), + /// A complete create result could not be settled with the bound adapter after domain validation. + UnsettledAdapterHandle(DisposableContextHandle), /// Destruction of this exact owned handle failed or could not be proven. UnprovenDestruction(DisposableContextHandle), } @@ -187,6 +189,7 @@ pub enum BrowserSessionRecoveryEvidence { pub struct DisposableContextCreateRequest { browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, + attempt_epoch: BrowserContextEpoch, } impl DisposableContextCreateRequest { @@ -201,6 +204,68 @@ impl DisposableContextCreateRequest { pub const fn incarnation(&self) -> BrowserSessionIncarnation { self.incarnation } + + /// Return the unique context epoch reserved for this create attempt. + #[must_use] + pub const fn attempt_epoch(&self) -> BrowserContextEpoch { + self.attempt_epoch + } +} + +/// Domain disposition for one completed disposable-context create attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableContextCreateDisposition { + /// The returned handle passed Browser Session ownership validation and may become authorizing. + Accepted, + /// The returned handle failed Browser Session ownership validation and must remain non-authorizing. + Rejected, +} + +/// Opaque Browser Session-issued completion for one exact create attempt. +/// +/// The adapter may stage remote protocol state while executing a create request, but it must not +/// promote that state into an authorizing binding until it receives an `Accepted` completion for the +/// same session incarnation and attempt epoch. `Rejected` candidates are recovery/quarantine evidence +/// only. There is deliberately no public constructor. +#[derive(Debug)] +pub struct DisposableContextCreateCompletion { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + attempt_epoch: BrowserContextEpoch, + disposition: DisposableContextCreateDisposition, +} + +impl DisposableContextCreateCompletion { + /// Return the Browser Session transport identity for adapter correlation. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the Browser Session incarnation for adapter correlation. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the create-attempt epoch that this completion settles. + #[must_use] + pub const fn attempt_epoch(&self) -> BrowserContextEpoch { + self.attempt_epoch + } + + /// Return whether Browser Session accepted or rejected the created candidate. + #[must_use] + pub const fn disposition(&self) -> DisposableContextCreateDisposition { + self.disposition + } +} + +/// Failure while settling one exact create attempt with the bound lifecycle adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableContextCreateCompletionError { + /// The adapter could not prove that the exact pending create attempt reached the requested state. + CompletionFailed, } /// Opaque Browser Session-issued request for destruction of one exact owned disposable context. @@ -258,6 +323,16 @@ pub trait DisposableContextPort { request: &DisposableContextCreateRequest, ) -> Result; + /// Settle the exact create attempt after Browser Session validates the returned domain handle. + /// + /// An adapter must keep a successful remote create result non-authorizing until this completion + /// accepts the matching attempt. A rejected attempt must remain non-authorizing and be retained + /// only for recovery/quarantine processing. + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError>; + /// Destroy the exact disposable isolation boundary represented by this authorized request. fn destroy_disposable_context( &mut self, @@ -509,6 +584,7 @@ impl BrowserSession { let request = DisposableContextCreateRequest { browser_session: self.id, incarnation: self.incarnation, + attempt_epoch: epoch, }; let handle = match port.create_disposable_context(&request) { Ok(handle) => handle, @@ -526,25 +602,60 @@ impl BrowserSession { } }; - if self + let duplicate_error = if self .contexts .values() .any(|record| record.handle.isolation == handle.isolation) { + Some(BrowserSessionError::DuplicateDisposableIsolation) + } else if self.contexts.contains_key(&handle.browsing_context) { + Some(BrowserSessionError::DuplicateBrowsingContext) + } else { + None + }; + + if let Some(error) = duplicate_error { + let completion = DisposableContextCreateCompletion { + browser_session: self.id, + incarnation: self.incarnation, + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Rejected, + }; self.recovery_evidence .push(BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( - handle, + handle.clone(), )); + if port + .complete_disposable_context_creation(&completion) + .is_err() + { + self.recovery_evidence + .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + handle, + )); + self.enter_recovery_required(); + return Err(BrowserSessionError::ContextCreationUncertain); + } self.enter_recovery_required(); - return Err(BrowserSessionError::DuplicateDisposableIsolation); + return Err(error); } - if self.contexts.contains_key(&handle.browsing_context) { + + let completion = DisposableContextCreateCompletion { + browser_session: self.id, + incarnation: self.incarnation, + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Accepted, + }; + if port + .complete_disposable_context_creation(&completion) + .is_err() + { self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( + .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( handle, )); self.enter_recovery_required(); - return Err(BrowserSessionError::DuplicateBrowsingContext); + return Err(BrowserSessionError::ContextCreationUncertain); } let browsing_context = handle.browsing_context; @@ -654,12 +765,6 @@ impl BoundBrowserSession

{ &self.session } - /// Return the bound lifecycle port for read-only diagnostics and adapter-local planning. - #[must_use] - pub const fn lifecycle_port(&self) -> &P { - &self.port - } - /// Create one disposable context through the exact port consumed when this session was bound. pub fn create_disposable_context( &mut self, @@ -734,10 +839,18 @@ mod tests { handles: VecDeque, create_error: Option, fail_destroy: bool, + fail_completion: bool, create_calls: usize, destroy_calls: usize, create_sessions: Vec, create_incarnations: Vec, + create_attempts: Vec, + create_completions: Vec<( + BrowserSessionId, + BrowserSessionIncarnation, + BrowserContextEpoch, + DisposableContextCreateDisposition, + )>, destroy_sessions: Vec, destroy_incarnations: Vec, destroyed_isolations: Vec, @@ -756,10 +869,13 @@ mod tests { handles: handles.into(), create_error: None, fail_destroy: false, + fail_completion: false, create_calls: 0, destroy_calls: 0, create_sessions: Vec::new(), create_incarnations: Vec::new(), + create_attempts: Vec::new(), + create_completions: Vec::new(), destroy_sessions: Vec::new(), destroy_incarnations: Vec::new(), destroyed_isolations: Vec::new(), @@ -775,6 +891,7 @@ mod tests { self.create_calls += 1; self.create_sessions.push(request.browser_session()); self.create_incarnations.push(request.incarnation()); + self.create_attempts.push(request.attempt_epoch()); match self.create_error.clone() { Some(error) => Err(error), None => Ok(self @@ -784,6 +901,23 @@ mod tests { } } + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + self.create_completions.push(( + completion.browser_session(), + completion.incarnation(), + completion.attempt_epoch(), + completion.disposition(), + )); + if self.fail_completion { + Err(DisposableContextCreateCompletionError::CompletionFailed) + } else { + Ok(()) + } + } + fn destroy_disposable_context( &mut self, request: &DisposableContextDestroyRequest, @@ -857,16 +991,26 @@ mod tests { let authority = bound .create_disposable_context() .expect("owned disposable context"); + assert_eq!(bound.port.create_sessions, vec![session_id(1)]); assert_eq!( - bound.lifecycle_port().create_sessions, - vec![session_id(1)] + bound.port.create_incarnations, + vec![bound.browser_session().incarnation()] ); + assert_eq!(bound.port.create_attempts, vec![BrowserContextEpoch(1)]); assert_eq!( - bound.lifecycle_port().create_incarnations, - vec![bound.browser_session().incarnation()] + bound.port.create_completions, + vec![( + session_id(1), + bound.browser_session().incarnation(), + BrowserContextEpoch(1), + DisposableContextCreateDisposition::Accepted, + )] ); assert_eq!(authority.browser_session(), session_id(1)); - assert_eq!(authority.incarnation(), bound.browser_session().incarnation()); + assert_eq!( + authority.incarnation(), + bound.browser_session().incarnation() + ); assert_eq!(authority.isolation().as_str(), "isolation-10"); assert_eq!(authority.browsing_context(), context_id(10)); assert_eq!(authority.context_epoch().value(), 1); @@ -961,6 +1105,68 @@ mod tests { ); } + #[test] + fn create_completion_failure_preserves_non_authorizing_recovery_evidence() { + let expected = + DisposableContextHandle::new(isolation_id("isolation-315"), context_id(315)); + let mut port = TestPort::with_handles(vec![expected.clone()]); + port.fail_completion = true; + let mut bound = session(315).bind_lifecycle_port(port); + + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert_eq!( + bound.browser_session().recovery_evidence(), + &[BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + expected + )] + ); + assert_eq!( + bound.port.create_completions[0].3, + DisposableContextCreateDisposition::Accepted + ); + assert_eq!( + bound.presentation_authority(context_id(315)), + Err(BrowserSessionError::SessionNotActive) + ); + } + + #[test] + fn rejected_create_completion_failure_preserves_duplicate_and_unsettled_evidence() { + let first = + DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); + let duplicate = + DisposableContextHandle::new(isolation_id("isolation-316-b"), context_id(316)); + let mut port = TestPort::with_handles(vec![first, duplicate.clone()]); + let mut bound = session(316).bind_lifecycle_port(port); + + bound + .create_disposable_context() + .expect("first candidate accepted"); + bound.port.fail_completion = true; + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert_eq!( + bound.browser_session().recovery_evidence(), + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate.clone()), + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate), + ] + ); + assert_eq!( + bound.port.create_completions[1].3, + DisposableContextCreateDisposition::Rejected + ); + } + #[test] fn bound_port_is_structural_and_not_swappable() { let approved = TestPort::new(320, "isolation-320"); @@ -974,8 +1180,8 @@ mod tests { bound .destroy_disposable_context(&authority) .expect("same structurally bound port destroys context"); - assert_eq!(bound.lifecycle_port().create_calls, 1); - assert_eq!(bound.lifecycle_port().destroy_calls, 1); + assert_eq!(bound.port.create_calls, 1); + assert_eq!(bound.port.destroy_calls, 1); } #[test] @@ -986,15 +1192,13 @@ mod tests { bound.create_disposable_context(), Err(BrowserSessionError::EpochExhausted) ); - assert_eq!(bound.lifecycle_port().create_calls, 0); + assert_eq!(bound.port.create_calls, 0); } #[test] fn epoch_exhaustion_prevents_advance_mutation() { let mut bound = session(41).bind_lifecycle_port(TestPort::new(410, "isolation-410")); - let authority = bound - .create_disposable_context() - .expect("owned context"); + let authority = bound.create_disposable_context().expect("owned context"); bound.session.next_epoch = u64::MAX; assert_eq!( bound.advance_context_epoch(context_id(410)), @@ -1006,9 +1210,7 @@ mod tests { #[test] fn epoch_advance_invalidates_old_and_unknown_authority() { let mut bound = session(5).bind_lifecycle_port(TestPort::new(50, "isolation-50")); - let old = bound - .create_disposable_context() - .expect("owned context"); + let old = bound.create_disposable_context().expect("owned context"); assert_eq!( bound.advance_context_epoch(context_id(51)), Err(BrowserSessionError::ContextNotOwned) @@ -1025,7 +1227,7 @@ mod tests { .destroy_disposable_context(&new) .expect("destroy current epoch"); assert_eq!( - bound.lifecycle_port().destroy_incarnations, + bound.port.destroy_incarnations, vec![bound.browser_session().incarnation()] ); assert_eq!( @@ -1041,9 +1243,7 @@ mod tests { #[test] fn cross_session_and_foreign_isolation_authority_fail_before_io() { let mut owner = session(6).bind_lifecycle_port(TestPort::new(60, "isolation-60")); - let authority = owner - .create_disposable_context() - .expect("owner context"); + let authority = owner.create_disposable_context().expect("owner context"); let mut foreign = session(7).bind_lifecycle_port(TestPort::new(60, "isolation-60")); foreign @@ -1053,7 +1253,7 @@ mod tests { foreign.destroy_disposable_context(&authority), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(foreign.lifecycle_port().destroy_calls, 0); + assert_eq!(foreign.port.destroy_calls, 0); let forged = PresentationMutationAuthority { browser_session: owner.browser_session().id(), @@ -1066,7 +1266,7 @@ mod tests { owner.destroy_disposable_context(&forged), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(owner.lifecycle_port().destroy_calls, 0); + assert_eq!(owner.port.destroy_calls, 0); } #[test] @@ -1075,9 +1275,7 @@ mod tests { let mut session_a = BrowserSession::start(shared_id) .expect("A incarnation") .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); - let authority_a = session_a - .create_disposable_context() - .expect("A context"); + let authority_a = session_a.create_disposable_context().expect("A context"); session_a .destroy_disposable_context(&authority_a) .expect("A destroy"); @@ -1086,9 +1284,7 @@ mod tests { let mut session_b = BrowserSession::start(shared_id) .expect("B incarnation") .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); - let authority_b = session_b - .create_disposable_context() - .expect("B context"); + let authority_b = session_b.create_disposable_context().expect("B context"); assert_ne!( session_a.browser_session().incarnation(), session_b.browser_session().incarnation() @@ -1097,11 +1293,11 @@ mod tests { session_b.destroy_disposable_context(&authority_a), Err(BrowserSessionError::AuthorityMismatch) ); - assert_eq!(session_b.lifecycle_port().destroy_calls, 0); + assert_eq!(session_b.port.destroy_calls, 0); session_b .destroy_disposable_context(&authority_b) .expect("B destroy"); - assert_eq!(session_b.lifecycle_port().destroy_calls, 1); + assert_eq!(session_b.port.destroy_calls, 1); } #[test] @@ -1111,9 +1307,7 @@ mod tests { let mut port = TestPort::new(90, "isolation-90"); port.fail_destroy = true; let mut bound = session(9).bind_lifecycle_port(port); - let authority = bound - .create_disposable_context() - .expect("owned context"); + let authority = bound.create_disposable_context().expect("owned context"); assert_eq!( bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) @@ -1154,9 +1348,7 @@ mod tests { #[test] fn transport_loss_invalidates_active_contexts_and_is_idempotent() { let mut bound = session(10).bind_lifecycle_port(TestPort::new(100, "isolation-100")); - let authority = bound - .create_disposable_context() - .expect("owned context"); + let authority = bound.create_disposable_context().expect("owned context"); assert!(bound.record_transport_loss()); assert_eq!( bound.browser_session().state(), @@ -1168,25 +1360,20 @@ mod tests { bound.destroy_disposable_context(&authority), Err(BrowserSessionError::SessionNotActive) ); - assert_eq!(bound.lifecycle_port().destroy_calls, 0); + assert_eq!(bound.port.destroy_calls, 0); } #[test] fn normal_end_requires_proven_destruction_and_ignores_late_transport_report() { let mut bound = session(11).bind_lifecycle_port(TestPort::new(110, "isolation-110")); - let authority = bound - .create_disposable_context() - .expect("owned context"); + let authority = bound.create_disposable_context().expect("owned context"); assert_eq!(bound.end(), Err(BrowserSessionError::ActiveContextRemains)); bound .destroy_disposable_context(&authority) .expect("proven destruction"); + assert_eq!(bound.port.destroy_sessions, vec![session_id(11)]); assert_eq!( - bound.lifecycle_port().destroy_sessions, - vec![session_id(11)] - ); - assert_eq!( - bound.lifecycle_port().destroyed_isolations, + bound.port.destroyed_isolations, vec![isolation_id("isolation-110")] ); bound.end().expect("normal end"); From 6be41094ca81d77f4bebb88f0f5de50899d15f45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:21:39 +0900 Subject: [PATCH 015/632] docs(browser-session): record create transaction authority --- ...er-session-disposable-context-authority.md | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index b30fb85e3..1dc178b96 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -5,107 +5,117 @@ ## Context -OriginWeave's WebDriver BiDi presentation adapter requires opaque ownership witnesses before viewport/device-pixel-ratio, timezone, or screen-area mutation can be planned. A caller that merely knows a browser-session or browsing-context identifier therefore cannot overwrite another owner's presentation state and later clear it to an implementation default. +OriginWeave's Browser Session bounded context is the authority for disposable browser lifecycle ownership and presentation mutation. WebDriver BiDi session ids, user-context ids, browsing-context ids, and adapter-selected values are protocol addressability, not authorization. -Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are protocol or implementation addressability. They are not Browser Session authority. A previous repair introduced opaque `DisposableContextCreateRequest` and `DisposableContextDestroyRequest`, but also asked each adapter to self-report a public numeric port id. That left two defects: a second adapter could select the same id, and Browser Session had to invoke arbitrary adapter code to read that id before lifecycle authority existed. Rust `&self` does not make such a callback pure. +Two active-PR findings refine the lifecycle-port boundary. First, `BoundBrowserSession::lifecycle_port(&self) -> &P` exposed the concrete adapter after binding. Rust shared references do not prove purity: interior mutability, synchronization primitives, or an internally synchronized client can still mutate local state or perform remote I/O. A "read-only" label therefore does not create a security boundary. -Lifecycle failures also need lossless evidence. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. These outcomes require recovery quarantine while retaining every exact browser-issued identity that is already known. +Second, one Browser Session incarnation can issue multiple remote create attempts. A create request carrying only `(BrowserSessionId, BrowserSessionIncarnation)` does not identify which returned protocol tuple Browser Session later accepted or rejected. A WebDriver BiDi adapter needs an exact **per-create transaction** so it can stage remote state as pending, promote only the accepted candidate, and quarantine the rejected candidate without relying on call order or adapter-local counters as authority. -Transport liveness is independent from ownership certainty. A session already in `RecoveryRequired` can subsequently lose its transport; that new fact must be recorded without erasing recovery evidence. Conversely, merely entering recovery does not prove the transport is dead. +Lifecycle failures still require lossless evidence. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. Transport liveness remains orthogonal to ownership certainty. -The 9 September 2026 WebDriver BiDi Working Draft defines user-context identifiers and the `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext` lifecycle. Those commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. +The 9 September 2026 WebDriver BiDi Working Draft defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. ## Decision drivers -- Raw WebDriver/BiDi identifiers and adapter-chosen ids are addressability, not mutation or cleanup authority. +- Raw WebDriver/BiDi identifiers and adapter-chosen values are addressability, not mutation or cleanup authority. - No arbitrary adapter callback may be required to establish lifecycle-port ownership. -- A caller must not be able to substitute a second adapter instance after Browser Session lifecycle binding. -- Create/destroy requests must remain non-caller-constructible and usable only through the bound aggregate composition. -- Sequential aggregate recreation must not make a retained stale authority valid again. -- Known remote identities from partial creation, duplicate output, or unproven destruction must be retained as recovery evidence without becoming command authority. -- Ownership recovery and transport liveness remain orthogonal. +- A caller must not be able to substitute or recover the concrete adapter after Browser Session binding. +- Browser Session create/destroy capabilities remain non-caller-constructible. +- Every successful remote create result must be correlated to one exact Browser Session-issued attempt before it can become authorizing. +- Browser Session, not the adapter, decides whether a returned domain handle is accepted or rejected. +- Protocol-specific pending/accepted/quarantined tuples remain the WebDriver BiDi ACL owner's truth. +- Sequential aggregate recreation must not make retained stale authority valid again. +- Recovery evidence and transport liveness remain orthogonal. - Browser Session remains the domain authority; WebDriver BiDi, CDP, MCP, and LLMs remain adapters or consumers. ## Decision Introduce and retain `originweave-browser-session` as an independent Rust bounded context. ADR status remains `Proposed` until protected-main and real-browser acceptance exist. -1. `BrowserSession::start` allocates a process-local, monotonically non-reused `BrowserSessionIncarnation` before browser I/O. Allocation fails closed before `u64` wrap. -2. Presentation authority is intentionally non-serializable. Within one process, `BrowserSessionIncarnation` prevents sequential ABA when a later aggregate reuses the same external session, isolation, context, and local epoch values. -3. Browser Session uses a **linear lifecycle-port binding**. `BrowserSession::bind_lifecycle_port` consumes both the aggregate and one concrete adapter value into `BoundBrowserSession

`. Binding performs no adapter callback. -4. `BoundBrowserSession

` does not expose mutable port access and its public create/destroy methods accept no alternate port argument. The exact adapter instance is therefore structural composition rather than a caller-selected or self-asserted scalar identity. -5. `DisposableContextPort` has no `port_id()` preflight method. `DisposableContextPortId` is removed. A second adapter cannot claim equality by choosing the same scalar. -6. `DisposableContextCreateRequest` and `DisposableContextDestroyRequest` remain opaque, have no public constructor, and are created only inside the bound Browser Session path after aggregate state or exact presentation authority has been validated. They carry Browser Session addressability and incarnation; the destroy request additionally carries the exact stored handle. -7. The adapter is part of the reviewed lifecycle anti-corruption boundary. A malicious adapter implementation that internally delegates an authorized request is outside what a Rust trait can prevent without inverting the dependency boundary; protocol-specific pending/accepted/quarantine ownership remains the responsibility of the separately reviewed BiDi ACL adapter in ADR 0115. -8. A context enters the owned set only after the bound port returns a `DisposableContextHandle`. Raw `BrowsingContextId` input never creates ownership. -9. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before destruction I/O. -10. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; a known browser-issued isolation identity is preserved exactly. -11. Duplicate browsing-context or isolation output enters `RecoveryRequired` and stores the complete offending `DisposableContextHandle` as recovery evidence. OriginWeave does not auto-destroy ambiguous output. -12. `BrowserSessionRecoveryEvidence` records only reconciliation evidence: `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnprovenDestruction`. It grants no browser command authority. -13. Destruction validates exact authority before I/O and passes the current incarnation and stored handle in `DisposableContextDestroyRequest`. `DisposableContextDestroyError` moves the record and aggregate into recovery and retains the exact failed handle. -14. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; repeated reports are idempotent. -15. `RecoveryRequired`, `TransportLost`, and `Ended` reject active-only creation, authority issuance/advance, destruction, and normal end. Reconciliation is a later, separately authorized design. -16. Context epochs remain monotonic authority identities within one aggregate. They invalidate older authority after navigation or another lifecycle boundary but are not a substitute for session incarnation. +1. `BrowserSession::start` allocates a process-local, monotonically non-reused `BrowserSessionIncarnation` before browser I/O. Exhaustion fails closed. +2. Presentation authority is intentionally non-serializable. `BrowserSessionIncarnation` prevents sequential ABA within one process. +3. Browser Session uses a **linear lifecycle-port binding**. `BrowserSession::bind_lifecycle_port` consumes both aggregate and one concrete `DisposableContextPort` into `BoundBrowserSession

` without invoking adapter code. +4. `BoundBrowserSession

` has **no public raw port accessor** and no lifecycle method that accepts an alternate port. Tests observe adapter behavior through independently retained inert counters/ledgers rather than extracting `&P`. +5. `DisposableContextPort` has no identity-preflight method. Adapter identity is structural composition, not a self-asserted scalar. +6. `DisposableContextCreateRequest` remains opaque and gains the already-reserved `BrowserContextEpoch` as an exact create-attempt identity. The `(session, incarnation, attempt epoch)` tuple is unique for create attempts in one live aggregate and is not caller-constructible as a request. +7. After `create_disposable_context` returns a handle, Browser Session validates isolation and browsing-context ownership before granting authority. +8. Browser Session then privately issues `DisposableContextCreateCompletion` for that exact attempt with `Accepted` or `Rejected`. +9. The adapter must keep a successful remote create result non-authorizing until the matching `Accepted` completion. `Rejected` results remain non-authorizing recovery/quarantine state. A completion that cannot be proven for the exact pending attempt fails closed and sends the aggregate to `RecoveryRequired`. +10. Protocol-specific remote tuple contents are not copied into Browser Session. #314/#316 owns WebDriver BiDi pending/accepted/quarantined storage and remote-liveness validation. +11. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, and the exact stored handle. +12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before destruction I/O. +13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. +14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. +15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, an unsettled complete adapter handle when completion itself cannot be proven, and exact unproven-destruction handle. Recovery evidence grants no browser command authority. +16. Destruction validates exact authority before I/O. Unproven destruction moves the aggregate into recovery and retains the exact failed handle. +17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; repeated reports are idempotent. +18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. +19. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. ## Alternatives considered -### Adapter-supplied numeric port id +### Adapter-supplied identity or preflight callback -Rejected. A public scalar is caller-selectable and replayable by a distinct adapter. Making the callback side-effect-free by documentation is also insufficient because Rust `&self` permits interior mutation and delegated effects. +Rejected. A scalar can be replayed and a shared-reference callback can still have side effects before Browser Session authority exists. -### Pointer-address identity +### Public read-only `&P` after binding -Rejected. Object addresses are implementation details, can change when values move, and can be reused after destruction. Pointer equality would replace one ABA surface with another. +Rejected. Rust `&P` forbids an ordinary mutable borrow but does not prohibit interior mutation or remote effects from `&self` methods. The concrete adapter would remain a capability escape. -### Session-owned wrapper with the concrete port +### Adapter-local create sequence number -Selected. Ownership is represented by Rust move semantics and private fields. No identity probe is required, the caller cannot swap a second adapter into public lifecycle methods, and opaque requests remain confined to the bound call path. +Rejected as authority. It could be useful internally, but Browser Session could not prove which pending remote tuple it was accepting. Correlation must originate in the aggregate-issued request. -### Persist authority generations globally +### Reserved BrowserContextEpoch as create-attempt identity -Deferred. Presentation authority is not durable across process restart; restart reconciliation belongs to evidence and browser observation, not silent authority resurrection. +Selected. Browser Session already reserves the epoch before create I/O, it is non-caller-constructible, monotonic within the aggregate, and the same value becomes the accepted context's first mutation epoch. -### Automatically clean duplicate or partial state +### Automatically clean duplicate or rejected state -Rejected. When ownership is ambiguous, cleanup itself can become a cross-owner destructive action. +Rejected. Ambiguous ownership makes speculative cleanup a potential cross-owner destructive action. ## Consequences -Browser Session no longer asks an adapter to prove its own identity before authority. The aggregate and exact lifecycle port become one composed runtime object, while adapter-specific remote identifiers remain outside the Browser Session domain model. +The active stack receives a breaking trait change: every `DisposableContextPort` implementation must settle successful create results through `complete_disposable_context_creation`. #316 must restack non-force and map this completion into its adapter-local pending/accepted/quarantined state. -The API change is intentionally breaking on the active stack: consumers must call `BrowserSession::bind_lifecycle_port(port)` and then perform lifecycle operations through `BoundBrowserSession`. ADR 0115/#316 must be non-force restacked and adapt its WebDriver BiDi lifecycle adapter to this composition before adoption. +The bound adapter is no longer publicly recoverable from `BoundBrowserSession`. Application and test code that needs observability must retain inert metrics or diagnostic projections separately; those projections must not expose adapter command capability. -This binding closes ordinary caller substitution and self-selected-id replay. It does not claim that an adversarial implementation of the trusted `DisposableContextPort` trait cannot internally forward calls; such an implementation already executes inside the reviewed adapter TCB. The BiDi ACL still must prove pending → accepted/quarantined remote ownership, complete recovery tuples, and live-target validation independently. +A completion failure is treated as ownership uncertainty. Browser Session retains the returned handle as recovery evidence and does not mint normal authority. ## Security and governance impact -No page-controlled value, raw browser-session id, raw browsing-context id, user-context string, adapter-selected scalar, provider/model decision, or LLM output can mint lifecycle requests or presentation authority. Browser Session performs no arbitrary adapter callback while establishing the lifecycle-port binding. +No page-controlled value, raw browser identifier, adapter-selected scalar, diagnostic reference, provider/model decision, or LLM output can mint lifecycle completion or presentation authority. Remote creation stays non-authorizing until the aggregate validates ownership and accepts that exact attempt. -Unknown or duplicate remote state is quarantined rather than destroyed speculatively. This does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. +This decision does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. ## Tests and exact evidence -The suite retains recovery, sequential ABA, epoch, foreign-authority, destruction, transport-loss, and normal-end coverage. `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` proves that binding performs no adapter callback before the aggregate-issued create request. `distinct_adapter_cannot_be_substituted_for_create_after_binding` and `distinct_adapter_cannot_be_substituted_for_destroy_after_binding`, together with repository source contracts, require lifecycle methods to use only the consumed port and prohibit reintroduction of public `DisposableContextPortId`/`port_id()` or arbitrary-port Browser Session lifecycle methods. +Required executable cases include: -The prior hostile RED was captured on exact `d43a4d86c8487ebdb9db9f1c4650fb7ee6225afc` in CI `34524654914`: the pre-authority callback fixture observed one identity callback where zero was required. This decision replaces that self-asserted identity design rather than suppressing the test. +- binding invokes no arbitrary adapter callback before an aggregate-issued create request; +- the concrete bound adapter cannot be recovered through a public `lifecycle_port()` accessor; +- a second adapter cannot be substituted for create or destroy after binding; +- two successful remote create candidates in the same session incarnation receive distinct attempt epochs; +- one candidate can be accepted and the other rejected without pending-state collision or overwrite; +- accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; +- recovery, sequential-incarnation ABA, epoch exhaustion, foreign authority, destruction failure, transport loss, and normal end remain covered. -Repository contracts, canonical formatting, locked Rust tests, strict Clippy, rustdoc/API docs, exact function/line/region/branch coverage, current review findings, and applicable central checks remain required on the successor exact head. Predecessor GREEN never transfers. +The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. That exact head also still exposed raw `&P` and lacked per-create completion. Successor evidence must therefore be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. ## Buyer acceptance still open -This slice does not yet prove real WebDriver BiDi `browser.createUserContext`/`browsingContext.create`/`browser.removeUserContext` integration, pending/accepted/quarantined remote binding, browser-observed destruction, Browser Session→BiDi private-witness conversion, pinned Chromium presentation post-conditions, crash/restart cleanup, #299 3/3 Agent Task replay, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not prove real WebDriver BiDi lifecycle integration, browser-observed destruction, Browser Session→BiDi private-witness conversion, current Chromium presentation post-conditions, crash/restart reconciliation, #299 3/3 Agent Task replay, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Migration and rollback -Consumers on the active stack replace `session.create_disposable_context(&mut port)` / `session.destroy_disposable_context(..., &mut port)` with one `let mut bound = session.bind_lifecycle_port(port)` followed by bound lifecycle calls. The wrapper exposes read-only access to the aggregate and adapter for policy validation and diagnostics but does not return mutable adapter access or an unbound session. +Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` and perform lifecycle work through `BoundBrowserSession`. Code must not depend on recovering `&P`. Adapter implementations add exact-attempt staging and completion settlement. -Rollback returns to the predecessor active-PR API only if the lifecycle-port authority finding is rejected with stronger evidence; it must not restore self-reported scalar identity as a security boundary. +Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, self-reported identity, or adapter-local call order as an authorization boundary. ## Open follow-ups -- Restack #316 onto the verified Browser Session successor and adapt the WebDriver BiDi lifecycle ACL to `BoundBrowserSession` without exposing a second lifecycle side door. -- Implement protocol-specific pending → accepted/quarantined creation and complete recovery tuples in the BiDi ACL owner. -- Define separately authorized reconciliation for `BrowserSessionRecoveryEvidence`, including browser/process restart. +- Restack #316 onto the verified Browser Session successor and implement WebDriver BiDi pending → accepted/quarantined transaction settlement. +- Define separately authorized recovery reconciliation for `BrowserSessionRecoveryEvidence`. - Replay #299 historical pinned Chromium evidence after the canonical sandbox/runtime repair, then run a separate current-Stable qualification. ## Supersession / reversal conditions From aecb402fb9ecdb1d59036c408078903b699b9cc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:22:04 +0900 Subject: [PATCH 016/632] docs(browser-session): trace exact create settlement --- .../browser-session-lifecycle-authority.md | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 1f77f4afa..b0507d725 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -8,7 +8,7 @@ ## Problem and invariant -Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are addresses. They are not evidence that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. A retained authority must not regain meaning if a later aggregate reuses the same remote identifiers and local epoch, and a caller must not be able to redirect a valid lifecycle request into a second adapter instance by choosing or replaying an adapter id. +Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are addresses. They are not evidence that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. The active implementation establishes this chain: @@ -16,11 +16,14 @@ The active implementation establishes this chain: validated BrowserSessionId → BrowserSession::start allocates non-reused BrowserSessionIncarnation → BrowserSession::bind_lifecycle_port consumes one concrete DisposableContextPort -→ BoundBrowserSession

owns aggregate + exact port; binding invokes no adapter callback -→ aggregate validates Active + reserves monotonic context epoch -→ aggregate privately constructs DisposableContextCreateRequest(session, incarnation) -→ exact owned port creates task-owned isolation boundary + browsing context -→ aggregate records exact handle + epoch +→ BoundBrowserSession

owns aggregate + exact port; no public raw port accessor exists +→ aggregate validates Active + reserves monotonic BrowserContextEpoch +→ aggregate privately constructs DisposableContextCreateRequest(session, incarnation, attempt epoch) +→ exact owned port creates a remote candidate but must keep it non-authorizing +→ aggregate validates returned isolation/context against current ownership +→ aggregate privately constructs DisposableContextCreateCompletion(attempt, Accepted|Rejected) +→ accepted candidate may become adapter-authorizing; rejected candidate remains quarantined +→ aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) → exact authority validation before destroy I/O → aggregate privately constructs DisposableContextDestroyRequest(session, incarnation, stored handle) @@ -29,33 +32,39 @@ validated BrowserSessionId → normal BrowserSession end admitted ``` -`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, no mutable port accessor is exposed, and `DisposableContextPort` has no identity-preflight callback. The previous public `DisposableContextPortId`/`port_id()` design was removed because the value was self-asserted and the callback itself could have side effects before authority. +`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P` and invoke an inherent shared-reference method with interior mutation or remote I/O. -`DisposableContextCreateRequest` and `DisposableContextDestroyRequest` have private construction paths. Their getters expose only addressability needed by a reviewed adapter. A caller that knows those values cannot reconstruct the request. +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, and `DisposableContextDestroyRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. The exact attempt is settled only after Browser Session validates the returned handle. -## Lossless recovery evidence +## Transactional remote creation + +A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing yet. + +Browser Session examines the returned `DisposableContextHandle`: -`DisposableContextCreateError::CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known user-context/isolation identity as `BrowserSessionRecoveryEvidence::PartialCreationIsolation`; `None` remains representable when no identity was obtained. Both uncertain cases enter `RecoveryRequired` and mint no authority. +- if ownership validation succeeds, `DisposableContextCreateCompletion::Accepted` settles that exact attempt before normal presentation authority is returned; +- if the handle aliases an existing isolation or browsing context, `Rejected` settles that exact attempt and the aggregate enters `RecoveryRequired`; +- if exact completion cannot be proven, Browser Session stores the complete handle as `UnsettledAdapterHandle`, enters recovery, and mints no normal authority. -Duplicate browsing-context or isolation output stores the complete offending `DisposableContextHandle` as `DuplicateAdapterHandle` before recovery quarantine. Failed or unproven destruction records `UnprovenDestruction` with the exact owned handle. Recovery evidence authorizes no browser command. +Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only the attempt identity, domain validation, and accept/reject decision. -Protocol-specific complete BiDi tuples, pending → accepted/quarantined mapping, and remote target liveness remain #314/#316 responsibilities; they are not copied into Browser Session domain truth. +## Lossless recovery evidence + +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. None of this evidence grants browser command authority. ## Orthogonal transport liveness -Transport liveness is tracked independently from ownership recovery. If transport loss occurs after `RecoveryRequired`, the aggregate keeps `RecoveryRequired`, preserves all recovery evidence, and separately records `transport_lost = true`. The first loss report is observable; repeated reports are idempotent. If loss occurs while `Active`, the lifecycle state becomes `TransportLost` and active context records become uncertain. +Transport liveness is tracked independently from ownership recovery. If transport loss occurs after `RecoveryRequired`, the aggregate keeps recovery evidence and separately records `transport_lost = true`. Repeated loss reports are idempotent. ## Sequential ABA safety -Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external `S`; the browser may return the same `U/C`, and B also begins at local epoch 1. A's retained authority still fails before B adapter I/O because B has a different `BrowserSessionIncarnation`. - -The bound port receives the incarnation inside aggregate-issued create/destroy requests. The caller cannot replace the bound adapter after creation to reinterpret that current incarnation against a different adapter-local map. +Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external values and also begin at epoch 1. A's retained authority still fails because B has a different `BrowserSessionIncarnation`. The bound port receives the incarnation inside aggregate-issued lifecycle capabilities. ## Standards trace -The design dossier references the 9 September 2026 WebDriver BiDi Working Draft. A user context has a user-context id set on creation. `browser.createUserContext` creates it, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. +The design dossier references the 9 September 2026 WebDriver BiDi Working Draft. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. -OriginWeave does not turn that protocol identifier into policy authority or assume historical non-reuse after removal. `DisposableIsolationId` remains lifecycle addressability. A successful command ACK is insufficient evidence that the disposable boundary is actually gone. +OriginWeave does not treat those protocol identifiers as policy authority or assume historical non-reuse after removal. A command ACK is insufficient proof that the disposable boundary is actually gone. ## Source and executable evidence @@ -63,26 +72,27 @@ OriginWeave does not turn that protocol identifier into policy authority or assu |---|---| | independent Browser Session bounded context | `crates/originweave-browser-session/`; `tests/test_browser_session_lifecycle_contract.py` | | lifecycle port ownership is structural | `BoundBrowserSession`; `bound_port_is_structural_and_not_swappable` | +| no public raw port accessor | absence of `BoundBrowserSession::lifecycle_port`; repository contract | | binding performs no arbitrary adapter callback | `BrowserSession::bind_lifecycle_port`; `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` | -| no self-asserted adapter id authority | absence of `DisposableContextPortId` / `port_id()`; repository contract | -| create/destroy requests are aggregate-issued | `DisposableContextCreateRequest`; `DisposableContextDestroyRequest`; `aggregate_issued_request_is_reachable_only_through_owned_port_binding` | +| no self-asserted adapter id authority | absence of `DisposableContextPortId` / `port_id()` | +| create requests are aggregate-issued and attempt-scoped | `DisposableContextCreateRequest::attempt_epoch`; transaction hostile fixture | +| per-create transaction settles accepted/rejected candidates | `DisposableContextCreateCompletion`; `accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt` | +| completion failure fails closed | `UnsettledAdapterHandle`; internal completion-failure tests | | raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | -| authority includes non-reused BrowserSessionIncarnation | `PresentationMutationAuthority`; `sequential_incarnation_reuse_rejects_stale_authority` | -| lossless recovery evidence for known partial identity | `BrowserSessionRecoveryEvidence`; `creation_failure_preserves_known_recovery_identity` | -| duplicate adapter handle retained without speculative cleanup | `create_disposable_context_with_port`; `duplicate_adapter_output_preserves_offending_handle` | -| unproven destruction retains exact handle | `destroy_disposable_context_with_port`; `destroy_failure_requires_recovery_before_any_new_authority` | -| transport liveness remains orthogonal to recovery | `BrowserSession::record_transport_loss`; `destroy_failure_retains_handle_and_transport_loss_orthogonally` | -| sequential ABA authority is rejected before I/O | `BrowserSession::context_for_authority_mut`; `stale_authority_cannot_cross_sequential_session_incarnations` | -| normal end requires proved destruction | `BrowserSession::end`; `normal_end_requires_proven_destruction_and_ignores_late_transport_report` | -| incarnation exhaustion fails closed | `allocate_incarnation`; `incarnation_allocator_fails_closed_before_wrap` | +| sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | +| lossless recovery evidence | `BrowserSessionRecoveryEvidence`; recovery tests | +| unproven destruction retains exact handle | `destroy_failure_requires_recovery_before_any_new_authority` | +| transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | +| normal end requires proved destruction | `BrowserSession::end` | +| incarnation exhaustion fails closed | `allocate_incarnation` | -The pre-authority adapter-callback RED was captured on exact `d43a4d86c8487ebdb9db9f1c4650fb7ee6225afc` in CI `34524654914`: the hostile fixture observed one identity callback where zero was required. The same predecessor also retained the self-selected scalar identity defect. The bound-session successor must earn fresh exact-head formatting, tests, Clippy, rustdoc, and function/line/region/branch 100% evidence; historical GREEN does not transfer. +Exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is historical RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical GREEN never transfers. Protected-main integration is required before capability maturity can be promoted beyond `IMPLEMENTED_ON_ACTIVE_PR`. ## Buyer acceptance still open -This slice does not yet prove actual WebDriver BiDi `browser.createUserContext`/`browsingContext.create` integration, observed `browser.removeUserContext` post-condition, protocol-specific pending/accepted/quarantined binding, separately authorized recovery reconciliation, Browser Session authority conversion into BiDi presentation private witnesses, pinned Chromium post-condition observation, crash/process-restart reconciliation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, separately authorized recovery reconciliation, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, crash/process-restart reconciliation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Reference From 729603ae4feadd369eee7819a45d6850604975da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:22:28 +0900 Subject: [PATCH 017/632] docs(browser-session): model create settlement transaction --- .../browser-session-lifecycle-authority.md | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 54bd12ead..dc9010a52 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -15,19 +15,35 @@ sequenceDiagram S->>S: allocate BrowserSessionIncarnation C->>S: bind_lifecycle_port(port by value) S-->>C: BoundBrowserSession owns aggregate + exact port - Note over S,P: binding invokes no adapter callback + Note over S,P: binding invokes no adapter callback; no public raw port accessor C->>BS: create_disposable_context() BS->>S: require Active + reserve monotonic epoch - S->>S: mint DisposableContextCreateRequest + S->>S: mint DisposableContextCreateRequest(session, incarnation, attempt epoch) S->>P: create_disposable_context(request) - P->>B: create fresh isolation boundary + browsing context + P->>B: create/stage isolation boundary + browsing context B-->>P: unique isolation id + BrowsingContextId or typed create error - P-->>S: DisposableContextHandle - S->>S: register exact handle + Active epoch - S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) - - Note over C,S: Raw ids and adapter-selected scalar identities cannot mint lifecycle or presentation authority. + P-->>S: DisposableContextHandle (still pending in adapter) + + alt domain handle accepted + S->>S: validate no isolation/context alias + S->>S: mint DisposableContextCreateCompletion(Accepted, exact attempt) + S->>P: complete_disposable_context_creation(completion) + P->>P: pending exact attempt → accepted + S->>S: register exact handle + Active epoch + S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) + else domain handle rejected + S->>S: retain duplicate handle as recovery evidence + S->>S: mint DisposableContextCreateCompletion(Rejected, exact attempt) + S->>P: complete_disposable_context_creation(completion) + P->>P: pending exact attempt → quarantined/non-authorizing + S->>S: RecoveryRequired + else completion cannot be proven + S->>S: retain UnsettledAdapterHandle + S->>S: RecoveryRequired + end + + Note over C,S: Raw ids, adapter-selected values, and diagnostic references cannot mint lifecycle or presentation authority. C->>BS: advance_context_epoch(context_id) BS->>S: replace epoch; old authority becomes stale @@ -46,23 +62,24 @@ sequenceDiagram S-->>C: Ended ``` -`BoundBrowserSession` is a linear lifecycle-port binding: it consumes one concrete port and exposes no public lifecycle method that accepts a replacement port. `DisposableContextPort` has no identity callback, so Browser Session does not execute arbitrary adapter code merely to establish adapter ownership. `DisposableContextCreateRequest` and `DisposableContextDestroyRequest` are non-caller-constructible capabilities created inside the bound path. +`BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and exposes no lifecycle method that accepts a replacement port. Tests retain inert observation state separately from the moved adapter. -`BrowserSessionIncarnation` separates sequential aggregate lifecycles even when the browser later reuses the same external session, user-context/isolation, browsing-context, and local epoch values. The incarnation is checked by authority validation and reaches the lifecycle port inside the opaque request. +`DisposableContextCreateRequest` and `DisposableContextCreateCompletion` are non-caller-constructible. The create request carries the reserved `BrowserContextEpoch` as a per-create transaction id; Browser Session alone decides whether the returned domain handle is accepted or rejected. -For a WebDriver BiDi adapter, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. That protocol id remains lifecycle addressability rather than OriginWeave policy authority. Protocol-specific pending/accepted/quarantined remote tuples remain in the BiDi ACL boundary rather than this domain model. +For WebDriver BiDi, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. Protocol-specific pending/accepted/quarantined remote tuples remain in the BiDi ACL boundary rather than this domain model. ## Recovery and transport state ```mermaid stateDiagram-v2 [*] --> Active - Active --> Active: fresh isolation + context / authority minted + Active --> Active: create candidate + exact Accepted completion + authority Active --> Active: context epoch advanced / prior authority stale Active --> Active: exact owned isolation destruction proved Active --> Active: DisposableContextCreateError::CreateFailedClean Active --> RecoveryRequired: CreateFailedUncertain / retain known partial isolation - Active --> RecoveryRequired: duplicate output / retain offending handle + Active --> RecoveryRequired: duplicate output + exact Rejected completion + Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven Active --> Ended: all owned contexts Destroyed + end Active --> TransportLost: browser transport lost @@ -73,13 +90,9 @@ stateDiagram-v2 note right of RecoveryRequired BrowserSessionRecoveryEvidence retains known - partial identity, duplicate handle, or exact - unproven-destruction handle. It grants no I/O. - end note - - note right of TransportLost - Transport liveness is orthogonal to ownership - recovery. Duplicate loss reports are idempotent. + partial identity, duplicate/unsettled handle, + or exact unproven-destruction handle. + It grants no I/O. end note ``` @@ -94,18 +107,20 @@ sequenceDiagram participant PB as Lifecycle port B A->>A: start(S) => incarnation A; bind PA - A->>PA: create(request S, incarnation A) - PA-->>A: U, C + A->>PA: create(request S, incarnation A, attempt 1) + PA-->>A: U, C pending + A->>PA: completion Accepted(attempt 1) A->>PA: destroy(request S, incarnation A, U/C) A->>A: end() B->>B: start(S) => incarnation B; bind PB - B->>PB: create(request S, incarnation B) - PB-->>B: same U, same C - Note over A,B: both local context epochs may equal 1 + B->>PB: create(request S, incarnation B, attempt 1) + PB-->>B: same U, same C pending + B->>PB: completion Accepted(attempt 1) + Note over A,B: local attempt/epoch may both equal 1, but incarnations differ B->>B: validate retained authority A B-->>A: AuthorityMismatch before PB destroy I/O B->>PB: destroy with authority B + incarnation B ``` -`RecoveryRequired` and `TransportLost` remain terminal for normal authority in this slice. A later reconciliation design may inspect `BrowserSessionRecoveryEvidence`, but it must not reconstruct cleanup authority from raw identifiers or treat command ACK as proof of destruction. +`RecoveryRequired` and `TransportLost` remain terminal for normal authority in this slice. Later reconciliation may inspect recovery evidence, but it must not reconstruct cleanup authority from raw identifiers or treat command ACK as proof of destruction. From b161a8a6ecb40853705db2878bf30ef911d3135c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 13:04:28 +0900 Subject: [PATCH 018/632] docs(browser-session): trace destroy failure contract --- docs/adr/0114-browser-session-disposable-context-authority.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 1dc178b96..ba5e3d3c8 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -47,7 +47,7 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. 14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. 15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, an unsettled complete adapter handle when completion itself cannot be proven, and exact unproven-destruction handle. Recovery evidence grants no browser command authority. -16. Destruction validates exact authority before I/O. Unproven destruction moves the aggregate into recovery and retains the exact failed handle. +16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. 17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; repeated reports are idempotent. 18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. 19. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. @@ -98,6 +98,7 @@ Required executable cases include: - two successful remote create candidates in the same session incarnation receive distinct attempt epochs; - one candidate can be accepted and the other rejected without pending-state collision or overwrite; - accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; +- `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; - recovery, sequential-incarnation ABA, epoch exhaustion, foreign authority, destruction failure, transport loss, and normal end remain covered. The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. That exact head also still exposed raw `&P` and lacked per-create completion. Successor evidence must therefore be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. From b721f6a161ae020c30c82ef93a6dc5f8a666f952 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 14:02:17 +0900 Subject: [PATCH 019/632] test(browser-session): retain transport-loss recovery handle --- .../tests/transport_loss_recovery_evidence.rs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs diff --git a/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs new file mode 100644 index 000000000..0dda28a91 --- /dev/null +++ b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs @@ -0,0 +1,111 @@ +use std::cell::Cell; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionState, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct ObservedPort { + create_calls: Rc>, + completion_calls: Rc>, + destroy_calls: Rc>, +} + +impl DisposableContextPort for ObservedPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_calls.set(self.create_calls.get() + 1); + Ok(DisposableContextHandle::new( + DisposableIsolationId::parse("transport-user-context-501") + .expect("valid isolation id"), + BrowsingContextId::new(501).expect("valid browsing context"), + )) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + self.completion_calls.set(self.completion_calls.get() + 1); + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls.set(self.destroy_calls.get() + 1); + Ok(()) + } +} + +#[test] +fn transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence() { + let create_calls = Rc::new(Cell::new(0)); + let completion_calls = Rc::new(Cell::new(0)); + let destroy_calls = Rc::new(Cell::new(0)); + let port = ObservedPort { + create_calls: Rc::clone(&create_calls), + completion_calls: Rc::clone(&completion_calls), + destroy_calls: Rc::clone(&destroy_calls), + }; + let session = BrowserSession::start(BrowserSessionId::new(501).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port); + + let authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + assert_eq!(authority.browsing_context().value(), 501); + assert_eq!(create_calls.get(), 1); + assert_eq!(completion_calls.get(), 1); + assert_eq!(destroy_calls.get(), 0); + + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert_eq!(create_calls.get(), 1, "transport loss must not create browser state"); + assert_eq!( + completion_calls.get(), + 1, + "transport loss must not settle another create attempt" + ); + assert_eq!(destroy_calls.get(), 0, "transport loss is not destruction proof"); + + let evidence = bound.browser_session().recovery_evidence(); + assert_eq!( + evidence.len(), + 1, + "the exact previously owned handle must remain externally recoverable after transport loss" + ); + let rendered = format!("{:?}", evidence[0]); + assert!( + rendered.contains("transport-user-context-501"), + "recovery evidence lost the exact disposable isolation identity: {rendered}" + ); + assert!( + rendered.contains("501"), + "recovery evidence lost the exact browsing-context identity: {rendered}" + ); + + assert!(!bound.record_transport_loss()); + assert_eq!( + bound.browser_session().recovery_evidence().len(), + 1, + "repeated transport-loss reports must not duplicate recovery evidence" + ); + assert_eq!( + bound.presentation_authority(authority.browsing_context()), + Err(originweave_browser_session::BrowserSessionError::SessionNotActive), + "transport-loss recovery evidence must never resurrect presentation authority" + ); + assert_eq!(destroy_calls.get(), 0); +} From 89706a1bef8fb0c99eb2a4cd4b305860bc56ffb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 14:02:37 +0900 Subject: [PATCH 020/632] test(browser-session): reject adapter Debug capability escape --- .../tests/bound_session_debug_redaction.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 crates/originweave-browser-session/tests/bound_session_debug_redaction.rs diff --git a/crates/originweave-browser-session/tests/bound_session_debug_redaction.rs b/crates/originweave-browser-session/tests/bound_session_debug_redaction.rs new file mode 100644 index 000000000..edbd99a1c --- /dev/null +++ b/crates/originweave-browser-session/tests/bound_session_debug_redaction.rs @@ -0,0 +1,68 @@ +use std::cell::Cell; +use std::fmt; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, +}; +use originweave_core::BrowserSessionId; + +struct SideEffectingDebugPort { + debug_callbacks: Rc>, +} + +impl fmt::Debug for SideEffectingDebugPort { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.debug_callbacks + .set(self.debug_callbacks.get().saturating_add(1)); + formatter.write_str("adapter-secret-sentinel") + } +} + +impl DisposableContextPort for SideEffectingDebugPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + Err(DisposableContextCreateError::CreateFailedClean) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +#[test] +fn bound_session_debug_never_executes_or_exposes_adapter_debug() { + let debug_callbacks = Rc::new(Cell::new(0)); + let port = SideEffectingDebugPort { + debug_callbacks: Rc::clone(&debug_callbacks), + }; + let session = BrowserSession::start(BrowserSessionId::new(502).expect("valid session id")) + .expect("incarnation capacity"); + let bound = session.bind_lifecycle_port(port); + + let rendered = format!("{bound:?}"); + + assert_eq!( + debug_callbacks.get(), + 0, + "formatting a bound session must not execute adapter-owned Debug code" + ); + assert!( + !rendered.contains("adapter-secret-sentinel"), + "bound-session diagnostics must not expose adapter-internal state" + ); +} From bd662351f997ae330db7b7954e64a48cd207da6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:02:58 +0900 Subject: [PATCH 021/632] fix(browser-session): preserve transport recovery evidence --- crates/originweave-browser-session/src/lib.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 7c8632a7d..56bfc97ae 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -8,6 +8,7 @@ #![deny(missing_docs)] use std::collections::BTreeMap; +use std::fmt; use std::sync::atomic::{AtomicU64, Ordering}; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -178,6 +179,8 @@ pub enum BrowserSessionRecoveryEvidence { UnsettledAdapterHandle(DisposableContextHandle), /// Destruction of this exact owned handle failed or could not be proven. UnprovenDestruction(DisposableContextHandle), + /// Transport loss made this previously active owned handle uncertain. + TransportLossOwnedHandle(DisposableContextHandle), } /// Opaque Browser Session-issued request for one disposable-context creation attempt. @@ -430,12 +433,21 @@ pub struct BrowserSession { /// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and /// no public Browser Session lifecycle method accepts an arbitrary port parameter. This makes adapter /// ownership structural rather than dependent on a caller-selected scalar or an adapter callback. -#[derive(Debug)] pub struct BoundBrowserSession

{ session: BrowserSession, port: P, } +impl

fmt::Debug for BoundBrowserSession

{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("BoundBrowserSession") + .field("session", &self.session) + .field("port", &"") + .finish() + } +} + impl BrowserSession { /// Start an active Browser Session around an already validated transport session identity. /// @@ -555,6 +567,16 @@ impl BrowserSession { } self.transport_lost = true; if self.state == BrowserSessionState::Active { + self.recovery_evidence.extend( + self.contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| { + BrowserSessionRecoveryEvidence::TransportLossOwnedHandle( + record.handle.clone(), + ) + }), + ); self.state = BrowserSessionState::TransportLost; self.mark_active_contexts_uncertain(); } From f660ec8ec068e6d183502896f34f189b5a5e94b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:08:16 +0900 Subject: [PATCH 022/632] fix(browser-session): bind authorized operations and abandonment --- crates/originweave-browser-session/src/lib.rs | 150 +++++++++++++++++- .../tests/authorized_context_operation.rs | 129 +++++++++++++++ .../tests/bound_session_abandonment.rs | 92 +++++++++++ 3 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 crates/originweave-browser-session/tests/authorized_context_operation.rs create mode 100644 crates/originweave-browser-session/tests/bound_session_abandonment.rs diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 56bfc97ae..1a2e49b6b 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -14,6 +14,17 @@ use std::sync::atomic::{AtomicU64, Ordering}; use originweave_core::{BrowserSessionId, BrowsingContextId}; static NEXT_BROWSER_SESSION_INCARNATION: AtomicU64 = AtomicU64::new(1); +static ABANDONED_BOUND_SESSIONS: AtomicU64 = AtomicU64::new(0); + +/// Return the number of bound Browser Sessions abandoned with unresolved remote ownership. +/// +/// This is a process-local, non-I/O operability signal. It deliberately does not claim that remote +/// browser cleanup happened and is not a substitute for persisting exact recovery evidence before a +/// process exits. +#[must_use] +pub fn abandoned_bound_session_count() -> u64 { + ABANDONED_BOUND_SESSIONS.load(Ordering::Relaxed) +} /// Current lifecycle state of one Browser Session aggregate. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -343,6 +354,74 @@ pub trait DisposableContextPort { ) -> Result<(), DisposableContextDestroyError>; } +/// Opaque aggregate-authorized request for one purpose-bounded adapter operation. +/// +/// The caller supplies only the adapter-defined operation value. Browser Session validates the +/// accompanying presentation authority first and privately binds the operation to the exact owned +/// context before the consumed adapter can observe it. There is deliberately no public constructor. +pub struct AuthorizedContextOperationRequest { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + context: DisposableContextHandle, + operation: O, +} + +impl AuthorizedContextOperationRequest { + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle correlation. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the exact currently owned context validated before adapter I/O. + #[must_use] + pub const fn context(&self) -> &DisposableContextHandle { + &self.context + } + + /// Return the adapter-defined purpose-bounded operation payload. + #[must_use] + pub const fn operation(&self) -> &O { + &self.operation + } +} + +/// Failure from executing an aggregate-authorized operation through the consumed adapter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthorizedContextOperationError { + /// Browser Session rejected the authority before adapter I/O. + BrowserSession(BrowserSessionError), + /// The bound adapter attempted the authorized operation and returned its bounded failure. + Adapter(E), +} + +/// Adapter extension for purpose-bounded operations that must use the exact consumed adapter. +/// +/// Browser Session remains protocol-agnostic: the adapter owns the operation, output, and error +/// types. The wrapper only proves current ownership and routes the opaque request to the same concrete +/// adapter instance used for lifecycle creation and destruction. Implementations must not treat the +/// request as permission to mutate any other context. +pub trait AuthorizedContextOperationPort: DisposableContextPort { + /// Adapter-defined operation vocabulary, such as a reviewed BiDi presentation command. + type Operation; + /// Adapter-defined successful result. + type Output; + /// Adapter-defined bounded operation failure. + type Error; + + /// Execute one aggregate-authorized operation against the exact context carried by the request. + fn execute_authorized_context_operation( + &mut self, + request: &AuthorizedContextOperationRequest, + ) -> Result; +} + /// Monotonic identity for one owned browsing-context authority epoch. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BrowserContextEpoch(u64); @@ -433,6 +512,7 @@ pub struct BrowserSession { /// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and /// no public Browser Session lifecycle method accepts an arbitrary port parameter. This makes adapter /// ownership structural rather than dependent on a caller-selected scalar or an adapter callback. +#[must_use = "destroy owned browser state and finish the session, or hand unresolved ownership to recovery"] pub struct BoundBrowserSession

{ session: BrowserSession, port: P, @@ -442,12 +522,29 @@ impl

fmt::Debug for BoundBrowserSession

{ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("BoundBrowserSession") - .field("session", &self.session) + .field("browser_session", &self.session.id) + .field("incarnation", &self.session.incarnation) + .field("state", &self.session.state) + .field("transport_lost", &self.session.transport_lost) + .field("owned_context_count", &self.session.contexts.len()) + .field("recovery_evidence_count", &self.session.recovery_evidence.len()) .field("port", &"") .finish() } } +impl

Drop for BoundBrowserSession

{ + fn drop(&mut self) { + if self.session.has_unresolved_remote_ownership() { + let _ = ABANDONED_BOUND_SESSIONS.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |value| Some(value.saturating_add(1)), + ); + } + } +} + impl BrowserSession { /// Start an active Browser Session around an already validated transport session identity. /// @@ -778,6 +875,18 @@ impl BrowserSession { } } } + + fn has_unresolved_remote_ownership(&self) -> bool { + matches!( + self.state, + BrowserSessionState::TransportLost | BrowserSessionState::RecoveryRequired + ) || self.contexts.values().any(|record| { + matches!( + record.state, + OwnedContextState::Active | OwnedContextState::Uncertain + ) + }) + } } impl BoundBrowserSession

{ @@ -829,6 +938,45 @@ impl BoundBrowserSession

{ pub fn end(&mut self) -> Result<(), BrowserSessionError> { self.session.end() } + + /// Consume the bound session after verifying that every owned context has proven destruction. + /// + /// Failure consumes the wrapper as well; its non-I/O `Drop` fail-safe records abandonment when + /// unresolved ownership remains instead of pretending remote cleanup succeeded. + pub fn finish(mut self) -> Result<(), BrowserSessionError> { + self.session.end() + } +} + +impl BoundBrowserSession

{ + /// Execute one adapter-defined operation through the exact consumed adapter after authority validation. + /// + /// Browser Session validates session incarnation, isolation identity, browsing-context identity, + /// and epoch before the adapter receives the operation. Stale or foreign authority therefore fails + /// before adapter I/O, while the adapter-specific operation vocabulary remains outside this domain. + pub fn execute_authorized_context_operation( + &mut self, + authority: &PresentationMutationAuthority, + operation: P::Operation, + ) -> Result> { + let browser_session = self.session.id; + let incarnation = self.session.incarnation; + let context = self + .session + .context_for_authority_mut(authority) + .map_err(AuthorizedContextOperationError::BrowserSession)? + .handle + .clone(); + let request = AuthorizedContextOperationRequest { + browser_session, + incarnation, + context, + operation, + }; + self.port + .execute_authorized_context_operation(&request) + .map_err(AuthorizedContextOperationError::Adapter) + } } fn reserve_epoch(next_epoch: &mut u64) -> Result { diff --git a/crates/originweave-browser-session/tests/authorized_context_operation.rs b/crates/originweave-browser-session/tests/authorized_context_operation.rs new file mode 100644 index 000000000..6c139cf7b --- /dev/null +++ b/crates/originweave-browser-session/tests/authorized_context_operation.rs @@ -0,0 +1,129 @@ +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +use originweave_browser_session::{ + AuthorizedContextOperationError, AuthorizedContextOperationPort, + AuthorizedContextOperationRequest, BrowserSession, BrowserSessionError, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct OperationPort { + handle: Option, + operation_calls: Rc>, + observed_operations: Rc>>, + fail_operation: Rc>, +} + +impl DisposableContextPort for OperationPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.handle + .take() + .ok_or(DisposableContextCreateError::CreateFailedClean) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +impl AuthorizedContextOperationPort for OperationPort { + type Operation = &'static str; + type Output = BrowsingContextId; + type Error = (); + + fn execute_authorized_context_operation( + &mut self, + request: &AuthorizedContextOperationRequest, + ) -> Result { + self.operation_calls.set(self.operation_calls.get() + 1); + self.observed_operations + .borrow_mut() + .push(*request.operation()); + if self.fail_operation.get() { + Err(()) + } else { + Ok(request.context().browsing_context()) + } + } +} + +#[test] +fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io() { + let operation_calls = Rc::new(Cell::new(0)); + let observed_operations = Rc::new(RefCell::new(Vec::new())); + let fail_operation = Rc::new(Cell::new(false)); + let context = BrowsingContextId::new(503).expect("valid browsing context"); + let port = OperationPort { + handle: Some(DisposableContextHandle::new( + DisposableIsolationId::parse("operation-user-context-503") + .expect("valid isolation id"), + context, + )), + operation_calls: Rc::clone(&operation_calls), + observed_operations: Rc::clone(&observed_operations), + fail_operation: Rc::clone(&fail_operation), + }; + let session = BrowserSession::start(BrowserSessionId::new(503).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port); + + let authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + assert_eq!( + bound.execute_authorized_context_operation(&authority, "set-viewport"), + Ok(context) + ); + assert_eq!(operation_calls.get(), 1); + assert_eq!(observed_operations.borrow().as_slice(), &["set-viewport"]); + + fail_operation.set(true); + assert_eq!( + bound.execute_authorized_context_operation(&authority, "remote-failure"), + Err(AuthorizedContextOperationError::Adapter(())) + ); + assert_eq!(operation_calls.get(), 2); + fail_operation.set(false); + + let current = bound + .advance_context_epoch(context) + .expect("advance authority epoch"); + assert_eq!( + bound.execute_authorized_context_operation(&authority, "stale-operation"), + Err(AuthorizedContextOperationError::BrowserSession( + BrowserSessionError::AuthorityMismatch + )) + ); + assert_eq!( + operation_calls.get(), + 2, + "stale authority must fail before the bound adapter observes an operation" + ); + + assert_eq!( + bound.execute_authorized_context_operation(¤t, "reconcile-liveness"), + Ok(context) + ); + assert_eq!(operation_calls.get(), 3); + assert_eq!( + observed_operations.borrow().as_slice(), + &["set-viewport", "remote-failure", "reconcile-liveness"] + ); +} diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs new file mode 100644 index 000000000..b540ba6b7 --- /dev/null +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -0,0 +1,92 @@ +use std::cell::Cell; +use std::rc::Rc; + +use originweave_browser_session::{ + abandoned_bound_session_count, BrowserSession, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct AbandonmentPort { + handle: Option, + destroy_calls: Rc>, +} + +impl DisposableContextPort for AbandonmentPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.handle + .take() + .ok_or(DisposableContextCreateError::CreateFailedClean) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls.set(self.destroy_calls.get() + 1); + Ok(()) + } +} + +fn port_for(context: u64, destroy_calls: &Rc>) -> AbandonmentPort { + AbandonmentPort { + handle: Some(DisposableContextHandle::new( + DisposableIsolationId::parse(&format!("abandoned-user-context-{context}")) + .expect("valid isolation id"), + BrowsingContextId::new(context).expect("valid browsing context"), + )), + destroy_calls: Rc::clone(destroy_calls), + } +} + +#[test] +fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() { + let destroy_calls = Rc::new(Cell::new(0)); + let before = abandoned_bound_session_count(); + let session = BrowserSession::start(BrowserSessionId::new(504).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port_for(504, &destroy_calls)); + let _authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + + drop(bound); + + assert_eq!( + destroy_calls.get(), + 0, + "Drop must never pretend synchronous browser cleanup succeeded" + ); + assert!( + abandoned_bound_session_count() > before, + "unresolved bound-session abandonment must be observable to recovery/operability code" + ); +} + +#[test] +fn proven_destruction_can_finish_without_abandonment_path() { + let destroy_calls = Rc::new(Cell::new(0)); + let session = BrowserSession::start(BrowserSessionId::new(505).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port_for(505, &destroy_calls)); + let authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + bound + .destroy_disposable_context(&authority) + .expect("proven destruction"); + bound.finish().expect("consume normally ended bound session"); + assert_eq!(destroy_calls.get(), 1); +} From 6bf2c0df0d2241af86c1afd709cd9c9e11d95d1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:12:04 +0900 Subject: [PATCH 023/632] docs(browser-session): trace authorized operation and recovery boundaries --- ...er-session-disposable-context-authority.md | 74 +++-- .../browser-session-lifecycle-authority.md | 51 +++- .../browser-session-lifecycle-authority.md | 67 ++++- ...test_browser_session_lifecycle_contract.py | 265 +++++++++++------- 4 files changed, 303 insertions(+), 154 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index ba5e3d3c8..68b742eba 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -2,16 +2,15 @@ - Status: Proposed - Date: 2026-09-10 +- Last code-current review: 2026-09-11 ## Context -OriginWeave's Browser Session bounded context is the authority for disposable browser lifecycle ownership and presentation mutation. WebDriver BiDi session ids, user-context ids, browsing-context ids, and adapter-selected values are protocol addressability, not authorization. +OriginWeave's Browser Session bounded context is the domain authority for disposable browser lifecycle ownership and presentation mutation. WebDriver BiDi session ids, user-context ids, browsing-context ids, and adapter-selected values are protocol addressability, not authorization. -Two active-PR findings refine the lifecycle-port boundary. First, `BoundBrowserSession::lifecycle_port(&self) -> &P` exposed the concrete adapter after binding. Rust shared references do not prove purity: interior mutability, synchronization primitives, or an internally synchronized client can still mutate local state or perform remote I/O. A "read-only" label therefore does not create a security boundary. +The active implementation has to satisfy four constraints at once. First, `BoundBrowserSession

` must consume the one concrete lifecycle adapter without later exposing raw `&P`/`&mut P` or a replacement-port path. Second, one Browser Session incarnation can issue multiple remote creates, so each result requires an aggregate-issued per-create transaction identity before it may become authorizing. Third, dependent WebDriver BiDi presentation and reconciliation work still needs to reach the same consumed adapter after exact `PresentationMutationAuthority` validation; retaining a second adapter or generic raw callback would recreate the capability-substitution defect. Fourth, uncertain lifecycle outcomes must preserve exact non-authorizing recovery evidence and ordinary wrapper abandonment must be observable without pretending that Rust `Drop` proves remote cleanup. -Second, one Browser Session incarnation can issue multiple remote create attempts. A create request carrying only `(BrowserSessionId, BrowserSessionIncarnation)` does not identify which returned protocol tuple Browser Session later accepted or rejected. A WebDriver BiDi adapter needs an exact **per-create transaction** so it can stage remote state as pending, promote only the accepted candidate, and quarantine the rejected candidate without relying on call order or adapter-local counters as authority. - -Lifecycle failures still require lossless evidence. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. Transport liveness remains orthogonal to ownership certainty. +Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. Transport liveness remains orthogonal to ownership certainty. The 9 September 2026 WebDriver BiDi Working Draft defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. @@ -24,6 +23,9 @@ The 9 September 2026 WebDriver BiDi Working Draft defines `browser.createUserCon - Every successful remote create result must be correlated to one exact Browser Session-issued attempt before it can become authorizing. - Browser Session, not the adapter, decides whether a returned domain handle is accepted or rejected. - Protocol-specific pending/accepted/quarantined tuples remain the WebDriver BiDi ACL owner's truth. +- Presentation/reconciliation I/O must use the exact consumed adapter only after current aggregate authority validation. +- Diagnostic formatting must not invoke adapter-owned `Debug` or expose adapter-internal state. +- Silent loss of active/uncertain ownership on ordinary `BoundBrowserSession` drop must be observable without performing browser I/O from `Drop`. - Sequential aggregate recreation must not make retained stale authority valid again. - Recovery evidence and transport liveness remain orthogonal. - Browser Session remains the domain authority; WebDriver BiDi, CDP, MCP, and LLMs remain adapters or consumers. @@ -37,20 +39,24 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 3. Browser Session uses a **linear lifecycle-port binding**. `BrowserSession::bind_lifecycle_port` consumes both aggregate and one concrete `DisposableContextPort` into `BoundBrowserSession

` without invoking adapter code. 4. `BoundBrowserSession

` has **no public raw port accessor** and no lifecycle method that accepts an alternate port. Tests observe adapter behavior through independently retained inert counters/ledgers rather than extracting `&P`. 5. `DisposableContextPort` has no identity-preflight method. Adapter identity is structural composition, not a self-asserted scalar. -6. `DisposableContextCreateRequest` remains opaque and gains the already-reserved `BrowserContextEpoch` as an exact create-attempt identity. The `(session, incarnation, attempt epoch)` tuple is unique for create attempts in one live aggregate and is not caller-constructible as a request. +6. `DisposableContextCreateRequest` remains opaque and carries the already-reserved `BrowserContextEpoch` as an exact per-create transaction identity. The `(session, incarnation, attempt epoch)` tuple is unique for create attempts in one live aggregate and is not caller-constructible as a request. 7. After `create_disposable_context` returns a handle, Browser Session validates isolation and browsing-context ownership before granting authority. -8. Browser Session then privately issues `DisposableContextCreateCompletion` for that exact attempt with `Accepted` or `Rejected`. +8. Browser Session privately issues `DisposableContextCreateCompletion` for that exact attempt with `Accepted` or `Rejected`. 9. The adapter must keep a successful remote create result non-authorizing until the matching `Accepted` completion. `Rejected` results remain non-authorizing recovery/quarantine state. A completion that cannot be proven for the exact pending attempt fails closed and sends the aggregate to `RecoveryRequired`. 10. Protocol-specific remote tuple contents are not copied into Browser Session. #314/#316 owns WebDriver BiDi pending/accepted/quarantined storage and remote-liveness validation. 11. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, and the exact stored handle. -12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before destruction I/O. +12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. 13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. 14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. -15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, an unsettled complete adapter handle when completion itself cannot be proven, and exact unproven-destruction handle. Recovery evidence grants no browser command authority. +15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Evidence grants no browser command authority. Repeated transport-loss reports are idempotent. 16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. -17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records the fact even after `RecoveryRequired`; repeated reports are idempotent. +17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. 18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. -19. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. +19. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. +20. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. +21. `BoundBrowserSession

` is `#[must_use]` and provides consuming `finish()`, which admits normal completion only after all owned contexts have proven destruction. `Drop` never performs browser I/O. If unresolved remote ownership remains, `Drop` increments the process-local `abandoned_bound_session_count()` operability signal. +22. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery handles must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. +23. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. ## Alternatives considered @@ -62,29 +68,47 @@ Rejected. A scalar can be replayed and a shared-reference callback can still hav Rejected. Rust `&P` forbids an ordinary mutable borrow but does not prohibit interior mutation or remote effects from `&self` methods. The concrete adapter would remain a capability escape. +### `#[derive(Debug)]` over `BoundBrowserSession

` + +Rejected. Derived formatting delegates to `P::fmt`; a side-effecting or secret-bearing adapter `Debug` becomes an authority/data-exposure escape. Manual redacted formatting is selected. + +### Generic `FnOnce(&mut P)` callback + +Rejected. Although it would reach the exact consumed adapter, it hands unrestricted adapter authority back to callers and defeats the anti-corruption boundary. A typed `AuthorizedContextOperationPort` operation is selected instead. + +### Second retained adapter or shared client outside `BoundBrowserSession` + +Rejected. It recreates same-key/different-adapter target redirection and lets presentation/reconciliation work escape the exact lifecycle instance Browser Session accepted. + ### Adapter-local create sequence number -Rejected as authority. It could be useful internally, but Browser Session could not prove which pending remote tuple it was accepting. Correlation must originate in the aggregate-issued request. +Rejected as authority. It may be useful internally, but Browser Session could not prove which pending remote tuple it was accepting. Correlation must originate in the aggregate-issued request. ### Reserved BrowserContextEpoch as create-attempt identity Selected. Browser Session already reserves the epoch before create I/O, it is non-caller-constructible, monotonic within the aggregate, and the same value becomes the accepted context's first mutation epoch. +### Browser I/O from `Drop` + +Rejected. Rust destruction is synchronous and cannot prove remote cleanup. `Drop` is restricted to non-I/O abandonment observability; normal completion is explicit through proven destruction plus `finish()`. + ### Automatically clean duplicate or rejected state Rejected. Ambiguous ownership makes speculative cleanup a potential cross-owner destructive action. ## Consequences -The active stack receives a breaking trait change: every `DisposableContextPort` implementation must settle successful create results through `complete_disposable_context_creation`. #316 must restack non-force and map this completion into its adapter-local pending/accepted/quarantined state. +The active stack receives a breaking trait extension for presentation/reconciliation adapters: implementations that need post-create authorized operations implement `AuthorizedContextOperationPort` and keep their protocol-specific command vocabulary in the adapter. #316 must restack non-force and map this boundary into its pending/accepted/quarantined BiDi state. -The bound adapter is no longer publicly recoverable from `BoundBrowserSession`. Application and test code that needs observability must retain inert metrics or diagnostic projections separately; those projections must not expose adapter command capability. +The bound adapter is not publicly recoverable from `BoundBrowserSession`. Application and test code that needs observability retains inert metrics or diagnostic projections separately. Manual `Debug` exposes only Browser Session domain summary fields and a redacted port marker. -A completion failure is treated as ownership uncertainty. Browser Session retains the returned handle as recovery evidence and does not mint normal authority. +Transport loss now preserves exact previously active handles as non-authorizing recovery evidence. Completion or destruction failure remains ownership uncertainty and does not mint normal authority. + +Ordinary unresolved wrapper abandonment is process-locally observable, but exact crash/restart recovery still requires a canonical persistence/handoff path. This ADR does not claim that the in-memory counter is durable recovery. ## Security and governance impact -No page-controlled value, raw browser identifier, adapter-selected scalar, diagnostic reference, provider/model decision, or LLM output can mint lifecycle completion or presentation authority. Remote creation stays non-authorizing until the aggregate validates ownership and accepts that exact attempt. +No page-controlled value, raw browser identifier, adapter-selected scalar, diagnostic reference, provider/model decision, or LLM output can mint lifecycle completion or presentation authority. Remote creation stays non-authorizing until the aggregate validates ownership and accepts that exact attempt. Post-create adapter I/O is admitted only through current aggregate authority and the exact consumed adapter instance. This decision does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. @@ -99,24 +123,30 @@ Required executable cases include: - one candidate can be accepted and the other rejected without pending-state collision or overwrite; - accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; - `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; +- transport loss preserves every previously active exact handle as `TransportLossOwnedHandle` without adapter I/O or authority resurrection; +- formatting a bound session does not invoke adapter-owned `Debug` and does not expose adapter-internal state; +- an authorized operation reaches the exact consumed adapter, adapter errors remain typed, and stale authority fails before adapter I/O; +- dropping a bound session with unresolved ownership performs no implicit browser cleanup and increments the abandonment operability signal; proven destruction followed by `finish()` is the normal consuming path; - recovery, sequential-incarnation ABA, epoch exhaustion, foreign authority, destruction failure, transport loss, and normal end remain covered. -The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. That exact head also still exposed raw `&P` and lacked per-create completion. Successor evidence must therefore be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. +The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. The historical `729603ae4feadd369eee7819a45d6850604975da` run `34541860394` passed exact production coverage but failed the repository contract because ADR 0114 had lost the `DisposableContextDestroyError` trace. Historical GREEN never transfers. Successor evidence must be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. ## Buyer acceptance still open -This slice does not prove real WebDriver BiDi lifecycle integration, browser-observed destruction, Browser Session→BiDi private-witness conversion, current Chromium presentation post-conditions, crash/restart reconciliation, #299 3/3 Agent Task replay, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not yet prove actual WebDriver BiDi lifecycle integration, browser-observed destruction, protocol-specific pending/accepted/quarantined binding, durable crash/process-restart recovery handoff, Browser Session authority conversion into BiDi presentation private witnesses, current Chromium post-condition observation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Migration and rollback -Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` and perform lifecycle work through `BoundBrowserSession`. Code must not depend on recovering `&P`. Adapter implementations add exact-attempt staging and completion settlement. +Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` and perform lifecycle work through `BoundBrowserSession`. Code must not depend on recovering `&P`. Adapter implementations add exact-attempt staging/completion and, when they need post-create presentation or reconciliation I/O, implement the typed `AuthorizedContextOperationPort` operation vocabulary. + +Normal owners destroy every owned context and consume the wrapper with `finish()`. Recovery owners must persist exact recovery evidence before terminating a process that still has unresolved ownership; the abandonment counter is an operability alert, not a persistence mechanism. -Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, self-reported identity, or adapter-local call order as an authorization boundary. +Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, or adapter-local call order as an authorization boundary. ## Open follow-ups -- Restack #316 onto the verified Browser Session successor and implement WebDriver BiDi pending → accepted/quarantined transaction settlement. -- Define separately authorized recovery reconciliation for `BrowserSessionRecoveryEvidence`. +- Restack #316 onto the verified Browser Session successor and implement WebDriver BiDi pending → accepted/quarantined transaction settlement plus typed presentation/reconciliation operations. +- Define the separately authorized durable recovery persistence/reconciliation owner for `BrowserSessionRecoveryEvidence` and unresolved abandonment. - Replay #299 historical pinned Chromium evidence after the canonical sandbox/runtime repair, then run a separate current-Stable qualification. ## Supersession / reversal conditions diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index b0507d725..498e073fb 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -25,16 +25,19 @@ validated BrowserSessionId → accepted candidate may become adapter-authorizing; rejected candidate remains quarantined → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) -→ exact authority validation before destroy I/O -→ aggregate privately constructs DisposableContextDestroyRequest(session, incarnation, stored handle) -→ exact owned port proves destruction -→ context Destroyed -→ normal BrowserSession end admitted +→ exact authority validation before any lifecycle or purpose-bounded adapter I/O +→ lifecycle destruction uses private DisposableContextDestroyRequest +→ presentation/reconciliation uses private AuthorizedContextOperationRequest +→ exact consumed adapter only +→ proven destruction for every context +→ BoundBrowserSession::finish() consumes the normal lifecycle ``` -`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P` and invoke an inherent shared-reference method with interior mutation or remote I/O. +`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P`, `&mut P`, or a generic callback that would recreate unrestricted adapter authority. -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, and `DisposableContextDestroyRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. The exact attempt is settled only after Browser Session validates the returned handle. +The wrapper has a manual redacted `Debug` implementation. Formatting exposes inert Browser Session summary fields only and never calls `P::fmt`, so a side-effecting or secret-bearing adapter `Debug` cannot become a diagnostic capability escape. + +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. Purpose-bounded operations are constructed only after exact `PresentationMutationAuthority` validation. ## Transactional remote creation @@ -46,15 +49,29 @@ Browser Session examines the returned `DisposableContextHandle`: - if the handle aliases an existing isolation or browsing context, `Rejected` settles that exact attempt and the aggregate enters `RecoveryRequired`; - if exact completion cannot be proven, Browser Session stores the complete handle as `UnsettledAdapterHandle`, enters recovery, and mints no normal authority. -Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only the attempt identity, domain validation, and accept/reject decision. +Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only attempt identity, domain validation, accept/reject decision, and current authority validation. + +## Same-bound-adapter authorized operations + +`AuthorizedContextOperationPort` extends the lifecycle port for adapters that need post-create presentation or reconciliation work. The operation/output/error vocabulary remains adapter-owned. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. + +Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O. An operation attempted by the exact bound adapter can return `AuthorizedContextOperationError::Adapter`. No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. + +## Lossless recovery evidence while retained + +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. Transport loss records each previously active exact handle as `TransportLossOwnedHandle` before marking it uncertain. None of this evidence grants browser command authority. + +Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. + +## Abandonment and lifecycle completion -## Lossless recovery evidence +`BoundBrowserSession

` is `#[must_use]`. The normal consuming path is `finish()`, which succeeds only after all owned contexts have proven destruction. `Drop` never performs browser I/O and never treats object destruction as browser destruction proof. -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. None of this evidence grants browser command authority. +Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact crash/process-restart recovery therefore remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. ## Orthogonal transport liveness -Transport liveness is tracked independently from ownership recovery. If transport loss occurs after `RecoveryRequired`, the aggregate keeps recovery evidence and separately records `transport_lost = true`. Repeated loss reports are idempotent. +Transport liveness is tracked independently from ownership recovery. A first transport loss from `Active` preserves exact active handles as recovery evidence, moves those records to uncertain, and moves the aggregate to `TransportLost`. If transport loss occurs after `RecoveryRequired`, the stronger ownership-recovery state remains while `transport_lost = true` records the orthogonal fact. Repeated loss reports are idempotent. ## Sequential ABA safety @@ -79,20 +96,26 @@ OriginWeave does not treat those protocol identifiers as policy authority or ass | per-create transaction settles accepted/rejected candidates | `DisposableContextCreateCompletion`; `accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt` | | completion failure fails closed | `UnsettledAdapterHandle`; internal completion-failure tests | | raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | +| same consumed adapter handles authorized post-create work | `AuthorizedContextOperationPort`; `authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io` | +| stale operation authority fails before adapter I/O | `AuthorizedContextOperationError::BrowserSession`; authorized-operation hostile fixture | +| adapter-owned Debug is not executed or rendered | manual `Debug for BoundBrowserSession

`; `bound_session_debug_never_executes_or_exposes_adapter_debug` | | sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | -| lossless recovery evidence | `BrowserSessionRecoveryEvidence`; recovery tests | +| lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; recovery tests | +| transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | | unproven destruction retains exact handle | `destroy_failure_requires_recovery_before_any_new_authority` | +| unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | +| normal consuming completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | | transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | | normal end requires proved destruction | `BrowserSession::end` | | incarnation exhaustion fails closed | `allocate_incarnation` | -Exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is historical RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical GREEN never transfers. +Historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical exact `729603ae4feadd369eee7819a45d6850604975da` / CI `34541860394` passed production exact coverage but failed the repository contract after the ADR lost the `DisposableContextDestroyError` trace. Historical GREEN never transfers. Protected-main integration is required before capability maturity can be promoted beyond `IMPLEMENTED_ON_ACTIVE_PR`. ## Buyer acceptance still open -This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, separately authorized recovery reconciliation, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, crash/process-restart reconciliation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, durable crash/process-restart recovery persistence, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Reference diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index dc9010a52..788224ac0 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -8,7 +8,7 @@ sequenceDiagram participant C as Application service participant S as BrowserSession aggregate participant BS as BoundBrowserSession - participant P as DisposableContextPort + participant P as DisposableContextPort / AuthorizedContextOperationPort participant B as Browser adapter (planned) C->>S: start(valid BrowserSessionId) @@ -43,7 +43,21 @@ sequenceDiagram S->>S: RecoveryRequired end - Note over C,S: Raw ids, adapter-selected values, and diagnostic references cannot mint lifecycle or presentation authority. + C->>BS: execute_authorized_context_operation(authority, operation) + BS->>S: validate session/incarnation/isolation/context/epoch + alt authority current + S-->>BS: exact stored handle + BS->>BS: mint private AuthorizedContextOperationRequest + BS->>P: execute_authorized_context_operation(request) + P->>B: adapter-owned presentation/reconciliation command + B-->>P: typed adapter result + P-->>C: output or AuthorizedContextOperationError::Adapter + else stale or foreign authority + S-->>C: AuthorizedContextOperationError::BrowserSession + Note over BS,P: adapter I/O = 0 + end + + Note over C,S: Raw ids, adapter-selected values, diagnostic references, and a second adapter cannot mint lifecycle or presentation authority. C->>BS: advance_context_epoch(context_id) BS->>S: replace epoch; old authority becomes stale @@ -57,23 +71,24 @@ sequenceDiagram B-->>P: observed destruction post-condition or DisposableContextDestroyError P-->>S: success S->>S: context = Destroyed - C->>BS: end() + C->>BS: finish() BS->>S: require every owned context Destroyed - S-->>C: Ended + S-->>C: Ended; wrapper consumed ``` -`BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and exposes no lifecycle method that accepts a replacement port. Tests retain inert observation state separately from the moved adapter. +`BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and exposes no lifecycle method that accepts a replacement port. `AuthorizedContextOperationPort` adds a typed, purpose-bounded post-create operation vocabulary without exposing the adapter itself. Tests retain inert observation state separately from the moved adapter. -`DisposableContextCreateRequest` and `DisposableContextCreateCompletion` are non-caller-constructible. The create request carries the reserved `BrowserContextEpoch` as a per-create transaction id; Browser Session alone decides whether the returned domain handle is accepted or rejected. +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` are non-caller-constructible. The create request carries the reserved `BrowserContextEpoch` as a per-create transaction id; Browser Session alone decides whether the returned domain handle is accepted or rejected and validates current authority before any later adapter operation. -For WebDriver BiDi, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. Protocol-specific pending/accepted/quarantined remote tuples remain in the BiDi ACL boundary rather than this domain model. +For WebDriver BiDi, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. Protocol-specific pending/accepted/quarantined remote tuples and command semantics remain in the BiDi ACL boundary rather than this domain model. -## Recovery and transport state +## Recovery, transport, and abandonment state ```mermaid stateDiagram-v2 [*] --> Active Active --> Active: create candidate + exact Accepted completion + authority + Active --> Active: authorized operation / current authority / exact bound adapter Active --> Active: context epoch advanced / prior authority stale Active --> Active: exact owned isolation destruction proved Active --> Active: DisposableContextCreateError::CreateFailedClean @@ -81,8 +96,8 @@ stateDiagram-v2 Active --> RecoveryRequired: duplicate output + exact Rejected completion Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven - Active --> Ended: all owned contexts Destroyed + end - Active --> TransportLost: browser transport lost + Active --> Ended: all owned contexts Destroyed + finish + Active --> TransportLost: browser transport lost / retain TransportLossOwnedHandle / mark uncertain RecoveryRequired --> RecoveryRequired: transport_lost = true / preserve recovery evidence Ended --> [*] RecoveryRequired --> [*] @@ -91,11 +106,35 @@ stateDiagram-v2 note right of RecoveryRequired BrowserSessionRecoveryEvidence retains known partial identity, duplicate/unsettled handle, - or exact unproven-destruction handle. + exact unproven-destruction handle, or transport-loss handle. It grants no I/O. end note ``` +```mermaid +sequenceDiagram + autonumber + participant C as Application service + participant BS as BoundBrowserSession + participant P as exact bound adapter + participant O as Operability / recovery observer + + C->>BS: create accepted remote ownership + alt normal completion + C->>BS: destroy exact authority + BS->>P: proven remote destruction + C->>BS: finish() + BS-->>C: consumed / Ended + else ordinary wrapper abandonment + C-xBS: drop without proven cleanup + Note over BS,P: Drop performs no browser I/O + BS->>O: increment abandoned_bound_session_count() + Note over O: process-local signal only; not destruction proof or durable exact-handle storage + end +``` + +The abandonment signal is deliberately weaker than durable recovery. Exact crash/process-restart reconciliation remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. + ## Sequential ABA hostile case ```mermaid @@ -111,7 +150,7 @@ sequenceDiagram PA-->>A: U, C pending A->>PA: completion Accepted(attempt 1) A->>PA: destroy(request S, incarnation A, U/C) - A->>A: end() + A->>A: finish() B->>B: start(S) => incarnation B; bind PB B->>PB: create(request S, incarnation B, attempt 1) @@ -119,8 +158,8 @@ sequenceDiagram B->>PB: completion Accepted(attempt 1) Note over A,B: local attempt/epoch may both equal 1, but incarnations differ B->>B: validate retained authority A - B-->>A: AuthorityMismatch before PB destroy I/O - B->>PB: destroy with authority B + incarnation B + B-->>A: AuthorityMismatch before PB adapter I/O + B->>PB: operate/destroy only with authority B + incarnation B ``` `RecoveryRequired` and `TransportLost` remain terminal for normal authority in this slice. Later reconciliation may inspect recovery evidence, but it must not reconstruct cleanup authority from raw identifiers or treat command ACK as proof of destruction. diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 0dd8afb50..5f15da204 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -31,22 +31,33 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: """Raw driver identifiers must never become caller-mintable authority tokens.""" source = (CRATE / "src/lib.rs").read_text(encoding="utf-8") - self.assertIn("pub struct BrowserSession", source) - self.assertIn("pub struct BoundBrowserSession", source) - self.assertIn("pub trait DisposableContextPort", source) - self.assertIn("pub struct DisposableIsolationId", source) - self.assertIn("pub struct DisposableContextHandle", source) - self.assertIn("pub struct BrowserSessionIncarnation", source) - self.assertIn("pub struct PresentationMutationAuthority", source) - self.assertIn("pub struct DisposableContextCreateRequest", source) - self.assertIn("pub struct DisposableContextCreateCompletion", source) - self.assertIn("pub enum DisposableContextCreateDisposition", source) - self.assertIn("pub enum DisposableContextCreateCompletionError", source) - self.assertIn("pub struct DisposableContextDestroyRequest", source) - self.assertIn("pub enum BrowserSessionRecoveryEvidence", source) + required_symbols = ( + "pub struct BrowserSession", + "pub struct BoundBrowserSession", + "pub trait DisposableContextPort", + "pub struct DisposableIsolationId", + "pub struct DisposableContextHandle", + "pub struct BrowserSessionIncarnation", + "pub struct PresentationMutationAuthority", + "pub struct DisposableContextCreateRequest", + "pub struct DisposableContextCreateCompletion", + "pub enum DisposableContextCreateDisposition", + "pub enum DisposableContextCreateCompletionError", + "pub struct DisposableContextDestroyRequest", + "pub enum BrowserSessionRecoveryEvidence", + "pub struct AuthorizedContextOperationRequest", + "pub enum AuthorizedContextOperationError", + "pub trait AuthorizedContextOperationPort", + "pub enum DisposableContextCreateError", + "pub enum DisposableContextDestroyError", + "pub fn abandoned_bound_session_count", + "pub fn finish", + "pub fn execute_authorized_context_operation", + ) + for symbol in required_symbols: + self.assertIn(symbol, source) + self.assertIn("BrowserSessionState::RecoveryRequired", source) - self.assertIn("pub enum DisposableContextCreateError", source) - self.assertIn("pub enum DisposableContextDestroyError", source) self.assertNotIn("pub enum DisposableContextPortError", source) self.assertNotIn("DisposableContextPortId", source) self.assertNotIn("fn port_id(&self)", source) @@ -64,11 +75,17 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertIn("DuplicateAdapterHandle", source) self.assertIn("UnsettledAdapterHandle", source) self.assertIn("UnprovenDestruction", source) + self.assertIn("TransportLossOwnedHandle", source) self.assertIn("create_disposable_context_with_port", source) self.assertIn("advance_context_epoch", source) self.assertIn("record_transport_loss", source) self.assertIn("transport_is_lost", source) self.assertIn("recovery_evidence", source) + self.assertIn("AuthorizedContextOperationError::BrowserSession", source) + self.assertIn("AuthorizedContextOperationError::Adapter", source) + self.assertIn("#[must_use =", source) + self.assertIn("impl

Drop for BoundBrowserSession

", source) + self.assertIn("", source) self.assertIn("user-context", source) self.assertIn("Reconstructing cleanup authority", source) self.assertIn("sequential_incarnation_reuse_rejects_stale_authority", source) @@ -88,78 +105,93 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: destroy_request_impl = source.split("impl DisposableContextDestroyRequest", 1)[1].split( "pub trait DisposableContextPort", 1 )[0] - self.assertNotIn("pub fn new", create_request_impl) - self.assertNotIn("pub const fn new", create_request_impl) - self.assertNotIn("pub fn new", completion_impl) - self.assertNotIn("pub const fn new", completion_impl) - self.assertNotIn("pub fn new", destroy_request_impl) - self.assertNotIn("pub const fn new", destroy_request_impl) - - def test_hostile_recovery_and_reincarnation_fixtures_remain_external(self) -> None: - """Recovery, binding, transaction, and sequential reuse invariants execute externally.""" - - destroy_hostile = ( - CRATE / "tests/destroy_failure_requires_recovery.rs" - ).read_text(encoding="utf-8") - reincarnation_hostile = ( - CRATE / "tests/sequential_incarnation_reuse.rs" - ).read_text(encoding="utf-8") - preflight_hostile = ( - CRATE / "tests/lifecycle_port_preflight_side_effect.rs" - ).read_text(encoding="utf-8") - substitution_hostile = ( - CRATE / "tests/lifecycle_port_same_id_spoof.rs" - ).read_text(encoding="utf-8") - transaction_hostile = ( - CRATE / "tests/creation_transaction_completion.rs" - ).read_text(encoding="utf-8") + operation_request_impl = source.split("impl AuthorizedContextOperationRequest", 1)[1].split( + "pub enum AuthorizedContextOperationError", 1 + )[0] + for request_impl in ( + create_request_impl, + completion_impl, + destroy_request_impl, + operation_request_impl, + ): + self.assertNotIn("pub fn new", request_impl) + self.assertNotIn("pub const fn new", request_impl) - self.assertIn( - "destroy_failure_requires_recovery_before_any_new_authority", - destroy_hostile, + def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) -> None: + """Recovery, binding, transactions, operations, and abandonment execute externally.""" + + destroy_hostile = (CRATE / "tests/destroy_failure_requires_recovery.rs").read_text( + encoding="utf-8" ) + reincarnation_hostile = (CRATE / "tests/sequential_incarnation_reuse.rs").read_text( + encoding="utf-8" + ) + preflight_hostile = (CRATE / "tests/lifecycle_port_preflight_side_effect.rs").read_text( + encoding="utf-8" + ) + substitution_hostile = (CRATE / "tests/lifecycle_port_same_id_spoof.rs").read_text( + encoding="utf-8" + ) + transaction_hostile = (CRATE / "tests/creation_transaction_completion.rs").read_text( + encoding="utf-8" + ) + debug_hostile = (CRATE / "tests/bound_session_debug_redaction.rs").read_text( + encoding="utf-8" + ) + transport_hostile = (CRATE / "tests/transport_loss_recovery_evidence.rs").read_text( + encoding="utf-8" + ) + operation_hostile = (CRATE / "tests/authorized_context_operation.rs").read_text( + encoding="utf-8" + ) + abandonment_hostile = (CRATE / "tests/bound_session_abandonment.rs").read_text( + encoding="utf-8" + ) + + self.assertIn("destroy_failure_requires_recovery_before_any_new_authority", destroy_hostile) self.assertIn("BrowserSessionRecoveryEvidence::UnprovenDestruction", destroy_hostile) self.assertIn("assert!(bound.record_transport_loss());", destroy_hostile) self.assertIn("assert!(!bound.record_transport_loss());", destroy_hostile) self.assertNotIn("lifecycle_port()", destroy_hostile) - self.assertIn( - "stale_authority_cannot_cross_sequential_session_incarnations", - reincarnation_hostile, - ) - self.assertIn( - "assert_ne!(\n bound_a.browser_session().incarnation(),", - reincarnation_hostile, - ) + self.assertIn("stale_authority_cannot_cross_sequential_session_incarnations", reincarnation_hostile) + self.assertIn("assert_ne!(\n bound_a.browser_session().incarnation(),", reincarnation_hostile) self.assertIn("assert!(destroy_b.borrow().is_empty());", reincarnation_hostile) self.assertNotIn("lifecycle_port()", reincarnation_hostile) - self.assertIn( - "lifecycle_binding_invokes_no_adapter_callback_before_authorized_create", - preflight_hostile, - ) + self.assertIn("lifecycle_binding_invokes_no_adapter_callback_before_authorized_create", preflight_hostile) self.assertIn("identity_callbacks", preflight_hostile) self.assertNotIn("bound.lifecycle_port()", preflight_hostile) - self.assertIn( - "distinct_adapter_cannot_be_substituted_for_create_after_binding", - substitution_hostile, - ) - self.assertIn( - "distinct_adapter_cannot_be_substituted_for_destroy_after_binding", - substitution_hostile, - ) + self.assertIn("distinct_adapter_cannot_be_substituted_for_create_after_binding", substitution_hostile) + self.assertIn("distinct_adapter_cannot_be_substituted_for_destroy_after_binding", substitution_hostile) self.assertNotIn("bound.lifecycle_port()", substitution_hostile) - self.assertIn( - "accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt", - transaction_hostile, - ) + self.assertIn("accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt", transaction_hostile) self.assertIn("request.attempt_epoch().value()", transaction_hostile) self.assertIn("DisposableContextCreateDisposition::Accepted", transaction_hostile) self.assertIn("DisposableContextCreateDisposition::Rejected", transaction_hostile) self.assertIn("assert!(ledger.pending.is_empty());", transaction_hostile) + self.assertIn("bound_session_debug_never_executes_or_exposes_adapter_debug", debug_hostile) + self.assertIn("adapter-secret-sentinel", debug_hostile) + self.assertIn("debug_callbacks.get(),\n 0", debug_hostile) + + self.assertIn("transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence", transport_hostile) + self.assertIn("transport-user-context-501", transport_hostile) + self.assertIn("recovery_evidence().len(),\n 1", transport_hostile) + + self.assertIn("authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io", operation_hostile) + self.assertIn("AuthorizedContextOperationError::BrowserSession", operation_hostile) + self.assertIn("AuthorizedContextOperationError::Adapter", operation_hostile) + self.assertIn("stale authority must fail before the bound adapter", operation_hostile) + + self.assertIn("dropping_unresolved_bound_session_is_observable_without_implicit_browser_io", abandonment_hostile) + self.assertIn("abandoned_bound_session_count", abandonment_hostile) + self.assertIn("Drop must never pretend synchronous browser cleanup succeeded", abandonment_hostile) + self.assertIn("proven_destruction_can_finish_without_abandonment_path", abandonment_hostile) + self.assertIn("bound.finish()", abandonment_hostile) + def test_architecture_decision_and_traceability_are_explicit(self) -> None: """Disposable ownership must remain a Proposed, standards-traced active-PR claim.""" @@ -172,43 +204,68 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: uml = (ROOT / "docs/uml/browser-session-lifecycle-authority.md").read_text( encoding="utf-8" ) - self.assertIn("Status: Proposed", adr) - self.assertIn("WD-webdriver-bidi-20260909", adr) - self.assertIn("RecoveryRequired", adr) - self.assertIn("BrowserSessionIncarnation", adr) - self.assertIn("BrowserSessionRecoveryEvidence", adr) - self.assertIn("DisposableContextCreateRequest", adr) - self.assertIn("DisposableContextCreateCompletion", adr) - self.assertIn("DisposableContextDestroyRequest", adr) - self.assertIn("BoundBrowserSession", adr) - self.assertIn("linear lifecycle-port binding", adr) - self.assertIn("no public raw port accessor", adr) - self.assertIn("per-create transaction", adr) - self.assertIn("DisposableContextCreateError", adr) - self.assertIn("DisposableContextDestroyError", adr) - self.assertIn("CreateFailedClean", adr) - self.assertIn("CreateFailedUncertain", adr) - self.assertIn("transport liveness", adr) - self.assertIn("sequential", adr) - self.assertIn("unproven destruction", adr) - self.assertIn("IMPLEMENTED_ON_ACTIVE_PR", trace) - self.assertIn("BoundBrowserSession", trace) - self.assertIn("DisposableContextCreateCompletion", trace) - self.assertIn("per-create transaction", trace) - self.assertIn("no public raw port accessor", trace) - self.assertIn("RecoveryRequired", trace) - self.assertIn("BrowserSessionIncarnation", trace) - self.assertIn("lossless recovery evidence", trace) - self.assertIn("transport liveness", trace) - self.assertIn("sequential ABA", trace) - self.assertIn("command ACK", trace) - self.assertIn("PresentationMutationAuthority", uml) - self.assertIn("BoundBrowserSession", uml) - self.assertIn("DisposableContextCreateCompletion", uml) - self.assertIn("BrowserSessionIncarnation", uml) - self.assertIn("RecoveryRequired", uml) - self.assertIn("transport_lost", uml) - self.assertIn("DisposableContextDestroyError / cleanup unproven", uml) + for token in ( + "Status: Proposed", + "WD-webdriver-bidi-20260909", + "RecoveryRequired", + "BrowserSessionIncarnation", + "BrowserSessionRecoveryEvidence", + "DisposableContextCreateRequest", + "DisposableContextCreateCompletion", + "DisposableContextDestroyRequest", + "BoundBrowserSession", + "linear lifecycle-port binding", + "no public raw port accessor", + "per-create transaction", + "DisposableContextCreateError", + "DisposableContextDestroyError", + "CreateFailedClean", + "CreateFailedUncertain", + "transport liveness", + "sequential", + "unproven destruction", + "AuthorizedContextOperationPort", + "TransportLossOwnedHandle", + "abandoned_bound_session_count", + "Drop", + "finish()", + ): + self.assertIn(token, adr) + + for token in ( + "IMPLEMENTED_ON_ACTIVE_PR", + "BoundBrowserSession", + "DisposableContextCreateCompletion", + "per-create transaction", + "no public raw port accessor", + "RecoveryRequired", + "BrowserSessionIncarnation", + "lossless recovery evidence", + "transport liveness", + "Sequential ABA", + "command ACK", + "AuthorizedContextOperationPort", + "TransportLossOwnedHandle", + "abandoned_bound_session_count", + "durable crash/process-restart recovery", + ): + self.assertIn(token, trace) + + for token in ( + "PresentationMutationAuthority", + "BoundBrowserSession", + "DisposableContextCreateCompletion", + "BrowserSessionIncarnation", + "RecoveryRequired", + "transport_lost", + "DisposableContextDestroyError / cleanup unproven", + "AuthorizedContextOperationRequest", + "AuthorizedContextOperationError::BrowserSession", + "TransportLossOwnedHandle", + "abandoned_bound_session_count", + "finish()", + ): + self.assertIn(token, uml) self.assertNotIn("IMPLEMENTED_ON_PROTECTED_MAIN", trace) From 37b37cde91264a386e5b783220b7dcf4d5c00673 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:21:04 +0900 Subject: [PATCH 024/632] docs(browser-session): correct lifecycle trace and BiDi reference --- .../0114-browser-session-disposable-context-authority.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 68b742eba..48876181d 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -12,7 +12,7 @@ The active implementation has to satisfy four constraints at once. First, `Bound Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. Transport liveness remains orthogonal to ownership certainty. -The 9 September 2026 WebDriver BiDi Working Draft defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. +The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is the 24 August 2026 publication. It defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. A previously cited 9 September 2026 snapshot could not be verified in W3C's latest-published report or publication index and is not used as authoritative evidence here. ## Decision drivers @@ -48,7 +48,7 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. 13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. 14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. -15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Evidence grants no browser command authority. Repeated transport-loss reports are idempotent. +15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. This is explicit **unproven destruction** evidence rather than cleanup proof. Evidence grants no browser command authority. Repeated transport-loss reports are idempotent. 16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. 17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. 18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. @@ -155,4 +155,4 @@ Supersede this ADR if the browser platform provides a complete, queryable, gener ## References -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +Browser Testing and Tools Working Group. (2026, August 24). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260824/ From cc7ecf9ad184afda27c1b125b4f9fd9d04957b37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:21:41 +0900 Subject: [PATCH 025/632] test(browser-session): require verified BiDi publication --- tests/test_browser_session_lifecycle_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 5f15da204..6b89cfebe 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -206,7 +206,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: ) for token in ( "Status: Proposed", - "WD-webdriver-bidi-20260909", + "WD-webdriver-bidi-20260824", "RecoveryRequired", "BrowserSessionIncarnation", "BrowserSessionRecoveryEvidence", From 710666229b5d9229b47c19ea980bc015a2454af8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:22:14 +0900 Subject: [PATCH 026/632] docs(browser-session): align BiDi standards trace --- docs/traceability/browser-session-lifecycle-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 498e073fb..b4fef0745 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -79,7 +79,7 @@ Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate ## Standards trace -The design dossier references the 9 September 2026 WebDriver BiDi Working Draft. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. +The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is the 24 August 2026 publication. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. A previously cited 9 September snapshot could not be verified in the W3C latest-published report or publication index and is therefore not treated as authoritative evidence. OriginWeave does not treat those protocol identifiers as policy authority or assume historical non-reuse after removal. A command ACK is insufficient proof that the disposable boundary is actually gone. @@ -119,4 +119,4 @@ This slice does not yet prove actual WebDriver BiDi lifecycle integration, obser ## Reference -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +Browser Testing and Tools Working Group. (2026, August 24). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260824/ From bdd4b9c898ac1b7f0a0cb0b979e765d4ebe03791 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:03:55 +0900 Subject: [PATCH 027/632] test(browser-session): prove failed finish retains ownership --- .../tests/bound_session_abandonment.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index b540ba6b7..5d9993be4 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -75,6 +75,38 @@ fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() ); } +#[test] +fn failed_finish_must_not_be_reclassified_as_abandonment() { + let destroy_calls = Rc::new(Cell::new(0)); + let before = abandoned_bound_session_count(); + let session = BrowserSession::start(BrowserSessionId::new(506).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port_for(506, &destroy_calls)); + let _authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + + let finish = bound.finish(); + + assert!( + matches!( + finish, + Err(originweave_browser_session::BrowserSessionError::ActiveContextRemains) + ), + "finish must reject while remote ownership remains unresolved" + ); + assert_eq!( + abandoned_bound_session_count(), + before, + "a failed deliberate finish must retain the bound lifecycle owner instead of dropping it as abandonment" + ); + assert_eq!( + destroy_calls.get(), + 0, + "failed finish validation must not perform implicit browser cleanup" + ); +} + #[test] fn proven_destruction_can_finish_without_abandonment_path() { let destroy_calls = Rc::new(Cell::new(0)); From f096c94dd56b0b95c83c8bf91738fc9348b4e568 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:04:46 +0900 Subject: [PATCH 028/632] test(browser-session): preserve sibling recovery handles --- .../recovery_required_sibling_evidence.rs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs diff --git a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs new file mode 100644 index 000000000..7addbe2b3 --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs @@ -0,0 +1,108 @@ +use std::collections::VecDeque; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct FailingDestroyPort { + handles: VecDeque, +} + +impl DisposableContextPort for FailingDestroyPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.handles + .pop_front() + .ok_or(DisposableContextCreateError::CreateFailedClean) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Err(DisposableContextDestroyError::DestroyFailed) + } +} + +fn handle(context: u64, isolation: &str) -> DisposableContextHandle { + DisposableContextHandle::new( + DisposableIsolationId::parse(isolation).expect("valid isolation id"), + BrowsingContextId::new(context).expect("valid browsing context"), + ) +} + +fn existing_exact_handle(evidence: &BrowserSessionRecoveryEvidence) -> Option<&DisposableContextHandle> { + match evidence { + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(handle) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle) + | BrowserSessionRecoveryEvidence::UnprovenDestruction(handle) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(handle) => Some(handle), + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => None, + } +} + +#[test] +fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() { + let first = handle(5070, "recovery-user-context-a"); + let sibling = handle(5071, "recovery-user-context-b"); + let port = FailingDestroyPort { + handles: VecDeque::from([first.clone(), sibling.clone()]), + }; + let session = BrowserSession::start(BrowserSessionId::new(507).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port); + + let first_authority = bound + .create_disposable_context() + .expect("first accepted context"); + let _sibling_authority = bound + .create_disposable_context() + .expect("second accepted context"); + + assert_eq!( + bound.destroy_disposable_context(&first_authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + + let evidence = bound.browser_session().recovery_evidence(); + assert!( + evidence.contains(&BrowserSessionRecoveryEvidence::UnprovenDestruction( + first.clone() + )), + "the directly failed destruction must keep its cause-specific evidence" + ); + assert_eq!( + evidence + .iter() + .filter_map(existing_exact_handle) + .filter(|candidate| *candidate == &first) + .count(), + 1, + "the directly failed context must not be duplicated as generic recovery evidence" + ); + assert!( + evidence + .iter() + .filter_map(existing_exact_handle) + .any(|candidate| candidate == &sibling), + "a sibling made uncertain by RecoveryRequired must remain exactly enumerable for recovery" + ); +} From 1f02e0b4253e4bdac2685950d678006910d5176e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:06:17 +0900 Subject: [PATCH 029/632] test(browser-session): cover authorized operation identity contract --- .../tests/authorized_context_operation.rs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/originweave-browser-session/tests/authorized_context_operation.rs b/crates/originweave-browser-session/tests/authorized_context_operation.rs index 6c139cf7b..958c6e315 100644 --- a/crates/originweave-browser-session/tests/authorized_context_operation.rs +++ b/crates/originweave-browser-session/tests/authorized_context_operation.rs @@ -4,10 +4,10 @@ use std::rc::Rc; use originweave_browser_session::{ AuthorizedContextOperationError, AuthorizedContextOperationPort, AuthorizedContextOperationRequest, BrowserSession, BrowserSessionError, - DisposableContextCreateCompletion, DisposableContextCreateCompletionError, - DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, - DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, - DisposableIsolationId, + BrowserSessionIncarnation, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -15,6 +15,8 @@ struct OperationPort { handle: Option, operation_calls: Rc>, observed_operations: Rc>>, + observed_sessions: Rc>>, + observed_incarnations: Rc>>, fail_operation: Rc>, } @@ -56,6 +58,12 @@ impl AuthorizedContextOperationPort for OperationPort { self.observed_operations .borrow_mut() .push(*request.operation()); + self.observed_sessions + .borrow_mut() + .push(request.browser_session()); + self.observed_incarnations + .borrow_mut() + .push(request.incarnation()); if self.fail_operation.get() { Err(()) } else { @@ -68,6 +76,8 @@ impl AuthorizedContextOperationPort for OperationPort { fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io() { let operation_calls = Rc::new(Cell::new(0)); let observed_operations = Rc::new(RefCell::new(Vec::new())); + let observed_sessions = Rc::new(RefCell::new(Vec::new())); + let observed_incarnations = Rc::new(RefCell::new(Vec::new())); let fail_operation = Rc::new(Cell::new(false)); let context = BrowsingContextId::new(503).expect("valid browsing context"); let port = OperationPort { @@ -78,10 +88,13 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before )), operation_calls: Rc::clone(&operation_calls), observed_operations: Rc::clone(&observed_operations), + observed_sessions: Rc::clone(&observed_sessions), + observed_incarnations: Rc::clone(&observed_incarnations), fail_operation: Rc::clone(&fail_operation), }; - let session = BrowserSession::start(BrowserSessionId::new(503).expect("valid session id")) - .expect("incarnation capacity"); + let session_id = BrowserSessionId::new(503).expect("valid session id"); + let session = BrowserSession::start(session_id).expect("incarnation capacity"); + let incarnation = session.incarnation(); let mut bound = session.bind_lifecycle_port(port); let authority = bound @@ -93,6 +106,8 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before ); assert_eq!(operation_calls.get(), 1); assert_eq!(observed_operations.borrow().as_slice(), &["set-viewport"]); + assert_eq!(observed_sessions.borrow().as_slice(), &[session_id]); + assert_eq!(observed_incarnations.borrow().as_slice(), &[incarnation]); fail_operation.set(true); assert_eq!( @@ -126,4 +141,14 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before observed_operations.borrow().as_slice(), &["set-viewport", "remote-failure", "reconcile-liveness"] ); + assert_eq!( + observed_sessions.borrow().as_slice(), + &[session_id, session_id, session_id], + "the purpose-bounded adapter must observe only the bound Browser Session identity" + ); + assert_eq!( + observed_incarnations.borrow().as_slice(), + &[incarnation, incarnation, incarnation], + "the purpose-bounded adapter must observe only the bound Browser Session incarnation" + ); } From d5046e76cb7555b448b728ea1bed9ba1ea8de8c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:10:38 +0900 Subject: [PATCH 030/632] test(browser-session): serialize abandonment counter assertions --- .../tests/bound_session_abandonment.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index 5d9993be4..f9861217c 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -1,5 +1,6 @@ use std::cell::Cell; use std::rc::Rc; +use std::sync::Mutex; use originweave_browser_session::{ abandoned_bound_session_count, BrowserSession, DisposableContextCreateCompletion, @@ -9,6 +10,8 @@ use originweave_browser_session::{ }; use originweave_core::{BrowserSessionId, BrowsingContextId}; +static ABANDONMENT_COUNTER_LOCK: Mutex<()> = Mutex::new(()); + struct AbandonmentPort { handle: Option, destroy_calls: Rc>, @@ -53,6 +56,9 @@ fn port_for(context: u64, destroy_calls: &Rc>) -> AbandonmentPort { #[test] fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() { + let _guard = ABANDONMENT_COUNTER_LOCK + .lock() + .expect("abandonment counter test lock"); let destroy_calls = Rc::new(Cell::new(0)); let before = abandoned_bound_session_count(); let session = BrowserSession::start(BrowserSessionId::new(504).expect("valid session id")) @@ -77,6 +83,9 @@ fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() #[test] fn failed_finish_must_not_be_reclassified_as_abandonment() { + let _guard = ABANDONMENT_COUNTER_LOCK + .lock() + .expect("abandonment counter test lock"); let destroy_calls = Rc::new(Cell::new(0)); let before = abandoned_bound_session_count(); let session = BrowserSession::start(BrowserSessionId::new(506).expect("valid session id")) @@ -109,6 +118,9 @@ fn failed_finish_must_not_be_reclassified_as_abandonment() { #[test] fn proven_destruction_can_finish_without_abandonment_path() { + let _guard = ABANDONMENT_COUNTER_LOCK + .lock() + .expect("abandonment counter test lock"); let destroy_calls = Rc::new(Cell::new(0)); let session = BrowserSession::start(BrowserSessionId::new(505).expect("valid session id")) .expect("incarnation capacity"); From 561993c6b09b60d79404eb0fab2ac6993a6c4535 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:02:59 +0900 Subject: [PATCH 031/632] fix(browser-session): retain recovery ownership on failed completion --- crates/originweave-browser-session/src/lib.rs | 83 +++++++++++++------ 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 1a2e49b6b..4ead8d7ea 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -190,6 +190,8 @@ pub enum BrowserSessionRecoveryEvidence { UnsettledAdapterHandle(DisposableContextHandle), /// Destruction of this exact owned handle failed or could not be proven. UnprovenDestruction(DisposableContextHandle), + /// A recovery condition elsewhere in the session made this active owned handle uncertain. + RecoveryRequiredOwnedHandle(DisposableContextHandle), /// Transport loss made this previously active owned handle uncertain. TransportLossOwnedHandle(DisposableContextHandle), } @@ -527,7 +529,10 @@ impl

fmt::Debug for BoundBrowserSession

{ .field("state", &self.session.state) .field("transport_lost", &self.session.transport_lost) .field("owned_context_count", &self.session.contexts.len()) - .field("recovery_evidence_count", &self.session.recovery_evidence.len()) + .field( + "recovery_evidence_count", + &self.session.recovery_evidence.len(), + ) .field("port", &"") .finish() } @@ -748,10 +753,9 @@ impl BrowserSession { .complete_disposable_context_creation(&completion) .is_err() { - self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( - handle, - )); + self.recovery_evidence.push( + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), + ); self.enter_recovery_required(); return Err(BrowserSessionError::ContextCreationUncertain); } @@ -769,10 +773,9 @@ impl BrowserSession { .complete_disposable_context_creation(&completion) .is_err() { - self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( - handle, - )); + self.recovery_evidence.push( + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), + ); self.enter_recovery_required(); return Err(BrowserSessionError::ContextCreationUncertain); } @@ -864,6 +867,29 @@ impl BrowserSession { } fn enter_recovery_required(&mut self) { + let sibling_handles = self + .contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| record.handle.clone()) + .filter(|handle| { + !self.recovery_evidence.iter().any(|evidence| match evidence { + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::UnprovenDestruction(existing) + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { + existing == handle + } + }) + }) + .collect::>(); + self.recovery_evidence.extend( + sibling_handles + .into_iter() + .map(BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle), + ); self.state = BrowserSessionState::RecoveryRequired; self.mark_active_contexts_uncertain(); } @@ -939,11 +965,11 @@ impl BoundBrowserSession

{ self.session.end() } - /// Consume the bound session after verifying that every owned context has proven destruction. + /// Verify normal completion without relinquishing the exact bound lifecycle owner on failure. /// - /// Failure consumes the wrapper as well; its non-I/O `Drop` fail-safe records abandonment when - /// unresolved ownership remains instead of pretending remote cleanup succeeded. - pub fn finish(mut self) -> Result<(), BrowserSessionError> { + /// A rejected finish leaves the wrapper intact so the caller can destroy or reconcile outstanding + /// contexts and retry. After success the aggregate is `Ended`; dropping the wrapper is then inert. + pub fn finish(&mut self) -> Result<(), BrowserSessionError> { self.session.end() } } @@ -1232,10 +1258,12 @@ mod tests { #[test] fn duplicate_adapter_output_preserves_offending_handle() { + let first_context_handle = + DisposableContextHandle::new(isolation_id("isolation-30-a"), context_id(30)); let duplicate_context_handle = DisposableContextHandle::new(isolation_id("isolation-30-b"), context_id(30)); let context_port = TestPort::with_handles(vec![ - DisposableContextHandle::new(isolation_id("isolation-30-a"), context_id(30)), + first_context_handle.clone(), duplicate_context_handle.clone(), ]); let mut duplicate_context = session(3).bind_lifecycle_port(context_port); @@ -1248,15 +1276,18 @@ mod tests { ); assert_eq!( duplicate_context.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( - duplicate_context_handle - )] + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_context_handle), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_context_handle), + ] ); + let first_isolation_handle = + DisposableContextHandle::new(isolation_id("isolation-31"), context_id(310)); let duplicate_isolation_handle = DisposableContextHandle::new(isolation_id("isolation-31"), context_id(311)); let isolation_port = TestPort::with_handles(vec![ - DisposableContextHandle::new(isolation_id("isolation-31"), context_id(310)), + first_isolation_handle.clone(), duplicate_isolation_handle.clone(), ]); let mut duplicate_isolation = session(31).bind_lifecycle_port(isolation_port); @@ -1269,16 +1300,16 @@ mod tests { ); assert_eq!( duplicate_isolation.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( - duplicate_isolation_handle - )] + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_isolation_handle), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_isolation_handle), + ] ); } #[test] fn create_completion_failure_preserves_non_authorizing_recovery_evidence() { - let expected = - DisposableContextHandle::new(isolation_id("isolation-315"), context_id(315)); + let expected = DisposableContextHandle::new(isolation_id("isolation-315"), context_id(315)); let mut port = TestPort::with_handles(vec![expected.clone()]); port.fail_completion = true; let mut bound = session(315).bind_lifecycle_port(port); @@ -1309,11 +1340,10 @@ mod tests { #[test] fn rejected_create_completion_failure_preserves_duplicate_and_unsettled_evidence() { - let first = - DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); + let first = DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); let duplicate = DisposableContextHandle::new(isolation_id("isolation-316-b"), context_id(316)); - let mut port = TestPort::with_handles(vec![first, duplicate.clone()]); + let mut port = TestPort::with_handles(vec![first.clone(), duplicate.clone()]); let mut bound = session(316).bind_lifecycle_port(port); bound @@ -1329,6 +1359,7 @@ mod tests { &[ BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate.clone()), BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first), ] ); assert_eq!( From 6e23406ec6e0128a5d32af34c8c5cca3c5639415 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:04:43 +0900 Subject: [PATCH 032/632] test(browser-session): prove sibling recovery projection --- .../recovery_required_sibling_evidence.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs index 7addbe2b3..0e9bb0243 100644 --- a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs +++ b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs @@ -45,11 +45,14 @@ fn handle(context: u64, isolation: &str) -> DisposableContextHandle { ) } -fn existing_exact_handle(evidence: &BrowserSessionRecoveryEvidence) -> Option<&DisposableContextHandle> { +fn existing_exact_handle( + evidence: &BrowserSessionRecoveryEvidence, +) -> Option<&DisposableContextHandle> { match evidence { BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(handle) | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle) | BrowserSessionRecoveryEvidence::UnprovenDestruction(handle) + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(handle) | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(handle) => Some(handle), BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => None, } @@ -89,6 +92,12 @@ fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() )), "the directly failed destruction must keep its cause-specific evidence" ); + assert!( + evidence.contains(&BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle( + sibling.clone() + )), + "the indirectly invalidated sibling must be projected as non-authorizing exact recovery evidence" + ); assert_eq!( evidence .iter() @@ -98,11 +107,13 @@ fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() 1, "the directly failed context must not be duplicated as generic recovery evidence" ); - assert!( + assert_eq!( evidence .iter() .filter_map(existing_exact_handle) - .any(|candidate| candidate == &sibling), - "a sibling made uncertain by RecoveryRequired must remain exactly enumerable for recovery" + .filter(|candidate| *candidate == &sibling) + .count(), + 1, + "an indirectly invalidated sibling must be retained exactly once" ); } From 9a1caaa76905a3aae46f67817207fed5617a505a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:05:01 +0900 Subject: [PATCH 033/632] test(browser-session): retain owner after failed finish --- .../tests/bound_session_abandonment.rs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index f9861217c..e0a30dfff 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -3,10 +3,11 @@ use std::rc::Rc; use std::sync::Mutex; use originweave_browser_session::{ - abandoned_bound_session_count, BrowserSession, DisposableContextCreateCompletion, - DisposableContextCreateCompletionError, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + abandoned_bound_session_count, BrowserSession, BrowserSessionError, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -82,7 +83,7 @@ fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() } #[test] -fn failed_finish_must_not_be_reclassified_as_abandonment() { +fn failed_finish_retains_same_bound_owner_for_cleanup_and_retry() { let _guard = ABANDONMENT_COUNTER_LOCK .lock() .expect("abandonment counter test lock"); @@ -91,19 +92,11 @@ fn failed_finish_must_not_be_reclassified_as_abandonment() { let session = BrowserSession::start(BrowserSessionId::new(506).expect("valid session id")) .expect("incarnation capacity"); let mut bound = session.bind_lifecycle_port(port_for(506, &destroy_calls)); - let _authority = bound + let authority = bound .create_disposable_context() .expect("accepted disposable context"); - let finish = bound.finish(); - - assert!( - matches!( - finish, - Err(originweave_browser_session::BrowserSessionError::ActiveContextRemains) - ), - "finish must reject while remote ownership remains unresolved" - ); + assert_eq!(bound.finish(), Err(BrowserSessionError::ActiveContextRemains)); assert_eq!( abandoned_bound_session_count(), before, @@ -114,6 +107,18 @@ fn failed_finish_must_not_be_reclassified_as_abandonment() { 0, "failed finish validation must not perform implicit browser cleanup" ); + + bound + .destroy_disposable_context(&authority) + .expect("the same bound lifecycle owner must remain available for cleanup"); + assert_eq!(destroy_calls.get(), 1); + bound.finish().expect("retry succeeds after proven destruction"); + drop(bound); + assert_eq!( + abandoned_bound_session_count(), + before, + "successful retry must leave no abandonment signal" + ); } #[test] @@ -131,6 +136,6 @@ fn proven_destruction_can_finish_without_abandonment_path() { bound .destroy_disposable_context(&authority) .expect("proven destruction"); - bound.finish().expect("consume normally ended bound session"); + bound.finish().expect("end normally after proven destruction"); assert_eq!(destroy_calls.get(), 1); } From 730d8afc2227040eed591c2a1f9c22b58493cd23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:06:01 +0900 Subject: [PATCH 034/632] style(browser-session): apply canonical formatting --- .../tests/authorized_context_operation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/tests/authorized_context_operation.rs b/crates/originweave-browser-session/tests/authorized_context_operation.rs index 958c6e315..926871c1f 100644 --- a/crates/originweave-browser-session/tests/authorized_context_operation.rs +++ b/crates/originweave-browser-session/tests/authorized_context_operation.rs @@ -82,8 +82,7 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before let context = BrowsingContextId::new(503).expect("valid browsing context"); let port = OperationPort { handle: Some(DisposableContextHandle::new( - DisposableIsolationId::parse("operation-user-context-503") - .expect("valid isolation id"), + DisposableIsolationId::parse("operation-user-context-503").expect("valid isolation id"), context, )), operation_calls: Rc::clone(&operation_calls), From bc0743638993aeab108b198f16061a1a6b828d65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:06:35 +0900 Subject: [PATCH 035/632] test(repo): lock recovery and finish ownership contracts --- tests/test_browser_session_lifecycle_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 6b89cfebe..00528d583 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -75,6 +75,7 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertIn("DuplicateAdapterHandle", source) self.assertIn("UnsettledAdapterHandle", source) self.assertIn("UnprovenDestruction", source) + self.assertIn("RecoveryRequiredOwnedHandle", source) self.assertIn("TransportLossOwnedHandle", source) self.assertIn("create_disposable_context_with_port", source) self.assertIn("advance_context_epoch", source) @@ -89,6 +90,8 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertIn("user-context", source) self.assertIn("Reconstructing cleanup authority", source) self.assertIn("sequential_incarnation_reuse_rejects_stale_authority", source) + self.assertIn("pub fn finish(&mut self)", source) + self.assertNotIn("pub fn finish(mut self)", source) authority_impl = source.split("impl PresentationMutationAuthority", 1)[1].split( "enum OwnedContextState", 1 @@ -141,6 +144,9 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - transport_hostile = (CRATE / "tests/transport_loss_recovery_evidence.rs").read_text( encoding="utf-8" ) + recovery_hostile = (CRATE / "tests/recovery_required_sibling_evidence.rs").read_text( + encoding="utf-8" + ) operation_hostile = (CRATE / "tests/authorized_context_operation.rs").read_text( encoding="utf-8" ) @@ -181,6 +187,10 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - self.assertIn("transport-user-context-501", transport_hostile) self.assertIn("recovery_evidence().len(),\n 1", transport_hostile) + self.assertIn("recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings", recovery_hostile) + self.assertIn("BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle", recovery_hostile) + self.assertIn("indirectly invalidated sibling", recovery_hostile) + self.assertIn("authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io", operation_hostile) self.assertIn("AuthorizedContextOperationError::BrowserSession", operation_hostile) self.assertIn("AuthorizedContextOperationError::Adapter", operation_hostile) @@ -189,6 +199,8 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - self.assertIn("dropping_unresolved_bound_session_is_observable_without_implicit_browser_io", abandonment_hostile) self.assertIn("abandoned_bound_session_count", abandonment_hostile) self.assertIn("Drop must never pretend synchronous browser cleanup succeeded", abandonment_hostile) + self.assertIn("failed_finish_retains_same_bound_owner_for_cleanup_and_retry", abandonment_hostile) + self.assertIn("same bound lifecycle owner must remain available for cleanup", abandonment_hostile) self.assertIn("proven_destruction_can_finish_without_abandonment_path", abandonment_hostile) self.assertIn("bound.finish()", abandonment_hostile) @@ -208,6 +220,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "Status: Proposed", "WD-webdriver-bidi-20260824", "RecoveryRequired", + "RecoveryRequiredOwnedHandle", "BrowserSessionIncarnation", "BrowserSessionRecoveryEvidence", "DisposableContextCreateRequest", @@ -227,6 +240,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "AuthorizedContextOperationPort", "TransportLossOwnedHandle", "abandoned_bound_session_count", + "failed `finish()`", "Drop", "finish()", ): @@ -239,6 +253,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "per-create transaction", "no public raw port accessor", "RecoveryRequired", + "RecoveryRequiredOwnedHandle", "BrowserSessionIncarnation", "lossless recovery evidence", "transport liveness", @@ -257,6 +272,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "DisposableContextCreateCompletion", "BrowserSessionIncarnation", "RecoveryRequired", + "RecoveryRequiredOwnedHandle", "transport_lost", "DisposableContextDestroyError / cleanup unproven", "AuthorizedContextOperationRequest", From e82d721d4607506944b30985c5a7a6a51abfe2bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:07:21 +0900 Subject: [PATCH 036/632] docs(browser-session): align recovery and finish invariants --- ...er-session-disposable-context-authority.md | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 48876181d..7e6e4beb2 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -8,9 +8,9 @@ OriginWeave's Browser Session bounded context is the domain authority for disposable browser lifecycle ownership and presentation mutation. WebDriver BiDi session ids, user-context ids, browsing-context ids, and adapter-selected values are protocol addressability, not authorization. -The active implementation has to satisfy four constraints at once. First, `BoundBrowserSession

` must consume the one concrete lifecycle adapter without later exposing raw `&P`/`&mut P` or a replacement-port path. Second, one Browser Session incarnation can issue multiple remote creates, so each result requires an aggregate-issued per-create transaction identity before it may become authorizing. Third, dependent WebDriver BiDi presentation and reconciliation work still needs to reach the same consumed adapter after exact `PresentationMutationAuthority` validation; retaining a second adapter or generic raw callback would recreate the capability-substitution defect. Fourth, uncertain lifecycle outcomes must preserve exact non-authorizing recovery evidence and ordinary wrapper abandonment must be observable without pretending that Rust `Drop` proves remote cleanup. +The active implementation has to satisfy four constraints at once. First, `BoundBrowserSession

` must consume the one concrete lifecycle adapter without later exposing raw `&P`/`&mut P` or a replacement-port path. Second, one Browser Session incarnation can issue multiple remote creates, so each result requires an aggregate-issued per-create transaction identity before it may become authorizing. Third, dependent WebDriver BiDi presentation and reconciliation work still needs to reach the same consumed adapter after exact `PresentationMutationAuthority` validation; retaining a second adapter or generic raw callback would recreate the capability-substitution defect. Fourth, uncertain lifecycle outcomes must preserve every exact non-authorizing owned handle needed for recovery, including siblings invalidated indirectly by another context's failure, and a failed `finish()` must not discard the same bound adapter needed to repair the rejected completion. -Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. Transport liveness remains orthogonal to ownership certainty. +Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is the 24 August 2026 publication. It defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. A previously cited 9 September 2026 snapshot could not be verified in W3C's latest-published report or publication index and is not used as authoritative evidence here. @@ -26,6 +26,7 @@ The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is - Presentation/reconciliation I/O must use the exact consumed adapter only after current aggregate authority validation. - Diagnostic formatting must not invoke adapter-owned `Debug` or expose adapter-internal state. - Silent loss of active/uncertain ownership on ordinary `BoundBrowserSession` drop must be observable without performing browser I/O from `Drop`. +- A rejected `finish()` must retain the exact bound lifecycle owner so cleanup/reconciliation and a later retry remain possible. - Sequential aggregate recreation must not make retained stale authority valid again. - Recovery evidence and transport liveness remain orthogonal. - Browser Session remains the domain authority; WebDriver BiDi, CDP, MCP, and LLMs remain adapters or consumers. @@ -48,13 +49,13 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. 13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. 14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. -15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. This is explicit **unproven destruction** evidence rather than cleanup proof. Evidence grants no browser command authority. Repeated transport-loss reports are idempotent. -16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. +15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, `RecoveryRequiredOwnedHandle` for every still-active sibling made uncertain by a recovery transition, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Cause-specific evidence is not duplicated as generic sibling evidence. These values are explicit **unproven destruction** evidence rather than cleanup proof and grant no browser command authority. Repeated recovery/loss observation must not duplicate exact-handle evidence. +16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. Any other active sibling is projected as `RecoveryRequiredOwnedHandle` before it becomes uncertain. 17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. 18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. 19. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. 20. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. -21. `BoundBrowserSession

` is `#[must_use]` and provides consuming `finish()`, which admits normal completion only after all owned contexts have proven destruction. `Drop` never performs browser I/O. If unresolved remote ownership remains, `Drop` increments the process-local `abandoned_bound_session_count()` operability signal. +21. `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` admits normal completion only after all owned contexts have proven destruction. A failed `finish()` returns the domain error without consuming or dropping the wrapper, so the same exact bound adapter and ownership ledger remain available for cleanup/reconciliation and retry. After success the aggregate is `Ended`, and later wrapper destruction is inert. `Drop` never performs browser I/O; if unresolved remote ownership remains, it increments the process-local `abandoned_bound_session_count()` operability signal. 22. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery handles must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. 23. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. @@ -88,6 +89,10 @@ Rejected as authority. It may be useful internally, but Browser Session could no Selected. Browser Session already reserves the epoch before create I/O, it is non-caller-constructible, monotonic within the aggregate, and the same value becomes the accepted context's first mutation epoch. +### Consuming `finish(self)` before validation + +Rejected. An expected `ActiveContextRemains` would destroy the only wrapper that owns the accepted adapter and private lifecycle ledger. Validation therefore occurs through `finish(&mut self)`; only successful completion changes the aggregate to `Ended`. + ### Browser I/O from `Drop` Rejected. Rust destruction is synchronous and cannot prove remote cleanup. `Drop` is restricted to non-I/O abandonment observability; normal completion is explicit through proven destruction plus `finish()`. @@ -102,9 +107,11 @@ The active stack receives a breaking trait extension for presentation/reconcilia The bound adapter is not publicly recoverable from `BoundBrowserSession`. Application and test code that needs observability retains inert metrics or diagnostic projections separately. Manual `Debug` exposes only Browser Session domain summary fields and a redacted port marker. -Transport loss now preserves exact previously active handles as non-authorizing recovery evidence. Completion or destruction failure remains ownership uncertainty and does not mint normal authority. +Entering `RecoveryRequired` now preserves exact handles for active siblings before marking them uncertain. Cause-specific evidence for the triggering context remains distinct, so recovery can enumerate every potentially live boundary without reconstructing command authority from identifiers. + +Transport loss preserves exact previously active handles as non-authorizing recovery evidence. Completion or destruction failure remains ownership uncertainty and does not mint normal authority. -Ordinary unresolved wrapper abandonment is process-locally observable, but exact crash/restart recovery still requires a canonical persistence/handoff path. This ADR does not claim that the in-memory counter is durable recovery. +A failed `finish()` leaves the same `BoundBrowserSession` usable for cleanup/reconciliation and retry. Ordinary unresolved wrapper abandonment is process-locally observable, but exact crash/restart recovery still requires a canonical persistence/handoff path. This ADR does not claim that the in-memory counter is durable recovery. ## Security and governance impact @@ -123,13 +130,15 @@ Required executable cases include: - one candidate can be accepted and the other rejected without pending-state collision or overwrite; - accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; - `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; +- `RecoveryRequired` preserves each indirectly invalidated active sibling exactly once as `RecoveryRequiredOwnedHandle` while retaining the triggering context's cause-specific evidence; - transport loss preserves every previously active exact handle as `TransportLossOwnedHandle` without adapter I/O or authority resurrection; - formatting a bound session does not invoke adapter-owned `Debug` and does not expose adapter-internal state; - an authorized operation reaches the exact consumed adapter, adapter errors remain typed, and stale authority fails before adapter I/O; -- dropping a bound session with unresolved ownership performs no implicit browser cleanup and increments the abandonment operability signal; proven destruction followed by `finish()` is the normal consuming path; +- dropping a bound session with unresolved ownership performs no implicit browser cleanup and increments the abandonment operability signal; +- a failed `finish()` performs no browser I/O or abandonment, retains the same bound owner, permits exact cleanup, and succeeds on retry after proven destruction; - recovery, sequential-incarnation ABA, epoch exhaustion, foreign authority, destruction failure, transport loss, and normal end remain covered. -The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. The historical `729603ae4feadd369eee7819a45d6850604975da` run `34541860394` passed exact production coverage but failed the repository contract because ADR 0114 had lost the `DisposableContextDestroyError` trace. Historical GREEN never transfers. Successor evidence must be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. +The historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` CI `34531025582` passed exact production coverage but failed canonical Rust formatting. The historical `729603ae4feadd369eee7819a45d6850604975da` run `34541860394` passed exact production coverage but failed the repository contract because ADR 0114 had lost the `DisposableContextDestroyError` trace. Exact `d5046e76cb7555b448b728ea1bed9ba1ea8de8c3` / CI `34573175780` passed Python repository contracts but failed canonical formatting; production coverage stopped during measurement because the two intentionally RED hostile lifecycle cases were still unresolved. Historical GREEN never transfers. Successor evidence must be fresh: repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, and production function/line/region/branch coverage each exactly 100%. ## Buyer acceptance still open @@ -139,9 +148,9 @@ This slice does not yet prove actual WebDriver BiDi lifecycle integration, brows Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` and perform lifecycle work through `BoundBrowserSession`. Code must not depend on recovering `&P`. Adapter implementations add exact-attempt staging/completion and, when they need post-create presentation or reconciliation I/O, implement the typed `AuthorizedContextOperationPort` operation vocabulary. -Normal owners destroy every owned context and consume the wrapper with `finish()`. Recovery owners must persist exact recovery evidence before terminating a process that still has unresolved ownership; the abandonment counter is an operability alert, not a persistence mechanism. +Normal owners destroy every owned context, call `finish()`, and may then release the ended wrapper. If `finish()` rejects, they retain the same wrapper, perform permitted cleanup/reconciliation, and retry. Recovery owners must persist exact recovery evidence before terminating a process that still has unresolved ownership; the abandonment counter is an operability alert, not a persistence mechanism. -Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, or adapter-local call order as an authorization boundary. +Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, consuming failed-finish path, or adapter-local call order as an authorization boundary. ## Open follow-ups From 5992e34e5af2a5366e5e8c900f26b0d12beb916a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:07:49 +0900 Subject: [PATCH 037/632] docs(trace): record sibling recovery and finish retry --- .../browser-session-lifecycle-authority.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index b4fef0745..ec0369cf3 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -30,7 +30,7 @@ validated BrowserSessionId → presentation/reconciliation uses private AuthorizedContextOperationRequest → exact consumed adapter only → proven destruction for every context -→ BoundBrowserSession::finish() consumes the normal lifecycle +→ BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` `BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P`, `&mut P`, or a generic callback that would recreate unrestricted adapter authority. @@ -59,15 +59,15 @@ Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSess ## Lossless recovery evidence while retained -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. Transport loss records each previously active exact handle as `TransportLossOwnedHandle` before marking it uncertain. None of this evidence grants browser command authority. +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. -Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. +Cause-specific evidence is retained for the triggering handle and is not duplicated as generic sibling evidence. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. ## Abandonment and lifecycle completion -`BoundBrowserSession

` is `#[must_use]`. The normal consuming path is `finish()`, which succeeds only after all owned contexts have proven destruction. `Drop` never performs browser I/O and never treats object destruction as browser destruction proof. +`BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` succeeds only after all owned contexts have proven destruction. If it returns `ActiveContextRemains`, the wrapper, exact bound adapter, and private ownership ledger remain intact. The same owner can therefore destroy or reconcile the remaining context and retry `finish()` without introducing a second adapter or ambient cleanup capability. -Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact crash/process-restart recovery therefore remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. +`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. ## Orthogonal transport liveness @@ -101,15 +101,17 @@ OriginWeave does not treat those protocol identifiers as policy authority or ass | adapter-owned Debug is not executed or rendered | manual `Debug for BoundBrowserSession

`; `bound_session_debug_never_executes_or_exposes_adapter_debug` | | sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | | lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; recovery tests | +| `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings` | | transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | | unproven destruction retains exact handle | `destroy_failure_requires_recovery_before_any_new_authority` | | unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | -| normal consuming completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | +| failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | +| normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | | transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | | normal end requires proved destruction | `BrowserSession::end` | | incarnation exhaustion fails closed | `allocate_incarnation` | -Historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical exact `729603ae4feadd369eee7819a45d6850604975da` / CI `34541860394` passed production exact coverage but failed the repository contract after the ADR lost the `DisposableContextDestroyError` trace. Historical GREEN never transfers. +Historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical exact `729603ae4feadd369eee7819a45d6850604975da` / CI `34541860394` passed production exact coverage but failed the repository contract after the ADR lost the `DisposableContextDestroyError` trace. Exact `d5046e76cb7555b448b728ea1bed9ba1ea8de8c3` / CI `34573175780` passed Python repository contracts, then failed canonical formatting; production coverage stopped during measurement because the two intentional hostile lifecycle REDs were still unresolved. Historical GREEN never transfers. Protected-main integration is required before capability maturity can be promoted beyond `IMPLEMENTED_ON_ACTIVE_PR`. From 77639717ca0e9f66ce8bc2cfc9a2173764173eb5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:08:15 +0900 Subject: [PATCH 038/632] docs(uml): show recovery sibling and finish retry flows --- .../browser-session-lifecycle-authority.md | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 788224ac0..09cf518f1 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -34,12 +34,14 @@ sequenceDiagram S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) else domain handle rejected S->>S: retain duplicate handle as recovery evidence + S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle S->>S: mint DisposableContextCreateCompletion(Rejected, exact attempt) S->>P: complete_disposable_context_creation(completion) P->>P: pending exact attempt → quarantined/non-authorizing S->>S: RecoveryRequired else completion cannot be proven S->>S: retain UnsettledAdapterHandle + S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle S->>S: RecoveryRequired end @@ -69,11 +71,24 @@ sequenceDiagram S->>P: destroy_disposable_context(request) P->>B: remove exact owned isolation boundary B-->>P: observed destruction post-condition or DisposableContextDestroyError - P-->>S: success - S->>S: context = Destroyed + alt destruction proved + P-->>S: success + S->>S: context = Destroyed + else destruction unproven + S->>S: retain UnprovenDestruction for failed handle + S->>S: retain each other Active sibling as RecoveryRequiredOwnedHandle + S->>S: RecoveryRequired; all active siblings become Uncertain + end + C->>BS: finish() - BS->>S: require every owned context Destroyed - S-->>C: Ended; wrapper consumed + alt every owned context Destroyed + BS->>S: end() + S-->>C: Ended + else ownership remains + BS->>S: end() + S-->>C: ActiveContextRemains + Note over C,P: same BoundBrowserSession + exact adapter remain available for cleanup/retry + end ``` `BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and exposes no lifecycle method that accepts a replacement port. `AuthorizedContextOperationPort` adds a typed, purpose-bounded post-create operation vocabulary without exposing the adapter itself. Tests retain inert observation state separately from the moved adapter. @@ -92,11 +107,12 @@ stateDiagram-v2 Active --> Active: context epoch advanced / prior authority stale Active --> Active: exact owned isolation destruction proved Active --> Active: DisposableContextCreateError::CreateFailedClean + Active --> Active: failed finish / retain same bound owner Active --> RecoveryRequired: CreateFailedUncertain / retain known partial isolation - Active --> RecoveryRequired: duplicate output + exact Rejected completion - Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle - Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven - Active --> Ended: all owned contexts Destroyed + finish + Active --> RecoveryRequired: duplicate output + exact Rejected completion + sibling RecoveryRequiredOwnedHandle + Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle + sibling RecoveryRequiredOwnedHandle + Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven + sibling RecoveryRequiredOwnedHandle + Active --> Ended: all owned contexts Destroyed + finish() Active --> TransportLost: browser transport lost / retain TransportLossOwnedHandle / mark uncertain RecoveryRequired --> RecoveryRequired: transport_lost = true / preserve recovery evidence Ended --> [*] @@ -106,7 +122,8 @@ stateDiagram-v2 note right of RecoveryRequired BrowserSessionRecoveryEvidence retains known partial identity, duplicate/unsettled handle, - exact unproven-destruction handle, or transport-loss handle. + exact unproven-destruction handle, and + RecoveryRequiredOwnedHandle for indirect siblings. It grants no I/O. end note ``` @@ -120,11 +137,13 @@ sequenceDiagram participant O as Operability / recovery observer C->>BS: create accepted remote ownership - alt normal completion + alt premature finish + C->>BS: finish() + BS-->>C: ActiveContextRemains; wrapper retained C->>BS: destroy exact authority BS->>P: proven remote destruction C->>BS: finish() - BS-->>C: consumed / Ended + BS-->>C: Ended else ordinary wrapper abandonment C-xBS: drop without proven cleanup Note over BS,P: Drop performs no browser I/O From 16f92dd3403c8a5c690afa82c407a479ab1fd108 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:08:38 +0900 Subject: [PATCH 039/632] style(browser-session): format transaction fixture --- .../tests/creation_transaction_completion.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-browser-session/tests/creation_transaction_completion.rs b/crates/originweave-browser-session/tests/creation_transaction_completion.rs index 2c6f17592..e0cb9c49c 100644 --- a/crates/originweave-browser-session/tests/creation_transaction_completion.rs +++ b/crates/originweave-browser-session/tests/creation_transaction_completion.rs @@ -5,9 +5,9 @@ use std::rc::Rc; use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionIncarnation, BrowserSessionState, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, - DisposableContextCreateDisposition, DisposableContextCreateError, DisposableContextCreateRequest, - DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, - DisposableContextPort, DisposableIsolationId, + DisposableContextCreateDisposition, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; From 371ca37d9979ee97167a73eb2042625f0d5ddc9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:08:54 +0900 Subject: [PATCH 040/632] style(browser-session): format preflight fixture --- .../tests/lifecycle_port_preflight_side_effect.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs index 01c1d658b..cee8c46ed 100644 --- a/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs +++ b/crates/originweave-browser-session/tests/lifecycle_port_preflight_side_effect.rs @@ -63,10 +63,8 @@ fn lifecycle_binding_invokes_no_adapter_callback_before_authorized_create() { .expect("incarnation capacity"); let identity_callbacks = Rc::new(Cell::new(0)); let create_calls = Rc::new(Cell::new(0)); - let port = SideEffectingIdentityPort::new( - Rc::clone(&identity_callbacks), - Rc::clone(&create_calls), - ); + let port = + SideEffectingIdentityPort::new(Rc::clone(&identity_callbacks), Rc::clone(&create_calls)); // Prove the fixture observes a shared-reference callback without retaining adapter access after bind. port.identity_probe(); From 916646dd3fcc03e3c6273fc925fb3e0230b97662 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:09:11 +0900 Subject: [PATCH 041/632] style(browser-session): format transport recovery fixture --- .../tests/transport_loss_recovery_evidence.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs index 0dda28a91..90c6b8b69 100644 --- a/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs +++ b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs @@ -22,8 +22,7 @@ impl DisposableContextPort for ObservedPort { ) -> Result { self.create_calls.set(self.create_calls.get() + 1); Ok(DisposableContextHandle::new( - DisposableIsolationId::parse("transport-user-context-501") - .expect("valid isolation id"), + DisposableIsolationId::parse("transport-user-context-501").expect("valid isolation id"), BrowsingContextId::new(501).expect("valid browsing context"), )) } @@ -72,13 +71,21 @@ fn transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evide bound.browser_session().state(), BrowserSessionState::TransportLost ); - assert_eq!(create_calls.get(), 1, "transport loss must not create browser state"); + assert_eq!( + create_calls.get(), + 1, + "transport loss must not create browser state" + ); assert_eq!( completion_calls.get(), 1, "transport loss must not settle another create attempt" ); - assert_eq!(destroy_calls.get(), 0, "transport loss is not destruction proof"); + assert_eq!( + destroy_calls.get(), + 0, + "transport loss is not destruction proof" + ); let evidence = bound.browser_session().recovery_evidence(); assert_eq!( From bd1bd857bba754ed06e3c326eeeba953f96faaa0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:09:43 +0900 Subject: [PATCH 042/632] style(browser-session): format abandonment fixture --- .../tests/bound_session_abandonment.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index e0a30dfff..5a1c07fd2 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -3,11 +3,11 @@ use std::rc::Rc; use std::sync::Mutex; use originweave_browser_session::{ - abandoned_bound_session_count, BrowserSession, BrowserSessionError, - DisposableContextCreateCompletion, DisposableContextCreateCompletionError, - DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, - DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, - DisposableIsolationId, + BrowserSession, BrowserSessionError, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + abandoned_bound_session_count, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; From 509866acd1041c34a819ab68120d0553f6e6478c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:16:24 +0900 Subject: [PATCH 043/632] style(browser-session): apply hosted rustfmt diagnostics --- .../tests/bound_session_abandonment.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index 5a1c07fd2..6106b5f24 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -96,7 +96,10 @@ fn failed_finish_retains_same_bound_owner_for_cleanup_and_retry() { .create_disposable_context() .expect("accepted disposable context"); - assert_eq!(bound.finish(), Err(BrowserSessionError::ActiveContextRemains)); + assert_eq!( + bound.finish(), + Err(BrowserSessionError::ActiveContextRemains) + ); assert_eq!( abandoned_bound_session_count(), before, @@ -112,7 +115,9 @@ fn failed_finish_retains_same_bound_owner_for_cleanup_and_retry() { .destroy_disposable_context(&authority) .expect("the same bound lifecycle owner must remain available for cleanup"); assert_eq!(destroy_calls.get(), 1); - bound.finish().expect("retry succeeds after proven destruction"); + bound + .finish() + .expect("retry succeeds after proven destruction"); drop(bound); assert_eq!( abandoned_bound_session_count(), @@ -136,6 +141,8 @@ fn proven_destruction_can_finish_without_abandonment_path() { bound .destroy_disposable_context(&authority) .expect("proven destruction"); - bound.finish().expect("end normally after proven destruction"); + bound + .finish() + .expect("end normally after proven destruction"); assert_eq!(destroy_calls.get(), 1); } From da4d049bbb815456f7d114c98197e12121a8ea6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:16:52 +0900 Subject: [PATCH 044/632] style(browser-session): apply hosted rustfmt diagnostics --- .../tests/recovery_required_sibling_evidence.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs index 0e9bb0243..164e217d7 100644 --- a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs +++ b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs @@ -93,9 +93,9 @@ fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() "the directly failed destruction must keep its cause-specific evidence" ); assert!( - evidence.contains(&BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle( - sibling.clone() - )), + evidence.contains( + &BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(sibling.clone()) + ), "the indirectly invalidated sibling must be projected as non-authorizing exact recovery evidence" ); assert_eq!( From ba3656a2165332f4ea6c367dda91c071cc34d9af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:19:17 +0900 Subject: [PATCH 045/632] style(browser-session): apply hosted rustfmt diagnostics --- crates/originweave-browser-session/src/lib.rs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 4ead8d7ea..fa2982909 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -773,9 +773,10 @@ impl BrowserSession { .complete_disposable_context_creation(&completion) .is_err() { - self.recovery_evidence.push( - BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), - ); + self.recovery_evidence + .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + handle, + )); self.enter_recovery_required(); return Err(BrowserSessionError::ContextCreationUncertain); } @@ -867,24 +868,24 @@ impl BrowserSession { } fn enter_recovery_required(&mut self) { - let sibling_handles = self - .contexts - .values() - .filter(|record| record.state == OwnedContextState::Active) - .map(|record| record.handle.clone()) - .filter(|handle| { - !self.recovery_evidence.iter().any(|evidence| match evidence { - BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::UnprovenDestruction(existing) - | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) - | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { - existing == handle - } + let sibling_handles = + self.contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| record.handle.clone()) + .filter(|handle| { + !self.recovery_evidence.iter().any(|evidence| match evidence { + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::UnprovenDestruction(existing) + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { + existing == handle + } + }) }) - }) - .collect::>(); + .collect::>(); self.recovery_evidence.extend( sibling_handles .into_iter() From 56a96ad8407b418d1775cfdf091519b57ad1e893 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:20:47 +0900 Subject: [PATCH 046/632] docs(product): currentize browser-session buyer gap baseline --- docs/product-technical-gap-baseline.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 490e46014..618f64cd8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,6 +2,15 @@ This is a dated delivery baseline, not a substitute for the PRD, TRD, roadmap, architecture decisions, or live GitHub state. It keeps buyer-visible gaps, current issues, active pull-request evidence, and commercial completion tracks in one discoverable place. Protected `main` is the implementation boundary: code in an open pull request is not shipped behavior. +## Live continuity note: 2026-09-11 + +- Protected `main` remains signature-valid at `87c4daa1830bac5a5228b6036752ad5633232085`. The live repository sweep found 132 open pull requests and 16 open non-PR issues; no release or tag exists. Active-PR work below is evidence only and is not protected-main behavior. +- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active branch now keeps one concrete lifecycle adapter structurally bound, uses aggregate-issued per-create transaction identity, routes typed post-create operations through the same consumed adapter after current authority validation, redacts adapter `Debug`, and preserves exact transport-loss recovery handles. The current repair additionally preserves every indirectly invalidated active sibling as non-authorizing `RecoveryRequiredOwnedHandle` and changes `finish(&mut self)` so a rejected normal completion retains the same bound owner for cleanup/reconciliation and retry. These claims remain active-PR claims until the successor exact head passes repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. +- #316 remains Draft at exact `8ca6c5a190d9ad2b4c7843d440e91f6070d681c2`. It must non-force restack only after a verified #317 successor and continues to own WebDriver BiDi-specific pending/accepted/quarantined tuples, command semantics, fresh-authority-at-I/O, and remote-liveness reconciliation. Browser Session must not absorb that protocol truth. +- Durable crash/process-restart persistence of exact recovery evidence remains open. `abandoned_bound_session_count()` is only a process-local non-I/O operability signal; it is not destruction proof or durable recovery storage. +- W3C's latest-published WebDriver BiDi document verified for this baseline is the **24 August 2026 Working Draft** (`WD-webdriver-bidi-20260824`). Standards freshness does not authorize an automatic runtime repin. +- #299's Chrome/ChromeDriver `150.0.7871.129` Agent Task result remains historical RED: 0/3 trials reached navigation because session creation failed. Current desktop Stable was promoted on 2026-09-08 as Chrome 153 (`153.0.8010.36` on Linux; `.36/.37` on Windows/macOS). Buyer-current browser acceptance therefore still requires a separately controlled current-Stable qualification with real navigation, interaction, observed post-condition, destruction, and cleanup evidence; a command ACK or wrapper drop is not success. + ## Live continuity note: 2026-09-09 - Protected `main` was re-fetched at `87c4daa1830bac5a5228b6036752ad5633232085`. Issue #292 remains open; its buyer-visible acceptance is still pinned Chromium application followed by page-observed and post-cleanup evidence. From 9818ada98cbdb9ed4054e96b7c7c4f3e52d51f3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 00:05:35 +0900 Subject: [PATCH 047/632] test(browser-session): expose arbitrary user-context length cap --- .../tests/user_context_identity_length.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 crates/originweave-browser-session/tests/user_context_identity_length.rs diff --git a/crates/originweave-browser-session/tests/user_context_identity_length.rs b/crates/originweave-browser-session/tests/user_context_identity_length.rs new file mode 100644 index 000000000..d8c23c79f --- /dev/null +++ b/crates/originweave-browser-session/tests/user_context_identity_length.rs @@ -0,0 +1,11 @@ +use originweave_browser_session::DisposableIsolationId; + +#[test] +fn webdriver_bidi_user_context_is_not_rejected_by_an_arbitrary_domain_length_cap() { + let remote_user_context = "u".repeat(4097); + + let identity = DisposableIsolationId::parse(&remote_user_context) + .expect("WebDriver BiDi browser.UserContext has no 4096-byte protocol limit"); + + assert_eq!(identity.as_str(), remote_user_context); +} From eb2701bcd41aca1e442e8919e8dcef7bde805d47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 00:08:31 +0900 Subject: [PATCH 048/632] docs(browser-session): correct BiDi provenance and identity contract --- ...er-session-disposable-context-authority.md | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 7e6e4beb2..c49b2d251 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -2,7 +2,7 @@ - Status: Proposed - Date: 2026-09-10 -- Last code-current review: 2026-09-11 +- Last code-current review: 2026-09-12 ## Context @@ -12,11 +12,12 @@ The active implementation has to satisfy four constraints at once. First, `Bound Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. -The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is the 24 August 2026 publication. It defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. These commands remain adapter capabilities rather than OriginWeave policy authority, and command ACK alone is not destruction proof. A previously cited 9 September 2026 snapshot could not be verified in W3C's latest-published report or publication index and is not used as authoritative evidence here. +The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-12 is the 9 September 2026 publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous version. It defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. The specification requires the user-context id to uniquely identify a user context but does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. Runtime-qualified protocol/browser revisions remain separately controlled and are not repinned by this standards-trace update. ## Decision drivers - Raw WebDriver/BiDi identifiers and adapter-chosen values are addressability, not mutation or cleanup authority. +- Browser-issued protocol identity needed for exact lifecycle ownership and recovery must remain losslessly representable; an uncited implementation constant must not silently redefine `browser.UserContext` semantics. - No arbitrary adapter callback may be required to establish lifecycle-port ownership. - A caller must not be able to substitute or recover the concrete adapter after Browser Session binding. - Browser Session create/destroy capabilities remain non-caller-constructible. @@ -45,19 +46,20 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 8. Browser Session privately issues `DisposableContextCreateCompletion` for that exact attempt with `Accepted` or `Rejected`. 9. The adapter must keep a successful remote create result non-authorizing until the matching `Accepted` completion. `Rejected` results remain non-authorizing recovery/quarantine state. A completion that cannot be proven for the exact pending attempt fails closed and sends the aggregate to `RecoveryRequired`. 10. Protocol-specific remote tuple contents are not copied into Browser Session. #314/#316 owns WebDriver BiDi pending/accepted/quarantined storage and remote-liveness validation. -11. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, and the exact stored handle. -12. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. -13. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. -14. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. -15. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, `RecoveryRequiredOwnedHandle` for every still-active sibling made uncertain by a recovery transition, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Cause-specific evidence is not duplicated as generic sibling evidence. These values are explicit **unproven destruction** evidence rather than cleanup proof and grant no browser command authority. Repeated recovery/loss observation must not duplicate exact-handle evidence. -16. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. Any other active sibling is projected as `RecoveryRequiredOwnedHandle` before it becomes uncertain. -17. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. -18. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. -19. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. -20. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. -21. `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` admits normal completion only after all owned contexts have proven destruction. A failed `finish()` returns the domain error without consuming or dropping the wrapper, so the same exact bound adapter and ownership ledger remain available for cleanup/reconciliation and retry. After success the aggregate is `Ended`, and later wrapper destruction is inert. `Drop` never performs browser I/O; if unresolved remote ownership remains, it increments the process-local `abandoned_bound_session_count()` operability signal. -22. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery handles must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. -23. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. +11. `DisposableIsolationId` preserves the browser-issued isolation identity exactly for create/destroy/recovery addressability. It must not truncate, normalize, hash, or reject an otherwise protocol-valid `browser.UserContext` solely because of an arbitrary local identifier-length constant. Resource-exhaustion limits, where required, belong at a cited protocol/frame/runtime boundary or an explicit deployment policy that still preserves lossless recovery evidence. +12. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, and the exact stored handle. +13. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. +14. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. +15. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. +16. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, `RecoveryRequiredOwnedHandle` for every still-active sibling made uncertain by a recovery transition, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Cause-specific evidence is not duplicated as generic sibling evidence. These values are explicit **unproven destruction** evidence rather than cleanup proof and grant no browser command authority. Repeated recovery/loss observation must not duplicate exact-handle evidence. +17. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. Any other active sibling is projected as `RecoveryRequiredOwnedHandle` before it becomes uncertain. +18. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. +19. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. +20. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. +21. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. +22. `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` admits normal completion only after all owned contexts have proven destruction. A failed `finish()` returns the domain error without consuming or dropping the wrapper, so the same exact bound adapter and ownership ledger remain available for cleanup/reconciliation and retry. After success the aggregate is `Ended`, and later wrapper destruction is inert. `Drop` never performs browser I/O; if unresolved remote ownership remains, it increments the process-local `abandoned_bound_session_count()` operability signal. +23. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery handles must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. +24. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. ## Alternatives considered @@ -89,6 +91,10 @@ Rejected as authority. It may be useful internally, but Browser Session could no Selected. Browser Session already reserves the epoch before create I/O, it is non-caller-constructible, monotonic within the aggregate, and the same value becomes the accepted context's first mutation epoch. +### Hard-coded maximum length for `browser.UserContext` + +Rejected unless an authoritative protocol/runtime bound is cited and versioned. The current WebDriver BiDi WD defines `browser.UserContext` as `text` and does not define a 4096-byte identifier ceiling. OriginWeave therefore must not convert a browser-issued, otherwise valid identity into ownership loss because of an implementation-chosen domain constant. + ### Consuming `finish(self)` before validation Rejected. An expected `ActiveContextRemains` would destroy the only wrapper that owns the accepted adapter and private lifecycle ledger. Validation therefore occurs through `finish(&mut self)`; only successful completion changes the aggregate to `Ended`. @@ -117,6 +123,8 @@ A failed `finish()` leaves the same `BoundBrowserSession` usable for cleanup/rec No page-controlled value, raw browser identifier, adapter-selected scalar, diagnostic reference, provider/model decision, or LLM output can mint lifecycle completion or presentation authority. Remote creation stays non-authorizing until the aggregate validates ownership and accepts that exact attempt. Post-create adapter I/O is admitted only through current aggregate authority and the exact consumed adapter instance. +Lossless retention of browser-issued user-context identity is an ownership requirement, not authority delegation. Resource controls must not create an untracked remote isolation boundary by discarding or rewriting the only exact addressability needed for recovery. + This decision does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. ## Tests and exact evidence @@ -129,6 +137,7 @@ Required executable cases include: - two successful remote create candidates in the same session incarnation receive distinct attempt epochs; - one candidate can be accepted and the other rejected without pending-state collision or overwrite; - accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; +- an otherwise-valid browser-issued `browser.UserContext` longer than the former 4096-byte implementation threshold remains losslessly representable for exact destroy/recovery rather than being rejected by an arbitrary domain cap; - `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; - `RecoveryRequired` preserves each indirectly invalidated active sibling exactly once as `RecoveryRequiredOwnedHandle` while retaining the triggering context's cause-specific evidence; - transport loss preserves every previously active exact handle as `TransportLossOwnedHandle` without adapter I/O or authority resurrection; @@ -150,7 +159,7 @@ Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` Normal owners destroy every owned context, call `finish()`, and may then release the ended wrapper. If `finish()` rejects, they retain the same wrapper, perform permitted cleanup/reconciliation, and retry. Recovery owners must persist exact recovery evidence before terminating a process that still has unresolved ownership; the abandonment counter is an operability alert, not a persistence mechanism. -Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, consuming failed-finish path, or adapter-local call order as an authorization boundary. +Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, consuming failed-finish path, arbitrary browser-user-context length cap, or adapter-local call order as an authorization boundary. ## Open follow-ups @@ -164,4 +173,4 @@ Supersede this ADR if the browser platform provides a complete, queryable, gener ## References -Browser Testing and Tools Working Group. (2026, August 24). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260824/ +Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ From 9c17e7c8d6e491a2610e7cc5b48089a6980bfebe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:03:48 +0900 Subject: [PATCH 049/632] test(browser-session): track current WebDriver BiDi publication --- tests/test_browser_session_lifecycle_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 00528d583..a37ea034a 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -218,7 +218,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: ) for token in ( "Status: Proposed", - "WD-webdriver-bidi-20260824", + "WD-webdriver-bidi-20260909", "RecoveryRequired", "RecoveryRequiredOwnedHandle", "BrowserSessionIncarnation", From f73cc5def267b99f43986cd3c504b86cb3d489d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:04:34 +0900 Subject: [PATCH 050/632] docs(browser-session): sync WebDriver BiDi trace to current publication --- .../browser-session-lifecycle-authority.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index ec0369cf3..7f91dc418 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -63,6 +63,8 @@ Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSess Cause-specific evidence is retained for the triggering handle and is not duplicated as generic sibling evidence. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. +The active successor still has open recovery-correlation work: create uncertainty and create-completion failures must retain the exact aggregate-issued attempt epoch; destructive and purpose-bounded adapter requests must retain the exact validated context epoch as non-authorizing provenance; `RecoveryRequired`/`TransportLost` need a purpose-bounded handoff that keeps the exact same adapter with the exact evidence instead of reconstructing a second adapter. + ## Abandonment and lifecycle completion `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` succeeds only after all owned contexts have proven destruction. If it returns `ActiveContextRemains`, the wrapper, exact bound adapter, and private ownership ledger remain intact. The same owner can therefore destroy or reconcile the remaining context and retry `finish()` without introducing a second adapter or ambient cleanup capability. @@ -77,11 +79,17 @@ Transport liveness is tracked independently from ownership recovery. A first tra Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external values and also begin at epoch 1. A's retained authority still fails because B has a different `BrowserSessionIncarnation`. The bound port receives the incarnation inside aggregate-issued lifecycle capabilities. +## Browser-issued user-context identity + +`DisposableIsolationId` maps one-to-one to the browser-issued WebDriver BiDi `browser.UserContext` identity. That value is addressability and recovery evidence, not command authority. OriginWeave must preserve a protocol-valid browser identity losslessly so the exact remote boundary can later be destroyed or reconciled. The current active branch still contains a historical 4096-byte parser ceiling; `user_context_identity_length.rs` intentionally keeps that mismatch RED until the arbitrary domain constant is removed or replaced by a cited, versioned protocol/runtime/deployment boundary. No truncation or normalization is acceptable for a browser-issued identity. + ## Standards trace -The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-11 is the 24 August 2026 publication. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. A previously cited 9 September snapshot could not be verified in the W3C latest-published report or publication index and is therefore not treated as authoritative evidence. +The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-12 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. `browser.UserContext` is defined as `text`; the published protocol does not define the active branch's 4096-byte domain ceiling. + +Standards freshness and runtime qualification are separate controls. Updating this citation does not repin the separately qualified Chromium/WebDriver BiDi runtime revision. -OriginWeave does not treat those protocol identifiers as policy authority or assume historical non-reuse after removal. A command ACK is insufficient proof that the disposable boundary is actually gone. +OriginWeave does not treat protocol identifiers as policy authority or assume historical non-reuse after removal. A command ACK is insufficient proof that the disposable boundary is actually gone. ## Source and executable evidence @@ -107,6 +115,7 @@ OriginWeave does not treat those protocol identifiers as policy authority or ass | unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | | failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | | normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | +| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs` (currently RED against the historical 4096-byte ceiling) | | transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | | normal end requires proved destruction | `BrowserSession::end` | | incarnation exhaustion fails closed | `allocate_incarnation` | @@ -121,4 +130,4 @@ This slice does not yet prove actual WebDriver BiDi lifecycle integration, obser ## Reference -Browser Testing and Tools Working Group. (2026, August 24). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260824/ +Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ From 3adc22a60cdf02a0b753729d728160ec68418bac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:08:19 +0900 Subject: [PATCH 051/632] test(browser-session): require operation epoch provenance --- .../tests/authorized_context_operation.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/originweave-browser-session/tests/authorized_context_operation.rs b/crates/originweave-browser-session/tests/authorized_context_operation.rs index 926871c1f..86438445c 100644 --- a/crates/originweave-browser-session/tests/authorized_context_operation.rs +++ b/crates/originweave-browser-session/tests/authorized_context_operation.rs @@ -17,6 +17,7 @@ struct OperationPort { observed_operations: Rc>>, observed_sessions: Rc>>, observed_incarnations: Rc>>, + observed_epochs: Rc>>, fail_operation: Rc>, } @@ -64,6 +65,9 @@ impl AuthorizedContextOperationPort for OperationPort { self.observed_incarnations .borrow_mut() .push(request.incarnation()); + self.observed_epochs + .borrow_mut() + .push(request.context_epoch().value()); if self.fail_operation.get() { Err(()) } else { @@ -78,6 +82,7 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before let observed_operations = Rc::new(RefCell::new(Vec::new())); let observed_sessions = Rc::new(RefCell::new(Vec::new())); let observed_incarnations = Rc::new(RefCell::new(Vec::new())); + let observed_epochs = Rc::new(RefCell::new(Vec::new())); let fail_operation = Rc::new(Cell::new(false)); let context = BrowsingContextId::new(503).expect("valid browsing context"); let port = OperationPort { @@ -89,6 +94,7 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before observed_operations: Rc::clone(&observed_operations), observed_sessions: Rc::clone(&observed_sessions), observed_incarnations: Rc::clone(&observed_incarnations), + observed_epochs: Rc::clone(&observed_epochs), fail_operation: Rc::clone(&fail_operation), }; let session_id = BrowserSessionId::new(503).expect("valid session id"); @@ -107,6 +113,7 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before assert_eq!(observed_operations.borrow().as_slice(), &["set-viewport"]); assert_eq!(observed_sessions.borrow().as_slice(), &[session_id]); assert_eq!(observed_incarnations.borrow().as_slice(), &[incarnation]); + assert_eq!(observed_epochs.borrow().as_slice(), &[1]); fail_operation.set(true); assert_eq!( @@ -114,6 +121,7 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before Err(AuthorizedContextOperationError::Adapter(())) ); assert_eq!(operation_calls.get(), 2); + assert_eq!(observed_epochs.borrow().as_slice(), &[1, 1]); fail_operation.set(false); let current = bound @@ -130,6 +138,11 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before 2, "stale authority must fail before the bound adapter observes an operation" ); + assert_eq!( + observed_epochs.borrow().as_slice(), + &[1, 1], + "stale authority must not emit an adapter request or provenance epoch" + ); assert_eq!( bound.execute_authorized_context_operation(¤t, "reconcile-liveness"), @@ -150,4 +163,9 @@ fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before &[incarnation, incarnation, incarnation], "the purpose-bounded adapter must observe only the bound Browser Session incarnation" ); + assert_eq!( + observed_epochs.borrow().as_slice(), + &[1, 1, 2], + "adapter requests must retain the exact validated authority epoch" + ); } From 9bcad29a936d1ec0922c7c0c63394be1140292fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:08:39 +0900 Subject: [PATCH 052/632] test(browser-session): preserve destroy epoch provenance --- .../tests/destroy_failure_requires_recovery.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs index e88a1b964..5b645dc8d 100644 --- a/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs +++ b/crates/originweave-browser-session/tests/destroy_failure_requires_recovery.rs @@ -15,6 +15,7 @@ struct FailingDestroyPort { next_handle: DisposableContextHandle, create_calls: Rc>, destroy_calls: Rc>, + observed_destroy_epoch: Rc>>, } impl FailingDestroyPort { @@ -23,6 +24,7 @@ impl FailingDestroyPort { isolation: &str, create_calls: Rc>, destroy_calls: Rc>, + observed_destroy_epoch: Rc>>, ) -> Result { let isolation = DisposableIsolationId::parse(isolation) .map_err(|_| "static fixture isolation id must be valid")?; @@ -32,6 +34,7 @@ impl FailingDestroyPort { next_handle: DisposableContextHandle::new(isolation, browsing_context), create_calls, destroy_calls, + observed_destroy_epoch, }) } } @@ -54,9 +57,11 @@ impl DisposableContextPort for FailingDestroyPort { fn destroy_disposable_context( &mut self, - _request: &DisposableContextDestroyRequest, + request: &DisposableContextDestroyRequest, ) -> Result<(), DisposableContextDestroyError> { self.destroy_calls.set(self.destroy_calls.get() + 1); + self.observed_destroy_epoch + .set(Some(request.context_epoch().value())); Err(DisposableContextDestroyError::DestroyFailed) } } @@ -75,31 +80,36 @@ fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &' .map_err(|_| "browser session incarnation must be available")?; let create_calls = Rc::new(Cell::new(0)); let destroy_calls = Rc::new(Cell::new(0)); + let observed_destroy_epoch = Rc::new(Cell::new(None)); let failing_port = FailingDestroyPort::new( 5010, "user-context-501", Rc::clone(&create_calls), Rc::clone(&destroy_calls), + Rc::clone(&observed_destroy_epoch), )?; let mut bound = session.bind_lifecycle_port(failing_port); let authority = bound .create_disposable_context() .map_err(|_| "fixture disposable context creation must succeed")?; + let expected_epoch = authority.context_epoch(); assert_eq!( bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) ); assert_eq!(destroy_calls.get(), 1); + assert_eq!(observed_destroy_epoch.get(), Some(expected_epoch.value())); assert_eq!( bound.browser_session().state(), BrowserSessionState::RecoveryRequired ); assert_eq!( bound.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::UnprovenDestruction( - expected_handle - )] + &[BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: expected_handle, + context_epoch: expected_epoch, + }] ); assert!(!bound.browser_session().transport_is_lost()); From faa9b46d003cf4d3f8972588ec7d9560f204f3a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:15:26 +0900 Subject: [PATCH 053/632] fix(browser-session): preserve validated epoch provenance --- crates/originweave-browser-session/src/lib.rs | 92 ++++++++++++------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index fa2982909..c2060905c 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -188,8 +188,13 @@ pub enum BrowserSessionRecoveryEvidence { DuplicateAdapterHandle(DisposableContextHandle), /// A complete create result could not be settled with the bound adapter after domain validation. UnsettledAdapterHandle(DisposableContextHandle), - /// Destruction of this exact owned handle failed or could not be proven. - UnprovenDestruction(DisposableContextHandle), + /// Destruction of this exact owned handle and validated authority epoch failed or could not be proven. + UnprovenDestruction { + /// Exact owned context whose remote boundary remains uncertain. + context: DisposableContextHandle, + /// Browser Session epoch validated immediately before destroy I/O. + context_epoch: BrowserContextEpoch, + }, /// A recovery condition elsewhere in the session made this active owned handle uncertain. RecoveryRequiredOwnedHandle(DisposableContextHandle), /// Transport loss made this previously active owned handle uncertain. @@ -288,12 +293,14 @@ pub enum DisposableContextCreateCompletionError { /// /// There is deliberately no public constructor. The bound aggregate creates this request only after /// validating the supplied presentation authority against current ownership. A caller cannot rebuild -/// cleanup authority from raw browser identifiers. +/// cleanup authority from raw browser identifiers. The validated epoch is carried only as correlation +/// evidence for the already-authorized request; it is not independently sufficient to destroy state. #[derive(Debug)] pub struct DisposableContextDestroyRequest { browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, context: DisposableContextHandle, + context_epoch: BrowserContextEpoch, } impl DisposableContextDestroyRequest { @@ -314,6 +321,12 @@ impl DisposableContextDestroyRequest { pub const fn context(&self) -> &DisposableContextHandle { &self.context } + + /// Return the exact Browser Session epoch validated before destroy I/O. + #[must_use] + pub const fn context_epoch(&self) -> BrowserContextEpoch { + self.context_epoch + } } /// Port implemented by a reviewed browser adapter for disposable context lifecycle operations. @@ -360,11 +373,13 @@ pub trait DisposableContextPort { /// /// The caller supplies only the adapter-defined operation value. Browser Session validates the /// accompanying presentation authority first and privately binds the operation to the exact owned -/// context before the consumed adapter can observe it. There is deliberately no public constructor. +/// context and validated epoch before the consumed adapter can observe it. There is deliberately no +/// public constructor, and the epoch is correlation/provenance rather than standalone authority. pub struct AuthorizedContextOperationRequest { browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, context: DisposableContextHandle, + context_epoch: BrowserContextEpoch, operation: O, } @@ -387,6 +402,12 @@ impl AuthorizedContextOperationRequest { &self.context } + /// Return the exact Browser Session epoch validated before adapter I/O. + #[must_use] + pub const fn context_epoch(&self) -> BrowserContextEpoch { + self.context_epoch + } + /// Return the adapter-defined purpose-bounded operation payload. #[must_use] pub const fn operation(&self) -> &O { @@ -802,10 +823,12 @@ impl BrowserSession { let browser_session = self.id; let incarnation = self.incarnation; let record = self.context_for_authority_mut(authority)?; + let context_epoch = record.epoch; let request = DisposableContextDestroyRequest { browser_session, incarnation, context: record.handle.clone(), + context_epoch, }; match port.destroy_disposable_context(&request) { Ok(()) => { @@ -815,9 +838,10 @@ impl BrowserSession { Err(DisposableContextDestroyError::DestroyFailed) => { record.state = OwnedContextState::Uncertain; self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnprovenDestruction( - request.context, - )); + .push(BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: request.context, + context_epoch, + }); self.enter_recovery_required(); Err(BrowserSessionError::ContextDestructionFailed) } @@ -868,24 +892,27 @@ impl BrowserSession { } fn enter_recovery_required(&mut self) { - let sibling_handles = - self.contexts - .values() - .filter(|record| record.state == OwnedContextState::Active) - .map(|record| record.handle.clone()) - .filter(|handle| { - !self.recovery_evidence.iter().any(|evidence| match evidence { - BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::UnprovenDestruction(existing) - | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) - | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { - existing == handle - } - }) + let sibling_handles = self + .contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| record.handle.clone()) + .filter(|handle| { + !self.recovery_evidence.iter().any(|evidence| match evidence { + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { + existing == handle + } + BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: existing, + .. + } => existing == handle, }) - .collect::>(); + }) + .collect::>(); self.recovery_evidence.extend( sibling_handles .into_iter() @@ -988,16 +1015,17 @@ impl BoundBrowserSession

{ ) -> Result> { let browser_session = self.session.id; let incarnation = self.session.incarnation; - let context = self + let record = self .session .context_for_authority_mut(authority) - .map_err(AuthorizedContextOperationError::BrowserSession)? - .handle - .clone(); + .map_err(AuthorizedContextOperationError::BrowserSession)?; + let context = record.handle.clone(); + let context_epoch = record.epoch; let request = AuthorizedContextOperationRequest { browser_session, incarnation, context, + context_epoch, operation, }; self.port @@ -1510,6 +1538,7 @@ mod tests { port.fail_destroy = true; let mut bound = session(9).bind_lifecycle_port(port); let authority = bound.create_disposable_context().expect("owned context"); + let expected_epoch = authority.context_epoch(); assert_eq!( bound.destroy_disposable_context(&authority), Err(BrowserSessionError::ContextDestructionFailed) @@ -1520,9 +1549,10 @@ mod tests { ); assert_eq!( bound.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::UnprovenDestruction( - expected_handle - )] + &[BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: expected_handle, + context_epoch: expected_epoch, + }] ); assert!(!bound.browser_session().transport_is_lost()); assert!(bound.record_transport_loss()); From f56efa8a0cbfe4b95cdfbde0b29d3c0602c7b4a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:16:54 +0900 Subject: [PATCH 054/632] docs(browser-session): trace validated epoch provenance --- .../browser-session-lifecycle-authority.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 7f91dc418..3776bc847 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -26,9 +26,10 @@ validated BrowserSessionId → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) → exact authority validation before any lifecycle or purpose-bounded adapter I/O -→ lifecycle destruction uses private DisposableContextDestroyRequest -→ presentation/reconciliation uses private AuthorizedContextOperationRequest +→ lifecycle destruction uses private DisposableContextDestroyRequest(handle, validated epoch) +→ presentation/reconciliation uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) → exact consumed adapter only +→ failed/unproven destruction retains exact handle + validated epoch as non-authorizing recovery evidence → proven destruction for every context → BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` @@ -37,7 +38,7 @@ validated BrowserSessionId The wrapper has a manual redacted `Debug` implementation. Formatting exposes inert Browser Session summary fields only and never calls `P::fmt`, so a side-effecting or secret-bearing adapter `Debug` cannot become a diagnostic capability escape. -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. Purpose-bounded operations are constructed only after exact `PresentationMutationAuthority` validation. +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. Destroy and purpose-bounded operation requests carry the exact context epoch that Browser Session validated immediately before adapter I/O. That epoch is correlation/provenance only: it does not authorize a command independently from the private aggregate-issued request. Purpose-bounded operations are constructed only after exact `PresentationMutationAuthority` validation. ## Transactional remote creation @@ -53,17 +54,17 @@ Protocol-specific tuple contents and pending/accepted/quarantined storage remain ## Same-bound-adapter authorized operations -`AuthorizedContextOperationPort` extends the lifecycle port for adapters that need post-create presentation or reconciliation work. The operation/output/error vocabulary remains adapter-owned. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. +`AuthorizedContextOperationPort` extends the lifecycle port for adapters that need post-create presentation or reconciliation work. The operation/output/error vocabulary remains adapter-owned. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. The request exposes that already-validated epoch so adapter execution logs and protocol correlation cannot collapse distinct authority generations that happen to reuse the same external identifiers. -Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O. An operation attempted by the exact bound adapter can return `AuthorizedContextOperationError::Adapter`. No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. +Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O, so no request and no epoch provenance reaches the adapter on rejection. An operation attempted by the exact bound adapter can return `AuthorizedContextOperationError::Adapter`. No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. ## Lossless recovery evidence while retained -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records the exact owned handle. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records both the exact owned handle and the exact validated `BrowserContextEpoch` that was sent on the destroy request. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. Cause-specific evidence is retained for the triggering handle and is not duplicated as generic sibling evidence. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. -The active successor still has open recovery-correlation work: create uncertainty and create-completion failures must retain the exact aggregate-issued attempt epoch; destructive and purpose-bounded adapter requests must retain the exact validated context epoch as non-authorizing provenance; `RecoveryRequired`/`TransportLost` need a purpose-bounded handoff that keeps the exact same adapter with the exact evidence instead of reconstructing a second adapter. +Operation/destroy epoch provenance is now implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch. The active successor still has open recovery-correlation work: create uncertainty and create-completion failures must retain the exact aggregate-issued attempt epoch; `RecoveryRequired`/`TransportLost` need a purpose-bounded handoff that keeps the exact same adapter with the exact evidence instead of reconstructing a second adapter. ## Abandonment and lifecycle completion @@ -105,13 +106,15 @@ OriginWeave does not treat protocol identifiers as policy authority or assume hi | completion failure fails closed | `UnsettledAdapterHandle`; internal completion-failure tests | | raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | | same consumed adapter handles authorized post-create work | `AuthorizedContextOperationPort`; `authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io` | +| authorized operation carries exact validated epoch | `AuthorizedContextOperationRequest::context_epoch`; `authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io` | | stale operation authority fails before adapter I/O | `AuthorizedContextOperationError::BrowserSession`; authorized-operation hostile fixture | +| destroy request carries exact validated epoch | `DisposableContextDestroyRequest::context_epoch`; `destroy_failure_requires_recovery_before_any_new_authority` | +| unproven destroy preserves exact handle + validated epoch | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; `destroy_failure_requires_recovery_before_any_new_authority` | | adapter-owned Debug is not executed or rendered | manual `Debug for BoundBrowserSession

`; `bound_session_debug_never_executes_or_exposes_adapter_debug` | | sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | | lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; recovery tests | | `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings` | | transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | -| unproven destruction retains exact handle | `destroy_failure_requires_recovery_before_any_new_authority` | | unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | | failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | | normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | From 31be3c73cc6682cf125cb148a6e6fe97fde29737 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:23:31 +0900 Subject: [PATCH 055/632] test(browser-session): repair epoch-aware sibling recovery evidence --- .../tests/recovery_required_sibling_evidence.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs index 164e217d7..741bd015f 100644 --- a/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs +++ b/crates/originweave-browser-session/tests/recovery_required_sibling_evidence.rs @@ -51,9 +51,9 @@ fn existing_exact_handle( match evidence { BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(handle) | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle) - | BrowserSessionRecoveryEvidence::UnprovenDestruction(handle) | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(handle) | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(handle) => Some(handle), + BrowserSessionRecoveryEvidence::UnprovenDestruction { context, .. } => Some(context), BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => None, } } @@ -72,6 +72,7 @@ fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() let first_authority = bound .create_disposable_context() .expect("first accepted context"); + let first_epoch = first_authority.context_epoch(); let _sibling_authority = bound .create_disposable_context() .expect("second accepted context"); @@ -87,10 +88,11 @@ fn recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings() let evidence = bound.browser_session().recovery_evidence(); assert!( - evidence.contains(&BrowserSessionRecoveryEvidence::UnprovenDestruction( - first.clone() - )), - "the directly failed destruction must keep its cause-specific evidence" + evidence.contains(&BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: first.clone(), + context_epoch: first_epoch, + }), + "the directly failed destruction must keep its cause-specific handle and validated epoch" ); assert!( evidence.contains( From 974039b6079527de0aa0075643c76bf85fee33e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:41:57 +0900 Subject: [PATCH 056/632] test(browser-session): assert transport-loss evidence discriminator --- .../tests/transport_loss_recovery_evidence.rs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs index 90c6b8b69..5d0a9677f 100644 --- a/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs +++ b/crates/originweave-browser-session/tests/transport_loss_recovery_evidence.rs @@ -2,10 +2,11 @@ use std::cell::Cell; use std::rc::Rc; use originweave_browser_session::{ - BrowserSession, BrowserSessionState, DisposableContextCreateCompletion, - DisposableContextCreateCompletionError, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + BrowserSession, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -89,18 +90,15 @@ fn transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evide let evidence = bound.browser_session().recovery_evidence(); assert_eq!( - evidence.len(), - 1, - "the exact previously owned handle must remain externally recoverable after transport loss" - ); - let rendered = format!("{:?}", evidence[0]); - assert!( - rendered.contains("transport-user-context-501"), - "recovery evidence lost the exact disposable isolation identity: {rendered}" - ); - assert!( - rendered.contains("501"), - "recovery evidence lost the exact browsing-context identity: {rendered}" + evidence, + &[BrowserSessionRecoveryEvidence::TransportLossOwnedHandle( + DisposableContextHandle::new( + DisposableIsolationId::parse("transport-user-context-501") + .expect("valid isolation id"), + BrowsingContextId::new(501).expect("valid browsing context"), + ), + )], + "transport loss must retain the exact identity-bearing evidence discriminator and handle" ); assert!(!bound.record_transport_loss()); From 70b5b93f2973978992965d4d96fa146c8b39fe46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:44:42 +0900 Subject: [PATCH 057/632] fix(browser-session): remove invented user-context cap and abandonment false positive --- crates/originweave-browser-session/src/lib.rs | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index c2060905c..882c712df 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -88,8 +88,6 @@ pub enum DisposableContextDestroyError { pub enum DisposableIsolationIdError { /// The identity is empty. Empty, - /// The identity exceeds the bounded adapter evidence size. - TooLong, /// The identity contains surrounding whitespace or control characters. InvalidCharacter, } @@ -103,14 +101,11 @@ pub enum DisposableIsolationIdError { pub struct DisposableIsolationId(String); impl DisposableIsolationId { - /// Parse one bounded browser-issued isolation identity. + /// Parse one browser-issued isolation identity without inventing a protocol length limit. pub fn parse(value: &str) -> Result { if value.is_empty() { return Err(DisposableIsolationIdError::Empty); } - if value.len() > 4096 { - return Err(DisposableIsolationIdError::TooLong); - } if value.trim() != value || value.chars().any(char::is_control) { return Err(DisposableIsolationIdError::InvalidCharacter); } @@ -931,15 +926,13 @@ impl BrowserSession { } fn has_unresolved_remote_ownership(&self) -> bool { - matches!( - self.state, - BrowserSessionState::TransportLost | BrowserSessionState::RecoveryRequired - ) || self.contexts.values().any(|record| { - matches!( - record.state, - OwnedContextState::Active | OwnedContextState::Uncertain - ) - }) + matches!(self.state, BrowserSessionState::RecoveryRequired) + || self.contexts.values().any(|record| { + matches!( + record.state, + OwnedContextState::Active | OwnedContextState::Uncertain + ) + }) } } @@ -1177,15 +1170,15 @@ mod tests { } #[test] - fn isolation_identity_validation_is_bounded() { + fn isolation_identity_validation_preserves_protocol_text() { assert_eq!( DisposableIsolationId::parse(""), Err(DisposableIsolationIdError::Empty) ); - assert_eq!( - DisposableIsolationId::parse(&"x".repeat(4097)), - Err(DisposableIsolationIdError::TooLong) - ); + let long = "x".repeat(4097); + let long_identity = DisposableIsolationId::parse(&long) + .expect("WebDriver BiDi browser.UserContext does not define a 4096-byte limit"); + assert_eq!(long_identity.as_str(), long); assert_eq!( DisposableIsolationId::parse(" user-context "), Err(DisposableIsolationIdError::InvalidCharacter) From 7328a23429ca51113f187acf56388fe7351f25fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:45:09 +0900 Subject: [PATCH 058/632] test(browser-session): reject empty transport-loss abandonment --- .../tests/bound_session_abandonment.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/originweave-browser-session/tests/bound_session_abandonment.rs b/crates/originweave-browser-session/tests/bound_session_abandonment.rs index 6106b5f24..3c57318d3 100644 --- a/crates/originweave-browser-session/tests/bound_session_abandonment.rs +++ b/crates/originweave-browser-session/tests/bound_session_abandonment.rs @@ -82,6 +82,45 @@ fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() ); } +#[test] +fn transport_loss_without_remote_ownership_is_not_counted_as_abandonment() { + let _guard = ABANDONMENT_COUNTER_LOCK + .lock() + .expect("abandonment counter test lock"); + let destroy_calls = Rc::new(Cell::new(0)); + let before = abandoned_bound_session_count(); + + let empty_session = BrowserSession::start(BrowserSessionId::new(507).expect("valid session id")) + .expect("incarnation capacity"); + let mut empty_bound = empty_session.bind_lifecycle_port(port_for(507, &destroy_calls)); + assert!(empty_bound.record_transport_loss()); + drop(empty_bound); + assert_eq!( + abandoned_bound_session_count(), + before, + "transport loss with no owned or uncertain browser context is not unresolved remote ownership" + ); + + let session = BrowserSession::start(BrowserSessionId::new(508).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(port_for(508, &destroy_calls)); + let authority = bound + .create_disposable_context() + .expect("accepted disposable context"); + bound + .destroy_disposable_context(&authority) + .expect("proven destruction"); + assert!(bound.record_transport_loss()); + drop(bound); + + assert_eq!(destroy_calls.get(), 1); + assert_eq!( + abandoned_bound_session_count(), + before, + "transport loss after all remote ownership was proven destroyed must not create a false abandonment signal" + ); +} + #[test] fn failed_finish_retains_same_bound_owner_for_cleanup_and_retry() { let _guard = ABANDONMENT_COUNTER_LOCK From 24b08275413586b2759f2e4ec2de0a6255ac5734 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 23:54:08 +0900 Subject: [PATCH 059/632] fix(browser-session): clear strict Clippy blockers --- crates/originweave-browser-session/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 882c712df..c0a8538f1 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -595,7 +595,6 @@ impl BrowserSession { /// /// Binding invokes no adapter method. All subsequent create/destroy I/O is reachable only through /// the owned port inside the returned wrapper. - #[must_use] pub fn bind_lifecycle_port(self, port: P) -> BoundBrowserSession

{ BoundBrowserSession { session: self, @@ -1365,7 +1364,7 @@ mod tests { let first = DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); let duplicate = DisposableContextHandle::new(isolation_id("isolation-316-b"), context_id(316)); - let mut port = TestPort::with_handles(vec![first.clone(), duplicate.clone()]); + let port = TestPort::with_handles(vec![first.clone(), duplicate.clone()]); let mut bound = session(316).bind_lifecycle_port(port); bound From 84de9247a4c15f2c430612a19cc008c42c000f08 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 00:11:33 +0900 Subject: [PATCH 060/632] test(browser-session): preserve same-handle recovery facts --- ...eate_recovery_same_handle_distinct_fact.rs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs diff --git a/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs new file mode 100644 index 000000000..b18984495 --- /dev/null +++ b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs @@ -0,0 +1,103 @@ +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateDisposition, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug)] +struct SameHandleRejectedCompletionPort { + handle: DisposableContextHandle, + create_attempts: Vec, + completion_attempts: Vec<(u64, DisposableContextCreateDisposition)>, +} + +impl SameHandleRejectedCompletionPort { + fn new(handle: DisposableContextHandle) -> Self { + Self { + handle, + create_attempts: Vec::new(), + completion_attempts: Vec::new(), + } + } +} + +impl DisposableContextPort for SameHandleRejectedCompletionPort { + fn create_disposable_context( + &mut self, + request: &DisposableContextCreateRequest, + ) -> Result { + self.create_attempts.push(request.attempt_epoch().value()); + Ok(self.handle.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + self.completion_attempts + .push((completion.attempt_epoch().value(), completion.disposition())); + if completion.disposition() == DisposableContextCreateDisposition::Rejected { + Err(DisposableContextCreateCompletionError::CompletionFailed) + } else { + Ok(()) + } + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +#[test] +fn same_valued_rejected_create_keeps_prior_ownership_as_a_distinct_recovery_fact() { + let handle = DisposableContextHandle::new( + DisposableIsolationId::parse("same-valued-create-recovery") + .expect("valid browser-issued isolation identity"), + BrowsingContextId::new(94_001).expect("valid browsing-context identity"), + ); + let session = BrowserSession::start( + BrowserSessionId::new(94_001).expect("valid browser-session identity"), + ) + .expect("browser-session incarnation capacity"); + let port = SameHandleRejectedCompletionPort::new(handle.clone()); + let mut bound = session.bind_lifecycle_port(port); + + let first_authority = bound + .create_disposable_context() + .expect("attempt 1 becomes the accepted owned context"); + assert_eq!(first_authority.context_epoch().value(), 1); + + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain), + "attempt 2 returns the same remote handle but its rejected completion is unproven" + ); + + assert_eq!(bound.browser_session().recovery_evidence().len(), 3); + assert!(bound + .browser_session() + .recovery_evidence() + .contains(&BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( + handle.clone() + ))); + assert!(bound + .browser_session() + .recovery_evidence() + .contains(&BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + handle.clone() + ))); + assert!(bound + .browser_session() + .recovery_evidence() + .contains(&BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle( + handle + )), + "attempt 1 ownership and attempt 2 candidate are distinct lifecycle facts even when their remote handle values are equal" + ); +} From e776115c415280878157ae5ac64b42d3794ae015 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:05:03 +0900 Subject: [PATCH 061/632] fix(browser-session): correlate create recovery attempts --- crates/originweave-browser-session/src/lib.rs | 147 ++++++++++++++++-- 1 file changed, 138 insertions(+), 9 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index c0a8538f1..47d8ccc0a 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -196,6 +196,39 @@ pub enum BrowserSessionRecoveryEvidence { TransportLossOwnedHandle(DisposableContextHandle), } +/// Exact create-attempt facts retained when one Browser Session creation transaction becomes uncertain. +/// +/// The legacy identity-oriented [`BrowserSessionRecoveryEvidence`] remains useful to recovery code that +/// reconciles remote handles. This companion evidence preserves the aggregate-issued attempt epoch and +/// completion disposition so two lifecycle facts with the same remote values cannot be collapsed into +/// one transaction. These values grant no browser command authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisposableContextCreateRecoveryEvidence { + /// The adapter reported an uncertain create failure before a complete handle was available. + FailedUncertain { + /// Exact aggregate-issued epoch reserved for the failed create attempt. + attempt_epoch: BrowserContextEpoch, + /// Browser-issued isolation identity known at the failure boundary, when available. + isolation: Option, + }, + /// A complete candidate aliased already-owned browser state and was rejected by the aggregate. + DuplicateCandidate { + /// Exact aggregate-issued epoch reserved for the rejected create attempt. + attempt_epoch: BrowserContextEpoch, + /// Exact adapter-returned candidate associated with that attempt. + context: DisposableContextHandle, + }, + /// The adapter could not prove completion settlement for one exact create attempt. + CompletionUnsettled { + /// Exact aggregate-issued epoch reserved for the unsettled create attempt. + attempt_epoch: BrowserContextEpoch, + /// Aggregate decision whose delivery to the adapter could not be proven. + disposition: DisposableContextCreateDisposition, + /// Exact adapter-returned candidate associated with that attempt. + context: DisposableContextHandle, + }, +} + /// Opaque Browser Session-issued request for one disposable-context creation attempt. /// /// There is deliberately no public constructor. A request is created only inside a @@ -523,6 +556,7 @@ pub struct BrowserSession { next_epoch: u64, contexts: BTreeMap, recovery_evidence: Vec, + create_recovery_evidence: Vec, } /// Browser Session composed with the one lifecycle-port instance allowed to mutate its remote state. @@ -549,6 +583,10 @@ impl

fmt::Debug for BoundBrowserSession

{ "recovery_evidence_count", &self.session.recovery_evidence.len(), ) + .field( + "create_recovery_evidence_count", + &self.session.create_recovery_evidence.len(), + ) .field("port", &"") .finish() } @@ -557,7 +595,7 @@ impl

fmt::Debug for BoundBrowserSession

{ impl

Drop for BoundBrowserSession

{ fn drop(&mut self) { if self.session.has_unresolved_remote_ownership() { - let _ = ABANDONED_BOUND_SESSIONS.fetch_update( + let _ = ABANDONED_BOUND_SESSIONS.try_update( Ordering::Relaxed, Ordering::Relaxed, |value| Some(value.saturating_add(1)), @@ -588,6 +626,7 @@ impl BrowserSession { next_epoch: 1, contexts: BTreeMap::new(), recovery_evidence: Vec::new(), + create_recovery_evidence: Vec::new(), }) } @@ -632,6 +671,12 @@ impl BrowserSession { &self.recovery_evidence } + /// Return exact create-attempt recovery facts retained for transaction correlation. + #[must_use] + pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { + &self.create_recovery_evidence + } + /// Return current presentation authority for an already-owned active context. pub fn presentation_authority( &self, @@ -731,6 +776,12 @@ impl BrowserSession { return Err(BrowserSessionError::ContextCreationFailed); } Err(DisposableContextCreateError::CreateFailedUncertain(isolation)) => { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch: epoch, + isolation: isolation.clone(), + }, + ); if let Some(isolation) = isolation { self.recovery_evidence.push( BrowserSessionRecoveryEvidence::PartialCreationIsolation(isolation), @@ -760,6 +811,12 @@ impl BrowserSession { attempt_epoch: epoch, disposition: DisposableContextCreateDisposition::Rejected, }; + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { + attempt_epoch: epoch, + context: handle.clone(), + }, + ); self.recovery_evidence .push(BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( handle.clone(), @@ -768,6 +825,13 @@ impl BrowserSession { .complete_disposable_context_creation(&completion) .is_err() { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Rejected, + context: handle.clone(), + }, + ); self.recovery_evidence.push( BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), ); @@ -788,6 +852,13 @@ impl BrowserSession { .complete_disposable_context_creation(&completion) .is_err() { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Accepted, + context: handle.clone(), + }, + ); self.recovery_evidence .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( handle, @@ -893,10 +964,10 @@ impl BrowserSession { .map(|record| record.handle.clone()) .filter(|handle| { !self.recovery_evidence.iter().any(|evidence| match evidence { - BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => false, - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(existing) - | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) + | BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(_) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(_) => false, + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { existing == handle } @@ -1038,7 +1109,7 @@ fn allocate_incarnation( counter: &AtomicU64, ) -> Result { let value = counter - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { + .try_update(Ordering::SeqCst, Ordering::SeqCst, |current| { current.checked_add(1) }) .map_err(|_| BrowserSessionError::IncarnationExhausted)?; @@ -1254,6 +1325,17 @@ mod tests { Err(BrowserSessionError::ContextCreationUncertain) ); assert!(unknown.browser_session().recovery_evidence().is_empty()); + assert_eq!(unknown.browser_session().create_attempt_recovery_evidence().len(), 1); + match &unknown.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch, + isolation, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(isolation, &None); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } let known = isolation_id("partial-user-context-211"); let mut known_port = TestPort::new(211, "unused"); @@ -1268,9 +1350,20 @@ mod tests { assert_eq!( known_session.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::PartialCreationIsolation( - known + known.clone() )] ); + assert_eq!(known_session.browser_session().create_attempt_recovery_evidence().len(), 1); + match &known_session.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch, + isolation, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(isolation.as_ref(), Some(&known)); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } assert_eq!( known_session.end(), Err(BrowserSessionError::SessionNotActive) @@ -1346,9 +1439,22 @@ mod tests { assert_eq!( bound.browser_session().recovery_evidence(), &[BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( - expected + expected.clone() )] ); + assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 1); + match &bound.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch, + disposition, + context, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(*disposition, DisposableContextCreateDisposition::Accepted); + assert_eq!(context, &expected); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } assert_eq!( bound.port.create_completions[0].3, DisposableContextCreateDisposition::Accepted @@ -1379,10 +1485,33 @@ mod tests { bound.browser_session().recovery_evidence(), &[ BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate.clone()), - BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate), + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate.clone()), BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first), ] ); + assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 2); + match &bound.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { + attempt_epoch, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(context, &duplicate); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + match &bound.browser_session().create_attempt_recovery_evidence()[1] { + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch, + disposition, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(*disposition, DisposableContextCreateDisposition::Rejected); + assert_eq!(context, &duplicate); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } assert_eq!( bound.port.create_completions[1].3, DisposableContextCreateDisposition::Rejected From 0cb08726ac446575e83bb8fab3733417f800ee3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:05:33 +0900 Subject: [PATCH 062/632] test(browser-session): assert create attempt recovery correlation --- ...eate_recovery_same_handle_distinct_fact.rs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs index b18984495..8a4edba86 100644 --- a/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs +++ b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs @@ -2,8 +2,9 @@ use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, DisposableContextCreateDisposition, DisposableContextCreateError, - DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, - DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + DisposableContextCreateRecoveryEvidence, DisposableContextCreateRequest, + DisposableContextDestroyError, DisposableContextDestroyRequest, DisposableContextHandle, + DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -96,8 +97,33 @@ fn same_valued_rejected_create_keeps_prior_ownership_as_a_distinct_recovery_fact .browser_session() .recovery_evidence() .contains(&BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle( - handle + handle.clone() )), "attempt 1 ownership and attempt 2 candidate are distinct lifecycle facts even when their remote handle values are equal" ); + + let create_evidence = bound.browser_session().create_attempt_recovery_evidence(); + assert_eq!(create_evidence.len(), 2); + match &create_evidence[0] { + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { + attempt_epoch, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(context, &handle); + } + other => panic!("unexpected duplicate recovery evidence: {other:?}"), + } + match &create_evidence[1] { + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch, + disposition, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(*disposition, DisposableContextCreateDisposition::Rejected); + assert_eq!(context, &handle); + } + other => panic!("unexpected completion recovery evidence: {other:?}"), + } } From 79ca35cd951fa4f901a1a00af3926c4f067f3477 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:09:57 +0900 Subject: [PATCH 063/632] docs(browser-session): trace create recovery correlation --- .../browser-session-lifecycle-authority.md | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 3776bc847..11453954b 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -23,6 +23,7 @@ validated BrowserSessionId → aggregate validates returned isolation/context against current ownership → aggregate privately constructs DisposableContextCreateCompletion(attempt, Accepted|Rejected) → accepted candidate may become adapter-authorizing; rejected candidate remains quarantined +→ uncertain/rejected create paths retain exact aggregate-issued attempt identity as non-authorizing recovery evidence → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) → exact authority validation before any lifecycle or purpose-bounded adapter I/O @@ -50,7 +51,11 @@ Browser Session examines the returned `DisposableContextHandle`: - if the handle aliases an existing isolation or browsing context, `Rejected` settles that exact attempt and the aggregate enters `RecoveryRequired`; - if exact completion cannot be proven, Browser Session stores the complete handle as `UnsettledAdapterHandle`, enters recovery, and mints no normal authority. -Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only attempt identity, domain validation, accept/reject decision, and current authority validation. +`DisposableContextCreateRecoveryEvidence` preserves the transaction dimension that raw handle evidence cannot represent. `FailedUncertain` stores the exact aggregate-issued `attempt_epoch` even when no complete handle exists; `DuplicateCandidate` binds an aliased returned handle to its exact rejected attempt; `CompletionUnsettled` binds the exact attempt, `Accepted|Rejected` disposition, and complete returned handle when settlement cannot be proven. This evidence grants no browser authority. + +Identity-oriented `BrowserSessionRecoveryEvidence` remains separately useful for exact remote reconciliation. Candidate evidence such as `DuplicateAdapterHandle(H)` or `UnsettledAdapterHandle(H)` is not treated as proof that an already-owned same-valued `H` has been recorded: the existing owner is retained independently as `RecoveryRequiredOwnedHandle(H)`. Equal remote values therefore cannot collapse distinct lifecycle facts from different create attempts. + +Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only attempt identity, domain validation, accept/reject decision, current authority validation, and non-authorizing recovery facts. ## Same-bound-adapter authorized operations @@ -60,17 +65,19 @@ Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSess ## Lossless recovery evidence while retained -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains the exact known isolation identity. Duplicate output stores the complete offending handle. Completion failure retains an unsettled complete handle. Failed or unproven destruction records both the exact owned handle and the exact validated `BrowserContextEpoch` that was sent on the destroy request. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains both the exact known isolation identity and the exact aggregate-issued create-attempt epoch; `CreateFailedUncertain(None)` still retains the exact attempt epoch. Duplicate output stores the complete offending handle and its exact rejected attempt. Completion failure retains the complete handle, attempt epoch, and aggregate disposition. Failed or unproven destruction records both the exact owned handle and the exact validated `BrowserContextEpoch` that was sent on the destroy request. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. -Cause-specific evidence is retained for the triggering handle and is not duplicated as generic sibling evidence. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. +Cause-specific candidate evidence is retained separately from generic sibling ownership evidence. A same-valued candidate from a later failed attempt cannot erase a previously accepted ownership fact. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. -Operation/destroy epoch provenance is now implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch. The active successor still has open recovery-correlation work: create uncertainty and create-completion failures must retain the exact aggregate-issued attempt epoch; `RecoveryRequired`/`TransportLost` need a purpose-bounded handoff that keeps the exact same adapter with the exact evidence instead of reconstructing a second adapter. +Operation/destroy/create-attempt provenance is implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch; create uncertainty and completion failure retain the reserved `attempt_epoch` in `DisposableContextCreateRecoveryEvidence`. The remaining recovery-boundary gap is a purpose-bounded `RecoveryRequired`/`TransportLost` handoff that keeps the exact same adapter and exact evidence instead of reconstructing a second adapter or restoring ordinary mutation authority. ## Abandonment and lifecycle completion `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` succeeds only after all owned contexts have proven destruction. If it returns `ActiveContextRemains`, the wrapper, exact bound adapter, and private ownership ledger remain intact. The same owner can therefore destroy or reconcile the remaining context and retry `finish()` without introducing a second adapter or ambient cleanup capability. -`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. +`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists Browser Session recovery evidence before process termination. + +The abandonment counter and Browser Session incarnation allocator use `AtomicU64::try_update` with the same memory-ordering and closure semantics as the predecessor `fetch_update` calls. This removes the pinned-nightly deprecation without weakening overflow behavior or synchronization semantics. ## Orthogonal transport liveness @@ -82,11 +89,11 @@ Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate ## Browser-issued user-context identity -`DisposableIsolationId` maps one-to-one to the browser-issued WebDriver BiDi `browser.UserContext` identity. That value is addressability and recovery evidence, not command authority. OriginWeave must preserve a protocol-valid browser identity losslessly so the exact remote boundary can later be destroyed or reconciled. The current active branch still contains a historical 4096-byte parser ceiling; `user_context_identity_length.rs` intentionally keeps that mismatch RED until the arbitrary domain constant is removed or replaced by a cited, versioned protocol/runtime/deployment boundary. No truncation or normalization is acceptable for a browser-issued identity. +`DisposableIsolationId` maps one-to-one to the browser-issued WebDriver BiDi `browser.UserContext` identity. That value is addressability and recovery evidence, not command authority. OriginWeave preserves a protocol-valid browser identity losslessly so the exact remote boundary can later be destroyed or reconciled. The previous arbitrary 4096-byte parser ceiling has been removed; the hostile 4097-byte fixture and internal round-trip test now require lossless preservation. No truncation or normalization is acceptable for a browser-issued identity. ## Standards trace -The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-12 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. `browser.UserContext` is defined as `text`; the published protocol does not define the active branch's 4096-byte domain ceiling. +The latest immutable W3C WebDriver BiDi Working Draft directly verified on 2026-09-15 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. The mutable `/TR/webdriver-bidi/` index can lag this dated publication and is not used to erase immutable provenance. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. `browser.UserContext` is defined as `text`; the published protocol defines no 4096-byte domain ceiling. Standards freshness and runtime qualification are separate controls. Updating this citation does not repin the separately qualified Chromium/WebDriver BiDi runtime revision. @@ -102,6 +109,10 @@ OriginWeave does not treat protocol identifiers as policy authority or assume hi | binding performs no arbitrary adapter callback | `BrowserSession::bind_lifecycle_port`; `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` | | no self-asserted adapter id authority | absence of `DisposableContextPortId` / `port_id()` | | create requests are aggregate-issued and attempt-scoped | `DisposableContextCreateRequest::attempt_epoch`; transaction hostile fixture | +| uncertain create preserves exact transaction identity with or without a complete handle | `DisposableContextCreateRecoveryEvidence::FailedUncertain`; internal Some/None recovery tests | +| duplicate candidate retains exact rejected attempt | `DisposableContextCreateRecoveryEvidence::DuplicateCandidate`; `create_recovery_same_handle_distinct_fact.rs` | +| completion failure retains exact attempt + disposition + handle | `DisposableContextCreateRecoveryEvidence::CompletionUnsettled`; internal accepted/rejected completion tests | +| same-valued prior owner and later candidate remain distinct recovery facts | `RecoveryRequiredOwnedHandle`; `create_recovery_same_handle_distinct_fact.rs` | | per-create transaction settles accepted/rejected candidates | `DisposableContextCreateCompletion`; `accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt` | | completion failure fails closed | `UnsettledAdapterHandle`; internal completion-failure tests | | raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | @@ -112,13 +123,14 @@ OriginWeave does not treat protocol identifiers as policy authority or assume hi | unproven destroy preserves exact handle + validated epoch | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; `destroy_failure_requires_recovery_before_any_new_authority` | | adapter-owned Debug is not executed or rendered | manual `Debug for BoundBrowserSession

`; `bound_session_debug_never_executes_or_exposes_adapter_debug` | | sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | -| lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; recovery tests | +| lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; `DisposableContextCreateRecoveryEvidence`; recovery tests | | `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings` | | transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | | unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | | failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | | normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | -| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs` (currently RED against the historical 4096-byte ceiling) | +| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs`; internal 4097-byte round-trip test | +| atomic lifecycle counters use the non-deprecated API without changing ordering | `ABANDONED_BOUND_SESSIONS.try_update`; `allocate_incarnation` | | transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | | normal end requires proved destruction | `BrowserSession::end` | | incarnation exhaustion fails closed | `allocate_incarnation` | From 0f539a5a675a63cb89810ce802b1c5e41211c581 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:11:10 +0900 Subject: [PATCH 064/632] docs(adr): bind create recovery to attempt identity --- ...er-session-disposable-context-authority.md | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index c49b2d251..415b3bdfc 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -2,17 +2,17 @@ - Status: Proposed - Date: 2026-09-10 -- Last code-current review: 2026-09-12 +- Last code-current review: 2026-09-15 ## Context OriginWeave's Browser Session bounded context is the domain authority for disposable browser lifecycle ownership and presentation mutation. WebDriver BiDi session ids, user-context ids, browsing-context ids, and adapter-selected values are protocol addressability, not authorization. -The active implementation has to satisfy four constraints at once. First, `BoundBrowserSession

` must consume the one concrete lifecycle adapter without later exposing raw `&P`/`&mut P` or a replacement-port path. Second, one Browser Session incarnation can issue multiple remote creates, so each result requires an aggregate-issued per-create transaction identity before it may become authorizing. Third, dependent WebDriver BiDi presentation and reconciliation work still needs to reach the same consumed adapter after exact `PresentationMutationAuthority` validation; retaining a second adapter or generic raw callback would recreate the capability-substitution defect. Fourth, uncertain lifecycle outcomes must preserve every exact non-authorizing owned handle needed for recovery, including siblings invalidated indirectly by another context's failure, and a failed `finish()` must not discard the same bound adapter needed to repair the rejected completion. +The active implementation has to satisfy four constraints at once. First, `BoundBrowserSession

` must consume the one concrete lifecycle adapter without later exposing raw `&P`/`&mut P` or a replacement-port path. Second, one Browser Session incarnation can issue multiple remote creates, so each result requires an aggregate-issued per-create transaction identity before it may become authorizing and every uncertain create outcome must retain that identity for recovery. Third, dependent WebDriver BiDi presentation and reconciliation work still needs to reach the same consumed adapter after exact `PresentationMutationAuthority` validation; retaining a second adapter or generic raw callback would recreate the capability-substitution defect. Fourth, uncertain lifecycle outcomes must preserve every exact non-authorizing owned handle needed for recovery, including siblings invalidated indirectly by another context's failure, and a failed `finish()` must not discard the same bound adapter needed to repair the rejected completion. -Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. +Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Two lifecycle facts can have identical remote handle values while belonging to different create attempts; raw-handle equality must not collapse an accepted owner and a later rejected/unsettled candidate. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. -The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-12 is the 9 September 2026 publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous version. It defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. The specification requires the user-context id to uniquely identify a user context but does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. Runtime-qualified protocol/browser revisions remain separately controlled and are not repinned by this standards-trace update. +The immutable WebDriver BiDi Working Draft directly verified on 2026-09-15 is the 9 September 2026 publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous version. The mutable `/TR/webdriver-bidi/` index currently lags that dated publication, so dated provenance is retained from the immutable URI rather than inferred from the mutable index. The publication defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. It does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. Runtime-qualified protocol/browser revisions remain separately controlled and are not repinned by this standards-trace update. ## Decision drivers @@ -21,8 +21,9 @@ The latest W3C-published WebDriver BiDi Working Draft verified on 2026-09-12 is - No arbitrary adapter callback may be required to establish lifecycle-port ownership. - A caller must not be able to substitute or recover the concrete adapter after Browser Session binding. - Browser Session create/destroy capabilities remain non-caller-constructible. -- Every successful remote create result must be correlated to one exact Browser Session-issued attempt before it can become authorizing. +- Every remote create attempt and every uncertain/rejected result must be correlated to one exact Browser Session-issued attempt. - Browser Session, not the adapter, decides whether a returned domain handle is accepted or rejected. +- Equal remote handle values do not imply equal lifecycle facts; owner evidence and later candidate evidence require separate provenance. - Protocol-specific pending/accepted/quarantined tuples remain the WebDriver BiDi ACL owner's truth. - Presentation/reconciliation I/O must use the exact consumed adapter only after current aggregate authority validation. - Diagnostic formatting must not invoke adapter-owned `Debug` or expose adapter-internal state. @@ -45,21 +46,24 @@ Introduce and retain `originweave-browser-session` as an independent Rust bounde 7. After `create_disposable_context` returns a handle, Browser Session validates isolation and browsing-context ownership before granting authority. 8. Browser Session privately issues `DisposableContextCreateCompletion` for that exact attempt with `Accepted` or `Rejected`. 9. The adapter must keep a successful remote create result non-authorizing until the matching `Accepted` completion. `Rejected` results remain non-authorizing recovery/quarantine state. A completion that cannot be proven for the exact pending attempt fails closed and sends the aggregate to `RecoveryRequired`. -10. Protocol-specific remote tuple contents are not copied into Browser Session. #314/#316 owns WebDriver BiDi pending/accepted/quarantined storage and remote-liveness validation. -11. `DisposableIsolationId` preserves the browser-issued isolation identity exactly for create/destroy/recovery addressability. It must not truncate, normalize, hash, or reject an otherwise protocol-valid `browser.UserContext` solely because of an arbitrary local identifier-length constant. Resource-exhaustion limits, where required, belong at a cited protocol/frame/runtime boundary or an explicit deployment policy that still preserves lossless recovery evidence. -12. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, and the exact stored handle. -13. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. -14. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired`; any known isolation identity is preserved exactly. -15. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, and sends a `Rejected` completion for the exact attempt. OriginWeave does not auto-destroy ambiguous output. -16. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle, `RecoveryRequiredOwnedHandle` for every still-active sibling made uncertain by a recovery transition, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. Cause-specific evidence is not duplicated as generic sibling evidence. These values are explicit **unproven destruction** evidence rather than cleanup proof and grant no browser command authority. Repeated recovery/loss observation must not duplicate exact-handle evidence. -17. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle is retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. Any other active sibling is projected as `RecoveryRequiredOwnedHandle` before it becomes uncertain. -18. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. -19. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. -20. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. -21. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. -22. `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` admits normal completion only after all owned contexts have proven destruction. A failed `finish()` returns the domain error without consuming or dropping the wrapper, so the same exact bound adapter and ownership ledger remain available for cleanup/reconciliation and retry. After success the aggregate is `Ended`, and later wrapper destruction is inert. `Drop` never performs browser I/O; if unresolved remote ownership remains, it increments the process-local `abandoned_bound_session_count()` operability signal. -23. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery handles must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. -24. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. +10. `DisposableContextCreateRecoveryEvidence` preserves create-transaction identity independently from remote-handle reconciliation: `FailedUncertain` retains the exact aggregate-issued attempt epoch plus optional known isolation; `DuplicateCandidate` retains exact attempt epoch plus complete candidate handle; `CompletionUnsettled` retains exact attempt epoch, `Accepted|Rejected` disposition, and complete candidate handle. These records grant no browser command authority. +11. Candidate-oriented `DuplicateAdapterHandle` and `UnsettledAdapterHandle` do not count as owner-oriented recovery evidence solely because their handle values equal a previously accepted owner. `RecoveryRequiredOwnedHandle` remains a separate lifecycle fact for that owner. +12. Protocol-specific remote tuple contents are not copied into Browser Session. #314/#316 owns WebDriver BiDi pending/accepted/quarantined storage and remote-liveness validation. +13. `DisposableIsolationId` preserves the browser-issued isolation identity exactly for create/destroy/recovery addressability. It must not truncate, normalize, hash, or reject an otherwise protocol-valid `browser.UserContext` solely because of an arbitrary local identifier-length constant. Resource-exhaustion limits, where required, belong at a cited protocol/frame/runtime boundary or an explicit deployment policy that still preserves lossless recovery evidence. +14. `DisposableContextDestroyRequest` remains opaque and is created only after exact presentation-authority validation. It carries Browser Session addressability, incarnation, the exact stored handle, and the exact validated context epoch for provenance/correlation. +15. `PresentationMutationAuthority` binds browser session, Browser Session incarnation, disposable isolation, browsing context, and context epoch. All fields must match current aggregate ownership before adapter I/O. +16. `DisposableContextCreateError::CreateFailedClean` is valid only when no remote boundary exists. `CreateFailedUncertain(Option)` enters `RecoveryRequired` and retains the exact create attempt even if the optional isolation is absent; any known isolation identity is preserved exactly. +17. Duplicate browsing-context or isolation output enters `RecoveryRequired`, stores the complete offending `DisposableContextHandle`, stores the exact rejected create-attempt fact, and sends a `Rejected` completion for that exact attempt. OriginWeave does not auto-destroy ambiguous output. +18. `BrowserSessionRecoveryEvidence` includes partial-creation identity, duplicate handle, unsettled complete adapter handle, exact unproven-destruction handle plus validated epoch, `RecoveryRequiredOwnedHandle` for every still-active sibling made uncertain by a recovery transition, and `TransportLossOwnedHandle` for each active handle whose remote liveness becomes uncertain on transport loss. These values are explicit **unproven destruction** evidence rather than cleanup proof and grant no browser command authority. Repeated recovery/loss observation must not duplicate the same owner-specific evidence. +19. Destruction validates exact authority before I/O. `DisposableContextDestroyError::DestroyFailed` means destruction was not proven; the owned record becomes uncertain, the exact failed handle and validated context epoch are retained as `UnprovenDestruction`, and the aggregate enters recovery rather than treating command acknowledgement or bookkeeping as cleanup proof. Any other active sibling is projected as `RecoveryRequiredOwnedHandle` before it becomes uncertain. +20. Transport liveness is stored separately from ownership state. The first `record_transport_loss()` records exact previously active handles as non-authorizing recovery evidence, marks them uncertain, and records the transport fact. If ownership is already `RecoveryRequired`, the stronger lifecycle state is preserved. +21. `RecoveryRequired`, `TransportLost`, and `Ended` reject normal active-only lifecycle and authority operations. +22. `AuthorizedContextOperationRequest` is non-caller-constructible. `AuthorizedContextOperationPort` lets a dependent adapter define a narrow operation vocabulary while Browser Session first validates current `PresentationMutationAuthority`, binds the exact stored handle and validated epoch, and routes the request through the same consumed adapter instance. `AuthorizedContextOperationError::BrowserSession` is returned before adapter I/O for stale/foreign authority; adapter execution errors remain separately typed. Browser Session does not own WebDriver BiDi command semantics. +23. `BoundBrowserSession

` implements a manual redacted `Debug` projection over inert Browser Session fields only. Formatting never calls `P::fmt` and never renders adapter-internal state. +24. `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` admits normal completion only after all owned contexts have proven destruction. A failed `finish()` returns the domain error without consuming or dropping the wrapper, so the same exact bound adapter and ownership ledger remain available for cleanup/reconciliation and retry. After success the aggregate is `Ended`, and later wrapper destruction is inert. `Drop` never performs browser I/O; if unresolved remote ownership remains, it increments the process-local `abandoned_bound_session_count()` operability signal. +25. The abandonment counter is deliberately not destruction proof and is not durable cross-process recovery storage. Exact recovery facts must be persisted by the separately authorized recovery owner before process termination. Until that owner path is integrated, crash/process-restart reconciliation remains an explicit buyer-acceptance gap rather than an implicit guarantee. +26. Context epochs remain monotonic authority identities within one aggregate and also provide the create-attempt correlation allocated before remote create I/O. +27. Process-local atomic counter updates use `AtomicU64::try_update` with the predecessor memory orderings and closures. This is a deprecation root fix, not a gate suppression or semantics change. ## Alternatives considered @@ -87,6 +91,10 @@ Rejected. It recreates same-key/different-adapter target redirection and lets pr Rejected as authority. It may be useful internally, but Browser Session could not prove which pending remote tuple it was accepting. Correlation must originate in the aggregate-issued request. +### Raw handle equality as recovery-fact identity + +Rejected. One accepted ownership fact and a later duplicate/unsettled candidate can carry exactly the same isolation/context values while representing different lifecycle transactions. Create-attempt evidence and owner evidence therefore remain separately typed. + ### Reserved BrowserContextEpoch as create-attempt identity Selected. Browser Session already reserves the epoch before create I/O, it is non-caller-constructible, monotonic within the aggregate, and the same value becomes the accepted context's first mutation epoch. @@ -113,9 +121,9 @@ The active stack receives a breaking trait extension for presentation/reconcilia The bound adapter is not publicly recoverable from `BoundBrowserSession`. Application and test code that needs observability retains inert metrics or diagnostic projections separately. Manual `Debug` exposes only Browser Session domain summary fields and a redacted port marker. -Entering `RecoveryRequired` now preserves exact handles for active siblings before marking them uncertain. Cause-specific evidence for the triggering context remains distinct, so recovery can enumerate every potentially live boundary without reconstructing command authority from identifiers. +Entering `RecoveryRequired` now preserves exact handles for active siblings before marking them uncertain. Create-candidate facts and active-owner facts remain distinct even when they contain the same external handle values, so recovery can enumerate every potentially live boundary without reconstructing command authority from identifiers. -Transport loss preserves exact previously active handles as non-authorizing recovery evidence. Completion or destruction failure remains ownership uncertainty and does not mint normal authority. +Create uncertainty and completion-settlement failure now retain exact aggregate-issued create-attempt provenance independently from handle-level reconciliation evidence. Transport loss preserves exact previously active handles as non-authorizing recovery evidence. Completion or destruction failure remains ownership uncertainty and does not mint normal authority. A failed `finish()` leaves the same `BoundBrowserSession` usable for cleanup/reconciliation and retry. Ordinary unresolved wrapper abandonment is process-locally observable, but exact crash/restart recovery still requires a canonical persistence/handoff path. This ADR does not claim that the in-memory counter is durable recovery. @@ -123,7 +131,7 @@ A failed `finish()` leaves the same `BoundBrowserSession` usable for cleanup/rec No page-controlled value, raw browser identifier, adapter-selected scalar, diagnostic reference, provider/model decision, or LLM output can mint lifecycle completion or presentation authority. Remote creation stays non-authorizing until the aggregate validates ownership and accepts that exact attempt. Post-create adapter I/O is admitted only through current aggregate authority and the exact consumed adapter instance. -Lossless retention of browser-issued user-context identity is an ownership requirement, not authority delegation. Resource controls must not create an untracked remote isolation boundary by discarding or rewriting the only exact addressability needed for recovery. +Lossless retention of browser-issued user-context identity and create-attempt provenance is an ownership requirement, not authority delegation. Resource controls must not create an untracked remote isolation boundary by discarding or rewriting the only exact addressability or transaction identity needed for recovery. This decision does not replace Chromium sandboxing, EgressWeave, Keyverse, Wardnet, or central workflow security. @@ -135,10 +143,12 @@ Required executable cases include: - the concrete bound adapter cannot be recovered through a public `lifecycle_port()` accessor; - a second adapter cannot be substituted for create or destroy after binding; - two successful remote create candidates in the same session incarnation receive distinct attempt epochs; +- `CreateFailedUncertain(Some/None)` retains the exact aggregate-issued attempt epoch even without a complete handle; - one candidate can be accepted and the other rejected without pending-state collision or overwrite; -- accepted-completion failure and rejected-completion failure both fail closed and preserve exact recovery evidence; +- accepted-completion failure and rejected-completion failure both fail closed and preserve exact attempt epoch, disposition, and complete candidate handle; +- an accepted owner and a later rejected/unsettled candidate with identical remote handle values remain separate lifecycle facts (`create_recovery_same_handle_distinct_fact.rs`); - an otherwise-valid browser-issued `browser.UserContext` longer than the former 4096-byte implementation threshold remains losslessly representable for exact destroy/recovery rather than being rejected by an arbitrary domain cap; -- `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; +- `DisposableContextDestroyError::DestroyFailed` preserves the exact failed handle and validated epoch, enters `RecoveryRequired`, and never counts a destroy command acknowledgement as proof; - `RecoveryRequired` preserves each indirectly invalidated active sibling exactly once as `RecoveryRequiredOwnedHandle` while retaining the triggering context's cause-specific evidence; - transport loss preserves every previously active exact handle as `TransportLossOwnedHandle` without adapter I/O or authority resurrection; - formatting a bound session does not invoke adapter-owned `Debug` and does not expose adapter-internal state; @@ -159,12 +169,14 @@ Consumers continue to bind once with `BrowserSession::bind_lifecycle_port(port)` Normal owners destroy every owned context, call `finish()`, and may then release the ended wrapper. If `finish()` rejects, they retain the same wrapper, perform permitted cleanup/reconciliation, and retry. Recovery owners must persist exact recovery evidence before terminating a process that still has unresolved ownership; the abandonment counter is an operability alert, not a persistence mechanism. -Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, consuming failed-finish path, arbitrary browser-user-context length cap, or adapter-local call order as an authorization boundary. +Rollback may return to the predecessor active-PR API only if these authority findings are disproved with stronger executable evidence. It must not restore a raw adapter accessor, derived adapter `Debug`, self-reported identity, unrestricted adapter callback, consuming failed-finish path, arbitrary browser-user-context length cap, raw-handle recovery deduplication, or adapter-local call order as an authorization boundary. ## Open follow-ups +- Define a purpose-bounded same-adapter recovery handoff for `RecoveryRequired` / `TransportLost` that carries exact recovery evidence without recreating ordinary mutation authority. +- Bound hot ownership state independently from durable/audit history so proven destruction does not create unbounded validation cost. - Restack #316 onto the verified Browser Session successor and implement WebDriver BiDi pending → accepted/quarantined transaction settlement plus typed presentation/reconciliation operations. -- Define the separately authorized durable recovery persistence/reconciliation owner for `BrowserSessionRecoveryEvidence` and unresolved abandonment. +- Define the separately authorized durable recovery persistence/reconciliation owner for Browser Session recovery evidence and unresolved abandonment. - Replay #299 historical pinned Chromium evidence after the canonical sandbox/runtime repair, then run a separate current-Stable qualification. ## Supersession / reversal conditions From 6f91abfc194ec342219115f7c1f28890a89ed03f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:12:22 +0900 Subject: [PATCH 065/632] docs(changelog): record Browser Session recovery provenance --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a317fc24e..e67d084b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Preserved Browser Session create-attempt provenance through uncertain creation, duplicate-candidate rejection, and accepted/rejected completion-settlement failure. Recovery now keeps aggregate-issued attempt epoch, disposition, and complete candidate identity without collapsing a previously accepted same-valued owner into later candidate evidence; the abandonment/incarnation atomic updates use `AtomicU64::try_update` without changing their memory ordering or overflow behavior. - Prevented the reusable profile-derived WebDriver BiDi planner from scheduling `setScreenSettingsOverride` from `ScreenMetrics` alone, because the standard operation also changes the page-observable available screen rectangle that the current presentation identity neither selects nor digest-binds. - Restored canonical Rust formatting for the WebDriver BiDi presentation cleanup assertion so exact-head contracts can execute the test, Clippy, and rustdoc gates. ### Added +- Added exact Browser Session create-recovery transaction evidence for `CreateFailedUncertain(Some/None)`, duplicate candidates, and unsettled `Accepted|Rejected` completions, plus a hostile same-valued-handle fixture proving candidate facts and prior ownership facts remain distinct. - Added a version-pinned `originweave-bidi` presentation-capability boundary for the W3C WebDriver BiDi Working Draft published on 3 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`). It depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. - Added a bounded Rust presentation-identity kernel for explicit browser-visible profiles and credential-free replay digests, including control-safe mobile UA-CH model values; applying those profiles to Chromium and proving page-observed effects remain separate adapter and browser-E2E work. @@ -57,6 +59,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Updated active Browser Session doctoring to the immutable WebDriver BiDi Working Draft dated 9 September 2026 while keeping standards publication evidence separate from Chromium runtime qualification. - Aligned the hourly product-development branch-coverage toolchain and its one-shot materializer with the reviewed `nightly-2026-08-18` pin, and corrected the official Dependabot Rust-toolchain reference. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. From b191cf9ef45115202a59db8e91b0233c54eae287 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:13:56 +0900 Subject: [PATCH 066/632] docs(gap): currentize Browser Session delivery baseline --- docs/product-technical-gap-baseline.md | 41 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 618f64cd8..5c938d14f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,6 +2,18 @@ This is a dated delivery baseline, not a substitute for the PRD, TRD, roadmap, architecture decisions, or live GitHub state. It keeps buyer-visible gaps, current issues, active pull-request evidence, and commercial completion tracks in one discoverable place. Protected `main` is the implementation boundary: code in an open pull request is not shipped behavior. +## Live continuity note: 2026-09-15 + +- Protected `main` remains signature-valid and protected at `87c4daa1830bac5a5228b6036752ad5633232085`. The complete GitHub search surface reports 134 open pull requests and 17 open non-PR issues. The #317 source-repair lane is intentionally Draft outside short exact-head CI probes; with that repair lane Draft the queue is 121 Draft and 13 non-Draft. GitHub Release inventory remains empty. +- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active production repair preserves create-transaction provenance through `CreateFailedUncertain(Some/None)`, duplicate-candidate rejection, and accepted/rejected completion-settlement failure via `DisposableContextCreateRecoveryEvidence`. Aggregate-issued attempt epoch, disposition, and complete candidate identity remain non-authorizing recovery facts. Same-valued later candidate evidence no longer suppresses the separately accepted owner's `RecoveryRequiredOwnedHandle`. The same lineage removes the arbitrary 4096-byte `browser.UserContext` ceiling and migrates the two reported atomic `fetch_update` calls to `AtomicU64::try_update` without changing memory ordering or overflow behavior. +- The exact hostile acceptance `create_recovery_same_handle_distinct_fact.rs` now requires attempt 1's accepted ownership and attempt 2's duplicate/unsettled candidate facts to coexist even when their remote handle values are identical. `CreateFailedUncertain(None)` still retains exact aggregate-issued attempt identity, so “no complete handle” is not “no transaction evidence.” These are active-PR claims until one exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. +- #318 remains Draft and structurally RED for the Browser Session navigation state machine. Its current base still points to an earlier #317 exact head, so the child is a repair/restack finding rather than a reason to rewrite the production lane concurrently. #321 remains its Draft same-raw-id recreation/ABA acceptance child. Both must ordinary non-force adopt a verified #317 successor; no child acceptance delta may be discarded merely to restore mergeability. +- #316 remains Draft at exact `8ca6c5a190d9ad2b4c7843d440e91f6070d681c2`. It owns WebDriver BiDi correlation/pending→accepted/quarantined protocol truth and must ordinary non-force restack after the Browser Session prerequisite is verified. Browser Session does not absorb protocol tuple storage or command semantics. +- The next Browser Session production gaps after create-attempt correlation are a purpose-bounded same-adapter `RecoveryRequired`/`TransportLost` handoff and bounded hot ownership/history separation. Durable crash/process-restart persistence remains separate; `abandoned_bound_session_count()` is process-local operability evidence, not destruction proof or durable recovery storage. +- The immutable W3C WebDriver BiDi Working Draft directly verified for this baseline is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), which names the 3 September 2026 draft as its previous version. The mutable `/TR/webdriver-bidi/` index currently exposes an older 24 August surface, so immutable dated provenance and mutable-index freshness are recorded separately. Standards freshness does not authorize an automatic runtime repin. +- Current Chrome desktop Stable remains Chrome 153, promoted 2026-09-08 (`153.0.8010.36` on Linux; `.36/.37` on Windows/macOS). #299's older Chrome/ChromeDriver `150.0.7871.129` Agent Task result remains historical RED: 0/3 trials reached navigation because session creation failed. Buyer-current browser acceptance still requires explicitly qualified real navigation, interaction, browser-observed post-condition, destruction, and cleanup evidence; a command ACK or wrapper drop is not success. +- The active organization ruleset `18156473` still requires one counted approval, stale-review dismissal on push, resolved review threads, additional approval for unattributed changes, seven central required workflows, and deletion/non-fast-forward protection. Administrative bypass exists but is not a normal delivery path and does not justify self-approval, stale-head promotion, or gate weakening. + ## Live continuity note: 2026-09-11 - Protected `main` remains signature-valid at `87c4daa1830bac5a5228b6036752ad5633232085`. The live repository sweep found 132 open pull requests and 16 open non-PR issues; no release or tag exists. Active-PR work below is evidence only and is not protected-main behavior. @@ -39,7 +51,7 @@ The interactive maintenance loop performed the following verified state changes |---|---| | Supersession closure | #153 closed with replacement evidence: base-stack tip (`4da223ac`) already implements `_terminate_owned_process_bounded` exit-race tolerance that supersedes the branch delta | | Conflict reconciliation | Merge commits pushed to #37 (`27f6acd6`, ci.yml aligned to reviewed `nightly-2026-08-18` pin), #149 (`7852a540` + rustfmt fix `54f96008`), #152 (`65b0c705`), #173 (`ecc9574a`), #175 (`765c88f6`, keeps `crate_root.rs` naming) | -| Governance remediation (#212) | #43 reconciled with main in `04e262d5`; the `chrome_sandbox` workflow mutation was first removed, then restored under recorded independent authorization (issue #212 option (b)) because the PR's own contract test fails closed without it; fresh exact-head checks re-ran on the restored head | +| Governance remediation (#212) | #43 reconciled with main in `04e262d5`; the `chrome_sandbox` workflow mutation was first removed, then restored under recorded independent authorization (issue #212 option (b)) because the PR's own contract test fails closed without it; fresh exact-head checks re-ran on the restored branch | | Security finding fix (#124) | Strix vuln-0001 (Unicode homoglyph path confusion, MEDIUM) remediated in `30cc458b`: audited workflow paths now restricted to a canonical ASCII alphabet with homoglyph/fraction-slash/fullwidth regression contract tests; CHANGELOG updated | | Fail-closed provider re-dispatch | ~21 failed Strix required-check runs re-dispatched on unchanged exact heads; completed reruns returned success on #46, #48, #156, #157, #159, #218, and #219 heads at snapshot time; cancellations only where newer heads superseded the run | | Current-head review re-dispatch | Central merge-scheduler dispatches sent for #47, #62, #63, #65, #74, #166, #173, #175, and #220 because their stale `CHANGES_REQUESTED` verdicts cited coverage-evidence results that are green on the same heads today | @@ -90,9 +102,9 @@ The following rows were current on 2026-08-24 and are retained only as regressio | PR | State | Exact base head | Exact head | |---|---|---|---| | #222 | Draft | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | `1e2ce3d4071a1a75ee891bdcd71c506b3b50d4bc` | -| #221 | Draft | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | `6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f` | +| #221 | Draft | `8145d40d5470a7753b8211907c190367f742f2f12` | `6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f` | | #220 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `ed4cab16cf88c76ce1c145a22d0a274ef2d57263` | -| #219 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | +| #219 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `8145d40a279686a28309d59b8b3b9bfbd283a80` | | #218 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `49e98fba6974219b3bb0336c822b12667f1e1c03` | | #217 | Draft | `529d11a3571f6b1834b9baa49ef67eb08f043978` | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | | #216 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `75130851a0f7ce528a7a36382eb026ac7942a0aa` | @@ -122,7 +134,7 @@ The current queue must be processed in dependency order. A green child branch ca ### Review and merge authority -The active `CWL Central required workflows` ruleset (re-fetched for this snapshot) requires one approving review, resolved review threads, no last-push approval requirement, `merge`/`squash` merge methods, and seven configured required workflows (`close-empty-pr`, `opencode-review`, `pr-review-merge-scheduler`, `security-scan`, `strix`, `sast-semgrep`, `noema-review`). The current collaborator inventory contains only `seonghobae` with administration and push permissions, creating a **reviewer-provisioning gap** for counted non-author approval. +The active `CWL Central required workflows` ruleset (re-fetched for this snapshot) requires one approving review, resolved review threads, no last-push approval requirement, `merge`/`squash` merge methods, and seven configured required workflows (`opencode-review`, `pr-review-merge-scheduler`, `security-scan`, `strix`, `sast-semgrep`, `noema-review`, `codeql-pr`). The current collaborator inventory contains only `seonghobae` with administration and push permissions, creating a **reviewer-provisioning gap** for counted non-author approval. This gap does not authorize self-approval, stale-head merges, administrative bypass, or weaker checks. Because the current GitHub ruleset independently requires a counted approval, the solo-maintainer hold does not satisfy the live merge gate: an eligible non-author collaborator must submit a formal `APPROVED` review on the current head. Until that reviewer-provisioning gap is repaired, protected-main merges stop even when exact-head checks, security gates, complete coverage, rustdoc/Clippy, threads, and AI-review evidence are otherwise complete. Before any merge decision, re-fetch the exact ruleset, collaborators, PR head/base, reviews, unresolved threads, and required checks; do not assume this dated observation remains current. @@ -163,7 +175,7 @@ The hourly product-development loop is operational infrastructure, not proof tha | P1 | Buyers can install, update, verify, and roll back a supported product | **Not shipped** | #201; signed Windows/macOS/Linux/headless artifacts, Chromium revision manifest, updater security, patch SLA, SBOM, SLSA provenance, and recovery | | P1 | Enterprise teams can provision, approve, audit, operate, and recover the service | **Not shipped** | #202; Keyverse-compatible OIDC/SCIM, tenant isolation, policy/approval/evidence UI, SLO/incident controls, data residency, CSAP/SOC 2 evidence mapping, WCAG 2.2, Figma File ID, and Storybook | | P0 | A release has reproducible proof of usefulness, safety, evidence completeness, and recovery | **No product-wide release gate** | #203; deterministic, compatibility, adversarial, recovery, and enterprise suites with statistical reporting and an exact-artifact commercial acceptance gate | -| P0 | Valid changes reach protected `main` without authority improvisation or unbounded stack growth | **Blocked / high integration debt** | Shrink the 126-PR queue in dependency order, provision legitimate review authority, require exact-current evidence, and close duplicates/superseded branches | +| P0 | Valid changes reach protected `main` without authority improvisation or unbounded stack growth | **Blocked / high integration debt** | Shrink the live PR queue in dependency order, provision legitimate review authority, require exact-current evidence, and close duplicates/superseded branches only with verified successor coverage | ## Commercial completion definition @@ -182,15 +194,16 @@ OriginWeave is not complete merely because every low-level primitive exists in s ## Next executable queue -1. Drain the merge gate in dependency order: for every ready root PR whose current head is check-green with resolved threads, obtain the current ruleset's counted `APPROVED` review from an eligible non-author collaborator; OpenCode approval or skip evidence does not substitute for that GitHub review. If no eligible approver exists, record the reviewer-provisioning gap and do not merge. Root candidates include #37, #40, #43, #45–#48, #51, #62–#65, #74, #82, #124, #149, #152, #156–#166, #170, #173, #175, #208, #209, #218, and #219 as their re-dispatched checks land. Treat dependent children separately: only after a predecessor reaches protected `main`, retarget and independently revalidate its immediate child; preserve orders such as #218 → #221 → #220 rather than treating #208–#220 as a flat merge range. -2. Keep the organization review pipeline healthy: monitor the central Actions backlog recorded above; if OpenCode reviews stop landing on OriginWeave heads while the queue is idle, repair `ContextualWisdomLab/.github` dispatch/concurrency configuration rather than weakening any gate. -3. Finish the #9/#28 browser-network and Chromium vertical slice, including the #181–#205 WebSocket opening path and framed BiDi command/response stack, then semantic observation, policy, action, post-condition, and recovery boundaries on protected `main`. -4. Finish #27 and #10 as separate security tracks; neither should be hidden inside the first browser PR. -5. Implement #199, then #200, so durable evidence and stable task authority precede broad enterprise integrations. -6. Implement #201 before making release/support claims; exact CI browser evidence must be bound to the actual signed artifact. -7. Design #202 in Figma, record the Figma File ID in the ADR, implement reusable design tokens and Storybook components, then add identity/tenant/approval/audit/operations integration. -8. Make #203 the final release gate across the exact signed distribution, not a source branch or model narrative. -9. Only after the commercial acceptance gate passes, increment the version, finalize CHANGELOG/release notes, publish signed artifacts, and verify upgrade/rollback from the prior supported release. +1. Finish #317's Browser Session prerequisite in its single-writer lane: exact create-attempt recovery correlation is implemented; next prove the same-adapter recovery handoff and bounded hot ownership/history split, then run the complete exact-head contract/format/test/Clippy/rustdoc/100%-coverage/review sequence. Do not begin #318/#321 production implementation in parallel. +2. After #317 is verified, ordinary non-force restack #318 and #321 while preserving every valid acceptance delta; then restack #316 and implement BiDi pending→accepted/quarantined correlation and remote-liveness reconciliation without moving Browser Session authority into the adapter. +3. Re-run the explicitly qualified real-Chromium acceptance (#299/#320 path): navigation, interaction, download/lifecycle transitions where applicable, page/browser-observed post-conditions, destruction, crash/cleanup, and stale-event replay. Command ACK alone is non-passing. +4. Drain the remaining merge gate in dependency order: every ready root PR needs current exact-head checks, resolved threads, and the current ruleset's counted `APPROVED` review from an eligible non-author collaborator. OpenCode/CodeRabbit narrative evidence does not substitute for that GitHub review. +5. Finish #27 and #10 as separate security tracks; neither should be hidden inside the browser-session or first real-browser PR. +6. Implement #199, then #200, so durable evidence and stable task authority precede broad enterprise integrations. +7. Implement #201 before making release/support claims; exact CI browser evidence must be bound to the actual signed artifact. +8. Design #202 in Figma, record the Figma File ID in the ADR, implement reusable design tokens and Storybook components, then add identity/tenant/approval/audit/operations integration. +9. Make #203 the final release gate across the exact signed distribution, not a source branch or model narrative. +10. Only after the commercial acceptance gate passes, increment the version, finalize CHANGELOG/release notes, publish signed artifacts, and verify upgrade/rollback from the prior supported release. ## Evidence commands From 60612ed2ee87ad0079d93780da668d3f47fde392 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 01:16:30 +0900 Subject: [PATCH 067/632] test(browser-session): observe exact create attempt sequence --- ...eate_recovery_same_handle_distinct_fact.rs | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs index 8a4edba86..a3b59af30 100644 --- a/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs +++ b/crates/originweave-browser-session/tests/create_recovery_same_handle_distinct_fact.rs @@ -7,21 +7,27 @@ use originweave_browser_session::{ DisposableContextPort, DisposableIsolationId, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Default)] +struct CreateAttemptLedger { + create_attempts: Vec, + completion_attempts: Vec<(u64, DisposableContextCreateDisposition)>, +} #[derive(Debug)] struct SameHandleRejectedCompletionPort { handle: DisposableContextHandle, - create_attempts: Vec, - completion_attempts: Vec<(u64, DisposableContextCreateDisposition)>, + ledger: Rc>, } impl SameHandleRejectedCompletionPort { - fn new(handle: DisposableContextHandle) -> Self { - Self { - handle, - create_attempts: Vec::new(), - completion_attempts: Vec::new(), - } + fn new( + handle: DisposableContextHandle, + ledger: Rc>, + ) -> Self { + Self { handle, ledger } } } @@ -30,7 +36,10 @@ impl DisposableContextPort for SameHandleRejectedCompletionPort { &mut self, request: &DisposableContextCreateRequest, ) -> Result { - self.create_attempts.push(request.attempt_epoch().value()); + self.ledger + .borrow_mut() + .create_attempts + .push(request.attempt_epoch().value()); Ok(self.handle.clone()) } @@ -38,8 +47,10 @@ impl DisposableContextPort for SameHandleRejectedCompletionPort { &mut self, completion: &DisposableContextCreateCompletion, ) -> Result<(), DisposableContextCreateCompletionError> { - self.completion_attempts - .push((completion.attempt_epoch().value(), completion.disposition())); + self.ledger.borrow_mut().completion_attempts.push(( + completion.attempt_epoch().value(), + completion.disposition(), + )); if completion.disposition() == DisposableContextCreateDisposition::Rejected { Err(DisposableContextCreateCompletionError::CompletionFailed) } else { @@ -66,7 +77,8 @@ fn same_valued_rejected_create_keeps_prior_ownership_as_a_distinct_recovery_fact BrowserSessionId::new(94_001).expect("valid browser-session identity"), ) .expect("browser-session incarnation capacity"); - let port = SameHandleRejectedCompletionPort::new(handle.clone()); + let ledger = Rc::new(RefCell::new(CreateAttemptLedger::default())); + let port = SameHandleRejectedCompletionPort::new(handle.clone(), Rc::clone(&ledger)); let mut bound = session.bind_lifecycle_port(port); let first_authority = bound @@ -80,6 +92,18 @@ fn same_valued_rejected_create_keeps_prior_ownership_as_a_distinct_recovery_fact "attempt 2 returns the same remote handle but its rejected completion is unproven" ); + let observed = ledger.borrow(); + assert_eq!(observed.create_attempts, vec![1, 2]); + assert_eq!( + observed.completion_attempts, + vec![ + (1, DisposableContextCreateDisposition::Accepted), + (2, DisposableContextCreateDisposition::Rejected), + ], + "the adapter must observe the same aggregate-issued attempt identity that recovery evidence retains" + ); + drop(observed); + assert_eq!(bound.browser_session().recovery_evidence().len(), 3); assert!(bound .browser_session() From 44c4dae36a5298a67d5af4b29632a6dac6dd6e33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:01:24 +0900 Subject: [PATCH 068/632] test(browser-session): require same-adapter recovery handoff --- .../tests/recovery_owner_handoff.rs | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_owner_handoff.rs diff --git a/crates/originweave-browser-session/tests/recovery_owner_handoff.rs b/crates/originweave-browser-session/tests/recovery_owner_handoff.rs new file mode 100644 index 000000000..34c95e0b1 --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_owner_handoff.rs @@ -0,0 +1,197 @@ +use std::cell::Cell; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct RecoveryTrackedPort { + handle: DisposableContextHandle, + fail_destroy: bool, + create_calls: Rc>, + destroy_calls: Rc>, + drop_calls: Rc>, +} + +impl Drop for RecoveryTrackedPort { + fn drop(&mut self) { + self.drop_calls.set(self.drop_calls.get() + 1); + } +} + +impl DisposableContextPort for RecoveryTrackedPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_calls.set(self.create_calls.get() + 1); + Ok(self.handle.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls.set(self.destroy_calls.get() + 1); + if self.fail_destroy { + Err(DisposableContextDestroyError::DestroyFailed) + } else { + Ok(()) + } + } +} + +fn recovery_port( + context: u64, + isolation: &str, + fail_destroy: bool, + create_calls: Rc>, + destroy_calls: Rc>, + drop_calls: Rc>, +) -> Result { + let isolation = DisposableIsolationId::parse(isolation) + .map_err(|_| "static fixture isolation id must be valid")?; + let browsing_context = BrowsingContextId::new(context) + .map_err(|_| "static fixture browsing context id must be valid")?; + Ok(RecoveryTrackedPort { + handle: DisposableContextHandle::new(isolation, browsing_context), + fail_destroy, + create_calls, + destroy_calls, + drop_calls, + }) +} + +#[test] +fn unproven_destroy_hands_exact_bound_adapter_and_evidence_to_recovery_owner( +) -> Result<(), &'static str> { + let create_calls = Rc::new(Cell::new(0)); + let destroy_calls = Rc::new(Cell::new(0)); + let drop_calls = Rc::new(Cell::new(0)); + let session = BrowserSession::start( + BrowserSessionId::new(710).map_err(|_| "static session id must be valid")?, + ) + .map_err(|_| "browser session incarnation must be available")?; + let port = recovery_port( + 7_100, + "recovery-handoff-destroy", + true, + Rc::clone(&create_calls), + Rc::clone(&destroy_calls), + Rc::clone(&drop_calls), + )?; + let mut bound = session.bind_lifecycle_port(port); + + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture context creation must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + let expected_evidence = bound.browser_session().recovery_evidence().to_vec(); + assert!(matches!( + expected_evidence.as_slice(), + [BrowserSessionRecoveryEvidence::UnprovenDestruction { .. }] + )); + assert_eq!(create_calls.get(), 1); + assert_eq!(destroy_calls.get(), 1); + assert_eq!(drop_calls.get(), 0); + + let recovery = bound + .into_recovery() + .map_err(|_| "RecoveryRequired must permit consuming recovery handoff")?; + + assert_eq!(drop_calls.get(), 0, "handoff must move, not replace, the bound adapter"); + assert_eq!( + recovery.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert_eq!(recovery.browser_session().recovery_evidence(), expected_evidence); + assert_eq!(create_calls.get(), 1, "handoff must not create browser state"); + assert_eq!(destroy_calls.get(), 1, "handoff must not imply cleanup I/O"); + + drop(recovery); + assert_eq!( + drop_calls.get(), + 1, + "the exact non-Clone adapter must stay alive until the recovery owner is dropped" + ); + Ok(()) +} + +#[test] +fn transport_loss_hands_exact_bound_adapter_and_evidence_to_recovery_owner( +) -> Result<(), &'static str> { + let create_calls = Rc::new(Cell::new(0)); + let destroy_calls = Rc::new(Cell::new(0)); + let drop_calls = Rc::new(Cell::new(0)); + let session = BrowserSession::start( + BrowserSessionId::new(711).map_err(|_| "static session id must be valid")?, + ) + .map_err(|_| "browser session incarnation must be available")?; + let port = recovery_port( + 7_110, + "recovery-handoff-transport", + false, + Rc::clone(&create_calls), + Rc::clone(&destroy_calls), + Rc::clone(&drop_calls), + )?; + let mut bound = session.bind_lifecycle_port(port); + + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture context creation must succeed")?; + assert_eq!(authority.browsing_context().value(), 7_110); + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + let expected_evidence = bound.browser_session().recovery_evidence().to_vec(); + assert!(matches!( + expected_evidence.as_slice(), + [BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(_)] + )); + assert_eq!(create_calls.get(), 1); + assert_eq!(destroy_calls.get(), 0); + assert_eq!(drop_calls.get(), 0); + + let recovery = bound + .into_recovery() + .map_err(|_| "TransportLost must permit consuming recovery handoff")?; + + assert_eq!(drop_calls.get(), 0, "handoff must preserve the same bound adapter instance"); + assert_eq!( + recovery.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert_eq!(recovery.browser_session().recovery_evidence(), expected_evidence); + assert_eq!(create_calls.get(), 1, "handoff must not create browser state"); + assert_eq!(destroy_calls.get(), 0, "transport loss is not destruction proof"); + + drop(recovery); + assert_eq!( + drop_calls.get(), + 1, + "the exact non-Clone adapter must remain owned by the recovery wrapper until drop" + ); + Ok(()) +} From 64e7d4838b636d24cbee094b3a31e82985549986 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:04:35 +0900 Subject: [PATCH 069/632] fix(browser-session): hand unresolved sessions to recovery custody --- .../src/browser_session.rs | 1745 +++++++++++++++++ crates/originweave-browser-session/src/lib.rs | 1744 +--------------- .../src/recovery.rs | 45 + 3 files changed, 1796 insertions(+), 1738 deletions(-) create mode 100644 crates/originweave-browser-session/src/browser_session.rs create mode 100644 crates/originweave-browser-session/src/recovery.rs diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs new file mode 100644 index 000000000..47d8ccc0a --- /dev/null +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -0,0 +1,1745 @@ +//! Browser Session lifecycle authority for OriginWeave. +//! +//! This crate owns the domain transition that turns a newly created disposable +//! browser isolation boundary into presentation-mutation authority. Driver identifiers +//! remain adapter data: naming a session or browsing context is never sufficient to mint authority. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +use std::collections::BTreeMap; +use std::fmt; +use std::sync::atomic::{AtomicU64, Ordering}; + +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +static NEXT_BROWSER_SESSION_INCARNATION: AtomicU64 = AtomicU64::new(1); +static ABANDONED_BOUND_SESSIONS: AtomicU64 = AtomicU64::new(0); + +/// Return the number of bound Browser Sessions abandoned with unresolved remote ownership. +/// +/// This is a process-local, non-I/O operability signal. It deliberately does not claim that remote +/// browser cleanup happened and is not a substitute for persisting exact recovery evidence before a +/// process exits. +#[must_use] +pub fn abandoned_bound_session_count() -> u64 { + ABANDONED_BOUND_SESSIONS.load(Ordering::Relaxed) +} + +/// Current lifecycle state of one Browser Session aggregate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserSessionState { + /// The session may create and own disposable contexts. + Active, + /// Every owned context was destroyed and the session was ended normally. + Ended, + /// The browser transport was lost while no ownership-recovery condition preceded it. + TransportLost, + /// Browser lifecycle ownership became uncertain and requires external reconciliation. + RecoveryRequired, +} + +/// Domain failure while changing Browser Session ownership state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserSessionError { + /// The requested transition requires an active Browser Session. + SessionNotActive, + /// No unused session-incarnation identity remains in this process. + IncarnationExhausted, + /// No unused context epoch remains, so no new authority can be issued safely. + EpochExhausted, + /// The disposable-context port proved that context creation failed without creating a boundary. + ContextCreationFailed, + /// Context creation may have created browser state that the aggregate cannot safely own or destroy. + ContextCreationUncertain, + /// The port returned a browsing-context identity already known to this aggregate. + DuplicateBrowsingContext, + /// The port returned an isolation identity already known to this aggregate. + DuplicateDisposableIsolation, + /// The requested context is not currently owned and active in this session. + ContextNotOwned, + /// The supplied authority belongs to another incarnation, isolation boundary, session, context, or epoch. + AuthorityMismatch, + /// The disposable-context port could not prove destruction of the owned isolation boundary. + ContextDestructionFailed, + /// Normal session end was requested while an owned or uncertain context remains. + ActiveContextRemains, +} + +/// Bounded failure from disposable-context creation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisposableContextCreateError { + /// Creation failed and the adapter proved that no disposable boundary was created. + CreateFailedClean, + /// Creation failed after ownership may have changed. The optional identity is the exact + /// browser-issued isolation identity already known at the failure boundary, when available. + CreateFailedUncertain(Option), +} + +/// Bounded failure from disposable-context destruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableContextDestroyError { + /// Destruction of an owned disposable context failed or could not be proven. + DestroyFailed, +} + +/// Validation failure for a browser-issued disposable isolation identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableIsolationIdError { + /// The identity is empty. + Empty, + /// The identity contains surrounding whitespace or control characters. + InvalidCharacter, +} + +/// Browser-issued identity for one disposable isolation boundary. +/// +/// This value is addressability, not mutation authority. A conforming adapter must return a value +/// that is non-aliasing for the live lifetime of the created boundary. A WebDriver BiDi adapter +/// should map this one-to-one to the specification-defined unique user-context identifier. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DisposableIsolationId(String); + +impl DisposableIsolationId { + /// Parse one browser-issued isolation identity without inventing a protocol length limit. + pub fn parse(value: &str) -> Result { + if value.is_empty() { + return Err(DisposableIsolationIdError::Empty); + } + if value.trim() != value || value.chars().any(char::is_control) { + return Err(DisposableIsolationIdError::InvalidCharacter); + } + Ok(Self(value.to_owned())) + } + + /// Return the validated browser-issued isolation identity. + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Process-local, non-reused identity for one Browser Session aggregate incarnation. +/// +/// Presentation authority is intentionally non-serializable. A process restart therefore destroys +/// every outstanding authority value. Within one process this monotonic identity prevents a later +/// aggregate from revalidating an authority retained from an earlier aggregate that reused the same +/// transport/session and browser-issued context identifiers. The identity is also passed through the +/// lifecycle port so an adapter must scope its remote ownership mapping to the same incarnation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BrowserSessionIncarnation(u64); + +impl BrowserSessionIncarnation { + /// Return the monotonic process-local incarnation value. + #[must_use] + pub const fn value(self) -> u64 { + self.0 + } +} + +/// Adapter result for one newly created disposable browser context. +/// +/// The isolation identity scopes the lifecycle boundary used for destruction; the browsing-context +/// identity addresses the independently navigable context inside that boundary. Neither field alone +/// is presentation-mutation authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DisposableContextHandle { + isolation: DisposableIsolationId, + browsing_context: BrowsingContextId, +} + +impl DisposableContextHandle { + /// Bind one validated isolation identity to its created browsing context. + #[must_use] + pub fn new(isolation: DisposableIsolationId, browsing_context: BrowsingContextId) -> Self { + Self { + isolation, + browsing_context, + } + } + + /// Return the non-aliasing disposable isolation identity. + #[must_use] + pub fn isolation(&self) -> &DisposableIsolationId { + &self.isolation + } + + /// Return the browsing-context address inside the disposable boundary. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.browsing_context + } +} + +/// Lossless evidence retained when browser lifecycle ownership is no longer proven. +/// +/// These values authorize no browser command. They exist only so a separately reviewed recovery +/// path can later reconcile exact remote identities instead of guessing from raw session/context ids. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BrowserSessionRecoveryEvidence { + /// A partial creation exposed a browser-issued isolation identity before completion became uncertain. + PartialCreationIsolation(DisposableIsolationId), + /// A create call returned a complete handle that aliased an already-owned context or isolation. + DuplicateAdapterHandle(DisposableContextHandle), + /// A complete create result could not be settled with the bound adapter after domain validation. + UnsettledAdapterHandle(DisposableContextHandle), + /// Destruction of this exact owned handle and validated authority epoch failed or could not be proven. + UnprovenDestruction { + /// Exact owned context whose remote boundary remains uncertain. + context: DisposableContextHandle, + /// Browser Session epoch validated immediately before destroy I/O. + context_epoch: BrowserContextEpoch, + }, + /// A recovery condition elsewhere in the session made this active owned handle uncertain. + RecoveryRequiredOwnedHandle(DisposableContextHandle), + /// Transport loss made this previously active owned handle uncertain. + TransportLossOwnedHandle(DisposableContextHandle), +} + +/// Exact create-attempt facts retained when one Browser Session creation transaction becomes uncertain. +/// +/// The legacy identity-oriented [`BrowserSessionRecoveryEvidence`] remains useful to recovery code that +/// reconciles remote handles. This companion evidence preserves the aggregate-issued attempt epoch and +/// completion disposition so two lifecycle facts with the same remote values cannot be collapsed into +/// one transaction. These values grant no browser command authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisposableContextCreateRecoveryEvidence { + /// The adapter reported an uncertain create failure before a complete handle was available. + FailedUncertain { + /// Exact aggregate-issued epoch reserved for the failed create attempt. + attempt_epoch: BrowserContextEpoch, + /// Browser-issued isolation identity known at the failure boundary, when available. + isolation: Option, + }, + /// A complete candidate aliased already-owned browser state and was rejected by the aggregate. + DuplicateCandidate { + /// Exact aggregate-issued epoch reserved for the rejected create attempt. + attempt_epoch: BrowserContextEpoch, + /// Exact adapter-returned candidate associated with that attempt. + context: DisposableContextHandle, + }, + /// The adapter could not prove completion settlement for one exact create attempt. + CompletionUnsettled { + /// Exact aggregate-issued epoch reserved for the unsettled create attempt. + attempt_epoch: BrowserContextEpoch, + /// Aggregate decision whose delivery to the adapter could not be proven. + disposition: DisposableContextCreateDisposition, + /// Exact adapter-returned candidate associated with that attempt. + context: DisposableContextHandle, + }, +} + +/// Opaque Browser Session-issued request for one disposable-context creation attempt. +/// +/// There is deliberately no public constructor. A request is created only inside a +/// [`BoundBrowserSession`], after Browser Session has validated that the aggregate is active. Raw +/// session, incarnation, context, isolation, or adapter-selected identifiers cannot recreate it. +#[derive(Debug)] +pub struct DisposableContextCreateRequest { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + attempt_epoch: BrowserContextEpoch, +} + +impl DisposableContextCreateRequest { + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the unique context epoch reserved for this create attempt. + #[must_use] + pub const fn attempt_epoch(&self) -> BrowserContextEpoch { + self.attempt_epoch + } +} + +/// Domain disposition for one completed disposable-context create attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableContextCreateDisposition { + /// The returned handle passed Browser Session ownership validation and may become authorizing. + Accepted, + /// The returned handle failed Browser Session ownership validation and must remain non-authorizing. + Rejected, +} + +/// Opaque Browser Session-issued completion for one exact create attempt. +/// +/// The adapter may stage remote protocol state while executing a create request, but it must not +/// promote that state into an authorizing binding until it receives an `Accepted` completion for the +/// same session incarnation and attempt epoch. `Rejected` candidates are recovery/quarantine evidence +/// only. There is deliberately no public constructor. +#[derive(Debug)] +pub struct DisposableContextCreateCompletion { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + attempt_epoch: BrowserContextEpoch, + disposition: DisposableContextCreateDisposition, +} + +impl DisposableContextCreateCompletion { + /// Return the Browser Session transport identity for adapter correlation. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the Browser Session incarnation for adapter correlation. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the create-attempt epoch that this completion settles. + #[must_use] + pub const fn attempt_epoch(&self) -> BrowserContextEpoch { + self.attempt_epoch + } + + /// Return whether Browser Session accepted or rejected the created candidate. + #[must_use] + pub const fn disposition(&self) -> DisposableContextCreateDisposition { + self.disposition + } +} + +/// Failure while settling one exact create attempt with the bound lifecycle adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DisposableContextCreateCompletionError { + /// The adapter could not prove that the exact pending create attempt reached the requested state. + CompletionFailed, +} + +/// Opaque Browser Session-issued request for destruction of one exact owned disposable context. +/// +/// There is deliberately no public constructor. The bound aggregate creates this request only after +/// validating the supplied presentation authority against current ownership. A caller cannot rebuild +/// cleanup authority from raw browser identifiers. The validated epoch is carried only as correlation +/// evidence for the already-authorized request; it is not independently sufficient to destroy state. +#[derive(Debug)] +pub struct DisposableContextDestroyRequest { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + context: DisposableContextHandle, + context_epoch: BrowserContextEpoch, +} + +impl DisposableContextDestroyRequest { + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the exact domain handle whose remote isolation boundary must be destroyed. + #[must_use] + pub const fn context(&self) -> &DisposableContextHandle { + &self.context + } + + /// Return the exact Browser Session epoch validated before destroy I/O. + #[must_use] + pub const fn context_epoch(&self) -> BrowserContextEpoch { + self.context_epoch + } +} + +/// Port implemented by a reviewed browser adapter for disposable context lifecycle operations. +/// +/// The port never self-asserts an instance identifier. Instead, Browser Session consumes one concrete +/// port value into [`BoundBrowserSession`]. Public lifecycle methods then use only that owned port, so a +/// caller cannot swap a second adapter instance into create or destroy after binding. The port receives +/// only aggregate-issued request values with private construction paths. +/// +/// For WebDriver BiDi, creation should map the isolation identity one-to-one to the user-context +/// identifier returned by `browser.createUserContext`. [`DisposableContextCreateError::CreateFailedClean`] +/// is allowed only when the adapter proves that no disposable state was created. If a user-context +/// identity is already known when later creation or verification becomes uncertain, the adapter must +/// return it inside [`DisposableContextCreateError::CreateFailedUncertain`]. +/// +/// `destroy_disposable_context` must destroy the exact boundary carried by the supplied request and +/// return success only after destruction is proven. Reconstructing cleanup authority from raw driver +/// identifiers is forbidden, and a command acknowledgement alone is insufficient evidence. +pub trait DisposableContextPort { + /// Create one fresh disposable isolation boundary and browsing context for this authorized request. + fn create_disposable_context( + &mut self, + request: &DisposableContextCreateRequest, + ) -> Result; + + /// Settle the exact create attempt after Browser Session validates the returned domain handle. + /// + /// An adapter must keep a successful remote create result non-authorizing until this completion + /// accepts the matching attempt. A rejected attempt must remain non-authorizing and be retained + /// only for recovery/quarantine processing. + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError>; + + /// Destroy the exact disposable isolation boundary represented by this authorized request. + fn destroy_disposable_context( + &mut self, + request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError>; +} + +/// Opaque aggregate-authorized request for one purpose-bounded adapter operation. +/// +/// The caller supplies only the adapter-defined operation value. Browser Session validates the +/// accompanying presentation authority first and privately binds the operation to the exact owned +/// context and validated epoch before the consumed adapter can observe it. There is deliberately no +/// public constructor, and the epoch is correlation/provenance rather than standalone authority. +pub struct AuthorizedContextOperationRequest { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + context: DisposableContextHandle, + context_epoch: BrowserContextEpoch, + operation: O, +} + +impl AuthorizedContextOperationRequest { + /// Return the Browser Session transport identity for adapter addressability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the non-reused Browser Session incarnation for adapter lifecycle correlation. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the exact currently owned context validated before adapter I/O. + #[must_use] + pub const fn context(&self) -> &DisposableContextHandle { + &self.context + } + + /// Return the exact Browser Session epoch validated before adapter I/O. + #[must_use] + pub const fn context_epoch(&self) -> BrowserContextEpoch { + self.context_epoch + } + + /// Return the adapter-defined purpose-bounded operation payload. + #[must_use] + pub const fn operation(&self) -> &O { + &self.operation + } +} + +/// Failure from executing an aggregate-authorized operation through the consumed adapter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthorizedContextOperationError { + /// Browser Session rejected the authority before adapter I/O. + BrowserSession(BrowserSessionError), + /// The bound adapter attempted the authorized operation and returned its bounded failure. + Adapter(E), +} + +/// Adapter extension for purpose-bounded operations that must use the exact consumed adapter. +/// +/// Browser Session remains protocol-agnostic: the adapter owns the operation, output, and error +/// types. The wrapper only proves current ownership and routes the opaque request to the same concrete +/// adapter instance used for lifecycle creation and destruction. Implementations must not treat the +/// request as permission to mutate any other context. +pub trait AuthorizedContextOperationPort: DisposableContextPort { + /// Adapter-defined operation vocabulary, such as a reviewed BiDi presentation command. + type Operation; + /// Adapter-defined successful result. + type Output; + /// Adapter-defined bounded operation failure. + type Error; + + /// Execute one aggregate-authorized operation against the exact context carried by the request. + fn execute_authorized_context_operation( + &mut self, + request: &AuthorizedContextOperationRequest, + ) -> Result; +} + +/// Monotonic identity for one owned browsing-context authority epoch. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BrowserContextEpoch(u64); + +impl BrowserContextEpoch { + /// Return the internal monotonic epoch value. + #[must_use] + pub const fn value(self) -> u64 { + self.0 + } +} + +/// Opaque proof that Browser Session currently owns presentation mutation for one context epoch. +/// +/// The fields are private and no public constructor exists. A caller obtains this value only after +/// Browser Session has created a disposable boundary through its bound lifecycle port. Session +/// incarnation, isolation identity, context identity, and epoch must all still match before adapter I/O +/// is allowed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PresentationMutationAuthority { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + isolation: DisposableIsolationId, + browsing_context: BrowsingContextId, + context_epoch: BrowserContextEpoch, +} + +impl PresentationMutationAuthority { + /// Return the Browser Session transport identity associated with this authority. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the Browser Session incarnation that minted this authority. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the owned disposable isolation identity. + #[must_use] + pub fn isolation(&self) -> &DisposableIsolationId { + &self.isolation + } + + /// Return the owned browsing-context identity. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.browsing_context + } + + /// Return the exact context epoch covered by this authority. + #[must_use] + pub const fn context_epoch(&self) -> BrowserContextEpoch { + self.context_epoch + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OwnedContextState { + Active, + Destroyed, + Uncertain, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct OwnedContextRecord { + handle: DisposableContextHandle, + epoch: BrowserContextEpoch, + state: OwnedContextState, +} + +/// Aggregate root for disposable browser-context lifecycle and presentation mutation authority. +#[derive(Debug)] +pub struct BrowserSession { + id: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + state: BrowserSessionState, + transport_lost: bool, + next_epoch: u64, + contexts: BTreeMap, + recovery_evidence: Vec, + create_recovery_evidence: Vec, +} + +/// Browser Session composed with the one lifecycle-port instance allowed to mutate its remote state. +/// +/// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and +/// no public Browser Session lifecycle method accepts an arbitrary port parameter. This makes adapter +/// ownership structural rather than dependent on a caller-selected scalar or an adapter callback. +#[must_use = "destroy owned browser state and finish the session, or hand unresolved ownership to recovery"] +pub struct BoundBrowserSession

{ + session: BrowserSession, + port: P, +} + +impl

fmt::Debug for BoundBrowserSession

{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("BoundBrowserSession") + .field("browser_session", &self.session.id) + .field("incarnation", &self.session.incarnation) + .field("state", &self.session.state) + .field("transport_lost", &self.session.transport_lost) + .field("owned_context_count", &self.session.contexts.len()) + .field( + "recovery_evidence_count", + &self.session.recovery_evidence.len(), + ) + .field( + "create_recovery_evidence_count", + &self.session.create_recovery_evidence.len(), + ) + .field("port", &"") + .finish() + } +} + +impl

Drop for BoundBrowserSession

{ + fn drop(&mut self) { + if self.session.has_unresolved_remote_ownership() { + let _ = ABANDONED_BOUND_SESSIONS.try_update( + Ordering::Relaxed, + Ordering::Relaxed, + |value| Some(value.saturating_add(1)), + ); + } + } +} + +impl BrowserSession { + /// Start an active Browser Session around an already validated transport session identity. + /// + /// A fresh process-local incarnation is allocated before any browser I/O. Exhaustion fails closed + /// rather than wrapping and making an older authority structurally valid again. + pub fn start(id: BrowserSessionId) -> Result { + Self::start_with_counter(id, &NEXT_BROWSER_SESSION_INCARNATION) + } + + fn start_with_counter( + id: BrowserSessionId, + counter: &AtomicU64, + ) -> Result { + let incarnation = allocate_incarnation(counter)?; + Ok(Self { + id, + incarnation, + state: BrowserSessionState::Active, + transport_lost: false, + next_epoch: 1, + contexts: BTreeMap::new(), + recovery_evidence: Vec::new(), + create_recovery_evidence: Vec::new(), + }) + } + + /// Consume this aggregate and one concrete lifecycle port into a linear bound session. + /// + /// Binding invokes no adapter method. All subsequent create/destroy I/O is reachable only through + /// the owned port inside the returned wrapper. + pub fn bind_lifecycle_port(self, port: P) -> BoundBrowserSession

{ + BoundBrowserSession { + session: self, + port, + } + } + + /// Return this aggregate's browser-session transport identity. + #[must_use] + pub const fn id(&self) -> BrowserSessionId { + self.id + } + + /// Return this aggregate's non-reused process-local incarnation. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the current aggregate lifecycle state. + #[must_use] + pub const fn state(&self) -> BrowserSessionState { + self.state + } + + /// Report whether browser transport loss has been observed for this aggregate. + #[must_use] + pub const fn transport_is_lost(&self) -> bool { + self.transport_lost + } + + /// Return immutable recovery evidence retained after uncertain browser lifecycle outcomes. + #[must_use] + pub fn recovery_evidence(&self) -> &[BrowserSessionRecoveryEvidence] { + &self.recovery_evidence + } + + /// Return exact create-attempt recovery facts retained for transaction correlation. + #[must_use] + pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { + &self.create_recovery_evidence + } + + /// Return current presentation authority for an already-owned active context. + pub fn presentation_authority( + &self, + browsing_context: BrowsingContextId, + ) -> Result { + self.require_active()?; + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + Ok(Self::authority_for( + self.id, + self.incarnation, + &record.handle, + record.epoch, + )) + } + + /// Advance one active owned context to a new authority epoch. + pub fn advance_context_epoch( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.require_active()?; + let browser_session = self.id; + let incarnation = self.incarnation; + let record = self + .contexts + .get_mut(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + let next = reserve_epoch(&mut self.next_epoch)?; + record.epoch = next; + Ok(Self::authority_for( + browser_session, + incarnation, + &record.handle, + next, + )) + } + + /// Record browser transport loss independently from ownership-recovery state. + /// + /// Returns `true` only for the first observed transport loss. If ownership was already uncertain, + /// `RecoveryRequired` remains the lifecycle state while the transport-loss fact is retained. + pub fn record_transport_loss(&mut self) -> bool { + if self.transport_lost || self.state == BrowserSessionState::Ended { + return false; + } + self.transport_lost = true; + if self.state == BrowserSessionState::Active { + self.recovery_evidence.extend( + self.contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| { + BrowserSessionRecoveryEvidence::TransportLossOwnedHandle( + record.handle.clone(), + ) + }), + ); + self.state = BrowserSessionState::TransportLost; + self.mark_active_contexts_uncertain(); + } + true + } + + /// End the Browser Session only after every owned context has proven destruction. + pub fn end(&mut self) -> Result<(), BrowserSessionError> { + self.require_active()?; + if self + .contexts + .values() + .any(|record| record.state != OwnedContextState::Destroyed) + { + return Err(BrowserSessionError::ActiveContextRemains); + } + self.state = BrowserSessionState::Ended; + Ok(()) + } + + fn create_disposable_context_with_port( + &mut self, + port: &mut P, + ) -> Result { + self.require_active()?; + let epoch = reserve_epoch(&mut self.next_epoch)?; + let request = DisposableContextCreateRequest { + browser_session: self.id, + incarnation: self.incarnation, + attempt_epoch: epoch, + }; + let handle = match port.create_disposable_context(&request) { + Ok(handle) => handle, + Err(DisposableContextCreateError::CreateFailedClean) => { + return Err(BrowserSessionError::ContextCreationFailed); + } + Err(DisposableContextCreateError::CreateFailedUncertain(isolation)) => { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch: epoch, + isolation: isolation.clone(), + }, + ); + if let Some(isolation) = isolation { + self.recovery_evidence.push( + BrowserSessionRecoveryEvidence::PartialCreationIsolation(isolation), + ); + } + self.enter_recovery_required(); + return Err(BrowserSessionError::ContextCreationUncertain); + } + }; + + let duplicate_error = if self + .contexts + .values() + .any(|record| record.handle.isolation == handle.isolation) + { + Some(BrowserSessionError::DuplicateDisposableIsolation) + } else if self.contexts.contains_key(&handle.browsing_context) { + Some(BrowserSessionError::DuplicateBrowsingContext) + } else { + None + }; + + if let Some(error) = duplicate_error { + let completion = DisposableContextCreateCompletion { + browser_session: self.id, + incarnation: self.incarnation, + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Rejected, + }; + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { + attempt_epoch: epoch, + context: handle.clone(), + }, + ); + self.recovery_evidence + .push(BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( + handle.clone(), + )); + if port + .complete_disposable_context_creation(&completion) + .is_err() + { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Rejected, + context: handle.clone(), + }, + ); + self.recovery_evidence.push( + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), + ); + self.enter_recovery_required(); + return Err(BrowserSessionError::ContextCreationUncertain); + } + self.enter_recovery_required(); + return Err(error); + } + + let completion = DisposableContextCreateCompletion { + browser_session: self.id, + incarnation: self.incarnation, + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Accepted, + }; + if port + .complete_disposable_context_creation(&completion) + .is_err() + { + self.create_recovery_evidence.push( + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch: epoch, + disposition: DisposableContextCreateDisposition::Accepted, + context: handle.clone(), + }, + ); + self.recovery_evidence + .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + handle, + )); + self.enter_recovery_required(); + return Err(BrowserSessionError::ContextCreationUncertain); + } + + let browsing_context = handle.browsing_context; + let authority = Self::authority_for(self.id, self.incarnation, &handle, epoch); + self.contexts.insert( + browsing_context, + OwnedContextRecord { + handle, + epoch, + state: OwnedContextState::Active, + }, + ); + Ok(authority) + } + + fn destroy_disposable_context_with_port( + &mut self, + authority: &PresentationMutationAuthority, + port: &mut P, + ) -> Result<(), BrowserSessionError> { + let browser_session = self.id; + let incarnation = self.incarnation; + let record = self.context_for_authority_mut(authority)?; + let context_epoch = record.epoch; + let request = DisposableContextDestroyRequest { + browser_session, + incarnation, + context: record.handle.clone(), + context_epoch, + }; + match port.destroy_disposable_context(&request) { + Ok(()) => { + record.state = OwnedContextState::Destroyed; + Ok(()) + } + Err(DisposableContextDestroyError::DestroyFailed) => { + record.state = OwnedContextState::Uncertain; + self.recovery_evidence + .push(BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: request.context, + context_epoch, + }); + self.enter_recovery_required(); + Err(BrowserSessionError::ContextDestructionFailed) + } + } + } + + fn require_active(&self) -> Result<(), BrowserSessionError> { + if self.state == BrowserSessionState::Active { + Ok(()) + } else { + Err(BrowserSessionError::SessionNotActive) + } + } + + fn authority_for( + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + handle: &DisposableContextHandle, + context_epoch: BrowserContextEpoch, + ) -> PresentationMutationAuthority { + PresentationMutationAuthority { + browser_session, + incarnation, + isolation: handle.isolation.clone(), + browsing_context: handle.browsing_context, + context_epoch, + } + } + + fn context_for_authority_mut( + &mut self, + authority: &PresentationMutationAuthority, + ) -> Result<&mut OwnedContextRecord, BrowserSessionError> { + self.require_active()?; + if authority.browser_session != self.id || authority.incarnation != self.incarnation { + return Err(BrowserSessionError::AuthorityMismatch); + } + let record = self + .contexts + .get_mut(&authority.browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.epoch != authority.context_epoch || record.handle.isolation != authority.isolation + { + return Err(BrowserSessionError::AuthorityMismatch); + } + Ok(record) + } + + fn enter_recovery_required(&mut self) { + let sibling_handles = self + .contexts + .values() + .filter(|record| record.state == OwnedContextState::Active) + .map(|record| record.handle.clone()) + .filter(|handle| { + !self.recovery_evidence.iter().any(|evidence| match evidence { + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) + | BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(_) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(_) => false, + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { + existing == handle + } + BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: existing, + .. + } => existing == handle, + }) + }) + .collect::>(); + self.recovery_evidence.extend( + sibling_handles + .into_iter() + .map(BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle), + ); + self.state = BrowserSessionState::RecoveryRequired; + self.mark_active_contexts_uncertain(); + } + + fn mark_active_contexts_uncertain(&mut self) { + for record in self.contexts.values_mut() { + if record.state == OwnedContextState::Active { + record.state = OwnedContextState::Uncertain; + } + } + } + + fn has_unresolved_remote_ownership(&self) -> bool { + matches!(self.state, BrowserSessionState::RecoveryRequired) + || self.contexts.values().any(|record| { + matches!( + record.state, + OwnedContextState::Active | OwnedContextState::Uncertain + ) + }) + } +} + +impl BoundBrowserSession

{ + /// Return the bound Browser Session for read-only policy and ACL validation. + #[must_use] + pub const fn browser_session(&self) -> &BrowserSession { + &self.session + } + + /// Create one disposable context through the exact port consumed when this session was bound. + pub fn create_disposable_context( + &mut self, + ) -> Result { + self.session + .create_disposable_context_with_port(&mut self.port) + } + + /// Return current presentation authority for an already-owned active context. + pub fn presentation_authority( + &self, + browsing_context: BrowsingContextId, + ) -> Result { + self.session.presentation_authority(browsing_context) + } + + /// Advance one active owned context to a new authority epoch. + pub fn advance_context_epoch( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.session.advance_context_epoch(browsing_context) + } + + /// Destroy the exact owned disposable boundary through the bound lifecycle port. + pub fn destroy_disposable_context( + &mut self, + authority: &PresentationMutationAuthority, + ) -> Result<(), BrowserSessionError> { + self.session + .destroy_disposable_context_with_port(authority, &mut self.port) + } + + /// Record browser transport loss without exposing mutable lifecycle-port access. + pub fn record_transport_loss(&mut self) -> bool { + self.session.record_transport_loss() + } + + /// End the Browser Session only after every owned context has proven destruction. + pub fn end(&mut self) -> Result<(), BrowserSessionError> { + self.session.end() + } + + /// Verify normal completion without relinquishing the exact bound lifecycle owner on failure. + /// + /// A rejected finish leaves the wrapper intact so the caller can destroy or reconcile outstanding + /// contexts and retry. After success the aggregate is `Ended`; dropping the wrapper is then inert. + pub fn finish(&mut self) -> Result<(), BrowserSessionError> { + self.session.end() + } +} + +impl BoundBrowserSession

{ + /// Execute one adapter-defined operation through the exact consumed adapter after authority validation. + /// + /// Browser Session validates session incarnation, isolation identity, browsing-context identity, + /// and epoch before the adapter receives the operation. Stale or foreign authority therefore fails + /// before adapter I/O, while the adapter-specific operation vocabulary remains outside this domain. + pub fn execute_authorized_context_operation( + &mut self, + authority: &PresentationMutationAuthority, + operation: P::Operation, + ) -> Result> { + let browser_session = self.session.id; + let incarnation = self.session.incarnation; + let record = self + .session + .context_for_authority_mut(authority) + .map_err(AuthorizedContextOperationError::BrowserSession)?; + let context = record.handle.clone(); + let context_epoch = record.epoch; + let request = AuthorizedContextOperationRequest { + browser_session, + incarnation, + context, + context_epoch, + operation, + }; + self.port + .execute_authorized_context_operation(&request) + .map_err(AuthorizedContextOperationError::Adapter) + } +} + +fn reserve_epoch(next_epoch: &mut u64) -> Result { + let epoch = BrowserContextEpoch(*next_epoch); + *next_epoch = next_epoch + .checked_add(1) + .ok_or(BrowserSessionError::EpochExhausted)?; + Ok(epoch) +} + +fn allocate_incarnation( + counter: &AtomicU64, +) -> Result { + let value = counter + .try_update(Ordering::SeqCst, Ordering::SeqCst, |current| { + current.checked_add(1) + }) + .map_err(|_| BrowserSessionError::IncarnationExhausted)?; + Ok(BrowserSessionIncarnation(value)) +} + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + use std::collections::VecDeque; + + #[derive(Debug)] + struct TestPort { + handles: VecDeque, + create_error: Option, + fail_destroy: bool, + fail_completion: bool, + create_calls: usize, + destroy_calls: usize, + create_sessions: Vec, + create_incarnations: Vec, + create_attempts: Vec, + create_completions: Vec<( + BrowserSessionId, + BrowserSessionIncarnation, + BrowserContextEpoch, + DisposableContextCreateDisposition, + )>, + destroy_sessions: Vec, + destroy_incarnations: Vec, + destroyed_isolations: Vec, + } + + impl TestPort { + fn new(context: u64, isolation: &str) -> Self { + Self::with_handles(vec![DisposableContextHandle::new( + isolation_id(isolation), + context_id(context), + )]) + } + + fn with_handles(handles: Vec) -> Self { + Self { + handles: handles.into(), + create_error: None, + fail_destroy: false, + fail_completion: false, + create_calls: 0, + destroy_calls: 0, + create_sessions: Vec::new(), + create_incarnations: Vec::new(), + create_attempts: Vec::new(), + create_completions: Vec::new(), + destroy_sessions: Vec::new(), + destroy_incarnations: Vec::new(), + destroyed_isolations: Vec::new(), + } + } + } + + impl DisposableContextPort for TestPort { + fn create_disposable_context( + &mut self, + request: &DisposableContextCreateRequest, + ) -> Result { + self.create_calls += 1; + self.create_sessions.push(request.browser_session()); + self.create_incarnations.push(request.incarnation()); + self.create_attempts.push(request.attempt_epoch()); + match self.create_error.clone() { + Some(error) => Err(error), + None => Ok(self + .handles + .pop_front() + .expect("test must provide one handle per successful creation")), + } + } + + fn complete_disposable_context_creation( + &mut self, + completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + self.create_completions.push(( + completion.browser_session(), + completion.incarnation(), + completion.attempt_epoch(), + completion.disposition(), + )); + if self.fail_completion { + Err(DisposableContextCreateCompletionError::CompletionFailed) + } else { + Ok(()) + } + } + + fn destroy_disposable_context( + &mut self, + request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls += 1; + self.destroy_sessions.push(request.browser_session()); + self.destroy_incarnations.push(request.incarnation()); + self.destroyed_isolations + .push(request.context().isolation.clone()); + if self.fail_destroy { + Err(DisposableContextDestroyError::DestroyFailed) + } else { + Ok(()) + } + } + } + + fn session_id(value: u64) -> BrowserSessionId { + BrowserSessionId::new(value).expect("valid session id") + } + + fn context_id(value: u64) -> BrowsingContextId { + BrowsingContextId::new(value).expect("valid context id") + } + + fn isolation_id(value: &str) -> DisposableIsolationId { + DisposableIsolationId::parse(value).expect("valid isolation id") + } + + fn session(value: u64) -> BrowserSession { + BrowserSession::start(session_id(value)).expect("incarnation capacity") + } + + #[test] + fn isolation_identity_validation_preserves_protocol_text() { + assert_eq!( + DisposableIsolationId::parse(""), + Err(DisposableIsolationIdError::Empty) + ); + let long = "x".repeat(4097); + let long_identity = DisposableIsolationId::parse(&long) + .expect("WebDriver BiDi browser.UserContext does not define a 4096-byte limit"); + assert_eq!(long_identity.as_str(), long); + assert_eq!( + DisposableIsolationId::parse(" user-context "), + Err(DisposableIsolationIdError::InvalidCharacter) + ); + assert_eq!( + DisposableIsolationId::parse("user\ncontext"), + Err(DisposableIsolationIdError::InvalidCharacter) + ); + let valid = isolation_id("webdriver-user-context-10"); + assert_eq!(valid.as_str(), "webdriver-user-context-10"); + let handle = DisposableContextHandle::new(valid.clone(), context_id(10)); + assert_eq!(handle.isolation(), &valid); + assert_eq!(handle.browsing_context(), context_id(10)); + } + + #[test] + fn bound_creation_is_the_only_raw_context_entry_to_authority() { + let raw_session = session(1); + assert_eq!(raw_session.id(), session_id(1)); + assert_ne!(raw_session.incarnation().value(), 0); + assert!(!raw_session.transport_is_lost()); + assert!(raw_session.recovery_evidence().is_empty()); + assert_eq!( + raw_session.presentation_authority(context_id(10)), + Err(BrowserSessionError::ContextNotOwned) + ); + let mut bound = raw_session.bind_lifecycle_port(TestPort::new(10, "isolation-10")); + let authority = bound + .create_disposable_context() + .expect("owned disposable context"); + assert_eq!(bound.port.create_sessions, vec![session_id(1)]); + assert_eq!( + bound.port.create_incarnations, + vec![bound.browser_session().incarnation()] + ); + assert_eq!(bound.port.create_attempts, vec![BrowserContextEpoch(1)]); + assert_eq!( + bound.port.create_completions, + vec![( + session_id(1), + bound.browser_session().incarnation(), + BrowserContextEpoch(1), + DisposableContextCreateDisposition::Accepted, + )] + ); + assert_eq!(authority.browser_session(), session_id(1)); + assert_eq!( + authority.incarnation(), + bound.browser_session().incarnation() + ); + assert_eq!(authority.isolation().as_str(), "isolation-10"); + assert_eq!(authority.browsing_context(), context_id(10)); + assert_eq!(authority.context_epoch().value(), 1); + assert_eq!(bound.presentation_authority(context_id(10)), Ok(authority)); + } + + #[test] + fn creation_failure_preserves_known_recovery_identity() { + let mut clean_port = TestPort::new(20, "isolation-20"); + clean_port.create_error = Some(DisposableContextCreateError::CreateFailedClean); + let mut clean = session(2).bind_lifecycle_port(clean_port); + assert_eq!( + clean.create_disposable_context(), + Err(BrowserSessionError::ContextCreationFailed) + ); + assert_eq!(clean.browser_session().state(), BrowserSessionState::Active); + clean.end().expect("clean failure can end"); + + let mut unknown_port = TestPort::new(210, "isolation-210"); + unknown_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(None)); + let mut unknown = session(21).bind_lifecycle_port(unknown_port); + assert_eq!( + unknown.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert!(unknown.browser_session().recovery_evidence().is_empty()); + assert_eq!(unknown.browser_session().create_attempt_recovery_evidence().len(), 1); + match &unknown.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch, + isolation, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(isolation, &None); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + + let known = isolation_id("partial-user-context-211"); + let mut known_port = TestPort::new(211, "unused"); + known_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(Some( + known.clone(), + ))); + let mut known_session = session(22).bind_lifecycle_port(known_port); + assert_eq!( + known_session.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert_eq!( + known_session.browser_session().recovery_evidence(), + &[BrowserSessionRecoveryEvidence::PartialCreationIsolation( + known.clone() + )] + ); + assert_eq!(known_session.browser_session().create_attempt_recovery_evidence().len(), 1); + match &known_session.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::FailedUncertain { + attempt_epoch, + isolation, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(isolation.as_ref(), Some(&known)); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + assert_eq!( + known_session.end(), + Err(BrowserSessionError::SessionNotActive) + ); + } + + #[test] + fn duplicate_adapter_output_preserves_offending_handle() { + let first_context_handle = + DisposableContextHandle::new(isolation_id("isolation-30-a"), context_id(30)); + let duplicate_context_handle = + DisposableContextHandle::new(isolation_id("isolation-30-b"), context_id(30)); + let context_port = TestPort::with_handles(vec![ + first_context_handle.clone(), + duplicate_context_handle.clone(), + ]); + let mut duplicate_context = session(3).bind_lifecycle_port(context_port); + duplicate_context + .create_disposable_context() + .expect("first owned context"); + assert_eq!( + duplicate_context.create_disposable_context(), + Err(BrowserSessionError::DuplicateBrowsingContext) + ); + assert_eq!( + duplicate_context.browser_session().recovery_evidence(), + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_context_handle), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_context_handle), + ] + ); + + let first_isolation_handle = + DisposableContextHandle::new(isolation_id("isolation-31"), context_id(310)); + let duplicate_isolation_handle = + DisposableContextHandle::new(isolation_id("isolation-31"), context_id(311)); + let isolation_port = TestPort::with_handles(vec![ + first_isolation_handle.clone(), + duplicate_isolation_handle.clone(), + ]); + let mut duplicate_isolation = session(31).bind_lifecycle_port(isolation_port); + duplicate_isolation + .create_disposable_context() + .expect("first owned isolation"); + assert_eq!( + duplicate_isolation.create_disposable_context(), + Err(BrowserSessionError::DuplicateDisposableIsolation) + ); + assert_eq!( + duplicate_isolation.browser_session().recovery_evidence(), + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_isolation_handle), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_isolation_handle), + ] + ); + } + + #[test] + fn create_completion_failure_preserves_non_authorizing_recovery_evidence() { + let expected = DisposableContextHandle::new(isolation_id("isolation-315"), context_id(315)); + let mut port = TestPort::with_handles(vec![expected.clone()]); + port.fail_completion = true; + let mut bound = session(315).bind_lifecycle_port(port); + + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert_eq!( + bound.browser_session().recovery_evidence(), + &[BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( + expected.clone() + )] + ); + assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 1); + match &bound.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch, + disposition, + context, + } => { + assert_eq!(attempt_epoch.value(), 1); + assert_eq!(*disposition, DisposableContextCreateDisposition::Accepted); + assert_eq!(context, &expected); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + assert_eq!( + bound.port.create_completions[0].3, + DisposableContextCreateDisposition::Accepted + ); + assert_eq!( + bound.presentation_authority(context_id(315)), + Err(BrowserSessionError::SessionNotActive) + ); + } + + #[test] + fn rejected_create_completion_failure_preserves_duplicate_and_unsettled_evidence() { + let first = DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); + let duplicate = + DisposableContextHandle::new(isolation_id("isolation-316-b"), context_id(316)); + let port = TestPort::with_handles(vec![first.clone(), duplicate.clone()]); + let mut bound = session(316).bind_lifecycle_port(port); + + bound + .create_disposable_context() + .expect("first candidate accepted"); + bound.port.fail_completion = true; + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + assert_eq!( + bound.browser_session().recovery_evidence(), + &[ + BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate.clone()), + BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate.clone()), + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first), + ] + ); + assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 2); + match &bound.browser_session().create_attempt_recovery_evidence()[0] { + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { + attempt_epoch, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(context, &duplicate); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + match &bound.browser_session().create_attempt_recovery_evidence()[1] { + DisposableContextCreateRecoveryEvidence::CompletionUnsettled { + attempt_epoch, + disposition, + context, + } => { + assert_eq!(attempt_epoch.value(), 2); + assert_eq!(*disposition, DisposableContextCreateDisposition::Rejected); + assert_eq!(context, &duplicate); + } + other => panic!("unexpected recovery evidence: {other:?}"), + } + assert_eq!( + bound.port.create_completions[1].3, + DisposableContextCreateDisposition::Rejected + ); + } + + #[test] + fn bound_port_is_structural_and_not_swappable() { + let approved = TestPort::new(320, "isolation-320"); + let other = TestPort::new(321, "isolation-321"); + let mut bound = session(32).bind_lifecycle_port(approved); + let authority = bound + .create_disposable_context() + .expect("owned context uses consumed port"); + assert_eq!(other.create_calls, 0); + assert_eq!(other.destroy_calls, 0); + bound + .destroy_disposable_context(&authority) + .expect("same structurally bound port destroys context"); + assert_eq!(bound.port.create_calls, 1); + assert_eq!(bound.port.destroy_calls, 1); + } + + #[test] + fn epoch_exhaustion_prevents_creation_io() { + let mut bound = session(4).bind_lifecycle_port(TestPort::new(40, "isolation-40")); + bound.session.next_epoch = u64::MAX; + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::EpochExhausted) + ); + assert_eq!(bound.port.create_calls, 0); + } + + #[test] + fn epoch_exhaustion_prevents_advance_mutation() { + let mut bound = session(41).bind_lifecycle_port(TestPort::new(410, "isolation-410")); + let authority = bound.create_disposable_context().expect("owned context"); + bound.session.next_epoch = u64::MAX; + assert_eq!( + bound.advance_context_epoch(context_id(410)), + Err(BrowserSessionError::EpochExhausted) + ); + assert_eq!(bound.presentation_authority(context_id(410)), Ok(authority)); + } + + #[test] + fn epoch_advance_invalidates_old_and_unknown_authority() { + let mut bound = session(5).bind_lifecycle_port(TestPort::new(50, "isolation-50")); + let old = bound.create_disposable_context().expect("owned context"); + assert_eq!( + bound.advance_context_epoch(context_id(51)), + Err(BrowserSessionError::ContextNotOwned) + ); + let new = bound + .advance_context_epoch(context_id(50)) + .expect("advanced epoch"); + assert_eq!(new.context_epoch().value(), 2); + assert_eq!( + bound.destroy_disposable_context(&old), + Err(BrowserSessionError::AuthorityMismatch) + ); + bound + .destroy_disposable_context(&new) + .expect("destroy current epoch"); + assert_eq!( + bound.port.destroy_incarnations, + vec![bound.browser_session().incarnation()] + ); + assert_eq!( + bound.presentation_authority(context_id(50)), + Err(BrowserSessionError::ContextNotOwned) + ); + assert_eq!( + bound.destroy_disposable_context(&new), + Err(BrowserSessionError::ContextNotOwned) + ); + } + + #[test] + fn cross_session_and_foreign_isolation_authority_fail_before_io() { + let mut owner = session(6).bind_lifecycle_port(TestPort::new(60, "isolation-60")); + let authority = owner.create_disposable_context().expect("owner context"); + + let mut foreign = session(7).bind_lifecycle_port(TestPort::new(60, "isolation-60")); + foreign + .create_disposable_context() + .expect("foreign context"); + assert_eq!( + foreign.destroy_disposable_context(&authority), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!(foreign.port.destroy_calls, 0); + + let forged = PresentationMutationAuthority { + browser_session: owner.browser_session().id(), + incarnation: owner.browser_session().incarnation(), + isolation: isolation_id("foreign-isolation"), + browsing_context: authority.browsing_context(), + context_epoch: authority.context_epoch(), + }; + assert_eq!( + owner.destroy_disposable_context(&forged), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!(owner.port.destroy_calls, 0); + } + + #[test] + fn sequential_incarnation_reuse_rejects_stale_authority() { + let shared_id = session_id(8); + let mut session_a = BrowserSession::start(shared_id) + .expect("A incarnation") + .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); + let authority_a = session_a.create_disposable_context().expect("A context"); + session_a + .destroy_disposable_context(&authority_a) + .expect("A destroy"); + session_a.end().expect("A end"); + + let mut session_b = BrowserSession::start(shared_id) + .expect("B incarnation") + .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); + let authority_b = session_b.create_disposable_context().expect("B context"); + assert_ne!( + session_a.browser_session().incarnation(), + session_b.browser_session().incarnation() + ); + assert_eq!( + session_b.destroy_disposable_context(&authority_a), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!(session_b.port.destroy_calls, 0); + session_b + .destroy_disposable_context(&authority_b) + .expect("B destroy"); + assert_eq!(session_b.port.destroy_calls, 1); + } + + #[test] + fn destroy_failure_retains_handle_and_transport_loss_orthogonally() { + let expected_handle = + DisposableContextHandle::new(isolation_id("isolation-90"), context_id(90)); + let mut port = TestPort::new(90, "isolation-90"); + port.fail_destroy = true; + let mut bound = session(9).bind_lifecycle_port(port); + let authority = bound.create_disposable_context().expect("owned context"); + let expected_epoch = authority.context_epoch(); + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert_eq!( + bound.browser_session().recovery_evidence(), + &[BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: expected_handle, + context_epoch: expected_epoch, + }] + ); + assert!(!bound.browser_session().transport_is_lost()); + assert!(bound.record_transport_loss()); + assert!(bound.browser_session().transport_is_lost()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::RecoveryRequired + ); + assert!(!bound.record_transport_loss()); + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::SessionNotActive) + ); + assert_eq!( + bound.presentation_authority(context_id(90)), + Err(BrowserSessionError::SessionNotActive) + ); + assert_eq!( + bound.advance_context_epoch(context_id(90)), + Err(BrowserSessionError::SessionNotActive) + ); + assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); + } + + #[test] + fn transport_loss_invalidates_active_contexts_and_is_idempotent() { + let mut bound = session(10).bind_lifecycle_port(TestPort::new(100, "isolation-100")); + let authority = bound.create_disposable_context().expect("owned context"); + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert!(bound.browser_session().transport_is_lost()); + assert!(!bound.record_transport_loss()); + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::SessionNotActive) + ); + assert_eq!(bound.port.destroy_calls, 0); + } + + #[test] + fn normal_end_requires_proven_destruction_and_ignores_late_transport_report() { + let mut bound = session(11).bind_lifecycle_port(TestPort::new(110, "isolation-110")); + let authority = bound.create_disposable_context().expect("owned context"); + assert_eq!(bound.end(), Err(BrowserSessionError::ActiveContextRemains)); + bound + .destroy_disposable_context(&authority) + .expect("proven destruction"); + assert_eq!(bound.port.destroy_sessions, vec![session_id(11)]); + assert_eq!( + bound.port.destroyed_isolations, + vec![isolation_id("isolation-110")] + ); + bound.end().expect("normal end"); + assert_eq!(bound.browser_session().state(), BrowserSessionState::Ended); + assert!(!bound.record_transport_loss()); + assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); + } + + #[test] + fn incarnation_allocator_fails_closed_before_wrap() { + let counter = AtomicU64::new(u64::MAX); + let error = BrowserSession::start_with_counter(session_id(12), &counter) + .expect_err("incarnation allocation must fail closed before wrapping"); + assert_eq!(error, BrowserSessionError::IncarnationExhausted); + } +} diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 47d8ccc0a..e16a75fa9 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -1,1745 +1,13 @@ //! Browser Session lifecycle authority for OriginWeave. //! -//! This crate owns the domain transition that turns a newly created disposable -//! browser isolation boundary into presentation-mutation authority. Driver identifiers -//! remain adapter data: naming a session or browsing context is never sufficient to mint authority. +//! The aggregate implementation remains isolated from recovery custody. Public callers receive only +//! the narrow domain surface re-exported here; the concrete lifecycle adapter is never exposed. #![forbid(unsafe_code)] #![deny(missing_docs)] -use std::collections::BTreeMap; -use std::fmt; -use std::sync::atomic::{AtomicU64, Ordering}; +mod browser_session; +mod recovery; -use originweave_core::{BrowserSessionId, BrowsingContextId}; - -static NEXT_BROWSER_SESSION_INCARNATION: AtomicU64 = AtomicU64::new(1); -static ABANDONED_BOUND_SESSIONS: AtomicU64 = AtomicU64::new(0); - -/// Return the number of bound Browser Sessions abandoned with unresolved remote ownership. -/// -/// This is a process-local, non-I/O operability signal. It deliberately does not claim that remote -/// browser cleanup happened and is not a substitute for persisting exact recovery evidence before a -/// process exits. -#[must_use] -pub fn abandoned_bound_session_count() -> u64 { - ABANDONED_BOUND_SESSIONS.load(Ordering::Relaxed) -} - -/// Current lifecycle state of one Browser Session aggregate. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BrowserSessionState { - /// The session may create and own disposable contexts. - Active, - /// Every owned context was destroyed and the session was ended normally. - Ended, - /// The browser transport was lost while no ownership-recovery condition preceded it. - TransportLost, - /// Browser lifecycle ownership became uncertain and requires external reconciliation. - RecoveryRequired, -} - -/// Domain failure while changing Browser Session ownership state. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BrowserSessionError { - /// The requested transition requires an active Browser Session. - SessionNotActive, - /// No unused session-incarnation identity remains in this process. - IncarnationExhausted, - /// No unused context epoch remains, so no new authority can be issued safely. - EpochExhausted, - /// The disposable-context port proved that context creation failed without creating a boundary. - ContextCreationFailed, - /// Context creation may have created browser state that the aggregate cannot safely own or destroy. - ContextCreationUncertain, - /// The port returned a browsing-context identity already known to this aggregate. - DuplicateBrowsingContext, - /// The port returned an isolation identity already known to this aggregate. - DuplicateDisposableIsolation, - /// The requested context is not currently owned and active in this session. - ContextNotOwned, - /// The supplied authority belongs to another incarnation, isolation boundary, session, context, or epoch. - AuthorityMismatch, - /// The disposable-context port could not prove destruction of the owned isolation boundary. - ContextDestructionFailed, - /// Normal session end was requested while an owned or uncertain context remains. - ActiveContextRemains, -} - -/// Bounded failure from disposable-context creation. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DisposableContextCreateError { - /// Creation failed and the adapter proved that no disposable boundary was created. - CreateFailedClean, - /// Creation failed after ownership may have changed. The optional identity is the exact - /// browser-issued isolation identity already known at the failure boundary, when available. - CreateFailedUncertain(Option), -} - -/// Bounded failure from disposable-context destruction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DisposableContextDestroyError { - /// Destruction of an owned disposable context failed or could not be proven. - DestroyFailed, -} - -/// Validation failure for a browser-issued disposable isolation identity. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DisposableIsolationIdError { - /// The identity is empty. - Empty, - /// The identity contains surrounding whitespace or control characters. - InvalidCharacter, -} - -/// Browser-issued identity for one disposable isolation boundary. -/// -/// This value is addressability, not mutation authority. A conforming adapter must return a value -/// that is non-aliasing for the live lifetime of the created boundary. A WebDriver BiDi adapter -/// should map this one-to-one to the specification-defined unique user-context identifier. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DisposableIsolationId(String); - -impl DisposableIsolationId { - /// Parse one browser-issued isolation identity without inventing a protocol length limit. - pub fn parse(value: &str) -> Result { - if value.is_empty() { - return Err(DisposableIsolationIdError::Empty); - } - if value.trim() != value || value.chars().any(char::is_control) { - return Err(DisposableIsolationIdError::InvalidCharacter); - } - Ok(Self(value.to_owned())) - } - - /// Return the validated browser-issued isolation identity. - #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } -} - -/// Process-local, non-reused identity for one Browser Session aggregate incarnation. -/// -/// Presentation authority is intentionally non-serializable. A process restart therefore destroys -/// every outstanding authority value. Within one process this monotonic identity prevents a later -/// aggregate from revalidating an authority retained from an earlier aggregate that reused the same -/// transport/session and browser-issued context identifiers. The identity is also passed through the -/// lifecycle port so an adapter must scope its remote ownership mapping to the same incarnation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct BrowserSessionIncarnation(u64); - -impl BrowserSessionIncarnation { - /// Return the monotonic process-local incarnation value. - #[must_use] - pub const fn value(self) -> u64 { - self.0 - } -} - -/// Adapter result for one newly created disposable browser context. -/// -/// The isolation identity scopes the lifecycle boundary used for destruction; the browsing-context -/// identity addresses the independently navigable context inside that boundary. Neither field alone -/// is presentation-mutation authority. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DisposableContextHandle { - isolation: DisposableIsolationId, - browsing_context: BrowsingContextId, -} - -impl DisposableContextHandle { - /// Bind one validated isolation identity to its created browsing context. - #[must_use] - pub fn new(isolation: DisposableIsolationId, browsing_context: BrowsingContextId) -> Self { - Self { - isolation, - browsing_context, - } - } - - /// Return the non-aliasing disposable isolation identity. - #[must_use] - pub fn isolation(&self) -> &DisposableIsolationId { - &self.isolation - } - - /// Return the browsing-context address inside the disposable boundary. - #[must_use] - pub const fn browsing_context(&self) -> BrowsingContextId { - self.browsing_context - } -} - -/// Lossless evidence retained when browser lifecycle ownership is no longer proven. -/// -/// These values authorize no browser command. They exist only so a separately reviewed recovery -/// path can later reconcile exact remote identities instead of guessing from raw session/context ids. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum BrowserSessionRecoveryEvidence { - /// A partial creation exposed a browser-issued isolation identity before completion became uncertain. - PartialCreationIsolation(DisposableIsolationId), - /// A create call returned a complete handle that aliased an already-owned context or isolation. - DuplicateAdapterHandle(DisposableContextHandle), - /// A complete create result could not be settled with the bound adapter after domain validation. - UnsettledAdapterHandle(DisposableContextHandle), - /// Destruction of this exact owned handle and validated authority epoch failed or could not be proven. - UnprovenDestruction { - /// Exact owned context whose remote boundary remains uncertain. - context: DisposableContextHandle, - /// Browser Session epoch validated immediately before destroy I/O. - context_epoch: BrowserContextEpoch, - }, - /// A recovery condition elsewhere in the session made this active owned handle uncertain. - RecoveryRequiredOwnedHandle(DisposableContextHandle), - /// Transport loss made this previously active owned handle uncertain. - TransportLossOwnedHandle(DisposableContextHandle), -} - -/// Exact create-attempt facts retained when one Browser Session creation transaction becomes uncertain. -/// -/// The legacy identity-oriented [`BrowserSessionRecoveryEvidence`] remains useful to recovery code that -/// reconciles remote handles. This companion evidence preserves the aggregate-issued attempt epoch and -/// completion disposition so two lifecycle facts with the same remote values cannot be collapsed into -/// one transaction. These values grant no browser command authority. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DisposableContextCreateRecoveryEvidence { - /// The adapter reported an uncertain create failure before a complete handle was available. - FailedUncertain { - /// Exact aggregate-issued epoch reserved for the failed create attempt. - attempt_epoch: BrowserContextEpoch, - /// Browser-issued isolation identity known at the failure boundary, when available. - isolation: Option, - }, - /// A complete candidate aliased already-owned browser state and was rejected by the aggregate. - DuplicateCandidate { - /// Exact aggregate-issued epoch reserved for the rejected create attempt. - attempt_epoch: BrowserContextEpoch, - /// Exact adapter-returned candidate associated with that attempt. - context: DisposableContextHandle, - }, - /// The adapter could not prove completion settlement for one exact create attempt. - CompletionUnsettled { - /// Exact aggregate-issued epoch reserved for the unsettled create attempt. - attempt_epoch: BrowserContextEpoch, - /// Aggregate decision whose delivery to the adapter could not be proven. - disposition: DisposableContextCreateDisposition, - /// Exact adapter-returned candidate associated with that attempt. - context: DisposableContextHandle, - }, -} - -/// Opaque Browser Session-issued request for one disposable-context creation attempt. -/// -/// There is deliberately no public constructor. A request is created only inside a -/// [`BoundBrowserSession`], after Browser Session has validated that the aggregate is active. Raw -/// session, incarnation, context, isolation, or adapter-selected identifiers cannot recreate it. -#[derive(Debug)] -pub struct DisposableContextCreateRequest { - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - attempt_epoch: BrowserContextEpoch, -} - -impl DisposableContextCreateRequest { - /// Return the Browser Session transport identity for adapter addressability. - #[must_use] - pub const fn browser_session(&self) -> BrowserSessionId { - self.browser_session - } - - /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the unique context epoch reserved for this create attempt. - #[must_use] - pub const fn attempt_epoch(&self) -> BrowserContextEpoch { - self.attempt_epoch - } -} - -/// Domain disposition for one completed disposable-context create attempt. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DisposableContextCreateDisposition { - /// The returned handle passed Browser Session ownership validation and may become authorizing. - Accepted, - /// The returned handle failed Browser Session ownership validation and must remain non-authorizing. - Rejected, -} - -/// Opaque Browser Session-issued completion for one exact create attempt. -/// -/// The adapter may stage remote protocol state while executing a create request, but it must not -/// promote that state into an authorizing binding until it receives an `Accepted` completion for the -/// same session incarnation and attempt epoch. `Rejected` candidates are recovery/quarantine evidence -/// only. There is deliberately no public constructor. -#[derive(Debug)] -pub struct DisposableContextCreateCompletion { - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - attempt_epoch: BrowserContextEpoch, - disposition: DisposableContextCreateDisposition, -} - -impl DisposableContextCreateCompletion { - /// Return the Browser Session transport identity for adapter correlation. - #[must_use] - pub const fn browser_session(&self) -> BrowserSessionId { - self.browser_session - } - - /// Return the Browser Session incarnation for adapter correlation. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the create-attempt epoch that this completion settles. - #[must_use] - pub const fn attempt_epoch(&self) -> BrowserContextEpoch { - self.attempt_epoch - } - - /// Return whether Browser Session accepted or rejected the created candidate. - #[must_use] - pub const fn disposition(&self) -> DisposableContextCreateDisposition { - self.disposition - } -} - -/// Failure while settling one exact create attempt with the bound lifecycle adapter. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DisposableContextCreateCompletionError { - /// The adapter could not prove that the exact pending create attempt reached the requested state. - CompletionFailed, -} - -/// Opaque Browser Session-issued request for destruction of one exact owned disposable context. -/// -/// There is deliberately no public constructor. The bound aggregate creates this request only after -/// validating the supplied presentation authority against current ownership. A caller cannot rebuild -/// cleanup authority from raw browser identifiers. The validated epoch is carried only as correlation -/// evidence for the already-authorized request; it is not independently sufficient to destroy state. -#[derive(Debug)] -pub struct DisposableContextDestroyRequest { - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - context: DisposableContextHandle, - context_epoch: BrowserContextEpoch, -} - -impl DisposableContextDestroyRequest { - /// Return the Browser Session transport identity for adapter addressability. - #[must_use] - pub const fn browser_session(&self) -> BrowserSessionId { - self.browser_session - } - - /// Return the non-reused Browser Session incarnation for adapter lifecycle mapping. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the exact domain handle whose remote isolation boundary must be destroyed. - #[must_use] - pub const fn context(&self) -> &DisposableContextHandle { - &self.context - } - - /// Return the exact Browser Session epoch validated before destroy I/O. - #[must_use] - pub const fn context_epoch(&self) -> BrowserContextEpoch { - self.context_epoch - } -} - -/// Port implemented by a reviewed browser adapter for disposable context lifecycle operations. -/// -/// The port never self-asserts an instance identifier. Instead, Browser Session consumes one concrete -/// port value into [`BoundBrowserSession`]. Public lifecycle methods then use only that owned port, so a -/// caller cannot swap a second adapter instance into create or destroy after binding. The port receives -/// only aggregate-issued request values with private construction paths. -/// -/// For WebDriver BiDi, creation should map the isolation identity one-to-one to the user-context -/// identifier returned by `browser.createUserContext`. [`DisposableContextCreateError::CreateFailedClean`] -/// is allowed only when the adapter proves that no disposable state was created. If a user-context -/// identity is already known when later creation or verification becomes uncertain, the adapter must -/// return it inside [`DisposableContextCreateError::CreateFailedUncertain`]. -/// -/// `destroy_disposable_context` must destroy the exact boundary carried by the supplied request and -/// return success only after destruction is proven. Reconstructing cleanup authority from raw driver -/// identifiers is forbidden, and a command acknowledgement alone is insufficient evidence. -pub trait DisposableContextPort { - /// Create one fresh disposable isolation boundary and browsing context for this authorized request. - fn create_disposable_context( - &mut self, - request: &DisposableContextCreateRequest, - ) -> Result; - - /// Settle the exact create attempt after Browser Session validates the returned domain handle. - /// - /// An adapter must keep a successful remote create result non-authorizing until this completion - /// accepts the matching attempt. A rejected attempt must remain non-authorizing and be retained - /// only for recovery/quarantine processing. - fn complete_disposable_context_creation( - &mut self, - completion: &DisposableContextCreateCompletion, - ) -> Result<(), DisposableContextCreateCompletionError>; - - /// Destroy the exact disposable isolation boundary represented by this authorized request. - fn destroy_disposable_context( - &mut self, - request: &DisposableContextDestroyRequest, - ) -> Result<(), DisposableContextDestroyError>; -} - -/// Opaque aggregate-authorized request for one purpose-bounded adapter operation. -/// -/// The caller supplies only the adapter-defined operation value. Browser Session validates the -/// accompanying presentation authority first and privately binds the operation to the exact owned -/// context and validated epoch before the consumed adapter can observe it. There is deliberately no -/// public constructor, and the epoch is correlation/provenance rather than standalone authority. -pub struct AuthorizedContextOperationRequest { - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - context: DisposableContextHandle, - context_epoch: BrowserContextEpoch, - operation: O, -} - -impl AuthorizedContextOperationRequest { - /// Return the Browser Session transport identity for adapter addressability. - #[must_use] - pub const fn browser_session(&self) -> BrowserSessionId { - self.browser_session - } - - /// Return the non-reused Browser Session incarnation for adapter lifecycle correlation. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the exact currently owned context validated before adapter I/O. - #[must_use] - pub const fn context(&self) -> &DisposableContextHandle { - &self.context - } - - /// Return the exact Browser Session epoch validated before adapter I/O. - #[must_use] - pub const fn context_epoch(&self) -> BrowserContextEpoch { - self.context_epoch - } - - /// Return the adapter-defined purpose-bounded operation payload. - #[must_use] - pub const fn operation(&self) -> &O { - &self.operation - } -} - -/// Failure from executing an aggregate-authorized operation through the consumed adapter. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthorizedContextOperationError { - /// Browser Session rejected the authority before adapter I/O. - BrowserSession(BrowserSessionError), - /// The bound adapter attempted the authorized operation and returned its bounded failure. - Adapter(E), -} - -/// Adapter extension for purpose-bounded operations that must use the exact consumed adapter. -/// -/// Browser Session remains protocol-agnostic: the adapter owns the operation, output, and error -/// types. The wrapper only proves current ownership and routes the opaque request to the same concrete -/// adapter instance used for lifecycle creation and destruction. Implementations must not treat the -/// request as permission to mutate any other context. -pub trait AuthorizedContextOperationPort: DisposableContextPort { - /// Adapter-defined operation vocabulary, such as a reviewed BiDi presentation command. - type Operation; - /// Adapter-defined successful result. - type Output; - /// Adapter-defined bounded operation failure. - type Error; - - /// Execute one aggregate-authorized operation against the exact context carried by the request. - fn execute_authorized_context_operation( - &mut self, - request: &AuthorizedContextOperationRequest, - ) -> Result; -} - -/// Monotonic identity for one owned browsing-context authority epoch. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct BrowserContextEpoch(u64); - -impl BrowserContextEpoch { - /// Return the internal monotonic epoch value. - #[must_use] - pub const fn value(self) -> u64 { - self.0 - } -} - -/// Opaque proof that Browser Session currently owns presentation mutation for one context epoch. -/// -/// The fields are private and no public constructor exists. A caller obtains this value only after -/// Browser Session has created a disposable boundary through its bound lifecycle port. Session -/// incarnation, isolation identity, context identity, and epoch must all still match before adapter I/O -/// is allowed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PresentationMutationAuthority { - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - isolation: DisposableIsolationId, - browsing_context: BrowsingContextId, - context_epoch: BrowserContextEpoch, -} - -impl PresentationMutationAuthority { - /// Return the Browser Session transport identity associated with this authority. - #[must_use] - pub const fn browser_session(&self) -> BrowserSessionId { - self.browser_session - } - - /// Return the Browser Session incarnation that minted this authority. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the owned disposable isolation identity. - #[must_use] - pub fn isolation(&self) -> &DisposableIsolationId { - &self.isolation - } - - /// Return the owned browsing-context identity. - #[must_use] - pub const fn browsing_context(&self) -> BrowsingContextId { - self.browsing_context - } - - /// Return the exact context epoch covered by this authority. - #[must_use] - pub const fn context_epoch(&self) -> BrowserContextEpoch { - self.context_epoch - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum OwnedContextState { - Active, - Destroyed, - Uncertain, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct OwnedContextRecord { - handle: DisposableContextHandle, - epoch: BrowserContextEpoch, - state: OwnedContextState, -} - -/// Aggregate root for disposable browser-context lifecycle and presentation mutation authority. -#[derive(Debug)] -pub struct BrowserSession { - id: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - state: BrowserSessionState, - transport_lost: bool, - next_epoch: u64, - contexts: BTreeMap, - recovery_evidence: Vec, - create_recovery_evidence: Vec, -} - -/// Browser Session composed with the one lifecycle-port instance allowed to mutate its remote state. -/// -/// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and -/// no public Browser Session lifecycle method accepts an arbitrary port parameter. This makes adapter -/// ownership structural rather than dependent on a caller-selected scalar or an adapter callback. -#[must_use = "destroy owned browser state and finish the session, or hand unresolved ownership to recovery"] -pub struct BoundBrowserSession

{ - session: BrowserSession, - port: P, -} - -impl

fmt::Debug for BoundBrowserSession

{ - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("BoundBrowserSession") - .field("browser_session", &self.session.id) - .field("incarnation", &self.session.incarnation) - .field("state", &self.session.state) - .field("transport_lost", &self.session.transport_lost) - .field("owned_context_count", &self.session.contexts.len()) - .field( - "recovery_evidence_count", - &self.session.recovery_evidence.len(), - ) - .field( - "create_recovery_evidence_count", - &self.session.create_recovery_evidence.len(), - ) - .field("port", &"") - .finish() - } -} - -impl

Drop for BoundBrowserSession

{ - fn drop(&mut self) { - if self.session.has_unresolved_remote_ownership() { - let _ = ABANDONED_BOUND_SESSIONS.try_update( - Ordering::Relaxed, - Ordering::Relaxed, - |value| Some(value.saturating_add(1)), - ); - } - } -} - -impl BrowserSession { - /// Start an active Browser Session around an already validated transport session identity. - /// - /// A fresh process-local incarnation is allocated before any browser I/O. Exhaustion fails closed - /// rather than wrapping and making an older authority structurally valid again. - pub fn start(id: BrowserSessionId) -> Result { - Self::start_with_counter(id, &NEXT_BROWSER_SESSION_INCARNATION) - } - - fn start_with_counter( - id: BrowserSessionId, - counter: &AtomicU64, - ) -> Result { - let incarnation = allocate_incarnation(counter)?; - Ok(Self { - id, - incarnation, - state: BrowserSessionState::Active, - transport_lost: false, - next_epoch: 1, - contexts: BTreeMap::new(), - recovery_evidence: Vec::new(), - create_recovery_evidence: Vec::new(), - }) - } - - /// Consume this aggregate and one concrete lifecycle port into a linear bound session. - /// - /// Binding invokes no adapter method. All subsequent create/destroy I/O is reachable only through - /// the owned port inside the returned wrapper. - pub fn bind_lifecycle_port(self, port: P) -> BoundBrowserSession

{ - BoundBrowserSession { - session: self, - port, - } - } - - /// Return this aggregate's browser-session transport identity. - #[must_use] - pub const fn id(&self) -> BrowserSessionId { - self.id - } - - /// Return this aggregate's non-reused process-local incarnation. - #[must_use] - pub const fn incarnation(&self) -> BrowserSessionIncarnation { - self.incarnation - } - - /// Return the current aggregate lifecycle state. - #[must_use] - pub const fn state(&self) -> BrowserSessionState { - self.state - } - - /// Report whether browser transport loss has been observed for this aggregate. - #[must_use] - pub const fn transport_is_lost(&self) -> bool { - self.transport_lost - } - - /// Return immutable recovery evidence retained after uncertain browser lifecycle outcomes. - #[must_use] - pub fn recovery_evidence(&self) -> &[BrowserSessionRecoveryEvidence] { - &self.recovery_evidence - } - - /// Return exact create-attempt recovery facts retained for transaction correlation. - #[must_use] - pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { - &self.create_recovery_evidence - } - - /// Return current presentation authority for an already-owned active context. - pub fn presentation_authority( - &self, - browsing_context: BrowsingContextId, - ) -> Result { - self.require_active()?; - let record = self - .contexts - .get(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - Ok(Self::authority_for( - self.id, - self.incarnation, - &record.handle, - record.epoch, - )) - } - - /// Advance one active owned context to a new authority epoch. - pub fn advance_context_epoch( - &mut self, - browsing_context: BrowsingContextId, - ) -> Result { - self.require_active()?; - let browser_session = self.id; - let incarnation = self.incarnation; - let record = self - .contexts - .get_mut(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - let next = reserve_epoch(&mut self.next_epoch)?; - record.epoch = next; - Ok(Self::authority_for( - browser_session, - incarnation, - &record.handle, - next, - )) - } - - /// Record browser transport loss independently from ownership-recovery state. - /// - /// Returns `true` only for the first observed transport loss. If ownership was already uncertain, - /// `RecoveryRequired` remains the lifecycle state while the transport-loss fact is retained. - pub fn record_transport_loss(&mut self) -> bool { - if self.transport_lost || self.state == BrowserSessionState::Ended { - return false; - } - self.transport_lost = true; - if self.state == BrowserSessionState::Active { - self.recovery_evidence.extend( - self.contexts - .values() - .filter(|record| record.state == OwnedContextState::Active) - .map(|record| { - BrowserSessionRecoveryEvidence::TransportLossOwnedHandle( - record.handle.clone(), - ) - }), - ); - self.state = BrowserSessionState::TransportLost; - self.mark_active_contexts_uncertain(); - } - true - } - - /// End the Browser Session only after every owned context has proven destruction. - pub fn end(&mut self) -> Result<(), BrowserSessionError> { - self.require_active()?; - if self - .contexts - .values() - .any(|record| record.state != OwnedContextState::Destroyed) - { - return Err(BrowserSessionError::ActiveContextRemains); - } - self.state = BrowserSessionState::Ended; - Ok(()) - } - - fn create_disposable_context_with_port( - &mut self, - port: &mut P, - ) -> Result { - self.require_active()?; - let epoch = reserve_epoch(&mut self.next_epoch)?; - let request = DisposableContextCreateRequest { - browser_session: self.id, - incarnation: self.incarnation, - attempt_epoch: epoch, - }; - let handle = match port.create_disposable_context(&request) { - Ok(handle) => handle, - Err(DisposableContextCreateError::CreateFailedClean) => { - return Err(BrowserSessionError::ContextCreationFailed); - } - Err(DisposableContextCreateError::CreateFailedUncertain(isolation)) => { - self.create_recovery_evidence.push( - DisposableContextCreateRecoveryEvidence::FailedUncertain { - attempt_epoch: epoch, - isolation: isolation.clone(), - }, - ); - if let Some(isolation) = isolation { - self.recovery_evidence.push( - BrowserSessionRecoveryEvidence::PartialCreationIsolation(isolation), - ); - } - self.enter_recovery_required(); - return Err(BrowserSessionError::ContextCreationUncertain); - } - }; - - let duplicate_error = if self - .contexts - .values() - .any(|record| record.handle.isolation == handle.isolation) - { - Some(BrowserSessionError::DuplicateDisposableIsolation) - } else if self.contexts.contains_key(&handle.browsing_context) { - Some(BrowserSessionError::DuplicateBrowsingContext) - } else { - None - }; - - if let Some(error) = duplicate_error { - let completion = DisposableContextCreateCompletion { - browser_session: self.id, - incarnation: self.incarnation, - attempt_epoch: epoch, - disposition: DisposableContextCreateDisposition::Rejected, - }; - self.create_recovery_evidence.push( - DisposableContextCreateRecoveryEvidence::DuplicateCandidate { - attempt_epoch: epoch, - context: handle.clone(), - }, - ); - self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::DuplicateAdapterHandle( - handle.clone(), - )); - if port - .complete_disposable_context_creation(&completion) - .is_err() - { - self.create_recovery_evidence.push( - DisposableContextCreateRecoveryEvidence::CompletionUnsettled { - attempt_epoch: epoch, - disposition: DisposableContextCreateDisposition::Rejected, - context: handle.clone(), - }, - ); - self.recovery_evidence.push( - BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(handle), - ); - self.enter_recovery_required(); - return Err(BrowserSessionError::ContextCreationUncertain); - } - self.enter_recovery_required(); - return Err(error); - } - - let completion = DisposableContextCreateCompletion { - browser_session: self.id, - incarnation: self.incarnation, - attempt_epoch: epoch, - disposition: DisposableContextCreateDisposition::Accepted, - }; - if port - .complete_disposable_context_creation(&completion) - .is_err() - { - self.create_recovery_evidence.push( - DisposableContextCreateRecoveryEvidence::CompletionUnsettled { - attempt_epoch: epoch, - disposition: DisposableContextCreateDisposition::Accepted, - context: handle.clone(), - }, - ); - self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( - handle, - )); - self.enter_recovery_required(); - return Err(BrowserSessionError::ContextCreationUncertain); - } - - let browsing_context = handle.browsing_context; - let authority = Self::authority_for(self.id, self.incarnation, &handle, epoch); - self.contexts.insert( - browsing_context, - OwnedContextRecord { - handle, - epoch, - state: OwnedContextState::Active, - }, - ); - Ok(authority) - } - - fn destroy_disposable_context_with_port( - &mut self, - authority: &PresentationMutationAuthority, - port: &mut P, - ) -> Result<(), BrowserSessionError> { - let browser_session = self.id; - let incarnation = self.incarnation; - let record = self.context_for_authority_mut(authority)?; - let context_epoch = record.epoch; - let request = DisposableContextDestroyRequest { - browser_session, - incarnation, - context: record.handle.clone(), - context_epoch, - }; - match port.destroy_disposable_context(&request) { - Ok(()) => { - record.state = OwnedContextState::Destroyed; - Ok(()) - } - Err(DisposableContextDestroyError::DestroyFailed) => { - record.state = OwnedContextState::Uncertain; - self.recovery_evidence - .push(BrowserSessionRecoveryEvidence::UnprovenDestruction { - context: request.context, - context_epoch, - }); - self.enter_recovery_required(); - Err(BrowserSessionError::ContextDestructionFailed) - } - } - } - - fn require_active(&self) -> Result<(), BrowserSessionError> { - if self.state == BrowserSessionState::Active { - Ok(()) - } else { - Err(BrowserSessionError::SessionNotActive) - } - } - - fn authority_for( - browser_session: BrowserSessionId, - incarnation: BrowserSessionIncarnation, - handle: &DisposableContextHandle, - context_epoch: BrowserContextEpoch, - ) -> PresentationMutationAuthority { - PresentationMutationAuthority { - browser_session, - incarnation, - isolation: handle.isolation.clone(), - browsing_context: handle.browsing_context, - context_epoch, - } - } - - fn context_for_authority_mut( - &mut self, - authority: &PresentationMutationAuthority, - ) -> Result<&mut OwnedContextRecord, BrowserSessionError> { - self.require_active()?; - if authority.browser_session != self.id || authority.incarnation != self.incarnation { - return Err(BrowserSessionError::AuthorityMismatch); - } - let record = self - .contexts - .get_mut(&authority.browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - if record.epoch != authority.context_epoch || record.handle.isolation != authority.isolation - { - return Err(BrowserSessionError::AuthorityMismatch); - } - Ok(record) - } - - fn enter_recovery_required(&mut self) { - let sibling_handles = self - .contexts - .values() - .filter(|record| record.state == OwnedContextState::Active) - .map(|record| record.handle.clone()) - .filter(|handle| { - !self.recovery_evidence.iter().any(|evidence| match evidence { - BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) - | BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(_) - | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(_) => false, - BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(existing) - | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(existing) => { - existing == handle - } - BrowserSessionRecoveryEvidence::UnprovenDestruction { - context: existing, - .. - } => existing == handle, - }) - }) - .collect::>(); - self.recovery_evidence.extend( - sibling_handles - .into_iter() - .map(BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle), - ); - self.state = BrowserSessionState::RecoveryRequired; - self.mark_active_contexts_uncertain(); - } - - fn mark_active_contexts_uncertain(&mut self) { - for record in self.contexts.values_mut() { - if record.state == OwnedContextState::Active { - record.state = OwnedContextState::Uncertain; - } - } - } - - fn has_unresolved_remote_ownership(&self) -> bool { - matches!(self.state, BrowserSessionState::RecoveryRequired) - || self.contexts.values().any(|record| { - matches!( - record.state, - OwnedContextState::Active | OwnedContextState::Uncertain - ) - }) - } -} - -impl BoundBrowserSession

{ - /// Return the bound Browser Session for read-only policy and ACL validation. - #[must_use] - pub const fn browser_session(&self) -> &BrowserSession { - &self.session - } - - /// Create one disposable context through the exact port consumed when this session was bound. - pub fn create_disposable_context( - &mut self, - ) -> Result { - self.session - .create_disposable_context_with_port(&mut self.port) - } - - /// Return current presentation authority for an already-owned active context. - pub fn presentation_authority( - &self, - browsing_context: BrowsingContextId, - ) -> Result { - self.session.presentation_authority(browsing_context) - } - - /// Advance one active owned context to a new authority epoch. - pub fn advance_context_epoch( - &mut self, - browsing_context: BrowsingContextId, - ) -> Result { - self.session.advance_context_epoch(browsing_context) - } - - /// Destroy the exact owned disposable boundary through the bound lifecycle port. - pub fn destroy_disposable_context( - &mut self, - authority: &PresentationMutationAuthority, - ) -> Result<(), BrowserSessionError> { - self.session - .destroy_disposable_context_with_port(authority, &mut self.port) - } - - /// Record browser transport loss without exposing mutable lifecycle-port access. - pub fn record_transport_loss(&mut self) -> bool { - self.session.record_transport_loss() - } - - /// End the Browser Session only after every owned context has proven destruction. - pub fn end(&mut self) -> Result<(), BrowserSessionError> { - self.session.end() - } - - /// Verify normal completion without relinquishing the exact bound lifecycle owner on failure. - /// - /// A rejected finish leaves the wrapper intact so the caller can destroy or reconcile outstanding - /// contexts and retry. After success the aggregate is `Ended`; dropping the wrapper is then inert. - pub fn finish(&mut self) -> Result<(), BrowserSessionError> { - self.session.end() - } -} - -impl BoundBrowserSession

{ - /// Execute one adapter-defined operation through the exact consumed adapter after authority validation. - /// - /// Browser Session validates session incarnation, isolation identity, browsing-context identity, - /// and epoch before the adapter receives the operation. Stale or foreign authority therefore fails - /// before adapter I/O, while the adapter-specific operation vocabulary remains outside this domain. - pub fn execute_authorized_context_operation( - &mut self, - authority: &PresentationMutationAuthority, - operation: P::Operation, - ) -> Result> { - let browser_session = self.session.id; - let incarnation = self.session.incarnation; - let record = self - .session - .context_for_authority_mut(authority) - .map_err(AuthorizedContextOperationError::BrowserSession)?; - let context = record.handle.clone(); - let context_epoch = record.epoch; - let request = AuthorizedContextOperationRequest { - browser_session, - incarnation, - context, - context_epoch, - operation, - }; - self.port - .execute_authorized_context_operation(&request) - .map_err(AuthorizedContextOperationError::Adapter) - } -} - -fn reserve_epoch(next_epoch: &mut u64) -> Result { - let epoch = BrowserContextEpoch(*next_epoch); - *next_epoch = next_epoch - .checked_add(1) - .ok_or(BrowserSessionError::EpochExhausted)?; - Ok(epoch) -} - -fn allocate_incarnation( - counter: &AtomicU64, -) -> Result { - let value = counter - .try_update(Ordering::SeqCst, Ordering::SeqCst, |current| { - current.checked_add(1) - }) - .map_err(|_| BrowserSessionError::IncarnationExhausted)?; - Ok(BrowserSessionIncarnation(value)) -} - -#[cfg(test)] -#[allow(clippy::expect_used)] -mod tests { - use super::*; - use std::collections::VecDeque; - - #[derive(Debug)] - struct TestPort { - handles: VecDeque, - create_error: Option, - fail_destroy: bool, - fail_completion: bool, - create_calls: usize, - destroy_calls: usize, - create_sessions: Vec, - create_incarnations: Vec, - create_attempts: Vec, - create_completions: Vec<( - BrowserSessionId, - BrowserSessionIncarnation, - BrowserContextEpoch, - DisposableContextCreateDisposition, - )>, - destroy_sessions: Vec, - destroy_incarnations: Vec, - destroyed_isolations: Vec, - } - - impl TestPort { - fn new(context: u64, isolation: &str) -> Self { - Self::with_handles(vec![DisposableContextHandle::new( - isolation_id(isolation), - context_id(context), - )]) - } - - fn with_handles(handles: Vec) -> Self { - Self { - handles: handles.into(), - create_error: None, - fail_destroy: false, - fail_completion: false, - create_calls: 0, - destroy_calls: 0, - create_sessions: Vec::new(), - create_incarnations: Vec::new(), - create_attempts: Vec::new(), - create_completions: Vec::new(), - destroy_sessions: Vec::new(), - destroy_incarnations: Vec::new(), - destroyed_isolations: Vec::new(), - } - } - } - - impl DisposableContextPort for TestPort { - fn create_disposable_context( - &mut self, - request: &DisposableContextCreateRequest, - ) -> Result { - self.create_calls += 1; - self.create_sessions.push(request.browser_session()); - self.create_incarnations.push(request.incarnation()); - self.create_attempts.push(request.attempt_epoch()); - match self.create_error.clone() { - Some(error) => Err(error), - None => Ok(self - .handles - .pop_front() - .expect("test must provide one handle per successful creation")), - } - } - - fn complete_disposable_context_creation( - &mut self, - completion: &DisposableContextCreateCompletion, - ) -> Result<(), DisposableContextCreateCompletionError> { - self.create_completions.push(( - completion.browser_session(), - completion.incarnation(), - completion.attempt_epoch(), - completion.disposition(), - )); - if self.fail_completion { - Err(DisposableContextCreateCompletionError::CompletionFailed) - } else { - Ok(()) - } - } - - fn destroy_disposable_context( - &mut self, - request: &DisposableContextDestroyRequest, - ) -> Result<(), DisposableContextDestroyError> { - self.destroy_calls += 1; - self.destroy_sessions.push(request.browser_session()); - self.destroy_incarnations.push(request.incarnation()); - self.destroyed_isolations - .push(request.context().isolation.clone()); - if self.fail_destroy { - Err(DisposableContextDestroyError::DestroyFailed) - } else { - Ok(()) - } - } - } - - fn session_id(value: u64) -> BrowserSessionId { - BrowserSessionId::new(value).expect("valid session id") - } - - fn context_id(value: u64) -> BrowsingContextId { - BrowsingContextId::new(value).expect("valid context id") - } - - fn isolation_id(value: &str) -> DisposableIsolationId { - DisposableIsolationId::parse(value).expect("valid isolation id") - } - - fn session(value: u64) -> BrowserSession { - BrowserSession::start(session_id(value)).expect("incarnation capacity") - } - - #[test] - fn isolation_identity_validation_preserves_protocol_text() { - assert_eq!( - DisposableIsolationId::parse(""), - Err(DisposableIsolationIdError::Empty) - ); - let long = "x".repeat(4097); - let long_identity = DisposableIsolationId::parse(&long) - .expect("WebDriver BiDi browser.UserContext does not define a 4096-byte limit"); - assert_eq!(long_identity.as_str(), long); - assert_eq!( - DisposableIsolationId::parse(" user-context "), - Err(DisposableIsolationIdError::InvalidCharacter) - ); - assert_eq!( - DisposableIsolationId::parse("user\ncontext"), - Err(DisposableIsolationIdError::InvalidCharacter) - ); - let valid = isolation_id("webdriver-user-context-10"); - assert_eq!(valid.as_str(), "webdriver-user-context-10"); - let handle = DisposableContextHandle::new(valid.clone(), context_id(10)); - assert_eq!(handle.isolation(), &valid); - assert_eq!(handle.browsing_context(), context_id(10)); - } - - #[test] - fn bound_creation_is_the_only_raw_context_entry_to_authority() { - let raw_session = session(1); - assert_eq!(raw_session.id(), session_id(1)); - assert_ne!(raw_session.incarnation().value(), 0); - assert!(!raw_session.transport_is_lost()); - assert!(raw_session.recovery_evidence().is_empty()); - assert_eq!( - raw_session.presentation_authority(context_id(10)), - Err(BrowserSessionError::ContextNotOwned) - ); - let mut bound = raw_session.bind_lifecycle_port(TestPort::new(10, "isolation-10")); - let authority = bound - .create_disposable_context() - .expect("owned disposable context"); - assert_eq!(bound.port.create_sessions, vec![session_id(1)]); - assert_eq!( - bound.port.create_incarnations, - vec![bound.browser_session().incarnation()] - ); - assert_eq!(bound.port.create_attempts, vec![BrowserContextEpoch(1)]); - assert_eq!( - bound.port.create_completions, - vec![( - session_id(1), - bound.browser_session().incarnation(), - BrowserContextEpoch(1), - DisposableContextCreateDisposition::Accepted, - )] - ); - assert_eq!(authority.browser_session(), session_id(1)); - assert_eq!( - authority.incarnation(), - bound.browser_session().incarnation() - ); - assert_eq!(authority.isolation().as_str(), "isolation-10"); - assert_eq!(authority.browsing_context(), context_id(10)); - assert_eq!(authority.context_epoch().value(), 1); - assert_eq!(bound.presentation_authority(context_id(10)), Ok(authority)); - } - - #[test] - fn creation_failure_preserves_known_recovery_identity() { - let mut clean_port = TestPort::new(20, "isolation-20"); - clean_port.create_error = Some(DisposableContextCreateError::CreateFailedClean); - let mut clean = session(2).bind_lifecycle_port(clean_port); - assert_eq!( - clean.create_disposable_context(), - Err(BrowserSessionError::ContextCreationFailed) - ); - assert_eq!(clean.browser_session().state(), BrowserSessionState::Active); - clean.end().expect("clean failure can end"); - - let mut unknown_port = TestPort::new(210, "isolation-210"); - unknown_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(None)); - let mut unknown = session(21).bind_lifecycle_port(unknown_port); - assert_eq!( - unknown.create_disposable_context(), - Err(BrowserSessionError::ContextCreationUncertain) - ); - assert!(unknown.browser_session().recovery_evidence().is_empty()); - assert_eq!(unknown.browser_session().create_attempt_recovery_evidence().len(), 1); - match &unknown.browser_session().create_attempt_recovery_evidence()[0] { - DisposableContextCreateRecoveryEvidence::FailedUncertain { - attempt_epoch, - isolation, - } => { - assert_eq!(attempt_epoch.value(), 1); - assert_eq!(isolation, &None); - } - other => panic!("unexpected recovery evidence: {other:?}"), - } - - let known = isolation_id("partial-user-context-211"); - let mut known_port = TestPort::new(211, "unused"); - known_port.create_error = Some(DisposableContextCreateError::CreateFailedUncertain(Some( - known.clone(), - ))); - let mut known_session = session(22).bind_lifecycle_port(known_port); - assert_eq!( - known_session.create_disposable_context(), - Err(BrowserSessionError::ContextCreationUncertain) - ); - assert_eq!( - known_session.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::PartialCreationIsolation( - known.clone() - )] - ); - assert_eq!(known_session.browser_session().create_attempt_recovery_evidence().len(), 1); - match &known_session.browser_session().create_attempt_recovery_evidence()[0] { - DisposableContextCreateRecoveryEvidence::FailedUncertain { - attempt_epoch, - isolation, - } => { - assert_eq!(attempt_epoch.value(), 1); - assert_eq!(isolation.as_ref(), Some(&known)); - } - other => panic!("unexpected recovery evidence: {other:?}"), - } - assert_eq!( - known_session.end(), - Err(BrowserSessionError::SessionNotActive) - ); - } - - #[test] - fn duplicate_adapter_output_preserves_offending_handle() { - let first_context_handle = - DisposableContextHandle::new(isolation_id("isolation-30-a"), context_id(30)); - let duplicate_context_handle = - DisposableContextHandle::new(isolation_id("isolation-30-b"), context_id(30)); - let context_port = TestPort::with_handles(vec![ - first_context_handle.clone(), - duplicate_context_handle.clone(), - ]); - let mut duplicate_context = session(3).bind_lifecycle_port(context_port); - duplicate_context - .create_disposable_context() - .expect("first owned context"); - assert_eq!( - duplicate_context.create_disposable_context(), - Err(BrowserSessionError::DuplicateBrowsingContext) - ); - assert_eq!( - duplicate_context.browser_session().recovery_evidence(), - &[ - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_context_handle), - BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_context_handle), - ] - ); - - let first_isolation_handle = - DisposableContextHandle::new(isolation_id("isolation-31"), context_id(310)); - let duplicate_isolation_handle = - DisposableContextHandle::new(isolation_id("isolation-31"), context_id(311)); - let isolation_port = TestPort::with_handles(vec![ - first_isolation_handle.clone(), - duplicate_isolation_handle.clone(), - ]); - let mut duplicate_isolation = session(31).bind_lifecycle_port(isolation_port); - duplicate_isolation - .create_disposable_context() - .expect("first owned isolation"); - assert_eq!( - duplicate_isolation.create_disposable_context(), - Err(BrowserSessionError::DuplicateDisposableIsolation) - ); - assert_eq!( - duplicate_isolation.browser_session().recovery_evidence(), - &[ - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate_isolation_handle), - BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first_isolation_handle), - ] - ); - } - - #[test] - fn create_completion_failure_preserves_non_authorizing_recovery_evidence() { - let expected = DisposableContextHandle::new(isolation_id("isolation-315"), context_id(315)); - let mut port = TestPort::with_handles(vec![expected.clone()]); - port.fail_completion = true; - let mut bound = session(315).bind_lifecycle_port(port); - - assert_eq!( - bound.create_disposable_context(), - Err(BrowserSessionError::ContextCreationUncertain) - ); - assert_eq!( - bound.browser_session().state(), - BrowserSessionState::RecoveryRequired - ); - assert_eq!( - bound.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::UnsettledAdapterHandle( - expected.clone() - )] - ); - assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 1); - match &bound.browser_session().create_attempt_recovery_evidence()[0] { - DisposableContextCreateRecoveryEvidence::CompletionUnsettled { - attempt_epoch, - disposition, - context, - } => { - assert_eq!(attempt_epoch.value(), 1); - assert_eq!(*disposition, DisposableContextCreateDisposition::Accepted); - assert_eq!(context, &expected); - } - other => panic!("unexpected recovery evidence: {other:?}"), - } - assert_eq!( - bound.port.create_completions[0].3, - DisposableContextCreateDisposition::Accepted - ); - assert_eq!( - bound.presentation_authority(context_id(315)), - Err(BrowserSessionError::SessionNotActive) - ); - } - - #[test] - fn rejected_create_completion_failure_preserves_duplicate_and_unsettled_evidence() { - let first = DisposableContextHandle::new(isolation_id("isolation-316-a"), context_id(316)); - let duplicate = - DisposableContextHandle::new(isolation_id("isolation-316-b"), context_id(316)); - let port = TestPort::with_handles(vec![first.clone(), duplicate.clone()]); - let mut bound = session(316).bind_lifecycle_port(port); - - bound - .create_disposable_context() - .expect("first candidate accepted"); - bound.port.fail_completion = true; - assert_eq!( - bound.create_disposable_context(), - Err(BrowserSessionError::ContextCreationUncertain) - ); - assert_eq!( - bound.browser_session().recovery_evidence(), - &[ - BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(duplicate.clone()), - BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(duplicate.clone()), - BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(first), - ] - ); - assert_eq!(bound.browser_session().create_attempt_recovery_evidence().len(), 2); - match &bound.browser_session().create_attempt_recovery_evidence()[0] { - DisposableContextCreateRecoveryEvidence::DuplicateCandidate { - attempt_epoch, - context, - } => { - assert_eq!(attempt_epoch.value(), 2); - assert_eq!(context, &duplicate); - } - other => panic!("unexpected recovery evidence: {other:?}"), - } - match &bound.browser_session().create_attempt_recovery_evidence()[1] { - DisposableContextCreateRecoveryEvidence::CompletionUnsettled { - attempt_epoch, - disposition, - context, - } => { - assert_eq!(attempt_epoch.value(), 2); - assert_eq!(*disposition, DisposableContextCreateDisposition::Rejected); - assert_eq!(context, &duplicate); - } - other => panic!("unexpected recovery evidence: {other:?}"), - } - assert_eq!( - bound.port.create_completions[1].3, - DisposableContextCreateDisposition::Rejected - ); - } - - #[test] - fn bound_port_is_structural_and_not_swappable() { - let approved = TestPort::new(320, "isolation-320"); - let other = TestPort::new(321, "isolation-321"); - let mut bound = session(32).bind_lifecycle_port(approved); - let authority = bound - .create_disposable_context() - .expect("owned context uses consumed port"); - assert_eq!(other.create_calls, 0); - assert_eq!(other.destroy_calls, 0); - bound - .destroy_disposable_context(&authority) - .expect("same structurally bound port destroys context"); - assert_eq!(bound.port.create_calls, 1); - assert_eq!(bound.port.destroy_calls, 1); - } - - #[test] - fn epoch_exhaustion_prevents_creation_io() { - let mut bound = session(4).bind_lifecycle_port(TestPort::new(40, "isolation-40")); - bound.session.next_epoch = u64::MAX; - assert_eq!( - bound.create_disposable_context(), - Err(BrowserSessionError::EpochExhausted) - ); - assert_eq!(bound.port.create_calls, 0); - } - - #[test] - fn epoch_exhaustion_prevents_advance_mutation() { - let mut bound = session(41).bind_lifecycle_port(TestPort::new(410, "isolation-410")); - let authority = bound.create_disposable_context().expect("owned context"); - bound.session.next_epoch = u64::MAX; - assert_eq!( - bound.advance_context_epoch(context_id(410)), - Err(BrowserSessionError::EpochExhausted) - ); - assert_eq!(bound.presentation_authority(context_id(410)), Ok(authority)); - } - - #[test] - fn epoch_advance_invalidates_old_and_unknown_authority() { - let mut bound = session(5).bind_lifecycle_port(TestPort::new(50, "isolation-50")); - let old = bound.create_disposable_context().expect("owned context"); - assert_eq!( - bound.advance_context_epoch(context_id(51)), - Err(BrowserSessionError::ContextNotOwned) - ); - let new = bound - .advance_context_epoch(context_id(50)) - .expect("advanced epoch"); - assert_eq!(new.context_epoch().value(), 2); - assert_eq!( - bound.destroy_disposable_context(&old), - Err(BrowserSessionError::AuthorityMismatch) - ); - bound - .destroy_disposable_context(&new) - .expect("destroy current epoch"); - assert_eq!( - bound.port.destroy_incarnations, - vec![bound.browser_session().incarnation()] - ); - assert_eq!( - bound.presentation_authority(context_id(50)), - Err(BrowserSessionError::ContextNotOwned) - ); - assert_eq!( - bound.destroy_disposable_context(&new), - Err(BrowserSessionError::ContextNotOwned) - ); - } - - #[test] - fn cross_session_and_foreign_isolation_authority_fail_before_io() { - let mut owner = session(6).bind_lifecycle_port(TestPort::new(60, "isolation-60")); - let authority = owner.create_disposable_context().expect("owner context"); - - let mut foreign = session(7).bind_lifecycle_port(TestPort::new(60, "isolation-60")); - foreign - .create_disposable_context() - .expect("foreign context"); - assert_eq!( - foreign.destroy_disposable_context(&authority), - Err(BrowserSessionError::AuthorityMismatch) - ); - assert_eq!(foreign.port.destroy_calls, 0); - - let forged = PresentationMutationAuthority { - browser_session: owner.browser_session().id(), - incarnation: owner.browser_session().incarnation(), - isolation: isolation_id("foreign-isolation"), - browsing_context: authority.browsing_context(), - context_epoch: authority.context_epoch(), - }; - assert_eq!( - owner.destroy_disposable_context(&forged), - Err(BrowserSessionError::AuthorityMismatch) - ); - assert_eq!(owner.port.destroy_calls, 0); - } - - #[test] - fn sequential_incarnation_reuse_rejects_stale_authority() { - let shared_id = session_id(8); - let mut session_a = BrowserSession::start(shared_id) - .expect("A incarnation") - .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); - let authority_a = session_a.create_disposable_context().expect("A context"); - session_a - .destroy_disposable_context(&authority_a) - .expect("A destroy"); - session_a.end().expect("A end"); - - let mut session_b = BrowserSession::start(shared_id) - .expect("B incarnation") - .bind_lifecycle_port(TestPort::new(80, "reused-user-context")); - let authority_b = session_b.create_disposable_context().expect("B context"); - assert_ne!( - session_a.browser_session().incarnation(), - session_b.browser_session().incarnation() - ); - assert_eq!( - session_b.destroy_disposable_context(&authority_a), - Err(BrowserSessionError::AuthorityMismatch) - ); - assert_eq!(session_b.port.destroy_calls, 0); - session_b - .destroy_disposable_context(&authority_b) - .expect("B destroy"); - assert_eq!(session_b.port.destroy_calls, 1); - } - - #[test] - fn destroy_failure_retains_handle_and_transport_loss_orthogonally() { - let expected_handle = - DisposableContextHandle::new(isolation_id("isolation-90"), context_id(90)); - let mut port = TestPort::new(90, "isolation-90"); - port.fail_destroy = true; - let mut bound = session(9).bind_lifecycle_port(port); - let authority = bound.create_disposable_context().expect("owned context"); - let expected_epoch = authority.context_epoch(); - assert_eq!( - bound.destroy_disposable_context(&authority), - Err(BrowserSessionError::ContextDestructionFailed) - ); - assert_eq!( - bound.browser_session().state(), - BrowserSessionState::RecoveryRequired - ); - assert_eq!( - bound.browser_session().recovery_evidence(), - &[BrowserSessionRecoveryEvidence::UnprovenDestruction { - context: expected_handle, - context_epoch: expected_epoch, - }] - ); - assert!(!bound.browser_session().transport_is_lost()); - assert!(bound.record_transport_loss()); - assert!(bound.browser_session().transport_is_lost()); - assert_eq!( - bound.browser_session().state(), - BrowserSessionState::RecoveryRequired - ); - assert!(!bound.record_transport_loss()); - assert_eq!( - bound.create_disposable_context(), - Err(BrowserSessionError::SessionNotActive) - ); - assert_eq!( - bound.presentation_authority(context_id(90)), - Err(BrowserSessionError::SessionNotActive) - ); - assert_eq!( - bound.advance_context_epoch(context_id(90)), - Err(BrowserSessionError::SessionNotActive) - ); - assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); - } - - #[test] - fn transport_loss_invalidates_active_contexts_and_is_idempotent() { - let mut bound = session(10).bind_lifecycle_port(TestPort::new(100, "isolation-100")); - let authority = bound.create_disposable_context().expect("owned context"); - assert!(bound.record_transport_loss()); - assert_eq!( - bound.browser_session().state(), - BrowserSessionState::TransportLost - ); - assert!(bound.browser_session().transport_is_lost()); - assert!(!bound.record_transport_loss()); - assert_eq!( - bound.destroy_disposable_context(&authority), - Err(BrowserSessionError::SessionNotActive) - ); - assert_eq!(bound.port.destroy_calls, 0); - } - - #[test] - fn normal_end_requires_proven_destruction_and_ignores_late_transport_report() { - let mut bound = session(11).bind_lifecycle_port(TestPort::new(110, "isolation-110")); - let authority = bound.create_disposable_context().expect("owned context"); - assert_eq!(bound.end(), Err(BrowserSessionError::ActiveContextRemains)); - bound - .destroy_disposable_context(&authority) - .expect("proven destruction"); - assert_eq!(bound.port.destroy_sessions, vec![session_id(11)]); - assert_eq!( - bound.port.destroyed_isolations, - vec![isolation_id("isolation-110")] - ); - bound.end().expect("normal end"); - assert_eq!(bound.browser_session().state(), BrowserSessionState::Ended); - assert!(!bound.record_transport_loss()); - assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); - } - - #[test] - fn incarnation_allocator_fails_closed_before_wrap() { - let counter = AtomicU64::new(u64::MAX); - let error = BrowserSession::start_with_counter(session_id(12), &counter) - .expect_err("incarnation allocation must fail closed before wrapping"); - assert_eq!(error, BrowserSessionError::IncarnationExhausted); - } -} +pub use browser_session::*; +pub use recovery::BoundBrowserSessionRecovery; diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs new file mode 100644 index 000000000..46fd6eab2 --- /dev/null +++ b/crates/originweave-browser-session/src/recovery.rs @@ -0,0 +1,45 @@ +use crate::browser_session::{ + BoundBrowserSession, BrowserSession, BrowserSessionState, DisposableContextPort, +}; + +/// Recovery-only custody of a Browser Session and its exact consumed lifecycle adapter. +/// +/// This wrapper is obtained only by consuming a bound session that has already entered +/// [`BrowserSessionState::RecoveryRequired`] or [`BrowserSessionState::TransportLost`]. It exposes +/// immutable Browser Session evidence but deliberately provides none of the ordinary create, +/// presentation-authority, epoch-advance, destroy, or authorized-operation methods. The concrete +/// adapter remains private and is moved, not reconstructed or replaced. +/// +/// Dropping this wrapper performs no browser I/O. The contained [`BoundBrowserSession`] retains its +/// existing abandonment accounting when unresolved remote ownership is finally dropped. +#[must_use = "persist or reconcile unresolved Browser Session ownership before dropping recovery custody"] +pub struct BoundBrowserSessionRecovery

{ + bound: BoundBrowserSession

, +} + +impl BoundBrowserSession

{ + /// Consume an unresolved bound session into recovery-only custody without adapter I/O. + /// + /// The handoff succeeds only after Browser Session has entered `RecoveryRequired` or + /// `TransportLost`. Active or normally ended sessions are returned unchanged so callers cannot + /// use the recovery type as an alternate path around ordinary lifecycle policy. + pub fn into_recovery(self) -> Result, Self> { + match self.browser_session().state() { + BrowserSessionState::RecoveryRequired | BrowserSessionState::TransportLost => { + Ok(BoundBrowserSessionRecovery { bound: self }) + } + BrowserSessionState::Active | BrowserSessionState::Ended => Err(self), + } + } +} + +impl BoundBrowserSessionRecovery

{ + /// Return immutable Browser Session recovery state and evidence. + /// + /// No adapter reference is exposed. Protocol-specific recovery code must consume a separately + /// reviewed recovery operation boundary rather than regaining ordinary mutation authority. + #[must_use] + pub const fn browser_session(&self) -> &BrowserSession { + self.bound.browser_session() + } +} From c70b8b2f052effd07ed2b9800de934c2c9b36387 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:06:44 +0900 Subject: [PATCH 070/632] test(browser-session): trace recovery custody module --- ...test_browser_session_lifecycle_contract.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index a37ea034a..73736dac4 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -30,10 +30,18 @@ def test_browser_session_is_an_independent_workspace_boundary(self) -> None: def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: """Raw driver identifiers must never become caller-mintable authority tokens.""" - source = (CRATE / "src/lib.rs").read_text(encoding="utf-8") + source = "\n".join( + (CRATE / relative_path).read_text(encoding="utf-8") + for relative_path in ( + "src/lib.rs", + "src/browser_session.rs", + "src/recovery.rs", + ) + ) required_symbols = ( "pub struct BrowserSession", "pub struct BoundBrowserSession", + "pub struct BoundBrowserSessionRecovery", "pub trait DisposableContextPort", "pub struct DisposableIsolationId", "pub struct DisposableContextHandle", @@ -52,12 +60,14 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: "pub enum DisposableContextDestroyError", "pub fn abandoned_bound_session_count", "pub fn finish", + "pub fn into_recovery", "pub fn execute_authorized_context_operation", ) for symbol in required_symbols: self.assertIn(symbol, source) self.assertIn("BrowserSessionState::RecoveryRequired", source) + self.assertIn("BrowserSessionState::TransportLost", source) self.assertNotIn("pub enum DisposableContextPortError", source) self.assertNotIn("DisposableContextPortId", source) self.assertNotIn("fn port_id(&self)", source) @@ -92,6 +102,8 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertIn("sequential_incarnation_reuse_rejects_stale_authority", source) self.assertIn("pub fn finish(&mut self)", source) self.assertNotIn("pub fn finish(mut self)", source) + self.assertNotIn("pub const fn port", source) + self.assertNotIn("pub fn port", source) authority_impl = source.split("impl PresentationMutationAuthority", 1)[1].split( "enum OwnedContextState", 1 @@ -147,6 +159,9 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - recovery_hostile = (CRATE / "tests/recovery_required_sibling_evidence.rs").read_text( encoding="utf-8" ) + recovery_handoff = (CRATE / "tests/recovery_owner_handoff.rs").read_text( + encoding="utf-8" + ) operation_hostile = (CRATE / "tests/authorized_context_operation.rs").read_text( encoding="utf-8" ) @@ -191,6 +206,19 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - self.assertIn("BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle", recovery_hostile) self.assertIn("indirectly invalidated sibling", recovery_hostile) + self.assertIn( + "unproven_destroy_hands_exact_bound_adapter_and_evidence_to_recovery_owner", + recovery_handoff, + ) + self.assertIn( + "transport_loss_hands_exact_bound_adapter_and_evidence_to_recovery_owner", + recovery_handoff, + ) + self.assertIn(".into_recovery()", recovery_handoff) + self.assertIn("handoff must move, not replace, the bound adapter", recovery_handoff) + self.assertIn("handoff must not imply cleanup I/O", recovery_handoff) + self.assertIn("transport loss is not destruction proof", recovery_handoff) + self.assertIn("authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io", operation_hostile) self.assertIn("AuthorizedContextOperationError::BrowserSession", operation_hostile) self.assertIn("AuthorizedContextOperationError::Adapter", operation_hostile) From 454d412be11ec979cd666b25632f2df7e3794cb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:08:17 +0900 Subject: [PATCH 071/632] test(browser-session): compile-fail normal recovery mutation --- .../src/recovery.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index 46fd6eab2..0eba53ee9 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -10,6 +10,64 @@ use crate::browser_session::{ /// presentation-authority, epoch-advance, destroy, or authorized-operation methods. The concrete /// adapter remains private and is moved, not reconstructed or replaced. /// +/// Ordinary context creation is not available from recovery custody: +/// +/// ```compile_fail +/// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; +/// fn forbidden(mut recovery: BoundBrowserSessionRecovery

) { +/// let _ = recovery.create_disposable_context(); +/// } +/// ``` +/// +/// Presentation-authority lookup is not available from recovery custody: +/// +/// ```compile_fail +/// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; +/// use originweave_core::BrowsingContextId; +/// fn forbidden( +/// recovery: BoundBrowserSessionRecovery

, +/// context: BrowsingContextId, +/// ) { +/// let _ = recovery.presentation_authority(context); +/// } +/// ``` +/// +/// Context-epoch advancement is not available from recovery custody: +/// +/// ```compile_fail +/// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; +/// use originweave_core::BrowsingContextId; +/// fn forbidden( +/// mut recovery: BoundBrowserSessionRecovery

, +/// context: BrowsingContextId, +/// ) { +/// let _ = recovery.advance_context_epoch(context); +/// } +/// ``` +/// +/// Ordinary destruction is not available from recovery custody: +/// +/// ```compile_fail +/// use originweave_browser_session::{ +/// BoundBrowserSessionRecovery, DisposableContextPort, PresentationMutationAuthority, +/// }; +/// fn forbidden( +/// mut recovery: BoundBrowserSessionRecovery

, +/// authority: PresentationMutationAuthority, +/// ) { +/// let _ = recovery.destroy_disposable_context(&authority); +/// } +/// ``` +/// +/// Normal session completion is not available from recovery custody: +/// +/// ```compile_fail +/// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; +/// fn forbidden(mut recovery: BoundBrowserSessionRecovery

) { +/// let _ = recovery.finish(); +/// } +/// ``` +/// /// Dropping this wrapper performs no browser I/O. The contained [`BoundBrowserSession`] retains its /// existing abandonment accounting when unresolved remote ownership is finally dropped. #[must_use = "persist or reconcile unresolved Browser Session ownership before dropping recovery custody"] From 90423ac55ee5b53f2b2adb0d4b00a16bff9edda5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:10:30 +0900 Subject: [PATCH 072/632] test(browser-session): require bounded hot ownership after destroy --- .../proven_destroy_releases_hot_ownership.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs diff --git a/crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs b/crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs new file mode 100644 index 000000000..dd3dddc21 --- /dev/null +++ b/crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs @@ -0,0 +1,118 @@ +use std::cell::Cell; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct ReusedHandlePort { + handle: DisposableContextHandle, + create_calls: Rc>, + destroy_calls: Rc>, +} + +impl DisposableContextPort for ReusedHandlePort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_calls.set(self.create_calls.get() + 1); + Ok(self.handle.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + self.destroy_calls.set(self.destroy_calls.get() + 1); + Ok(()) + } +} + +#[test] +fn proven_destroy_releases_hot_ownership_without_resurrecting_stale_authority( +) -> Result<(), &'static str> { + let browsing_context = BrowsingContextId::new(8_100) + .map_err(|_| "static browsing context id must be valid")?; + let isolation = DisposableIsolationId::parse("bounded-hot-ownership") + .map_err(|_| "static isolation id must be valid")?; + let handle = DisposableContextHandle::new(isolation, browsing_context); + let create_calls = Rc::new(Cell::new(0)); + let destroy_calls = Rc::new(Cell::new(0)); + let port = ReusedHandlePort { + handle, + create_calls: Rc::clone(&create_calls), + destroy_calls: Rc::clone(&destroy_calls), + }; + let session = BrowserSession::start( + BrowserSessionId::new(810).map_err(|_| "static browser session id must be valid")?, + ) + .map_err(|_| "browser session incarnation must be available")?; + let mut bound = session.bind_lifecycle_port(port); + + let first = bound + .create_disposable_context() + .map_err(|_| "first ownership generation must be accepted")?; + assert_eq!(first.context_epoch().value(), 1); + bound + .destroy_disposable_context(&first) + .map_err(|_| "first ownership generation must be proven destroyed")?; + assert_eq!(create_calls.get(), 1); + assert_eq!(destroy_calls.get(), 1); + assert_eq!( + bound.destroy_disposable_context(&first), + Err(BrowserSessionError::ContextNotOwned), + "a proven-destroyed authority must fail before another destroy call" + ); + assert_eq!(destroy_calls.get(), 1); + + let second = bound + .create_disposable_context() + .map_err(|_| "proven destruction must release the reusable remote identity from hot ownership")?; + assert_eq!(second.browsing_context(), first.browsing_context()); + assert_eq!(second.isolation(), first.isolation()); + assert_eq!(second.context_epoch().value(), first.context_epoch().value() + 1); + assert_eq!( + bound.destroy_disposable_context(&first), + Err(BrowserSessionError::AuthorityMismatch), + "same-valued remote identity reuse must not resurrect the predecessor epoch" + ); + assert_eq!( + destroy_calls.get(), + 1, + "stale authority must fail before lifecycle adapter I/O" + ); + bound + .destroy_disposable_context(&second) + .map_err(|_| "current ownership generation must still authorize exact destruction")?; + + let mut previous_epoch = second.context_epoch().value(); + for _ in 0..256 { + let current = bound + .create_disposable_context() + .map_err(|_| "proven-destroyed identity reuse must remain bounded and admissible")?; + assert_eq!(current.context_epoch().value(), previous_epoch + 1); + previous_epoch = current.context_epoch().value(); + bound + .destroy_disposable_context(¤t) + .map_err(|_| "each current ownership generation must be proven destroyed")?; + } + + assert_eq!(create_calls.get(), 258); + assert_eq!(destroy_calls.get(), 258); + bound + .end() + .map_err(|_| "no live or uncertain ownership may remain after proven destruction")?; + Ok(()) +} From e97791b1ec85e23ee0421060005094cd09b86cf0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:11:35 +0900 Subject: [PATCH 073/632] fix(browser-session): narrow recovery custody projection --- .../src/recovery.rs | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index 0eba53ee9..cdcc8cf5a 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -1,14 +1,15 @@ use crate::browser_session::{ - BoundBrowserSession, BrowserSession, BrowserSessionState, DisposableContextPort, + BoundBrowserSession, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateRecoveryEvidence, DisposableContextPort, }; /// Recovery-only custody of a Browser Session and its exact consumed lifecycle adapter. /// /// This wrapper is obtained only by consuming a bound session that has already entered /// [`BrowserSessionState::RecoveryRequired`] or [`BrowserSessionState::TransportLost`]. It exposes -/// immutable Browser Session evidence but deliberately provides none of the ordinary create, -/// presentation-authority, epoch-advance, destroy, or authorized-operation methods. The concrete -/// adapter remains private and is moved, not reconstructed or replaced. +/// only lifecycle state and exact non-authorizing recovery evidence. It deliberately provides none +/// of the ordinary create, presentation-authority, epoch-advance, destroy, authorized-operation, or +/// normal-finish methods, and it does not expose the inner [`BoundBrowserSession`] or concrete port. /// /// Ordinary context creation is not available from recovery custody: /// @@ -19,7 +20,7 @@ use crate::browser_session::{ /// } /// ``` /// -/// Presentation-authority lookup is not available from recovery custody: +/// Presentation-authority lookup is not available directly or through an inner Browser Session: /// /// ```compile_fail /// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; @@ -32,6 +33,17 @@ use crate::browser_session::{ /// } /// ``` /// +/// ```compile_fail +/// use originweave_browser_session::{BoundBrowserSessionRecovery, DisposableContextPort}; +/// use originweave_core::BrowsingContextId; +/// fn forbidden( +/// recovery: BoundBrowserSessionRecovery

, +/// context: BrowsingContextId, +/// ) { +/// let _ = recovery.browser_session().presentation_authority(context); +/// } +/// ``` +/// /// Context-epoch advancement is not available from recovery custody: /// /// ```compile_fail @@ -92,12 +104,23 @@ impl BoundBrowserSession

{ } impl BoundBrowserSessionRecovery

{ - /// Return immutable Browser Session recovery state and evidence. - /// - /// No adapter reference is exposed. Protocol-specific recovery code must consume a separately - /// reviewed recovery operation boundary rather than regaining ordinary mutation authority. + /// Return the lifecycle state captured by the unresolved Browser Session aggregate. + #[must_use] + pub const fn state(&self) -> BrowserSessionState { + self.bound.browser_session().state() + } + + /// Return exact non-authorizing ownership-recovery evidence. + #[must_use] + pub fn recovery_evidence(&self) -> &[BrowserSessionRecoveryEvidence] { + self.bound.browser_session().recovery_evidence() + } + + /// Return exact non-authorizing create-attempt recovery provenance. #[must_use] - pub const fn browser_session(&self) -> &BrowserSession { - self.bound.browser_session() + pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { + self.bound + .browser_session() + .create_attempt_recovery_evidence() } } From 80fdcbb219608adac18510112cf1b811076d2e40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:11:55 +0900 Subject: [PATCH 074/632] test(browser-session): enforce narrow recovery projection --- .../tests/recovery_owner_handoff.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_owner_handoff.rs b/crates/originweave-browser-session/tests/recovery_owner_handoff.rs index 34c95e0b1..063489ea7 100644 --- a/crates/originweave-browser-session/tests/recovery_owner_handoff.rs +++ b/crates/originweave-browser-session/tests/recovery_owner_handoff.rs @@ -118,12 +118,14 @@ fn unproven_destroy_hands_exact_bound_adapter_and_evidence_to_recovery_owner( .into_recovery() .map_err(|_| "RecoveryRequired must permit consuming recovery handoff")?; - assert_eq!(drop_calls.get(), 0, "handoff must move, not replace, the bound adapter"); assert_eq!( - recovery.browser_session().state(), - BrowserSessionState::RecoveryRequired + drop_calls.get(), + 0, + "handoff must move, not replace, the bound adapter" ); - assert_eq!(recovery.browser_session().recovery_evidence(), expected_evidence); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence(), expected_evidence); + assert!(recovery.create_attempt_recovery_evidence().is_empty()); assert_eq!(create_calls.get(), 1, "handoff must not create browser state"); assert_eq!(destroy_calls.get(), 1, "handoff must not imply cleanup I/O"); @@ -178,12 +180,14 @@ fn transport_loss_hands_exact_bound_adapter_and_evidence_to_recovery_owner( .into_recovery() .map_err(|_| "TransportLost must permit consuming recovery handoff")?; - assert_eq!(drop_calls.get(), 0, "handoff must preserve the same bound adapter instance"); assert_eq!( - recovery.browser_session().state(), - BrowserSessionState::TransportLost + drop_calls.get(), + 0, + "handoff must preserve the same bound adapter instance" ); - assert_eq!(recovery.browser_session().recovery_evidence(), expected_evidence); + assert_eq!(recovery.state(), BrowserSessionState::TransportLost); + assert_eq!(recovery.recovery_evidence(), expected_evidence); + assert!(recovery.create_attempt_recovery_evidence().is_empty()); assert_eq!(create_calls.get(), 1, "handoff must not create browser state"); assert_eq!(destroy_calls.get(), 0, "transport loss is not destruction proof"); From 88a2586d4ed9eee9179d0c8ed42e0f08b6cd2dd8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:12:28 +0900 Subject: [PATCH 075/632] test(browser-session): pin recovery-owner method surface --- tests/test_browser_session_lifecycle_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 73736dac4..09980d4bc 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -30,6 +30,7 @@ def test_browser_session_is_an_independent_workspace_boundary(self) -> None: def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: """Raw driver identifiers must never become caller-mintable authority tokens.""" + recovery_source = (CRATE / "src/recovery.rs").read_text(encoding="utf-8") source = "\n".join( (CRATE / relative_path).read_text(encoding="utf-8") for relative_path in ( @@ -105,6 +106,17 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertNotIn("pub const fn port", source) self.assertNotIn("pub fn port", source) + self.assertIn("pub const fn state(&self) -> BrowserSessionState", recovery_source) + self.assertIn("pub fn recovery_evidence(&self)", recovery_source) + self.assertIn("pub fn create_attempt_recovery_evidence(&self)", recovery_source) + self.assertNotIn("pub const fn browser_session(&self)", recovery_source) + self.assertNotIn("pub fn browser_session(&self)", recovery_source) + self.assertIn( + "recovery.browser_session().presentation_authority(context)", + recovery_source, + ) + self.assertGreaterEqual(recovery_source.count("```compile_fail"), 6) + authority_impl = source.split("impl PresentationMutationAuthority", 1)[1].split( "enum OwnedContextState", 1 )[0] @@ -215,6 +227,9 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - recovery_handoff, ) self.assertIn(".into_recovery()", recovery_handoff) + self.assertIn("recovery.state()", recovery_handoff) + self.assertIn("recovery.recovery_evidence()", recovery_handoff) + self.assertNotIn("recovery.browser_session()", recovery_handoff) self.assertIn("handoff must move, not replace, the bound adapter", recovery_handoff) self.assertIn("handoff must not imply cleanup I/O", recovery_handoff) self.assertIn("transport loss is not destruction proof", recovery_handoff) From f1fce9f591277babdad83f212a8e53a3bf74b6bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 02:14:56 +0900 Subject: [PATCH 076/632] fix(browser-session): release proven-destroyed hot ownership --- .../src/browser_session.rs | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 47d8ccc0a..8900a40ef 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -535,7 +535,6 @@ impl PresentationMutationAuthority { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OwnedContextState { Active, - Destroyed, Uncertain, } @@ -748,11 +747,7 @@ impl BrowserSession { /// End the Browser Session only after every owned context has proven destruction. pub fn end(&mut self) -> Result<(), BrowserSessionError> { self.require_active()?; - if self - .contexts - .values() - .any(|record| record.state != OwnedContextState::Destroyed) - { + if !self.contexts.is_empty() { return Err(BrowserSessionError::ActiveContextRemains); } self.state = BrowserSessionState::Ended; @@ -887,25 +882,30 @@ impl BrowserSession { ) -> Result<(), BrowserSessionError> { let browser_session = self.id; let incarnation = self.incarnation; - let record = self.context_for_authority_mut(authority)?; - let context_epoch = record.epoch; - let request = DisposableContextDestroyRequest { - browser_session, - incarnation, - context: record.handle.clone(), - context_epoch, + let request = { + let record = self.context_for_authority_mut(authority)?; + DisposableContextDestroyRequest { + browser_session, + incarnation, + context: record.handle.clone(), + context_epoch: record.epoch, + } }; match port.destroy_disposable_context(&request) { Ok(()) => { - record.state = OwnedContextState::Destroyed; + // Exclusive aggregate mutation plus the pre-I/O authority check guarantee this key is + // still the generation just proven destroyed. Removing it keeps command-authority hot + // state proportional to live/uncertain ownership; the monotonic epoch rejects any stale + // authority if the browser later reuses the same raw context and isolation identifiers. + let _ = self.contexts.remove(&authority.browsing_context); Ok(()) } Err(DisposableContextDestroyError::DestroyFailed) => { - record.state = OwnedContextState::Uncertain; + self.context_for_authority_mut(authority)?.state = OwnedContextState::Uncertain; self.recovery_evidence .push(BrowserSessionRecoveryEvidence::UnprovenDestruction { context: request.context, - context_epoch, + context_epoch: request.context_epoch, }); self.enter_recovery_required(); Err(BrowserSessionError::ContextDestructionFailed) @@ -998,10 +998,7 @@ impl BrowserSession { fn has_unresolved_remote_ownership(&self) -> bool { matches!(self.state, BrowserSessionState::RecoveryRequired) || self.contexts.values().any(|record| { - matches!( - record.state, - OwnedContextState::Active | OwnedContextState::Uncertain - ) + matches!(record.state, OwnedContextState::Active | OwnedContextState::Uncertain) }) } } From 8c105dda5283fd4d167d10bd0d3ed85e2dd2c517 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:04:17 +0900 Subject: [PATCH 077/632] docs(browser-session): align recovery custody trace --- .../browser-session-lifecycle-authority.md | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 11453954b..507467dac 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -31,7 +31,9 @@ validated BrowserSessionId → presentation/reconciliation uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) → exact consumed adapter only → failed/unproven destruction retains exact handle + validated epoch as non-authorizing recovery evidence -→ proven destruction for every context +→ RecoveryRequired|TransportLost can consume the same bound owner into BoundBrowserSessionRecovery

+→ recovery custody exposes exact non-authorizing evidence but no ordinary lifecycle/authority surface +→ proven destruction removes the live hot-ownership record; failed destruction retains Uncertain ownership → BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` @@ -69,13 +71,23 @@ Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSess Cause-specific candidate evidence is retained separately from generic sibling ownership evidence. A same-valued candidate from a later failed attempt cannot erase a previously accepted ownership fact. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. -Operation/destroy/create-attempt provenance is implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch; create uncertainty and completion failure retain the reserved `attempt_epoch` in `DisposableContextCreateRecoveryEvidence`. The remaining recovery-boundary gap is a purpose-bounded `RecoveryRequired`/`TransportLost` handoff that keeps the exact same adapter and exact evidence instead of reconstructing a second adapter or restoring ordinary mutation authority. +Operation/destroy/create-attempt provenance is implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch; create uncertainty and completion failure retain the reserved `attempt_epoch` in `DisposableContextCreateRecoveryEvidence`. + +## Recovery-only custody and bounded hot ownership + +`BoundBrowserSession::into_recovery(self)` is the one-way custody boundary for unresolved ownership. It succeeds only from `RecoveryRequired` or `TransportLost`, moves the exact already-consumed non-`Clone` adapter without browser I/O, and returns `BoundBrowserSessionRecovery

`. `Active` or `Ended` returns the original bound owner unchanged, so recovery custody cannot be used as an alternate normal lifecycle path. + +`BoundBrowserSessionRecovery

` deliberately exposes only `state()`, `recovery_evidence()`, and `create_attempt_recovery_evidence()`. It exposes neither raw `P`, the inner `BoundBrowserSession`, nor the inner `BrowserSession`; ordinary create, presentation-authority lookup, epoch advancement, destroy, authorized operation, and normal finish remain unavailable. Rustdoc `compile_fail` contracts pin those negative capabilities. The wrapper therefore preserves same-adapter custody for the protocol recovery owner without reconstructing ordinary mutation authority from identifiers or evidence. Protocol-specific recovery commands and pending/accepted/quarantined tuple reconciliation remain #316-owned adapter work. + +Hot command-authority state contains only live or uncertain ownership. After exact authority validation and adapter-confirmed destruction, Browser Session removes that context record from the hot ownership map instead of accumulating a permanent `Destroyed` tombstone. A failed destroy does the opposite: the exact record remains `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains enumerable, and normal authority stays closed. The 258-generation same-handle hostile fixture proves a proven-destroyed raw isolation/context identity can be reused without unbounded hot-state growth while retained predecessor authority still fails before adapter I/O. Immediately after destruction it fails as `ContextNotOwned`; after the same raw identity is recreated under the next monotonic epoch it fails as `AuthorityMismatch`. + +Removing proven-destroyed command-authority records is not durable history deletion. Durable crash/process-restart recovery and audit history remain a separate persistence concern; they must not be reconstructed from the bounded hot map or from `abandoned_bound_session_count()`. ## Abandonment and lifecycle completion `BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` succeeds only after all owned contexts have proven destruction. If it returns `ActiveContextRemains`, the wrapper, exact bound adapter, and private ownership ledger remain intact. The same owner can therefore destroy or reconcile the remaining context and retry `finish()` without introducing a second adapter or ambient cleanup capability. -`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists Browser Session recovery evidence before process termination. +`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. `BoundBrowserSessionRecovery

` keeps that same contained owner, so dropping unresolved recovery custody retains the same non-I/O abandonment accounting. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists Browser Session recovery evidence before process termination. The abandonment counter and Browser Session incarnation allocator use `AtomicU64::try_update` with the same memory-ordering and closure semantics as the predecessor `fetch_update` calls. This removes the pinned-nightly deprecation without weakening overflow behavior or synchronization semantics. @@ -85,7 +97,7 @@ Transport liveness is tracked independently from ownership recovery. A first tra ## Sequential ABA safety -Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external values and also begin at epoch 1. A's retained authority still fails because B has a different `BrowserSessionIncarnation`. The bound port receives the incarnation inside aggregate-issued lifecycle capabilities. +Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external values and also begin at epoch 1. A's retained authority still fails because B has a different `BrowserSessionIncarnation`. Within one aggregate, proven destruction releases the same raw identity from hot ownership but the monotonic context epoch prevents a retained authority from becoming current when that raw identity is recreated. The bound port receives the incarnation inside aggregate-issued lifecycle capabilities. ## Browser-issued user-context identity @@ -126,6 +138,10 @@ OriginWeave does not treat protocol identifiers as policy authority or assume hi | lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; `DisposableContextCreateRecoveryEvidence`; recovery tests | | `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings` | | transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | +| unresolved state can move to recovery-only custody without replacing the adapter | `BoundBrowserSession::into_recovery`; `BoundBrowserSessionRecovery`; `recovery_owner_handoff.rs` | +| recovery custody cannot regain ordinary lifecycle or presentation authority | `BoundBrowserSessionRecovery` rustdoc `compile_fail` contracts; repository contract | +| proven destruction releases bounded hot ownership while stale authority remains rejected | `proven_destroy_releases_hot_ownership.rs`; post-success context removal in `BrowserSession` | +| failed destruction retains uncertain hot ownership and exact evidence | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; destroy-failure tests | | unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | | failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | | normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | @@ -141,8 +157,8 @@ Protected-main integration is required before capability maturity can be promote ## Buyer acceptance still open -This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, durable crash/process-restart recovery persistence, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. +This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, protocol-specific recovery commands through the same custody boundary, durable crash/process-restart recovery persistence, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. ## Reference -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ \ No newline at end of file From b63ff291035e36b321f96b46d65c6aaec530a561 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:04:50 +0900 Subject: [PATCH 078/632] docs(browser-session): model recovery custody and hot state --- .../browser-session-lifecycle-authority.md | 88 +++++++++++++++---- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 09cf518f1..4c8b0e858 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -33,14 +33,14 @@ sequenceDiagram S->>S: register exact handle + Active epoch S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) else domain handle rejected - S->>S: retain duplicate handle as recovery evidence + S->>S: retain duplicate handle + exact rejected attempt S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle S->>S: mint DisposableContextCreateCompletion(Rejected, exact attempt) S->>P: complete_disposable_context_creation(completion) P->>P: pending exact attempt → quarantined/non-authorizing S->>S: RecoveryRequired else completion cannot be proven - S->>S: retain UnsettledAdapterHandle + S->>S: retain UnsettledAdapterHandle + exact unsettled attempt S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle S->>S: RecoveryRequired end @@ -67,21 +67,23 @@ sequenceDiagram C->>BS: destroy_disposable_context(authority) BS->>S: validate exact session/incarnation/isolation/context/epoch before I/O - S->>S: mint DisposableContextDestroyRequest with exact stored handle + S->>S: mint DisposableContextDestroyRequest with exact stored handle + validated epoch S->>P: destroy_disposable_context(request) P->>B: remove exact owned isolation boundary B-->>P: observed destruction post-condition or DisposableContextDestroyError alt destruction proved P-->>S: success - S->>S: context = Destroyed + S->>S: remove live hot-ownership record + Note over S: epoch allocator remains monotonic; retained predecessor authority stays stale else destruction unproven - S->>S: retain UnprovenDestruction for failed handle + S->>S: retain UnprovenDestruction(handle, validated epoch) S->>S: retain each other Active sibling as RecoveryRequiredOwnedHandle + S->>S: keep failed record as Uncertain S->>S: RecoveryRequired; all active siblings become Uncertain end C->>BS: finish() - alt every owned context Destroyed + alt no live or uncertain ownership remains BS->>S: end() S-->>C: Ended else ownership remains @@ -105,26 +107,37 @@ stateDiagram-v2 Active --> Active: create candidate + exact Accepted completion + authority Active --> Active: authorized operation / current authority / exact bound adapter Active --> Active: context epoch advanced / prior authority stale - Active --> Active: exact owned isolation destruction proved + Active --> Active: exact owned isolation destruction proved / remove hot record Active --> Active: DisposableContextCreateError::CreateFailedClean Active --> Active: failed finish / retain same bound owner - Active --> RecoveryRequired: CreateFailedUncertain / retain known partial isolation + Active --> RecoveryRequired: CreateFailedUncertain / retain attempt provenance Active --> RecoveryRequired: duplicate output + exact Rejected completion + sibling RecoveryRequiredOwnedHandle - Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle + sibling RecoveryRequiredOwnedHandle - Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven + sibling RecoveryRequiredOwnedHandle - Active --> Ended: all owned contexts Destroyed + finish() + Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle + exact attempt + sibling evidence + Active --> RecoveryRequired: DisposableContextDestroyError / keep failed record Uncertain + sibling evidence + Active --> Ended: hot ownership empty + finish() Active --> TransportLost: browser transport lost / retain TransportLossOwnedHandle / mark uncertain - RecoveryRequired --> RecoveryRequired: transport_lost = true / preserve recovery evidence + RecoveryRequired --> RecoveryRequired: transport_lost = true / preserve stronger recovery state + RecoveryRequired --> RecoveryCustody: into_recovery(self) / move exact adapter + evidence / no I/O + TransportLost --> RecoveryCustody: into_recovery(self) / move exact adapter + evidence / no I/O + RecoveryCustody --> [*]: persist or protocol-reconcile elsewhere; no ordinary authority surface Ended --> [*] - RecoveryRequired --> [*] - TransportLost --> [*] note right of RecoveryRequired BrowserSessionRecoveryEvidence retains known partial identity, duplicate/unsettled handle, - exact unproven-destruction handle, and + exact unproven-destruction handle + epoch, and RecoveryRequiredOwnedHandle for indirect siblings. - It grants no I/O. + DisposableContextCreateRecoveryEvidence retains + create-attempt epoch/disposition separately. + Neither evidence family grants I/O authority. + end note + + note right of RecoveryCustody + BoundBrowserSessionRecovery

exposes only + state + exact non-authorizing evidence. + It does not expose raw P, BrowserSession, + create, presentation authority, epoch advance, + destroy, authorized operation, or finish. end note ``` @@ -133,6 +146,7 @@ sequenceDiagram autonumber participant C as Application service participant BS as BoundBrowserSession + participant R as BoundBrowserSessionRecovery participant P as exact bound adapter participant O as Operability / recovery observer @@ -142,8 +156,15 @@ sequenceDiagram BS-->>C: ActiveContextRemains; wrapper retained C->>BS: destroy exact authority BS->>P: proven remote destruction + BS->>BS: remove live hot-ownership record C->>BS: finish() BS-->>C: Ended + else unresolved ownership enters recovery + C->>BS: destroy failure or record_transport_loss() + BS-->>C: RecoveryRequired or TransportLost + exact evidence + C->>BS: into_recovery(self) + BS-->>R: move exact non-Clone adapter + evidence; adapter I/O = 0 + Note over R,P: recovery custody cannot regain ordinary lifecycle/presentation authority else ordinary wrapper abandonment C-xBS: drop without proven cleanup Note over BS,P: Drop performs no browser I/O @@ -152,7 +173,38 @@ sequenceDiagram end ``` -The abandonment signal is deliberately weaker than durable recovery. Exact crash/process-restart reconciliation remains open until a canonical recovery owner persists `BrowserSessionRecoveryEvidence` before process termination. +Recovery custody is deliberately narrower than protocol reconciliation. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth and any purpose-bounded protocol recovery operation that uses the exact adapter held by recovery custody. Durable crash/process-restart persistence remains open until a canonical recovery owner stores exact recovery evidence before process termination. + +## Same-raw-identity hot-state hostile case + +```mermaid +sequenceDiagram + autonumber + participant C as Application service + participant BS as BoundBrowserSession + participant P as exact lifecycle port + + C->>BS: create U/C + BS->>P: create(attempt epoch 1) + Accepted + BS-->>C: authority epoch 1 + C->>BS: destroy(authority epoch 1) + BS->>P: destroy exact U/C + epoch 1 + P-->>BS: destruction proved + BS->>BS: remove U/C from hot ownership + + C->>BS: destroy(retained authority epoch 1) + BS-->>C: ContextNotOwned + Note over BS,P: stale check rejects before adapter I/O + + C->>BS: create same raw U/C again + BS->>P: create(attempt epoch 2) + Accepted + BS-->>C: authority epoch 2 + C->>BS: destroy(retained authority epoch 1) + BS-->>C: AuthorityMismatch + Note over BS,P: same raw values cannot resurrect predecessor epoch +``` + +The hostile acceptance repeats this create → proven destroy → same-handle recreate cycle for 258 ownership generations. Hot command-authority state remains bounded to live/uncertain ownership instead of accumulating proven-destroyed tombstones. Durable audit/history retention is a separate persistence concern. ## Sequential ABA hostile case @@ -181,4 +233,4 @@ sequenceDiagram B->>PB: operate/destroy only with authority B + incarnation B ``` -`RecoveryRequired` and `TransportLost` remain terminal for normal authority in this slice. Later reconciliation may inspect recovery evidence, but it must not reconstruct cleanup authority from raw identifiers or treat command ACK as proof of destruction. +`RecoveryRequired` and `TransportLost` remain closed to normal lifecycle and presentation authority. `into_recovery(self)` is a one-way custody transfer, not a command-authority resurrection path; later protocol reconciliation must stay purpose-bounded and must not infer cleanup authority from raw identifiers or treat command ACK as proof of destruction. \ No newline at end of file From 0fe893e9b09037ae35dacbce7cf7b2263aabe98f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:05:23 +0900 Subject: [PATCH 079/632] docs(browser-session): record recovery custody decision --- ...sion-recovery-custody-and-hot-ownership.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md diff --git a/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md new file mode 100644 index 000000000..50bd4fbbc --- /dev/null +++ b/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md @@ -0,0 +1,118 @@ +# ADR 0115: Browser Session recovery custody and bounded hot ownership + +- Status: Proposed +- Date: 2026-09-15 +- Extends: ADR 0114 +- Owning bounded context: `originweave-browser-session` + +## Context + +ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. + +First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. + +Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. + +These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery commands remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. + +## Decision drivers + +- Preserve the exact consumed adapter across unresolved ownership without making it generally accessible again. +- Keep recovery evidence non-authorizing. +- Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. +- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. +- Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. +- Permit browser reuse of the same raw user-context/browsing-context identity only as a new monotonic ownership generation. +- Reject retained stale authority before adapter I/O after both destruction and same-raw-identity recreation. +- Keep durable audit/history and process-restart persistence separate from the hot authorization map. + +## Decision + +1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. +2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. +3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. +4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. +5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. +6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. +7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. +8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. +9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. +10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. +11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. +12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. +13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. +14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. +15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. + +## Alternatives considered + +### Return raw `P` from the failed bound session + +Rejected. Raw adapter recovery recreates ambient capability and permits callers to issue protocol commands outside Browser Session authority. + +### Expose `&BrowserSession` from recovery custody + +Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections. + +### Clone or reconstruct the adapter for recovery + +Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. + +### Keep every proven-destroyed context as a permanent hot tombstone + +Rejected. It makes authority-admission state grow with historical throughput and conflates authorization with audit retention. Monotonic epochs plus exact validation are sufficient to reject predecessor capabilities after a proven destroy and same-identity recreation. + +### Delete records after any destroy command acknowledgement + +Rejected. A command ACK is not proof that the disposable browser boundary is gone. Failed or otherwise unproven destruction must retain uncertain ownership and exact recovery evidence. + +### Reset context epochs when raw identifiers are reused + +Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch could make retained authority current again. + +## Consequences + +The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody deliberately cannot complete the protocol-specific recovery by itself; that operation belongs to the WebDriver BiDi ACL/adapter owner and must remain purpose-bounded. + +Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. + +A child navigation implementation must treat ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. + +## Security and governance impact + +The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned but not ambiently callable. + +Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. + +This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. + +## Executable evidence + +- `crates/originweave-browser-session/src/recovery.rs` + - `BoundBrowserSession::into_recovery` + - `BoundBrowserSessionRecovery

` + - negative `compile_fail` capability contracts +- `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` + - unproven destroy moves the exact adapter and exact evidence without I/O + - transport loss moves the exact adapter and exact evidence without I/O +- `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` + - 258 same-handle ownership generations remain admissible after proven destruction + - epochs are strictly monotonic + - retained predecessor authority fails before lifecycle adapter I/O +- destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. + +These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. + +## Buyer acceptance still open + +- WebDriver BiDi purpose-bounded recovery operations through the exact recovery-held adapter; +- real Chromium proof of remote destruction and post-cleanup state; +- durable crash/process-restart persistence of exact recovery evidence; +- child navigation ownership-generation/witness implementation and real-browser ABA acceptance; +- protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. + +## References + +Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ + +Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file From 01bfc8c4678011f83767d9aa8393fd7008173845 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:05:54 +0900 Subject: [PATCH 080/632] docs(adr): index browser-session recovery custody --- docs/adr/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 2c492ba95..5403580ed 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -67,10 +67,11 @@ ADR 0013, ADR 0014, ADR 0110, ADR 0111, and ADR 0112 exist only on this document | [0016](0016-bap-task-lifecycle-authority.md) | BAP task lifecycle and state authority | Proposed | BAP task states, transitions, recovery validation, transition sequencing, and authority separation | | [0113](0113-webdriver-bidi-screen-area-ownership.md) | WebDriver BiDi screen-area ownership witness | Proposed | Browser Session-owned screen-settings mutation, destructive reset boundary, and fail-closed adapter authority | | [0114](0114-browser-session-disposable-context-authority.md) | Browser Session disposable-context authority | Proposed | owned disposable context lifecycle, exact context epochs, presentation mutation authority, cleanup uncertainty and transport-loss invalidation | +| [0115](0115-browser-session-recovery-custody-and-hot-ownership.md) | Browser Session recovery custody and bounded hot ownership | Proposed | same-adapter one-way recovery custody, non-authorizing recovery evidence, proven-destroy hot-state retirement, and stale-authority ABA rejection | -ADR 0016 belongs to the active BAP lifecycle feature branch. ADR 0113 belongs to the active WebDriver BiDi screen-area ownership successor. ADR 0114 belongs to the Browser Session lifecycle successor for issue #312. Indexing them makes the branch documentation graph complete while preserving Proposed lifecycle and active-PR, non-protected-main maturity. +ADR 0016 belongs to the active BAP lifecycle feature branch. ADR 0113 belongs to the active WebDriver BiDi screen-area ownership successor. ADR 0114 and ADR 0115 belong to the Browser Session lifecycle successor for issue #312; ADR 0115 extends ADR 0114 without promoting either decision beyond Proposed. Indexing them makes the branch documentation graph complete while preserving active-PR, non-protected-main maturity. -After protected-main integration, retain this subsection only when it is intentionally serving as historical provenance; otherwise protected-main reconciliation must remove it. In either case, integration alone does not change ADR 0016, ADR 0113, or ADR 0114 from Proposed or assert implementation maturity. +After protected-main integration, retain this subsection only when it is intentionally serving as historical provenance; otherwise protected-main reconciliation must remove it. In either case, integration alone does not change ADR 0016, ADR 0113, ADR 0114, or ADR 0115 from Proposed or assert implementation maturity. Other active feature PRs may contain additional Proposed ADRs. Those files are not part of this canonical documentation line until integrated or deliberately reconciled here. Historical PR checks, stale branch state, or chat decisions never transfer ADR acceptance across a changed head. @@ -146,4 +147,4 @@ Material external standards or research belong in APA 7th format in [`../doctori - [`../traceability/README.md`](../traceability/README.md) maps requirements and decisions to implementation and evidence. - [`../DOCUMENTATION_FITNESS.md`](../DOCUMENTATION_FITNESS.md) records semantic completeness and stale/current findings across the graph. -If these artifacts disagree about current implementation, protected-main source, executable tests, built/released artifacts, configuration/migrations, and protected-main operational evidence appropriate to the claim define implementation truth. Accepted ADRs explain governing design decisions; they do not upgrade missing behavior into shipped behavior. The disagreement is a documentation or implementation defect that must be repaired rather than silently rationalized from conversation history. +If these artifacts disagree about current implementation, protected-main source, executable tests, built/released artifacts, configuration/migrations, and protected-main operational evidence appropriate to the claim define implementation truth. Accepted ADRs explain governing design decisions; they do not upgrade missing behavior into shipped behavior. The disagreement is a documentation or implementation defect that must be repaired rather than silently rationalized from conversation history. \ No newline at end of file From dda618d96064953bc8f7fc227b4f34b18855be8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:06:27 +0900 Subject: [PATCH 081/632] docs(browser-session): link recovery custody ADR --- docs/traceability/browser-session-lifecycle-authority.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 507467dac..b3dd28114 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -2,7 +2,7 @@ - Status: IMPLEMENTED_ON_ACTIVE_PR - Owning bounded context: `originweave-browser-session` -- Governing proposal: ADR 0114 +- Governing proposals: ADR 0114; ADR 0115 - Requirement owner: issue #312 - Integration prerequisites: #229 presentation-ownership witnesses; #314/#316 WebDriver BiDi ACL after this foundation is exact-head GREEN From fbcb64a44add7116ee6460a38751d1866abbcdfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:08:40 +0900 Subject: [PATCH 082/632] test(browser-session): pin recovery custody doctoring --- ...test_browser_session_lifecycle_contract.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 09980d4bc..efc9684a6 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -174,6 +174,9 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - recovery_handoff = (CRATE / "tests/recovery_owner_handoff.rs").read_text( encoding="utf-8" ) + hot_ownership = (CRATE / "tests/proven_destroy_releases_hot_ownership.rs").read_text( + encoding="utf-8" + ) operation_hostile = (CRATE / "tests/authorized_context_operation.rs").read_text( encoding="utf-8" ) @@ -234,6 +237,17 @@ def test_hostile_recovery_binding_and_operation_fixtures_remain_external(self) - self.assertIn("handoff must not imply cleanup I/O", recovery_handoff) self.assertIn("transport loss is not destruction proof", recovery_handoff) + self.assertIn( + "proven_destroy_releases_hot_ownership_without_resurrecting_stale_authority", + hot_ownership, + ) + self.assertIn("for _ in 0..256", hot_ownership) + self.assertIn("Err(BrowserSessionError::ContextNotOwned)", hot_ownership) + self.assertIn("Err(BrowserSessionError::AuthorityMismatch)", hot_ownership) + self.assertIn("stale authority must fail before lifecycle adapter I/O", hot_ownership) + self.assertIn("create_calls.get(), 258", hot_ownership) + self.assertIn("destroy_calls.get(), 258", hot_ownership) + self.assertIn("authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io", operation_hostile) self.assertIn("AuthorizedContextOperationError::BrowserSession", operation_hostile) self.assertIn("AuthorizedContextOperationError::Adapter", operation_hostile) @@ -253,6 +267,9 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: adr = (ROOT / "docs/adr/0114-browser-session-disposable-context-authority.md").read_text( encoding="utf-8" ) + recovery_adr = ( + ROOT / "docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md" + ).read_text(encoding="utf-8") trace = (ROOT / "docs/traceability/browser-session-lifecycle-authority.md").read_text( encoding="utf-8" ) @@ -289,9 +306,29 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: ): self.assertIn(token, adr) + for token in ( + "Status: Proposed", + "Extends: ADR 0114", + "BoundBrowserSessionRecovery", + "into_recovery(self)", + "same non-`Clone` adapter instance", + "non-authorizing", + "hot command-authority state", + "ContextNotOwned", + "AuthorityMismatch", + "monotonic", + "process-restart", + "#316", + "compile_fail", + "protected `main`", + ): + self.assertIn(token, recovery_adr) + for token in ( "IMPLEMENTED_ON_ACTIVE_PR", + "ADR 0115", "BoundBrowserSession", + "BoundBrowserSessionRecovery", "DisposableContextCreateCompletion", "per-create transaction", "no public raw port accessor", @@ -306,12 +343,17 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "TransportLossOwnedHandle", "abandoned_bound_session_count", "durable crash/process-restart recovery", + "258-generation", + "ContextNotOwned", + "AuthorityMismatch", ): self.assertIn(token, trace) for token in ( "PresentationMutationAuthority", "BoundBrowserSession", + "BoundBrowserSessionRecovery", + "RecoveryCustody", "DisposableContextCreateCompletion", "BrowserSessionIncarnation", "RecoveryRequired", @@ -323,6 +365,8 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "TransportLossOwnedHandle", "abandoned_bound_session_count", "finish()", + "remove live hot-ownership record", + "258 ownership generations", ): self.assertIn(token, uml) self.assertNotIn("IMPLEMENTED_ON_PROTECTED_MAIN", trace) From 77819f1236d295f9cca2c5eced72cac9b812b78f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:09:16 +0900 Subject: [PATCH 083/632] docs(browser-session): complete recovery custody ADR --- ...sion-recovery-custody-and-hot-ownership.md | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md index 50bd4fbbc..01b2c7e89 100644 --- a/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md @@ -26,25 +26,15 @@ These questions are Browser Session domain concerns. WebDriver BiDi pending/acce - Reject retained stale authority before adapter I/O after both destruction and same-raw-identity recreation. - Keep durable audit/history and process-restart persistence separate from the hot authorization map. -## Decision +## Assumptions and authority boundaries -1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. -2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. -3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. -4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. -5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. -6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. -7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. -8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. -9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. -10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. -11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. -12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. -13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. -14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. -15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. +Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. + +WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. + +Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, and recovery evidence do not grant Browser Session command authority. -## Alternatives considered +## Options considered ### Return raw `P` from the failed bound session @@ -70,6 +60,24 @@ Rejected. A command ACK is not proof that the disposable browser boundary is gon Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch could make retained authority current again. +## Decision + +1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. +2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. +3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. +4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. +5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. +6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. +7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. +8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. +9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. +10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. +11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. +12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. +13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. +14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. +15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. + ## Consequences The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody deliberately cannot complete the protocol-specific recovery by itself; that operation belongs to the WebDriver BiDi ACL/adapter owner and must remain purpose-bounded. @@ -78,15 +86,23 @@ Proven destruction makes hot ownership proportional to current live/uncertain st A child navigation implementation must treat ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. -## Security and governance impact +## Failure and degraded behavior + +If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. + +A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. + +Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation allocation is exhausted, allocation fails closed rather than reusing authority identity. + +## Security / privacy / governance impact The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned but not ambiently callable. Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. -This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. +This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. Recovery evidence may identify remote browser boundaries but does not itself contain page content or create a new purpose for PII processing. -## Executable evidence +## Tests and acceptance evidence - `crates/originweave-browser-session/src/recovery.rs` - `BoundBrowserSession::into_recovery` @@ -100,16 +116,30 @@ This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, - epochs are strictly monotonic - retained predecessor authority fails before lifecycle adapter I/O - destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. +- `tests/test_browser_session_lifecycle_contract.py` pins the recovery wrapper surface, hostile fixtures, ADR 0115, traceability, and UML doctoring. These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. -## Buyer acceptance still open +## Migration and rollback + +This active-PR change is additive at the ownership-type boundary but changes the internal retention model after proven destruction. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery authority from the new wrapper. + +Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access or keeping record eviction without the stale-authority tests. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. + +## Open follow-ups + +- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation and remote-liveness semantics. +- #318/#321: production navigation ownership-generation/current-witness state plus same-raw-id ABA acceptance after this foundation receives exact-head verification. +- Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. +- Real Chromium proof of remote destruction, cleanup, navigation, interaction, and browser-observed post-conditions. +- `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. +- Protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. + +## Supersession / reversal conditions + +Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. -- WebDriver BiDi purpose-bounded recovery operations through the exact recovery-held adapter; -- real Chromium proof of remote destruction and post-cleanup state; -- durable crash/process-restart persistence of exact recovery evidence; -- child navigation ownership-generation/witness implementation and real-browser ABA acceptance; -- protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. +Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. ## References From 2446aa2ddec3db098a451370548df13fdc747d4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:11:54 +0900 Subject: [PATCH 084/632] docs(browser-session): move recovery custody ADR to 0116 --- ...sion-recovery-custody-and-hot-ownership.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md new file mode 100644 index 000000000..f0990efcf --- /dev/null +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -0,0 +1,148 @@ +# ADR 0116: Browser Session recovery custody and bounded hot ownership + +- Status: Proposed +- Date: 2026-09-15 +- Extends: ADR 0114 +- Owning bounded context: `originweave-browser-session` + +## Context + +ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. + +First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. + +Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. + +These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery commands remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. + +## Decision drivers + +- Preserve the exact consumed adapter across unresolved ownership without making it generally accessible again. +- Keep recovery evidence non-authorizing. +- Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. +- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. +- Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. +- Permit browser reuse of the same raw user-context/browsing-context identity only as a new monotonic ownership generation. +- Reject retained stale authority before adapter I/O after both destruction and same-raw-identity recreation. +- Keep durable audit/history and process-restart persistence separate from the hot authorization map. + +## Assumptions and authority boundaries + +Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. + +WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. + +Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, and recovery evidence do not grant Browser Session command authority. + +## Options considered + +### Return raw `P` from the failed bound session + +Rejected. Raw adapter recovery recreates ambient capability and permits callers to issue protocol commands outside Browser Session authority. + +### Expose `&BrowserSession` from recovery custody + +Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections. + +### Clone or reconstruct the adapter for recovery + +Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. + +### Keep every proven-destroyed context as a permanent hot tombstone + +Rejected. It makes authority-admission state grow with historical throughput and conflates authorization with audit retention. Monotonic epochs plus exact validation are sufficient to reject predecessor capabilities after a proven destroy and same-identity recreation. + +### Delete records after any destroy command acknowledgement + +Rejected. A command ACK is not proof that the disposable browser boundary is gone. Failed or otherwise unproven destruction must retain uncertain ownership and exact recovery evidence. + +### Reset context epochs when raw identifiers are reused + +Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch could make retained authority current again. + +## Decision + +1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. +2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. +3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. +4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. +5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. +6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. +7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. +8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. +9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. +10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. +11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. +12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. +13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. +14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. +15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. + +## Consequences + +The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody deliberately cannot complete the protocol-specific recovery by itself; that operation belongs to the WebDriver BiDi ACL/adapter owner and must remain purpose-bounded. + +Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. + +A child navigation implementation must treat ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. + +## Failure and degraded behavior + +If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. + +A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. + +Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation allocation is exhausted, allocation fails closed rather than reusing authority identity. + +## Security / privacy / governance impact + +The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned but not ambiently callable. + +Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. + +This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. Recovery evidence may identify remote browser boundaries but does not itself contain page content or create a new purpose for PII processing. + +## Tests and acceptance evidence + +- `crates/originweave-browser-session/src/recovery.rs` + - `BoundBrowserSession::into_recovery` + - `BoundBrowserSessionRecovery

` + - negative `compile_fail` capability contracts +- `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` + - unproven destroy moves the exact adapter and exact evidence without I/O + - transport loss moves the exact adapter and exact evidence without I/O +- `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` + - 258 same-handle ownership generations remain admissible after proven destruction + - epochs are strictly monotonic + - retained predecessor authority fails before lifecycle adapter I/O +- destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. +- `tests/test_browser_session_lifecycle_contract.py` pins the recovery wrapper surface, hostile fixtures, ADR 0116, traceability, and UML doctoring. + +These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. + +## Migration and rollback + +This active-PR change is additive at the ownership-type boundary but changes the internal retention model after proven destruction. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery authority from the new wrapper. + +Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access or keeping record eviction without the stale-authority tests. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. + +## Open follow-ups + +- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation and remote-liveness semantics. +- #318/#321: production navigation ownership-generation/current-witness state plus same-raw-id ABA acceptance after this foundation receives exact-head verification. +- Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. +- Real Chromium proof of remote destruction, cleanup, navigation, interaction, and browser-observed post-conditions. +- `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. +- Protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. + +## Supersession / reversal conditions + +Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. + +Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. + +## References + +Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ + +Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file From 12d26a895b897e4789fe00146e139c62aad6d7f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:12:24 +0900 Subject: [PATCH 085/632] docs(adr): repair active ADR number collision --- docs/adr/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 5403580ed..6820a423a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -67,11 +67,11 @@ ADR 0013, ADR 0014, ADR 0110, ADR 0111, and ADR 0112 exist only on this document | [0016](0016-bap-task-lifecycle-authority.md) | BAP task lifecycle and state authority | Proposed | BAP task states, transitions, recovery validation, transition sequencing, and authority separation | | [0113](0113-webdriver-bidi-screen-area-ownership.md) | WebDriver BiDi screen-area ownership witness | Proposed | Browser Session-owned screen-settings mutation, destructive reset boundary, and fail-closed adapter authority | | [0114](0114-browser-session-disposable-context-authority.md) | Browser Session disposable-context authority | Proposed | owned disposable context lifecycle, exact context epochs, presentation mutation authority, cleanup uncertainty and transport-loss invalidation | -| [0115](0115-browser-session-recovery-custody-and-hot-ownership.md) | Browser Session recovery custody and bounded hot ownership | Proposed | same-adapter one-way recovery custody, non-authorizing recovery evidence, proven-destroy hot-state retirement, and stale-authority ABA rejection | +| [0116](0116-browser-session-recovery-custody-and-hot-ownership.md) | Browser Session recovery custody and bounded hot ownership | Proposed | same-adapter one-way recovery custody, non-authorizing recovery evidence, proven-destroy hot-state retirement, and stale-authority ABA rejection | -ADR 0016 belongs to the active BAP lifecycle feature branch. ADR 0113 belongs to the active WebDriver BiDi screen-area ownership successor. ADR 0114 and ADR 0115 belong to the Browser Session lifecycle successor for issue #312; ADR 0115 extends ADR 0114 without promoting either decision beyond Proposed. Indexing them makes the branch documentation graph complete while preserving active-PR, non-protected-main maturity. +ADR 0016 belongs to the active BAP lifecycle feature branch. ADR 0113 belongs to the active WebDriver BiDi screen-area ownership successor. ADR 0114 and ADR 0116 belong to the Browser Session lifecycle successor for issue #312; ADR 0116 extends ADR 0114 without promoting either decision beyond Proposed. ADR 0115 is reserved by the active #316 WebDriver BiDi lifecycle ACL stack and must not be duplicated here. Indexing these decisions makes the branch documentation graph complete while preserving active-PR, non-protected-main maturity. -After protected-main integration, retain this subsection only when it is intentionally serving as historical provenance; otherwise protected-main reconciliation must remove it. In either case, integration alone does not change ADR 0016, ADR 0113, ADR 0114, or ADR 0115 from Proposed or assert implementation maturity. +After protected-main integration, retain this subsection only when it is intentionally serving as historical provenance; otherwise protected-main reconciliation must remove it. In either case, integration alone does not change ADR 0016, ADR 0113, ADR 0114, or ADR 0116 from Proposed or assert implementation maturity. Other active feature PRs may contain additional Proposed ADRs. Those files are not part of this canonical documentation line until integrated or deliberately reconciled here. Historical PR checks, stale branch state, or chat decisions never transfer ADR acceptance across a changed head. From d66f7b85e427ef74f113726c3f16d5a747a462f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:12:59 +0900 Subject: [PATCH 086/632] test(browser-session): follow ADR 0116 collision repair --- tests/test_browser_session_lifecycle_contract.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index efc9684a6..97c48c362 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -268,7 +268,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: encoding="utf-8" ) recovery_adr = ( - ROOT / "docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md" + ROOT / "docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md" ).read_text(encoding="utf-8") trace = (ROOT / "docs/traceability/browser-session-lifecycle-authority.md").read_text( encoding="utf-8" @@ -307,6 +307,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: self.assertIn(token, adr) for token in ( + "ADR 0116", "Status: Proposed", "Extends: ADR 0114", "BoundBrowserSessionRecovery", @@ -321,12 +322,15 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: "#316", "compile_fail", "protected `main`", + "Assumptions and authority boundaries", + "Migration and rollback", + "Supersession / reversal conditions", ): self.assertIn(token, recovery_adr) for token in ( "IMPLEMENTED_ON_ACTIVE_PR", - "ADR 0115", + "ADR 0116", "BoundBrowserSession", "BoundBrowserSessionRecovery", "DisposableContextCreateCompletion", From ddebc59b34e863896d3f3738a20f49e484175513 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:13:32 +0900 Subject: [PATCH 087/632] docs(browser-session): follow ADR 0116 collision repair --- docs/traceability/browser-session-lifecycle-authority.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index b3dd28114..66e9d0df9 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -2,7 +2,7 @@ - Status: IMPLEMENTED_ON_ACTIVE_PR - Owning bounded context: `originweave-browser-session` -- Governing proposals: ADR 0114; ADR 0115 +- Governing proposals: ADR 0114; ADR 0116 - Requirement owner: issue #312 - Integration prerequisites: #229 presentation-ownership witnesses; #314/#316 WebDriver BiDi ACL after this foundation is exact-head GREEN From cde661784345b9489f2a98f4ccc4d261b6c8da3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 03:13:39 +0900 Subject: [PATCH 088/632] docs(browser-session): retire collided ADR 0115 path --- ...sion-recovery-custody-and-hot-ownership.md | 148 ------------------ 1 file changed, 148 deletions(-) delete mode 100644 docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md diff --git a/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md deleted file mode 100644 index 01b2c7e89..000000000 --- a/docs/adr/0115-browser-session-recovery-custody-and-hot-ownership.md +++ /dev/null @@ -1,148 +0,0 @@ -# ADR 0115: Browser Session recovery custody and bounded hot ownership - -- Status: Proposed -- Date: 2026-09-15 -- Extends: ADR 0114 -- Owning bounded context: `originweave-browser-session` - -## Context - -ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. - -First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. - -Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. - -These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery commands remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. - -## Decision drivers - -- Preserve the exact consumed adapter across unresolved ownership without making it generally accessible again. -- Keep recovery evidence non-authorizing. -- Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. -- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. -- Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. -- Permit browser reuse of the same raw user-context/browsing-context identity only as a new monotonic ownership generation. -- Reject retained stale authority before adapter I/O after both destruction and same-raw-identity recreation. -- Keep durable audit/history and process-restart persistence separate from the hot authorization map. - -## Assumptions and authority boundaries - -Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. - -WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. - -Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, and recovery evidence do not grant Browser Session command authority. - -## Options considered - -### Return raw `P` from the failed bound session - -Rejected. Raw adapter recovery recreates ambient capability and permits callers to issue protocol commands outside Browser Session authority. - -### Expose `&BrowserSession` from recovery custody - -Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections. - -### Clone or reconstruct the adapter for recovery - -Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. - -### Keep every proven-destroyed context as a permanent hot tombstone - -Rejected. It makes authority-admission state grow with historical throughput and conflates authorization with audit retention. Monotonic epochs plus exact validation are sufficient to reject predecessor capabilities after a proven destroy and same-identity recreation. - -### Delete records after any destroy command acknowledgement - -Rejected. A command ACK is not proof that the disposable browser boundary is gone. Failed or otherwise unproven destruction must retain uncertain ownership and exact recovery evidence. - -### Reset context epochs when raw identifiers are reused - -Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch could make retained authority current again. - -## Decision - -1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. -2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. -3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. -4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. -5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. -6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. -7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. -8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. -9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. -10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. -11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. -12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. -13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. -14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. -15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. - -## Consequences - -The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody deliberately cannot complete the protocol-specific recovery by itself; that operation belongs to the WebDriver BiDi ACL/adapter owner and must remain purpose-bounded. - -Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. - -A child navigation implementation must treat ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. - -## Failure and degraded behavior - -If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. - -A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. - -Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation allocation is exhausted, allocation fails closed rather than reusing authority identity. - -## Security / privacy / governance impact - -The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned but not ambiently callable. - -Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. - -This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. Recovery evidence may identify remote browser boundaries but does not itself contain page content or create a new purpose for PII processing. - -## Tests and acceptance evidence - -- `crates/originweave-browser-session/src/recovery.rs` - - `BoundBrowserSession::into_recovery` - - `BoundBrowserSessionRecovery

` - - negative `compile_fail` capability contracts -- `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` - - unproven destroy moves the exact adapter and exact evidence without I/O - - transport loss moves the exact adapter and exact evidence without I/O -- `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` - - 258 same-handle ownership generations remain admissible after proven destruction - - epochs are strictly monotonic - - retained predecessor authority fails before lifecycle adapter I/O -- destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. -- `tests/test_browser_session_lifecycle_contract.py` pins the recovery wrapper surface, hostile fixtures, ADR 0115, traceability, and UML doctoring. - -These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. - -## Migration and rollback - -This active-PR change is additive at the ownership-type boundary but changes the internal retention model after proven destruction. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery authority from the new wrapper. - -Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access or keeping record eviction without the stale-authority tests. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. - -## Open follow-ups - -- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation and remote-liveness semantics. -- #318/#321: production navigation ownership-generation/current-witness state plus same-raw-id ABA acceptance after this foundation receives exact-head verification. -- Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. -- Real Chromium proof of remote destruction, cleanup, navigation, interaction, and browser-observed post-conditions. -- `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. -- Protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. - -## Supersession / reversal conditions - -Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. - -Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. - -## References - -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ - -Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file From 6e8d37bd13194d48c4d232882eb622148947eeb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 04:04:45 +0900 Subject: [PATCH 089/632] docs(browser-session): currentize recovery and hot-ownership baseline --- docs/product-technical-gap-baseline.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5c938d14f..6effd381d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -5,11 +5,12 @@ This is a dated delivery baseline, not a substitute for the PRD, TRD, roadmap, a ## Live continuity note: 2026-09-15 - Protected `main` remains signature-valid and protected at `87c4daa1830bac5a5228b6036752ad5633232085`. The complete GitHub search surface reports 134 open pull requests and 17 open non-PR issues. The #317 source-repair lane is intentionally Draft outside short exact-head CI probes; with that repair lane Draft the queue is 121 Draft and 13 non-Draft. GitHub Release inventory remains empty. -- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active production repair preserves create-transaction provenance through `CreateFailedUncertain(Some/None)`, duplicate-candidate rejection, and accepted/rejected completion-settlement failure via `DisposableContextCreateRecoveryEvidence`. Aggregate-issued attempt epoch, disposition, and complete candidate identity remain non-authorizing recovery facts. Same-valued later candidate evidence no longer suppresses the separately accepted owner's `RecoveryRequiredOwnedHandle`. The same lineage removes the arbitrary 4096-byte `browser.UserContext` ceiling and migrates the two reported atomic `fetch_update` calls to `AtomicU64::try_update` without changing memory ordering or overflow behavior. -- The exact hostile acceptance `create_recovery_same_handle_distinct_fact.rs` now requires attempt 1's accepted ownership and attempt 2's duplicate/unsettled candidate facts to coexist even when their remote handle values are identical. `CreateFailedUncertain(None)` still retains exact aggregate-issued attempt identity, so “no complete handle” is not “no transaction evidence.” These are active-PR claims until one exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. +- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active production repair preserves create-transaction provenance through `CreateFailedUncertain(Some/None)`, duplicate-candidate rejection, and accepted/rejected completion-settlement failure via `DisposableContextCreateRecoveryEvidence`; removes the arbitrary 4096-byte `browser.UserContext` ceiling; and migrates the two reported atomic `fetch_update` calls to `AtomicU64::try_update` without changing memory ordering or overflow behavior. It also adds one-way same-adapter recovery custody through `BoundBrowserSessionRecovery

` for `RecoveryRequired|TransportLost`, with recovery-only evidence and no path back to raw adapter or ordinary Browser Session authority. +- Proven destruction now retires the exact live hot command-authority record only after authority validation and adapter-proven success. Failed destruction retains the same record as `Uncertain` with exact `UnprovenDestruction { context, context_epoch }` evidence. The hostile acceptance `proven_destroy_releases_hot_ownership.rs` reuses the same raw context identity across 258 generations and requires monotonic epochs plus stale predecessor rejection before lifecycle I/O. These are active-PR claims until one exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. +- The exact hostile acceptance `create_recovery_same_handle_distinct_fact.rs` requires attempt 1's accepted ownership and attempt 2's duplicate/unsettled candidate facts to coexist even when their remote handle values are identical. `CreateFailedUncertain(None)` still retains exact aggregate-issued attempt identity, so “no complete handle” is not “no transaction evidence.” - #318 remains Draft and structurally RED for the Browser Session navigation state machine. Its current base still points to an earlier #317 exact head, so the child is a repair/restack finding rather than a reason to rewrite the production lane concurrently. #321 remains its Draft same-raw-id recreation/ABA acceptance child. Both must ordinary non-force adopt a verified #317 successor; no child acceptance delta may be discarded merely to restore mergeability. - #316 remains Draft at exact `8ca6c5a190d9ad2b4c7843d440e91f6070d681c2`. It owns WebDriver BiDi correlation/pending→accepted/quarantined protocol truth and must ordinary non-force restack after the Browser Session prerequisite is verified. Browser Session does not absorb protocol tuple storage or command semantics. -- The next Browser Session production gaps after create-attempt correlation are a purpose-bounded same-adapter `RecoveryRequired`/`TransportLost` handoff and bounded hot ownership/history separation. Durable crash/process-restart persistence remains separate; `abandoned_bound_session_count()` is process-local operability evidence, not destruction proof or durable recovery storage. +- Same-adapter recovery custody and bounded hot ownership/history separation are implemented on the active #317 branch rather than remaining “next gaps.” Durable crash/process-restart persistence remains separate; `abandoned_bound_session_count()` is process-local operability evidence, not destruction proof or durable recovery storage. After exact-head verification, the next Browser Session production slice is the ownership-generation/current-navigation-witness state machine required by #318/#321. - The immutable W3C WebDriver BiDi Working Draft directly verified for this baseline is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), which names the 3 September 2026 draft as its previous version. The mutable `/TR/webdriver-bidi/` index currently exposes an older 24 August surface, so immutable dated provenance and mutable-index freshness are recorded separately. Standards freshness does not authorize an automatic runtime repin. - Current Chrome desktop Stable remains Chrome 153, promoted 2026-09-08 (`153.0.8010.36` on Linux; `.36/.37` on Windows/macOS). #299's older Chrome/ChromeDriver `150.0.7871.129` Agent Task result remains historical RED: 0/3 trials reached navigation because session creation failed. Buyer-current browser acceptance still requires explicitly qualified real navigation, interaction, browser-observed post-condition, destruction, and cleanup evidence; a command ACK or wrapper drop is not success. - The active organization ruleset `18156473` still requires one counted approval, stale-review dismissal on push, resolved review threads, additional approval for unattributed changes, seven central required workflows, and deletion/non-fast-forward protection. Administrative bypass exists but is not a normal delivery path and does not justify self-approval, stale-head promotion, or gate weakening. @@ -194,8 +195,8 @@ OriginWeave is not complete merely because every low-level primitive exists in s ## Next executable queue -1. Finish #317's Browser Session prerequisite in its single-writer lane: exact create-attempt recovery correlation is implemented; next prove the same-adapter recovery handoff and bounded hot ownership/history split, then run the complete exact-head contract/format/test/Clippy/rustdoc/100%-coverage/review sequence. Do not begin #318/#321 production implementation in parallel. -2. After #317 is verified, ordinary non-force restack #318 and #321 while preserving every valid acceptance delta; then restack #316 and implement BiDi pending→accepted/quarantined correlation and remote-liveness reconciliation without moving Browser Session authority into the adapter. +1. Finish #317's Browser Session prerequisite in its single-writer lane by obtaining runner-backed exact-head repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. Same-adapter recovery custody and bounded hot ownership are already implemented on the active branch and must not be reopened as planned work without a new failing contract. +2. After #317 is verified, ordinary non-force restack #318 and #321 while preserving every valid acceptance delta; then implement the Browser Session ownership-generation/current-navigation-witness production state machine required by those RED contracts. Restack #316 afterward and keep its BiDi pending→accepted/quarantined correlation and remote-liveness reconciliation in the protocol-owner lane. 3. Re-run the explicitly qualified real-Chromium acceptance (#299/#320 path): navigation, interaction, download/lifecycle transitions where applicable, page/browser-observed post-conditions, destruction, crash/cleanup, and stale-event replay. Command ACK alone is non-passing. 4. Drain the remaining merge gate in dependency order: every ready root PR needs current exact-head checks, resolved threads, and the current ruleset's counted `APPROVED` review from an eligible non-author collaborator. OpenCode/CodeRabbit narrative evidence does not substitute for that GitHub review. 5. Finish #27 and #10 as separate security tracks; neither should be hidden inside the browser-session or first real-browser PR. From d327c0593a42a06190c37b5839c8d0c7ec20ec6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 04:05:46 +0900 Subject: [PATCH 090/632] docs(standards): pin WebDriver BiDi 9 September publication --- docs/doctoring.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 44fb51d13..59c178549 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -6,7 +6,7 @@ This document records external evidence that changes OriginWeave architecture, t ### Browser automation and interoperability -The 3 September 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. OriginWeave pins this publication to the immutable dated TR `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`; the mutable `w3c.github.io/webdriver-bidi/` Editor's Draft is tracked separately and cannot silently redefine the adapter contract. Because the standard remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +The 9 September 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. OriginWeave pins this publication to the immutable dated TR `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`; that document identifies the 3 September 2026 draft as its previous published version. The mutable `w3c.github.io/webdriver-bidi/` Editor's Draft is tracked separately and cannot silently redefine the adapter contract. Because the standard remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. The final Model Context Protocol `2026-07-28` specification defines the currently reviewed MCP generation. Its stateless request model carries protocol metadata per request and standard Streamable HTTP routing metadata for MCP operations; its Tools surface defines bounded, case-sensitive tool names and requires clients to treat tool annotations as untrusted unless supplied by a trusted server. OriginWeave therefore keeps MCP outside the product authority model. Active PR #168 implements only a bounded Rust `tools/call` routing/action-policy foundation for that exact generation; the complete transport, request-metadata, discovery, OAuth, browser, secret, and persistence adapter remains planned and cannot be inferred from the core routing primitive. @@ -48,9 +48,9 @@ object with enumerated architecture/bitness/platform tokens, an at-most-32 ASCII brand-name limit, a non-empty brand list, and the draft's coherence rule that a non-mobile user agent reports an empty model (see ADR 0112). -The pinned 3 September 2026 WebDriver BiDi Working Draft exposes locale, media, +The pinned 9 September 2026 WebDriver BiDi Working Draft exposes locale, media, screen, user-agent, viewport, and time-zone emulation commands under the immutable -publication `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`. The screen +publication `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`. The screen shape contains width and height but not color depth, and locale accepts one value rather than an ordered language list, so neither proves the corresponding complete OriginWeave surface. The draft also does not define a hardware-concurrency @@ -266,6 +266,8 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2025, September 25). *Mitigating browser fingerprinting in Web specifications*. https://www.w3.org/TR/fingerprinting-guidance/ +World Wide Web Consortium. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ + World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ World Wide Web Consortium. (2026). *WebDriver BiDi* (Editor's Draft). https://w3c.github.io/webdriver-bidi/ From 09f5dde3ebedc0a7abcedac55bc966c64a71f86f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 04:07:23 +0900 Subject: [PATCH 091/632] docs(browser-session): record recovery custody and hot-state retirement --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e67d084b3..5e8fc822d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,14 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed - Preserved Browser Session create-attempt provenance through uncertain creation, duplicate-candidate rejection, and accepted/rejected completion-settlement failure. Recovery now keeps aggregate-issued attempt epoch, disposition, and complete candidate identity without collapsing a previously accepted same-valued owner into later candidate evidence; the abandonment/incarnation atomic updates use `AtomicU64::try_update` without changing their memory ordering or overflow behavior. +- Added one-way same-adapter Browser Session recovery custody for `RecoveryRequired` and `TransportLost` through `BoundBrowserSessionRecovery

`, preventing recovery evidence from regaining raw adapter or ordinary command authority. Proven destruction now retires only the exact live hot ownership record after adapter-proven success; failed destruction keeps `Uncertain` ownership with exact `UnprovenDestruction { context, context_epoch }` evidence. - Prevented the reusable profile-derived WebDriver BiDi planner from scheduling `setScreenSettingsOverride` from `ScreenMetrics` alone, because the standard operation also changes the page-observable available screen rectangle that the current presentation identity neither selects nor digest-binds. - Restored canonical Rust formatting for the WebDriver BiDi presentation cleanup assertion so exact-head contracts can execute the test, Clippy, and rustdoc gates. ### Added - Added exact Browser Session create-recovery transaction evidence for `CreateFailedUncertain(Some/None)`, duplicate candidates, and unsettled `Accepted|Rejected` completions, plus a hostile same-valued-handle fixture proving candidate facts and prior ownership facts remain distinct. -- Added a version-pinned `originweave-bidi` presentation-capability boundary for the W3C WebDriver BiDi Working Draft published on 3 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`). It depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. +- Added a 258-generation same-raw-context hostile acceptance proving proven-destroy hot-state retirement, monotonic context epochs, and predecessor-authority rejection before lifecycle I/O. +- Added a version-pinned `originweave-bidi` presentation-capability boundary for the W3C WebDriver BiDi Working Draft published on 9 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`). It depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. - Added a bounded Rust presentation-identity kernel for explicit browser-visible profiles and credential-free replay digests, including control-safe mobile UA-CH model values; applying those profiles to Chromium and proving page-observed effects remain separate adapter and browser-E2E work. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. From 9d48b3e0f81dfadd20140fb5145794a5c2df1292 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 05:01:13 +0900 Subject: [PATCH 092/632] test(browser-session): preserve full WebDriver BiDi user-context text --- .../tests/user_context_identity_length.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/originweave-browser-session/tests/user_context_identity_length.rs b/crates/originweave-browser-session/tests/user_context_identity_length.rs index d8c23c79f..37c5ddcdc 100644 --- a/crates/originweave-browser-session/tests/user_context_identity_length.rs +++ b/crates/originweave-browser-session/tests/user_context_identity_length.rs @@ -1,11 +1,19 @@ use originweave_browser_session::DisposableIsolationId; #[test] -fn webdriver_bidi_user_context_is_not_rejected_by_an_arbitrary_domain_length_cap() { - let remote_user_context = "u".repeat(4097); +fn webdriver_bidi_user_context_preserves_protocol_text_without_domain_grammar() { + let cases = [ + String::new(), + " context ".to_owned(), + "ctx\n".to_owned(), + "u".repeat(4097), + ]; - let identity = DisposableIsolationId::parse(&remote_user_context) - .expect("WebDriver BiDi browser.UserContext has no 4096-byte protocol limit"); + for remote_user_context in cases { + let identity = DisposableIsolationId::parse(&remote_user_context).expect( + "WebDriver BiDi browser.UserContext is CDDL text; Browser Session must preserve the exact remote identity", + ); - assert_eq!(identity.as_str(), remote_user_context); + assert_eq!(identity.as_str(), remote_user_context); + } } From 9a886b4bb8feeddef746b5bad2646f5c414d4189 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 06:07:04 +0900 Subject: [PATCH 093/632] fix(browser-session): preserve browser user-context text exactly --- .../src/browser_session.rs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 8900a40ef..b40a9767a 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -83,12 +83,16 @@ pub enum DisposableContextDestroyError { DestroyFailed, } -/// Validation failure for a browser-issued disposable isolation identity. +/// Compatibility error type for parsing a browser-issued disposable isolation identity. +/// +/// Browser Session preserves protocol text exactly and therefore does not currently emit either +/// variant. The result-shaped API remains stable for callers while protocol/runtime qualification +/// stays in the adapter boundary rather than being redefined as Browser Session lexical grammar. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DisposableIsolationIdError { - /// The identity is empty. + /// Reserved for compatibility with callers compiled against the earlier non-empty constraint. Empty, - /// The identity contains surrounding whitespace or control characters. + /// Reserved for compatibility with callers compiled against the earlier character constraint. InvalidCharacter, } @@ -101,18 +105,16 @@ pub enum DisposableIsolationIdError { pub struct DisposableIsolationId(String); impl DisposableIsolationId { - /// Parse one browser-issued isolation identity without inventing a protocol length limit. + /// Preserve one browser-issued isolation identity exactly as protocol text. + /// + /// Browser Session does not trim, normalize, reject empty text, reject control characters, or + /// impose an implementation-selected length limit. Any narrower runtime grammar must be proven + /// and enforced by the versioned adapter before this remote lifecycle address enters the domain. pub fn parse(value: &str) -> Result { - if value.is_empty() { - return Err(DisposableIsolationIdError::Empty); - } - if value.trim() != value || value.chars().any(char::is_control) { - return Err(DisposableIsolationIdError::InvalidCharacter); - } Ok(Self(value.to_owned())) } - /// Return the validated browser-issued isolation identity. + /// Return the exact browser-issued isolation identity. #[must_use] pub fn as_str(&self) -> &str { &self.0 @@ -1229,7 +1231,7 @@ mod tests { } fn isolation_id(value: &str) -> DisposableIsolationId { - DisposableIsolationId::parse(value).expect("valid isolation id") + DisposableIsolationId::parse(value).expect("protocol text representation is infallible") } fn session(value: u64) -> BrowserSession { @@ -1237,25 +1239,22 @@ mod tests { } #[test] - fn isolation_identity_validation_preserves_protocol_text() { - assert_eq!( - DisposableIsolationId::parse(""), - Err(DisposableIsolationIdError::Empty) - ); - let long = "x".repeat(4097); - let long_identity = DisposableIsolationId::parse(&long) - .expect("WebDriver BiDi browser.UserContext does not define a 4096-byte limit"); - assert_eq!(long_identity.as_str(), long); - assert_eq!( - DisposableIsolationId::parse(" user-context "), - Err(DisposableIsolationIdError::InvalidCharacter) - ); - assert_eq!( - DisposableIsolationId::parse("user\ncontext"), - Err(DisposableIsolationIdError::InvalidCharacter) - ); + fn isolation_identity_preserves_protocol_text_without_domain_grammar() { + let cases = [ + String::new(), + " user-context ".to_owned(), + "user\ncontext".to_owned(), + "x".repeat(4097), + "webdriver-user-context-10".to_owned(), + ]; + + for remote_user_context in cases { + let identity = DisposableIsolationId::parse(&remote_user_context) + .expect("protocol text representation is infallible"); + assert_eq!(identity.as_str(), remote_user_context); + } + let valid = isolation_id("webdriver-user-context-10"); - assert_eq!(valid.as_str(), "webdriver-user-context-10"); let handle = DisposableContextHandle::new(valid.clone(), context_id(10)); assert_eq!(handle.isolation(), &valid); assert_eq!(handle.browsing_context(), context_id(10)); From 606355881cc646aabc9af05152852bb7b20bff45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 06:11:06 +0900 Subject: [PATCH 094/632] fix(browser-session): keep read view non-authorizing --- crates/originweave-browser-session/src/browser_session.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index b40a9767a..721e6cfa2 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -678,8 +678,7 @@ impl BrowserSession { &self.create_recovery_evidence } - /// Return current presentation authority for an already-owned active context. - pub fn presentation_authority( + fn presentation_authority( &self, browsing_context: BrowsingContextId, ) -> Result { From 12c2d03b875c5a12fd8ba4225dc51c8a064d1d6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 06:11:18 +0900 Subject: [PATCH 095/632] test(browser-session): deny authority minting from read view --- crates/originweave-browser-session/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index e16a75fa9..a008f97b4 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -2,6 +2,21 @@ //! //! The aggregate implementation remains isolated from recovery custody. Public callers receive only //! the narrow domain surface re-exported here; the concrete lifecycle adapter is never exposed. +//! +//! The read-only Browser Session projection must not become a capability-minting escape hatch. Only +//! the bound owner exposes the explicit presentation-authority surface. +//! +//! ```compile_fail +//! use originweave_browser_session::{BoundBrowserSession, DisposableContextPort}; +//! use originweave_core::BrowsingContextId; +//! +//! fn read_view_cannot_mint( +//! bound: &BoundBrowserSession

, +//! context: BrowsingContextId, +//! ) { +//! let _ = bound.browser_session().presentation_authority(context); +//! } +//! ``` #![forbid(unsafe_code)] #![deny(missing_docs)] From dc8c4b41062dcb59d97ac3596605dbe62195c926 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 05:10:14 +0900 Subject: [PATCH 096/632] feat(browser-session): implement navigation authority state machine --- .../src/browser_session.rs | 511 +++++++++++++++++- 1 file changed, 491 insertions(+), 20 deletions(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 721e6cfa2..6645f938c 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -46,7 +46,7 @@ pub enum BrowserSessionError { SessionNotActive, /// No unused session-incarnation identity remains in this process. IncarnationExhausted, - /// No unused context epoch remains, so no new authority can be issued safely. + /// No unused monotonic authority or navigation generation remains, so issuance must fail closed. EpochExhausted, /// The disposable-context port proved that context creation failed without creating a boundary. ContextCreationFailed, @@ -293,7 +293,7 @@ impl DisposableContextCreateCompletion { self.browser_session } - /// Return the Browser Session incarnation for adapter correlation. + /// Return the non-reused Browser Session incarnation for adapter correlation. #[must_use] pub const fn incarnation(&self) -> BrowserSessionIncarnation { self.incarnation @@ -322,9 +322,9 @@ pub enum DisposableContextCreateCompletionError { /// Opaque Browser Session-issued request for destruction of one exact owned disposable context. /// /// There is deliberately no public constructor. The bound aggregate creates this request only after -/// validating the supplied presentation authority against current ownership. A caller cannot rebuild -/// cleanup authority from raw browser identifiers. The validated epoch is carried only as correlation -/// evidence for the already-authorized request; it is not independently sufficient to destroy state. +/// validating current lifecycle custody. A caller cannot rebuild cleanup authority from raw browser +/// identifiers. The epoch is correlation evidence for the already-authorized request; it is not +/// independently sufficient to destroy state. #[derive(Debug)] pub struct DisposableContextDestroyRequest { browser_session: BrowserSessionId, @@ -534,17 +534,52 @@ impl PresentationMutationAuthority { } } +/// Opaque Browser Session-issued witness for one admitted navigation generation. +/// +/// Raw protocol navigation/context identifiers are evidence only. This value is minted only after the +/// aggregate validates the current session incarnation, owned context, and presentation epoch. Its +/// fields remain private so a caller cannot manufacture terminal or commit authority from raw BiDi +/// event data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NavigationSettlementAuthority { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + browsing_context: BrowsingContextId, + context_epoch: BrowserContextEpoch, + navigation_generation: u64, +} + +/// Typed negative terminal outcome for one admitted navigation witness. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NavigationTerminationOutcome { + /// The browser reported navigation abortion. + Aborted, + /// The browser reported navigation failure. + Failed, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OwnedContextState { Active, Uncertain, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PresentationNavigationState { + Established, + Pending { + navigation_generation: u64, + committed: bool, + }, + Eligible, +} + #[derive(Debug, Clone, PartialEq, Eq)] struct OwnedContextRecord { handle: DisposableContextHandle, epoch: BrowserContextEpoch, state: OwnedContextState, + presentation_navigation: PresentationNavigationState, } /// Aggregate root for disposable browser-context lifecycle and presentation mutation authority. @@ -555,6 +590,7 @@ pub struct BrowserSession { state: BrowserSessionState, transport_lost: bool, next_epoch: u64, + next_navigation_generation: u64, contexts: BTreeMap, recovery_evidence: Vec, create_recovery_evidence: Vec, @@ -625,6 +661,7 @@ impl BrowserSession { state: BrowserSessionState::Active, transport_lost: false, next_epoch: 1, + next_navigation_generation: 1, contexts: BTreeMap::new(), recovery_evidence: Vec::new(), create_recovery_evidence: Vec::new(), @@ -688,6 +725,9 @@ impl BrowserSession { .get(&browsing_context) .filter(|record| record.state == OwnedContextState::Active) .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.presentation_navigation != PresentationNavigationState::Established { + return Err(BrowserSessionError::AuthorityMismatch); + } Ok(Self::authority_for( self.id, self.incarnation, @@ -696,20 +736,29 @@ impl BrowserSession { )) } - /// Advance one active owned context to a new authority epoch. + /// Advance one active, presentation-authorized owned context to a new authority epoch. pub fn advance_context_epoch( &mut self, browsing_context: BrowsingContextId, ) -> Result { self.require_active()?; + { + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.presentation_navigation != PresentationNavigationState::Established { + return Err(BrowserSessionError::AuthorityMismatch); + } + } + let next = reserve_epoch(&mut self.next_epoch)?; let browser_session = self.id; let incarnation = self.incarnation; let record = self .contexts .get_mut(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - let next = reserve_epoch(&mut self.next_epoch)?; + .expect("validated owned context remains present under exclusive aggregate access"); record.epoch = next; Ok(Self::authority_for( browser_session, @@ -871,11 +920,138 @@ impl BrowserSession { handle, epoch, state: OwnedContextState::Active, + presentation_navigation: PresentationNavigationState::Established, }, ); Ok(authority) } + fn begin_observed_navigation( + &mut self, + incarnation: BrowserSessionIncarnation, + browsing_context: BrowsingContextId, + context_epoch: BrowserContextEpoch, + ) -> Result { + self.require_active()?; + if incarnation != self.incarnation { + return Err(BrowserSessionError::AuthorityMismatch); + } + { + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.epoch != context_epoch { + return Err(BrowserSessionError::AuthorityMismatch); + } + } + let navigation_generation = + reserve_navigation_generation(&mut self.next_navigation_generation)?; + let record = self + .contexts + .get_mut(&browsing_context) + .expect("validated owned context remains present under exclusive aggregate access"); + record.presentation_navigation = PresentationNavigationState::Pending { + navigation_generation, + committed: false, + }; + Ok(NavigationSettlementAuthority { + browser_session: self.id, + incarnation: self.incarnation, + browsing_context, + context_epoch, + navigation_generation, + }) + } + + fn current_pending_navigation_mut( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<&mut OwnedContextRecord, BrowserSessionError> { + self.require_active()?; + if authority.browser_session != self.id || authority.incarnation != self.incarnation { + return Err(BrowserSessionError::AuthorityMismatch); + } + let record = self + .contexts + .get_mut(&authority.browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.epoch != authority.context_epoch { + return Err(BrowserSessionError::AuthorityMismatch); + } + match record.presentation_navigation { + PresentationNavigationState::Pending { + navigation_generation, + .. + } if navigation_generation == authority.navigation_generation => Ok(record), + _ => Err(BrowserSessionError::AuthorityMismatch), + } + } + + fn mark_observed_navigation_committed( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<(), BrowserSessionError> { + let record = self.current_pending_navigation_mut(authority)?; + match record.presentation_navigation { + PresentationNavigationState::Pending { + navigation_generation, + committed: false, + } => { + record.presentation_navigation = PresentationNavigationState::Pending { + navigation_generation, + committed: true, + }; + Ok(()) + } + PresentationNavigationState::Pending { + committed: true, .. + } => Err(BrowserSessionError::AuthorityMismatch), + _ => Err(BrowserSessionError::AuthorityMismatch), + } + } + + fn close_observed_navigation( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<(), BrowserSessionError> { + let record = self.current_pending_navigation_mut(authority)?; + record.presentation_navigation = PresentationNavigationState::Eligible; + Ok(()) + } + + fn reestablish_presentation_authority_for_context( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.require_active()?; + { + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.presentation_navigation != PresentationNavigationState::Eligible { + return Err(BrowserSessionError::AuthorityMismatch); + } + } + let next = reserve_epoch(&mut self.next_epoch)?; + let record = self + .contexts + .get_mut(&browsing_context) + .expect("validated owned context remains present under exclusive aggregate access"); + record.epoch = next; + record.presentation_navigation = PresentationNavigationState::Established; + Ok(Self::authority_for( + self.id, + self.incarnation, + &record.handle, + next, + )) + } + fn destroy_disposable_context_with_port( &mut self, authority: &PresentationMutationAuthority, @@ -883,6 +1059,7 @@ impl BrowserSession { ) -> Result<(), BrowserSessionError> { let browser_session = self.id; let incarnation = self.incarnation; + let browsing_context = authority.browsing_context; let request = { let record = self.context_for_authority_mut(authority)?; DisposableContextDestroyRequest { @@ -892,17 +1069,46 @@ impl BrowserSession { context_epoch: record.epoch, } }; + self.destroy_request_with_port(browsing_context, request, port) + } + + fn destroy_owned_disposable_context_with_port( + &mut self, + browsing_context: BrowsingContextId, + port: &mut P, + ) -> Result<(), BrowserSessionError> { + self.require_active()?; + let request = { + let record = self + .contexts + .get(&browsing_context) + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + DisposableContextDestroyRequest { + browser_session: self.id, + incarnation: self.incarnation, + context: record.handle.clone(), + context_epoch: record.epoch, + } + }; + self.destroy_request_with_port(browsing_context, request, port) + } + + fn destroy_request_with_port( + &mut self, + browsing_context: BrowsingContextId, + request: DisposableContextDestroyRequest, + port: &mut P, + ) -> Result<(), BrowserSessionError> { match port.destroy_disposable_context(&request) { Ok(()) => { - // Exclusive aggregate mutation plus the pre-I/O authority check guarantee this key is - // still the generation just proven destroyed. Removing it keeps command-authority hot - // state proportional to live/uncertain ownership; the monotonic epoch rejects any stale - // authority if the browser later reuses the same raw context and isolation identifiers. - let _ = self.contexts.remove(&authority.browsing_context); + let _ = self.contexts.remove(&browsing_context); Ok(()) } Err(DisposableContextDestroyError::DestroyFailed) => { - self.context_for_authority_mut(authority)?.state = OwnedContextState::Uncertain; + if let Some(record) = self.contexts.get_mut(&browsing_context) { + record.state = OwnedContextState::Uncertain; + } self.recovery_evidence .push(BrowserSessionRecoveryEvidence::UnprovenDestruction { context: request.context, @@ -950,7 +1156,9 @@ impl BrowserSession { .get_mut(&authority.browsing_context) .filter(|record| record.state == OwnedContextState::Active) .ok_or(BrowserSessionError::ContextNotOwned)?; - if record.epoch != authority.context_epoch || record.handle.isolation != authority.isolation + if record.epoch != authority.context_epoch + || record.handle.isolation != authority.isolation + || record.presentation_navigation != PresentationNavigationState::Established { return Err(BrowserSessionError::AuthorityMismatch); } @@ -1027,7 +1235,7 @@ impl BoundBrowserSession

{ self.session.presentation_authority(browsing_context) } - /// Advance one active owned context to a new authority epoch. + /// Advance one presentation-authorized owned context to a new authority epoch. pub fn advance_context_epoch( &mut self, browsing_context: BrowsingContextId, @@ -1035,7 +1243,75 @@ impl BoundBrowserSession

{ self.session.advance_context_epoch(browsing_context) } - /// Destroy the exact owned disposable boundary through the bound lifecycle port. + /// Admit one browser-observed navigation start for the exact current ownership generation. + /// + /// Admission revokes presentation mutation immediately, performs no browser I/O, consumes no + /// presentation epoch, and returns the only witness accepted by later commit or terminal methods. + pub fn record_observed_navigation( + &mut self, + incarnation: BrowserSessionIncarnation, + browsing_context: BrowsingContextId, + context_epoch: BrowserContextEpoch, + ) -> Result { + self.session + .begin_observed_navigation(incarnation, browsing_context, context_epoch) + } + + /// Record the first qualified commit-progress event for one current navigation witness. + /// + /// Commit is non-terminal and does not create presentation re-establishment eligibility. + pub fn record_observed_navigation_committed( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<(), BrowserSessionError> { + self.session.mark_observed_navigation_committed(authority) + } + + /// Close one current navigation through a complete positive browser observation. + pub fn record_observed_navigation_settled( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<(), BrowserSessionError> { + self.session.close_observed_navigation(authority) + } + + /// Close one current navigation through an explicit typed negative browser outcome. + pub fn record_observed_navigation_terminated( + &mut self, + authority: &NavigationSettlementAuthority, + outcome: NavigationTerminationOutcome, + ) -> Result<(), BrowserSessionError> { + match outcome { + NavigationTerminationOutcome::Aborted | NavigationTerminationOutcome::Failed => { + self.session.close_observed_navigation(authority) + } + } + } + + /// Close one current navigation's liveness boundary when download start is observed. + /// + /// This does not claim file completion, persistence, scanning, egress authorization, or any + /// download-security outcome; those remain outside Browser Session. + pub fn record_observed_navigation_download_started( + &mut self, + authority: &NavigationSettlementAuthority, + ) -> Result<(), BrowserSessionError> { + self.session.close_observed_navigation(authority) + } + + /// Explicitly mint the next presentation authority after one qualified navigation closure. + /// + /// This is the only navigation path that consumes a new presentation epoch. The opportunity is + /// single-use; a newer navigation start supersedes any unused opportunity for the same context. + pub fn reestablish_presentation_authority( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result { + self.session + .reestablish_presentation_authority_for_context(browsing_context) + } + + /// Destroy the exact owned disposable boundary through current presentation authority. pub fn destroy_disposable_context( &mut self, authority: &PresentationMutationAuthority, @@ -1044,6 +1320,19 @@ impl BoundBrowserSession

{ .destroy_disposable_context_with_port(authority, &mut self.port) } + /// Destroy an owned disposable boundary through structural lifecycle custody. + /// + /// This cleanup path remains available while navigation has revoked presentation mutation, but it + /// can select only a context currently owned by this exact bound aggregate. It never re-establishes + /// presentation authority and consumes the retained browser-issued lifecycle handle on success. + pub fn destroy_owned_disposable_context( + &mut self, + browsing_context: BrowsingContextId, + ) -> Result<(), BrowserSessionError> { + self.session + .destroy_owned_disposable_context_with_port(browsing_context, &mut self.port) + } + /// Record browser transport loss without exposing mutable lifecycle-port access. pub fn record_transport_loss(&mut self) -> bool { self.session.record_transport_loss() @@ -1067,8 +1356,9 @@ impl BoundBrowserSession

{ /// Execute one adapter-defined operation through the exact consumed adapter after authority validation. /// /// Browser Session validates session incarnation, isolation identity, browsing-context identity, - /// and epoch before the adapter receives the operation. Stale or foreign authority therefore fails - /// before adapter I/O, while the adapter-specific operation vocabulary remains outside this domain. + /// current presentation state, and epoch before the adapter receives the operation. Stale or foreign + /// authority therefore fails before adapter I/O, while the adapter-specific operation vocabulary + /// remains outside this domain. pub fn execute_authorized_context_operation( &mut self, authority: &PresentationMutationAuthority, @@ -1103,6 +1393,14 @@ fn reserve_epoch(next_epoch: &mut u64) -> Result Result { + let generation = *next_generation; + *next_generation = next_generation + .checked_add(1) + .ok_or(BrowserSessionError::EpochExhausted)?; + Ok(generation) +} + fn allocate_incarnation( counter: &AtomicU64, ) -> Result { @@ -1730,6 +2028,179 @@ mod tests { assert_eq!(bound.end(), Err(BrowserSessionError::SessionNotActive)); } + #[test] + fn navigation_state_machine_separates_presentation_from_lifecycle_cleanup() { + let mut bound = session(13).bind_lifecycle_port(TestPort::new(130, "isolation-130")); + let initial = bound.create_disposable_context().expect("owned context"); + let calls_before_navigation = bound.port.destroy_calls; + let pending = bound + .record_observed_navigation( + initial.incarnation(), + initial.browsing_context(), + initial.context_epoch(), + ) + .expect("navigation start"); + assert_eq!( + bound.presentation_authority(initial.browsing_context()), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!( + bound.destroy_disposable_context(&initial), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!(bound.port.destroy_calls, calls_before_navigation); + bound + .record_observed_navigation_committed(&pending) + .expect("first commit progress"); + assert_eq!( + bound.record_observed_navigation_committed(&pending), + Err(BrowserSessionError::AuthorityMismatch) + ); + bound + .record_observed_navigation_settled(&pending) + .expect("positive terminal"); + assert_eq!( + bound.record_observed_navigation_download_started(&pending), + Err(BrowserSessionError::AuthorityMismatch) + ); + let fresh = bound + .reestablish_presentation_authority(initial.browsing_context()) + .expect("explicit re-establishment"); + assert_eq!(fresh.context_epoch().value(), initial.context_epoch().value() + 1); + assert_eq!( + bound.reestablish_presentation_authority(initial.browsing_context()), + Err(BrowserSessionError::AuthorityMismatch) + ); + bound + .destroy_owned_disposable_context(initial.browsing_context()) + .expect("bound lifecycle owner cleanup"); + assert_eq!(bound.port.destroy_calls, calls_before_navigation + 1); + assert_eq!( + bound.destroy_owned_disposable_context(initial.browsing_context()), + Err(BrowserSessionError::ContextNotOwned) + ); + } + + #[test] + fn navigation_generation_rejects_foreign_stale_and_invalid_evidence_without_epoch_spend() { + let handles = vec![ + DisposableContextHandle::new(isolation_id("isolation-140"), context_id(140)), + DisposableContextHandle::new(isolation_id("isolation-141"), context_id(141)), + ]; + let mut bound = session(14).bind_lifecycle_port(TestPort::with_handles(handles)); + let first = bound.create_disposable_context().expect("first context"); + let second = bound.create_disposable_context().expect("second context"); + assert_eq!( + bound.record_observed_navigation( + BrowserSessionIncarnation(first.incarnation().value() + 1), + first.browsing_context(), + first.context_epoch(), + ), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!( + bound.record_observed_navigation( + first.incarnation(), + context_id(999), + first.context_epoch(), + ), + Err(BrowserSessionError::ContextNotOwned) + ); + assert_eq!( + bound.record_observed_navigation( + first.incarnation(), + first.browsing_context(), + second.context_epoch(), + ), + Err(BrowserSessionError::AuthorityMismatch) + ); + let old_pending = bound + .record_observed_navigation( + first.incarnation(), + first.browsing_context(), + first.context_epoch(), + ) + .expect("first pending"); + let current_pending = bound + .record_observed_navigation( + first.incarnation(), + first.browsing_context(), + first.context_epoch(), + ) + .expect("superseding pending"); + assert_eq!( + bound.record_observed_navigation_settled(&old_pending), + Err(BrowserSessionError::AuthorityMismatch) + ); + bound + .record_observed_navigation_terminated( + ¤t_pending, + NavigationTerminationOutcome::Aborted, + ) + .expect("current negative terminal"); + let fresh = bound + .reestablish_presentation_authority(first.browsing_context()) + .expect("fresh authority"); + assert_eq!(fresh.context_epoch().value(), second.context_epoch().value() + 1); + assert_eq!( + bound.record_observed_navigation_terminated( + ¤t_pending, + NavigationTerminationOutcome::Failed, + ), + Err(BrowserSessionError::AuthorityMismatch) + ); + assert_eq!(bound.presentation_authority(first.browsing_context()), Ok(fresh)); + } + + #[test] + fn navigation_exhaustion_and_cleanup_failure_fail_closed_without_hidden_mutation() { + let mut bound = session(15).bind_lifecycle_port(TestPort::new(150, "isolation-150")); + let initial = bound.create_disposable_context().expect("owned context"); + bound.session.next_navigation_generation = u64::MAX; + assert_eq!( + bound.record_observed_navigation( + initial.incarnation(), + initial.browsing_context(), + initial.context_epoch(), + ), + Err(BrowserSessionError::EpochExhausted) + ); + assert_eq!( + bound.presentation_authority(initial.browsing_context()), + Ok(initial.clone()) + ); + bound.session.next_navigation_generation = 1; + let pending = bound + .record_observed_navigation( + initial.incarnation(), + initial.browsing_context(), + initial.context_epoch(), + ) + .expect("pending navigation"); + bound + .record_observed_navigation_download_started(&pending) + .expect("download liveness closure"); + bound.session.next_epoch = u64::MAX; + assert_eq!( + bound.reestablish_presentation_authority(initial.browsing_context()), + Err(BrowserSessionError::EpochExhausted) + ); + bound.port.fail_destroy = true; + assert_eq!( + bound.destroy_owned_disposable_context(initial.browsing_context()), + Err(BrowserSessionError::ContextDestructionFailed) + ); + assert_eq!(bound.browser_session().state(), BrowserSessionState::RecoveryRequired); + assert_eq!( + bound.destroy_owned_disposable_context(initial.browsing_context()), + Err(BrowserSessionError::SessionNotActive) + ); + assert_eq!( + bound.record_observed_navigation_settled(&pending), + Err(BrowserSessionError::SessionNotActive) + ); + } + #[test] fn incarnation_allocator_fails_closed_before_wrap() { let counter = AtomicU64::new(u64::MAX); From 33c6d8865c9c3063a2c4f6c6e4b901c1a725b294 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 05:14:28 +0900 Subject: [PATCH 097/632] fix(browser-session): avoid production panic paths in navigation state --- .../src/browser_session.rs | 62 +++++++------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 6645f938c..d9a51ddeb 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -742,27 +742,19 @@ impl BrowserSession { browsing_context: BrowsingContextId, ) -> Result { self.require_active()?; - { - let record = self - .contexts - .get(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - if record.presentation_navigation != PresentationNavigationState::Established { - return Err(BrowserSessionError::AuthorityMismatch); - } - } - let next = reserve_epoch(&mut self.next_epoch)?; - let browser_session = self.id; - let incarnation = self.incarnation; let record = self .contexts .get_mut(&browsing_context) - .expect("validated owned context remains present under exclusive aggregate access"); + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.presentation_navigation != PresentationNavigationState::Established { + return Err(BrowserSessionError::AuthorityMismatch); + } + let next = reserve_epoch(&mut self.next_epoch)?; record.epoch = next; Ok(Self::authority_for( - browser_session, - incarnation, + self.id, + self.incarnation, &record.handle, next, )) @@ -936,22 +928,16 @@ impl BrowserSession { if incarnation != self.incarnation { return Err(BrowserSessionError::AuthorityMismatch); } - { - let record = self - .contexts - .get(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - if record.epoch != context_epoch { - return Err(BrowserSessionError::AuthorityMismatch); - } - } - let navigation_generation = - reserve_navigation_generation(&mut self.next_navigation_generation)?; let record = self .contexts .get_mut(&browsing_context) - .expect("validated owned context remains present under exclusive aggregate access"); + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.epoch != context_epoch { + return Err(BrowserSessionError::AuthorityMismatch); + } + let navigation_generation = + reserve_navigation_generation(&mut self.next_navigation_generation)?; record.presentation_navigation = PresentationNavigationState::Pending { navigation_generation, committed: false, @@ -1027,21 +1013,15 @@ impl BrowserSession { browsing_context: BrowsingContextId, ) -> Result { self.require_active()?; - { - let record = self - .contexts - .get(&browsing_context) - .filter(|record| record.state == OwnedContextState::Active) - .ok_or(BrowserSessionError::ContextNotOwned)?; - if record.presentation_navigation != PresentationNavigationState::Eligible { - return Err(BrowserSessionError::AuthorityMismatch); - } - } - let next = reserve_epoch(&mut self.next_epoch)?; let record = self .contexts .get_mut(&browsing_context) - .expect("validated owned context remains present under exclusive aggregate access"); + .filter(|record| record.state == OwnedContextState::Active) + .ok_or(BrowserSessionError::ContextNotOwned)?; + if record.presentation_navigation != PresentationNavigationState::Eligible { + return Err(BrowserSessionError::AuthorityMismatch); + } + let next = reserve_epoch(&mut self.next_epoch)?; record.epoch = next; record.presentation_navigation = PresentationNavigationState::Established; Ok(Self::authority_for( From 3ac548bfd68cd84d3ce9df8bd42d26bc31477936 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:04:13 +0900 Subject: [PATCH 098/632] test(browser-session): pin navigation owner surface --- ...ssion_navigation_owner_surface_contract.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_browser_session_navigation_owner_surface_contract.py diff --git a/tests/test_browser_session_navigation_owner_surface_contract.py b/tests/test_browser_session_navigation_owner_surface_contract.py new file mode 100644 index 000000000..cafb93f4c --- /dev/null +++ b/tests/test_browser_session_navigation_owner_surface_contract.py @@ -0,0 +1,71 @@ +"""Owner-side repository contracts for Browser Session navigation authority.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_PATH = ROOT / "crates/originweave-browser-session/src/browser_session.rs" + + +class BrowserSessionNavigationOwnerSurfaceContractTests(unittest.TestCase): + """Pin the navigation surface in the production owner, independent of stacked acceptance PRs.""" + + def setUp(self) -> None: + """Read the Browser Session owner source once for each contract assertion.""" + + self.source = SOURCE_PATH.read_text(encoding="utf-8") + + def test_navigation_capabilities_are_owner_issued_and_not_raw_id_constructible(self) -> None: + """Navigation settlement must remain an opaque Browser Session capability.""" + + for symbol in ( + "pub struct NavigationSettlementAuthority", + "pub enum NavigationTerminationOutcome", + "pub fn record_observed_navigation(", + "pub fn record_observed_navigation_committed(", + "pub fn record_observed_navigation_settled(", + "pub fn record_observed_navigation_terminated(", + "pub fn record_observed_navigation_download_started(", + "pub fn reestablish_presentation_authority(", + "pub fn destroy_owned_disposable_context(", + ): + self.assertIn(symbol, self.source) + + witness_declaration = self.source.split( + "pub struct NavigationSettlementAuthority", 1 + )[1].split("pub enum NavigationTerminationOutcome", 1)[0] + self.assertNotIn("pub fn new", witness_declaration) + self.assertNotIn("pub const fn new", witness_declaration) + self.assertIn("navigation_generation: u64", witness_declaration) + self.assertIn("context_epoch: BrowserContextEpoch", witness_declaration) + + def test_navigation_state_machine_keeps_liveness_separate_from_presentation_epoch(self) -> None: + """Navigation generations close independently before explicit presentation re-establishment.""" + + for token in ( + "PresentationNavigationState::Pending", + "PresentationNavigationState::Eligible", + "reserve_navigation_generation", + "mark_observed_navigation_committed", + "close_observed_navigation", + "reestablish_presentation_authority_for_context", + "reserve_epoch(&mut self.next_epoch)", + ): + self.assertIn(token, self.source) + + bound_surface = self.source.split( + "impl BoundBrowserSession

", 1 + )[1].split("impl BoundBrowserSession

", 1)[0] + self.assertIn( + "self.session.begin_observed_navigation(incarnation, browsing_context, context_epoch)", + " ".join(bound_surface.split()), + ) + self.assertGreaterEqual(bound_surface.count("self.session.close_observed_navigation(authority)"), 3) + self.assertIn("NavigationTerminationOutcome::Aborted", bound_surface) + self.assertIn("NavigationTerminationOutcome::Failed", bound_surface) + + +if __name__ == "__main__": + unittest.main() From 322f598660814d7873ca02590f83cd2999a716e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:04:32 +0900 Subject: [PATCH 099/632] test(browser-session): make navigation contract whitespace-robust --- ...st_browser_session_navigation_owner_surface_contract.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_navigation_owner_surface_contract.py b/tests/test_browser_session_navigation_owner_surface_contract.py index cafb93f4c..f2ada67a4 100644 --- a/tests/test_browser_session_navigation_owner_surface_contract.py +++ b/tests/test_browser_session_navigation_owner_surface_contract.py @@ -58,11 +58,12 @@ def test_navigation_state_machine_keeps_liveness_separate_from_presentation_epoc bound_surface = self.source.split( "impl BoundBrowserSession

", 1 )[1].split("impl BoundBrowserSession

", 1)[0] + compact_surface = "".join(bound_surface.split()) self.assertIn( - "self.session.begin_observed_navigation(incarnation, browsing_context, context_epoch)", - " ".join(bound_surface.split()), + "self.session.begin_observed_navigation(incarnation,browsing_context,context_epoch)", + compact_surface, ) - self.assertGreaterEqual(bound_surface.count("self.session.close_observed_navigation(authority)"), 3) + self.assertGreaterEqual(compact_surface.count("self.session.close_observed_navigation(authority)"), 3) self.assertIn("NavigationTerminationOutcome::Aborted", bound_surface) self.assertIn("NavigationTerminationOutcome::Failed", bound_surface) From 314251ca47a566ae99701213244536e2880820e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:08:38 +0900 Subject: [PATCH 100/632] test(browser-session): harden navigation owner contract --- ...ssion_navigation_owner_surface_contract.py | 87 ++++++++++++------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/tests/test_browser_session_navigation_owner_surface_contract.py b/tests/test_browser_session_navigation_owner_surface_contract.py index f2ada67a4..89408c1ea 100644 --- a/tests/test_browser_session_navigation_owner_surface_contract.py +++ b/tests/test_browser_session_navigation_owner_surface_contract.py @@ -3,6 +3,7 @@ from __future__ import annotations import pathlib +import re import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -17,55 +18,77 @@ def setUp(self) -> None: self.source = SOURCE_PATH.read_text(encoding="utf-8") + def assert_source_pattern(self, pattern: str) -> None: + """Require a structural source token without depending on rustfmt whitespace.""" + + self.assertRegex(self.source, re.compile(pattern, re.MULTILINE | re.DOTALL)) + def test_navigation_capabilities_are_owner_issued_and_not_raw_id_constructible(self) -> None: """Navigation settlement must remain an opaque Browser Session capability.""" - for symbol in ( - "pub struct NavigationSettlementAuthority", - "pub enum NavigationTerminationOutcome", - "pub fn record_observed_navigation(", - "pub fn record_observed_navigation_committed(", - "pub fn record_observed_navigation_settled(", - "pub fn record_observed_navigation_terminated(", - "pub fn record_observed_navigation_download_started(", - "pub fn reestablish_presentation_authority(", - "pub fn destroy_owned_disposable_context(", + for pattern in ( + r"pub\s+struct\s+NavigationSettlementAuthority\s*\{", + r"pub\s+enum\s+NavigationTerminationOutcome\s*\{", + r"pub\s+fn\s+record_observed_navigation\s*\(", + r"pub\s+fn\s+record_observed_navigation_committed\s*\(", + r"pub\s+fn\s+record_observed_navigation_settled\s*\(", + r"pub\s+fn\s+record_observed_navigation_terminated\s*\(", + r"pub\s+fn\s+record_observed_navigation_download_started\s*\(", + r"pub\s+fn\s+reestablish_presentation_authority\s*\(", + r"pub\s+fn\s+destroy_owned_disposable_context\s*\(", ): - self.assertIn(symbol, self.source) + self.assert_source_pattern(pattern) - witness_declaration = self.source.split( - "pub struct NavigationSettlementAuthority", 1 - )[1].split("pub enum NavigationTerminationOutcome", 1)[0] - self.assertNotIn("pub fn new", witness_declaration) - self.assertNotIn("pub const fn new", witness_declaration) - self.assertIn("navigation_generation: u64", witness_declaration) - self.assertIn("context_epoch: BrowserContextEpoch", witness_declaration) + witness_match = re.search( + r"pub\s+struct\s+NavigationSettlementAuthority\s*\{(?P.*?)\n\}", + self.source, + flags=re.DOTALL, + ) + self.assertIsNotNone(witness_match) + witness_body = witness_match.group("body") if witness_match else "" + self.assertNotRegex(witness_body, r"(?m)^\s*pub(?:\([^)]*\))?\s+") + self.assertRegex(witness_body, r"navigation_generation\s*:\s*u64") + self.assertRegex(witness_body, r"context_epoch\s*:\s*BrowserContextEpoch") + self.assertNotRegex( + self.source, + r"impl\s+NavigationSettlementAuthority\s*\{", + "The settlement witness is intentionally opaque and has no public inherent mint/read surface.", + ) def test_navigation_state_machine_keeps_liveness_separate_from_presentation_epoch(self) -> None: """Navigation generations close independently before explicit presentation re-establishment.""" - for token in ( - "PresentationNavigationState::Pending", - "PresentationNavigationState::Eligible", - "reserve_navigation_generation", - "mark_observed_navigation_committed", - "close_observed_navigation", - "reestablish_presentation_authority_for_context", - "reserve_epoch(&mut self.next_epoch)", + for pattern in ( + r"PresentationNavigationState\s*::\s*Pending", + r"PresentationNavigationState\s*::\s*Eligible", + r"reserve_navigation_generation\s*\(", + r"mark_observed_navigation_committed\s*\(", + r"close_observed_navigation\s*\(", + r"reestablish_presentation_authority_for_context\s*\(", + r"reserve_epoch\s*\(\s*&mut\s+self\.next_epoch\s*\)", ): - self.assertIn(token, self.source) + self.assert_source_pattern(pattern) - bound_surface = self.source.split( - "impl BoundBrowserSession

", 1 - )[1].split("impl BoundBrowserSession

", 1)[0] + bound_start = re.search( + r"impl\s*<\s*P\s*:\s*DisposableContextPort\s*>\s*BoundBrowserSession\s*<\s*P\s*>\s*\{", + self.source, + ) + operation_start = re.search( + r"impl\s*<\s*P\s*:\s*AuthorizedContextOperationPort\s*>\s*BoundBrowserSession\s*<\s*P\s*>\s*\{", + self.source, + ) + self.assertIsNotNone(bound_start) + self.assertIsNotNone(operation_start) + self.assertLess(bound_start.start(), operation_start.start()) + bound_surface = self.source[bound_start.start() : operation_start.start()] compact_surface = "".join(bound_surface.split()) self.assertIn( "self.session.begin_observed_navigation(incarnation,browsing_context,context_epoch)", compact_surface, ) self.assertGreaterEqual(compact_surface.count("self.session.close_observed_navigation(authority)"), 3) - self.assertIn("NavigationTerminationOutcome::Aborted", bound_surface) - self.assertIn("NavigationTerminationOutcome::Failed", bound_surface) + self.assertRegex(bound_surface, r"NavigationTerminationOutcome\s*::\s*Aborted") + self.assertRegex(bound_surface, r"NavigationTerminationOutcome\s*::\s*Failed") if __name__ == "__main__": From 0c6d7d19d32724a5f3816ae8f27d696c3444b00e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:12:03 +0900 Subject: [PATCH 101/632] docs(browser-session): currentize recovery ADR for navigation authority --- ...sion-recovery-custody-and-hot-ownership.md | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index f0990efcf..4f72aa752 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -28,9 +28,9 @@ These questions are Browser Session domain concerns. WebDriver BiDi pending/acce ## Assumptions and authority boundaries -Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. +Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, navigation-generation custody, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. -WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. +WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, protocol event replay qualification, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, and recovery evidence do not grant Browser Session command authority. @@ -76,7 +76,11 @@ Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch cou 12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. 13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. 14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. -15. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction post-conditions are independently evidenced. +15. Browser-observed navigation is admitted only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` ownership generation. Admission revokes presentation authority with zero adapter I/O and mints an opaque `NavigationSettlementAuthority` using a separate monotonic navigation generation rather than spending a presentation epoch. +16. Commit progress is non-terminal. Positive settlement, typed `Aborted`/`Failed`, and download start share one exactly-once terminal closure. A newer admitted navigation supersedes an older pending witness; stale or superseded witnesses fail closed without changing a newer generation. +17. Terminal closure creates one context-local opportunity for explicit `reestablish_presentation_authority`; the first successful re-establishment consumes the next presentation epoch and returns the context to `Established`. Generic epoch advancement cannot bypass a navigation-invalidated state. +18. Presentation authority and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup, while the exact bound lifecycle owner may still destroy its retained disposable context through `destroy_owned_disposable_context` without reopening presentation authority. +19. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction/navigation post-conditions are independently evidenced. ## Consequences @@ -84,7 +88,13 @@ The Browser Session aggregate now has two linear owner forms: ordinary `BoundBro Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. -A child navigation implementation must treat ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. +The active #317 production lineage now treats ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. Navigation liveness uses its own monotonic generation; presentation epochs advance only when explicit re-establishment succeeds. + +## Navigation authority interaction + +`record_observed_navigation` is a protocol-agnostic Browser Session transition. It accepts already-qualified adapter evidence only after aggregate trust, exact incarnation, exact live ownership, and current context epoch match. It does not consume a WebDriver BiDi navigation id as policy authority. #316 remains responsible for mapping protocol events and replay qualification into this domain transition. + +The returned `NavigationSettlementAuthority` has private fields and no caller constructor. `record_observed_navigation_committed` records first commit progress but does not make presentation re-establishment eligible. `record_observed_navigation_settled`, `record_observed_navigation_terminated`, and `record_observed_navigation_download_started` share the same current-witness terminal closure. `reestablish_presentation_authority` is explicit and single-use. `RecoveryRequired`, `TransportLost`, `Ended`, proven context destruction, stale generations, and superseded witnesses all dominate terminal or re-establishment attempts before adapter I/O. ## Failure and degraded behavior @@ -92,7 +102,7 @@ If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, n A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. -Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation allocation is exhausted, allocation fails closed rather than reusing authority identity. +Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation/navigation-generation allocation is exhausted, allocation fails closed rather than reusing authority identity. ## Security / privacy / governance impact @@ -100,6 +110,8 @@ The recovery wrapper is a capability-reduction boundary. Untrusted page data, mo Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. +Navigation events are evidence, not deterministic policy authority. Browser Session creates and consumes its own opaque navigation witness only after exact current ownership validation. This prevents raw protocol ids, delayed events, sibling contexts, prior incarnations, or superseded generations from rewriting current presentation authority. + This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. Recovery evidence may identify remote browser boundaries but does not itself contain page content or create a new purpose for PII processing. ## Tests and acceptance evidence @@ -116,20 +128,22 @@ This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, - epochs are strictly monotonic - retained predecessor authority fails before lifecycle adapter I/O - destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. -- `tests/test_browser_session_lifecycle_contract.py` pins the recovery wrapper surface, hostile fixtures, ADR 0116, traceability, and UML doctoring. +- navigation owner tests cover revocation, current-witness commit/terminal ordering, supersession, cleanup separation, trust-state precedence, and presentation-epoch conservation. +- `tests/test_browser_session_lifecycle_contract.py` pins recovery wrapper surface, hostile fixtures, ADR 0116, traceability, and UML doctoring. +- `tests/test_browser_session_navigation_owner_surface_contract.py` pins the production-owner navigation API and its opaque witness/epoch-separation contract independently from stacked #318/#321 acceptance. These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. ## Migration and rollback -This active-PR change is additive at the ownership-type boundary but changes the internal retention model after proven destruction. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery authority from the new wrapper. +This active-PR change is additive at the ownership-type boundary but changes the internal retention and navigation-admission model. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery/navigation authority from adapter identifiers. -Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access or keeping record eviction without the stale-authority tests. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. +Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. ## Open follow-ups -- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation and remote-liveness semantics. -- #318/#321: production navigation ownership-generation/current-witness state plus same-raw-id ABA acceptance after this foundation receives exact-head verification. +- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics. +- #318/#321: executable acceptance/review successors for the implemented #317 navigation contract, including same-raw-id ABA and sibling-recreation matrices; they do not own a second production state machine. - Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. - Real Chromium proof of remote destruction, cleanup, navigation, interaction, and browser-observed post-conditions. - `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. @@ -137,7 +151,7 @@ Rollback before protected-main adoption is performed by reverting the whole reco ## Supersession / reversal conditions -Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. +Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. @@ -145,4 +159,4 @@ Changing the recovery owner or persistence architecture does not by itself requi Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ -Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file +Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html From ab13fd4ebd2fc29af48d19be2740b70ab61777b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:13:23 +0900 Subject: [PATCH 102/632] docs(browser-session): trace navigation authority owner --- .../browser-session-navigation-authority.md | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/traceability/browser-session-navigation-authority.md diff --git a/docs/traceability/browser-session-navigation-authority.md b/docs/traceability/browser-session-navigation-authority.md new file mode 100644 index 000000000..228c666f3 --- /dev/null +++ b/docs/traceability/browser-session-navigation-authority.md @@ -0,0 +1,85 @@ +# Browser Session navigation authority trace + +- Status: `IMPLEMENTED_ON_ACTIVE_PR` +- Owning bounded context: `originweave-browser-session` +- Production owner: #317 +- Acceptance successors: #318, #321 +- Protocol adapter / WebDriver BiDi correlation owner: #316 +- Governing proposals: ADR 0114, ADR 0116 +- Standards provenance: `WD-webdriver-bidi-20260909` + +## Domain boundary + +Browser Session owns deterministic navigation authority state for an already-owned browsing context. WebDriver BiDi navigation ids, remote context ids, protocol event order, replay qualification, transport liveness, and pending/accepted/quarantined adapter tuples remain #316 concerns. A raw protocol id or an LLM judgment cannot manufacture Browser Session authority. + +The active #317 production lineage implements this contract: + +```text +Active Browser Session ++ exact BrowserSessionIncarnation ++ exact live BrowsingContextId ++ exact current BrowserContextEpoch + | + | record_observed_navigation(...) + | zero adapter I/O; no presentation epoch spent + v +opaque NavigationSettlementAuthority ++ monotonic navigation_generation ++ presentation authority revoked + | + +-- first qualified commit -----------------------> Pending(committed=true) + | | + +-- positive complete observation -----------------+ + +-- typed Aborted / Failed ------------------------+--> Eligible + +-- download start --------------------------------+ | + | explicit, single-use + | reestablish_presentation_authority + | reserves next BrowserContextEpoch + v + Established +``` + +A newer qualified navigation start supersedes an older pending witness or unused eligibility for the same owned context without spending a presentation epoch. Old, foreign, cross-context, cross-incarnation, destroyed-generation, and superseded witnesses fail closed. + +## Invariants and source mapping + +| Invariant | Owner source / evidence | +|---|---| +| Navigation admission validates aggregate trust, incarnation, live ownership, and exact context epoch before mutation | `BrowserSession::begin_observed_navigation`; owner hostile tests | +| Admission performs zero adapter I/O and does not spend a presentation epoch | `BoundBrowserSession::record_observed_navigation`; presentation-epoch conservation tests | +| Browser Session, not adapter ids, mints terminal authority | private fields of `NavigationSettlementAuthority`; `tests/test_browser_session_navigation_owner_surface_contract.py` | +| Navigation generation is monotonic and independent from presentation epoch | `next_navigation_generation`; generation exhaustion tests | +| First commit is non-terminal | `mark_observed_navigation_committed`; #318 commit tests | +| Positive settlement, typed negative termination, and download start share one current-witness terminal closure | `close_observed_navigation`; #318 terminal/download tests | +| Duplicate or superseded commit/terminal evidence is non-authorizing | `current_pending_navigation_mut`; #318 replay tests | +| Terminal closure makes one context-local re-establishment opportunity | `PresentationNavigationState::Eligible`; #318 sibling/context-local tests | +| Re-establishment is explicit, single-use, and the only navigation transition that spends the next presentation epoch | `reestablish_presentation_authority_for_context`; #318 epoch/single-use tests | +| Navigation-invalidated presentation authority cannot authorize mutation or authority-based cleanup | `context_for_authority_mut`; cleanup hostile tests | +| Exact lifecycle owner may still clean up the retained context without reopening presentation authority | `destroy_owned_disposable_context`; cleanup hostile tests | +| RecoveryRequired and TransportLost dominate stale capability inspection | `require_active`; recovery/liveness tests | +| Proven destruction removes only that exact owned generation; sibling progress/eligibility survives | hot ownership map removal; #318/#321 sibling tests | +| Same raw context recreated later cannot inherit prior navigation or presentation authority | monotonic context epoch + session incarnation; #321 ABA matrix | + +## Adapter anti-corruption boundary + +#316 may correlate `browsingContext.navigationStarted`, `navigationCommitted`, completion/abort/failure/download observations, remote context destruction, and transport/session loss to Browser Session commands only after protocol qualification. It must retain protocol identifiers for addressability and provenance, not promote them into policy authority. + +The adapter must bind a remote candidate to the exact aggregate-issued create attempt before accepted state becomes authorizing. Navigation evidence for a rejected, quarantined, superseded, destroyed, or prior-incarnation tuple cannot rewrite Browser Session state. Silent rebind after transport/session loss is prohibited. + +Browser Session does not own Chromium/WebDriver transport truth. #316 does not own Browser Session policy truth. No source copy, cross-service SQL, mutable dependency, or duplicate state machine is permitted across this ACL. + +## Acceptance state + +#317 production semantics are implemented on an active PR, not protected-main shipment. #318 owns the broad navigation acceptance matrix and doctoring; #321 owns same-raw-id/sibling-recreation ABA acceptance. Those child PRs must remain ordinary non-force descendants of the exact #317 owner and do not replace owner-side contracts. + +Repository GREEN requires current exact-head repository contracts, canonical formatting, locked Rust tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, and applicable protected checks. Queued, skipped, predecessor, or runner-less jobs are not GREEN. + +Real-browser GREEN is separate. A protocol command acknowledgement is insufficient. Acceptance requires pinned Chromium/WebDriver BiDi evidence for navigation, policy-authorized interaction, browser/page-observed post-condition, reset, destruction/cleanup, crash/recovery behavior, and provenance on the current integrated head. + +## Standards trace + +The immutable W3C WebDriver BiDi Working Draft used for this active-PR contract is the 9 September 2026 publication: `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`. Protocol event vocabulary and user-context/browsing-context addressability come from that standard; OriginWeave's opaque authority and lifecycle invariants are internal domain controls and are not claimed as W3C requirements. + +## Release status + +Status remains `IMPLEMENTED_ON_ACTIVE_PR`. Do not promote it to Accepted, released, or buyer-complete until protected-main integration and immutable release evidence exist. #316 integration and real pinned-Chromium post-condition evidence remain open. From b82237dc0c7ae2462379ec6f1278c21a626e4050 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:13:39 +0900 Subject: [PATCH 103/632] docs(browser-session): model navigation authority states --- .../browser-session-navigation-authority.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/uml/browser-session-navigation-authority.md diff --git a/docs/uml/browser-session-navigation-authority.md b/docs/uml/browser-session-navigation-authority.md new file mode 100644 index 000000000..e66e466ea --- /dev/null +++ b/docs/uml/browser-session-navigation-authority.md @@ -0,0 +1,83 @@ +# Browser Session navigation authority UML + +Status: `IMPLEMENTED_ON_ACTIVE_PR` on #317. This diagram describes Browser Session domain state, not WebDriver BiDi adapter tuple state. + +```mermaid +stateDiagram-v2 + [*] --> Established: accepted owned context + + Established --> Pending: record_observed_navigation\nexact incarnation/context/epoch\nzero I/O, no presentation epoch + Pending --> Pending: first qualified commit\ncommitted = true + Pending --> Pending: newer navigation start\nsupersedes old witness\nnew navigation generation + Pending --> Eligible: positive settlement + Pending --> Eligible: Aborted / Failed + Pending --> Eligible: download start + Eligible --> Pending: newer navigation start\nsupersedes unused eligibility + Eligible --> Established: reestablish_presentation_authority\nreserve next BrowserContextEpoch + + Established --> Removed: proven lifecycle-owner destruction + Pending --> Removed: proven lifecycle-owner destruction + Eligible --> Removed: proven lifecycle-owner destruction + + Established --> RecoveryRequired: unproven destruction / ownership failure + Pending --> RecoveryRequired: unproven destruction / ownership failure + Eligible --> RecoveryRequired: unproven destruction / ownership failure + + Established --> TransportLost: transport loss + Pending --> TransportLost: transport loss + Eligible --> TransportLost: transport loss + + RecoveryRequired --> [*]: recovery-owner handoff / later canonical recovery + TransportLost --> [*]: recovery-owner handoff / later canonical recovery + Removed --> [*] +``` + +The `Pending` state is qualified by a private monotonic `navigation_generation` and a `committed` bit. `Eligible` is context-local and represents exactly one unused opportunity to re-establish presentation authority. Neither commit nor terminal closure advances `BrowserContextEpoch`; only successful explicit re-establishment does. + +```mermaid +sequenceDiagram + participant BiDi as #316 BiDi adapter + participant Session as BoundBrowserSession + participant Aggregate as BrowserSession + participant Port as Same consumed adapter + + BiDi->>Session: qualified navigationStarted(incarnation, context, epoch) + Session->>Aggregate: begin_observed_navigation(...) + Aggregate-->>Session: opaque NavigationSettlementAuthority + Note over Session,Aggregate: presentation authority revoked; zero adapter I/O + + BiDi->>Session: qualified navigationCommitted(witness) + Session->>Aggregate: mark_observed_navigation_committed(witness) + Aggregate-->>Session: commit progress only + + BiDi->>Session: complete | abort | fail | download(witness) + Session->>Aggregate: close_observed_navigation(witness) + Aggregate-->>Session: Eligible + + BiDi->>Session: reestablish_presentation_authority(witness) + Session->>Aggregate: validate current Eligible witness + Aggregate-->>Session: new PresentationMutationAuthority(next epoch) + + alt lifecycle cleanup is required while presentation is revoked + BiDi->>Session: destroy_owned_disposable_context(context) + Session->>Aggregate: validate exact lifecycle custody + Aggregate->>Port: opaque DisposableContextDestroyRequest(handle, epoch) + Port-->>Aggregate: destruction result + Aggregate-->>Session: remove exact owned generation or enter RecoveryRequired + end +``` + +## Capability boundary + +- `NavigationSettlementAuthority` is opaque and caller-unconstructible; raw BiDi navigation/context identifiers are evidence only. +- `PresentationMutationAuthority` becomes stale immediately when a qualified navigation is admitted. +- A stale presentation capability cannot be reused for mutation or authority-based cleanup. +- `destroy_owned_disposable_context` is a lifecycle-owner path, not a presentation-authority bypass. It acts only on the exact current owned generation through the already-consumed adapter. +- `RecoveryRequired`, `TransportLost`, ended sessions, destroyed generations, foreign incarnations, stale epochs, and superseded navigation generations fail closed before browser I/O. +- #316 owns protocol event qualification, correlation, replay handling, transport loss, and remote-liveness interpretation. It must not duplicate this Browser Session state machine. + +## Acceptance boundary + +#318 and #321 exercise the wider hostile matrix—cross-context/cross-incarnation replay, supersession, terminal single assignment, download ordering, sibling destruction, same-raw-id recreation, and cumulative stale-capability rejection. They are acceptance successors, not alternate production owners. + +Real Chromium success additionally requires browser-observed post-conditions and cleanup evidence. A command ACK alone does not transition this UML to buyer-visible GREEN. From a2d739f5887067c3d4a26592f4fb10c7147818c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 06:20:46 +0900 Subject: [PATCH 104/632] test(browser-session): reject every inherent navigation witness impl --- .../test_browser_session_navigation_owner_surface_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_navigation_owner_surface_contract.py b/tests/test_browser_session_navigation_owner_surface_contract.py index 89408c1ea..c6bf75258 100644 --- a/tests/test_browser_session_navigation_owner_surface_contract.py +++ b/tests/test_browser_session_navigation_owner_surface_contract.py @@ -51,8 +51,8 @@ def test_navigation_capabilities_are_owner_issued_and_not_raw_id_constructible(s self.assertRegex(witness_body, r"context_epoch\s*:\s*BrowserContextEpoch") self.assertNotRegex( self.source, - r"impl\s+NavigationSettlementAuthority\s*\{", - "The settlement witness is intentionally opaque and has no public inherent mint/read surface.", + r"impl\s+NavigationSettlementAuthority\b", + "The settlement witness is intentionally opaque and has no inherent mint/read surface.", ) def test_navigation_state_machine_keeps_liveness_separate_from_presentation_epoch(self) -> None: From 8a6b7ad0bb4cdb85d8f93be4ec85623edca61663 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 07:03:54 +0900 Subject: [PATCH 105/632] test(browser-session): close settlement witness trait-mint paths --- ...er_session_navigation_owner_surface_contract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_browser_session_navigation_owner_surface_contract.py b/tests/test_browser_session_navigation_owner_surface_contract.py index c6bf75258..d06002c9f 100644 --- a/tests/test_browser_session_navigation_owner_surface_contract.py +++ b/tests/test_browser_session_navigation_owner_surface_contract.py @@ -49,6 +49,20 @@ def test_navigation_capabilities_are_owner_issued_and_not_raw_id_constructible(s self.assertNotRegex(witness_body, r"(?m)^\s*pub(?:\([^)]*\))?\s+") self.assertRegex(witness_body, r"navigation_generation\s*:\s*u64") self.assertRegex(witness_body, r"context_epoch\s*:\s*BrowserContextEpoch") + self.assertNotRegex( + self.source, + r"#\s*\[\s*derive\s*\([^\]]*\bDefault\b[^\]]*\)\s*\]" + r"(?:(?:\s*#\s*\[[^\]]*\])|\s)*" + r"pub\s+struct\s+NavigationSettlementAuthority\b", + "Default would let raw callers fabricate a settlement witness", + ) + self.assertNotRegex( + self.source, + r"\bimpl(?:\s*<[^{};]*>)?\s+" + r"(?:Default|From\s*<[^{};]+>|TryFrom\s*<[^{};]+>)\s+for\s+" + r"NavigationSettlementAuthority\b", + "conversion/default traits must not expose a caller-mintable witness path", + ) self.assertNotRegex( self.source, r"impl\s+NavigationSettlementAuthority\b", From e27ac95d1ea805ead51ac5e5653e8f92827a311e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 08:10:22 +0900 Subject: [PATCH 106/632] test(browser-session): require same-adapter recovery operation boundary --- .../tests/recovery_same_adapter_operation.rs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs diff --git a/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs new file mode 100644 index 000000000..9c759fe8b --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs @@ -0,0 +1,188 @@ +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, RecoveryContextOperationError, RecoveryContextOperationPort, + RecoveryContextOperationRequest, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RecoveryOperation { + ReconcileExactEvidence, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RecoveryOperationFailure { + BackendUnavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct RecoveryObservation { + browser_session: BrowserSessionId, + incarnation: u64, + state: BrowserSessionState, + recovery_evidence: Vec, + create_evidence_count: usize, + operation: RecoveryOperation, +} + +struct RecoveryPort { + handle: DisposableContextHandle, + fail_destroy: bool, + fail_recovery: Rc>, + recovery_calls: Rc>, + observations: Rc>>, +} + +impl DisposableContextPort for RecoveryPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + Ok(self.handle.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + if self.fail_destroy { + Err(DisposableContextDestroyError::DestroyFailed) + } else { + Ok(()) + } + } +} + +impl RecoveryContextOperationPort for RecoveryPort { + type Operation = RecoveryOperation; + type Output = (); + type Error = RecoveryOperationFailure; + + fn execute_recovery_context_operation( + &mut self, + request: &RecoveryContextOperationRequest, + ) -> Result { + self.recovery_calls.set(self.recovery_calls.get() + 1); + self.observations.borrow_mut().push(RecoveryObservation { + browser_session: request.browser_session(), + incarnation: request.incarnation().value(), + state: request.state(), + recovery_evidence: request.recovery_evidence().to_vec(), + create_evidence_count: request.create_attempt_recovery_evidence().len(), + operation: *request.operation(), + }); + if self.fail_recovery.get() { + Err(RecoveryOperationFailure::BackendUnavailable) + } else { + Ok(()) + } + } +} + +fn isolation(value: &str) -> Result { + DisposableIsolationId::parse(value).map_err(|_| "fixture isolation must be representable") +} + +fn context(value: u64) -> Result { + BrowsingContextId::new(value).map_err(|_| "fixture browsing context must be valid") +} + +fn session(value: u64) -> Result { + BrowserSessionId::new(value).map_err(|_| "fixture session must be valid") +} + +#[test] +fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter( +) -> Result<(), &'static str> { + let fail_recovery = Rc::new(Cell::new(true)); + let recovery_calls = Rc::new(Cell::new(0)); + let observations = Rc::new(RefCell::new(Vec::new())); + let expected_session = session(7_901)?; + let expected_handle = DisposableContextHandle::new( + isolation("same-adapter-recovery-user-context")?, + context(79_010)?, + ); + let port = RecoveryPort { + handle: expected_handle.clone(), + fail_destroy: true, + fail_recovery: Rc::clone(&fail_recovery), + recovery_calls: Rc::clone(&recovery_calls), + observations: Rc::clone(&observations), + }; + let mut bound = BrowserSession::start(expected_session) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture create must succeed")?; + let expected_incarnation = bound.browser_session().incarnation(); + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + let expected_recovery_evidence = bound.browser_session().recovery_evidence().to_vec(); + assert_eq!( + expected_recovery_evidence, + vec![BrowserSessionRecoveryEvidence::UnprovenDestruction { + context: expected_handle, + context_epoch: authority.context_epoch(), + }] + ); + + let mut recovery = bound + .into_recovery() + .map_err(|_| "RecoveryRequired must enter recovery custody")?; + + assert_eq!( + recovery.execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence), + Err(RecoveryContextOperationError::Adapter( + RecoveryOperationFailure::BackendUnavailable + )) + ); + assert_eq!(recovery_calls.get(), 1); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence(), expected_recovery_evidence); + + let first = observations.borrow(); + assert_eq!(first.len(), 1); + assert_eq!(first[0].browser_session, expected_session); + assert_eq!(first[0].incarnation, expected_incarnation.value()); + assert_eq!(first[0].state, BrowserSessionState::RecoveryRequired); + assert_eq!(first[0].recovery_evidence, expected_recovery_evidence); + assert_eq!(first[0].create_evidence_count, 0); + assert_eq!( + first[0].operation, + RecoveryOperation::ReconcileExactEvidence + ); + drop(first); + + fail_recovery.set(false); + recovery + .execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence) + .map_err(|_| "purpose-bounded recovery operation must reach the retained adapter")?; + assert_eq!(recovery_calls.get(), 2); + assert_eq!( + recovery.state(), + BrowserSessionState::RecoveryRequired, + "generic recovery adapter success is not itself destruction or reconciliation proof" + ); + assert_eq!( + recovery.recovery_evidence(), + expected_recovery_evidence, + "recovery operation dispatch must not erase unresolved ownership evidence" + ); + Ok(()) +} From 9dcf8ab7487e9c728582ea05503f96abec12f460 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:02:50 +0900 Subject: [PATCH 107/632] fix(browser-session): retain adapter for recovery dispatch --- crates/originweave-browser-session/src/lib.rs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index a008f97b4..f9e1d6509 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -21,8 +21,27 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] -mod browser_session; +mod browser_session { + include!("browser_session.rs"); + + impl

BoundBrowserSession

{ + /// Route one crate-internal recovery operation through the exact retained adapter. + /// + /// The callback is deliberately crate-private: public callers never receive the raw adapter, + /// while recovery custody can still bind one purpose-bounded request to the same adapter + /// instance that performed lifecycle creation and destruction. + pub(crate) fn dispatch_recovery_operation( + &mut self, + dispatch: impl FnOnce(&BrowserSession, &mut P) -> R, + ) -> R { + dispatch(&self.session, &mut self.port) + } + } +} mod recovery; pub use browser_session::*; -pub use recovery::BoundBrowserSessionRecovery; +pub use recovery::{ + BoundBrowserSessionRecovery, RecoveryContextOperationError, RecoveryContextOperationPort, + RecoveryContextOperationRequest, +}; From e4f4cdff0c75431b16ca3583e0397c587986a7bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:03:14 +0900 Subject: [PATCH 108/632] fix(browser-session): implement bounded recovery operation --- .../src/recovery.rs | 122 +++++++++++++++++- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index cdcc8cf5a..8a1a5b392 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -1,15 +1,99 @@ +use originweave_core::BrowserSessionId; + use crate::browser_session::{ - BoundBrowserSession, BrowserSessionRecoveryEvidence, BrowserSessionState, - DisposableContextCreateRecoveryEvidence, DisposableContextPort, + BoundBrowserSession, BrowserSessionIncarnation, BrowserSessionRecoveryEvidence, + BrowserSessionState, DisposableContextCreateRecoveryEvidence, DisposableContextPort, }; +/// Opaque recovery-custody request for one purpose-bounded adapter operation. +/// +/// Construction is private to [`BoundBrowserSessionRecovery`]. The request snapshots the exact +/// Browser Session identity, incarnation, unresolved lifecycle state, and both non-authorizing +/// recovery-evidence ledgers immediately before adapter I/O. None of these fields independently grant +/// ordinary creation, presentation mutation, or destruction authority. +pub struct RecoveryContextOperationRequest { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + state: BrowserSessionState, + recovery_evidence: Vec, + create_attempt_recovery_evidence: Vec, + operation: O, +} + +impl RecoveryContextOperationRequest { + /// Return the exact browser-session transport identity under recovery custody. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the exact process-local Browser Session incarnation under recovery custody. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the unresolved Browser Session lifecycle state captured before adapter I/O. + #[must_use] + pub const fn state(&self) -> BrowserSessionState { + self.state + } + + /// Return the exact non-authorizing remote-ownership evidence captured before adapter I/O. + #[must_use] + pub fn recovery_evidence(&self) -> &[BrowserSessionRecoveryEvidence] { + &self.recovery_evidence + } + + /// Return exact create-attempt recovery provenance captured before adapter I/O. + #[must_use] + pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { + &self.create_attempt_recovery_evidence + } + + /// Return the adapter-defined purpose-bounded recovery operation. + #[must_use] + pub const fn operation(&self) -> &O { + &self.operation + } +} + +/// Adapter extension for purpose-bounded recovery operations on the exact consumed lifecycle port. +/// +/// Browser Session remains protocol-agnostic. Implementations own their operation, output, and error +/// vocabularies, while recovery custody supplies only immutable lifecycle provenance and routes the +/// request through the same concrete adapter instance consumed by [`BoundBrowserSession`]. A successful +/// adapter return is not itself proof that remote ownership was reconciled or destroyed. +pub trait RecoveryContextOperationPort: DisposableContextPort { + /// Adapter-defined recovery operation vocabulary. + type Operation; + /// Adapter-defined successful result. + type Output; + /// Adapter-defined bounded recovery failure. + type Error; + + /// Execute one purpose-bounded recovery operation using the exact recovery-custody request. + fn execute_recovery_context_operation( + &mut self, + request: &RecoveryContextOperationRequest, + ) -> Result; +} + +/// Failure from executing one recovery operation through the exact retained adapter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RecoveryContextOperationError { + /// The retained adapter attempted the recovery operation and returned its bounded failure. + Adapter(E), +} + /// Recovery-only custody of a Browser Session and its exact consumed lifecycle adapter. /// /// This wrapper is obtained only by consuming a bound session that has already entered /// [`BrowserSessionState::RecoveryRequired`] or [`BrowserSessionState::TransportLost`]. It exposes -/// only lifecycle state and exact non-authorizing recovery evidence. It deliberately provides none -/// of the ordinary create, presentation-authority, epoch-advance, destroy, authorized-operation, or -/// normal-finish methods, and it does not expose the inner [`BoundBrowserSession`] or concrete port. +/// lifecycle state, exact non-authorizing recovery evidence, and a purpose-bounded recovery-operation +/// path through the retained adapter. It deliberately provides none of the ordinary create, +/// presentation-authority, epoch-advance, destroy, authorized-operation, or normal-finish methods, and +/// it does not expose the inner [`BoundBrowserSession`] or concrete port. /// /// Ordinary context creation is not available from recovery custody: /// @@ -124,3 +208,31 @@ impl BoundBrowserSessionRecovery

{ .create_attempt_recovery_evidence() } } + +impl BoundBrowserSessionRecovery

{ + /// Execute one purpose-bounded recovery operation through the exact retained lifecycle adapter. + /// + /// The request snapshots the unresolved aggregate state and both recovery-evidence ledgers before + /// adapter I/O. Adapter success or failure leaves Browser Session state and evidence unchanged; + /// protocol-specific code must provide separate, reviewed reconciliation proof before uncertainty + /// can be resolved. + pub fn execute_recovery_context_operation( + &mut self, + operation: P::Operation, + ) -> Result> { + self.bound.dispatch_recovery_operation(|session, port| { + let request = RecoveryContextOperationRequest { + browser_session: session.id(), + incarnation: session.incarnation(), + state: session.state(), + recovery_evidence: session.recovery_evidence().to_vec(), + create_attempt_recovery_evidence: session + .create_attempt_recovery_evidence() + .to_vec(), + operation, + }; + port.execute_recovery_context_operation(&request) + .map_err(RecoveryContextOperationError::Adapter) + }) + } +} From f014a76595aa44e7d04d8d9baee2483a6431e371 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:12:29 +0900 Subject: [PATCH 109/632] fix(browser-session): keep recovery bridge inside owner module --- .../src/browser_session.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index d9a51ddeb..640352f2c 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -1330,6 +1330,18 @@ impl BoundBrowserSession

{ pub fn finish(&mut self) -> Result<(), BrowserSessionError> { self.session.end() } + + /// Route one crate-internal recovery operation through the exact retained adapter. + /// + /// The callback is deliberately crate-private: external consumers never receive the raw adapter, + /// while recovery custody can bind one purpose-bounded request to the same adapter instance that + /// performed lifecycle creation and destruction. + pub(crate) fn dispatch_recovery_operation( + &mut self, + dispatch: impl FnOnce(&BrowserSession, &mut P) -> R, + ) -> R { + dispatch(&self.session, &mut self.port) + } } impl BoundBrowserSession

{ From 38077f83ce05c3b474f35ff1735566e70286d74b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:12:37 +0900 Subject: [PATCH 110/632] fix(browser-session): restore native module boundary --- crates/originweave-browser-session/src/lib.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index f9e1d6509..35b3e08c1 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -21,23 +21,7 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] -mod browser_session { - include!("browser_session.rs"); - - impl

BoundBrowserSession

{ - /// Route one crate-internal recovery operation through the exact retained adapter. - /// - /// The callback is deliberately crate-private: public callers never receive the raw adapter, - /// while recovery custody can still bind one purpose-bounded request to the same adapter - /// instance that performed lifecycle creation and destruction. - pub(crate) fn dispatch_recovery_operation( - &mut self, - dispatch: impl FnOnce(&BrowserSession, &mut P) -> R, - ) -> R { - dispatch(&self.session, &mut self.port) - } - } -} +mod browser_session; mod recovery; pub use browser_session::*; From d4363e186382ee2993cd25f7bf4eff5542281f3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:19:45 +0900 Subject: [PATCH 111/632] docs(browser-session): currentize recovery-operation custody --- ...sion-recovery-custody-and-hot-ownership.md | 86 ++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index 4f72aa752..2f1985944 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -2,6 +2,7 @@ - Status: Proposed - Date: 2026-09-15 +- Last amended: 2026-09-16 - Extends: ADR 0114 - Owning bounded context: `originweave-browser-session` @@ -9,16 +10,17 @@ ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. -First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. +First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. Recovery nevertheless needs a narrow way to perform protocol-owner-defined reconciliation through that same retained adapter. Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. -These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery commands remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. +These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery command semantics remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. ## Decision drivers - Preserve the exact consumed adapter across unresolved ownership without making it generally accessible again. -- Keep recovery evidence non-authorizing. +- Permit only purpose-bounded recovery I/O through that retained adapter; never expose raw `P` or an unrestricted callback. +- Keep recovery evidence non-authorizing and unchanged by a mere adapter success/failure. - Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. - Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. - Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. @@ -30,9 +32,11 @@ These questions are Browser Session domain concerns. WebDriver BiDi pending/acce Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, navigation-generation custody, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. -WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, protocol event replay qualification, and any protocol recovery command remain #316 responsibilities. A future protocol recovery operation must be purpose-bounded against recovery custody rather than reconstructing an adapter from raw ids. +The recovery wrapper may execute an adapter-defined operation only when `P: RecoveryContextOperationPort`. Browser Session constructs an opaque `RecoveryContextOperationRequest` immediately before I/O, snapshots the exact `BrowserSessionId`, `BrowserSessionIncarnation`, current unresolved `BrowserSessionState`, `BrowserSessionRecoveryEvidence`, and `DisposableContextCreateRecoveryEvidence`, and routes it through the same retained adapter. The request has no public constructor and is not ordinary create/destroy/presentation authority. -Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, and recovery evidence do not grant Browser Session command authority. +WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, protocol event replay qualification, and protocol recovery command meaning remain #316 responsibilities. #316 may map its recovery vocabulary into `RecoveryContextOperationPort::Operation`; Browser Session does not inspect or own that protocol vocabulary. + +Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, recovery evidence, and a successful recovery adapter call do not grant Browser Session command authority or prove remote destruction. ## Options considered @@ -42,12 +46,20 @@ Rejected. Raw adapter recovery recreates ambient capability and permits callers ### Expose `&BrowserSession` from recovery custody -Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections. +Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections and the purpose-bounded recovery operation surface. + +### Expose `FnOnce(&mut P)` or another generic callback + +Rejected. A generic callback is equivalent to raw adapter escape. The callback bridge that touches `&mut P` is crate-private and callable only by the recovery wrapper after constructing the opaque request. ### Clone or reconstruct the adapter for recovery Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. +### Treat a successful recovery adapter call as reconciliation proof + +Rejected. Command success alone does not prove that remote ownership was destroyed or reconciled. Browser Session state and evidence remain unchanged until a separately reviewed proof-bearing transition exists. + ### Keep every proven-destroyed context as a permanent hot tombstone Rejected. It makes authority-admission state grow with historical throughput and conflates authorization with audit retention. Monotonic epochs plus exact validation are sufficient to reject predecessor capabilities after a proven destroy and same-identity recreation. @@ -65,30 +77,34 @@ Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch cou 1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. 2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. 3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. -4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes only lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, and exact `DisposableContextCreateRecoveryEvidence`. -5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, destroy, authorized-operation, or normal-finish surface. -6. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. -7. Protocol-specific recovery commands are not added to Browser Session. #316 may define purpose-bounded WebDriver BiDi recovery operations that consume the exact adapter held by recovery custody, but protocol tuple truth and command semantics remain outside the Browser Session aggregate. -8. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. -9. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. -10. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. -11. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. -12. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. -13. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. -14. `Drop` on recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. -15. Browser-observed navigation is admitted only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` ownership generation. Admission revokes presentation authority with zero adapter I/O and mints an opaque `NavigationSettlementAuthority` using a separate monotonic navigation generation rather than spending a presentation epoch. -16. Commit progress is non-terminal. Positive settlement, typed `Aborted`/`Failed`, and download start share one exactly-once terminal closure. A newer admitted navigation supersedes an older pending witness; stale or superseded witnesses fail closed without changing a newer generation. -17. Terminal closure creates one context-local opportunity for explicit `reestablish_presentation_authority`; the first successful re-establishment consumes the next presentation epoch and returns the context to `Established`. Generic epoch advancement cannot bypass a navigation-invalidated state. -18. Presentation authority and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup, while the exact bound lifecycle owner may still destroy its retained disposable context through `destroy_owned_disposable_context` without reopening presentation authority. -19. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction/navigation post-conditions are independently evidenced. +4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, exact `DisposableContextCreateRecoveryEvidence`, and—only when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. +5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, authority-based or owner-based destroy, ordinary authorized operation, navigation transition, or normal-finish surface. +6. `RecoveryContextOperationRequest` is non-caller-constructible. It snapshots exact session id, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-owned purpose-bounded operation before adapter I/O. +7. The only bridge that receives `&mut P` is `BoundBrowserSession::dispatch_recovery_operation`, which is `pub(crate)` and lives in the Browser Session owner module. External consumers cannot invoke it or supply a closure. +8. `RecoveryContextOperationPort` extends `DisposableContextPort` with adapter-owned `Operation`, `Output`, and `Error` types. Browser Session routes the opaque request but does not interpret protocol semantics. +9. `RecoveryContextOperationError::Adapter(E)` preserves typed adapter failure. Adapter success and failure both leave Browser Session lifecycle state and recovery evidence unchanged; neither is destruction/reconciliation proof. +10. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. +11. Protocol-specific recovery commands are not added to Browser Session. #316 consumes the generic recovery boundary for WebDriver BiDi pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics. +12. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. +13. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. +14. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. +15. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. +16. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. +17. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. +18. `Drop` on ordinary or recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. +19. Browser-observed navigation is admitted only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` ownership generation. Admission revokes presentation authority with zero adapter I/O and mints an opaque `NavigationSettlementAuthority` using a separate monotonic navigation generation rather than spending a presentation epoch. +20. Commit progress is non-terminal. Positive settlement, typed `Aborted`/`Failed`, and download start share one exactly-once terminal closure. A newer admitted navigation supersedes an older pending witness; stale or superseded witnesses fail closed without changing a newer generation. +21. Terminal closure creates one context-local opportunity for explicit `reestablish_presentation_authority`; the first successful re-establishment consumes the next presentation epoch and returns the context to `Established`. Generic epoch advancement cannot bypass a navigation-invalidated state. +22. Presentation authority and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup, while the exact bound lifecycle owner may still destroy its retained disposable context through `destroy_owned_disposable_context` without reopening presentation authority. +23. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction/navigation post-conditions are independently evidenced. ## Consequences -The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody deliberately cannot complete the protocol-specific recovery by itself; that operation belongs to the WebDriver BiDi ACL/adapter owner and must remain purpose-bounded. +The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can perform only the adapter-owned recovery operations admitted by `RecoveryContextOperationPort`; it cannot expose the adapter, regain ordinary Browser Session authority, or independently decide that protocol recovery is complete. Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. -The active #317 production lineage now treats ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. Navigation liveness uses its own monotonic generation; presentation epochs advance only when explicit re-establishment succeeds. +The active #317 production lineage treats ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. Navigation liveness uses its own monotonic generation; presentation epochs advance only when explicit re-establishment succeeds. ## Navigation authority interaction @@ -100,13 +116,15 @@ The returned `NavigationSettlementAuthority` has private fields and no caller co If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. +A failed recovery operation returns `RecoveryContextOperationError::Adapter` and preserves the same custody and evidence. A successful recovery operation returns the adapter-defined output but also preserves the same custody and evidence; a separate owner transition is required before uncertainty can be cleared. + A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation/navigation-generation allocation is exhausted, allocation fails closed rather than reusing authority identity. ## Security / privacy / governance impact -The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned but not ambiently callable. +The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned and is callable only through the purpose-bounded recovery trait; raw `P` never becomes ambient. Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. @@ -119,10 +137,18 @@ This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, - `crates/originweave-browser-session/src/recovery.rs` - `BoundBrowserSession::into_recovery` - `BoundBrowserSessionRecovery

` + - `RecoveryContextOperationRequest` + - `RecoveryContextOperationPort` + - `RecoveryContextOperationError` - negative `compile_fail` capability contracts - `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` - unproven destroy moves the exact adapter and exact evidence without I/O - transport loss moves the exact adapter and exact evidence without I/O +- `crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs` + - recovery operation executes through the exact retained adapter + - exact session/incarnation/state and both evidence ledgers reach the opaque request + - adapter failure preserves custody/evidence + - adapter success is not treated as cleanup proof - `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` - 258 same-handle ownership generations remain admissible after proven destruction - epochs are strictly monotonic @@ -136,22 +162,22 @@ These are active-PR contracts until the exact head passes repository contracts, ## Migration and rollback -This active-PR change is additive at the ownership-type boundary but changes the internal retention and navigation-admission model. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery/navigation authority from adapter identifiers. +This active-PR change is additive at the ownership-type boundary but changes the internal retention, recovery-operation, and navigation-admission model. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery/navigation authority from adapter identifiers. -Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. +Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/recovery-operation/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. ## Open follow-ups -- #316: purpose-bounded WebDriver BiDi recovery operations through the exact recovery-held adapter, including pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics. +- #316: adopt the released/verified Browser Session recovery-operation boundary and implement purpose-bounded WebDriver BiDi recovery operations, pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics without reconstructing an adapter. - #318/#321: executable acceptance/review successors for the implemented #317 navigation contract, including same-raw-id ABA and sibling-recreation matrices; they do not own a second production state machine. - Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. -- Real Chromium proof of remote destruction, cleanup, navigation, interaction, and browser-observed post-conditions. +- Real Chromium proof of remote destruction, cleanup, navigation, interaction, recovery, and browser-observed post-conditions. - `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. - Protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. ## Supersession / reversal conditions -Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. +Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, purpose-bounded same-adapter recovery I/O, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. From 7e8ffa0882fddb41a0a25462a0647573f18d8d27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:20:49 +0900 Subject: [PATCH 112/632] docs(browser-session): trace recovery-operation authority --- .../browser-session-lifecycle-authority.md | 134 +++++++----------- 1 file changed, 52 insertions(+), 82 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 66e9d0df9..ed693474d 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -8,7 +8,7 @@ ## Problem and invariant -Browser-session, user-context/isolation, browsing-context, and adapter-selected identifiers are addresses. They are not evidence that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. +Browser-session, user-context/isolation, browsing-context, adapter-selected identifiers, and recovery evidence are addresses or evidence. They are not proof that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. The active implementation establishes this chain: @@ -26,139 +26,109 @@ validated BrowserSessionId → uncertain/rejected create paths retain exact aggregate-issued attempt identity as non-authorizing recovery evidence → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) -→ exact authority validation before any lifecycle or purpose-bounded adapter I/O +→ exact authority validation before ordinary lifecycle or purpose-bounded adapter I/O → lifecycle destruction uses private DisposableContextDestroyRequest(handle, validated epoch) -→ presentation/reconciliation uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) +→ presentation work uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) → exact consumed adapter only → failed/unproven destruction retains exact handle + validated epoch as non-authorizing recovery evidence → RecoveryRequired|TransportLost can consume the same bound owner into BoundBrowserSessionRecovery

-→ recovery custody exposes exact non-authorizing evidence but no ordinary lifecycle/authority surface +→ recovery custody exposes exact non-authorizing evidence and one purpose-bounded RecoveryContextOperationPort path +→ recovery wrapper privately builds RecoveryContextOperationRequest(session, incarnation, state, evidence, operation) +→ the crate-private bridge dispatches that request through the exact retained adapter without exposing raw P +→ adapter success/failure leaves unresolved Browser Session state/evidence unchanged → proven destruction removes the live hot-ownership record; failed destruction retains Uncertain ownership → BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` -`BoundBrowserSession` is the lifecycle composition boundary. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P`, `&mut P`, or a generic callback that would recreate unrestricted adapter authority. +`BoundBrowserSession` is the linear lifecycle-port binding. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P`, `&mut P`, or a generic callback that would recreate unrestricted adapter authority. The wrapper has a manual redacted `Debug` implementation. Formatting exposes inert Browser Session summary fields only and never calls `P::fmt`, so a side-effecting or secret-bearing adapter `Debug` cannot become a diagnostic capability escape. -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` have private construction paths. The create request carries the already-reserved context epoch as a **per-create transaction** identity. Destroy and purpose-bounded operation requests carry the exact context epoch that Browser Session validated immediately before adapter I/O. That epoch is correlation/provenance only: it does not authorize a command independently from the private aggregate-issued request. Purpose-bounded operations are constructed only after exact `PresentationMutationAuthority` validation. +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, `AuthorizedContextOperationRequest`, and `RecoveryContextOperationRequest` have private construction paths. Create attempt epochs and validated context epochs are correlation/provenance, not standalone bearer authority. ## Transactional remote creation -A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing yet. - -Browser Session examines the returned `DisposableContextHandle`: - -- if ownership validation succeeds, `DisposableContextCreateCompletion::Accepted` settles that exact attempt before normal presentation authority is returned; -- if the handle aliases an existing isolation or browsing context, `Rejected` settles that exact attempt and the aggregate enters `RecoveryRequired`; -- if exact completion cannot be proven, Browser Session stores the complete handle as `UnsettledAdapterHandle`, enters recovery, and mints no normal authority. +A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing yet. Browser Session accepts or rejects the returned domain handle and settles that exact attempt through `DisposableContextCreateCompletion`. `DisposableContextCreateRecoveryEvidence` preserves the transaction dimension that raw handle evidence cannot represent. `FailedUncertain` stores the exact aggregate-issued `attempt_epoch` even when no complete handle exists; `DuplicateCandidate` binds an aliased returned handle to its exact rejected attempt; `CompletionUnsettled` binds the exact attempt, `Accepted|Rejected` disposition, and complete returned handle when settlement cannot be proven. This evidence grants no browser authority. -Identity-oriented `BrowserSessionRecoveryEvidence` remains separately useful for exact remote reconciliation. Candidate evidence such as `DuplicateAdapterHandle(H)` or `UnsettledAdapterHandle(H)` is not treated as proof that an already-owned same-valued `H` has been recorded: the existing owner is retained independently as `RecoveryRequiredOwnedHandle(H)`. Equal remote values therefore cannot collapse distinct lifecycle facts from different create attempts. +Identity-oriented `BrowserSessionRecoveryEvidence` remains separately useful for exact remote reconciliation. A same-valued later candidate does not erase a previously accepted ownership fact represented independently by `RecoveryRequiredOwnedHandle`. Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only attempt identity, domain validation, accept/reject decision, current authority validation, and non-authorizing recovery facts. -## Same-bound-adapter authorized operations - -`AuthorizedContextOperationPort` extends the lifecycle port for adapters that need post-create presentation or reconciliation work. The operation/output/error vocabulary remains adapter-owned. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. The request exposes that already-validated epoch so adapter execution logs and protocol correlation cannot collapse distinct authority generations that happen to reuse the same external identifiers. - -Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O, so no request and no epoch provenance reaches the adapter on rejection. An operation attempted by the exact bound adapter can return `AuthorizedContextOperationError::Adapter`. No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. - -## Lossless recovery evidence while retained +## Same-bound-adapter ordinary authorized operations -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains both the exact known isolation identity and the exact aggregate-issued create-attempt epoch; `CreateFailedUncertain(None)` still retains the exact attempt epoch. Duplicate output stores the complete offending handle and its exact rejected attempt. Completion failure retains the complete handle, attempt epoch, and aggregate disposition. Failed or unproven destruction records both the exact owned handle and the exact validated `BrowserContextEpoch` that was sent on the destroy request. When any such failure moves the aggregate to `RecoveryRequired`, every other still-active sibling is projected exactly once as `RecoveryRequiredOwnedHandle` before becoming uncertain. Transport loss records each previously active exact handle as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. +`AuthorizedContextOperationPort` extends the lifecycle port for post-create presentation work. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O; an adapter failure returns `AuthorizedContextOperationError::Adapter`. -Cause-specific candidate evidence is retained separately from generic sibling ownership evidence. A same-valued candidate from a later failed attempt cannot erase a previously accepted ownership fact. Repeated transport-loss reports are idempotent, so exact transport-loss evidence is not duplicated by repeated notification. +No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. -Operation/destroy/create-attempt provenance is implemented on the active #317 lineage: `AuthorizedContextOperationRequest::context_epoch` and `DisposableContextDestroyRequest::context_epoch` are copied only after exact authority validation; stale authority remains zero-I/O; `UnprovenDestruction` preserves that same epoch; create uncertainty and completion failure retain the reserved `attempt_epoch` in `DisposableContextCreateRecoveryEvidence`. +## Recovery-only custody and same-adapter recovery operation -## Recovery-only custody and bounded hot ownership +`BoundBrowserSession::into_recovery(self)` is the one-way custody boundary for unresolved ownership. It succeeds only from `RecoveryRequired` or `TransportLost`, moves the exact already-consumed non-`Clone` adapter without browser I/O, and returns `BoundBrowserSessionRecovery

`. `Active` or `Ended` returns the original bound owner unchanged. -`BoundBrowserSession::into_recovery(self)` is the one-way custody boundary for unresolved ownership. It succeeds only from `RecoveryRequired` or `TransportLost`, moves the exact already-consumed non-`Clone` adapter without browser I/O, and returns `BoundBrowserSessionRecovery

`. `Active` or `Ended` returns the original bound owner unchanged, so recovery custody cannot be used as an alternate normal lifecycle path. +Recovery custody exposes `state()`, `recovery_evidence()`, `create_attempt_recovery_evidence()`, and—when the retained adapter implements `RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. It exposes neither raw `P`, the inner `BoundBrowserSession`, nor the inner `BrowserSession`; ordinary create, presentation-authority lookup, epoch advancement, destroy, `AuthorizedContextOperationPort`, navigation authority, and normal finish remain unavailable. Rustdoc `compile_fail` contracts pin those negative capabilities. -`BoundBrowserSessionRecovery

` deliberately exposes only `state()`, `recovery_evidence()`, and `create_attempt_recovery_evidence()`. It exposes neither raw `P`, the inner `BoundBrowserSession`, nor the inner `BrowserSession`; ordinary create, presentation-authority lookup, epoch advancement, destroy, authorized operation, and normal finish remain unavailable. Rustdoc `compile_fail` contracts pin those negative capabilities. The wrapper therefore preserves same-adapter custody for the protocol recovery owner without reconstructing ordinary mutation authority from identifiers or evidence. Protocol-specific recovery commands and pending/accepted/quarantined tuple reconciliation remain #316-owned adapter work. +The recovery operation does not weaken that boundary. `RecoveryContextOperationRequest` is created only inside recovery custody and snapshots exact `BrowserSessionId`, `BrowserSessionIncarnation`, unresolved `BrowserSessionState`, `BrowserSessionRecoveryEvidence`, `DisposableContextCreateRecoveryEvidence`, and the adapter-defined operation immediately before I/O. `BoundBrowserSession::dispatch_recovery_operation` is `pub(crate)` and lives in the Browser Session owner module; downstream code cannot call it or supply a callback receiving `&mut P`. -Hot command-authority state contains only live or uncertain ownership. After exact authority validation and adapter-confirmed destruction, Browser Session removes that context record from the hot ownership map instead of accumulating a permanent `Destroyed` tombstone. A failed destroy does the opposite: the exact record remains `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains enumerable, and normal authority stays closed. The 258-generation same-handle hostile fixture proves a proven-destroyed raw isolation/context identity can be reused without unbounded hot-state growth while retained predecessor authority still fails before adapter I/O. Immediately after destruction it fails as `ContextNotOwned`; after the same raw identity is recreated under the next monotonic epoch it fails as `AuthorityMismatch`. +`RecoveryContextOperationPort` owns its operation/output/error vocabulary. `RecoveryContextOperationError::Adapter` preserves typed failure. Both adapter success and failure leave Browser Session state and evidence unchanged. A successful call is not destruction proof, not reconciliation proof, and not presentation authority. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and the meaning of concrete recovery commands. -Removing proven-destroyed command-authority records is not durable history deletion. Durable crash/process-restart recovery and audit history remain a separate persistence concern; they must not be reconstructed from the bounded hot map or from `abandoned_bound_session_count()`. +## Lossless recovery evidence while retained -## Abandonment and lifecycle completion +`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains both the exact known isolation identity and exact aggregate-issued create-attempt epoch; `CreateFailedUncertain(None)` still retains the exact attempt epoch. Duplicate output and completion failure preserve exact transaction identity. Failed or unproven destruction records the exact owned handle and validated `BrowserContextEpoch`. Entering `RecoveryRequired` projects still-active siblings as `RecoveryRequiredOwnedHandle`; transport loss records active handles as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. -`BoundBrowserSession

` is `#[must_use]`. `finish(&mut self)` succeeds only after all owned contexts have proven destruction. If it returns `ActiveContextRemains`, the wrapper, exact bound adapter, and private ownership ledger remain intact. The same owner can therefore destroy or reconcile the remaining context and retry `finish()` without introducing a second adapter or ambient cleanup capability. +## Bounded hot ownership and Sequential ABA -`Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping a wrapper with active/uncertain ownership increments the process-local `abandoned_bound_session_count()` signal. `BoundBrowserSessionRecovery

` keeps that same contained owner, so dropping unresolved recovery custody retains the same non-I/O abandonment accounting. This makes ordinary abandonment observable to operability/recovery code without reviving adapter authority. The counter is not durable storage and contains no exact handle payload. Exact durable crash/process-restart recovery therefore remains open until a canonical recovery owner persists Browser Session recovery evidence before process termination. +Hot command-authority state contains only live or uncertain ownership. After exact authority validation and adapter-confirmed destruction, Browser Session removes that context record instead of accumulating a permanent tombstone. A failed destroy retains the exact record as `Uncertain` plus `UnprovenDestruction { context, context_epoch }`. -The abandonment counter and Browser Session incarnation allocator use `AtomicU64::try_update` with the same memory-ordering and closure semantics as the predecessor `fetch_update` calls. This removes the pinned-nightly deprecation without weakening overflow behavior or synchronization semantics. +The 258-generation hostile fixture proves the same raw isolation/context values can be reused after proven destruction while epochs remain monotonic. Immediately after destruction a retained authority fails as `ContextNotOwned`; after same-raw-id recreation it fails as `AuthorityMismatch`. Sequential ABA across independent aggregates is also rejected by `BrowserSessionIncarnation` even when external ids and local epoch values alias. -## Orthogonal transport liveness +Removing proven-destroyed command-authority records is not durable history deletion. Durable crash/process-restart recovery and buyer audit history remain a separate persistence concern and must not be reconstructed from the bounded hot map or `abandoned_bound_session_count()`. -Transport liveness is tracked independently from ownership recovery. A first transport loss from `Active` preserves exact active handles as recovery evidence, moves those records to uncertain, and moves the aggregate to `TransportLost`. If transport loss occurs after `RecoveryRequired`, the stronger ownership-recovery state remains while `transport_lost = true` records the orthogonal fact. Repeated loss reports are idempotent. +## Abandonment and lifecycle completion -## Sequential ABA safety +`BoundBrowserSession

` is `#[must_use]`. A failed `finish()` does not consume the wrapper; the exact bound adapter and ownership ledger remain available for cleanup/retry. `Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping unresolved ordinary or recovery custody increments the process-local `abandoned_bound_session_count()` signal only. -Aggregate A may create `(S,U,C,epoch=1)`, prove destruction, and end. Aggregate B can later start with the same external values and also begin at epoch 1. A's retained authority still fails because B has a different `BrowserSessionIncarnation`. Within one aggregate, proven destruction releases the same raw identity from hot ownership but the monotonic context epoch prevents a retained authority from becoming current when that raw identity is recreated. The bound port receives the incarnation inside aggregate-issued lifecycle capabilities. +The abandonment counter and Browser Session incarnation allocator use the non-deprecated `AtomicU64::try_update` API while preserving ordering and overflow behavior. -## Browser-issued user-context identity +## Navigation authority interaction -`DisposableIsolationId` maps one-to-one to the browser-issued WebDriver BiDi `browser.UserContext` identity. That value is addressability and recovery evidence, not command authority. OriginWeave preserves a protocol-valid browser identity losslessly so the exact remote boundary can later be destroyed or reconciled. The previous arbitrary 4096-byte parser ceiling has been removed; the hostile 4097-byte fixture and internal round-trip test now require lossless preservation. No truncation or normalization is acceptable for a browser-issued identity. +The active #317 lineage admits an observed navigation only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` generation. It revokes presentation authority without adapter I/O, mints an opaque `NavigationSettlementAuthority`, treats commit as non-terminal progress, uses one exactly-once closure for positive settlement / typed negative terminal / download start, and permits one explicit re-establishment opportunity. A newer navigation supersedes an older witness. Aggregate trust and current live ownership dominate settlement and re-establishment. -## Standards trace +Presentation mutation and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup; the exact bound lifecycle owner may still perform `destroy_owned_disposable_context` without reopening presentation authority. -The latest immutable W3C WebDriver BiDi Working Draft directly verified on 2026-09-15 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. The mutable `/TR/webdriver-bidi/` index can lag this dated publication and is not used to erase immutable provenance. `browser.createUserContext` creates a user context, `browsingContext.create` can create a browsing context inside it, and `browser.removeUserContext` removes the selected user context after closing its navigables. `browser.UserContext` is defined as `text`; the published protocol defines no 4096-byte domain ceiling. +## Browser-issued identity and standards trace -Standards freshness and runtime qualification are separate controls. Updating this citation does not repin the separately qualified Chromium/WebDriver BiDi runtime revision. +`DisposableIsolationId` maps one-to-one to WebDriver BiDi `browser.UserContext` and preserves protocol text losslessly. OriginWeave does not trim, normalize, impose the removed 4096-byte limit, or treat that address as command authority. -OriginWeave does not treat protocol identifiers as policy authority or assume historical non-reuse after removal. A command ACK is insufficient proof that the disposable boundary is actually gone. +The latest immutable W3C WebDriver BiDi Working Draft directly verified on 2026-09-15 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. The mutable `/TR/webdriver-bidi/` index and the separately qualified Chromium/runtime revision are distinct provenance axes. A command ACK is insufficient proof that a disposable boundary is actually gone. ## Source and executable evidence | Invariant | Source / test | |---|---| | independent Browser Session bounded context | `crates/originweave-browser-session/`; `tests/test_browser_session_lifecycle_contract.py` | -| lifecycle port ownership is structural | `BoundBrowserSession`; `bound_port_is_structural_and_not_swappable` | -| no public raw port accessor | absence of `BoundBrowserSession::lifecycle_port`; repository contract | -| binding performs no arbitrary adapter callback | `BrowserSession::bind_lifecycle_port`; `lifecycle_binding_invokes_no_adapter_callback_before_authorized_create` | -| no self-asserted adapter id authority | absence of `DisposableContextPortId` / `port_id()` | +| lifecycle port ownership is structural; no public raw port accessor | `BoundBrowserSession`; lifecycle binding hostile tests | | create requests are aggregate-issued and attempt-scoped | `DisposableContextCreateRequest::attempt_epoch`; transaction hostile fixture | -| uncertain create preserves exact transaction identity with or without a complete handle | `DisposableContextCreateRecoveryEvidence::FailedUncertain`; internal Some/None recovery tests | -| duplicate candidate retains exact rejected attempt | `DisposableContextCreateRecoveryEvidence::DuplicateCandidate`; `create_recovery_same_handle_distinct_fact.rs` | -| completion failure retains exact attempt + disposition + handle | `DisposableContextCreateRecoveryEvidence::CompletionUnsettled`; internal accepted/rejected completion tests | | same-valued prior owner and later candidate remain distinct recovery facts | `RecoveryRequiredOwnedHandle`; `create_recovery_same_handle_distinct_fact.rs` | -| per-create transaction settles accepted/rejected candidates | `DisposableContextCreateCompletion`; `accepted_and_rejected_create_candidates_are_correlated_by_exact_attempt` | -| completion failure fails closed | `UnsettledAdapterHandle`; internal completion-failure tests | -| raw context cannot mint presentation authority | `BrowserSession::presentation_authority`; `bound_creation_is_the_only_raw_context_entry_to_authority` | -| same consumed adapter handles authorized post-create work | `AuthorizedContextOperationPort`; `authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io` | -| authorized operation carries exact validated epoch | `AuthorizedContextOperationRequest::context_epoch`; `authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io` | -| stale operation authority fails before adapter I/O | `AuthorizedContextOperationError::BrowserSession`; authorized-operation hostile fixture | -| destroy request carries exact validated epoch | `DisposableContextDestroyRequest::context_epoch`; `destroy_failure_requires_recovery_before_any_new_authority` | -| unproven destroy preserves exact handle + validated epoch | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; `destroy_failure_requires_recovery_before_any_new_authority` | -| adapter-owned Debug is not executed or rendered | manual `Debug for BoundBrowserSession

`; `bound_session_debug_never_executes_or_exposes_adapter_debug` | -| sequential ABA authority is rejected before I/O | `BrowserSessionIncarnation`; `stale_authority_cannot_cross_sequential_session_incarnations` | -| lossless recovery evidence while aggregate is retained | `BrowserSessionRecoveryEvidence`; `DisposableContextCreateRecoveryEvidence`; recovery tests | -| `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_projects_exact_handles_for_indirectly_uncertain_siblings` | -| transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_preserves_exact_owned_handle_as_non_authorizing_recovery_evidence` | -| unresolved state can move to recovery-only custody without replacing the adapter | `BoundBrowserSession::into_recovery`; `BoundBrowserSessionRecovery`; `recovery_owner_handoff.rs` | -| recovery custody cannot regain ordinary lifecycle or presentation authority | `BoundBrowserSessionRecovery` rustdoc `compile_fail` contracts; repository contract | -| proven destruction releases bounded hot ownership while stale authority remains rejected | `proven_destroy_releases_hot_ownership.rs`; post-success context removal in `BrowserSession` | -| failed destruction retains uncertain hot ownership and exact evidence | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; destroy-failure tests | -| unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `dropping_unresolved_bound_session_is_observable_without_implicit_browser_io` | -| failed finish retains exact bound owner | `BoundBrowserSession::finish`; `failed_finish_retains_same_bound_owner_for_cleanup_and_retry` | -| normal completion requires proven destruction | `BoundBrowserSession::finish`; `proven_destruction_can_finish_without_abandonment_path` | -| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs`; internal 4097-byte round-trip test | -| atomic lifecycle counters use the non-deprecated API without changing ordering | `ABANDONED_BOUND_SESSIONS.try_update`; `allocate_incarnation` | +| same consumed adapter handles ordinary purpose-bounded work | `AuthorizedContextOperationPort`; `authorized_context_operation.rs` | +| same consumed adapter handles recovery-only work without raw adapter escape | `RecoveryContextOperationPort`; `RecoveryContextOperationRequest`; `recovery_same_adapter_operation.rs` | +| recovery success/failure preserves unresolved state/evidence | `recovery_same_adapter_operation.rs` | +| unproven destroy preserves exact handle + epoch | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; destroy-failure tests | +| `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_sibling_evidence.rs` | +| transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_recovery_evidence.rs` | +| unresolved state moves to recovery-only custody without replacing adapter | `BoundBrowserSession::into_recovery`; `recovery_owner_handoff.rs` | +| recovery custody cannot regain ordinary lifecycle or presentation authority | `BoundBrowserSessionRecovery` rustdoc `compile_fail`; repository contract | +| proven destruction releases bounded hot ownership while stale authority remains rejected | `proven_destroy_releases_hot_ownership.rs` | +| unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `bound_session_abandonment.rs` | +| failed finish retains exact bound owner | `BoundBrowserSession::finish`; abandonment fixture | +| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs` | | transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | -| normal end requires proved destruction | `BrowserSession::end` | -| incarnation exhaustion fails closed | `allocate_incarnation` | - -Historical exact `9cde981899950b900698a17e7fa739af59f6bb4f` / CI `34531025582` is RED for this successor: production exact coverage passed, but canonical formatting failed, and the raw port accessor plus missing transaction completion remained. Historical exact `729603ae4feadd369eee7819a45d6850604975da` / CI `34541860394` passed production exact coverage but failed the repository contract after the ADR lost the `DisposableContextDestroyError` trace. Exact `d5046e76cb7555b448b728ea1bed9ba1ea8de8c3` / CI `34573175780` passed Python repository contracts, then failed canonical formatting; production coverage stopped during measurement because the two intentional hostile lifecycle REDs were still unresolved. Historical GREEN never transfers. - -Protected-main integration is required before capability maturity can be promoted beyond `IMPLEMENTED_ON_ACTIVE_PR`. +| navigation witness is opaque and context-generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | -## Buyer acceptance still open +Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source and tests remain non-shipment until exact-head repository contracts, rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required security/review workflows, and protected `main` integration are observed. -This slice does not yet prove actual WebDriver BiDi lifecycle integration, observed removal post-condition, protocol-specific pending/accepted/quarantined binding, protocol-specific recovery commands through the same custody boundary, durable crash/process-restart recovery persistence, Browser Session authority conversion into BiDi presentation private witnesses, Chromium post-condition observation, #299 3/3 browser trials, or protected-main release/SBOM/provenance/reproducibility/rollback. +## Current integration boundary -## Reference +#317 owns Browser Session domain policy and the generic same-adapter recovery-operation boundary. #318/#321 own stacked hostile acceptance and doctoring only. #316 owns WebDriver BiDi pending/accepted/quarantined state, event correlation, remote liveness, protocol-specific recovery semantics, and real-browser adapter integration. No child may copy Browser Session production source or reconstruct a second adapter from raw identifiers. -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ \ No newline at end of file +Real Chromium navigation, interaction, cleanup, recovery, and browser-observed post-condition evidence remains required before shipment. Immutable release/SBOM/provenance/reproducibility/rollback evidence remains a separate release gate. \ No newline at end of file From 1e94e7601efd4ba62670c013ee5f5391d12c8a44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:21:19 +0900 Subject: [PATCH 113/632] docs(browser-session): model same-adapter recovery operation --- .../browser-session-lifecycle-authority.md | 231 +++++++----------- 1 file changed, 82 insertions(+), 149 deletions(-) diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 4c8b0e858..11a6a2213 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -2,6 +2,8 @@ This diagram describes the active-PR domain contract for issue #312. It is not evidence that a WebDriver BiDi or Chromium adapter already implements the port. +## Ordinary lifecycle and presentation authority + ```mermaid sequenceDiagram autonumber @@ -18,12 +20,12 @@ sequenceDiagram Note over S,P: binding invokes no adapter callback; no public raw port accessor C->>BS: create_disposable_context() - BS->>S: require Active + reserve monotonic epoch + BS->>S: require Active + reserve monotonic BrowserContextEpoch S->>S: mint DisposableContextCreateRequest(session, incarnation, attempt epoch) S->>P: create_disposable_context(request) P->>B: create/stage isolation boundary + browsing context - B-->>P: unique isolation id + BrowsingContextId or typed create error - P-->>S: DisposableContextHandle (still pending in adapter) + B-->>P: DisposableContextHandle or typed create error + P-->>S: candidate remains pending/non-authorizing alt domain handle accepted S->>S: validate no isolation/context alias @@ -32,54 +34,37 @@ sequenceDiagram P->>P: pending exact attempt → accepted S->>S: register exact handle + Active epoch S-->>C: PresentationMutationAuthority(session, incarnation, isolation, context, epoch) - else domain handle rejected - S->>S: retain duplicate handle + exact rejected attempt - S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle - S->>S: mint DisposableContextCreateCompletion(Rejected, exact attempt) - S->>P: complete_disposable_context_creation(completion) - P->>P: pending exact attempt → quarantined/non-authorizing - S->>S: RecoveryRequired - else completion cannot be proven - S->>S: retain UnsettledAdapterHandle + exact unsettled attempt - S->>S: retain every other Active sibling as RecoveryRequiredOwnedHandle + else domain handle rejected/unsettled + S->>S: retain exact non-authorizing recovery evidence + S->>S: retain Active siblings as RecoveryRequiredOwnedHandle + S->>P: DisposableContextCreateCompletion(Rejected, exact attempt) S->>S: RecoveryRequired end C->>BS: execute_authorized_context_operation(authority, operation) BS->>S: validate session/incarnation/isolation/context/epoch alt authority current - S-->>BS: exact stored handle BS->>BS: mint private AuthorizedContextOperationRequest BS->>P: execute_authorized_context_operation(request) - P->>B: adapter-owned presentation/reconciliation command - B-->>P: typed adapter result - P-->>C: output or AuthorizedContextOperationError::Adapter + P->>B: adapter-owned presentation command + B-->>P: typed result + P-->>C: output or adapter error else stale or foreign authority S-->>C: AuthorizedContextOperationError::BrowserSession Note over BS,P: adapter I/O = 0 end - Note over C,S: Raw ids, adapter-selected values, diagnostic references, and a second adapter cannot mint lifecycle or presentation authority. - - C->>BS: advance_context_epoch(context_id) - BS->>S: replace epoch; old authority becomes stale - S-->>C: new opaque authority carrying same incarnation + isolation - C->>BS: destroy_disposable_context(authority) - BS->>S: validate exact session/incarnation/isolation/context/epoch before I/O - S->>S: mint DisposableContextDestroyRequest with exact stored handle + validated epoch - S->>P: destroy_disposable_context(request) + BS->>S: validate exact authority before I/O + S->>P: DisposableContextDestroyRequest(handle, validated epoch) P->>B: remove exact owned isolation boundary - B-->>P: observed destruction post-condition or DisposableContextDestroyError alt destruction proved P-->>S: success S->>S: remove live hot-ownership record - Note over S: epoch allocator remains monotonic; retained predecessor authority stays stale - else destruction unproven - S->>S: retain UnprovenDestruction(handle, validated epoch) - S->>S: retain each other Active sibling as RecoveryRequiredOwnedHandle - S->>S: keep failed record as Uncertain - S->>S: RecoveryRequired; all active siblings become Uncertain + else DisposableContextDestroyError / cleanup unproven + S->>S: keep record Uncertain + S->>S: retain exact UnprovenDestruction(handle, epoch) + S->>S: RecoveryRequired end C->>BS: finish() @@ -87,150 +72,98 @@ sequenceDiagram BS->>S: end() S-->>C: Ended else ownership remains - BS->>S: end() S-->>C: ActiveContextRemains - Note over C,P: same BoundBrowserSession + exact adapter remain available for cleanup/retry + Note over C,P: same BoundBrowserSession + exact adapter retained for cleanup/retry end ``` -`BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and exposes no lifecycle method that accepts a replacement port. `AuthorizedContextOperationPort` adds a typed, purpose-bounded post-create operation vocabulary without exposing the adapter itself. Tests retain inert observation state separately from the moved adapter. - -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, and `AuthorizedContextOperationRequest` are non-caller-constructible. The create request carries the reserved `BrowserContextEpoch` as a per-create transaction id; Browser Session alone decides whether the returned domain handle is accepted or rejected and validates current authority before any later adapter operation. - -For WebDriver BiDi, `DisposableIsolationId` maps to the user-context id created by `browser.createUserContext`. Protocol-specific pending/accepted/quarantined remote tuples and command semantics remain in the BiDi ACL boundary rather than this domain model. +`BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and accepts no replacement port on lifecycle methods. `AuthorizedContextOperationRequest` is privately constructed after exact `PresentationMutationAuthority` validation. A raw browser id, adapter-selected value, diagnostic view, or second adapter cannot mint Browser Session authority. -## Recovery, transport, and abandonment state +## RecoveryCustody and exact same-adapter recovery ```mermaid stateDiagram-v2 [*] --> Active - Active --> Active: create candidate + exact Accepted completion + authority - Active --> Active: authorized operation / current authority / exact bound adapter - Active --> Active: context epoch advanced / prior authority stale - Active --> Active: exact owned isolation destruction proved / remove hot record - Active --> Active: DisposableContextCreateError::CreateFailedClean - Active --> Active: failed finish / retain same bound owner - Active --> RecoveryRequired: CreateFailedUncertain / retain attempt provenance - Active --> RecoveryRequired: duplicate output + exact Rejected completion + sibling RecoveryRequiredOwnedHandle - Active --> RecoveryRequired: completion unproven / retain UnsettledAdapterHandle + exact attempt + sibling evidence - Active --> RecoveryRequired: DisposableContextDestroyError / keep failed record Uncertain + sibling evidence - Active --> Ended: hot ownership empty + finish() - Active --> TransportLost: browser transport lost / retain TransportLossOwnedHandle / mark uncertain - RecoveryRequired --> RecoveryRequired: transport_lost = true / preserve stronger recovery state - RecoveryRequired --> RecoveryCustody: into_recovery(self) / move exact adapter + evidence / no I/O - TransportLost --> RecoveryCustody: into_recovery(self) / move exact adapter + evidence / no I/O - RecoveryCustody --> [*]: persist or protocol-reconcile elsewhere; no ordinary authority surface - Ended --> [*] + Active --> Active: create + exact Accepted completion + Active --> Active: current authorized operation + Active --> Active: proven destroy / remove live hot-ownership record + Active --> RecoveryRequired: uncertain create / rejected-unsettled completion + Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven + Active --> TransportLost: transport_lost / TransportLossOwnedHandle + RecoveryRequired --> RecoveryCustody: into_recovery(self) + TransportLost --> RecoveryCustody: into_recovery(self) + RecoveryCustody --> RecoveryCustody: execute_recovery_context_operation(operation) + Active --> Ended: finish() after proven cleanup + RecoveryCustody --> [*]: persist or reconcile externally note right of RecoveryRequired - BrowserSessionRecoveryEvidence retains known - partial identity, duplicate/unsettled handle, - exact unproven-destruction handle + epoch, and - RecoveryRequiredOwnedHandle for indirect siblings. - DisposableContextCreateRecoveryEvidence retains - create-attempt epoch/disposition separately. - Neither evidence family grants I/O authority. + Exact BrowserSessionRecoveryEvidence and + DisposableContextCreateRecoveryEvidence are + non-authorizing. RecoveryRequiredOwnedHandle + preserves indirectly uncertain siblings. end note note right of RecoveryCustody - BoundBrowserSessionRecovery

exposes only - state + exact non-authorizing evidence. - It does not expose raw P, BrowserSession, - create, presentation authority, epoch advance, - destroy, authorized operation, or finish. + BoundBrowserSessionRecovery

retains the + exact consumed adapter. It exposes state, + evidence, and only RecoveryContextOperationPort. + Raw P and ordinary Browser Session authority + remain inaccessible. end note ``` ```mermaid sequenceDiagram autonumber - participant C as Application service - participant BS as BoundBrowserSession + participant C as Recovery owner / application service participant R as BoundBrowserSessionRecovery - participant P as exact bound adapter - participant O as Operability / recovery observer - - C->>BS: create accepted remote ownership - alt premature finish - C->>BS: finish() - BS-->>C: ActiveContextRemains; wrapper retained - C->>BS: destroy exact authority - BS->>P: proven remote destruction - BS->>BS: remove live hot-ownership record - C->>BS: finish() - BS-->>C: Ended - else unresolved ownership enters recovery - C->>BS: destroy failure or record_transport_loss() - BS-->>C: RecoveryRequired or TransportLost + exact evidence - C->>BS: into_recovery(self) - BS-->>R: move exact non-Clone adapter + evidence; adapter I/O = 0 - Note over R,P: recovery custody cannot regain ordinary lifecycle/presentation authority - else ordinary wrapper abandonment - C-xBS: drop without proven cleanup - Note over BS,P: Drop performs no browser I/O - BS->>O: increment abandoned_bound_session_count() - Note over O: process-local signal only; not destruction proof or durable exact-handle storage - end + participant BS as retained BoundBrowserSession + participant P as exact consumed adapter + participant B as Browser / protocol endpoint + + C->>BS: unresolved destroy or transport loss + BS->>BS: RecoveryRequired or TransportLost + exact evidence + C->>BS: into_recovery(self) + BS-->>R: move exact BoundBrowserSession + same adapter; no I/O + + C->>R: execute_recovery_context_operation(operation) + R->>R: snapshot session/incarnation/state + both evidence ledgers + R->>BS: crate-private dispatch_recovery_operation + BS->>P: RecoveryContextOperationRequest(operation + provenance) + P->>B: adapter-owned purpose-bounded recovery command + B-->>P: result + P-->>R: Output or RecoveryContextOperationError::Adapter + Note over R,BS: success/failure does not clear Browser Session uncertainty + Note over R,P: no raw P, no generic caller callback, no ordinary create/destroy/presentation authority ``` -Recovery custody is deliberately narrower than protocol reconciliation. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth and any purpose-bounded protocol recovery operation that uses the exact adapter held by recovery custody. Durable crash/process-restart persistence remains open until a canonical recovery owner stores exact recovery evidence before process termination. +Recovery custody is narrower than protocol reconciliation. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery-command semantics. `RecoveryContextOperationPort` only provides the same-consumed-adapter conduit. Adapter success is not destruction proof. -## Same-raw-identity hot-state hostile case +Dropping unresolved ordinary or recovery custody performs no browser I/O. `abandoned_bound_session_count()` is a process-local operability signal, not durable exact-handle storage or proof of cleanup. + +## Navigation / presentation interaction ```mermaid -sequenceDiagram - autonumber - participant C as Application service - participant BS as BoundBrowserSession - participant P as exact lifecycle port - - C->>BS: create U/C - BS->>P: create(attempt epoch 1) + Accepted - BS-->>C: authority epoch 1 - C->>BS: destroy(authority epoch 1) - BS->>P: destroy exact U/C + epoch 1 - P-->>BS: destruction proved - BS->>BS: remove U/C from hot ownership - - C->>BS: destroy(retained authority epoch 1) - BS-->>C: ContextNotOwned - Note over BS,P: stale check rejects before adapter I/O - - C->>BS: create same raw U/C again - BS->>P: create(attempt epoch 2) + Accepted - BS-->>C: authority epoch 2 - C->>BS: destroy(retained authority epoch 1) - BS-->>C: AuthorityMismatch - Note over BS,P: same raw values cannot resurrect predecessor epoch +stateDiagram-v2 + [*] --> Established + Established --> Pending: exact observed navigation start / mint NavigationSettlementAuthority + Pending --> Pending: commit progress + Pending --> Eligible: positive settlement + Pending --> Eligible: Failed or Aborted + Pending --> Eligible: download start + Pending --> Pending: newer navigation supersedes witness + Eligible --> Established: explicit reestablish_presentation_authority / next epoch + Established --> [*]: proven lifecycle destruction + Pending --> [*]: proven lifecycle destruction + Eligible --> [*]: proven lifecycle destruction ``` -The hostile acceptance repeats this create → proven destroy → same-handle recreate cycle for 258 ownership generations. Hot command-authority state remains bounded to live/uncertain ownership instead of accumulating proven-destroyed tombstones. Durable audit/history retention is a separate persistence concern. +Navigation admission is bound to exact `BrowserSessionIncarnation`, `BrowsingContextId`, and current `BrowserContextEpoch`. The opaque navigation witness, not raw WebDriver BiDi navigation ids, controls terminal assignment. A navigation-invalidated `PresentationMutationAuthority` cannot authorize presentation mutation or authority-based cleanup. The exact bound lifecycle owner can still destroy its owned context without reopening presentation authority. -## Sequential ABA hostile case +## Same-raw-identity and Sequential ABA hostile cases -```mermaid -sequenceDiagram - autonumber - participant A as BoundBrowserSession A - participant B as BoundBrowserSession B - participant PA as Lifecycle port A - participant PB as Lifecycle port B - - A->>A: start(S) => incarnation A; bind PA - A->>PA: create(request S, incarnation A, attempt 1) - PA-->>A: U, C pending - A->>PA: completion Accepted(attempt 1) - A->>PA: destroy(request S, incarnation A, U/C) - A->>A: finish() - - B->>B: start(S) => incarnation B; bind PB - B->>PB: create(request S, incarnation B, attempt 1) - PB-->>B: same U, same C pending - B->>PB: completion Accepted(attempt 1) - Note over A,B: local attempt/epoch may both equal 1, but incarnations differ - B->>B: validate retained authority A - B-->>A: AuthorityMismatch before PB adapter I/O - B->>PB: operate/destroy only with authority B + incarnation B -``` +A single aggregate may create → prove destroy → recreate the same raw isolation/browsing-context values for **258 ownership generations**. Hot command-authority state remains bounded to live/uncertain ownership. The predecessor authority fails as `ContextNotOwned` immediately after destruction and as `AuthorityMismatch` after same-raw-id recreation because the epoch is monotonic. + +Across aggregate restart/recreation, `BrowserSessionIncarnation` prevents a retained authority from aggregate A from becoming valid in aggregate B even when raw `BrowserSessionId`, isolation, browsing-context id, and local epoch numerically alias. -`RecoveryRequired` and `TransportLost` remain closed to normal lifecycle and presentation authority. `into_recovery(self)` is a one-way custody transfer, not a command-authority resurrection path; later protocol reconciliation must stay purpose-bounded and must not infer cleanup authority from raw identifiers or treat command ACK as proof of destruction. \ No newline at end of file +`RecoveryRequired` and `TransportLost` remain closed to ordinary lifecycle and presentation authority. `into_recovery(self)` is a one-way custody transfer, not command-authority resurrection; later protocol reconciliation stays purpose-bounded and must not infer cleanup authority from raw identifiers or treat command ACK as proof of destruction. \ No newline at end of file From d5ea4459baca23477dac12874e31a5a8af6bb31a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:21:40 +0900 Subject: [PATCH 114/632] test(browser-session): pin recovery-operation module boundary --- ...ser_session_recovery_operation_contract.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_browser_session_recovery_operation_contract.py diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py new file mode 100644 index 000000000..4b09f9eb8 --- /dev/null +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -0,0 +1,90 @@ +"""Repository contracts for Browser Session recovery-only adapter custody.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CRATE = ROOT / "crates/originweave-browser-session" + + +class BrowserSessionRecoveryOperationContractTests(unittest.TestCase): + """Keep recovery I/O purpose-bounded to the exact consumed adapter.""" + + def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: + """Do not reopen raw adapter access to bridge recovery custody.""" + + lib_source = (CRATE / "src/lib.rs").read_text(encoding="utf-8") + browser_source = (CRATE / "src/browser_session.rs").read_text(encoding="utf-8") + recovery_source = (CRATE / "src/recovery.rs").read_text(encoding="utf-8") + + self.assertIn("mod browser_session;", lib_source) + self.assertNotIn('include!("browser_session.rs")', lib_source) + self.assertIn("pub(crate) fn dispatch_recovery_operation", browser_source) + self.assertNotIn("pub fn dispatch_recovery_operation", browser_source) + + for symbol in ( + "pub struct RecoveryContextOperationRequest", + "pub trait RecoveryContextOperationPort", + "pub enum RecoveryContextOperationError", + "pub fn execute_recovery_context_operation", + ): + self.assertIn(symbol, recovery_source) + + request_impl = recovery_source.split( + "impl RecoveryContextOperationRequest", 1 + )[1].split("pub trait RecoveryContextOperationPort", 1)[0] + self.assertNotIn("pub fn new", request_impl) + self.assertNotIn("pub const fn new", request_impl) + self.assertNotIn("pub fn browser_session(&self)", recovery_source) + self.assertNotIn("pub fn port", recovery_source) + self.assertNotIn("pub const fn port", recovery_source) + + def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> None: + """Adapter success or failure must not silently become reconciliation proof.""" + + hostile = (CRATE / "tests/recovery_same_adapter_operation.rs").read_text( + encoding="utf-8" + ) + for token in ( + "RecoveryContextOperationPort", + "execute_recovery_context_operation", + "request.browser_session()", + "request.incarnation()", + "request.state()", + "request.recovery_evidence()", + "request.create_attempt_recovery_evidence()", + "RecoveryContextOperationError::Adapter", + ): + self.assertIn(token, hostile) + self.assertIn("state_before", hostile) + self.assertIn("evidence_before", hostile) + + def test_architecture_docs_describe_current_recovery_surface(self) -> None: + """ADR, traceability, and UML must not describe the pre-operation wrapper.""" + + adr = ( + ROOT / "docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md" + ).read_text(encoding="utf-8") + trace = ( + ROOT / "docs/traceability/browser-session-lifecycle-authority.md" + ).read_text(encoding="utf-8") + uml = (ROOT / "docs/uml/browser-session-lifecycle-authority.md").read_text( + encoding="utf-8" + ) + + for document in (adr, trace, uml): + self.assertIn("RecoveryContextOperationPort", document) + self.assertIn("RecoveryContextOperationRequest", document) + self.assertIn("same", document.lower()) + self.assertIn("pub(crate)", adr) + self.assertIn("pub(crate)", trace) + self.assertIn("success/failure does not clear Browser Session uncertainty", uml) + self.assertIn("#316", adr) + self.assertIn("#316", trace) + self.assertIn("#316", uml) + + +if __name__ == "__main__": + unittest.main() From c000d967fb253cb4efcb2385dafbc779d6331875 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:22:04 +0900 Subject: [PATCH 115/632] test(browser-session): match recovery evidence fixture --- tests/test_browser_session_recovery_operation_contract.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index 4b09f9eb8..e970bd028 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -56,10 +56,11 @@ def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> Non "request.recovery_evidence()", "request.create_attempt_recovery_evidence()", "RecoveryContextOperationError::Adapter", + "expected_recovery_evidence", + "generic recovery adapter success is not itself destruction or reconciliation proof", + "recovery operation dispatch must not erase unresolved ownership evidence", ): self.assertIn(token, hostile) - self.assertIn("state_before", hostile) - self.assertIn("evidence_before", hostile) def test_architecture_docs_describe_current_recovery_surface(self) -> None: """ADR, traceability, and UML must not describe the pre-operation wrapper.""" From d4b9b878fe5a18ec1ca6dce4aca1f7bc6c116a8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:36:30 +0900 Subject: [PATCH 116/632] test(browser-session): close recovery request mint paths --- ...ser_session_recovery_operation_contract.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index e970bd028..fb5ff3fee 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -3,6 +3,7 @@ from __future__ import annotations import pathlib +import re import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -32,11 +33,38 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: ): self.assertIn(symbol, recovery_source) + request_struct = recovery_source.split( + "pub struct RecoveryContextOperationRequest {", 1 + )[1].split("\n}", 1)[0] + self.assertNotRegex(request_struct, r"(?m)^\s*pub(?:\([^)]*\))?\s+") + + inherent_impls = re.findall( + r"impl(?:<[^\n{]+>)?\s+RecoveryContextOperationRequest(?:<[^\n{]+>)?\s*\{", + recovery_source, + ) + self.assertEqual(len(inherent_impls), 1) request_impl = recovery_source.split( "impl RecoveryContextOperationRequest", 1 )[1].split("pub trait RecoveryContextOperationPort", 1)[0] - self.assertNotIn("pub fn new", request_impl) - self.assertNotIn("pub const fn new", request_impl) + public_methods = re.findall( + r"(?m)^\s*pub(?:\s+const)?\s+fn\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)", + request_impl, + ) + self.assertGreater(len(public_methods), 0) + for method_name, parameters in public_methods: + self.assertIn( + "&self", + parameters, + f"{method_name} must remain an accessor, not a public construction path", + ) + + for constructor_pattern in ( + r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::default::)?Default\s+for\s+RecoveryContextOperationRequest", + r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::convert::)?From<[^\n{]+>\s+for\s+RecoveryContextOperationRequest", + r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::convert::)?TryFrom<[^\n{]+>\s+for\s+RecoveryContextOperationRequest", + ): + self.assertNotRegex(recovery_source, constructor_pattern) + self.assertNotIn("pub fn browser_session(&self)", recovery_source) self.assertNotIn("pub fn port", recovery_source) self.assertNotIn("pub const fn port", recovery_source) @@ -57,8 +85,10 @@ def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> Non "request.create_attempt_recovery_evidence()", "RecoveryContextOperationError::Adapter", "expected_recovery_evidence", + "expected_create_attempt_recovery_evidence", "generic recovery adapter success is not itself destruction or reconciliation proof", "recovery operation dispatch must not erase unresolved ownership evidence", + "recovery operation dispatch must not erase create-attempt provenance", ): self.assertIn(token, hostile) From d4bcd192679bba6714098e42f9443c9e2248cbd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:36:59 +0900 Subject: [PATCH 117/632] test(browser-session): preserve both recovery ledgers --- .../tests/recovery_same_adapter_operation.rs | 126 ++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs index 9c759fe8b..3f6407a1b 100644 --- a/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs +++ b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs @@ -4,10 +4,10 @@ use std::rc::Rc; use originweave_browser_session::{ BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, - DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, - DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, - DisposableIsolationId, RecoveryContextOperationError, RecoveryContextOperationPort, - RecoveryContextOperationRequest, + DisposableContextCreateError, DisposableContextCreateRecoveryEvidence, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + RecoveryContextOperationError, RecoveryContextOperationPort, RecoveryContextOperationRequest, }; use originweave_core::{BrowserSessionId, BrowsingContextId}; @@ -27,12 +27,13 @@ struct RecoveryObservation { incarnation: u64, state: BrowserSessionState, recovery_evidence: Vec, - create_evidence_count: usize, + create_attempt_recovery_evidence: Vec, operation: RecoveryOperation, } struct RecoveryPort { handle: DisposableContextHandle, + fail_create_uncertain: bool, fail_destroy: bool, fail_recovery: Rc>, recovery_calls: Rc>, @@ -44,7 +45,13 @@ impl DisposableContextPort for RecoveryPort { &mut self, _request: &DisposableContextCreateRequest, ) -> Result { - Ok(self.handle.clone()) + if self.fail_create_uncertain { + Err(DisposableContextCreateError::CreateFailedUncertain(Some( + self.handle.isolation().clone(), + ))) + } else { + Ok(self.handle.clone()) + } } fn complete_disposable_context_creation( @@ -81,7 +88,9 @@ impl RecoveryContextOperationPort for RecoveryPort { incarnation: request.incarnation().value(), state: request.state(), recovery_evidence: request.recovery_evidence().to_vec(), - create_evidence_count: request.create_attempt_recovery_evidence().len(), + create_attempt_recovery_evidence: request + .create_attempt_recovery_evidence() + .to_vec(), operation: *request.operation(), }); if self.fail_recovery.get() { @@ -117,6 +126,7 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter ); let port = RecoveryPort { handle: expected_handle.clone(), + fail_create_uncertain: false, fail_destroy: true, fail_recovery: Rc::clone(&fail_recovery), recovery_calls: Rc::clone(&recovery_calls), @@ -141,6 +151,10 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter context_epoch: authority.context_epoch(), }] ); + let expected_create_attempt_recovery_evidence = bound + .browser_session() + .create_attempt_recovery_evidence() + .to_vec(); let mut recovery = bound .into_recovery() @@ -155,6 +169,11 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter assert_eq!(recovery_calls.get(), 1); assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); assert_eq!(recovery.recovery_evidence(), expected_recovery_evidence); + assert_eq!( + recovery.create_attempt_recovery_evidence(), + expected_create_attempt_recovery_evidence, + "recovery operation dispatch must not erase create-attempt provenance" + ); let first = observations.borrow(); assert_eq!(first.len(), 1); @@ -162,7 +181,10 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter assert_eq!(first[0].incarnation, expected_incarnation.value()); assert_eq!(first[0].state, BrowserSessionState::RecoveryRequired); assert_eq!(first[0].recovery_evidence, expected_recovery_evidence); - assert_eq!(first[0].create_evidence_count, 0); + assert_eq!( + first[0].create_attempt_recovery_evidence, + expected_create_attempt_recovery_evidence + ); assert_eq!( first[0].operation, RecoveryOperation::ReconcileExactEvidence @@ -184,5 +206,93 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter expected_recovery_evidence, "recovery operation dispatch must not erase unresolved ownership evidence" ); + assert_eq!( + recovery.create_attempt_recovery_evidence(), + expected_create_attempt_recovery_evidence, + "recovery operation dispatch must not erase create-attempt provenance" + ); + Ok(()) +} + +#[test] +fn recovery_dispatch_preserves_non_empty_create_attempt_provenance_on_failure_and_success( +) -> Result<(), &'static str> { + let fail_recovery = Rc::new(Cell::new(true)); + let recovery_calls = Rc::new(Cell::new(0)); + let observations = Rc::new(RefCell::new(Vec::new())); + let expected_session = session(7_902)?; + let expected_handle = DisposableContextHandle::new( + isolation("same-adapter-uncertain-create")?, + context(79_020)?, + ); + let port = RecoveryPort { + handle: expected_handle, + fail_create_uncertain: true, + fail_destroy: false, + fail_recovery: Rc::clone(&fail_recovery), + recovery_calls: Rc::clone(&recovery_calls), + observations: Rc::clone(&observations), + }; + let mut bound = BrowserSession::start(expected_session) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let expected_incarnation = bound.browser_session().incarnation(); + + assert!(matches!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + )); + let expected_recovery_evidence = bound.browser_session().recovery_evidence().to_vec(); + let expected_create_attempt_recovery_evidence = bound + .browser_session() + .create_attempt_recovery_evidence() + .to_vec(); + assert!(!expected_recovery_evidence.is_empty()); + assert!(matches!( + expected_create_attempt_recovery_evidence.as_slice(), + [DisposableContextCreateRecoveryEvidence::FailedUncertain { .. }] + )); + + let mut recovery = bound + .into_recovery() + .map_err(|_| "uncertain create must enter recovery custody")?; + assert_eq!( + recovery.execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence), + Err(RecoveryContextOperationError::Adapter( + RecoveryOperationFailure::BackendUnavailable + )) + ); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence(), expected_recovery_evidence); + assert_eq!( + recovery.create_attempt_recovery_evidence(), + expected_create_attempt_recovery_evidence, + "recovery operation dispatch must not erase create-attempt provenance" + ); + + let first = observations.borrow(); + assert_eq!(first.len(), 1); + assert_eq!(first[0].browser_session, expected_session); + assert_eq!(first[0].incarnation, expected_incarnation.value()); + assert_eq!(first[0].state, BrowserSessionState::RecoveryRequired); + assert_eq!(first[0].recovery_evidence, expected_recovery_evidence); + assert_eq!( + first[0].create_attempt_recovery_evidence, + expected_create_attempt_recovery_evidence + ); + drop(first); + + fail_recovery.set(false); + recovery + .execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence) + .map_err(|_| "recovery success must use the retained adapter")?; + assert_eq!(recovery_calls.get(), 2); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence(), expected_recovery_evidence); + assert_eq!( + recovery.create_attempt_recovery_evidence(), + expected_create_attempt_recovery_evidence, + "recovery operation dispatch must not erase create-attempt provenance" + ); Ok(()) } From d1f2b163c47880cb68aa36d461084443e131cbc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:42:45 +0900 Subject: [PATCH 118/632] test(browser-session): close qualified recovery mint paths --- ...ser_session_recovery_operation_contract.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index fb5ff3fee..adfc10d0f 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -38,11 +38,23 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: )[1].split("\n}", 1)[0] self.assertNotRegex(request_struct, r"(?m)^\s*pub(?:\([^)]*\))?\s+") - inherent_impls = re.findall( - r"impl(?:<[^\n{]+>)?\s+RecoveryContextOperationRequest(?:<[^\n{]+>)?\s*\{", - recovery_source, + impl_headers = re.findall(r"(?ms)^\s*impl\b([^{}]*)\{", recovery_source) + request_impl_headers = [ + re.sub(r"\s+", " ", header).strip() + for header in impl_headers + if "RecoveryContextOperationRequest" in header + ] + inherent_impl_headers = [ + header + for header in request_impl_headers + if " for RecoveryContextOperationRequest" not in header + ] + self.assertEqual( + inherent_impl_headers, + [" RecoveryContextOperationRequest"], + "request accessors must remain the sole inherent impl; where-clause or multiline successors require review", ) - self.assertEqual(len(inherent_impls), 1) + request_impl = recovery_source.split( "impl RecoveryContextOperationRequest", 1 )[1].split("pub trait RecoveryContextOperationPort", 1)[0] @@ -59,9 +71,9 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: ) for constructor_pattern in ( - r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::default::)?Default\s+for\s+RecoveryContextOperationRequest", - r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::convert::)?From<[^\n{]+>\s+for\s+RecoveryContextOperationRequest", - r"impl(?:<[^\n{]+>)?\s+(?:(?:core|std)::convert::)?TryFrom<[^\n{]+>\s+for\s+RecoveryContextOperationRequest", + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::default::)?Default\s+for\s+RecoveryContextOperationRequest", + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::convert::)?From<[^{}]+?>\s+for\s+RecoveryContextOperationRequest", + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::convert::)?TryFrom<[^{}]+?>\s+for\s+RecoveryContextOperationRequest", ): self.assertNotRegex(recovery_source, constructor_pattern) From 04e17d3964cb71c4e8cb0aed78a6a7294dfc2d92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 09:48:18 +0900 Subject: [PATCH 119/632] test(browser-session): lex recovery impl headers safely --- ...ser_session_recovery_operation_contract.py | 144 +++++++++++++++++- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index adfc10d0f..09269e038 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -10,6 +10,119 @@ CRATE = ROOT / "crates/originweave-browser-session" +def _rust_impl_headers(source: str) -> list[str]: + """Return Rust impl headers while ignoring nested delimiters and literal/comment text.""" + + headers: list[str] = [] + length = len(source) + index = 0 + + def skip_non_code(position: int) -> int: + if source.startswith("//", position): + newline = source.find("\n", position + 2) + return length if newline < 0 else newline + 1 + if source.startswith("/*", position): + depth = 1 + cursor = position + 2 + while cursor < length and depth: + if source.startswith("/*", cursor): + depth += 1 + cursor += 2 + elif source.startswith("*/", cursor): + depth -= 1 + cursor += 2 + else: + cursor += 1 + return cursor + + raw = re.match(r"(?:br|r)(?P#{0,255})\"", source[position:]) + if raw: + hashes = raw.group("hashes") + cursor = position + raw.end() + terminator = '"' + hashes + end = source.find(terminator, cursor) + return length if end < 0 else end + len(terminator) + + if source[position] in ('"', "'"): + quote = source[position] + cursor = position + 1 + while cursor < length: + if source[cursor] == "\\": + cursor += 2 + continue + if source[cursor] == quote: + return cursor + 1 + if quote == "'" and source[cursor] == "\n": + # A Rust lifetime such as 'a is not a character literal. + return position + 1 + cursor += 1 + return position + 1 if quote == "'" else length + return position + + while index < length: + skipped = skip_non_code(index) + if skipped != index: + index = skipped + continue + match = re.match(r"impl\b", source[index:]) + if not match: + index += 1 + continue + if index > 0 and (source[index - 1].isalnum() or source[index - 1] == "_"): + index += 1 + continue + + start = index + cursor = index + match.end() + angle_depth = 0 + paren_depth = 0 + bracket_depth = 0 + nested_brace_depth = 0 + body_start: int | None = None + + while cursor < length: + skipped = skip_non_code(cursor) + if skipped != cursor: + cursor = skipped + continue + char = source[cursor] + if nested_brace_depth: + if char == "{": + nested_brace_depth += 1 + elif char == "}": + nested_brace_depth -= 1 + cursor += 1 + continue + if char == "(" : + paren_depth += 1 + elif char == ")" and paren_depth: + paren_depth -= 1 + elif char == "[": + bracket_depth += 1 + elif char == "]" and bracket_depth: + bracket_depth -= 1 + elif char == "<" and paren_depth == 0 and bracket_depth == 0: + angle_depth += 1 + elif char == ">" and angle_depth and paren_depth == 0 and bracket_depth == 0: + angle_depth -= 1 + elif char == "{": + if angle_depth == 0 and paren_depth == 0 and bracket_depth == 0: + body_start = cursor + break + nested_brace_depth = 1 + elif char == ";" and angle_depth == 0 and paren_depth == 0 and bracket_depth == 0: + break + cursor += 1 + + if body_start is not None: + headers.append(re.sub(r"\s+", " ", source[start:body_start]).strip()) + index = body_start + 1 + else: + index = cursor + 1 + + return headers + + class BrowserSessionRecoveryOperationContractTests(unittest.TestCase): """Keep recovery I/O purpose-bounded to the exact consumed adapter.""" @@ -38,10 +151,9 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: )[1].split("\n}", 1)[0] self.assertNotRegex(request_struct, r"(?m)^\s*pub(?:\([^)]*\))?\s+") - impl_headers = re.findall(r"(?ms)^\s*impl\b([^{}]*)\{", recovery_source) request_impl_headers = [ - re.sub(r"\s+", " ", header).strip() - for header in impl_headers + header + for header in _rust_impl_headers(recovery_source) if "RecoveryContextOperationRequest" in header ] inherent_impl_headers = [ @@ -51,8 +163,8 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: ] self.assertEqual( inherent_impl_headers, - [" RecoveryContextOperationRequest"], - "request accessors must remain the sole inherent impl; where-clause or multiline successors require review", + ["impl RecoveryContextOperationRequest"], + "request accessors must remain the sole inherent impl; every new inherent impl requires review", ) request_impl = recovery_source.split( @@ -81,6 +193,28 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: self.assertNotIn("pub fn port", recovery_source) self.assertNotIn("pub const fn port", recovery_source) + def test_impl_header_parser_keeps_braced_const_where_predicates_visible(self) -> None: + """A braced const expression in a where clause must not hide a second inherent impl.""" + + sample = """ +impl RecoveryContextOperationRequest { + pub fn operation(&self) -> &O { todo!() } +} +impl RecoveryContextOperationRequest +where + O: RecoveryMarker<{ 1 + 1 }>, +{ + pub fn from_raw(operation: O) -> Self { todo!() } +} +""" + request_headers = [ + header + for header in _rust_impl_headers(sample) + if "RecoveryContextOperationRequest" in header + ] + self.assertEqual(len(request_headers), 2) + self.assertIn("RecoveryMarker<{ 1 + 1 }>", request_headers[1]) + def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> None: """Adapter success or failure must not silently become reconciliation proof.""" From ccd3e0ac666182f43ab7fc2161c98fb77e9823ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 10:00:19 +0900 Subject: [PATCH 120/632] test(browser-session): strip comment text from impl classification --- ...ser_session_recovery_operation_contract.py | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index 09269e038..9417e91a3 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -79,10 +79,14 @@ def skip_non_code(position: int) -> int: bracket_depth = 0 nested_brace_depth = 0 body_start: int | None = None + comment_ranges: list[tuple[int, int]] = [] while cursor < length: + is_comment = source.startswith("//", cursor) or source.startswith("/*", cursor) skipped = skip_non_code(cursor) if skipped != cursor: + if is_comment: + comment_ranges.append((cursor, skipped)) cursor = skipped continue char = source[cursor] @@ -93,7 +97,7 @@ def skip_non_code(position: int) -> int: nested_brace_depth -= 1 cursor += 1 continue - if char == "(" : + if char == "(": paren_depth += 1 elif char == ")" and paren_depth: paren_depth -= 1 @@ -115,7 +119,14 @@ def skip_non_code(position: int) -> int: cursor += 1 if body_start is not None: - headers.append(re.sub(r"\s+", " ", source[start:body_start]).strip()) + header_parts: list[str] = [] + fragment_start = start + for comment_start, comment_end in comment_ranges: + header_parts.append(source[fragment_start:comment_start]) + header_parts.append(" ") + fragment_start = comment_end + header_parts.append(source[fragment_start:body_start]) + headers.append(re.sub(r"\s+", " ", "".join(header_parts)).strip()) index = body_start + 1 else: index = cursor + 1 @@ -215,6 +226,42 @@ def test_impl_header_parser_keeps_braced_const_where_predicates_visible(self) -> self.assertEqual(len(request_headers), 2) self.assertIn("RecoveryMarker<{ 1 + 1 }>", request_headers[1]) + def test_impl_header_parser_removes_comment_text_before_classification(self) -> None: + """Comment text must not disguise an inherent request implementation as a trait impl.""" + + sample = """ +impl RecoveryContextOperationRequest { + pub fn operation(&self) -> &O { todo!() } +} +impl RecoveryContextOperationRequest +// for RecoveryContextOperationRequest +{ + pub fn from_raw(operation: O) -> Self { todo!() } +} +impl RecoveryContextOperationRequest +/* for RecoveryContextOperationRequest */ +{ + pub fn from_raw_again(operation: O) -> Self { todo!() } +} +""" + request_headers = [ + header + for header in _rust_impl_headers(sample) + if "RecoveryContextOperationRequest" in header + ] + self.assertEqual( + request_headers, + ["impl RecoveryContextOperationRequest"] * 3, + ) + self.assertEqual( + [ + header + for header in request_headers + if " for RecoveryContextOperationRequest" not in header + ], + request_headers, + ) + def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> None: """Adapter success or failure must not silently become reconciliation proof.""" From a3a93f5ea0d5e8987d720df4b77d4ee103fcfc1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 10:06:58 +0900 Subject: [PATCH 121/632] test(browser-session): exclude literal text from impl classification --- ...ser_session_recovery_operation_contract.py | 74 ++++++++++++++----- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index 9417e91a3..8cba2cd36 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -17,6 +17,19 @@ def _rust_impl_headers(source: str) -> list[str]: length = len(source) index = 0 + def starts_lifetime(position: int) -> bool: + """Distinguish Rust lifetimes/labels from quoted character literals.""" + + if source[position] != "'" or position + 1 >= length: + return False + cursor = position + 1 + if not (source[cursor].isalpha() or source[cursor] == "_"): + return False + cursor += 1 + while cursor < length and (source[cursor].isalnum() or source[cursor] == "_"): + cursor += 1 + return cursor >= length or source[cursor] != "'" + def skip_non_code(position: int) -> int: if source.startswith("//", position): newline = source.find("\n", position + 2) @@ -43,6 +56,9 @@ def skip_non_code(position: int) -> int: end = source.find(terminator, cursor) return length if end < 0 else end + len(terminator) + if source[position] == "'" and starts_lifetime(position): + return position + if source[position] in ('"', "'"): quote = source[position] cursor = position + 1 @@ -52,11 +68,8 @@ def skip_non_code(position: int) -> int: continue if source[cursor] == quote: return cursor + 1 - if quote == "'" and source[cursor] == "\n": - # A Rust lifetime such as 'a is not a character literal. - return position + 1 cursor += 1 - return position + 1 if quote == "'" else length + return length return position while index < length: @@ -79,14 +92,12 @@ def skip_non_code(position: int) -> int: bracket_depth = 0 nested_brace_depth = 0 body_start: int | None = None - comment_ranges: list[tuple[int, int]] = [] + non_code_ranges: list[tuple[int, int]] = [] while cursor < length: - is_comment = source.startswith("//", cursor) or source.startswith("/*", cursor) skipped = skip_non_code(cursor) if skipped != cursor: - if is_comment: - comment_ranges.append((cursor, skipped)) + non_code_ranges.append((cursor, skipped)) cursor = skipped continue char = source[cursor] @@ -121,10 +132,10 @@ def skip_non_code(position: int) -> int: if body_start is not None: header_parts: list[str] = [] fragment_start = start - for comment_start, comment_end in comment_ranges: - header_parts.append(source[fragment_start:comment_start]) + for non_code_start, non_code_end in non_code_ranges: + header_parts.append(source[fragment_start:non_code_start]) header_parts.append(" ") - fragment_start = comment_end + fragment_start = non_code_end header_parts.append(source[fragment_start:body_start]) headers.append(re.sub(r"\s+", " ", "".join(header_parts)).strip()) index = body_start + 1 @@ -226,10 +237,10 @@ def test_impl_header_parser_keeps_braced_const_where_predicates_visible(self) -> self.assertEqual(len(request_headers), 2) self.assertIn("RecoveryMarker<{ 1 + 1 }>", request_headers[1]) - def test_impl_header_parser_removes_comment_text_before_classification(self) -> None: - """Comment text must not disguise an inherent request implementation as a trait impl.""" + def test_impl_header_parser_removes_non_code_text_before_classification(self) -> None: + """Comments and literals must not disguise an inherent request impl as a trait impl.""" - sample = """ + sample = r''' impl RecoveryContextOperationRequest { pub fn operation(&self) -> &O { todo!() } } @@ -243,16 +254,25 @@ def test_impl_header_parser_removes_comment_text_before_classification(self) -> { pub fn from_raw_again(operation: O) -> Self { todo!() } } -""" +impl RecoveryContextOperationRequest +where + O: RecoveryMarker<{ b" for RecoveryContextOperationRequest".len() }>, +{ + pub fn from_byte_literal(operation: O) -> Self { todo!() } +} +impl RecoveryContextOperationRequest +where + O: RecoveryMarker<{ br#" for RecoveryContextOperationRequest"#.len() }>, +{ + pub fn from_raw_literal(operation: O) -> Self { todo!() } +} +''' request_headers = [ header for header in _rust_impl_headers(sample) if "RecoveryContextOperationRequest" in header ] - self.assertEqual( - request_headers, - ["impl RecoveryContextOperationRequest"] * 3, - ) + self.assertEqual(len(request_headers), 5) self.assertEqual( [ header @@ -262,6 +282,22 @@ def test_impl_header_parser_removes_comment_text_before_classification(self) -> request_headers, ) + def test_impl_header_parser_preserves_lifetimes_while_skipping_char_literals(self) -> None: + """Apostrophe handling must not consume lifetimes while removing character literals.""" + + sample = """ +impl<'a, O> RecoveryContextOperationRequest<&'a O> +where + O: RecoveryMarker<'a, {'x' as u32}>, +{ + pub fn borrow(&self) -> &'a O { todo!() } +} +""" + headers = _rust_impl_headers(sample) + self.assertEqual(len(headers), 1) + self.assertIn("impl<'a, O> RecoveryContextOperationRequest<&'a O>", headers[0]) + self.assertNotIn("'x'", headers[0]) + def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> None: """Adapter success or failure must not silently become reconciliation proof.""" From 64098ce09414c8c2b0c7fd67352f653c14c5f03a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 11:04:22 +0900 Subject: [PATCH 122/632] test(browser-session): harden recovery impl classification --- ..._session_recovery_impl_surface_contract.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/test_browser_session_recovery_impl_surface_contract.py diff --git a/tests/test_browser_session_recovery_impl_surface_contract.py b/tests/test_browser_session_recovery_impl_surface_contract.py new file mode 100644 index 000000000..027f9fdba --- /dev/null +++ b/tests/test_browser_session_recovery_impl_surface_contract.py @@ -0,0 +1,137 @@ +"""Defense-in-depth contracts for recovery request impl classification.""" + +from __future__ import annotations + +import importlib.util +import pathlib +import re +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BASE_CONTRACT = ROOT / "tests/test_browser_session_recovery_operation_contract.py" +RECOVERY_SOURCE = ROOT / "crates/originweave-browser-session/src/recovery.rs" + +_spec = importlib.util.spec_from_file_location("_recovery_operation_contract", BASE_CONTRACT) +if _spec is None or _spec.loader is None: + raise RuntimeError("cannot load recovery operation contract helper") +_base_contract = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_base_contract) +_rust_impl_headers = _base_contract._rust_impl_headers + + +def _is_inherent_impl_header(header: str) -> bool: + """Classify impl kind using only top-level Rust tokens before a where clause.""" + + if not re.match(r"^impl\b", header): + return False + + angle_depth = 0 + paren_depth = 0 + bracket_depth = 0 + brace_depth = 0 + cursor = len("impl") + length = len(header) + + while cursor < length: + char = header[cursor] + if char == "<": + angle_depth += 1 + cursor += 1 + continue + if char == ">" and angle_depth: + angle_depth -= 1 + cursor += 1 + continue + if char == "(": + paren_depth += 1 + cursor += 1 + continue + if char == ")" and paren_depth: + paren_depth -= 1 + cursor += 1 + continue + if char == "[": + bracket_depth += 1 + cursor += 1 + continue + if char == "]" and bracket_depth: + bracket_depth -= 1 + cursor += 1 + continue + if char == "{": + brace_depth += 1 + cursor += 1 + continue + if char == "}" and brace_depth: + brace_depth -= 1 + cursor += 1 + continue + + top_level = not (angle_depth or paren_depth or bracket_depth or brace_depth) + if top_level and (char.isalpha() or char == "_"): + end = cursor + 1 + while end < length and (header[end].isalnum() or header[end] == "_"): + end += 1 + token = header[cursor:end] + if token == "for": + return False + if token == "where": + return True + cursor = end + continue + cursor += 1 + + return True + + +class BrowserSessionRecoveryImplSurfaceContractTests(unittest.TestCase): + """Prevent nested token text from hiding a second inherent request impl.""" + + def test_current_request_has_one_inherent_impl(self) -> None: + """Trait classification must depend on top-level syntax, not substring text.""" + + recovery_source = RECOVERY_SOURCE.read_text(encoding="utf-8") + request_headers = [ + header + for header in _rust_impl_headers(recovery_source) + if "RecoveryContextOperationRequest" in header + ] + inherent_headers = [ + header for header in request_headers if _is_inherent_impl_header(header) + ] + self.assertEqual( + inherent_headers, + ["impl RecoveryContextOperationRequest"], + "request accessors must remain the sole inherent impl", + ) + + def test_nested_macro_for_tokens_do_not_disguise_inherent_impl(self) -> None: + """A legal macro token tree containing `for Type` is not a trait impl.""" + + sample = r''' +macro_rules! marker { + ($($tokens:tt)*) => { 2usize }; +} +impl RecoveryContextOperationRequest +where + O: RecoveryMarker<{ marker! { for RecoveryContextOperationRequest } }>, +{ + pub fn from_macro(operation: O) -> Self { todo!() } +} +impl RecoveryContextOperationPort for RecoveryContextOperationRequest { + fn execute(&mut self) { todo!() } +} +''' + request_headers = [ + header + for header in _rust_impl_headers(sample) + if "RecoveryContextOperationRequest" in header + ] + self.assertEqual(len(request_headers), 2) + self.assertTrue(_is_inherent_impl_header(request_headers[0])) + self.assertFalse(_is_inherent_impl_header(request_headers[1])) + self.assertIn(" for RecoveryContextOperationRequest", request_headers[0]) + + +if __name__ == "__main__": + unittest.main() From 0ade9f7e1b6d63940563dc47a3c0ac04d1515a7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 12:01:10 +0900 Subject: [PATCH 123/632] test(browser-session): reject empty recovery handoff --- ...y_handoff_requires_unresolved_ownership.rs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs diff --git a/crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs b/crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs new file mode 100644 index 000000000..55ed78170 --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs @@ -0,0 +1,96 @@ +use originweave_browser_session::{ + BrowserSession, BrowserSessionState, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct CleanPort { + handle: DisposableContextHandle, +} + +impl DisposableContextPort for CleanPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + Ok(self.handle.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +fn clean_port(context: u64, isolation: &str) -> Result { + let isolation = DisposableIsolationId::parse(isolation) + .map_err(|_| "static fixture isolation id must be valid")?; + let browsing_context = BrowsingContextId::new(context) + .map_err(|_| "static fixture browsing context id must be valid")?; + Ok(CleanPort { + handle: DisposableContextHandle::new(isolation, browsing_context), + }) +} + +#[test] +fn transport_loss_without_remote_ownership_cannot_enter_recovery_custody( +) -> Result<(), &'static str> { + let session = BrowserSession::start( + BrowserSessionId::new(7_120).map_err(|_| "static session id must be valid")?, + ) + .map_err(|_| "browser session incarnation must be available")?; + let mut bound = session.bind_lifecycle_port(clean_port(71_200, "unused-recovery-port")?); + + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert!(bound.browser_session().recovery_evidence().is_empty()); + assert!( + bound.into_recovery().is_err(), + "transport loss without unresolved remote ownership must not mint recovery adapter authority" + ); + Ok(()) +} + +#[test] +fn transport_loss_after_proven_destruction_cannot_reopen_recovery_custody( +) -> Result<(), &'static str> { + let session = BrowserSession::start( + BrowserSessionId::new(7_121).map_err(|_| "static session id must be valid")?, + ) + .map_err(|_| "browser session incarnation must be available")?; + let mut bound = session.bind_lifecycle_port(clean_port(71_210, "destroyed-recovery-port")?); + + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture context creation must succeed")?; + bound + .destroy_disposable_context(&authority) + .map_err(|_| "fixture destruction must be proven")?; + assert!(bound.browser_session().recovery_evidence().is_empty()); + + assert!(bound.record_transport_loss()); + assert_eq!( + bound.browser_session().state(), + BrowserSessionState::TransportLost + ); + assert!(bound.browser_session().recovery_evidence().is_empty()); + assert!( + bound.into_recovery().is_err(), + "proven destruction must not be followed by a recovery-only adapter capability with no unresolved evidence" + ); + Ok(()) +} From 304ea8efc1e49ce30847046bc97e8d3da459fa62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 12:01:39 +0900 Subject: [PATCH 124/632] fix(browser-session): require unresolved recovery evidence --- .../src/recovery.rs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index 8a1a5b392..52640030b 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -89,11 +89,12 @@ pub enum RecoveryContextOperationError { /// Recovery-only custody of a Browser Session and its exact consumed lifecycle adapter. /// /// This wrapper is obtained only by consuming a bound session that has already entered -/// [`BrowserSessionState::RecoveryRequired`] or [`BrowserSessionState::TransportLost`]. It exposes -/// lifecycle state, exact non-authorizing recovery evidence, and a purpose-bounded recovery-operation -/// path through the retained adapter. It deliberately provides none of the ordinary create, -/// presentation-authority, epoch-advance, destroy, authorized-operation, or normal-finish methods, and -/// it does not expose the inner [`BoundBrowserSession`] or concrete port. +/// [`BrowserSessionState::RecoveryRequired`] or entered [`BrowserSessionState::TransportLost`] while +/// retaining unresolved remote-ownership evidence. It exposes lifecycle state, exact non-authorizing +/// recovery evidence, and a purpose-bounded recovery-operation path through the retained adapter. It +/// deliberately provides none of the ordinary create, presentation-authority, epoch-advance, destroy, +/// authorized-operation, or normal-finish methods, and it does not expose the inner +/// [`BoundBrowserSession`] or concrete port. /// /// Ordinary context creation is not available from recovery custody: /// @@ -174,15 +175,26 @@ pub struct BoundBrowserSessionRecovery

{ impl BoundBrowserSession

{ /// Consume an unresolved bound session into recovery-only custody without adapter I/O. /// - /// The handoff succeeds only after Browser Session has entered `RecoveryRequired` or - /// `TransportLost`. Active or normally ended sessions are returned unchanged so callers cannot - /// use the recovery type as an alternate path around ordinary lifecycle policy. + /// `RecoveryRequired` always represents unresolved lifecycle ownership. `TransportLost` permits + /// handoff only when the aggregate retained exact non-authorizing recovery evidence; transport loss + /// by itself, before any remote ownership or after proven destruction, must not create an alternate + /// adapter-operation capability. Active, ended, and ownership-clean transport-lost sessions are + /// returned unchanged. pub fn into_recovery(self) -> Result, Self> { - match self.browser_session().state() { - BrowserSessionState::RecoveryRequired | BrowserSessionState::TransportLost => { + let state = self.browser_session().state(); + let has_recovery_evidence = !self.browser_session().recovery_evidence().is_empty() + || !self + .browser_session() + .create_attempt_recovery_evidence() + .is_empty(); + match state { + BrowserSessionState::RecoveryRequired => Ok(BoundBrowserSessionRecovery { bound: self }), + BrowserSessionState::TransportLost if has_recovery_evidence => { Ok(BoundBrowserSessionRecovery { bound: self }) } - BrowserSessionState::Active | BrowserSessionState::Ended => Err(self), + BrowserSessionState::Active + | BrowserSessionState::Ended + | BrowserSessionState::TransportLost => Err(self), } } } From 504daea40baa64df016921c683bc5b6500df37b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 12:06:19 +0900 Subject: [PATCH 125/632] docs(browser-session): bind recovery custody to unresolved evidence --- ...sion-recovery-custody-and-hot-ownership.md | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index 2f1985944..f3e4b2ebe 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -10,7 +10,7 @@ ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. -First, a session that enters `RecoveryRequired` or `TransportLost` still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. Recovery nevertheless needs a narrow way to perform protocol-owner-defined reconciliation through that same retained adapter. +First, a session that enters `RecoveryRequired`, or enters `TransportLost` while exact unresolved remote-ownership evidence is retained, still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. Transport loss by itself is not evidence of unresolved browser ownership: a session may lose transport before creating any remote boundary or after every owned boundary has already been proven destroyed. Recovery therefore needs a narrow way to perform protocol-owner-defined reconciliation through the retained adapter only when there is an unresolved ownership fact to reconcile. Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. @@ -22,6 +22,7 @@ These questions are Browser Session domain concerns. WebDriver BiDi pending/acce - Permit only purpose-bounded recovery I/O through that retained adapter; never expose raw `P` or an unrestricted callback. - Keep recovery evidence non-authorizing and unchanged by a mere adapter success/failure. - Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. +- Do not mint recovery-only adapter capability from transport loss when no unresolved ownership evidence exists. - Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. - Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. - Permit browser reuse of the same raw user-context/browsing-context identity only as a new monotonic ownership generation. @@ -56,6 +57,10 @@ Rejected. A generic callback is equivalent to raw adapter escape. The callback b Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. +### Treat any `TransportLost` state as recovery authority + +Rejected. Transport loss proves only that the transport is unavailable. If the session never created browser state, or every owned boundary was already proven destroyed, there is no unresolved ownership to reconcile. Allowing recovery custody in that state creates an alternate adapter-operation capability without recovery evidence. + ### Treat a successful recovery adapter call as reconciliation proof Rejected. Command success alone does not prove that remote ownership was destroyed or reconciled. Browser Session state and evidence remain unchanged until a separately reviewed proof-bearing transition exists. @@ -74,8 +79,8 @@ Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch cou ## Decision -1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds only from `BrowserSessionState::RecoveryRequired` or `BrowserSessionState::TransportLost`. -2. `Active` and `Ended` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy. +1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds from `BrowserSessionState::RecoveryRequired`, or from `BrowserSessionState::TransportLost` only when exact `BrowserSessionRecoveryEvidence` or `DisposableContextCreateRecoveryEvidence` is retained. +2. `Active`, `Ended`, and ownership-clean `TransportLost` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy or minted from transport loss without an unresolved remote-ownership fact. 3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. 4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, exact `DisposableContextCreateRecoveryEvidence`, and—only when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. 5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, authority-based or owner-based destroy, ordinary authorized operation, navigation transition, or normal-finish surface. @@ -100,7 +105,7 @@ Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch cou ## Consequences -The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can perform only the adapter-owned recovery operations admitted by `RecoveryContextOperationPort`; it cannot expose the adapter, regain ordinary Browser Session authority, or independently decide that protocol recovery is complete. +The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can perform only the adapter-owned recovery operations admitted by `RecoveryContextOperationPort`; it cannot expose the adapter, regain ordinary Browser Session authority, or independently decide that protocol recovery is complete. A bare `TransportLost` state with no unresolved ownership evidence remains outside this reduced authority surface. Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. @@ -114,17 +119,17 @@ The returned `NavigationSettlementAuthority` has private fields and no caller co ## Failure and degraded behavior -If `into_recovery(self)` is called while the aggregate is `Active` or `Ended`, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. +If `into_recovery(self)` is called while the aggregate is `Active`, `Ended`, or `TransportLost` without any retained recovery/create-attempt evidence, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. A failed recovery operation returns `RecoveryContextOperationError::Adapter` and preserves the same custody and evidence. A successful recovery operation returns the adapter-defined output but also preserves the same custody and evidence; a separate owner transition is required before uncertainty can be cleared. -A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. +A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. Transport loss with no owned or otherwise unresolved browser state creates no recovery custody. Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation/navigation-generation allocation is exhausted, allocation fails closed rather than reusing authority identity. ## Security / privacy / governance impact -The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned and is callable only through the purpose-bounded recovery trait; raw `P` never becomes ambient. +The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned and is callable only through the purpose-bounded recovery trait; raw `P` never becomes ambient. An ownership-clean transport loss cannot mint that recovery-only operation capability. Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. @@ -143,7 +148,10 @@ This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, - negative `compile_fail` capability contracts - `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` - unproven destroy moves the exact adapter and exact evidence without I/O - - transport loss moves the exact adapter and exact evidence without I/O + - transport loss with an unresolved owned handle moves the exact adapter and exact evidence without I/O +- `crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs` + - transport loss before any remote ownership cannot mint recovery custody + - transport loss after proven destruction cannot reopen recovery custody - `crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs` - recovery operation executes through the exact retained adapter - exact session/incarnation/state and both evidence ledgers reach the opaque request @@ -164,7 +172,7 @@ These are active-PR contracts until the exact head passes repository contracts, This active-PR change is additive at the ownership-type boundary but changes the internal retention, recovery-operation, and navigation-admission model. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery/navigation authority from adapter identifiers. -Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/recovery-operation/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. +Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/recovery-operation/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, allowing evidence-free transport loss to mint recovery custody, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. ## Open follow-ups @@ -177,7 +185,7 @@ Rollback before protected-main adoption is performed by reverting the whole reco ## Supersession / reversal conditions -Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, purpose-bounded same-adapter recovery I/O, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. +Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, purpose-bounded same-adapter recovery I/O only for unresolved ownership, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. From c4eddf8a7096e9199e1d47b7fc8ecbdf848857f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 12:07:15 +0900 Subject: [PATCH 126/632] docs(changelog): qualify Browser Session recovery handoff --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8fc822d..5232cd5a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Prevented ownership-clean `TransportLost` sessions from entering Browser Session recovery custody. Recovery handoff now requires exact unresolved recovery/create-attempt evidence for `TransportLost`, while `RecoveryRequired` remains recovery-eligible; transport loss before remote ownership or after proven destruction therefore cannot mint a purpose-bounded adapter-operation capability. - Preserved Browser Session create-attempt provenance through uncertain creation, duplicate-candidate rejection, and accepted/rejected completion-settlement failure. Recovery now keeps aggregate-issued attempt epoch, disposition, and complete candidate identity without collapsing a previously accepted same-valued owner into later candidate evidence; the abandonment/incarnation atomic updates use `AtomicU64::try_update` without changing their memory ordering or overflow behavior. -- Added one-way same-adapter Browser Session recovery custody for `RecoveryRequired` and `TransportLost` through `BoundBrowserSessionRecovery

`, preventing recovery evidence from regaining raw adapter or ordinary command authority. Proven destruction now retires only the exact live hot ownership record after adapter-proven success; failed destruction keeps `Uncertain` ownership with exact `UnprovenDestruction { context, context_epoch }` evidence. +- Added one-way same-adapter Browser Session recovery custody for `RecoveryRequired` and for `TransportLost` with retained unresolved ownership evidence through `BoundBrowserSessionRecovery

`, preventing recovery evidence from regaining raw adapter or ordinary command authority. Proven destruction now retires only the exact live hot ownership record after adapter-proven success; failed destruction keeps `Uncertain` ownership with exact `UnprovenDestruction { context, context_epoch }` evidence. - Prevented the reusable profile-derived WebDriver BiDi planner from scheduling `setScreenSettingsOverride` from `ScreenMetrics` alone, because the standard operation also changes the page-observable available screen rectangle that the current presentation identity neither selects nor digest-binds. - Restored canonical Rust formatting for the WebDriver BiDi presentation cleanup assertion so exact-head contracts can execute the test, Clippy, and rustdoc gates. From 738ec7d9a6635a8b4b0b9324026c2f0b433b9c77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 14:01:43 +0900 Subject: [PATCH 127/632] test(browser-session): require exact recovery fact settlement --- .../tests/recovery_exact_fact_settlement.rs | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs diff --git a/crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs b/crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs new file mode 100644 index 000000000..62b15327f --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs @@ -0,0 +1,381 @@ +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, BrowserSessionState, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRecoveryEvidence, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, RecoverySettlementError, + RecoverySettlementPort, RecoverySettlementRequest, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ObservedAbsentProof { + browsing_context: BrowsingContextId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SettlementVerificationError { + WrongFact, +} + +struct SettlementPort { + create_results: VecDeque>, + settlement_calls: Rc>, + settled_recovery: Rc>>, + settled_create_attempts: Rc>>, +} + +impl DisposableContextPort for SettlementPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_results + .pop_front() + .unwrap_or(Err(DisposableContextCreateError::CreateFailedClean)) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Err(DisposableContextDestroyError::DestroyFailed) + } +} + +impl RecoverySettlementPort for SettlementPort { + type Proof = ObservedAbsentProof; + type Error = SettlementVerificationError; + + fn verify_recovery_settlement( + &mut self, + request: &RecoverySettlementRequest, + ) -> Result<(), Self::Error> { + self.settlement_calls.set(self.settlement_calls.get() + 1); + if let Some(evidence) = request.recovery_evidence() { + let expected_context = match evidence { + BrowserSessionRecoveryEvidence::UnprovenDestruction { context, .. } + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(context) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(context) + | BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(context) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(context) => { + Some(context.browsing_context()) + } + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) => None, + }; + if expected_context.is_some_and(|context| context != request.proof().browsing_context) { + return Err(SettlementVerificationError::WrongFact); + } + self.settled_recovery.borrow_mut().push(evidence.clone()); + return Ok(()); + } + if let Some(evidence) = request.create_attempt_recovery_evidence() { + let expected_context = match evidence { + DisposableContextCreateRecoveryEvidence::DuplicateCandidate { context, .. } + | DisposableContextCreateRecoveryEvidence::CompletionUnsettled { context, .. } => { + Some(context.browsing_context()) + } + DisposableContextCreateRecoveryEvidence::FailedUncertain { .. } => None, + }; + if expected_context.is_some_and(|context| context != request.proof().browsing_context) { + return Err(SettlementVerificationError::WrongFact); + } + self.settled_create_attempts.borrow_mut().push(evidence.clone()); + return Ok(()); + } + Err(SettlementVerificationError::WrongFact) + } +} + +fn isolation(value: &str) -> Result { + DisposableIsolationId::parse(value).map_err(|_| "fixture isolation must be representable") +} + +fn context(value: u64) -> Result { + BrowsingContextId::new(value).map_err(|_| "fixture browsing context must be valid") +} + +fn session(value: u64) -> Result { + BrowserSessionId::new(value).map_err(|_| "fixture session must be valid") +} + +fn handle(isolation_id: &str, context_id: u64) -> Result { + Ok(DisposableContextHandle::new( + isolation(isolation_id)?, + context(context_id)?, + )) +} + +#[test] +fn exact_fact_settlement_is_single_use_local_and_does_not_erase_sibling_uncertainty( +) -> Result<(), &'static str> { + let first_handle = handle("recovery-settlement-a", 81_001)?; + let second_handle = handle("recovery-settlement-b", 81_002)?; + let settlement_calls = Rc::new(Cell::new(0)); + let settled_recovery = Rc::new(RefCell::new(Vec::new())); + let settled_create_attempts = Rc::new(RefCell::new(Vec::new())); + let port = SettlementPort { + create_results: VecDeque::from([ + Ok(first_handle.clone()), + Ok(second_handle.clone()), + ]), + settlement_calls: Rc::clone(&settlement_calls), + settled_recovery: Rc::clone(&settled_recovery), + settled_create_attempts: Rc::clone(&settled_create_attempts), + }; + let mut bound = BrowserSession::start(session(8_101)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let first_authority = bound + .create_disposable_context() + .map_err(|_| "first create must succeed")?; + let second_authority = bound + .create_disposable_context() + .map_err(|_| "second create must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&first_authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + assert_eq!(bound.browser_session().state(), BrowserSessionState::RecoveryRequired); + assert_eq!(bound.browser_session().recovery_evidence().len(), 2); + + let mut recovery = bound + .into_recovery() + .map_err(|_| "RecoveryRequired must enter recovery custody")?; + let first_fact = recovery + .recovery_fact(0) + .ok_or("first recovery fact must be addressable")?; + let stale_replay = recovery + .recovery_fact(0) + .ok_or("same current fact may be inspected twice before settlement")?; + let sibling_fact = recovery + .recovery_fact(1) + .ok_or("sibling recovery fact must be addressable")?; + + recovery + .settle_recovery_fact( + first_fact, + ObservedAbsentProof { + browsing_context: first_handle.browsing_context(), + }, + ) + .map_err(|_| "independently verified exact first fact must settle")?; + assert_eq!(settlement_calls.get(), 1); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence().len(), 1); + assert!(recovery.recovery_evidence().iter().any(|evidence| { + matches!( + evidence, + BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(context) + if context == &second_handle + ) + })); + + assert_eq!( + recovery.settle_recovery_fact( + stale_replay, + ObservedAbsentProof { + browsing_context: first_handle.browsing_context(), + }, + ), + Err(RecoverySettlementError::StaleFact) + ); + assert_eq!( + settlement_calls.get(), + 1, + "stale replay must fail before adapter proof verification" + ); + + assert_eq!( + recovery.settle_recovery_fact( + sibling_fact, + ObservedAbsentProof { + browsing_context: first_handle.browsing_context(), + }, + ), + Err(RecoverySettlementError::StaleFact), + "settling one fact invalidates previously issued sibling handles; callers must reread current custody" + ); + assert_eq!(settlement_calls.get(), 1); + + let current_sibling = recovery + .recovery_fact(0) + .ok_or("remaining sibling fact must be re-addressable after revision change")?; + assert_eq!( + recovery.settle_recovery_fact( + current_sibling, + ObservedAbsentProof { + browsing_context: first_handle.browsing_context(), + }, + ), + Err(RecoverySettlementError::Adapter( + SettlementVerificationError::WrongFact + )) + ); + assert_eq!(settlement_calls.get(), 2); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + assert_eq!(recovery.recovery_evidence().len(), 1); + + let current_sibling = recovery + .recovery_fact(0) + .ok_or("failed proof must leave the exact sibling fact current")?; + recovery + .settle_recovery_fact( + current_sibling, + ObservedAbsentProof { + browsing_context: second_handle.browsing_context(), + }, + ) + .map_err(|_| "qualified sibling absence must settle")?; + assert_eq!(settlement_calls.get(), 3); + assert!(recovery.recovery_evidence().is_empty()); + assert!(recovery.create_attempt_recovery_evidence().is_empty()); + assert_eq!( + recovery.state(), + BrowserSessionState::Ended, + "settlement may reach a terminal closed state but must never restore ordinary browser authority" + ); + assert_eq!(settled_recovery.borrow().len(), 2); + assert!(settled_create_attempts.borrow().is_empty()); + let _ = second_authority; + Ok(()) +} + +#[test] +fn foreign_fact_is_rejected_before_the_other_session_adapter_observes_proof( +) -> Result<(), &'static str> { + fn recovering_session( + session_id: u64, + context_id: u64, + isolation_id: &str, + settlement_calls: Rc>, + ) -> Result, &'static str> + { + let owned = handle(isolation_id, context_id)?; + let port = SettlementPort { + create_results: VecDeque::from([Ok(owned)]), + settlement_calls, + settled_recovery: Rc::new(RefCell::new(Vec::new())), + settled_create_attempts: Rc::new(RefCell::new(Vec::new())), + }; + let mut bound = BrowserSession::start(session(session_id)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture create must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + bound + .into_recovery() + .map_err(|_| "fixture must enter recovery custody") + } + + let first_calls = Rc::new(Cell::new(0)); + let second_calls = Rc::new(Cell::new(0)); + let first = recovering_session(8_201, 82_001, "foreign-fact-a", first_calls)?; + let mut second = recovering_session( + 8_202, + 82_002, + "foreign-fact-b", + Rc::clone(&second_calls), + )?; + let foreign_fact = first + .recovery_fact(0) + .ok_or("foreign recovery fact must exist")?; + + assert_eq!( + second.settle_recovery_fact( + foreign_fact, + ObservedAbsentProof { + browsing_context: context(82_001)?, + }, + ), + Err(RecoverySettlementError::AuthorityMismatch) + ); + assert_eq!( + second_calls.get(), + 0, + "foreign session/incarnation fact must fail before adapter proof verification" + ); + assert_eq!(second.recovery_evidence().len(), 1); + Ok(()) +} + +#[test] +fn dual_recovery_ledgers_require_independent_exact_fact_retirement( +) -> Result<(), &'static str> { + let uncertain_handle = handle("uncertain-create-fact", 83_001)?; + let settlement_calls = Rc::new(Cell::new(0)); + let settled_recovery = Rc::new(RefCell::new(Vec::new())); + let settled_create_attempts = Rc::new(RefCell::new(Vec::new())); + let port = SettlementPort { + create_results: VecDeque::from([Err( + DisposableContextCreateError::CreateFailedUncertain(Some( + uncertain_handle.isolation().clone(), + )), + )]), + settlement_calls: Rc::clone(&settlement_calls), + settled_recovery: Rc::clone(&settled_recovery), + settled_create_attempts: Rc::clone(&settled_create_attempts), + }; + let mut bound = BrowserSession::start(session(8_301)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + assert_eq!( + bound.create_disposable_context(), + Err(BrowserSessionError::ContextCreationUncertain) + ); + let mut recovery = bound + .into_recovery() + .map_err(|_| "uncertain create must enter recovery custody")?; + assert_eq!(recovery.recovery_evidence().len(), 1); + assert_eq!(recovery.create_attempt_recovery_evidence().len(), 1); + + let identity_fact = recovery + .recovery_fact(0) + .ok_or("identity recovery fact must exist")?; + recovery + .settle_recovery_fact( + identity_fact, + ObservedAbsentProof { + browsing_context: uncertain_handle.browsing_context(), + }, + ) + .map_err(|_| "independently qualified identity fact must settle")?; + assert!(recovery.recovery_evidence().is_empty()); + assert_eq!(recovery.create_attempt_recovery_evidence().len(), 1); + assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); + + let transaction_fact = recovery + .create_attempt_recovery_fact(0) + .ok_or("create-attempt fact must remain separately addressable")?; + recovery + .settle_recovery_fact( + transaction_fact, + ObservedAbsentProof { + browsing_context: uncertain_handle.browsing_context(), + }, + ) + .map_err(|_| "independently qualified create-attempt fact must settle")?; + assert!(recovery.recovery_evidence().is_empty()); + assert!(recovery.create_attempt_recovery_evidence().is_empty()); + assert_eq!(recovery.state(), BrowserSessionState::Ended); + assert_eq!(settlement_calls.get(), 2); + assert_eq!(settled_recovery.borrow().len(), 1); + assert_eq!(settled_create_attempts.borrow().len(), 1); + Ok(()) +} From 0154f19cf5fca89c9e27d8c5d135482aa910553e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 14:07:41 +0900 Subject: [PATCH 128/632] docs(browser-session): specify proof-bearing recovery settlement --- .../browser-session-recovery-settlement.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/doctoring/browser-session-recovery-settlement.md diff --git a/docs/doctoring/browser-session-recovery-settlement.md b/docs/doctoring/browser-session-recovery-settlement.md new file mode 100644 index 000000000..4c5557af4 --- /dev/null +++ b/docs/doctoring/browser-session-recovery-settlement.md @@ -0,0 +1,61 @@ +# Browser Session recovery settlement boundary + +Status: active-PR design evidence for #317. This document does not claim protected-main adoption, executable GREEN, browser acceptance, or release readiness. + +## Problem + +`BoundBrowserSessionRecovery

` already preserves the exact consumed adapter and permits purpose-bounded recovery I/O without exposing raw `P`. That solves custody and dispatch, but not recovery completion. Adapter success or command acknowledgement deliberately leaves `RecoveryRequired` and both recovery ledgers unchanged because an I/O return value is not proof that remote browser ownership was destroyed or reconciled. + +Without a second-stage settlement transition, a protocol owner such as #316 can independently qualify browser evidence but cannot retire the matching Browser Session uncertainty. The only alternatives are unsafe: erase all evidence on adapter success, reconstruct ordinary authority from raw identifiers, or abandon a recovery owner that can never reach a terminal condition. + +## Constraints + +Browser Session owns deterministic lifecycle state and exact fact consumption. WebDriver BiDi remains an adapter and evidence source; its navigation ids, user-context ids, event ordering and liveness rules do not become Browser Session policy authority. + +A settlement boundary must therefore satisfy all of the following: + +- one opaque Browser Session-issued handle addresses exactly one current recovery fact; +- the handle is bound to the exact Browser Session incarnation and a current recovery-ledger revision; +- foreign session/incarnation handles fail before adapter proof-verification I/O; +- settlement of one fact invalidates every previously issued fact handle so index movement or sibling removal cannot redirect a stale handle; +- proof verification happens through the exact retained adapter, but successful verification retires only the fact named by the already validated handle; +- failed proof verification leaves lifecycle state and both recovery ledgers unchanged; +- identity-oriented `BrowserSessionRecoveryEvidence` and transaction-oriented `DisposableContextCreateRecoveryEvidence` remain separately addressable and separately retired; +- partial settlement preserves every unrelated sibling fact; +- complete settlement may close recovery custody, but it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, or an ordinary lifecycle owner. + +## Alternatives rejected + +Treating `RecoveryContextOperationPort` success as settlement is rejected because transport/protocol command completion is not independent proof of remote destruction or reconciliation. + +Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling fact. A current-revision opaque handle is required to make stale replay fail closed. + +Allowing the adapter to delete Browser Session evidence directly is rejected because it moves domain ownership truth into an adapter and makes protocol data authoritative over policy state. + +Returning from recovery custody to ordinary `BoundBrowserSession

` is rejected because reconciliation must not resurrect create, presentation, navigation or ordinary cleanup authority after uncertainty has crossed the recovery boundary. + +## Test-first contract + +Exact #317 commit `738ec7d9a6635a8b4b0b9324026c2f0b433b9c77` introduces `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` as a structural RED. The fixture intentionally references settlement types and methods that do not yet exist. + +The hostile cases require: + +1. two uncertain owned contexts: settling A preserves B; replay of A and a sibling handle issued before the revision change fail before proof I/O; rereading B permits proof verification; a wrong proof does not mutate B; exact B settlement closes only after no recovery facts remain; +2. a fact issued by another Browser Session cannot reach the target session's proof verifier even when the caller possesses the opaque value; +3. uncertain create evidence carried in the identity and create-attempt ledgers requires two independent settlements rather than one broad erase. + +The current head is RED by construction. Draft-policy skipped CI is not evidence that the test compiled or failed for the intended reason. Production implementation must not begin by weakening this fixture or by treating a successful generic recovery operation as proof. + +## Intended minimal production shape + +The implementation should stay inside the Browser Session bounded context and expose only protocol-agnostic concepts: an opaque recovery-fact handle, an opaque proof request passed to a narrow settlement-verification port, typed stale/foreign/adapter failures, and exact current-fact removal after successful verification. + +A monotonic recovery-ledger revision is preferable to trying to preserve stable vector indices. Every successful settlement increments the revision, making all previously issued handles stale. Validation order is aggregate/session-incarnation identity, current revision, exact fact identity, adapter proof verification, then mutation. No adapter call occurs before the first three checks succeed. + +When one recovery fact represents an owned context, successful settlement must also retire only the matching uncertain hot-ownership record. When no ownership or create-attempt facts remain, the recovery wrapper may reach terminal `Ended`. It must not return an ordinary owner or mint a presentation epoch. + +## Ownership handoff + +#317 owns this generic settlement boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is therefore #317 RED → minimal Browser Session settlement implementation → exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. + +Any implementation that copies #316 protocol tuple state into Browser Session, consumes mutable sibling source, or treats command ACK as proof violates this boundary. From 4f597b5d2b9128d9ab35c0028f3a97694f292251 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:04:46 +0900 Subject: [PATCH 129/632] feat(browser-session): settle exact recovery facts --- .../src/recovery.rs | 227 +++++++++++++++++- 1 file changed, 221 insertions(+), 6 deletions(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index 52640030b..a63975521 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -86,15 +86,122 @@ pub enum RecoveryContextOperationError { Adapter(E), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RecoveryFactLedger { + Recovery, + CreateAttempt, +} + +/// Opaque Browser Session-issued handle for one current recovery fact. +/// +/// The fields are private. A caller can retain or replay this value, but cannot construct a different +/// session, ledger, index, or revision. Every successful settlement advances the recovery-ledger +/// revision, which invalidates all handles issued before that mutation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RecoveryFact { + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + revision: u64, + ledger: RecoveryFactLedger, + index: usize, +} + +/// Opaque request used by the retained adapter to verify one independently qualified recovery proof. +/// +/// Browser Session chooses exactly one current recovery fact before adapter I/O. The request carries +/// that fact's immutable domain evidence together with the adapter-defined proof. Neither the proof nor +/// the evidence is command authority, and a successful verifier return is committed by Browser Session +/// only after the opaque fact handle has already passed session/incarnation/revision validation. +pub struct RecoverySettlementRequest

{ + browser_session: BrowserSessionId, + incarnation: BrowserSessionIncarnation, + state: BrowserSessionState, + recovery_evidence: Option, + create_attempt_recovery_evidence: Option, + proof: P, +} + +impl

RecoverySettlementRequest

{ + /// Return the exact browser-session transport identity under recovery custody. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the exact process-local Browser Session incarnation under recovery custody. + #[must_use] + pub const fn incarnation(&self) -> BrowserSessionIncarnation { + self.incarnation + } + + /// Return the unresolved Browser Session lifecycle state captured before proof verification. + #[must_use] + pub const fn state(&self) -> BrowserSessionState { + self.state + } + + /// Return the selected identity-oriented recovery fact, when this request targets that ledger. + #[must_use] + pub const fn recovery_evidence(&self) -> Option<&BrowserSessionRecoveryEvidence> { + self.recovery_evidence.as_ref() + } + + /// Return the selected create-attempt recovery fact, when this request targets that ledger. + #[must_use] + pub const fn create_attempt_recovery_evidence( + &self, + ) -> Option<&DisposableContextCreateRecoveryEvidence> { + self.create_attempt_recovery_evidence.as_ref() + } + + /// Return the adapter-defined independent reconciliation proof. + #[must_use] + pub const fn proof(&self) -> &P { + &self.proof + } +} + +/// Adapter extension that qualifies independent evidence for one exact recovery fact. +/// +/// The adapter owns protocol-specific proof semantics such as WebDriver BiDi event correlation and +/// remote-liveness qualification. Browser Session owns the selected domain fact and commits its +/// retirement only after this verifier succeeds. Command acknowledgement alone must not be modeled as +/// proof merely because it was returned by the retained adapter. +pub trait RecoverySettlementPort: DisposableContextPort { + /// Adapter-defined proof type for independent reconciliation evidence. + type Proof; + /// Adapter-defined proof-verification failure. + type Error; + + /// Verify that the supplied proof reconciles exactly the domain fact carried by the request. + fn verify_recovery_settlement( + &mut self, + request: &RecoverySettlementRequest, + ) -> Result<(), Self::Error>; +} + +/// Failure while settling one Browser Session-issued recovery fact. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RecoverySettlementError { + /// The fact belongs to another Browser Session or process-local incarnation. + AuthorityMismatch, + /// The fact was issued for an older ledger revision or no longer addresses the current ledger. + StaleFact, + /// The monotonic recovery-ledger revision cannot advance without wrapping. + RevisionExhausted, + /// The retained adapter rejected the independently supplied proof. + Adapter(E), +} + /// Recovery-only custody of a Browser Session and its exact consumed lifecycle adapter. /// /// This wrapper is obtained only by consuming a bound session that has already entered /// [`BrowserSessionState::RecoveryRequired`] or entered [`BrowserSessionState::TransportLost`] while /// retaining unresolved remote-ownership evidence. It exposes lifecycle state, exact non-authorizing -/// recovery evidence, and a purpose-bounded recovery-operation path through the retained adapter. It -/// deliberately provides none of the ordinary create, presentation-authority, epoch-advance, destroy, -/// authorized-operation, or normal-finish methods, and it does not expose the inner -/// [`BoundBrowserSession`] or concrete port. +/// recovery evidence, and purpose-bounded recovery-operation and recovery-settlement paths through the +/// retained adapter. It deliberately provides none of the ordinary create, presentation-authority, +/// epoch-advance, destroy, authorized-operation, or normal-finish methods, and it does not expose the +/// inner [`BoundBrowserSession`] or concrete port. /// /// Ordinary context creation is not available from recovery custody: /// @@ -170,6 +277,7 @@ pub enum RecoveryContextOperationError { #[must_use = "persist or reconcile unresolved Browser Session ownership before dropping recovery custody"] pub struct BoundBrowserSessionRecovery

{ bound: BoundBrowserSession

, + revision: u64, } impl BoundBrowserSession

{ @@ -188,9 +296,15 @@ impl BoundBrowserSession

{ .create_attempt_recovery_evidence() .is_empty(); match state { - BrowserSessionState::RecoveryRequired => Ok(BoundBrowserSessionRecovery { bound: self }), + BrowserSessionState::RecoveryRequired => Ok(BoundBrowserSessionRecovery { + bound: self, + revision: 1, + }), BrowserSessionState::TransportLost if has_recovery_evidence => { - Ok(BoundBrowserSessionRecovery { bound: self }) + Ok(BoundBrowserSessionRecovery { + bound: self, + revision: 1, + }) } BrowserSessionState::Active | BrowserSessionState::Ended @@ -219,6 +333,32 @@ impl BoundBrowserSessionRecovery

{ .browser_session() .create_attempt_recovery_evidence() } + + /// Issue an opaque handle for one current identity-oriented recovery fact. + #[must_use] + pub fn recovery_fact(&self, index: usize) -> Option { + self.recovery_evidence().get(index)?; + Some(RecoveryFact { + browser_session: self.bound.browser_session().id(), + incarnation: self.bound.browser_session().incarnation(), + revision: self.revision, + ledger: RecoveryFactLedger::Recovery, + index, + }) + } + + /// Issue an opaque handle for one current create-attempt recovery fact. + #[must_use] + pub fn create_attempt_recovery_fact(&self, index: usize) -> Option { + self.create_attempt_recovery_evidence().get(index)?; + Some(RecoveryFact { + browser_session: self.bound.browser_session().id(), + incarnation: self.bound.browser_session().incarnation(), + revision: self.revision, + ledger: RecoveryFactLedger::CreateAttempt, + index, + }) + } } impl BoundBrowserSessionRecovery

{ @@ -248,3 +388,78 @@ impl BoundBrowserSessionRecovery

{ }) } } + +impl BoundBrowserSessionRecovery

{ + /// Verify and retire exactly one current recovery fact through the retained adapter. + /// + /// Session/incarnation, current ledger revision, and current fact address are validated before the + /// adapter sees the proof. A successful proof retires only the selected fact, advances the ledger + /// revision, and therefore invalidates every handle issued before the mutation. Adapter failure or + /// any pre-I/O validation failure leaves Browser Session state and both recovery ledgers unchanged. + pub fn settle_recovery_fact( + &mut self, + fact: RecoveryFact, + proof: P::Proof, + ) -> Result<(), RecoverySettlementError> { + let session = self.bound.browser_session(); + if fact.browser_session != session.id() || fact.incarnation != session.incarnation() { + return Err(RecoverySettlementError::AuthorityMismatch); + } + if fact.revision != self.revision { + return Err(RecoverySettlementError::StaleFact); + } + let next_revision = self + .revision + .checked_add(1) + .ok_or(RecoverySettlementError::RevisionExhausted)?; + let (recovery_evidence, create_attempt_recovery_evidence) = match fact.ledger { + RecoveryFactLedger::Recovery => { + let evidence = self + .recovery_evidence() + .get(fact.index) + .cloned() + .ok_or(RecoverySettlementError::StaleFact)?; + (Some(evidence), None) + } + RecoveryFactLedger::CreateAttempt => { + let evidence = self + .create_attempt_recovery_evidence() + .get(fact.index) + .cloned() + .ok_or(RecoverySettlementError::StaleFact)?; + (None, Some(evidence)) + } + }; + let request = RecoverySettlementRequest { + browser_session: session.id(), + incarnation: session.incarnation(), + state: session.state(), + recovery_evidence: recovery_evidence.clone(), + create_attempt_recovery_evidence: create_attempt_recovery_evidence.clone(), + proof, + }; + self.bound + .dispatch_recovery_operation(|_, port| port.verify_recovery_settlement(&request)) + .map_err(RecoverySettlementError::Adapter)?; + + let retired = match fact.ledger { + RecoveryFactLedger::Recovery => self.bound.settle_recovery_evidence_at( + fact.index, + recovery_evidence + .as_ref() + .ok_or(RecoverySettlementError::StaleFact)?, + ), + RecoveryFactLedger::CreateAttempt => self.bound.settle_create_attempt_recovery_evidence_at( + fact.index, + create_attempt_recovery_evidence + .as_ref() + .ok_or(RecoverySettlementError::StaleFact)?, + ), + }; + if !retired { + return Err(RecoverySettlementError::StaleFact); + } + self.revision = next_revision; + Ok(()) + } +} From 8bbbb18dc4792604fe6a066190d03f322a6570c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:11:32 +0900 Subject: [PATCH 130/632] feat(browser-session): retire reconciled recovery facts --- .../src/browser_session.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 640352f2c..7d03c1a41 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -2201,3 +2201,79 @@ mod tests { assert_eq!(error, BrowserSessionError::IncarnationExhausted); } } + +impl BrowserSession { + fn settle_recovery_evidence_at( + &mut self, + index: usize, + expected: &BrowserSessionRecoveryEvidence, + ) -> bool { + if self.recovery_evidence.get(index) != Some(expected) { + return false; + } + let evidence = self.recovery_evidence.remove(index); + match &evidence { + BrowserSessionRecoveryEvidence::UnprovenDestruction { context, .. } + | BrowserSessionRecoveryEvidence::RecoveryRequiredOwnedHandle(context) + | BrowserSessionRecoveryEvidence::TransportLossOwnedHandle(context) => { + self.retire_uncertain_owned_context(context); + } + BrowserSessionRecoveryEvidence::PartialCreationIsolation(_) + | BrowserSessionRecoveryEvidence::DuplicateAdapterHandle(_) + | BrowserSessionRecoveryEvidence::UnsettledAdapterHandle(_) => {} + } + self.finish_recovery_if_resolved(); + true + } + + fn settle_create_attempt_recovery_evidence_at( + &mut self, + index: usize, + expected: &DisposableContextCreateRecoveryEvidence, + ) -> bool { + if self.create_recovery_evidence.get(index) != Some(expected) { + return false; + } + self.create_recovery_evidence.remove(index); + self.finish_recovery_if_resolved(); + true + } + + fn retire_uncertain_owned_context(&mut self, handle: &DisposableContextHandle) { + let browsing_context = handle.browsing_context(); + let should_remove = self.contexts.get(&browsing_context).is_some_and(|record| { + record.state == OwnedContextState::Uncertain && &record.handle == handle + }); + if should_remove { + let _ = self.contexts.remove(&browsing_context); + } + } + + fn finish_recovery_if_resolved(&mut self) { + if self.recovery_evidence.is_empty() + && self.create_recovery_evidence.is_empty() + && self.contexts.is_empty() + { + self.state = BrowserSessionState::Ended; + } + } +} + +impl

BoundBrowserSession

{ + pub(crate) fn settle_recovery_evidence_at( + &mut self, + index: usize, + expected: &BrowserSessionRecoveryEvidence, + ) -> bool { + self.session.settle_recovery_evidence_at(index, expected) + } + + pub(crate) fn settle_create_attempt_recovery_evidence_at( + &mut self, + index: usize, + expected: &DisposableContextCreateRecoveryEvidence, + ) -> bool { + self.session + .settle_create_attempt_recovery_evidence_at(index, expected) + } +} From 742ff74b395a1ec1750831d8a43b7e2d35e7c880 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:11:51 +0900 Subject: [PATCH 131/632] feat(browser-session): export recovery settlement contract --- crates/originweave-browser-session/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-browser-session/src/lib.rs b/crates/originweave-browser-session/src/lib.rs index 35b3e08c1..0cc472312 100644 --- a/crates/originweave-browser-session/src/lib.rs +++ b/crates/originweave-browser-session/src/lib.rs @@ -27,5 +27,6 @@ mod recovery; pub use browser_session::*; pub use recovery::{ BoundBrowserSessionRecovery, RecoveryContextOperationError, RecoveryContextOperationPort, - RecoveryContextOperationRequest, + RecoveryContextOperationRequest, RecoveryFact, RecoverySettlementError, RecoverySettlementPort, + RecoverySettlementRequest, }; From 71666b207c82e3a00d8c3bc71c27769100cb1256 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:12:26 +0900 Subject: [PATCH 132/632] docs(browser-session): currentize recovery settlement evidence --- .../browser-session-recovery-settlement.md | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/doctoring/browser-session-recovery-settlement.md b/docs/doctoring/browser-session-recovery-settlement.md index 4c5557af4..01c48863c 100644 --- a/docs/doctoring/browser-session-recovery-settlement.md +++ b/docs/doctoring/browser-session-recovery-settlement.md @@ -1,61 +1,67 @@ # Browser Session recovery settlement boundary -Status: active-PR design evidence for #317. This document does not claim protected-main adoption, executable GREEN, browser acceptance, or release readiness. +Status: active-PR implementation evidence for #317. The source implementation now exists, but this document does not claim protected-main adoption, executable GREEN, browser acceptance, or release readiness until the exact head passes repository gates. ## Problem -`BoundBrowserSessionRecovery

` already preserves the exact consumed adapter and permits purpose-bounded recovery I/O without exposing raw `P`. That solves custody and dispatch, but not recovery completion. Adapter success or command acknowledgement deliberately leaves `RecoveryRequired` and both recovery ledgers unchanged because an I/O return value is not proof that remote browser ownership was destroyed or reconciled. +`BoundBrowserSessionRecovery

` already preserved the exact consumed adapter and permitted purpose-bounded recovery I/O without exposing raw `P`. That solved custody and dispatch, but not recovery completion: adapter success or command acknowledgement deliberately left `RecoveryRequired` and both recovery ledgers unchanged because an I/O return value is not proof that remote browser ownership was destroyed or reconciled. -Without a second-stage settlement transition, a protocol owner such as #316 can independently qualify browser evidence but cannot retire the matching Browser Session uncertainty. The only alternatives are unsafe: erase all evidence on adapter success, reconstruct ordinary authority from raw identifiers, or abandon a recovery owner that can never reach a terminal condition. +The settlement repair gives a protocol owner such as #316 a second-stage path: independently qualify browser evidence, submit it against one Browser Session-issued recovery fact, and retire only that exact uncertainty after the retained adapter verifies the proof. ## Constraints Browser Session owns deterministic lifecycle state and exact fact consumption. WebDriver BiDi remains an adapter and evidence source; its navigation ids, user-context ids, event ordering and liveness rules do not become Browser Session policy authority. -A settlement boundary must therefore satisfy all of the following: +The implemented settlement boundary preserves these invariants: -- one opaque Browser Session-issued handle addresses exactly one current recovery fact; -- the handle is bound to the exact Browser Session incarnation and a current recovery-ledger revision; +- one opaque Browser Session-issued `RecoveryFact` addresses exactly one current recovery fact; +- the handle is bound to the exact Browser Session id, process-local incarnation, ledger kind, index and current recovery-ledger revision; - foreign session/incarnation handles fail before adapter proof-verification I/O; -- settlement of one fact invalidates every previously issued fact handle so index movement or sibling removal cannot redirect a stale handle; -- proof verification happens through the exact retained adapter, but successful verification retires only the fact named by the already validated handle; +- a successful settlement increments the revision, so every handle issued before that mutation becomes stale before adapter I/O; +- proof verification runs through the exact retained adapter via `RecoverySettlementPort`, but successful verification retires only the fact named by the already validated handle; - failed proof verification leaves lifecycle state and both recovery ledgers unchanged; - identity-oriented `BrowserSessionRecoveryEvidence` and transaction-oriented `DisposableContextCreateRecoveryEvidence` remain separately addressable and separately retired; +- settling an owned-context fact retires only the exact matching uncertain hot-ownership record; candidate/partial-create evidence never consumes an independently owned context merely because remote values alias; - partial settlement preserves every unrelated sibling fact; -- complete settlement may close recovery custody, but it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, or an ordinary lifecycle owner. +- when both recovery ledgers and uncertain ownership are empty, the aggregate reaches terminal `Ended`; it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, or an ordinary lifecycle owner. ## Alternatives rejected Treating `RecoveryContextOperationPort` success as settlement is rejected because transport/protocol command completion is not independent proof of remote destruction or reconciliation. -Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling fact. A current-revision opaque handle is required to make stale replay fail closed. +Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling fact. A current-revision opaque handle makes stale replay fail closed. Allowing the adapter to delete Browser Session evidence directly is rejected because it moves domain ownership truth into an adapter and makes protocol data authoritative over policy state. Returning from recovery custody to ordinary `BoundBrowserSession

` is rejected because reconciliation must not resurrect create, presentation, navigation or ordinary cleanup authority after uncertainty has crossed the recovery boundary. -## Test-first contract +## Test-first contract and source repair -Exact #317 commit `738ec7d9a6635a8b4b0b9324026c2f0b433b9c77` introduces `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` as a structural RED. The fixture intentionally references settlement types and methods that do not yet exist. - -The hostile cases require: +Commit `738ec7d9a6635a8b4b0b9324026c2f0b433b9c77` introduced `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` as a structural RED. It requires: 1. two uncertain owned contexts: settling A preserves B; replay of A and a sibling handle issued before the revision change fail before proof I/O; rereading B permits proof verification; a wrong proof does not mutate B; exact B settlement closes only after no recovery facts remain; 2. a fact issued by another Browser Session cannot reach the target session's proof verifier even when the caller possesses the opaque value; 3. uncertain create evidence carried in the identity and create-attempt ledgers requires two independent settlements rather than one broad erase. -The current head is RED by construction. Draft-policy skipped CI is not evidence that the test compiled or failed for the intended reason. Production implementation must not begin by weakening this fixture or by treating a successful generic recovery operation as proof. +The source repair adds the opaque fact/request/error contract and verifier port in `recovery.rs`, plus crate-private aggregate mutation hooks that retire exact evidence and exact matching uncertain ownership only after proof verification. It does not weaken the hostile fixture and does not reinterpret generic recovery-operation success as proof. + +Repository execution remains the next gate. Until the current exact head actually runs and passes repository contracts, rustfmt, locked tests, strict Clippy, rustdoc and production coverage, this is source-level GREEN intent rather than executable GREEN evidence. -## Intended minimal production shape +## Validation order -The implementation should stay inside the Browser Session bounded context and expose only protocol-agnostic concepts: an opaque recovery-fact handle, an opaque proof request passed to a narrow settlement-verification port, typed stale/foreign/adapter failures, and exact current-fact removal after successful verification. +`settle_recovery_fact` evaluates in this order: -A monotonic recovery-ledger revision is preferable to trying to preserve stable vector indices. Every successful settlement increments the revision, making all previously issued handles stale. Validation order is aggregate/session-incarnation identity, current revision, exact fact identity, adapter proof verification, then mutation. No adapter call occurs before the first three checks succeed. +1. exact Browser Session id and incarnation; +2. current recovery-ledger revision; +3. exact current ledger/index fact and next-revision capacity; +4. retained-adapter proof verification; +5. exact evidence retirement and, for owned-context evidence, exact uncertain ownership retirement; +6. monotonic revision advance and terminal `Ended` only when no uncertainty remains. -When one recovery fact represents an owned context, successful settlement must also retire only the matching uncertain hot-ownership record. When no ownership or create-attempt facts remain, the recovery wrapper may reach terminal `Ended`. It must not return an ordinary owner or mint a presentation epoch. +No adapter call occurs before the first three checks succeed. A failed verifier does not consume the fact. A successful mutation invalidates all previously issued fact handles before another settlement can be accepted. ## Ownership handoff -#317 owns this generic settlement boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is therefore #317 RED → minimal Browser Session settlement implementation → exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. +#317 owns this generic settlement boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is #317 exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. Any implementation that copies #316 protocol tuple state into Browser Session, consumes mutable sibling source, or treats command ACK as proof violates this boundary. From 748bae7de91fcb829e4632eecc13e586774a0a78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:22:40 +0900 Subject: [PATCH 133/632] test(browser-session): pin recovery settlement authority surface --- ...er_session_recovery_settlement_contract.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/test_browser_session_recovery_settlement_contract.py diff --git a/tests/test_browser_session_recovery_settlement_contract.py b/tests/test_browser_session_recovery_settlement_contract.py new file mode 100644 index 000000000..8e6ce1fb0 --- /dev/null +++ b/tests/test_browser_session_recovery_settlement_contract.py @@ -0,0 +1,136 @@ +"""Repository contracts for proof-bearing Browser Session recovery settlement.""" + +from __future__ import annotations + +import pathlib +import re +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CRATE = ROOT / "crates/originweave-browser-session" +RECOVERY = CRATE / "src/recovery.rs" +BROWSER_SESSION = CRATE / "src/browser_session.rs" +HOSTILE = CRATE / "tests/recovery_exact_fact_settlement.rs" +ADR = ROOT / "docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md" +TRACE = ROOT / "docs/traceability/browser-session-lifecycle-authority.md" +UML = ROOT / "docs/uml/browser-session-lifecycle-authority.md" +DOCTORING = ROOT / "docs/doctoring/browser-session-recovery-settlement.md" + + +def _struct_body(source: str, declaration: str) -> str: + """Return one simple Rust struct body used by the authority-surface contract.""" + + return source.split(declaration, 1)[1].split("\n}", 1)[0] + + +class BrowserSessionRecoverySettlementContractTests(unittest.TestCase): + """Keep recovery settlement exact-fact-bound and non-caller-constructible.""" + + def test_settlement_surface_is_explicit_and_opaque(self) -> None: + """New construction paths must not bypass Browser Session-issued fact custody.""" + + recovery_source = RECOVERY.read_text(encoding="utf-8") + browser_source = BROWSER_SESSION.read_text(encoding="utf-8") + + for symbol in ( + "pub struct RecoveryFact", + "pub struct RecoverySettlementRequest", + "pub trait RecoverySettlementPort", + "pub enum RecoverySettlementError", + "pub fn recovery_fact", + "pub fn create_attempt_recovery_fact", + "pub fn settle_recovery_fact", + ): + self.assertIn(symbol, recovery_source) + + for declaration in ( + "pub struct RecoveryFact {", + "pub struct RecoverySettlementRequest

{", + ): + body = _struct_body(recovery_source, declaration) + self.assertNotRegex( + body, + r"(?m)^\s*pub(?:\([^)]*\))?\s+", + f"{declaration} fields must stay private", + ) + + self.assertNotRegex( + recovery_source, + r"#\[derive\([^\]]*\bDefault\b[^\]]*\)\]\s*pub struct RecoveryFact", + ) + for constructor_pattern in ( + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::default::)?Default\s+for\s+RecoveryFact", + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::convert::)?From<[^{}]+?>\s+for\s+RecoveryFact", + r"impl(?:\s*<[^{}]*?>)?\s+(?:::)?(?:(?:core|std)::convert::)?TryFrom<[^{}]+?>\s+for\s+RecoveryFact", + ): + self.assertNotRegex(recovery_source, constructor_pattern) + + self.assertIn("pub(crate) fn dispatch_recovery_operation", browser_source) + self.assertNotIn("pub fn dispatch_recovery_operation", browser_source) + self.assertNotIn("pub fn port", recovery_source) + self.assertNotIn("pub const fn port", recovery_source) + + def test_settlement_order_pins_pre_io_validation_and_post_proof_commit(self) -> None: + """Fact validation must precede proof I/O, which must precede aggregate mutation.""" + + source = RECOVERY.read_text(encoding="utf-8") + method = source.split("pub fn settle_recovery_fact", 1)[1].split("\n }\n}", 1)[0] + + authority = method.index("RecoverySettlementError::AuthorityMismatch") + revision = method.index("fact.revision != self.revision") + exact_fact = method.index(".get(fact.index)") + verifier = method.index("verify_recovery_settlement") + retirement = method.index("settle_recovery_evidence_at") + revision_commit = method.index("self.revision = next_revision") + + self.assertLess(authority, verifier) + self.assertLess(revision, verifier) + self.assertLess(exact_fact, verifier) + self.assertLess(verifier, retirement) + self.assertLess(retirement, revision_commit) + self.assertIn("RecoveryFactLedger::CreateAttempt", method) + self.assertIn("settle_create_attempt_recovery_evidence_at", method) + + def test_hostile_fixture_pins_replay_sibling_foreign_and_dual_ledger_cases(self) -> None: + """The external fixture must keep realistic settlement abuse cases executable.""" + + hostile = HOSTILE.read_text(encoding="utf-8") + for token in ( + "RecoverySettlementPort", + "RecoverySettlementRequest", + "settle_recovery_fact", + "RecoverySettlementError::StaleFact", + "RecoverySettlementError::AuthorityMismatch", + "stale replay must fail before adapter proof verification", + "settling one fact invalidates previously issued sibling handles", + "foreign session/incarnation fact must fail before adapter proof verification", + "create_attempt_recovery_fact", + "BrowserSessionState::Ended", + "must never restore ordinary browser authority", + ): + self.assertIn(token, hostile) + + def test_architecture_docs_name_the_current_settlement_boundary(self) -> None: + """ADR/trace/UML/doctoring must describe proof-bearing settlement, not only dispatch.""" + + documents = [ + ADR.read_text(encoding="utf-8"), + TRACE.read_text(encoding="utf-8"), + UML.read_text(encoding="utf-8"), + DOCTORING.read_text(encoding="utf-8"), + ] + for document in documents: + for token in ( + "RecoveryFact", + "RecoverySettlementPort", + "RecoverySettlementRequest", + "settle_recovery_fact", + "#316", + ): + self.assertIn(token, document) + self.assertIn("revision", document.lower()) + self.assertIn("proof", document.lower()) + + +if __name__ == "__main__": + unittest.main() From de25aa62eef95be59b09cd5049289a68ced3edc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:23:36 +0900 Subject: [PATCH 134/632] docs(adr): specify proof-bearing recovery settlement --- ...sion-recovery-custody-and-hot-ownership.md | 231 ++++++++++-------- 1 file changed, 129 insertions(+), 102 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index f3e4b2ebe..50e7c7554 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -8,186 +8,213 @@ ## Context -ADR 0114 establishes that Browser Session owns disposable-context lifecycle authority, binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing recovery evidence when remote state is uncertain. Two follow-on architecture questions remained once that contract was implemented. +ADR 0114 establishes Browser Session as the owner of disposable-context lifecycle authority. It binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing evidence whenever remote ownership becomes uncertain. -First, a session that enters `RecoveryRequired`, or enters `TransportLost` while exact unresolved remote-ownership evidence is retained, still owns the exact adapter instance that observed the unresolved remote state. Reconstructing a second adapter from identifiers would break the same-instance boundary; exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. Transport loss by itself is not evidence of unresolved browser ownership: a session may lose transport before creating any remote boundary or after every owned boundary has already been proven destroyed. Recovery therefore needs a narrow way to perform protocol-owner-defined reconciliation through the retained adapter only when there is an unresolved ownership fact to reconcile. +Three follow-on problems are addressed here. -Second, retaining a permanent `Destroyed` record for every proven-destroyed context makes command-authority hot state grow with historical activity. That is unnecessary for authority admission once exact destruction has been proven, but deleting an unproven record would lose ownership evidence. Command-authority state and durable audit/history therefore require different retention semantics. +First, a session that enters `RecoveryRequired`, or enters `TransportLost` while unresolved ownership evidence remains, still owns the exact adapter instance that observed the unresolved remote state. Reconstructing another adapter from identifiers would break same-instance custody. Exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. -These questions are Browser Session domain concerns. WebDriver BiDi pending/accepted/quarantined tuples and protocol-specific recovery command semantics remain adapter concerns owned by #316. Durable cross-process recovery persistence is also separate from the in-memory hot map. +Second, recovery command success is not recovery completion. A command ACK cannot prove that a remote browser boundary is absent or reconciled. Browser Session therefore needs a second, proof-bearing transition that consumes exactly one current recovery fact only after a protocol owner has independently qualified evidence and the exact retained adapter verifies it. + +Third, retaining a permanent `Destroyed` record for every proven-destroyed context would make command-authority hot state grow with historical throughput. Current command admission and durable audit history require different retention semantics. + +WebDriver BiDi pending/accepted/quarantined tuple truth, protocol event correlation, replay qualification, remote-liveness interpretation, and the meaning of concrete recovery evidence remain adapter concerns owned by #316. Durable cross-process persistence is also separate from the in-memory Browser Session hot map. ## Decision drivers -- Preserve the exact consumed adapter across unresolved ownership without making it generally accessible again. -- Permit only purpose-bounded recovery I/O through that retained adapter; never expose raw `P` or an unrestricted callback. -- Keep recovery evidence non-authorizing and unchanged by a mere adapter success/failure. -- Prevent `RecoveryRequired` or `TransportLost` from becoming an alternate normal lifecycle path. -- Do not mint recovery-only adapter capability from transport loss when no unresolved ownership evidence exists. -- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves the boundary gone. -- Keep command-authority admission bounded by current live/uncertain ownership rather than historical throughput. -- Permit browser reuse of the same raw user-context/browsing-context identity only as a new monotonic ownership generation. -- Reject retained stale authority before adapter I/O after both destruction and same-raw-identity recreation. -- Keep durable audit/history and process-restart persistence separate from the hot authorization map. +- Preserve the exact consumed adapter across unresolved ownership without making it ambient. +- Permit only purpose-bounded recovery I/O; never expose raw `P` or an unrestricted callback. +- Do not mint recovery custody from `TransportLost` when no unresolved ownership fact exists. +- Keep adapter command success/failure separate from independent recovery proof. +- Bind settlement to one opaque Browser Session-issued fact, not a raw vector index or protocol identifier. +- Reject foreign, stale, replayed, or out-of-range recovery facts before proof-verifier I/O. +- Retire only the exact fact that was independently verified; preserve unrelated sibling uncertainty. +- Keep identity-oriented recovery facts and create-attempt facts independently addressable. +- Prevent recovery settlement from restoring ordinary create, navigation, presentation, or cleanup authority. +- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves that fact gone. +- Keep command-authority hot state bounded to live or uncertain ownership. +- Preserve monotonic stale-authority rejection across raw browser-id reuse. +- Keep durable history and process-restart persistence separate from command admission. -## Assumptions and authority boundaries +## Authority boundaries -Browser Session owns lifecycle identity, ownership state, context epochs, admission of ordinary lifecycle/presentation authority, navigation-generation custody, and the one-way transition into recovery custody. `BoundBrowserSessionRecovery

` owns custody of the same adapter instance but does not become a protocol-specific recovery engine. +Browser Session owns lifecycle identity, ownership state, context epochs, ordinary lifecycle/presentation admission, navigation-generation custody, the one-way transition into recovery custody, opaque recovery-fact issuance, and deterministic exact-fact retirement. -The recovery wrapper may execute an adapter-defined operation only when `P: RecoveryContextOperationPort`. Browser Session constructs an opaque `RecoveryContextOperationRequest` immediately before I/O, snapshots the exact `BrowserSessionId`, `BrowserSessionIncarnation`, current unresolved `BrowserSessionState`, `BrowserSessionRecoveryEvidence`, and `DisposableContextCreateRecoveryEvidence`, and routes it through the same retained adapter. The request has no public constructor and is not ordinary create/destroy/presentation authority. +`BoundBrowserSessionRecovery

` owns the same concrete adapter instance but is not a protocol-specific recovery engine. When `P: RecoveryContextOperationPort`, it may execute a purpose-bounded recovery operation through `RecoveryContextOperationRequest`. This path snapshots exact Browser Session identity, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-defined operation. Adapter success or failure does not mutate Browser Session uncertainty. -WebDriver BiDi correlation, pending/accepted/quarantined tuples, remote-liveness interpretation, protocol event replay qualification, and protocol recovery command meaning remain #316 responsibilities. #316 may map its recovery vocabulary into `RecoveryContextOperationPort::Operation`; Browser Session does not inspect or own that protocol vocabulary. +When `P: RecoverySettlementPort`, recovery custody may also issue opaque `RecoveryFact` handles and execute `settle_recovery_fact(fact, proof)`. `RecoveryFact` binds the exact Browser Session id, process-local incarnation, ledger kind, current ledger index, and monotonic recovery revision. `RecoverySettlementRequest

` is privately constructed only after Browser Session validates that handle against current custody. The exact retained adapter verifies the independently supplied proof. Browser Session, not the adapter, then commits retirement of exactly the selected fact. -Durable crash/process-restart persistence and buyer audit history are not stored in the hot `BrowserSession.contexts` map. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, raw protocol identifiers, recovery evidence, and a successful recovery adapter call do not grant Browser Session command authority or prove remote destruction. +`RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest` have no public construction path. The crate-private bridge that touches `&mut P` remains `BoundBrowserSession::dispatch_recovery_operation`; external consumers cannot supply arbitrary callbacks or recover raw adapter access. -## Options considered +#316 owns WebDriver BiDi proof qualification. A `contextDestroyed` event, session-loss observation, liveness conclusion, or tuple transition is not automatically proof merely because it came from the protocol. #316 must decide which observations satisfy `RecoverySettlementPort::Proof`; Browser Session consumes only that already-qualified proof under its deterministic exact-fact contract. -### Return raw `P` from the failed bound session +Durable crash/process-restart persistence and buyer audit history do not live in `BrowserSession.contexts`. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, protocol identifiers, recovery evidence, adapter command success, and model judgment never become deterministic Browser Session policy authority. -Rejected. Raw adapter recovery recreates ambient capability and permits callers to issue protocol commands outside Browser Session authority. +## Options considered -### Expose `&BrowserSession` from recovery custody +### Return raw `P` from the failed bound session -Rejected. Even a read-only projection exposes methods that can become an indirect presentation-authority lookup surface as the aggregate evolves. Recovery custody exposes only explicit non-authorizing projections and the purpose-bounded recovery operation surface. +Rejected. Raw adapter recovery recreates ambient capability and permits browser commands outside Browser Session authority. -### Expose `FnOnce(&mut P)` or another generic callback +### Expose `&BrowserSession` or `FnOnce(&mut P)` from recovery custody -Rejected. A generic callback is equivalent to raw adapter escape. The callback bridge that touches `&mut P` is crate-private and callable only by the recovery wrapper after constructing the opaque request. +Rejected. The first can become an indirect capability-minting escape as the aggregate evolves; the second is equivalent to raw adapter access. The adapter bridge remains crate-private. ### Clone or reconstruct the adapter for recovery -Rejected. Same credentials, endpoint, or identifier do not prove same lifecycle instance. A second adapter can diverge from the pending remote transaction that produced the evidence. +Rejected. Equal credentials, endpoint, or identifiers do not establish same lifecycle instance or pending protocol state. ### Treat any `TransportLost` state as recovery authority -Rejected. Transport loss proves only that the transport is unavailable. If the session never created browser state, or every owned boundary was already proven destroyed, there is no unresolved ownership to reconcile. Allowing recovery custody in that state creates an alternate adapter-operation capability without recovery evidence. +Rejected. Transport loss proves only liveness loss. Before any remote ownership, or after every boundary is proven destroyed, there is no unresolved fact to reconcile. -### Treat a successful recovery adapter call as reconciliation proof +### Treat a successful recovery command as reconciliation proof -Rejected. Command success alone does not prove that remote ownership was destroyed or reconciled. Browser Session state and evidence remain unchanged until a separately reviewed proof-bearing transition exists. +Rejected. Command completion is not a browser-observed post-condition. `execute_recovery_context_operation` always preserves Browser Session uncertainty. -### Keep every proven-destroyed context as a permanent hot tombstone +### Let the adapter delete recovery evidence directly -Rejected. It makes authority-admission state grow with historical throughput and conflates authorization with audit retention. Monotonic epochs plus exact validation are sufficient to reject predecessor capabilities after a proven destroy and same-identity recreation. +Rejected. That would move domain ownership truth into an adapter and let protocol data rewrite policy state. -### Delete records after any destroy command acknowledgement +### Identify a recovery fact by raw vector index -Rejected. A command ACK is not proof that the disposable browser boundary is gone. Failed or otherwise unproven destruction must retain uncertain ownership and exact recovery evidence. +Rejected. Retiring one fact shifts later indices. An old index could then address a different sibling. `RecoveryFact` therefore carries a monotonic revision; successful settlement advances it and invalidates all previously issued fact handles. -### Reset context epochs when raw identifiers are reused +### Keep previously issued sibling facts valid after another fact settles -Rejected. Raw identifier reuse is an ABA case. Reusing the predecessor epoch could make retained authority current again. +Rejected. It makes index-shift replay ambiguous. Callers must reread current custody after every successful settlement. -## Decision +### Clear both recovery ledgers when one remote condition is proven -1. `BoundBrowserSession::into_recovery(self)` is the only Browser Session transition into recovery-only custody. It succeeds from `BrowserSessionState::RecoveryRequired`, or from `BrowserSessionState::TransportLost` only when exact `BrowserSessionRecoveryEvidence` or `DisposableContextCreateRecoveryEvidence` is retained. -2. `Active`, `Ended`, and ownership-clean `TransportLost` sessions are returned unchanged. Recovery custody cannot be selected as an alternate path around ordinary lifecycle policy or minted from transport loss without an unresolved remote-ownership fact. -3. A successful handoff moves the exact existing `BoundBrowserSession

` and therefore the same non-`Clone` adapter instance. The handoff performs no browser I/O, no create, no destroy, and no implicit cleanup. -4. Recovery custody is represented by `BoundBrowserSessionRecovery

`. It exposes lifecycle `state()`, exact `BrowserSessionRecoveryEvidence`, exact `DisposableContextCreateRecoveryEvidence`, and—only when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. -5. `BoundBrowserSessionRecovery

` exposes neither raw `P`, `BoundBrowserSession

`, nor `BrowserSession`. It provides no ordinary create, presentation-authority lookup, context-epoch advancement, authority-based or owner-based destroy, ordinary authorized operation, navigation transition, or normal-finish surface. -6. `RecoveryContextOperationRequest` is non-caller-constructible. It snapshots exact session id, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-owned purpose-bounded operation before adapter I/O. -7. The only bridge that receives `&mut P` is `BoundBrowserSession::dispatch_recovery_operation`, which is `pub(crate)` and lives in the Browser Session owner module. External consumers cannot invoke it or supply a closure. -8. `RecoveryContextOperationPort` extends `DisposableContextPort` with adapter-owned `Operation`, `Output`, and `Error` types. Browser Session routes the opaque request but does not interpret protocol semantics. -9. `RecoveryContextOperationError::Adapter(E)` preserves typed adapter failure. Adapter success and failure both leave Browser Session lifecycle state and recovery evidence unchanged; neither is destruction/reconciliation proof. -10. Negative capability boundaries are executable contracts. Rustdoc `compile_fail` examples and repository contracts must fail if recovery custody can regain an ordinary lifecycle or presentation-authority path. -11. Protocol-specific recovery commands are not added to Browser Session. #316 consumes the generic recovery boundary for WebDriver BiDi pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics. -12. `BrowserSession.contexts` is hot command-authority state, not durable audit history. It contains only current live or uncertain ownership records. -13. `destroy_disposable_context` validates the exact current authority before adapter I/O. Only after the adapter proves destruction does Browser Session remove the corresponding hot ownership record. -14. If destruction is not proven, the record remains present as `Uncertain`, `UnprovenDestruction { context, context_epoch }` remains exact and enumerable, and normal authority stays closed. -15. Proven destruction releases the raw isolation/context identity for a later create attempt. Recreation reserves the next monotonic `BrowserContextEpoch`; a retained predecessor authority therefore cannot become current again merely because the browser reused the same raw identifiers. -16. Immediately after proven destruction, retained authority for that record fails before adapter I/O as `ContextNotOwned`. If the same raw identity is later recreated, the retained predecessor fails before adapter I/O as `AuthorityMismatch` because its epoch is stale. -17. Removal from hot command-authority state is not deletion of durable business/audit history. Durable process-restart recovery, evidence retention, and audit storage must be implemented by a separately authorized persistence owner and must not infer destruction from record eviction or from `abandoned_bound_session_count()`. -18. `Drop` on ordinary or recovery custody performs no browser I/O. The contained bound owner retains the same process-local abandonment accounting for unresolved remote ownership. -19. Browser-observed navigation is admitted only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` ownership generation. Admission revokes presentation authority with zero adapter I/O and mints an opaque `NavigationSettlementAuthority` using a separate monotonic navigation generation rather than spending a presentation epoch. -20. Commit progress is non-terminal. Positive settlement, typed `Aborted`/`Failed`, and download start share one exactly-once terminal closure. A newer admitted navigation supersedes an older pending witness; stale or superseded witnesses fail closed without changing a newer generation. -21. Terminal closure creates one context-local opportunity for explicit `reestablish_presentation_authority`; the first successful re-establishment consumes the next presentation epoch and returns the context to `Established`. Generic epoch advancement cannot bypass a navigation-invalidated state. -22. Presentation authority and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup, while the exact bound lifecycle owner may still destroy its retained disposable context through `destroy_owned_disposable_context` without reopening presentation authority. -23. This ADR remains `Proposed` until the change reaches protected `main` and real-browser recovery/destruction/navigation post-conditions are independently evidenced. +Rejected. `BrowserSessionRecoveryEvidence` records identity/ownership uncertainty while `DisposableContextCreateRecoveryEvidence` records create-attempt transaction uncertainty. They are independent facts and require independent retirement. -## Consequences +### Restore an ordinary `BoundBrowserSession

` after reconciliation -The Browser Session aggregate now has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can perform only the adapter-owned recovery operations admitted by `RecoveryContextOperationPort`; it cannot expose the adapter, regain ordinary Browser Session authority, or independently decide that protocol recovery is complete. A bare `TransportLost` state with no unresolved ownership evidence remains outside this reduced authority surface. +Rejected. Crossing the recovery boundary is one-way. Even complete recovery reaches terminal `Ended`; it never recreates `Active`, create authority, presentation authority, navigation authority, or normal lifecycle cleanup authority. -Proven destruction makes hot ownership proportional to current live/uncertain state rather than the total number of historical context generations. This reduces long-lived session state without weakening stale-authority rejection. Durable history must be captured elsewhere when required; it is not implicitly provided by the command-authority map. +### Keep every proven-destroyed context as a permanent hot tombstone -The active #317 production lineage treats ownership generation and current navigation witness as separate authority dimensions. Reusing a raw browsing-context id after proven destruction does not carry predecessor lifecycle or navigation authority forward. Navigation liveness uses its own monotonic generation; presentation epochs advance only when explicit re-establishment succeeds. +Rejected. It conflates authorization state with audit history. Exact destruction plus monotonic epochs are sufficient for stale-authority rejection. -## Navigation authority interaction +### Delete hot ownership after a destroy command ACK -`record_observed_navigation` is a protocol-agnostic Browser Session transition. It accepts already-qualified adapter evidence only after aggregate trust, exact incarnation, exact live ownership, and current context epoch match. It does not consume a WebDriver BiDi navigation id as policy authority. #316 remains responsible for mapping protocol events and replay qualification into this domain transition. +Rejected. Only proven destruction or proof-bearing recovery settlement may retire uncertainty. Failed/unproven destruction retains exact ownership and evidence. -The returned `NavigationSettlementAuthority` has private fields and no caller constructor. `record_observed_navigation_committed` records first commit progress but does not make presentation re-establishment eligible. `record_observed_navigation_settled`, `record_observed_navigation_terminated`, and `record_observed_navigation_download_started` share the same current-witness terminal closure. `reestablish_presentation_authority` is explicit and single-use. `RecoveryRequired`, `TransportLost`, `Ended`, proven context destruction, stale generations, and superseded witnesses all dominate terminal or re-establishment attempts before adapter I/O. +## Decision + +1. `BoundBrowserSession::into_recovery(self)` is the only transition into recovery-only custody. It succeeds from `RecoveryRequired`, or from `TransportLost` only when exact `BrowserSessionRecoveryEvidence` or `DisposableContextCreateRecoveryEvidence` remains. +2. `Active`, `Ended`, and ownership-clean `TransportLost` are returned unchanged. Handoff performs no browser I/O. +3. Handoff moves the exact existing `BoundBrowserSession

` and same non-`Clone` adapter instance. +4. Recovery custody exposes lifecycle state and exact non-authorizing evidence. It exposes no raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create, presentation lookup, epoch advance, destroy, authorized operation, navigation transition, or normal finish. +5. `RecoveryContextOperationPort` is the only generic recovery-command path. Its request is private-construction and its success/failure leaves all Browser Session recovery state unchanged. +6. `RecoverySettlementPort` is the only generic proof-verification path. Protocol-specific proof vocabulary remains adapter-owned. +7. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque current-revision handles only for facts that currently exist. +8. `settle_recovery_fact` validates exact Browser Session id and incarnation before adapter I/O. A foreign fact returns `RecoverySettlementError::AuthorityMismatch`. +9. It then validates the current recovery revision and exact current ledger/index fact before adapter I/O. Replay, sibling handles issued before another settlement, or out-of-range facts return `RecoverySettlementError::StaleFact`. +10. Revision increment capacity is checked before verifier I/O; exhaustion fails closed as `RevisionExhausted`. +11. Only after those checks does Browser Session construct `RecoverySettlementRequest` and call the exact retained adapter's `verify_recovery_settlement`. +12. Verifier failure returns `RecoverySettlementError::Adapter(E)` and mutates neither recovery ledger nor Browser Session lifecycle state. +13. Verifier success retires exactly the selected fact. Identity-oriented and create-attempt ledgers are independent; one settlement never broad-erases both. +14. For `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, or `TransportLossOwnedHandle`, retirement may also remove the exact matching `Uncertain` hot-ownership record. `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnsettledAdapterHandle` retire evidence only and cannot consume an independently accepted same-valued owner. +15. A successful settlement advances the monotonic recovery revision. Every `RecoveryFact` issued before that mutation becomes stale. +16. Partial settlement preserves every unrelated sibling fact and keeps the aggregate in its unresolved state. +17. When both recovery ledgers are empty and no uncertain hot ownership remains, Browser Session reaches terminal `Ended`. It never transitions back to `Active` or restores ordinary browser command authority. +18. The crate-private `dispatch_recovery_operation` remains the only bridge receiving `&mut P`; callers never receive raw adapter access. +19. Negative capability boundaries remain executable contracts through rustdoc `compile_fail` and repository tests. +20. `BrowserSession.contexts` contains only current live or uncertain ownership. Proven ordinary destruction removes a hot ownership record; failed destruction retains it as `Uncertain` plus exact `UnprovenDestruction { context, context_epoch }`. +21. Proven destruction releases raw isolation/context identities for later reuse only under a new monotonic `BrowserContextEpoch`. Predecessor authority therefore cannot revive after ABA reuse. +22. `Drop` performs no browser I/O. Unresolved custody preserves process-local abandonment accounting. +23. Browser-observed navigation remains generation-qualified and protocol-agnostic. Admission revokes presentation authority without adapter I/O; commit is non-terminal; positive settlement, typed negative terminal, and download start share one exactly-once closure; explicit re-establishment consumes a new presentation epoch. +24. Presentation mutation and lifecycle cleanup remain separate. Navigation-invalidated presentation authority cannot mutate or authorize authority-based cleanup, while the exact ordinary bound lifecycle owner may destroy its retained context without reopening presentation authority. +25. This ADR remains `Proposed` until the complete slice reaches protected `main` with exact-head repository gates and independently observed real-browser recovery/destruction/navigation post-conditions. + +## Consequences + +Browser Session has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can issue purpose-bounded adapter commands and can consume independently qualified proof, but neither mechanism exposes the adapter or recreates ordinary Browser Session authority. + +Recovery settlement is deliberately revision-coarse. Settling any fact invalidates all fact handles issued under the prior revision, including unrelated siblings. This forces callers to reread the current evidence ledger after mutation and prevents index-shift replay at the cost of extra handle acquisition. Recovery fact counts are expected to be small, and correctness at this security boundary dominates preserving stale handles. + +Hot ownership remains proportional to current live/uncertain state rather than historical throughput. Durable history must be retained by a separate authorized persistence owner. + +The active #317 lineage keeps ownership generation, navigation witness generation, and recovery revision as separate authority dimensions. Raw browser identifiers never substitute for any of them. ## Failure and degraded behavior -If `into_recovery(self)` is called while the aggregate is `Active`, `Ended`, or `TransportLost` without any retained recovery/create-attempt evidence, no transition occurs and the original `BoundBrowserSession

` is returned to the caller. No adapter I/O occurs during either a successful or rejected handoff. +A rejected `into_recovery` performs no I/O and returns the original bound owner. A failed recovery command preserves custody and evidence. A successful recovery command also preserves custody and evidence; it is not proof. + +A settlement with a foreign, stale, replayed, or out-of-range `RecoveryFact` fails before verifier I/O. A verifier rejection fails after proof I/O but before domain mutation. In both cases the ledgers remain unchanged. -A failed recovery operation returns `RecoveryContextOperationError::Adapter` and preserves the same custody and evidence. A successful recovery operation returns the adapter-defined output but also preserves the same custody and evidence; a separate owner transition is required before uncertainty can be cleared. +A failed destruction never retires hot ownership. Transport loss preserves active handles as non-authorizing evidence and does not prove destruction. Transport loss with no unresolved browser state creates no recovery custody. -A failed destruction never retires the hot ownership record. The exact record becomes or remains `Uncertain`, exact `UnprovenDestruction { context, context_epoch }` evidence is retained, normal authority is closed, and the aggregate enters or remains in recovery. A transport loss preserves owned handles as non-authorizing evidence and does not prove destruction. Transport loss with no owned or otherwise unresolved browser state creates no recovery custody. +If monotonic incarnation, context epoch, navigation generation, or recovery revision allocation exhausts, allocation fails closed rather than wrapping authority identity. -Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but this is neither cleanup nor durable recovery. If monotonic epoch/incarnation/navigation-generation allocation is exhausted, allocation fails closed rather than reusing authority identity. +Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but it is neither cleanup nor durable recovery. ## Security / privacy / governance impact -The recovery wrapper is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, recovery evidence, and adapter-selected handles cannot reconstruct ordinary Browser Session authority. The exact adapter remains owned and is callable only through the purpose-bounded recovery trait; raw `P` never becomes ambient. An ownership-clean transport loss cannot mint that recovery-only operation capability. +Recovery custody is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, adapter-selected handles, and recovery evidence cannot reconstruct ordinary command authority. Deterministic Browser Session policy is never delegated to an LLM. -Bounded hot-state retirement occurs only after exact pre-I/O authority validation and proven destruction. Therefore resource-bounding cannot convert uncertain remote ownership into an untracked boundary. +The exact adapter remains owned and reachable only through purpose-bounded traits. `RecoveryFact` and `RecoverySettlementRequest` are opaque and non-caller-constructible. Session/incarnation/revision/exact-fact validation occurs before proof-verifier I/O, preventing foreign or replayed handles from turning protocol proof checking into an oracle or mutation channel. -Navigation events are evidence, not deterministic policy authority. Browser Session creates and consumes its own opaque navigation witness only after exact current ownership validation. This prevents raw protocol ids, delayed events, sibling contexts, prior incarnations, or superseded generations from rewriting current presentation authority. +Alias-safe hot-state retirement prevents a rejected or partial create that reuses remote values from deleting a distinct previously accepted owner. Resource bounding therefore cannot convert unresolved ownership into an untracked boundary. -This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into OriginWeave Browser Session. Recovery evidence may identify remote browser boundaries but does not itself contain page content or create a new purpose for PII processing. +This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into Browser Session. Recovery evidence may identify remote browser boundaries but creates no new purpose for page-content or PII processing. ## Tests and acceptance evidence - `crates/originweave-browser-session/src/recovery.rs` - `BoundBrowserSession::into_recovery` - `BoundBrowserSessionRecovery

` - - `RecoveryContextOperationRequest` - - `RecoveryContextOperationPort` - - `RecoveryContextOperationError` + - `RecoveryContextOperationRequest` / `RecoveryContextOperationPort` + - `RecoveryFact` + - `RecoverySettlementRequest

` / `RecoverySettlementPort` + - `RecoverySettlementError` + - `settle_recovery_fact` - negative `compile_fail` capability contracts - `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` - - unproven destroy moves the exact adapter and exact evidence without I/O - - transport loss with an unresolved owned handle moves the exact adapter and exact evidence without I/O + - unproven destroy and unresolved transport loss move exact adapter/evidence without I/O - `crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs` - - transport loss before any remote ownership cannot mint recovery custody - - transport loss after proven destruction cannot reopen recovery custody + - ownership-clean transport loss cannot mint recovery custody - `crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs` - - recovery operation executes through the exact retained adapter - - exact session/incarnation/state and both evidence ledgers reach the opaque request - - adapter failure preserves custody/evidence - - adapter success is not treated as cleanup proof + - same-adapter operation; exact provenance; success/failure do not clear uncertainty +- `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` + - single-fact settlement preserves siblings + - replay and predecessor sibling handles fail stale before proof I/O + - foreign session/incarnation fact fails before proof I/O + - proof failure is non-mutating + - identity and create-attempt ledgers retire independently + - complete reconciliation reaches terminal `Ended` without authority resurrection +- `tests/test_browser_session_recovery_operation_contract.py` + - recovery-operation adapter custody and nonconstructible request surface +- `tests/test_browser_session_recovery_settlement_contract.py` + - opaque settlement API, pre-I/O validation order, external hostile fixture, and ADR/trace/UML/doctoring currentness - `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` - - 258 same-handle ownership generations remain admissible after proven destruction - - epochs are strictly monotonic - - retained predecessor authority fails before lifecycle adapter I/O -- destroy-failure tests require `Uncertain` ownership plus exact `UnprovenDestruction` evidence when destruction is not proven. -- navigation owner tests cover revocation, current-witness commit/terminal ordering, supersession, cleanup separation, trust-state precedence, and presentation-epoch conservation. -- `tests/test_browser_session_lifecycle_contract.py` pins recovery wrapper surface, hostile fixtures, ADR 0116, traceability, and UML doctoring. -- `tests/test_browser_session_navigation_owner_surface_contract.py` pins the production-owner navigation API and its opaque witness/epoch-separation contract independently from stacked #318/#321 acceptance. + - 258 same-handle generations; monotonic epochs; predecessor rejection +- navigation owner tests and `tests/test_browser_session_navigation_owner_surface_contract.py` + - current-witness revocation/closure/re-establishment and cleanup separation These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. ## Migration and rollback -This active-PR change is additive at the ownership-type boundary but changes the internal retention, recovery-operation, and navigation-admission model. Dependents must adopt it by ordinary non-force restack after the parent exact head is verified; they must not copy Browser Session source or infer recovery/navigation authority from adapter identifiers. +Dependents must adopt this foundation by ordinary non-force restack after the parent exact head is verified. They must not copy Browser Session source, infer recovery authority from raw identifiers, or reconstruct a second adapter. -Rollback before protected-main adoption is performed by reverting the whole recovery-custody/hot-retirement/recovery-operation/navigation-authority slice together with its hostile fixtures and ADR, not by selectively restoring raw adapter access, allowing evidence-free transport loss to mint recovery custody, keeping record eviction without stale-authority tests, or restoring raw-id navigation authority. After protected-main adoption, rollback requires a policy-compliant change that preserves all unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. +Rollback before protected-main adoption reverts the recovery-custody, settlement, hot-retirement, and navigation-authority slice together with its hostile fixtures and ADR. Selectively restoring raw adapter access, evidence-free recovery custody, ACK-as-proof, raw-index settlement, or authority resurrection is not a valid rollback. After protected-main adoption, rollback requires another policy-compliant change that preserves unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. ## Open follow-ups -- #316: adopt the released/verified Browser Session recovery-operation boundary and implement purpose-bounded WebDriver BiDi recovery operations, pending/accepted/quarantined correlation, event replay qualification, and remote-liveness semantics without reconstructing an adapter. -- #318/#321: executable acceptance/review successors for the implemented #317 navigation contract, including same-raw-id ABA and sibling-recreation matrices; they do not own a second production state machine. -- Durable crash/process-restart persistence of exact recovery evidence and buyer-required audit history. +- #316: after #317 exact-head executable GREEN, ordinary non-force adoption of the generic recovery boundary; implement WebDriver BiDi proof qualification, pending/accepted/quarantined correlation, event replay qualification, remote liveness, and pinned-Chromium recovery post-condition evidence. +- #318/#321: executable acceptance/review successors for the #317 navigation contract and same-raw-id/sibling-recreation matrices. +- Durable crash/process-restart persistence of exact recovery facts and buyer-required audit history. - Real Chromium proof of remote destruction, cleanup, navigation, interaction, recovery, and browser-observed post-conditions. -- `docs/product-technical-gap-baseline.md` and release evidence must stay synchronized with protected-main truth; active-PR implementation is not shipment. -- Protected-main immutable release, SBOM, provenance, reproducibility, and rollback evidence. +- `docs/product-technical-gap-baseline.md` must distinguish active-PR implementation from protected-main/release evidence. +- Protected-main immutable release, signed artifacts, SBOM, provenance, reproducibility, and rollback evidence. ## Supersession / reversal conditions -Supersede this ADR only when a later Accepted design provides at least equivalent guarantees for same-instance recovery custody, zero ambient adapter escape, purpose-bounded same-adapter recovery I/O only for unresolved ownership, exact uncertain-ownership retention, bounded command-authority hot state, monotonic stale-authority rejection across raw-id reuse, generation-bound navigation witness custody, single-assignment terminal closure, presentation/lifecycle cleanup separation, and separation of durable history from command admission. A protocol adapter that merely offers different identifiers, reconnects to the same endpoint, or reports command success does not satisfy those guarantees. +A successor may supersede this ADR only if it preserves at least: same-instance recovery custody; no ambient adapter escape; evidence-gated recovery capability; command-ACK/proof separation; opaque exact-fact settlement; pre-I/O foreign/stale rejection; sibling preservation; independent ledger retirement; alias-safe hot ownership; terminal non-resurrection; bounded command-authority state; monotonic ABA rejection; generation-bound navigation custody; presentation/lifecycle cleanup separation; and separation of durable history from command admission. -Changing the recovery owner or persistence architecture does not by itself require restoring destroyed tombstones to the hot map; the replacement must state how command authority remains bounded and how durable evidence is retained independently. +Changing the recovery owner or persistence architecture alone does not justify weakening those guarantees. ## References From 578d26f9e41aa7326cf27614394eece7c4ef3241 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 15:24:09 +0900 Subject: [PATCH 135/632] docs(trace): connect recovery facts to settlement proof --- .../browser-session-lifecycle-authority.md | 136 ++++++++++-------- 1 file changed, 79 insertions(+), 57 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index ed693474d..f4e112f3b 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -8,7 +8,7 @@ ## Problem and invariant -Browser-session, user-context/isolation, browsing-context, adapter-selected identifiers, and recovery evidence are addresses or evidence. They are not proof that the current Browser Session aggregate exclusively owns lifecycle or presentation mutation. +Browser-session, user-context/isolation, browsing-context, adapter-selected identifiers, navigation ids, and recovery evidence are addresses or evidence. They are not proof that the current Browser Session aggregate owns lifecycle or presentation mutation, and they are not self-authenticating recovery settlement authority. The active implementation establishes this chain: @@ -23,112 +23,134 @@ validated BrowserSessionId → aggregate validates returned isolation/context against current ownership → aggregate privately constructs DisposableContextCreateCompletion(attempt, Accepted|Rejected) → accepted candidate may become adapter-authorizing; rejected candidate remains quarantined -→ uncertain/rejected create paths retain exact aggregate-issued attempt identity as non-authorizing recovery evidence +→ uncertain/rejected create paths retain exact attempt and identity facts as non-authorizing recovery evidence → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) → exact authority validation before ordinary lifecycle or purpose-bounded adapter I/O → lifecycle destruction uses private DisposableContextDestroyRequest(handle, validated epoch) → presentation work uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) → exact consumed adapter only -→ failed/unproven destruction retains exact handle + validated epoch as non-authorizing recovery evidence -→ RecoveryRequired|TransportLost can consume the same bound owner into BoundBrowserSessionRecovery

-→ recovery custody exposes exact non-authorizing evidence and one purpose-bounded RecoveryContextOperationPort path -→ recovery wrapper privately builds RecoveryContextOperationRequest(session, incarnation, state, evidence, operation) -→ the crate-private bridge dispatches that request through the exact retained adapter without exposing raw P -→ adapter success/failure leaves unresolved Browser Session state/evidence unchanged -→ proven destruction removes the live hot-ownership record; failed destruction retains Uncertain ownership +→ failed/unproven destruction retains exact handle + epoch and enters RecoveryRequired +→ RecoveryRequired|evidence-bearing TransportLost may consume the same bound owner into BoundBrowserSessionRecovery

+→ recovery command path privately builds RecoveryContextOperationRequest and uses the same adapter +→ adapter command success/failure leaves unresolved Browser Session state/evidence unchanged +→ recovery custody issues opaque current-revision RecoveryFact values for exact recovery facts +→ caller supplies independently qualified adapter proof with one RecoveryFact +→ settle_recovery_fact validates session/incarnation/revision/exact fact before proof I/O +→ exact retained adapter verifies RecoverySettlementRequest through RecoverySettlementPort +→ verifier failure leaves both ledgers unchanged +→ verifier success retires exactly one fact and advances the recovery revision +→ predecessor/replayed/sibling handles issued under the old revision become stale +→ exact uncertain owned context is removed only for ownership evidence that names that same handle +→ all facts + uncertain hot ownership gone → terminal Ended; ordinary authority is never restored +→ proven ordinary destruction removes live hot ownership; failed destruction retains Uncertain ownership → BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` -`BoundBrowserSession` is the linear lifecycle-port binding. Public create/destroy methods accept no arbitrary port argument, and there is **no public raw port accessor**. Application code cannot recover `&P`, `&mut P`, or a generic callback that would recreate unrestricted adapter authority. +`BoundBrowserSession` is the linear lifecycle-port binding. Public create/destroy methods accept no arbitrary port argument, and there is no public raw port accessor. `BoundBrowserSessionRecovery` preserves the same adapter but is a reduced-capability owner, not an alternate ordinary lifecycle path. -The wrapper has a manual redacted `Debug` implementation. Formatting exposes inert Browser Session summary fields only and never calls `P::fmt`, so a side-effecting or secret-bearing adapter `Debug` cannot become a diagnostic capability escape. - -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, `AuthorizedContextOperationRequest`, and `RecoveryContextOperationRequest` have private construction paths. Create attempt epochs and validated context epochs are correlation/provenance, not standalone bearer authority. +`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, `AuthorizedContextOperationRequest`, `RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest

` all have private construction fields. Epochs, revisions, protocol ids, and evidence are correlation/provenance; none is standalone bearer authority. ## Transactional remote creation -A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing yet. Browser Session accepts or rejects the returned domain handle and settles that exact attempt through `DisposableContextCreateCompletion`. +A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing until Browser Session accepts that exact attempt through `DisposableContextCreateCompletion`. -`DisposableContextCreateRecoveryEvidence` preserves the transaction dimension that raw handle evidence cannot represent. `FailedUncertain` stores the exact aggregate-issued `attempt_epoch` even when no complete handle exists; `DuplicateCandidate` binds an aliased returned handle to its exact rejected attempt; `CompletionUnsettled` binds the exact attempt, `Accepted|Rejected` disposition, and complete returned handle when settlement cannot be proven. This evidence grants no browser authority. +`DisposableContextCreateRecoveryEvidence` preserves the transaction dimension that raw handle evidence cannot represent. `FailedUncertain` stores the exact aggregate-issued attempt epoch even without a complete handle; `DuplicateCandidate` binds an aliased candidate to its exact rejected attempt; `CompletionUnsettled` binds exact attempt, disposition, and returned handle when settlement cannot be proven. -Identity-oriented `BrowserSessionRecoveryEvidence` remains separately useful for exact remote reconciliation. A same-valued later candidate does not erase a previously accepted ownership fact represented independently by `RecoveryRequiredOwnedHandle`. +Identity-oriented `BrowserSessionRecoveryEvidence` is independently useful for ownership reconciliation. The two ledgers are deliberately separate. A later or rejected same-valued candidate cannot erase a previously accepted ownership fact merely because remote values alias. -Protocol-specific tuple contents and pending/accepted/quarantined storage remain #314/#316 responsibilities. Browser Session owns only attempt identity, domain validation, accept/reject decision, current authority validation, and non-authorizing recovery facts. +Protocol pending/accepted/quarantined tuple storage remains #314/#316 responsibility. Browser Session owns attempt identity, domain accept/reject, current command authority, non-authorizing recovery facts, and deterministic exact-fact retirement after proof verification. ## Same-bound-adapter ordinary authorized operations -`AuthorizedContextOperationPort` extends the lifecycle port for post-create presentation work. Browser Session validates session incarnation, isolation, browsing-context identity, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed into `BoundBrowserSession`. Stale or foreign authority returns `AuthorizedContextOperationError::BrowserSession` before adapter I/O; an adapter failure returns `AuthorizedContextOperationError::Adapter`. +`AuthorizedContextOperationPort` extends the lifecycle port for post-create presentation work. Browser Session validates session incarnation, isolation identity, browsing-context identity, current presentation state, and context epoch before creating `AuthorizedContextOperationRequest` and routing it to the same `port: P` already consumed by `BoundBrowserSession`. + +Stale or foreign authority fails before adapter I/O as `AuthorizedContextOperationError::BrowserSession`. Adapter failures remain typed as `AuthorizedContextOperationError::Adapter`. No raw `P`, second adapter, or unrestricted callback is exposed. + +## Recovery-only custody and command path + +`BoundBrowserSession::into_recovery(self)` is one-way. It succeeds from `RecoveryRequired`, or from `TransportLost` only if exact unresolved recovery/create-attempt evidence remains. Ownership-clean transport loss cannot mint recovery capability. + +Recovery custody exposes `state()`, both evidence ledgers, and—when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. It exposes neither raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create/presentation/navigation/cleanup authority, nor normal finish. -No raw `P` reference, second adapter, or unrestricted `FnOnce(&mut P)` is exposed. +`RecoveryContextOperationRequest` snapshots exact Browser Session id, incarnation, unresolved state, both evidence ledgers, and the adapter-defined operation immediately before I/O. `BoundBrowserSession::dispatch_recovery_operation` is `pub(crate)`. `RecoveryContextOperationError::Adapter` preserves adapter failure. Both success and failure leave Browser Session uncertainty unchanged: command ACK is not destruction or reconciliation proof. -## Recovery-only custody and same-adapter recovery operation +## Proof-bearing exact-fact recovery settlement -`BoundBrowserSession::into_recovery(self)` is the one-way custody boundary for unresolved ownership. It succeeds only from `RecoveryRequired` or `TransportLost`, moves the exact already-consumed non-`Clone` adapter without browser I/O, and returns `BoundBrowserSessionRecovery

`. `Active` or `Ended` returns the original bound owner unchanged. +Recovery completion is a separate path. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque `RecoveryFact` values only for currently addressable facts. Each handle binds exact Browser Session id, process-local incarnation, ledger kind, index, and the current monotonic recovery revision. -Recovery custody exposes `state()`, `recovery_evidence()`, `create_attempt_recovery_evidence()`, and—when the retained adapter implements `RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. It exposes neither raw `P`, the inner `BoundBrowserSession`, nor the inner `BrowserSession`; ordinary create, presentation-authority lookup, epoch advancement, destroy, `AuthorizedContextOperationPort`, navigation authority, and normal finish remain unavailable. Rustdoc `compile_fail` contracts pin those negative capabilities. +`settle_recovery_fact(fact, proof)` validates in this order: -The recovery operation does not weaken that boundary. `RecoveryContextOperationRequest` is created only inside recovery custody and snapshots exact `BrowserSessionId`, `BrowserSessionIncarnation`, unresolved `BrowserSessionState`, `BrowserSessionRecoveryEvidence`, `DisposableContextCreateRecoveryEvidence`, and the adapter-defined operation immediately before I/O. `BoundBrowserSession::dispatch_recovery_operation` is `pub(crate)` and lives in the Browser Session owner module; downstream code cannot call it or supply a callback receiving `&mut P`. +1. exact Browser Session id and incarnation; +2. current recovery revision; +3. exact current ledger/index fact and next-revision capacity; +4. retained-adapter proof verification through `RecoverySettlementPort::verify_recovery_settlement(RecoverySettlementRequest)`; +5. exact one-fact retirement; +6. monotonic revision advance. -`RecoveryContextOperationPort` owns its operation/output/error vocabulary. `RecoveryContextOperationError::Adapter` preserves typed failure. Both adapter success and failure leave Browser Session state and evidence unchanged. A successful call is not destruction proof, not reconciliation proof, and not presentation authority. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and the meaning of concrete recovery commands. +`AuthorityMismatch`, `StaleFact`, and `RevisionExhausted` are pre-I/O failures. `RecoverySettlementError::Adapter(E)` is post-verifier/pre-mutation. A failed proof does not consume evidence. A successful settlement invalidates every fact handle issued under the previous revision, including sibling handles, so the caller must reread current custody after mutation. -## Lossless recovery evidence while retained +The two recovery ledgers remain independently consumable. Retiring `BrowserSessionRecoveryEvidence` never implicitly erases matching `DisposableContextCreateRecoveryEvidence` and vice versa. -`CreateFailedClean` is valid only when no disposable browser state exists. `CreateFailedUncertain(Some(isolation))` retains both the exact known isolation identity and exact aggregate-issued create-attempt epoch; `CreateFailedUncertain(None)` still retains the exact attempt epoch. Duplicate output and completion failure preserve exact transaction identity. Failed or unproven destruction records the exact owned handle and validated `BrowserContextEpoch`. Entering `RecoveryRequired` projects still-active siblings as `RecoveryRequiredOwnedHandle`; transport loss records active handles as `TransportLossOwnedHandle`. None of this evidence grants browser command authority. +Ownership retirement is alias-safe. Only `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, or `TransportLossOwnedHandle` may remove an exact matching `Uncertain` hot ownership record. `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnsettledAdapterHandle` retire evidence only; a rejected candidate that reuses remote values cannot delete a distinct accepted owner. -## Bounded hot ownership and Sequential ABA +When both ledgers are empty and no uncertain hot ownership remains, Browser Session reaches `Ended`. Recovery custody never recreates `Active`, create authority, `PresentationMutationAuthority`, navigation authority, or ordinary cleanup authority. -Hot command-authority state contains only live or uncertain ownership. After exact authority validation and adapter-confirmed destruction, Browser Session removes that context record instead of accumulating a permanent tombstone. A failed destroy retains the exact record as `Uncertain` plus `UnprovenDestruction { context, context_epoch }`. +#316 remains canonical owner of WebDriver BiDi proof qualification, pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery commands. A protocol event is evidence, not Browser Session policy authority. #316 maps independently qualified evidence into `RecoverySettlementPort::Proof`; Browser Session only validates and consumes its own exact fact. -The 258-generation hostile fixture proves the same raw isolation/context values can be reused after proven destruction while epochs remain monotonic. Immediately after destruction a retained authority fails as `ContextNotOwned`; after same-raw-id recreation it fails as `AuthorityMismatch`. Sequential ABA across independent aggregates is also rejected by `BrowserSessionIncarnation` even when external ids and local epoch values alias. +## Lossless evidence and bounded hot ownership -Removing proven-destroyed command-authority records is not durable history deletion. Durable crash/process-restart recovery and buyer audit history remain a separate persistence concern and must not be reconstructed from the bounded hot map or `abandoned_bound_session_count()`. +`CreateFailedClean` is valid only when no disposable browser state exists. Uncertain create, duplicate output, completion failure, failed destroy, sibling invalidation, and transport loss preserve exact facts without granting browser authority. + +Hot command-authority state contains only live or uncertain ownership. Proven ordinary destruction removes the current hot record. Failed destroy retains the exact record as `Uncertain` plus `UnprovenDestruction { context, context_epoch }`. + +The 258-generation hostile fixture proves that the same raw isolation/context values may be reused after proven destruction while BrowserContextEpoch remains monotonic. Immediately after destruction a retained predecessor authority fails `ContextNotOwned`; after recreation it fails `AuthorityMismatch`. Across aggregate recreation, `BrowserSessionIncarnation` rejects stale authority even when raw ids and local epochs alias. + +Removing a proven-destroyed record is not durable history deletion. Cross-process recovery and buyer audit history remain separate persistence concerns. ## Abandonment and lifecycle completion -`BoundBrowserSession

` is `#[must_use]`. A failed `finish()` does not consume the wrapper; the exact bound adapter and ownership ledger remain available for cleanup/retry. `Drop` never performs browser I/O and never treats object destruction as browser destruction proof. Dropping unresolved ordinary or recovery custody increments the process-local `abandoned_bound_session_count()` signal only. +`BoundBrowserSession

` and recovery custody are `#[must_use]` linear owners. `Drop` never performs browser I/O. Dropping unresolved custody increments only the process-local `abandoned_bound_session_count()` signal. -The abandonment counter and Browser Session incarnation allocator use the non-deprecated `AtomicU64::try_update` API while preserving ordering and overflow behavior. +Successful exact-fact reconciliation updates underlying aggregate state before eventual drop. If every fact and uncertain owner is retired, the aggregate is `Ended`, so dropping a fully reconciled recovery wrapper is not reported as unresolved abandonment. ## Navigation authority interaction -The active #317 lineage admits an observed navigation only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` generation. It revokes presentation authority without adapter I/O, mints an opaque `NavigationSettlementAuthority`, treats commit as non-terminal progress, uses one exactly-once closure for positive settlement / typed negative terminal / download start, and permits one explicit re-establishment opportunity. A newer navigation supersedes an older witness. Aggregate trust and current live ownership dominate settlement and re-establishment. +The #317 lineage admits observed navigation only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` generation. Admission revokes presentation authority without adapter I/O and mints opaque `NavigationSettlementAuthority`. Commit is non-terminal; positive settlement, typed negative terminal, and download start share one exactly-once closure. A newer navigation supersedes an older witness. Explicit re-establishment alone consumes the next presentation epoch. + +Presentation mutation and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup; the exact ordinary bound lifecycle owner may still destroy its retained context without reopening presentation authority. -Presentation mutation and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup; the exact bound lifecycle owner may still perform `destroy_owned_disposable_context` without reopening presentation authority. +Recovery settlement is also separate from navigation settlement. Completing remote-ownership reconciliation cannot recreate navigation or presentation authority. ## Browser-issued identity and standards trace -`DisposableIsolationId` maps one-to-one to WebDriver BiDi `browser.UserContext` and preserves protocol text losslessly. OriginWeave does not trim, normalize, impose the removed 4096-byte limit, or treat that address as command authority. +`DisposableIsolationId` maps to WebDriver BiDi `browser.UserContext` and preserves protocol text losslessly. OriginWeave does not normalize that address or treat it as command authority. -The latest immutable W3C WebDriver BiDi Working Draft directly verified on 2026-09-15 is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous published version. The mutable `/TR/webdriver-bidi/` index and the separately qualified Chromium/runtime revision are distinct provenance axes. A command ACK is insufficient proof that a disposable boundary is actually gone. +The immutable W3C WebDriver BiDi Working Draft verified for this lineage is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`). The mutable `/TR/webdriver-bidi/` index and separately qualified Chromium runtime revision are distinct provenance axes. A command acknowledgement is insufficient proof that a disposable boundary is gone. ## Source and executable evidence | Invariant | Source / test | |---|---| | independent Browser Session bounded context | `crates/originweave-browser-session/`; `tests/test_browser_session_lifecycle_contract.py` | -| lifecycle port ownership is structural; no public raw port accessor | `BoundBrowserSession`; lifecycle binding hostile tests | -| create requests are aggregate-issued and attempt-scoped | `DisposableContextCreateRequest::attempt_epoch`; transaction hostile fixture | -| same-valued prior owner and later candidate remain distinct recovery facts | `RecoveryRequiredOwnedHandle`; `create_recovery_same_handle_distinct_fact.rs` | -| same consumed adapter handles ordinary purpose-bounded work | `AuthorizedContextOperationPort`; `authorized_context_operation.rs` | -| same consumed adapter handles recovery-only work without raw adapter escape | `RecoveryContextOperationPort`; `RecoveryContextOperationRequest`; `recovery_same_adapter_operation.rs` | -| recovery success/failure preserves unresolved state/evidence | `recovery_same_adapter_operation.rs` | -| unproven destroy preserves exact handle + epoch | `BrowserSessionRecoveryEvidence::UnprovenDestruction`; destroy-failure tests | -| `RecoveryRequired` preserves indirectly invalidated siblings | `RecoveryRequiredOwnedHandle`; `recovery_required_sibling_evidence.rs` | -| transport loss preserves exact active handles | `TransportLossOwnedHandle`; `transport_loss_recovery_evidence.rs` | -| unresolved state moves to recovery-only custody without replacing adapter | `BoundBrowserSession::into_recovery`; `recovery_owner_handoff.rs` | -| recovery custody cannot regain ordinary lifecycle or presentation authority | `BoundBrowserSessionRecovery` rustdoc `compile_fail`; repository contract | -| proven destruction releases bounded hot ownership while stale authority remains rejected | `proven_destroy_releases_hot_ownership.rs` | -| unresolved wrapper drop performs no browser I/O and is observable | `abandoned_bound_session_count`; `bound_session_abandonment.rs` | -| failed finish retains exact bound owner | `BoundBrowserSession::finish`; abandonment fixture | -| protocol-valid user-context identity is preserved losslessly | `user_context_identity_length.rs` | -| transport liveness remains orthogonal | `BrowserSession::record_transport_loss` | -| navigation witness is opaque and context-generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | - -Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source and tests remain non-shipment until exact-head repository contracts, rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required security/review workflows, and protected `main` integration are observed. +| structural lifecycle-port ownership; no public raw adapter | `BoundBrowserSession`; lifecycle hostile tests | +| aggregate-issued create attempt | `DisposableContextCreateRequest::attempt_epoch`; transaction fixture | +| same consumed adapter for ordinary work | `AuthorizedContextOperationPort`; `authorized_context_operation.rs` | +| same consumed adapter for recovery commands | `RecoveryContextOperationPort`; `RecoveryContextOperationRequest`; `recovery_same_adapter_operation.rs` | +| command success/failure does not settle ownership | `recovery_same_adapter_operation.rs` | +| exact proof-bearing recovery fact | `RecoveryFact`; `RecoverySettlementRequest`; `RecoverySettlementPort`; `settle_recovery_fact` | +| replay/sibling/foreign proof cases | `recovery_exact_fact_settlement.rs` | +| recovery settlement surface remains opaque/current | `tests/test_browser_session_recovery_settlement_contract.py` | +| unproven destroy preserves exact handle + epoch | `UnprovenDestruction`; destroy-failure tests | +| ownership-clean transport loss cannot mint recovery | `recovery_handoff_requires_unresolved_ownership.rs` | +| proven destruction bounds hot ownership | `proven_destroy_releases_hot_ownership.rs` | +| recovery custody cannot regain ordinary authority | recovery rustdoc `compile_fail`; repository contracts | +| navigation witness is opaque and generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | + +Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source remains non-shipment until exact-head repository contracts, rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review/security gates, and protected `main` integration are observed. ## Current integration boundary -#317 owns Browser Session domain policy and the generic same-adapter recovery-operation boundary. #318/#321 own stacked hostile acceptance and doctoring only. #316 owns WebDriver BiDi pending/accepted/quarantined state, event correlation, remote liveness, protocol-specific recovery semantics, and real-browser adapter integration. No child may copy Browser Session production source or reconstruct a second adapter from raw identifiers. +#317 owns Browser Session domain policy, same-adapter recovery-command custody, and generic exact-fact settlement. #318/#321 own stacked hostile navigation/ABA acceptance and doctoring only. #316 owns WebDriver BiDi proof qualification, pending/accepted/quarantined state, event correlation, remote liveness, protocol-specific recovery semantics, and real-browser adapter integration. -Real Chromium navigation, interaction, cleanup, recovery, and browser-observed post-condition evidence remains required before shipment. Immutable release/SBOM/provenance/reproducibility/rollback evidence remains a separate release gate. \ No newline at end of file +Real Chromium navigation, interaction, cleanup, recovery, and browser-observed post-condition evidence remains required before shipment. Immutable release, signed artifact, SBOM, provenance, reproducibility, and rollback evidence remain separate release gates. From a99ea6bce6174fa98336f455d2ce2b1634f361b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:00:41 +0900 Subject: [PATCH 136/632] test(browser-session): close recovery I/O after settlement --- .../recovery_operation_terminal_closure.rs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs diff --git a/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs b/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs new file mode 100644 index 000000000..6420cecde --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs @@ -0,0 +1,126 @@ +use std::cell::Cell; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionState, DisposableContextCreateCompletion, + DisposableContextCreateCompletionError, DisposableContextCreateError, + DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest, + DisposableContextHandle, DisposableContextPort, DisposableIsolationId, + RecoveryContextOperationError, RecoveryContextOperationPort, RecoveryContextOperationRequest, + RecoverySettlementPort, RecoverySettlementRequest, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct RecoveryPort { + context: DisposableContextHandle, + recovery_operation_calls: Rc>, +} + +impl DisposableContextPort for RecoveryPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + Ok(self.context.clone()) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Err(DisposableContextDestroyError::DestroyFailed) + } +} + +impl RecoveryContextOperationPort for RecoveryPort { + type Operation = (); + type Output = (); + type Error = (); + + fn execute_recovery_context_operation( + &mut self, + _request: &RecoveryContextOperationRequest, + ) -> Result { + self.recovery_operation_calls + .set(self.recovery_operation_calls.get() + 1); + Ok(()) + } +} + +impl RecoverySettlementPort for RecoveryPort { + type Proof = (); + type Error = (); + + fn verify_recovery_settlement( + &mut self, + _request: &RecoverySettlementRequest, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +fn session(value: u64) -> Result { + BrowserSessionId::new(value).map_err(|_| "fixture browser session must be valid") +} + +fn context(value: u64) -> Result { + BrowsingContextId::new(value).map_err(|_| "fixture browsing context must be valid") +} + +#[test] +fn terminal_recovery_settlement_revokes_generic_recovery_io() -> Result<(), &'static str> { + let recovery_operation_calls = Rc::new(Cell::new(0)); + let port = RecoveryPort { + context: DisposableContextHandle::new( + DisposableIsolationId::parse("terminal-recovery-context") + .map_err(|_| "fixture isolation must be representable")?, + context(91_001)?, + ), + recovery_operation_calls: Rc::clone(&recovery_operation_calls), + }; + let mut bound = BrowserSession::start(session(9_101)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture create must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + + let mut recovery = bound + .into_recovery() + .map_err(|_| "unproven destruction must enter recovery custody")?; + recovery + .execute_recovery_context_operation(()) + .map_err(|_| "recovery operation must be available while uncertainty remains")?; + assert_eq!(recovery_operation_calls.get(), 1); + + let fact = recovery + .recovery_fact(0) + .ok_or("unproven destruction recovery fact must exist")?; + recovery + .settle_recovery_fact(fact, ()) + .map_err(|_| "independently verified recovery fact must settle")?; + assert_eq!(recovery.state(), BrowserSessionState::Ended); + + assert_eq!( + recovery.execute_recovery_context_operation(()), + Err(RecoveryContextOperationError::RecoveryClosed), + "terminal recovery custody must not retain a generic adapter-I/O capability" + ); + assert_eq!( + recovery_operation_calls.get(), + 1, + "terminal-state rejection must happen before retained-adapter I/O" + ); + Ok(()) +} From 037ec490b9d1507cb1b2632764e30992eabf4d04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:02:49 +0900 Subject: [PATCH 137/632] fix(browser-session): revoke terminal recovery operations --- crates/originweave-browser-session/src/recovery.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index a63975521..b0ac2f8e4 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -82,6 +82,8 @@ pub trait RecoveryContextOperationPort: DisposableContextPort { /// Failure from executing one recovery operation through the exact retained adapter. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RecoveryContextOperationError { + /// Recovery custody has already reached a terminal state with no unresolved command purpose. + RecoveryClosed, /// The retained adapter attempted the recovery operation and returned its bounded failure. Adapter(E), } @@ -367,11 +369,18 @@ impl BoundBrowserSessionRecovery

{ /// The request snapshots the unresolved aggregate state and both recovery-evidence ledgers before /// adapter I/O. Adapter success or failure leaves Browser Session state and evidence unchanged; /// protocol-specific code must provide separate, reviewed reconciliation proof before uncertainty - /// can be resolved. + /// can be resolved. Once exact-fact settlement closes recovery to `Ended`, later operations fail + /// before retained-adapter I/O. pub fn execute_recovery_context_operation( &mut self, operation: P::Operation, ) -> Result> { + if !matches!( + self.state(), + BrowserSessionState::RecoveryRequired | BrowserSessionState::TransportLost + ) { + return Err(RecoveryContextOperationError::RecoveryClosed); + } self.bound.dispatch_recovery_operation(|session, port| { let request = RecoveryContextOperationRequest { browser_session: session.id(), From b51aa7ca60a880cd5ad6b9de68f9fdfba54b60c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:03:43 +0900 Subject: [PATCH 138/632] docs(browser-session): close terminal recovery capability --- ...sion-recovery-custody-and-hot-ownership.md | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index 50e7c7554..bfeecb136 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -24,6 +24,7 @@ WebDriver BiDi pending/accepted/quarantined tuple truth, protocol event correlat - Preserve the exact consumed adapter across unresolved ownership without making it ambient. - Permit only purpose-bounded recovery I/O; never expose raw `P` or an unrestricted callback. +- Revoke purpose-bounded recovery I/O once proof-bearing settlement removes the final unresolved fact. - Do not mint recovery custody from `TransportLost` when no unresolved ownership fact exists. - Keep adapter command success/failure separate from independent recovery proof. - Bind settlement to one opaque Browser Session-issued fact, not a raw vector index or protocol identifier. @@ -38,9 +39,9 @@ WebDriver BiDi pending/accepted/quarantined tuple truth, protocol event correlat ## Authority boundaries -Browser Session owns lifecycle identity, ownership state, context epochs, ordinary lifecycle/presentation admission, navigation-generation custody, the one-way transition into recovery custody, opaque recovery-fact issuance, and deterministic exact-fact retirement. +Browser Session owns lifecycle identity, ownership state, context epochs, ordinary lifecycle/presentation admission, navigation-generation custody, the one-way transition into recovery custody, opaque recovery-fact issuance, deterministic exact-fact retirement, and terminal revocation of recovery-command authority. -`BoundBrowserSessionRecovery

` owns the same concrete adapter instance but is not a protocol-specific recovery engine. When `P: RecoveryContextOperationPort`, it may execute a purpose-bounded recovery operation through `RecoveryContextOperationRequest`. This path snapshots exact Browser Session identity, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-defined operation. Adapter success or failure does not mutate Browser Session uncertainty. +`BoundBrowserSessionRecovery

` owns the same concrete adapter instance but is not a protocol-specific recovery engine. When `P: RecoveryContextOperationPort`, it may execute a purpose-bounded recovery operation through `RecoveryContextOperationRequest` only while the aggregate remains `RecoveryRequired` or `TransportLost`. This path snapshots exact Browser Session identity, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-defined operation. Adapter success or failure does not mutate Browser Session uncertainty. Once proof-bearing settlement closes custody to `Ended`, `execute_recovery_context_operation` returns `RecoveryContextOperationError::RecoveryClosed` before retained-adapter I/O. When `P: RecoverySettlementPort`, recovery custody may also issue opaque `RecoveryFact` handles and execute `settle_recovery_fact(fact, proof)`. `RecoveryFact` binds the exact Browser Session id, process-local incarnation, ledger kind, current ledger index, and monotonic recovery revision. `RecoverySettlementRequest

` is privately constructed only after Browser Session validates that handle against current custody. The exact retained adapter verifies the independently supplied proof. Browser Session, not the adapter, then commits retirement of exactly the selected fact. @@ -72,6 +73,10 @@ Rejected. Transport loss proves only liveness loss. Before any remote ownership, Rejected. Command completion is not a browser-observed post-condition. `execute_recovery_context_operation` always preserves Browser Session uncertainty. +### Keep generic recovery I/O callable after complete settlement + +Rejected. Once all recovery facts and uncertain ownership are gone, the purpose that justified access to the retained adapter is gone as well. Retaining the generic recovery-command path after `Ended` would be an ambient post-recovery capability even though ordinary browser authority is intentionally not resurrected. + ### Let the adapter delete recovery evidence directly Rejected. That would move domain ownership truth into an adapter and let protocol data rewrite policy state. @@ -106,7 +111,7 @@ Rejected. Only proven destruction or proof-bearing recovery settlement may retir 2. `Active`, `Ended`, and ownership-clean `TransportLost` are returned unchanged. Handoff performs no browser I/O. 3. Handoff moves the exact existing `BoundBrowserSession

` and same non-`Clone` adapter instance. 4. Recovery custody exposes lifecycle state and exact non-authorizing evidence. It exposes no raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create, presentation lookup, epoch advance, destroy, authorized operation, navigation transition, or normal finish. -5. `RecoveryContextOperationPort` is the only generic recovery-command path. Its request is private-construction and its success/failure leaves all Browser Session recovery state unchanged. +5. `RecoveryContextOperationPort` is the only generic recovery-command path. Its request is private-construction and its success/failure leaves all Browser Session recovery state unchanged. It is callable only while custody remains `RecoveryRequired` or `TransportLost`; terminal `Ended` returns `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. 6. `RecoverySettlementPort` is the only generic proof-verification path. Protocol-specific proof vocabulary remains adapter-owned. 7. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque current-revision handles only for facts that currently exist. 8. `settle_recovery_fact` validates exact Browser Session id and incarnation before adapter I/O. A foreign fact returns `RecoverySettlementError::AuthorityMismatch`. @@ -118,7 +123,7 @@ Rejected. Only proven destruction or proof-bearing recovery settlement may retir 14. For `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, or `TransportLossOwnedHandle`, retirement may also remove the exact matching `Uncertain` hot-ownership record. `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnsettledAdapterHandle` retire evidence only and cannot consume an independently accepted same-valued owner. 15. A successful settlement advances the monotonic recovery revision. Every `RecoveryFact` issued before that mutation becomes stale. 16. Partial settlement preserves every unrelated sibling fact and keeps the aggregate in its unresolved state. -17. When both recovery ledgers are empty and no uncertain hot ownership remains, Browser Session reaches terminal `Ended`. It never transitions back to `Active` or restores ordinary browser command authority. +17. When both recovery ledgers are empty and no uncertain hot ownership remains, Browser Session reaches terminal `Ended`. It never transitions back to `Active` or restores ordinary browser command authority, and generic recovery operations are thereafter rejected before retained-adapter I/O. 18. The crate-private `dispatch_recovery_operation` remains the only bridge receiving `&mut P`; callers never receive raw adapter access. 19. Negative capability boundaries remain executable contracts through rustdoc `compile_fail` and repository tests. 20. `BrowserSession.contexts` contains only current live or uncertain ownership. Proven ordinary destruction removes a hot ownership record; failed destruction retains it as `Uncertain` plus exact `UnprovenDestruction { context, context_epoch }`. @@ -130,7 +135,7 @@ Rejected. Only proven destruction or proof-bearing recovery settlement may retir ## Consequences -Browser Session has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can issue purpose-bounded adapter commands and can consume independently qualified proof, but neither mechanism exposes the adapter or recreates ordinary Browser Session authority. +Browser Session has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can issue purpose-bounded adapter commands and can consume independently qualified proof while unresolved recovery state exists, but neither mechanism exposes the adapter or recreates ordinary Browser Session authority. Complete settlement closes both ordinary and recovery-command authority while retaining the terminal wrapper as inert state/evidence custody. Recovery settlement is deliberately revision-coarse. Settling any fact invalidates all fact handles issued under the prior revision, including unrelated siblings. This forces callers to reread the current evidence ledger after mutation and prevents index-shift replay at the cost of extra handle acquisition. Recovery fact counts are expected to be small, and correctness at this security boundary dominates preserving stale handles. @@ -140,7 +145,7 @@ The active #317 lineage keeps ownership generation, navigation witness generatio ## Failure and degraded behavior -A rejected `into_recovery` performs no I/O and returns the original bound owner. A failed recovery command preserves custody and evidence. A successful recovery command also preserves custody and evidence; it is not proof. +A rejected `into_recovery` performs no I/O and returns the original bound owner. A failed recovery command preserves custody and evidence. A successful recovery command also preserves custody and evidence; it is not proof. After proof-bearing settlement reaches terminal `Ended`, another recovery command returns `RecoveryClosed` before adapter I/O. A settlement with a foreign, stale, replayed, or out-of-range `RecoveryFact` fails before verifier I/O. A verifier rejection fails after proof I/O but before domain mutation. In both cases the ledgers remain unchanged. @@ -154,7 +159,7 @@ Dropping unresolved ordinary or recovery custody performs no browser I/O. Proces Recovery custody is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, adapter-selected handles, and recovery evidence cannot reconstruct ordinary command authority. Deterministic Browser Session policy is never delegated to an LLM. -The exact adapter remains owned and reachable only through purpose-bounded traits. `RecoveryFact` and `RecoverySettlementRequest` are opaque and non-caller-constructible. Session/incarnation/revision/exact-fact validation occurs before proof-verifier I/O, preventing foreign or replayed handles from turning protocol proof checking into an oracle or mutation channel. +The exact adapter remains owned and reachable only through purpose-bounded traits while unresolved recovery state exists. `RecoveryFact` and `RecoverySettlementRequest` are opaque and non-caller-constructible. Session/incarnation/revision/exact-fact validation occurs before proof-verifier I/O, preventing foreign or replayed handles from turning protocol proof checking into an oracle or mutation channel. Terminal settlement also removes the remaining generic recovery-operation route before another adapter call can occur. Alias-safe hot-state retirement prevents a rejected or partial create that reuses remote values from deleting a distinct previously accepted owner. Resource bounding therefore cannot convert unresolved ownership into an untracked boundary. @@ -177,6 +182,8 @@ This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, - ownership-clean transport loss cannot mint recovery custody - `crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs` - same-adapter operation; exact provenance; success/failure do not clear uncertainty +- `crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs` + - final exact-fact settlement reaches `Ended` and later generic recovery I/O returns `RecoveryClosed` before the retained adapter is called - `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` - single-fact settlement preserves siblings - replay and predecessor sibling handles fail stale before proof I/O @@ -199,7 +206,7 @@ These are active-PR contracts until the exact head passes repository contracts, Dependents must adopt this foundation by ordinary non-force restack after the parent exact head is verified. They must not copy Browser Session source, infer recovery authority from raw identifiers, or reconstruct a second adapter. -Rollback before protected-main adoption reverts the recovery-custody, settlement, hot-retirement, and navigation-authority slice together with its hostile fixtures and ADR. Selectively restoring raw adapter access, evidence-free recovery custody, ACK-as-proof, raw-index settlement, or authority resurrection is not a valid rollback. After protected-main adoption, rollback requires another policy-compliant change that preserves unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. +Rollback before protected-main adoption reverts the recovery-custody, settlement, terminal-recovery closure, hot-retirement, and navigation-authority slice together with its hostile fixtures and ADR. Selectively restoring raw adapter access, evidence-free recovery custody, ACK-as-proof, raw-index settlement, post-`Ended` recovery I/O, or authority resurrection is not a valid rollback. After protected-main adoption, rollback requires another policy-compliant change that preserves unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. ## Open follow-ups @@ -212,7 +219,7 @@ Rollback before protected-main adoption reverts the recovery-custody, settlement ## Supersession / reversal conditions -A successor may supersede this ADR only if it preserves at least: same-instance recovery custody; no ambient adapter escape; evidence-gated recovery capability; command-ACK/proof separation; opaque exact-fact settlement; pre-I/O foreign/stale rejection; sibling preservation; independent ledger retirement; alias-safe hot ownership; terminal non-resurrection; bounded command-authority state; monotonic ABA rejection; generation-bound navigation custody; presentation/lifecycle cleanup separation; and separation of durable history from command admission. +A successor may supersede this ADR only if it preserves at least: same-instance recovery custody; no ambient adapter escape; evidence-gated recovery capability; command-ACK/proof separation; opaque exact-fact settlement; pre-I/O foreign/stale rejection; sibling preservation; independent ledger retirement; alias-safe hot ownership; terminal non-resurrection; post-settlement recovery-command revocation; bounded command-authority state; monotonic ABA rejection; generation-bound navigation custody; presentation/lifecycle cleanup separation; and separation of durable history from command admission. Changing the recovery owner or persistence architecture alone does not justify weakening those guarantees. @@ -220,4 +227,4 @@ Changing the recovery owner or persistence architecture alone does not justify w Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ -Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html +Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file From f01b68fbb9201ca097a7dcd4cf8af873b4b8e99f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:04:02 +0900 Subject: [PATCH 139/632] docs(browser-session): record terminal recovery closure --- .../browser-session-recovery-settlement.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/doctoring/browser-session-recovery-settlement.md b/docs/doctoring/browser-session-recovery-settlement.md index 01c48863c..59b517e3e 100644 --- a/docs/doctoring/browser-session-recovery-settlement.md +++ b/docs/doctoring/browser-session-recovery-settlement.md @@ -8,6 +8,8 @@ Status: active-PR implementation evidence for #317. The source implementation no The settlement repair gives a protocol owner such as #316 a second-stage path: independently qualify browser evidence, submit it against one Browser Session-issued recovery fact, and retire only that exact uncertainty after the retained adapter verifies the proof. +A follow-on capability gap existed after complete settlement. The final exact fact could move the aggregate to terminal `Ended` while the caller still held `BoundBrowserSessionRecovery

`, and the generic recovery-operation method had no state gate. That left the exact retained adapter callable after the recovery purpose had ceased to exist. Terminal settlement now revokes that remaining command route: later generic recovery operations return `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. + ## Constraints Browser Session owns deterministic lifecycle state and exact fact consumption. WebDriver BiDi remains an adapter and evidence source; its navigation ids, user-context ids, event ordering and liveness rules do not become Browser Session policy authority. @@ -23,12 +25,15 @@ The implemented settlement boundary preserves these invariants: - identity-oriented `BrowserSessionRecoveryEvidence` and transaction-oriented `DisposableContextCreateRecoveryEvidence` remain separately addressable and separately retired; - settling an owned-context fact retires only the exact matching uncertain hot-ownership record; candidate/partial-create evidence never consumes an independently owned context merely because remote values alias; - partial settlement preserves every unrelated sibling fact; -- when both recovery ledgers and uncertain ownership are empty, the aggregate reaches terminal `Ended`; it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, or an ordinary lifecycle owner. +- when both recovery ledgers and uncertain ownership are empty, the aggregate reaches terminal `Ended`; it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, an ordinary lifecycle owner, or a usable generic recovery-command capability; +- a terminal recovery-operation attempt is rejected as `RecoveryClosed` before the retained adapter is called. ## Alternatives rejected Treating `RecoveryContextOperationPort` success as settlement is rejected because transport/protocol command completion is not independent proof of remote destruction or reconciliation. +Keeping `execute_recovery_context_operation` callable after complete settlement is rejected because the retained adapter would remain an ambient browser-I/O capability after its recovery purpose ended. The wrapper may remain as terminal state/evidence custody, but the command route must be inert. + Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling fact. A current-revision opaque handle makes stale replay fail closed. Allowing the adapter to delete Browser Session evidence directly is rejected because it moves domain ownership truth into an adapter and makes protocol data authoritative over policy state. @@ -43,7 +48,9 @@ Commit `738ec7d9a6635a8b4b0b9324026c2f0b433b9c77` introduced `crates/originweave 2. a fact issued by another Browser Session cannot reach the target session's proof verifier even when the caller possesses the opaque value; 3. uncertain create evidence carried in the identity and create-attempt ledgers requires two independent settlements rather than one broad erase. -The source repair adds the opaque fact/request/error contract and verifier port in `recovery.rs`, plus crate-private aggregate mutation hooks that retire exact evidence and exact matching uncertain ownership only after proof verification. It does not weaken the hostile fixture and does not reinterpret generic recovery-operation success as proof. +Commit `a99ea6bce6174fa98336f455d2ce2b1634f361b9` added `crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs` as a focused terminal-capability RED. It proves that a recovery operation is available while uncertainty remains, then settles the only recovery fact, requires terminal `Ended`, and requires the next generic recovery operation to fail as `RecoveryClosed` without a second adapter call. + +The source repair adds the opaque fact/request/error contract and verifier port in `recovery.rs`, plus crate-private aggregate mutation hooks that retire exact evidence and exact matching uncertain ownership only after proof verification. It also gates `execute_recovery_context_operation` to unresolved recovery states and returns `RecoveryClosed` after terminal settlement. It does not weaken the hostile fixtures and does not reinterpret generic recovery-operation success as proof. Repository execution remains the next gate. Until the current exact head actually runs and passes repository contracts, rustfmt, locked tests, strict Clippy, rustdoc and production coverage, this is source-level GREEN intent rather than executable GREEN evidence. @@ -58,10 +65,10 @@ Repository execution remains the next gate. Until the current exact head actuall 5. exact evidence retirement and, for owned-context evidence, exact uncertain ownership retirement; 6. monotonic revision advance and terminal `Ended` only when no uncertainty remains. -No adapter call occurs before the first three checks succeed. A failed verifier does not consume the fact. A successful mutation invalidates all previously issued fact handles before another settlement can be accepted. +No adapter call occurs before the first three checks succeed. A failed verifier does not consume the fact. A successful mutation invalidates all previously issued fact handles before another settlement can be accepted. Separately, `execute_recovery_context_operation` first verifies that custody is still `RecoveryRequired` or `TransportLost`; `Ended` returns `RecoveryClosed` before `dispatch_recovery_operation` can touch the adapter. ## Ownership handoff -#317 owns this generic settlement boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is #317 exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. +#317 owns this generic settlement and terminal-recovery-closure boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is #317 exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. -Any implementation that copies #316 protocol tuple state into Browser Session, consumes mutable sibling source, or treats command ACK as proof violates this boundary. +Any implementation that copies #316 protocol tuple state into Browser Session, consumes mutable sibling source, treats command ACK as proof, or preserves generic adapter I/O after terminal recovery violates this boundary. \ No newline at end of file From 71c3040e365ff158ed001afa7d0766737de9f549 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:05:10 +0900 Subject: [PATCH 140/632] test(browser-session): bind recovery operations to exact facts --- .../recovery_operation_exact_fact_scope.rs | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 crates/originweave-browser-session/tests/recovery_operation_exact_fact_scope.rs diff --git a/crates/originweave-browser-session/tests/recovery_operation_exact_fact_scope.rs b/crates/originweave-browser-session/tests/recovery_operation_exact_fact_scope.rs new file mode 100644 index 000000000..325080bdc --- /dev/null +++ b/crates/originweave-browser-session/tests/recovery_operation_exact_fact_scope.rs @@ -0,0 +1,231 @@ +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use originweave_browser_session::{ + BrowserSession, BrowserSessionError, BrowserSessionRecoveryEvidence, + DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, RecoveryContextOperationError, RecoveryContextOperationPort, + RecoveryContextOperationRequest, RecoverySettlementPort, RecoverySettlementRequest, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RecoveryOperation { + InspectSelectedFact, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FixtureError { + Rejected, +} + +struct ExactFactRecoveryPort { + create_results: VecDeque, + operation_calls: Rc>, + observed_recovery: Rc>>>, +} + +impl DisposableContextPort for ExactFactRecoveryPort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + self.create_results + .pop_front() + .ok_or(DisposableContextCreateError::CreateFailedClean) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Err(DisposableContextDestroyError::DestroyFailed) + } +} + +impl RecoveryContextOperationPort for ExactFactRecoveryPort { + type Operation = RecoveryOperation; + type Output = (); + type Error = FixtureError; + + fn execute_recovery_context_operation( + &mut self, + request: &RecoveryContextOperationRequest, + ) -> Result { + self.operation_calls.set(self.operation_calls.get() + 1); + self.observed_recovery + .borrow_mut() + .push(request.recovery_evidence().cloned()); + if request.operation() != &RecoveryOperation::InspectSelectedFact { + return Err(FixtureError::Rejected); + } + Ok(()) + } +} + +impl RecoverySettlementPort for ExactFactRecoveryPort { + type Proof = (); + type Error = FixtureError; + + fn verify_recovery_settlement( + &mut self, + _request: &RecoverySettlementRequest, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +fn isolation(value: &str) -> Result { + DisposableIsolationId::parse(value).map_err(|_| "fixture isolation must be representable") +} + +fn context(value: u64) -> Result { + BrowsingContextId::new(value).map_err(|_| "fixture browsing context must be valid") +} + +fn session(value: u64) -> Result { + BrowserSessionId::new(value).map_err(|_| "fixture session must be valid") +} + +fn handle(isolation_id: &str, context_id: u64) -> Result { + Ok(DisposableContextHandle::new( + isolation(isolation_id)?, + context(context_id)?, + )) +} + +#[test] +fn recovery_operation_is_scoped_to_one_current_exact_fact() -> Result<(), &'static str> { + let first = handle("operation-fact-a", 91_001)?; + let second = handle("operation-fact-b", 91_002)?; + let operation_calls = Rc::new(Cell::new(0)); + let observed_recovery = Rc::new(RefCell::new(Vec::new())); + let port = ExactFactRecoveryPort { + create_results: VecDeque::from([first.clone(), second.clone()]), + operation_calls: Rc::clone(&operation_calls), + observed_recovery: Rc::clone(&observed_recovery), + }; + let mut bound = BrowserSession::start(session(9_101)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let first_authority = bound + .create_disposable_context() + .map_err(|_| "first create must succeed")?; + let _second_authority = bound + .create_disposable_context() + .map_err(|_| "second create must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&first_authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + + let mut recovery = bound + .into_recovery() + .map_err(|_| "destroy uncertainty must enter recovery custody")?; + assert_eq!(recovery.recovery_evidence().len(), 2); + let first_fact = recovery.recovery_fact(0).ok_or("first fact must exist")?; + let stale_sibling = recovery.recovery_fact(1).ok_or("sibling fact must exist")?; + + recovery + .execute_recovery_context_operation(first_fact, RecoveryOperation::InspectSelectedFact) + .map_err(|_| "current exact fact must authorize its bounded recovery operation")?; + assert_eq!(operation_calls.get(), 1); + assert_eq!(observed_recovery.borrow().len(), 1); + assert_eq!( + observed_recovery.borrow()[0].as_ref(), + recovery.recovery_evidence().first(), + "adapter request must expose only the selected recovery fact, not sibling uncertainty" + ); + + recovery + .settle_recovery_fact(first_fact, ()) + .map_err(|_| "first exact fact must settle")?; + assert_eq!( + recovery.execute_recovery_context_operation( + stale_sibling, + RecoveryOperation::InspectSelectedFact, + ), + Err(RecoveryContextOperationError::StaleFact), + "a fact issued before ledger mutation must fail before adapter I/O" + ); + assert_eq!(operation_calls.get(), 1); + + let current_sibling = recovery + .recovery_fact(0) + .ok_or("remaining sibling must be re-issued at the current revision")?; + recovery + .execute_recovery_context_operation( + current_sibling, + RecoveryOperation::InspectSelectedFact, + ) + .map_err(|_| "re-issued current sibling must reach the retained adapter")?; + assert_eq!(operation_calls.get(), 2); + assert_eq!(observed_recovery.borrow().len(), 2); + assert_eq!( + observed_recovery.borrow()[1].as_ref(), + recovery.recovery_evidence().first(), + ); + Ok(()) +} + +#[test] +fn foreign_recovery_fact_is_rejected_before_operation_io() -> Result<(), &'static str> { + fn recovering_session( + session_id: u64, + context_id: u64, + isolation_id: &str, + operation_calls: Rc>, + ) -> Result, &'static str> + { + let owned = handle(isolation_id, context_id)?; + let port = ExactFactRecoveryPort { + create_results: VecDeque::from([owned]), + operation_calls, + observed_recovery: Rc::new(RefCell::new(Vec::new())), + }; + let mut bound = BrowserSession::start(session(session_id)?) + .map_err(|_| "browser session incarnation must be available")? + .bind_lifecycle_port(port); + let authority = bound + .create_disposable_context() + .map_err(|_| "fixture create must succeed")?; + assert_eq!( + bound.destroy_disposable_context(&authority), + Err(BrowserSessionError::ContextDestructionFailed) + ); + bound + .into_recovery() + .map_err(|_| "fixture must enter recovery custody") + } + + let first_calls = Rc::new(Cell::new(0)); + let second_calls = Rc::new(Cell::new(0)); + let first = recovering_session(9_201, 92_001, "operation-foreign-a", first_calls)?; + let mut second = recovering_session( + 9_202, + 92_002, + "operation-foreign-b", + Rc::clone(&second_calls), + )?; + let foreign_fact = first.recovery_fact(0).ok_or("foreign fact must exist")?; + + assert_eq!( + second.execute_recovery_context_operation( + foreign_fact, + RecoveryOperation::InspectSelectedFact, + ), + Err(RecoveryContextOperationError::AuthorityMismatch) + ); + assert_eq!(second_calls.get(), 0); + Ok(()) +} From a050312bd273a518e1af6087ec8f76bcf90fff9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:06:02 +0900 Subject: [PATCH 141/632] fix(browser-session): scope recovery operations to exact facts --- .../src/recovery.rs | 143 ++++++++++++------ 1 file changed, 93 insertions(+), 50 deletions(-) diff --git a/crates/originweave-browser-session/src/recovery.rs b/crates/originweave-browser-session/src/recovery.rs index b0ac2f8e4..0923cbd70 100644 --- a/crates/originweave-browser-session/src/recovery.rs +++ b/crates/originweave-browser-session/src/recovery.rs @@ -8,15 +8,16 @@ use crate::browser_session::{ /// Opaque recovery-custody request for one purpose-bounded adapter operation. /// /// Construction is private to [`BoundBrowserSessionRecovery`]. The request snapshots the exact -/// Browser Session identity, incarnation, unresolved lifecycle state, and both non-authorizing -/// recovery-evidence ledgers immediately before adapter I/O. None of these fields independently grant -/// ordinary creation, presentation mutation, or destruction authority. +/// Browser Session identity, incarnation, unresolved lifecycle state, and exactly one current +/// non-authorizing recovery fact immediately before adapter I/O. Sibling recovery facts are not +/// disclosed to the operation adapter. None of these fields independently grant ordinary creation, +/// presentation mutation, or destruction authority. pub struct RecoveryContextOperationRequest { browser_session: BrowserSessionId, incarnation: BrowserSessionIncarnation, state: BrowserSessionState, - recovery_evidence: Vec, - create_attempt_recovery_evidence: Vec, + recovery_evidence: Option, + create_attempt_recovery_evidence: Option, operation: O, } @@ -39,16 +40,18 @@ impl RecoveryContextOperationRequest { self.state } - /// Return the exact non-authorizing remote-ownership evidence captured before adapter I/O. + /// Return the selected identity-oriented recovery fact, when this operation targets that ledger. #[must_use] - pub fn recovery_evidence(&self) -> &[BrowserSessionRecoveryEvidence] { - &self.recovery_evidence + pub const fn recovery_evidence(&self) -> Option<&BrowserSessionRecoveryEvidence> { + self.recovery_evidence.as_ref() } - /// Return exact create-attempt recovery provenance captured before adapter I/O. + /// Return the selected create-attempt recovery fact, when this operation targets that ledger. #[must_use] - pub fn create_attempt_recovery_evidence(&self) -> &[DisposableContextCreateRecoveryEvidence] { - &self.create_attempt_recovery_evidence + pub const fn create_attempt_recovery_evidence( + &self, + ) -> Option<&DisposableContextCreateRecoveryEvidence> { + self.create_attempt_recovery_evidence.as_ref() } /// Return the adapter-defined purpose-bounded recovery operation. @@ -61,9 +64,10 @@ impl RecoveryContextOperationRequest { /// Adapter extension for purpose-bounded recovery operations on the exact consumed lifecycle port. /// /// Browser Session remains protocol-agnostic. Implementations own their operation, output, and error -/// vocabularies, while recovery custody supplies only immutable lifecycle provenance and routes the -/// request through the same concrete adapter instance consumed by [`BoundBrowserSession`]. A successful -/// adapter return is not itself proof that remote ownership was reconciled or destroyed. +/// vocabularies, while recovery custody supplies only the one current recovery fact selected by a +/// Browser Session-issued [`RecoveryFact`] and routes the request through the same concrete adapter +/// instance consumed by [`BoundBrowserSession`]. A successful adapter return is not itself proof that +/// remote ownership was reconciled or destroyed. pub trait RecoveryContextOperationPort: DisposableContextPort { /// Adapter-defined recovery operation vocabulary. type Operation; @@ -84,6 +88,10 @@ pub trait RecoveryContextOperationPort: DisposableContextPort { pub enum RecoveryContextOperationError { /// Recovery custody has already reached a terminal state with no unresolved command purpose. RecoveryClosed, + /// The selected fact belongs to another Browser Session or process-local incarnation. + AuthorityMismatch, + /// The selected fact was issued for an older ledger revision or no longer addresses a current fact. + StaleFact, /// The retained adapter attempted the recovery operation and returned its bounded failure. Adapter(E), } @@ -108,6 +116,12 @@ pub struct RecoveryFact { index: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RecoveryFactValidationError { + AuthorityMismatch, + StaleFact, +} + /// Opaque request used by the retained adapter to verify one independently qualified recovery proof. /// /// Browser Session chooses exactly one current recovery fact before adapter I/O. The request carries @@ -200,8 +214,8 @@ pub enum RecoverySettlementError { /// This wrapper is obtained only by consuming a bound session that has already entered /// [`BrowserSessionState::RecoveryRequired`] or entered [`BrowserSessionState::TransportLost`] while /// retaining unresolved remote-ownership evidence. It exposes lifecycle state, exact non-authorizing -/// recovery evidence, and purpose-bounded recovery-operation and recovery-settlement paths through the -/// retained adapter. It deliberately provides none of the ordinary create, presentation-authority, +/// recovery evidence, and exact-fact-bounded recovery-operation and recovery-settlement paths through +/// the retained adapter. It deliberately provides none of the ordinary create, presentation-authority, /// epoch-advance, destroy, authorized-operation, or normal-finish methods, and it does not expose the /// inner [`BoundBrowserSession`] or concrete port. /// @@ -361,18 +375,57 @@ impl BoundBrowserSessionRecovery

{ index, }) } + + fn select_recovery_fact( + &self, + fact: RecoveryFact, + ) -> Result< + ( + Option, + Option, + ), + RecoveryFactValidationError, + > { + let session = self.bound.browser_session(); + if fact.browser_session != session.id() || fact.incarnation != session.incarnation() { + return Err(RecoveryFactValidationError::AuthorityMismatch); + } + if fact.revision != self.revision { + return Err(RecoveryFactValidationError::StaleFact); + } + match fact.ledger { + RecoveryFactLedger::Recovery => { + let evidence = self + .recovery_evidence() + .get(fact.index) + .cloned() + .ok_or(RecoveryFactValidationError::StaleFact)?; + Ok((Some(evidence), None)) + } + RecoveryFactLedger::CreateAttempt => { + let evidence = self + .create_attempt_recovery_evidence() + .get(fact.index) + .cloned() + .ok_or(RecoveryFactValidationError::StaleFact)?; + Ok((None, Some(evidence))) + } + } + } } impl BoundBrowserSessionRecovery

{ /// Execute one purpose-bounded recovery operation through the exact retained lifecycle adapter. /// - /// The request snapshots the unresolved aggregate state and both recovery-evidence ledgers before - /// adapter I/O. Adapter success or failure leaves Browser Session state and evidence unchanged; - /// protocol-specific code must provide separate, reviewed reconciliation proof before uncertainty - /// can be resolved. Once exact-fact settlement closes recovery to `Ended`, later operations fail - /// before retained-adapter I/O. + /// A current Browser Session-issued recovery fact must be supplied. Session/incarnation, current + /// ledger revision, and current fact address are validated before adapter I/O. The adapter receives + /// only that selected fact rather than the sibling recovery ledgers. Adapter success or failure + /// leaves Browser Session state and evidence unchanged; protocol-specific code must provide a + /// separate reviewed reconciliation proof before uncertainty can be retired. Once exact-fact + /// settlement closes recovery to `Ended`, later operations fail before retained-adapter I/O. pub fn execute_recovery_context_operation( &mut self, + fact: RecoveryFact, operation: P::Operation, ) -> Result> { if !matches!( @@ -381,15 +434,21 @@ impl BoundBrowserSessionRecovery

{ ) { return Err(RecoveryContextOperationError::RecoveryClosed); } + let (recovery_evidence, create_attempt_recovery_evidence) = self + .select_recovery_fact(fact) + .map_err(|error| match error { + RecoveryFactValidationError::AuthorityMismatch => { + RecoveryContextOperationError::AuthorityMismatch + } + RecoveryFactValidationError::StaleFact => RecoveryContextOperationError::StaleFact, + })?; self.bound.dispatch_recovery_operation(|session, port| { let request = RecoveryContextOperationRequest { browser_session: session.id(), incarnation: session.incarnation(), state: session.state(), - recovery_evidence: session.recovery_evidence().to_vec(), - create_attempt_recovery_evidence: session - .create_attempt_recovery_evidence() - .to_vec(), + recovery_evidence, + create_attempt_recovery_evidence, operation, }; port.execute_recovery_context_operation(&request) @@ -410,35 +469,19 @@ impl BoundBrowserSessionRecovery

{ fact: RecoveryFact, proof: P::Proof, ) -> Result<(), RecoverySettlementError> { - let session = self.bound.browser_session(); - if fact.browser_session != session.id() || fact.incarnation != session.incarnation() { - return Err(RecoverySettlementError::AuthorityMismatch); - } - if fact.revision != self.revision { - return Err(RecoverySettlementError::StaleFact); - } + let (recovery_evidence, create_attempt_recovery_evidence) = self + .select_recovery_fact(fact) + .map_err(|error| match error { + RecoveryFactValidationError::AuthorityMismatch => { + RecoverySettlementError::AuthorityMismatch + } + RecoveryFactValidationError::StaleFact => RecoverySettlementError::StaleFact, + })?; let next_revision = self .revision .checked_add(1) .ok_or(RecoverySettlementError::RevisionExhausted)?; - let (recovery_evidence, create_attempt_recovery_evidence) = match fact.ledger { - RecoveryFactLedger::Recovery => { - let evidence = self - .recovery_evidence() - .get(fact.index) - .cloned() - .ok_or(RecoverySettlementError::StaleFact)?; - (Some(evidence), None) - } - RecoveryFactLedger::CreateAttempt => { - let evidence = self - .create_attempt_recovery_evidence() - .get(fact.index) - .cloned() - .ok_or(RecoverySettlementError::StaleFact)?; - (None, Some(evidence)) - } - }; + let session = self.bound.browser_session(); let request = RecoverySettlementRequest { browser_session: session.id(), incarnation: session.incarnation(), From 29ff24e0adc8a8a2a87e0fd92ba22aa2d2975241 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:06:38 +0900 Subject: [PATCH 142/632] test(browser-session): consume exact recovery operation facts --- .../tests/recovery_same_adapter_operation.rs | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs index 3f6407a1b..f23c697a3 100644 --- a/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs +++ b/crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs @@ -26,8 +26,8 @@ struct RecoveryObservation { browser_session: BrowserSessionId, incarnation: u64, state: BrowserSessionState, - recovery_evidence: Vec, - create_attempt_recovery_evidence: Vec, + recovery_evidence: Option, + create_attempt_recovery_evidence: Option, operation: RecoveryOperation, } @@ -87,10 +87,10 @@ impl RecoveryContextOperationPort for RecoveryPort { browser_session: request.browser_session(), incarnation: request.incarnation().value(), state: request.state(), - recovery_evidence: request.recovery_evidence().to_vec(), + recovery_evidence: request.recovery_evidence().cloned(), create_attempt_recovery_evidence: request .create_attempt_recovery_evidence() - .to_vec(), + .cloned(), operation: *request.operation(), }); if self.fail_recovery.get() { @@ -159,9 +159,15 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter let mut recovery = bound .into_recovery() .map_err(|_| "RecoveryRequired must enter recovery custody")?; + let fact = recovery + .recovery_fact(0) + .ok_or("exact recovery fact must be addressable")?; assert_eq!( - recovery.execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence), + recovery.execute_recovery_context_operation( + fact, + RecoveryOperation::ReconcileExactEvidence, + ), Err(RecoveryContextOperationError::Adapter( RecoveryOperationFailure::BackendUnavailable )) @@ -180,11 +186,12 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter assert_eq!(first[0].browser_session, expected_session); assert_eq!(first[0].incarnation, expected_incarnation.value()); assert_eq!(first[0].state, BrowserSessionState::RecoveryRequired); - assert_eq!(first[0].recovery_evidence, expected_recovery_evidence); assert_eq!( - first[0].create_attempt_recovery_evidence, - expected_create_attempt_recovery_evidence + first[0].recovery_evidence.as_ref(), + expected_recovery_evidence.first(), + "request must expose only the selected exact recovery fact" ); + assert_eq!(first[0].create_attempt_recovery_evidence, None); assert_eq!( first[0].operation, RecoveryOperation::ReconcileExactEvidence @@ -193,7 +200,7 @@ fn recovery_custody_routes_only_purpose_bounded_io_to_the_exact_consumed_adapter fail_recovery.set(false); recovery - .execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence) + .execute_recovery_context_operation(fact, RecoveryOperation::ReconcileExactEvidence) .map_err(|_| "purpose-bounded recovery operation must reach the retained adapter")?; assert_eq!(recovery_calls.get(), 2); assert_eq!( @@ -256,8 +263,14 @@ fn recovery_dispatch_preserves_non_empty_create_attempt_provenance_on_failure_an let mut recovery = bound .into_recovery() .map_err(|_| "uncertain create must enter recovery custody")?; + let fact = recovery + .create_attempt_recovery_fact(0) + .ok_or("exact create-attempt recovery fact must be addressable")?; assert_eq!( - recovery.execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence), + recovery.execute_recovery_context_operation( + fact, + RecoveryOperation::ReconcileExactEvidence, + ), Err(RecoveryContextOperationError::Adapter( RecoveryOperationFailure::BackendUnavailable )) @@ -275,16 +288,17 @@ fn recovery_dispatch_preserves_non_empty_create_attempt_provenance_on_failure_an assert_eq!(first[0].browser_session, expected_session); assert_eq!(first[0].incarnation, expected_incarnation.value()); assert_eq!(first[0].state, BrowserSessionState::RecoveryRequired); - assert_eq!(first[0].recovery_evidence, expected_recovery_evidence); + assert_eq!(first[0].recovery_evidence, None); assert_eq!( - first[0].create_attempt_recovery_evidence, - expected_create_attempt_recovery_evidence + first[0].create_attempt_recovery_evidence.as_ref(), + expected_create_attempt_recovery_evidence.first(), + "request must expose only the selected exact create-attempt fact" ); drop(first); fail_recovery.set(false); recovery - .execute_recovery_context_operation(RecoveryOperation::ReconcileExactEvidence) + .execute_recovery_context_operation(fact, RecoveryOperation::ReconcileExactEvidence) .map_err(|_| "recovery success must use the retained adapter")?; assert_eq!(recovery_calls.get(), 2); assert_eq!(recovery.state(), BrowserSessionState::RecoveryRequired); From 4eed22fc922cafcee0dd8b483747579bed5f9dbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:06:53 +0900 Subject: [PATCH 143/632] test(browser-session): keep terminal closure fact-scoped --- .../tests/recovery_operation_terminal_closure.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs b/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs index 6420cecde..dcf5168a8 100644 --- a/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs +++ b/crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs @@ -99,21 +99,21 @@ fn terminal_recovery_settlement_revokes_generic_recovery_io() -> Result<(), &'st let mut recovery = bound .into_recovery() .map_err(|_| "unproven destruction must enter recovery custody")?; + let fact = recovery + .recovery_fact(0) + .ok_or("unproven destruction recovery fact must exist")?; recovery - .execute_recovery_context_operation(()) + .execute_recovery_context_operation(fact, ()) .map_err(|_| "recovery operation must be available while uncertainty remains")?; assert_eq!(recovery_operation_calls.get(), 1); - let fact = recovery - .recovery_fact(0) - .ok_or("unproven destruction recovery fact must exist")?; recovery .settle_recovery_fact(fact, ()) .map_err(|_| "independently verified recovery fact must settle")?; assert_eq!(recovery.state(), BrowserSessionState::Ended); assert_eq!( - recovery.execute_recovery_context_operation(()), + recovery.execute_recovery_context_operation(fact, ()), Err(RecoveryContextOperationError::RecoveryClosed), "terminal recovery custody must not retain a generic adapter-I/O capability" ); From c9e0b89842b3898c4610754c9dba31cc0a52fedc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:07:47 +0900 Subject: [PATCH 144/632] test(browser-session): contract exact-fact recovery operation scope --- ...ser_session_recovery_operation_contract.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_recovery_operation_contract.py b/tests/test_browser_session_recovery_operation_contract.py index 8cba2cd36..527cf63b2 100644 --- a/tests/test_browser_session_recovery_operation_contract.py +++ b/tests/test_browser_session_recovery_operation_contract.py @@ -146,7 +146,7 @@ def skip_non_code(position: int) -> int: class BrowserSessionRecoveryOperationContractTests(unittest.TestCase): - """Keep recovery I/O purpose-bounded to the exact consumed adapter.""" + """Keep recovery I/O purpose-bounded to one exact fact on the consumed adapter.""" def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: """Do not reopen raw adapter access to bridge recovery custody.""" @@ -172,6 +172,18 @@ def test_recovery_dispatch_stays_inside_native_owner_module(self) -> None: "pub struct RecoveryContextOperationRequest {", 1 )[1].split("\n}", 1)[0] self.assertNotRegex(request_struct, r"(?m)^\s*pub(?:\([^)]*\))?\s+") + self.assertIn("Option", request_struct) + self.assertIn("Option", request_struct) + self.assertNotIn("Vec", request_struct) + self.assertNotIn("Vec", request_struct) + + operation_impl = recovery_source.split( + "impl BoundBrowserSessionRecovery

", 1 + )[1].split("impl", 1)[0] + self.assertIn("fact: RecoveryFact", operation_impl) + self.assertIn("select_recovery_fact(fact)", operation_impl) + self.assertIn("RecoveryContextOperationError::AuthorityMismatch", operation_impl) + self.assertIn("RecoveryContextOperationError::StaleFact", operation_impl) request_impl_headers = [ header @@ -307,6 +319,8 @@ def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> Non for token in ( "RecoveryContextOperationPort", "execute_recovery_context_operation", + "recovery.recovery_fact(0)", + "recovery.create_attempt_recovery_fact(0)", "request.browser_session()", "request.incarnation()", "request.state()", @@ -315,12 +329,30 @@ def test_hostile_fixture_preserves_uncertainty_after_adapter_result(self) -> Non "RecoveryContextOperationError::Adapter", "expected_recovery_evidence", "expected_create_attempt_recovery_evidence", + "request must expose only the selected exact recovery fact", + "request must expose only the selected exact create-attempt fact", "generic recovery adapter success is not itself destruction or reconciliation proof", "recovery operation dispatch must not erase unresolved ownership evidence", "recovery operation dispatch must not erase create-attempt provenance", ): self.assertIn(token, hostile) + def test_hostile_fixture_rejects_foreign_and_stale_operation_facts_before_io(self) -> None: + """One fact may authorize only its current exact recovery operation scope.""" + + hostile = (CRATE / "tests/recovery_operation_exact_fact_scope.rs").read_text( + encoding="utf-8" + ) + for token in ( + "execute_recovery_context_operation(first_fact", + "adapter request must expose only the selected recovery fact, not sibling uncertainty", + "RecoveryContextOperationError::StaleFact", + "RecoveryContextOperationError::AuthorityMismatch", + "a fact issued before ledger mutation must fail before adapter I/O", + "foreign_recovery_fact_is_rejected_before_operation_io", + ): + self.assertIn(token, hostile) + def test_architecture_docs_describe_current_recovery_surface(self) -> None: """ADR, traceability, and UML must not describe the pre-operation wrapper.""" @@ -338,6 +370,7 @@ def test_architecture_docs_describe_current_recovery_surface(self) -> None: self.assertIn("RecoveryContextOperationPort", document) self.assertIn("RecoveryContextOperationRequest", document) self.assertIn("same", document.lower()) + self.assertIn("RecoveryFact", document) self.assertIn("pub(crate)", adr) self.assertIn("pub(crate)", trace) self.assertIn("success/failure does not clear Browser Session uncertainty", uml) From ec1a931c32137a962950a6fbcb7be4efe6a98b19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:08:36 +0900 Subject: [PATCH 145/632] docs(browser-session): scope recovery UML to exact facts --- .../browser-session-lifecycle-authority.md | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/uml/browser-session-lifecycle-authority.md b/docs/uml/browser-session-lifecycle-authority.md index 11a6a2213..3f2603723 100644 --- a/docs/uml/browser-session-lifecycle-authority.md +++ b/docs/uml/browser-session-lifecycle-authority.md @@ -79,7 +79,7 @@ sequenceDiagram `BoundBrowserSession` is a linear lifecycle-port binding. It consumes one concrete port, exposes no public raw `&P`, and accepts no replacement port on lifecycle methods. `AuthorizedContextOperationRequest` is privately constructed after exact `PresentationMutationAuthority` validation. A raw browser id, adapter-selected value, diagnostic view, or second adapter cannot mint Browser Session authority. -## RecoveryCustody and exact same-adapter recovery +## Recovery custody and exact same-adapter recovery ```mermaid stateDiagram-v2 @@ -91,10 +91,11 @@ stateDiagram-v2 Active --> RecoveryRequired: DisposableContextDestroyError / cleanup unproven Active --> TransportLost: transport_lost / TransportLossOwnedHandle RecoveryRequired --> RecoveryCustody: into_recovery(self) - TransportLost --> RecoveryCustody: into_recovery(self) - RecoveryCustody --> RecoveryCustody: execute_recovery_context_operation(operation) + TransportLost --> RecoveryCustody: into_recovery(self) only with unresolved evidence + RecoveryCustody --> RecoveryCustody: execute_recovery_context_operation(RecoveryFact, operation) + RecoveryCustody --> RecoveryCustody: settle one exact RecoveryFact / revision++ + RecoveryCustody --> Ended: final exact fact + hot ownership retired Active --> Ended: finish() after proven cleanup - RecoveryCustody --> [*]: persist or reconcile externally note right of RecoveryRequired Exact BrowserSessionRecoveryEvidence and @@ -105,8 +106,9 @@ stateDiagram-v2 note right of RecoveryCustody BoundBrowserSessionRecovery

retains the - exact consumed adapter. It exposes state, - evidence, and only RecoveryContextOperationPort. + exact consumed adapter. Recovery commands require + one current Browser Session-issued RecoveryFact; + the adapter sees only that selected fact. Raw P and ordinary Browser Session authority remain inaccessible. end note @@ -126,18 +128,44 @@ sequenceDiagram C->>BS: into_recovery(self) BS-->>R: move exact BoundBrowserSession + same adapter; no I/O - C->>R: execute_recovery_context_operation(operation) - R->>R: snapshot session/incarnation/state + both evidence ledgers + C->>R: recovery_fact(index) or create_attempt_recovery_fact(index) + R-->>C: opaque RecoveryFact(session, incarnation, ledger, index, revision) + C->>R: execute_recovery_context_operation(fact, operation) + R->>R: validate state + session/incarnation + revision + exact current fact + alt fact foreign, stale, or no longer current + R-->>C: AuthorityMismatch or StaleFact + Note over R,P: adapter I/O = 0 + else current exact fact + R->>BS: crate-private dispatch_recovery_operation + BS->>P: RecoveryContextOperationRequest(selected fact + operation) + Note over P: sibling recovery facts are not disclosed + P->>B: adapter-owned purpose-bounded recovery command + B-->>P: result + P-->>R: Output or RecoveryContextOperationError::Adapter + Note over R,BS: success/failure does not clear Browser Session uncertainty + end + + C->>R: settle_recovery_fact(fact, independently qualified proof) + R->>R: revalidate session/incarnation/revision/exact fact R->>BS: crate-private dispatch_recovery_operation - BS->>P: RecoveryContextOperationRequest(operation + provenance) - P->>B: adapter-owned purpose-bounded recovery command - B-->>P: result - P-->>R: Output or RecoveryContextOperationError::Adapter - Note over R,BS: success/failure does not clear Browser Session uncertainty + BS->>P: RecoverySettlementRequest(selected fact + proof) + P->>B: verify protocol-specific proof / post-condition evidence + alt proof rejected + P-->>R: RecoverySettlementError::Adapter + Note over R,BS: no domain fact is retired + else proof accepted + R->>BS: retire exactly selected fact + R->>R: recovery revision++ + Note over R: every previously issued RecoveryFact becomes stale + alt no recovery facts or uncertain hot ownership remain + R->>BS: terminal Ended + end + end + Note over R,P: no raw P, no generic caller callback, no ordinary create/destroy/presentation authority ``` -Recovery custody is narrower than protocol reconciliation. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery-command semantics. `RecoveryContextOperationPort` only provides the same-consumed-adapter conduit. Adapter success is not destruction proof. +Recovery custody is narrower than protocol reconciliation. #316 remains responsible for WebDriver BiDi pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery-command/proof semantics. `RecoveryContextOperationPort` provides the same-consumed-adapter conduit only for one current `RecoveryFact`; adapter success is not destruction proof. `RecoverySettlementPort` independently qualifies proof before Browser Session retires that exact fact. Dropping unresolved ordinary or recovery custody performs no browser I/O. `abandoned_bound_session_count()` is a process-local operability signal, not durable exact-handle storage or proof of cleanup. @@ -160,10 +188,10 @@ stateDiagram-v2 Navigation admission is bound to exact `BrowserSessionIncarnation`, `BrowsingContextId`, and current `BrowserContextEpoch`. The opaque navigation witness, not raw WebDriver BiDi navigation ids, controls terminal assignment. A navigation-invalidated `PresentationMutationAuthority` cannot authorize presentation mutation or authority-based cleanup. The exact bound lifecycle owner can still destroy its owned context without reopening presentation authority. -## Same-raw-identity and Sequential ABA hostile cases +## Same-raw-identity and sequential ABA hostile cases A single aggregate may create → prove destroy → recreate the same raw isolation/browsing-context values for **258 ownership generations**. Hot command-authority state remains bounded to live/uncertain ownership. The predecessor authority fails as `ContextNotOwned` immediately after destruction and as `AuthorityMismatch` after same-raw-id recreation because the epoch is monotonic. Across aggregate restart/recreation, `BrowserSessionIncarnation` prevents a retained authority from aggregate A from becoming valid in aggregate B even when raw `BrowserSessionId`, isolation, browsing-context id, and local epoch numerically alias. -`RecoveryRequired` and `TransportLost` remain closed to ordinary lifecycle and presentation authority. `into_recovery(self)` is a one-way custody transfer, not command-authority resurrection; later protocol reconciliation stays purpose-bounded and must not infer cleanup authority from raw identifiers or treat command ACK as proof of destruction. \ No newline at end of file +`RecoveryRequired` and `TransportLost` remain closed to ordinary lifecycle and presentation authority. `into_recovery(self)` is a one-way custody transfer, not command-authority resurrection. A recovery command additionally requires a current opaque `RecoveryFact`; command ACK never settles that fact. Proof-bearing settlement can only retire the selected fact after independent adapter verification, and complete settlement reaches terminal `Ended` rather than reopening ordinary authority. From 3cb79526a4233e1f0b753bf97f38492cd8bf7dbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:09:08 +0900 Subject: [PATCH 146/632] docs(browser-session): trace exact-fact recovery command authority --- .../browser-session-lifecycle-authority.md | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index f4e112f3b..7b1ce46f4 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -8,7 +8,7 @@ ## Problem and invariant -Browser-session, user-context/isolation, browsing-context, adapter-selected identifiers, navigation ids, and recovery evidence are addresses or evidence. They are not proof that the current Browser Session aggregate owns lifecycle or presentation mutation, and they are not self-authenticating recovery settlement authority. +Browser-session, user-context/isolation, browsing-context, adapter-selected identifiers, navigation ids, recovery evidence, and adapter command results are addresses or evidence. None is self-authenticating lifecycle, presentation, navigation, or recovery authority. The active implementation establishes this chain: @@ -27,37 +27,32 @@ validated BrowserSessionId → aggregate records accepted exact handle + epoch → opaque PresentationMutationAuthority(session, incarnation, isolation, context, epoch) → exact authority validation before ordinary lifecycle or purpose-bounded adapter I/O -→ lifecycle destruction uses private DisposableContextDestroyRequest(handle, validated epoch) -→ presentation work uses private AuthorizedContextOperationRequest(handle, validated epoch, operation) -→ exact consumed adapter only → failed/unproven destruction retains exact handle + epoch and enters RecoveryRequired → RecoveryRequired|evidence-bearing TransportLost may consume the same bound owner into BoundBrowserSessionRecovery

-→ recovery command path privately builds RecoveryContextOperationRequest and uses the same adapter -→ adapter command success/failure leaves unresolved Browser Session state/evidence unchanged -→ recovery custody issues opaque current-revision RecoveryFact values for exact recovery facts -→ caller supplies independently qualified adapter proof with one RecoveryFact -→ settle_recovery_fact validates session/incarnation/revision/exact fact before proof I/O +→ recovery custody issues opaque current-revision RecoveryFact values +→ recovery command requires one current RecoveryFact + adapter-defined operation +→ session/incarnation/revision/exact-fact validation occurs before recovery adapter I/O +→ RecoveryContextOperationRequest carries only the selected fact, never sibling recovery ledgers +→ adapter command success/failure leaves Browser Session uncertainty unchanged +→ caller supplies independently qualified proof with one current RecoveryFact +→ settle_recovery_fact revalidates session/incarnation/revision/exact fact before proof I/O → exact retained adapter verifies RecoverySettlementRequest through RecoverySettlementPort -→ verifier failure leaves both ledgers unchanged → verifier success retires exactly one fact and advances the recovery revision → predecessor/replayed/sibling handles issued under the old revision become stale → exact uncertain owned context is removed only for ownership evidence that names that same handle -→ all facts + uncertain hot ownership gone → terminal Ended; ordinary authority is never restored +→ all facts + uncertain hot ownership gone → terminal Ended; ordinary and recovery-command authority are closed → proven ordinary destruction removes live hot ownership; failed destruction retains Uncertain ownership -→ BoundBrowserSession::finish() validates normal completion without consuming the owner on rejection ``` -`BoundBrowserSession` is the linear lifecycle-port binding. Public create/destroy methods accept no arbitrary port argument, and there is no public raw port accessor. `BoundBrowserSessionRecovery` preserves the same adapter but is a reduced-capability owner, not an alternate ordinary lifecycle path. - -`DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, `AuthorizedContextOperationRequest`, `RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest

` all have private construction fields. Epochs, revisions, protocol ids, and evidence are correlation/provenance; none is standalone bearer authority. +`BoundBrowserSession` is the linear lifecycle-port binding. `BoundBrowserSessionRecovery` preserves that same adapter as a reduced-capability, one-way owner. `DisposableContextCreateRequest`, `DisposableContextCreateCompletion`, `DisposableContextDestroyRequest`, `AuthorizedContextOperationRequest`, `RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest

` have private construction fields. Epochs, revisions, protocol ids, and evidence are correlation/provenance rather than standalone bearer authority. ## Transactional remote creation -A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not make that result authorizing until Browser Session accepts that exact attempt through `DisposableContextCreateCompletion`. +A protocol adapter may stage a successful remote create result as pending when it receives the create request. It must not promote that result into an authorizing binding until Browser Session accepts the exact attempt through `DisposableContextCreateCompletion`. -`DisposableContextCreateRecoveryEvidence` preserves the transaction dimension that raw handle evidence cannot represent. `FailedUncertain` stores the exact aggregate-issued attempt epoch even without a complete handle; `DuplicateCandidate` binds an aliased candidate to its exact rejected attempt; `CompletionUnsettled` binds exact attempt, disposition, and returned handle when settlement cannot be proven. +`DisposableContextCreateRecoveryEvidence` preserves transaction identity that raw handle evidence cannot represent. `FailedUncertain` stores the aggregate-issued attempt epoch even without a complete handle; `DuplicateCandidate` binds an aliased candidate to its rejected attempt; `CompletionUnsettled` binds attempt, disposition, and returned handle when completion settlement cannot be proven. -Identity-oriented `BrowserSessionRecoveryEvidence` is independently useful for ownership reconciliation. The two ledgers are deliberately separate. A later or rejected same-valued candidate cannot erase a previously accepted ownership fact merely because remote values alias. +Identity-oriented `BrowserSessionRecoveryEvidence` remains independently useful for ownership reconciliation. The two ledgers are separate: a later or rejected same-valued candidate cannot erase a previously accepted ownership fact merely because remote values alias. Protocol pending/accepted/quarantined tuple storage remains #314/#316 responsibility. Browser Session owns attempt identity, domain accept/reject, current command authority, non-authorizing recovery facts, and deterministic exact-fact retirement after proof verification. @@ -67,36 +62,40 @@ Protocol pending/accepted/quarantined tuple storage remains #314/#316 responsibi Stale or foreign authority fails before adapter I/O as `AuthorizedContextOperationError::BrowserSession`. Adapter failures remain typed as `AuthorizedContextOperationError::Adapter`. No raw `P`, second adapter, or unrestricted callback is exposed. -## Recovery-only custody and command path +## Recovery-only custody and exact-fact command path `BoundBrowserSession::into_recovery(self)` is one-way. It succeeds from `RecoveryRequired`, or from `TransportLost` only if exact unresolved recovery/create-attempt evidence remains. Ownership-clean transport loss cannot mint recovery capability. -Recovery custody exposes `state()`, both evidence ledgers, and—when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(operation)`. It exposes neither raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create/presentation/navigation/cleanup authority, nor normal finish. +Recovery custody exposes `state()`, read-only recovery ledgers, current-fact issuance, and—when `P: RecoveryContextOperationPort`—`execute_recovery_context_operation(fact, operation)`. It exposes neither raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create/presentation/navigation/cleanup authority, nor normal finish. + +Before recovery-command I/O, Browser Session validates that the supplied `RecoveryFact` belongs to the exact session/incarnation, was issued at the current recovery revision, and still addresses the current ledger/index fact. Foreign facts fail as `RecoveryContextOperationError::AuthorityMismatch`; stale, replayed, shifted, or out-of-range facts fail as `RecoveryContextOperationError::StaleFact`. Both fail before the adapter is invoked. -`RecoveryContextOperationRequest` snapshots exact Browser Session id, incarnation, unresolved state, both evidence ledgers, and the adapter-defined operation immediately before I/O. `BoundBrowserSession::dispatch_recovery_operation` is `pub(crate)`. `RecoveryContextOperationError::Adapter` preserves adapter failure. Both success and failure leave Browser Session uncertainty unchanged: command ACK is not destruction or reconciliation proof. +`RecoveryContextOperationRequest` snapshots the Browser Session id, incarnation, unresolved state, the selected identity-oriented **or** create-attempt recovery fact, and the adapter-defined operation. Its evidence fields are `Option<...>`, not full vectors. Sibling facts are intentionally withheld from the adapter command path. `BoundBrowserSession::dispatch_recovery_operation` remains `pub(crate)`. + +Adapter success or `RecoveryContextOperationError::Adapter(E)` leaves all Browser Session recovery state unchanged. The same current fact may be retried until a successful settlement changes the recovery revision. Command ACK is not destruction or reconciliation proof. After complete settlement reaches `Ended`, the command path returns `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. ## Proof-bearing exact-fact recovery settlement -Recovery completion is a separate path. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque `RecoveryFact` values only for currently addressable facts. Each handle binds exact Browser Session id, process-local incarnation, ledger kind, index, and the current monotonic recovery revision. +Recovery completion is separate from recovery command execution. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque `RecoveryFact` values only for currently addressable facts. Each handle binds Browser Session id, process-local incarnation, ledger kind, index, and current monotonic recovery revision. -`settle_recovery_fact(fact, proof)` validates in this order: +`settle_recovery_fact(fact, proof)` validates: 1. exact Browser Session id and incarnation; -2. current recovery revision; -3. exact current ledger/index fact and next-revision capacity; +2. current recovery revision and exact current ledger/index fact; +3. next-revision capacity; 4. retained-adapter proof verification through `RecoverySettlementPort::verify_recovery_settlement(RecoverySettlementRequest)`; 5. exact one-fact retirement; 6. monotonic revision advance. -`AuthorityMismatch`, `StaleFact`, and `RevisionExhausted` are pre-I/O failures. `RecoverySettlementError::Adapter(E)` is post-verifier/pre-mutation. A failed proof does not consume evidence. A successful settlement invalidates every fact handle issued under the previous revision, including sibling handles, so the caller must reread current custody after mutation. +`AuthorityMismatch`, `StaleFact`, and `RevisionExhausted` are pre-I/O failures. `RecoverySettlementError::Adapter(E)` is post-verifier/pre-mutation. A failed proof consumes nothing. A successful settlement invalidates every fact handle issued under the previous revision, including unrelated sibling handles, so callers reread custody after mutation. The two recovery ledgers remain independently consumable. Retiring `BrowserSessionRecoveryEvidence` never implicitly erases matching `DisposableContextCreateRecoveryEvidence` and vice versa. Ownership retirement is alias-safe. Only `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, or `TransportLossOwnedHandle` may remove an exact matching `Uncertain` hot ownership record. `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnsettledAdapterHandle` retire evidence only; a rejected candidate that reuses remote values cannot delete a distinct accepted owner. -When both ledgers are empty and no uncertain hot ownership remains, Browser Session reaches `Ended`. Recovery custody never recreates `Active`, create authority, `PresentationMutationAuthority`, navigation authority, or ordinary cleanup authority. +When both ledgers are empty and no uncertain hot ownership remains, Browser Session reaches `Ended`. Recovery custody never recreates `Active`, create authority, `PresentationMutationAuthority`, navigation authority, ordinary cleanup authority, or generic recovery-command authority. -#316 remains canonical owner of WebDriver BiDi proof qualification, pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery commands. A protocol event is evidence, not Browser Session policy authority. #316 maps independently qualified evidence into `RecoverySettlementPort::Proof`; Browser Session only validates and consumes its own exact fact. +#316 remains canonical owner of WebDriver BiDi proof qualification, pending/accepted/quarantined tuple truth, remote liveness, event correlation, replay qualification, and concrete recovery commands. A protocol event is evidence, not Browser Session policy authority. #316 maps independently qualified evidence into `RecoverySettlementPort::Proof`; Browser Session validates and consumes only its own exact fact. ## Lossless evidence and bounded hot ownership @@ -104,13 +103,13 @@ When both ledgers are empty and no uncertain hot ownership remains, Browser Sess Hot command-authority state contains only live or uncertain ownership. Proven ordinary destruction removes the current hot record. Failed destroy retains the exact record as `Uncertain` plus `UnprovenDestruction { context, context_epoch }`. -The 258-generation hostile fixture proves that the same raw isolation/context values may be reused after proven destruction while BrowserContextEpoch remains monotonic. Immediately after destruction a retained predecessor authority fails `ContextNotOwned`; after recreation it fails `AuthorityMismatch`. Across aggregate recreation, `BrowserSessionIncarnation` rejects stale authority even when raw ids and local epochs alias. +The 258-generation hostile fixture proves the same raw isolation/context values may be reused after proven destruction while `BrowserContextEpoch` remains monotonic. Immediately after destruction a predecessor authority fails `ContextNotOwned`; after recreation it fails `AuthorityMismatch`. Across aggregate recreation, `BrowserSessionIncarnation` rejects stale authority even when raw ids and local epochs alias. Removing a proven-destroyed record is not durable history deletion. Cross-process recovery and buyer audit history remain separate persistence concerns. ## Abandonment and lifecycle completion -`BoundBrowserSession

` and recovery custody are `#[must_use]` linear owners. `Drop` never performs browser I/O. Dropping unresolved custody increments only the process-local `abandoned_bound_session_count()` signal. +`BoundBrowserSession

` and recovery custody are `#[must_use]` linear owners. `Drop` performs no browser I/O. Dropping unresolved custody increments only the process-local `abandoned_bound_session_count()` signal. Successful exact-fact reconciliation updates underlying aggregate state before eventual drop. If every fact and uncertain owner is retired, the aggregate is `Ended`, so dropping a fully reconciled recovery wrapper is not reported as unresolved abandonment. @@ -118,9 +117,7 @@ Successful exact-fact reconciliation updates underlying aggregate state before e The #317 lineage admits observed navigation only for the exact active `(BrowserSessionIncarnation, BrowsingContextId, BrowserContextEpoch)` generation. Admission revokes presentation authority without adapter I/O and mints opaque `NavigationSettlementAuthority`. Commit is non-terminal; positive settlement, typed negative terminal, and download start share one exactly-once closure. A newer navigation supersedes an older witness. Explicit re-establishment alone consumes the next presentation epoch. -Presentation mutation and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup; the exact ordinary bound lifecycle owner may still destroy its retained context without reopening presentation authority. - -Recovery settlement is also separate from navigation settlement. Completing remote-ownership reconciliation cannot recreate navigation or presentation authority. +Presentation mutation and lifecycle cleanup are separate. A navigation-invalidated presentation capability cannot authorize mutation or authority-based cleanup; the exact ordinary bound lifecycle owner may still destroy its retained context without reopening presentation authority. Recovery settlement remains separate from navigation settlement and cannot recreate navigation or presentation authority. ## Browser-issued identity and standards trace @@ -137,7 +134,10 @@ The immutable W3C WebDriver BiDi Working Draft verified for this lineage is the | aggregate-issued create attempt | `DisposableContextCreateRequest::attempt_epoch`; transaction fixture | | same consumed adapter for ordinary work | `AuthorizedContextOperationPort`; `authorized_context_operation.rs` | | same consumed adapter for recovery commands | `RecoveryContextOperationPort`; `RecoveryContextOperationRequest`; `recovery_same_adapter_operation.rs` | +| recovery command requires one current fact | `recovery_operation_exact_fact_scope.rs`; `tests/test_browser_session_recovery_operation_contract.py` | +| foreign/stale operation fact rejected before I/O | `recovery_operation_exact_fact_scope.rs` | | command success/failure does not settle ownership | `recovery_same_adapter_operation.rs` | +| terminal settlement closes recovery command path | `recovery_operation_terminal_closure.rs` | | exact proof-bearing recovery fact | `RecoveryFact`; `RecoverySettlementRequest`; `RecoverySettlementPort`; `settle_recovery_fact` | | replay/sibling/foreign proof cases | `recovery_exact_fact_settlement.rs` | | recovery settlement surface remains opaque/current | `tests/test_browser_session_recovery_settlement_contract.py` | @@ -147,10 +147,10 @@ The immutable W3C WebDriver BiDi Working Draft verified for this lineage is the | recovery custody cannot regain ordinary authority | recovery rustdoc `compile_fail`; repository contracts | | navigation witness is opaque and generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | -Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source remains non-shipment until exact-head repository contracts, rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review/security gates, and protected `main` integration are observed. +Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source remains non-shipment until exact-head repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review/security gates, and protected `main` integration are observed. ## Current integration boundary -#317 owns Browser Session domain policy, same-adapter recovery-command custody, and generic exact-fact settlement. #318/#321 own stacked hostile navigation/ABA acceptance and doctoring only. #316 owns WebDriver BiDi proof qualification, pending/accepted/quarantined state, event correlation, remote liveness, protocol-specific recovery semantics, and real-browser adapter integration. +#317 owns Browser Session domain policy, same-adapter exact-fact recovery-command custody, and generic exact-fact settlement. #318/#321 own stacked hostile navigation/ABA acceptance and doctoring only. #316 owns WebDriver BiDi proof qualification, pending/accepted/quarantined state, event correlation, remote liveness, protocol-specific recovery semantics, and real-browser adapter integration. Real Chromium navigation, interaction, cleanup, recovery, and browser-observed post-condition evidence remains required before shipment. Immutable release, signed artifact, SBOM, provenance, reproducibility, and rollback evidence remain separate release gates. From d03d4bb91ea109db80fe2ea02f5822b8f3ee97ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:09:54 +0900 Subject: [PATCH 147/632] docs(browser-session): bind recovery command custody to exact facts --- ...sion-recovery-custody-and-hot-ownership.md | 231 ++++++++---------- 1 file changed, 101 insertions(+), 130 deletions(-) diff --git a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md index bfeecb136..b1b486607 100644 --- a/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md +++ b/docs/adr/0116-browser-session-recovery-custody-and-hot-ownership.md @@ -8,223 +8,194 @@ ## Context -ADR 0114 establishes Browser Session as the owner of disposable-context lifecycle authority. It binds one concrete lifecycle adapter linearly, validates opaque presentation authority before browser I/O, and retains non-authorizing evidence whenever remote ownership becomes uncertain. +ADR 0114 establishes Browser Session as the owner of disposable-context lifecycle authority. One concrete lifecycle adapter is consumed into `BoundBrowserSession

`; raw protocol identifiers never mint create, presentation, navigation, cleanup, or recovery authority. -Three follow-on problems are addressed here. +Recovery introduces three additional constraints. -First, a session that enters `RecoveryRequired`, or enters `TransportLost` while unresolved ownership evidence remains, still owns the exact adapter instance that observed the unresolved remote state. Reconstructing another adapter from identifiers would break same-instance custody. Exposing raw `P`, the inner `BrowserSession`, or ordinary lifecycle methods would instead turn recovery into an authority escape. +First, unresolved ownership must retain the exact adapter instance that observed the remote state. Reconstructing a second adapter from identifiers breaks lifecycle custody, while exposing raw `P` or an unrestricted callback creates an authority escape. -Second, recovery command success is not recovery completion. A command ACK cannot prove that a remote browser boundary is absent or reconciled. Browser Session therefore needs a second, proof-bearing transition that consumes exactly one current recovery fact only after a protocol owner has independently qualified evidence and the exact retained adapter verifies it. +Second, recovery command execution and recovery completion are different operations. A browser or driver command ACK is not proof that remote ownership has been reconciled. Browser Session therefore needs a proof-bearing settlement transition that retires exactly one current recovery fact only after independently qualified evidence has been verified by the retained adapter. -Third, retaining a permanent `Destroyed` record for every proven-destroyed context would make command-authority hot state grow with historical throughput. Current command admission and durable audit history require different retention semantics. +Third, even while recovery remains open, a generic recovery command must not receive authority or evidence broader than the fact that justifies that command. Passing the full recovery ledgers to every adapter operation discloses unrelated sibling recovery facts and lets an operation run without presenting the Browser Session-issued fact it is meant to reconcile. Recovery operations therefore require one opaque current `RecoveryFact`, are validated before adapter I/O, and receive only the selected fact. -WebDriver BiDi pending/accepted/quarantined tuple truth, protocol event correlation, replay qualification, remote-liveness interpretation, and the meaning of concrete recovery evidence remain adapter concerns owned by #316. Durable cross-process persistence is also separate from the in-memory Browser Session hot map. +WebDriver BiDi pending/accepted/quarantined tuple truth, protocol event correlation, replay qualification, remote-liveness interpretation, and concrete proof semantics remain #316 responsibilities. Durable cross-process persistence also remains outside the in-memory Browser Session aggregate. ## Decision drivers - Preserve the exact consumed adapter across unresolved ownership without making it ambient. -- Permit only purpose-bounded recovery I/O; never expose raw `P` or an unrestricted callback. -- Revoke purpose-bounded recovery I/O once proof-bearing settlement removes the final unresolved fact. -- Do not mint recovery custody from `TransportLost` when no unresolved ownership fact exists. +- Require one current Browser Session-issued `RecoveryFact` for each generic recovery operation. +- Reject foreign, stale, replayed, shifted, or out-of-range operation facts before adapter I/O. +- Expose only the selected recovery fact to the adapter command path; do not disclose sibling ledgers. - Keep adapter command success/failure separate from independent recovery proof. -- Bind settlement to one opaque Browser Session-issued fact, not a raw vector index or protocol identifier. -- Reject foreign, stale, replayed, or out-of-range recovery facts before proof-verifier I/O. -- Retire only the exact fact that was independently verified; preserve unrelated sibling uncertainty. -- Keep identity-oriented recovery facts and create-attempt facts independently addressable. -- Prevent recovery settlement from restoring ordinary create, navigation, presentation, or cleanup authority. -- Preserve exact failed-destroy ownership and epoch evidence until reconciliation proves that fact gone. +- Revoke generic recovery I/O after final proof-bearing settlement reaches `Ended`. +- Do not mint recovery custody from ownership-clean `TransportLost`. +- Bind settlement to one opaque current fact, not a raw vector index or protocol identifier. +- Retire only the exact independently verified fact; preserve sibling uncertainty. +- Keep identity-oriented and create-attempt recovery facts independently addressable. +- Never restore ordinary create, navigation, presentation, cleanup, or generic recovery-command authority after reconciliation. +- Preserve failed-destroy ownership and epoch evidence until independently qualified reconciliation proves it gone. - Keep command-authority hot state bounded to live or uncertain ownership. -- Preserve monotonic stale-authority rejection across raw browser-id reuse. -- Keep durable history and process-restart persistence separate from command admission. ## Authority boundaries -Browser Session owns lifecycle identity, ownership state, context epochs, ordinary lifecycle/presentation admission, navigation-generation custody, the one-way transition into recovery custody, opaque recovery-fact issuance, deterministic exact-fact retirement, and terminal revocation of recovery-command authority. +Browser Session owns lifecycle identity, ownership state, context epochs, ordinary lifecycle/presentation admission, navigation-generation custody, one-way transition into recovery custody, opaque `RecoveryFact` issuance, current-fact validation, deterministic exact-fact retirement, recovery-revision advancement, and terminal revocation of recovery-command authority. -`BoundBrowserSessionRecovery

` owns the same concrete adapter instance but is not a protocol-specific recovery engine. When `P: RecoveryContextOperationPort`, it may execute a purpose-bounded recovery operation through `RecoveryContextOperationRequest` only while the aggregate remains `RecoveryRequired` or `TransportLost`. This path snapshots exact Browser Session identity, incarnation, unresolved state, both recovery-evidence ledgers, and the adapter-defined operation. Adapter success or failure does not mutate Browser Session uncertainty. Once proof-bearing settlement closes custody to `Ended`, `execute_recovery_context_operation` returns `RecoveryContextOperationError::RecoveryClosed` before retained-adapter I/O. +`BoundBrowserSessionRecovery

` owns the same concrete adapter instance but is not a protocol-specific recovery engine. It exposes read-only recovery evidence and opaque fact issuance. It does not expose raw `P`, the inner `BoundBrowserSession`, ordinary Browser Session methods, or a caller-provided callback over the adapter. -When `P: RecoverySettlementPort`, recovery custody may also issue opaque `RecoveryFact` handles and execute `settle_recovery_fact(fact, proof)`. `RecoveryFact` binds the exact Browser Session id, process-local incarnation, ledger kind, current ledger index, and monotonic recovery revision. `RecoverySettlementRequest

` is privately constructed only after Browser Session validates that handle against current custody. The exact retained adapter verifies the independently supplied proof. Browser Session, not the adapter, then commits retirement of exactly the selected fact. +When `P: RecoveryContextOperationPort`, `execute_recovery_context_operation(fact, operation)` is available only while the aggregate remains `RecoveryRequired` or evidence-bearing `TransportLost`. Before adapter I/O Browser Session validates: -`RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest` have no public construction path. The crate-private bridge that touches `&mut P` remains `BoundBrowserSession::dispatch_recovery_operation`; external consumers cannot supply arbitrary callbacks or recover raw adapter access. +1. the fact belongs to the exact Browser Session id and process-local incarnation; +2. the fact was issued at the current recovery revision; +3. the selected ledger/index still addresses a current recovery fact. -#316 owns WebDriver BiDi proof qualification. A `contextDestroyed` event, session-loss observation, liveness conclusion, or tuple transition is not automatically proof merely because it came from the protocol. #316 must decide which observations satisfy `RecoverySettlementPort::Proof`; Browser Session consumes only that already-qualified proof under its deterministic exact-fact contract. +`RecoveryContextOperationRequest` is then privately constructed with Browser Session id, incarnation, unresolved state, the **one selected** identity-oriented or create-attempt fact, and the adapter-defined operation. It does not contain full recovery vectors. Foreign facts return `RecoveryContextOperationError::AuthorityMismatch`; stale or no-longer-current facts return `RecoveryContextOperationError::StaleFact`; both fail before adapter I/O. Adapter success or `RecoveryContextOperationError::Adapter(E)` leaves Browser Session evidence unchanged. Reusing the same current fact for retries is allowed until a successful settlement advances the revision. -Durable crash/process-restart persistence and buyer audit history do not live in `BrowserSession.contexts`. `abandoned_bound_session_count()` is process-local operability evidence only. LLM output, page content, protocol identifiers, recovery evidence, adapter command success, and model judgment never become deterministic Browser Session policy authority. +When `P: RecoverySettlementPort`, `settle_recovery_fact(fact, proof)` applies the same exact-fact identity/revision/address validation. The retained adapter verifies independently qualified proof carried by `RecoverySettlementRequest

`. Browser Session, not the adapter, commits retirement of the selected fact and then advances the monotonic recovery revision. Every previously issued `RecoveryFact`, including unrelated sibling handles, becomes stale after that mutation. -## Options considered +Once both recovery ledgers and uncertain hot ownership are empty, the aggregate becomes terminal `Ended`. `execute_recovery_context_operation` then returns `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. Complete recovery never recreates `Active` or ordinary browser authority. + +The only bridge receiving `&mut P` remains crate-private `BoundBrowserSession::dispatch_recovery_operation`. `RecoveryContextOperationRequest`, `RecoveryFact`, and `RecoverySettlementRequest` have no public construction path. -### Return raw `P` from the failed bound session +#316 owns WebDriver BiDi proof qualification. A `browsingContext.contextDestroyed` event, session-loss observation, liveness conclusion, or tuple transition is not automatically proof merely because it came from the protocol. #316 decides which observations can satisfy `RecoverySettlementPort::Proof`; Browser Session consumes only its already-qualified proof under the deterministic exact-fact contract. -Rejected. Raw adapter recovery recreates ambient capability and permits browser commands outside Browser Session authority. +## Options considered -### Expose `&BrowserSession` or `FnOnce(&mut P)` from recovery custody +### Return raw `P` or expose an unrestricted callback -Rejected. The first can become an indirect capability-minting escape as the aggregate evolves; the second is equivalent to raw adapter access. The adapter bridge remains crate-private. +Rejected. Either form recreates ambient adapter capability outside Browser Session authority. ### Clone or reconstruct the adapter for recovery -Rejected. Equal credentials, endpoint, or identifiers do not establish same lifecycle instance or pending protocol state. +Rejected. Equal endpoint, credentials, or identifiers do not prove same lifecycle instance or pending protocol state. + +### Treat any `TransportLost` as recovery authority + +Rejected. Transport loss proves liveness loss, not unresolved remote ownership. Ownership-clean transport loss cannot mint a recovery command path. -### Treat any `TransportLost` state as recovery authority +### Execute a recovery operation without a `RecoveryFact` -Rejected. Transport loss proves only liveness loss. Before any remote ownership, or after every boundary is proven destroyed, there is no unresolved fact to reconcile. +Rejected. State-level recovery custody is too broad to authorize an arbitrary operation. It allows the caller to reach the retained adapter without identifying the exact unresolved fact that justifies the command. -### Treat a successful recovery command as reconciliation proof +### Pass both full recovery ledgers to every recovery operation -Rejected. Command completion is not a browser-observed post-condition. `execute_recovery_context_operation` always preserves Browser Session uncertainty. +Rejected. It violates purpose limitation and least authority by disclosing sibling recovery facts unrelated to the selected command. The operation request carries exactly one selected fact. + +### Treat successful command execution as reconciliation proof + +Rejected. Command completion is not a browser-observed post-condition. Recovery commands never mutate Browser Session uncertainty directly. ### Keep generic recovery I/O callable after complete settlement -Rejected. Once all recovery facts and uncertain ownership are gone, the purpose that justified access to the retained adapter is gone as well. Retaining the generic recovery-command path after `Ended` would be an ambient post-recovery capability even though ordinary browser authority is intentionally not resurrected. +Rejected. Once unresolved facts are gone, the purpose that justified retained-adapter access is gone. Terminal `Ended` closes that route before another adapter call. ### Let the adapter delete recovery evidence directly -Rejected. That would move domain ownership truth into an adapter and let protocol data rewrite policy state. +Rejected. Protocol data must not rewrite Browser Session ownership truth. ### Identify a recovery fact by raw vector index -Rejected. Retiring one fact shifts later indices. An old index could then address a different sibling. `RecoveryFact` therefore carries a monotonic revision; successful settlement advances it and invalidates all previously issued fact handles. +Rejected. Retiring one fact shifts indices. `RecoveryFact` carries a monotonic revision; successful settlement invalidates all prior handles. ### Keep previously issued sibling facts valid after another fact settles -Rejected. It makes index-shift replay ambiguous. Callers must reread current custody after every successful settlement. +Rejected. That would make index-shift replay ambiguous. Callers must reread current custody after mutation. -### Clear both recovery ledgers when one remote condition is proven +### Clear both recovery ledgers when one condition is proven -Rejected. `BrowserSessionRecoveryEvidence` records identity/ownership uncertainty while `DisposableContextCreateRecoveryEvidence` records create-attempt transaction uncertainty. They are independent facts and require independent retirement. +Rejected. Identity/ownership uncertainty and create-attempt transaction uncertainty are independent facts and retire independently. ### Restore an ordinary `BoundBrowserSession

` after reconciliation -Rejected. Crossing the recovery boundary is one-way. Even complete recovery reaches terminal `Ended`; it never recreates `Active`, create authority, presentation authority, navigation authority, or normal lifecycle cleanup authority. +Rejected. Recovery is a one-way boundary. Complete reconciliation reaches `Ended`, never `Active`. ### Keep every proven-destroyed context as a permanent hot tombstone -Rejected. It conflates authorization state with audit history. Exact destruction plus monotonic epochs are sufficient for stale-authority rejection. - -### Delete hot ownership after a destroy command ACK - -Rejected. Only proven destruction or proof-bearing recovery settlement may retire uncertainty. Failed/unproven destruction retains exact ownership and evidence. +Rejected. Authorization state and durable audit history have different retention requirements. Proven ordinary destruction removes hot ownership while monotonic epochs reject stale authority. ## Decision -1. `BoundBrowserSession::into_recovery(self)` is the only transition into recovery-only custody. It succeeds from `RecoveryRequired`, or from `TransportLost` only when exact `BrowserSessionRecoveryEvidence` or `DisposableContextCreateRecoveryEvidence` remains. -2. `Active`, `Ended`, and ownership-clean `TransportLost` are returned unchanged. Handoff performs no browser I/O. -3. Handoff moves the exact existing `BoundBrowserSession

` and same non-`Clone` adapter instance. -4. Recovery custody exposes lifecycle state and exact non-authorizing evidence. It exposes no raw `P`, inner `BoundBrowserSession`, inner `BrowserSession`, ordinary create, presentation lookup, epoch advance, destroy, authorized operation, navigation transition, or normal finish. -5. `RecoveryContextOperationPort` is the only generic recovery-command path. Its request is private-construction and its success/failure leaves all Browser Session recovery state unchanged. It is callable only while custody remains `RecoveryRequired` or `TransportLost`; terminal `Ended` returns `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. -6. `RecoverySettlementPort` is the only generic proof-verification path. Protocol-specific proof vocabulary remains adapter-owned. -7. `recovery_fact(index)` and `create_attempt_recovery_fact(index)` issue opaque current-revision handles only for facts that currently exist. -8. `settle_recovery_fact` validates exact Browser Session id and incarnation before adapter I/O. A foreign fact returns `RecoverySettlementError::AuthorityMismatch`. -9. It then validates the current recovery revision and exact current ledger/index fact before adapter I/O. Replay, sibling handles issued before another settlement, or out-of-range facts return `RecoverySettlementError::StaleFact`. -10. Revision increment capacity is checked before verifier I/O; exhaustion fails closed as `RevisionExhausted`. -11. Only after those checks does Browser Session construct `RecoverySettlementRequest` and call the exact retained adapter's `verify_recovery_settlement`. -12. Verifier failure returns `RecoverySettlementError::Adapter(E)` and mutates neither recovery ledger nor Browser Session lifecycle state. -13. Verifier success retires exactly the selected fact. Identity-oriented and create-attempt ledgers are independent; one settlement never broad-erases both. -14. For `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, or `TransportLossOwnedHandle`, retirement may also remove the exact matching `Uncertain` hot-ownership record. `PartialCreationIsolation`, `DuplicateAdapterHandle`, and `UnsettledAdapterHandle` retire evidence only and cannot consume an independently accepted same-valued owner. -15. A successful settlement advances the monotonic recovery revision. Every `RecoveryFact` issued before that mutation becomes stale. -16. Partial settlement preserves every unrelated sibling fact and keeps the aggregate in its unresolved state. -17. When both recovery ledgers are empty and no uncertain hot ownership remains, Browser Session reaches terminal `Ended`. It never transitions back to `Active` or restores ordinary browser command authority, and generic recovery operations are thereafter rejected before retained-adapter I/O. -18. The crate-private `dispatch_recovery_operation` remains the only bridge receiving `&mut P`; callers never receive raw adapter access. -19. Negative capability boundaries remain executable contracts through rustdoc `compile_fail` and repository tests. -20. `BrowserSession.contexts` contains only current live or uncertain ownership. Proven ordinary destruction removes a hot ownership record; failed destruction retains it as `Uncertain` plus exact `UnprovenDestruction { context, context_epoch }`. -21. Proven destruction releases raw isolation/context identities for later reuse only under a new monotonic `BrowserContextEpoch`. Predecessor authority therefore cannot revive after ABA reuse. -22. `Drop` performs no browser I/O. Unresolved custody preserves process-local abandonment accounting. -23. Browser-observed navigation remains generation-qualified and protocol-agnostic. Admission revokes presentation authority without adapter I/O; commit is non-terminal; positive settlement, typed negative terminal, and download start share one exactly-once closure; explicit re-establishment consumes a new presentation epoch. -24. Presentation mutation and lifecycle cleanup remain separate. Navigation-invalidated presentation authority cannot mutate or authorize authority-based cleanup, while the exact ordinary bound lifecycle owner may destroy its retained context without reopening presentation authority. -25. This ADR remains `Proposed` until the complete slice reaches protected `main` with exact-head repository gates and independently observed real-browser recovery/destruction/navigation post-conditions. +1. `BoundBrowserSession::into_recovery(self)` is the only transition into recovery-only custody. +2. It succeeds from `RecoveryRequired`, or from `TransportLost` only while exact unresolved recovery/create-attempt evidence remains. +3. Handoff moves the existing `BoundBrowserSession

` and exact same adapter instance without browser I/O. +4. Recovery custody exposes lifecycle state, read-only evidence, and opaque current-revision `RecoveryFact` issuance; it exposes no raw adapter or ordinary Browser Session authority. +5. `RecoveryContextOperationPort` is the generic recovery-command boundary. Every call supplies a `RecoveryFact` plus adapter-defined operation. +6. Operation validation rejects foreign or stale facts before adapter I/O and sends only the selected current fact in `RecoveryContextOperationRequest`. +7. Operation success/failure does not settle, erase, or mutate Browser Session recovery state. +8. `RecoverySettlementPort` is the generic proof-verification boundary. Protocol-specific proof vocabulary remains adapter-owned. +9. `settle_recovery_fact` validates session/incarnation/revision/exact current fact before proof I/O, checks next-revision capacity, verifies proof through the exact retained adapter, then retires only the selected fact. +10. Verifier failure is non-mutating. Successful retirement advances the recovery revision and invalidates all previously issued handles. +11. Identity-oriented and create-attempt ledgers retire independently. +12. `UnprovenDestruction`, `RecoveryRequiredOwnedHandle`, and `TransportLossOwnedHandle` may retire the exact matching `Uncertain` hot ownership record. Candidate/partial-create evidence cannot consume a distinct accepted owner merely because remote values alias. +13. Partial settlement preserves every unrelated sibling fact and keeps recovery open. +14. When both ledgers and uncertain ownership are empty, Browser Session reaches terminal `Ended`; ordinary and generic recovery-command authority stay closed. +15. Proven ordinary destruction removes the live hot record. Failed destruction retains `Uncertain` ownership plus exact `UnprovenDestruction { context, context_epoch }` evidence. +16. Proven destruction may release raw browser identities for later reuse only under a new monotonic `BrowserContextEpoch`; predecessor authority cannot revive after ABA reuse. +17. `Drop` performs no browser I/O. Unresolved custody preserves process-local abandonment accounting only. +18. Browser-observed navigation remains a separate generation-qualified authority machine; recovery settlement cannot recreate navigation or presentation authority. +19. ADR 0116 remains `Proposed` until this complete slice reaches protected `main` with exact-head gates and independently observed real-browser recovery/destruction/navigation post-conditions. ## Consequences -Browser Session has two linear owner forms: ordinary `BoundBrowserSession

` and one-way `BoundBrowserSessionRecovery

`. Recovery custody can issue purpose-bounded adapter commands and can consume independently qualified proof while unresolved recovery state exists, but neither mechanism exposes the adapter or recreates ordinary Browser Session authority. Complete settlement closes both ordinary and recovery-command authority while retaining the terminal wrapper as inert state/evidence custody. - -Recovery settlement is deliberately revision-coarse. Settling any fact invalidates all fact handles issued under the prior revision, including unrelated siblings. This forces callers to reread the current evidence ledger after mutation and prevents index-shift replay at the cost of extra handle acquisition. Recovery fact counts are expected to be small, and correctness at this security boundary dominates preserving stale handles. +Recovery now has two distinct least-authority paths on the same retained adapter: an exact-fact-scoped command path and an exact-fact proof-settlement path. The command path can be retried while its fact remains current but sees no sibling recovery evidence. Settlement changes the revision, forcing all pre-existing handles to be reacquired and preventing replay after index shifts. -Hot ownership remains proportional to current live/uncertain state rather than historical throughput. Durable history must be retained by a separate authorized persistence owner. +The revision is deliberately coarse: settling any fact invalidates every handle from the previous revision. Recovery fact counts are expected to be small, and correctness at this security boundary takes precedence over preserving stale handles. -The active #317 lineage keeps ownership generation, navigation witness generation, and recovery revision as separate authority dimensions. Raw browser identifiers never substitute for any of them. +Hot ownership remains proportional to current live/uncertain state rather than historical throughput. Durable history and cross-process recovery require a separate authorized persistence owner. ## Failure and degraded behavior -A rejected `into_recovery` performs no I/O and returns the original bound owner. A failed recovery command preserves custody and evidence. A successful recovery command also preserves custody and evidence; it is not proof. After proof-bearing settlement reaches terminal `Ended`, another recovery command returns `RecoveryClosed` before adapter I/O. +A rejected `into_recovery` performs no I/O and returns the original bound owner. -A settlement with a foreign, stale, replayed, or out-of-range `RecoveryFact` fails before verifier I/O. A verifier rejection fails after proof I/O but before domain mutation. In both cases the ledgers remain unchanged. +A recovery operation using a foreign or stale fact fails before adapter I/O. Adapter failure preserves custody and evidence. Adapter success also preserves custody and evidence; it is not proof. After final settlement reaches `Ended`, another operation returns `RecoveryClosed` before I/O. -A failed destruction never retires hot ownership. Transport loss preserves active handles as non-authorizing evidence and does not prove destruction. Transport loss with no unresolved browser state creates no recovery custody. +A settlement with a foreign, stale, replayed, or out-of-range fact fails before proof-verifier I/O. Proof rejection occurs after verifier I/O but before domain mutation. In both cases recovery ledgers remain unchanged. -If monotonic incarnation, context epoch, navigation generation, or recovery revision allocation exhausts, allocation fails closed rather than wrapping authority identity. +A failed destruction never retires hot ownership. Transport loss preserves active handles as non-authorizing evidence and cannot itself prove destruction. Transport loss with no unresolved browser state creates no recovery custody. -Dropping unresolved ordinary or recovery custody performs no browser I/O. Process-local abandonment observability may increase, but it is neither cleanup nor durable recovery. +If monotonic incarnation, context epoch, navigation generation, or recovery revision allocation exhausts, allocation fails closed rather than wrapping authority identity. ## Security / privacy / governance impact -Recovery custody is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, adapter-selected handles, and recovery evidence cannot reconstruct ordinary command authority. Deterministic Browser Session policy is never delegated to an LLM. - -The exact adapter remains owned and reachable only through purpose-bounded traits while unresolved recovery state exists. `RecoveryFact` and `RecoverySettlementRequest` are opaque and non-caller-constructible. Session/incarnation/revision/exact-fact validation occurs before proof-verifier I/O, preventing foreign or replayed handles from turning protocol proof checking into an oracle or mutation channel. Terminal settlement also removes the remaining generic recovery-operation route before another adapter call can occur. +Recovery custody is a capability-reduction boundary. Untrusted page data, model output, protocol identifiers, adapter-selected handles, recovery evidence, command acknowledgements, and model judgment cannot reconstruct deterministic Browser Session authority. -Alias-safe hot-state retirement prevents a rejected or partial create that reuses remote values from deleting a distinct previously accepted owner. Resource bounding therefore cannot convert unresolved ownership into an untracked boundary. +Exact-fact command validation prevents a generic recovery operation from using custody alone as authority. Selected-fact-only requests also avoid disclosing unrelated recovery facts to the adapter, reducing purpose-unrelated propagation of browser identifiers or other recovery metadata. Session/incarnation/revision/exact-address validation occurs before adapter I/O, so foreign or stale handles cannot turn adapter command execution into an oracle or mutation channel. -This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into Browser Session. Recovery evidence may identify remote browser boundaries but creates no new purpose for page-content or PII processing. +This ADR does not move EgressWeave, Wardnet, Keyverse, contextual-orchestrator, Chromium sandboxing, or WebDriver BiDi protocol truth into Browser Session. ## Tests and acceptance evidence - `crates/originweave-browser-session/src/recovery.rs` - - `BoundBrowserSession::into_recovery` - - `BoundBrowserSessionRecovery

` + - one-way `into_recovery` - `RecoveryContextOperationRequest` / `RecoveryContextOperationPort` - `RecoveryFact` - `RecoverySettlementRequest

` / `RecoverySettlementPort` - - `RecoverySettlementError` - - `settle_recovery_fact` - - negative `compile_fail` capability contracts -- `crates/originweave-browser-session/tests/recovery_owner_handoff.rs` - - unproven destroy and unresolved transport loss move exact adapter/evidence without I/O -- `crates/originweave-browser-session/tests/recovery_handoff_requires_unresolved_ownership.rs` - - ownership-clean transport loss cannot mint recovery custody + - pre-I/O exact-fact validation and terminal `RecoveryClosed` +- `crates/originweave-browser-session/tests/recovery_operation_exact_fact_scope.rs` + - selected-fact-only operation request + - stale fact after settlement rejected before adapter I/O + - foreign fact rejected before adapter I/O - `crates/originweave-browser-session/tests/recovery_same_adapter_operation.rs` - - same-adapter operation; exact provenance; success/failure do not clear uncertainty + - same-adapter command path for identity and create-attempt facts + - command success/failure is non-settling - `crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs` - - final exact-fact settlement reaches `Ended` and later generic recovery I/O returns `RecoveryClosed` before the retained adapter is called + - final settlement closes generic recovery I/O before another adapter call - `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` - - single-fact settlement preserves siblings - - replay and predecessor sibling handles fail stale before proof I/O - - foreign session/incarnation fact fails before proof I/O - - proof failure is non-mutating - - identity and create-attempt ledgers retire independently - - complete reconciliation reaches terminal `Ended` without authority resurrection + - exact one-fact settlement, replay/sibling/foreign rejection, proof-failure non-mutation, independent ledgers, terminal closure - `tests/test_browser_session_recovery_operation_contract.py` - - recovery-operation adapter custody and nonconstructible request surface + - nonconstructible selected-fact request surface and hostile exact-fact fixtures - `tests/test_browser_session_recovery_settlement_contract.py` - - opaque settlement API, pre-I/O validation order, external hostile fixture, and ADR/trace/UML/doctoring currentness -- `crates/originweave-browser-session/tests/proven_destroy_releases_hot_ownership.rs` - - 258 same-handle generations; monotonic epochs; predecessor rejection -- navigation owner tests and `tests/test_browser_session_navigation_owner_surface_contract.py` - - current-witness revocation/closure/re-establishment and cleanup separation + - opaque settlement API and validation order +- `docs/traceability/browser-session-lifecycle-authority.md` +- `docs/uml/browser-session-lifecycle-authority.md` -These are active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. +These remain active-PR contracts until the exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review, and protected-main integration. ## Migration and rollback -Dependents must adopt this foundation by ordinary non-force restack after the parent exact head is verified. They must not copy Browser Session source, infer recovery authority from raw identifiers, or reconstruct a second adapter. - -Rollback before protected-main adoption reverts the recovery-custody, settlement, terminal-recovery closure, hot-retirement, and navigation-authority slice together with its hostile fixtures and ADR. Selectively restoring raw adapter access, evidence-free recovery custody, ACK-as-proof, raw-index settlement, post-`Ended` recovery I/O, or authority resurrection is not a valid rollback. After protected-main adoption, rollback requires another policy-compliant change that preserves unresolved ownership evidence and demonstrates that predecessor capabilities cannot revive. - -## Open follow-ups - -- #316: after #317 exact-head executable GREEN, ordinary non-force adoption of the generic recovery boundary; implement WebDriver BiDi proof qualification, pending/accepted/quarantined correlation, event replay qualification, remote liveness, and pinned-Chromium recovery post-condition evidence. -- #318/#321: executable acceptance/review successors for the #317 navigation contract and same-raw-id/sibling-recreation matrices. -- Durable crash/process-restart persistence of exact recovery facts and buyer-required audit history. -- Real Chromium proof of remote destruction, cleanup, navigation, interaction, recovery, and browser-observed post-conditions. -- `docs/product-technical-gap-baseline.md` must distinguish active-PR implementation from protected-main/release evidence. -- Protected-main immutable release, signed artifacts, SBOM, provenance, reproducibility, and rollback evidence. - -## Supersession / reversal conditions - -A successor may supersede this ADR only if it preserves at least: same-instance recovery custody; no ambient adapter escape; evidence-gated recovery capability; command-ACK/proof separation; opaque exact-fact settlement; pre-I/O foreign/stale rejection; sibling preservation; independent ledger retirement; alias-safe hot ownership; terminal non-resurrection; post-settlement recovery-command revocation; bounded command-authority state; monotonic ABA rejection; generation-bound navigation custody; presentation/lifecycle cleanup separation; and separation of durable history from command admission. - -Changing the recovery owner or persistence architecture alone does not justify weakening those guarantees. +Consumers of the prior active-PR recovery command signature must reacquire a current `RecoveryFact` and pass it to `execute_recovery_context_operation(fact, operation)`. Adapters must treat `RecoveryContextOperationRequest` as one selected fact, not a snapshot of both ledgers. -## References +If the selected-fact command boundary cannot be supported safely, rollback means removing the generic recovery-command surface and retaining read-only recovery custody plus proof-bearing settlement. Rollback must not restore raw adapter access, whole-ledger command requests, caller-constructible fact handles, or command-ACK settlement. -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +## Follow-up -Rust Project Developers. (2026). *The Rust Programming Language: Ownership*. https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html \ No newline at end of file +#316 must consume this boundary without copying Browser Session source. Its next integration slice must bind WebDriver BiDi pending/accepted/quarantined tuple and remote-liveness evidence to the current `RecoveryFact`, qualify protocol-specific proof for `RecoverySettlementPort`, and prove with pinned Chromium that command ACK and browser-observed post-condition remain distinct. From 49d7419fe25c4d22d11ad928286549b4487ecf82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 17:10:20 +0900 Subject: [PATCH 148/632] docs(browser-session): doctor exact-fact recovery operation scope --- .../browser-session-recovery-settlement.md | 89 ++++++++++++------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/docs/doctoring/browser-session-recovery-settlement.md b/docs/doctoring/browser-session-recovery-settlement.md index 59b517e3e..a418fa299 100644 --- a/docs/doctoring/browser-session-recovery-settlement.md +++ b/docs/doctoring/browser-session-recovery-settlement.md @@ -1,74 +1,95 @@ # Browser Session recovery settlement boundary -Status: active-PR implementation evidence for #317. The source implementation now exists, but this document does not claim protected-main adoption, executable GREEN, browser acceptance, or release readiness until the exact head passes repository gates. +Status: active-PR implementation evidence for #317. Source implementation exists, but this document does not claim protected-main adoption, executable GREEN, browser acceptance, or release readiness until the exact head passes repository gates. ## Problem -`BoundBrowserSessionRecovery

` already preserved the exact consumed adapter and permitted purpose-bounded recovery I/O without exposing raw `P`. That solved custody and dispatch, but not recovery completion: adapter success or command acknowledgement deliberately left `RecoveryRequired` and both recovery ledgers unchanged because an I/O return value is not proof that remote browser ownership was destroyed or reconciled. +`BoundBrowserSessionRecovery

` preserves the exact consumed adapter without exposing raw `P`. Recovery command execution and recovery completion are deliberately separate: an adapter return or browser command ACK is not proof that remote ownership is absent or reconciled. -The settlement repair gives a protocol owner such as #316 a second-stage path: independently qualify browser evidence, submit it against one Browser Session-issued recovery fact, and retire only that exact uncertainty after the retained adapter verifies the proof. +The settlement boundary therefore lets a protocol owner such as #316 independently qualify browser evidence, submit it against one Browser Session-issued `RecoveryFact`, and retire only that exact uncertainty after the retained adapter verifies the proof. -A follow-on capability gap existed after complete settlement. The final exact fact could move the aggregate to terminal `Ended` while the caller still held `BoundBrowserSessionRecovery

`, and the generic recovery-operation method had no state gate. That left the exact retained adapter callable after the recovery purpose had ceased to exist. Terminal settlement now revokes that remaining command route: later generic recovery operations return `RecoveryContextOperationError::RecoveryClosed` before adapter I/O. +Two follow-on capability gaps were found while hardening this boundary. -## Constraints +First, the final settlement could move the aggregate to terminal `Ended` while `BoundBrowserSessionRecovery

` still exposed generic recovery I/O. That route is now closed by a pre-I/O lifecycle-state gate returning `RecoveryContextOperationError::RecoveryClosed`. -Browser Session owns deterministic lifecycle state and exact fact consumption. WebDriver BiDi remains an adapter and evidence source; its navigation ids, user-context ids, event ordering and liveness rules do not become Browser Session policy authority. +Second, the still-open recovery command path originally needed only the aggregate recovery state and an adapter-defined operation. `RecoveryContextOperationRequest` then copied **both complete recovery ledgers** into the adapter request. That meant one recovery command could reach the retained adapter without naming the exact unresolved fact that justified it and could observe unrelated sibling facts. The current repair requires a Browser Session-issued current `RecoveryFact` for every operation and sends only that selected fact to the adapter. -The implemented settlement boundary preserves these invariants: +## Constraints and invariants -- one opaque Browser Session-issued `RecoveryFact` addresses exactly one current recovery fact; -- the handle is bound to the exact Browser Session id, process-local incarnation, ledger kind, index and current recovery-ledger revision; -- foreign session/incarnation handles fail before adapter proof-verification I/O; -- a successful settlement increments the revision, so every handle issued before that mutation becomes stale before adapter I/O; -- proof verification runs through the exact retained adapter via `RecoverySettlementPort`, but successful verification retires only the fact named by the already validated handle; -- failed proof verification leaves lifecycle state and both recovery ledgers unchanged; -- identity-oriented `BrowserSessionRecoveryEvidence` and transaction-oriented `DisposableContextCreateRecoveryEvidence` remain separately addressable and separately retired; -- settling an owned-context fact retires only the exact matching uncertain hot-ownership record; candidate/partial-create evidence never consumes an independently owned context merely because remote values alias; -- partial settlement preserves every unrelated sibling fact; -- when both recovery ledgers and uncertain ownership are empty, the aggregate reaches terminal `Ended`; it never recreates `Active`, `PresentationMutationAuthority`, navigation authority, normal create authority, an ordinary lifecycle owner, or a usable generic recovery-command capability; -- a terminal recovery-operation attempt is rejected as `RecoveryClosed` before the retained adapter is called. +Browser Session owns deterministic lifecycle state and exact fact validation/consumption. WebDriver BiDi remains an adapter and evidence source; protocol ids, tuple state, event ordering, command ACKs and liveness conclusions do not become Browser Session policy authority. + +The implementation preserves these invariants: + +- one opaque `RecoveryFact` addresses exactly one current identity-oriented or create-attempt recovery fact; +- the handle binds Browser Session id, process-local incarnation, ledger kind, index and current recovery revision; +- every generic recovery operation supplies a current `RecoveryFact` plus adapter-defined operation; +- foreign operation facts fail as `RecoveryContextOperationError::AuthorityMismatch` before adapter I/O; +- stale, replayed, shifted or out-of-range operation facts fail as `RecoveryContextOperationError::StaleFact` before adapter I/O; +- `RecoveryContextOperationRequest` contains only the selected fact. It never snapshots sibling recovery vectors; +- operation success/failure leaves lifecycle state and both recovery ledgers unchanged; +- the same current fact may authorize retry attempts until settlement advances the revision; +- settlement uses the same session/incarnation/revision/exact-address validation before proof-verifier I/O; +- successful settlement increments the revision, invalidating every handle issued before that mutation; +- failed proof verification leaves lifecycle state and both ledgers unchanged; +- identity-oriented `BrowserSessionRecoveryEvidence` and transaction-oriented `DisposableContextCreateRecoveryEvidence` remain separately addressable and retired; +- owned-context settlement retires only the exact matching uncertain hot record; candidate/partial-create evidence cannot consume an independently owned context merely because values alias; +- partial settlement preserves unrelated sibling facts; +- when both recovery ledgers and uncertain ownership are empty, the aggregate reaches terminal `Ended` and never recreates ordinary or generic recovery-command authority; +- terminal operation attempts return `RecoveryClosed` before the retained adapter is called. ## Alternatives rejected Treating `RecoveryContextOperationPort` success as settlement is rejected because transport/protocol command completion is not independent proof of remote destruction or reconciliation. -Keeping `execute_recovery_context_operation` callable after complete settlement is rejected because the retained adapter would remain an ambient browser-I/O capability after its recovery purpose ended. The wrapper may remain as terminal state/evidence custody, but the command route must be inert. +Allowing `execute_recovery_context_operation(operation)` without a fact is rejected because aggregate recovery state alone is too broad to authorize retained-adapter I/O. + +Copying both recovery ledgers into every `RecoveryContextOperationRequest` is rejected because it violates least authority and purpose limitation: the adapter learns sibling uncertainty that the selected operation does not need. + +Keeping generic recovery I/O callable after complete settlement is rejected because the retained adapter would remain an ambient browser-I/O capability after its recovery purpose ended. -Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling fact. A current-revision opaque handle makes stale replay fail closed. +Passing a raw vector index is rejected because removal of one fact can make an old index address a different sibling. A current-revision opaque handle makes index-shift replay fail closed. Allowing the adapter to delete Browser Session evidence directly is rejected because it moves domain ownership truth into an adapter and makes protocol data authoritative over policy state. -Returning from recovery custody to ordinary `BoundBrowserSession

` is rejected because reconciliation must not resurrect create, presentation, navigation or ordinary cleanup authority after uncertainty has crossed the recovery boundary. +Returning from recovery custody to ordinary `BoundBrowserSession

` is rejected because reconciliation must not resurrect create, presentation, navigation or cleanup authority after uncertainty crossed the recovery boundary. ## Test-first contract and source repair -Commit `738ec7d9a6635a8b4b0b9324026c2f0b433b9c77` introduced `crates/originweave-browser-session/tests/recovery_exact_fact_settlement.rs` as a structural RED. It requires: +`recovery_exact_fact_settlement.rs` established proof-bearing exact-fact settlement: sibling preservation, stale replay rejection, foreign-fact pre-I/O rejection, verifier-failure non-mutation, independent identity/create-attempt ledgers, and terminal `Ended` without authority resurrection. -1. two uncertain owned contexts: settling A preserves B; replay of A and a sibling handle issued before the revision change fail before proof I/O; rereading B permits proof verification; a wrong proof does not mutate B; exact B settlement closes only after no recovery facts remain; -2. a fact issued by another Browser Session cannot reach the target session's proof verifier even when the caller possesses the opaque value; -3. uncertain create evidence carried in the identity and create-attempt ledgers requires two independent settlements rather than one broad erase. +`recovery_operation_terminal_closure.rs` established that generic recovery I/O works while a current recovery purpose exists, but final exact-fact settlement closes the command route before another adapter call. -Commit `a99ea6bce6174fa98336f455d2ce2b1634f361b9` added `crates/originweave-browser-session/tests/recovery_operation_terminal_closure.rs` as a focused terminal-capability RED. It proves that a recovery operation is available while uncertainty remains, then settles the only recovery fact, requires terminal `Ended`, and requires the next generic recovery operation to fail as `RecoveryClosed` without a second adapter call. +The current hardening adds `recovery_operation_exact_fact_scope.rs`. Its structural RED required the command API to accept `RecoveryFact`, disclose only the selected fact to `RecoveryContextOperationPort`, reject a fact issued before another settlement as `StaleFact` before operation I/O, and reject a foreign Browser Session fact as `AuthorityMismatch` before operation I/O. -The source repair adds the opaque fact/request/error contract and verifier port in `recovery.rs`, plus crate-private aggregate mutation hooks that retire exact evidence and exact matching uncertain ownership only after proof verification. It also gates `execute_recovery_context_operation` to unresolved recovery states and returns `RecoveryClosed` after terminal settlement. It does not weaken the hostile fixtures and does not reinterpret generic recovery-operation success as proof. +Production `recovery.rs` now shares exact-fact selection logic between command execution and settlement. `RecoveryContextOperationRequest` uses optional selected identity/create-attempt evidence fields rather than full vectors. `recovery_same_adapter_operation.rs` covers both ledger kinds while preserving the rule that command success or failure is non-settling. The terminal fixture now passes the same exact fact to the command before settling it and confirms `RecoveryClosed` takes precedence after terminal closure. -Repository execution remains the next gate. Until the current exact head actually runs and passes repository contracts, rustfmt, locked tests, strict Clippy, rustdoc and production coverage, this is source-level GREEN intent rather than executable GREEN evidence. +Repository execution remains the next gate. Until the current exact head actually runs and passes repository contracts, rustfmt, locked tests, strict Clippy, rustdoc and production coverage, this is source-level repair evidence rather than executable GREEN. ## Validation order -`settle_recovery_fact` evaluates in this order: +For `execute_recovery_context_operation(fact, operation)`: + +1. recovery custody must still be `RecoveryRequired` or `TransportLost`; otherwise `RecoveryClosed`; +2. exact Browser Session id and incarnation must match; +3. recovery revision must still match; +4. the fact must still address a current entry in its ledger; +5. only then is `RecoveryContextOperationRequest` built and the retained adapter invoked. + +Steps 2–4 map to `AuthorityMismatch` or `StaleFact` and occur before adapter I/O. The request carries only the selected fact. Adapter return does not mutate recovery state. + +For `settle_recovery_fact(fact, proof)`: 1. exact Browser Session id and incarnation; -2. current recovery-ledger revision; -3. exact current ledger/index fact and next-revision capacity; +2. current recovery revision and exact current ledger/index fact; +3. next-revision capacity; 4. retained-adapter proof verification; 5. exact evidence retirement and, for owned-context evidence, exact uncertain ownership retirement; 6. monotonic revision advance and terminal `Ended` only when no uncertainty remains. -No adapter call occurs before the first three checks succeed. A failed verifier does not consume the fact. A successful mutation invalidates all previously issued fact handles before another settlement can be accepted. Separately, `execute_recovery_context_operation` first verifies that custody is still `RecoveryRequired` or `TransportLost`; `Ended` returns `RecoveryClosed` before `dispatch_recovery_operation` can touch the adapter. +No proof-verifier call occurs before the exact-fact checks succeed. A failed verifier consumes nothing. A successful mutation invalidates all previously issued fact handles. ## Ownership handoff -#317 owns this generic settlement and terminal-recovery-closure boundary. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, for pending/accepted/quarantined tuple correlation, event replay handling and remote liveness. The dependency order is #317 exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and post-condition evidence. +#317 owns generic same-adapter recovery custody, exact-fact-scoped recovery commands, proof-bearing exact-fact settlement and terminal closure. #316 remains responsible for deciding what WebDriver BiDi observation constitutes acceptable proof, pending/accepted/quarantined tuple correlation, event replay handling, remote liveness and concrete recovery-command semantics. -Any implementation that copies #316 protocol tuple state into Browser Session, consumes mutable sibling source, treats command ACK as proof, or preserves generic adapter I/O after terminal recovery violates this boundary. \ No newline at end of file +The dependency order remains #317 exact-head executable GREEN → #318/#321 acceptance → ordinary non-force #316 adoption → pinned-Chromium recovery and browser-observed post-condition evidence. Any implementation that copies #316 protocol tuple state into Browser Session, treats command ACK as proof, leaks sibling recovery facts into unrelated commands, or preserves generic adapter I/O after terminal recovery violates this boundary. From afd1b70102b8f19482441218a54d4f90bc03fa39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 18:12:58 +0900 Subject: [PATCH 149/632] docs(browser-session): restore canonical gap-baseline ownership --- docs/product-technical-gap-baseline.md | 51 +++++++------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6effd381d..490e46014 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,28 +2,6 @@ This is a dated delivery baseline, not a substitute for the PRD, TRD, roadmap, architecture decisions, or live GitHub state. It keeps buyer-visible gaps, current issues, active pull-request evidence, and commercial completion tracks in one discoverable place. Protected `main` is the implementation boundary: code in an open pull request is not shipped behavior. -## Live continuity note: 2026-09-15 - -- Protected `main` remains signature-valid and protected at `87c4daa1830bac5a5228b6036752ad5633232085`. The complete GitHub search surface reports 134 open pull requests and 17 open non-PR issues. The #317 source-repair lane is intentionally Draft outside short exact-head CI probes; with that repair lane Draft the queue is 121 Draft and 13 non-Draft. GitHub Release inventory remains empty. -- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active production repair preserves create-transaction provenance through `CreateFailedUncertain(Some/None)`, duplicate-candidate rejection, and accepted/rejected completion-settlement failure via `DisposableContextCreateRecoveryEvidence`; removes the arbitrary 4096-byte `browser.UserContext` ceiling; and migrates the two reported atomic `fetch_update` calls to `AtomicU64::try_update` without changing memory ordering or overflow behavior. It also adds one-way same-adapter recovery custody through `BoundBrowserSessionRecovery

` for `RecoveryRequired|TransportLost`, with recovery-only evidence and no path back to raw adapter or ordinary Browser Session authority. -- Proven destruction now retires the exact live hot command-authority record only after authority validation and adapter-proven success. Failed destruction retains the same record as `Uncertain` with exact `UnprovenDestruction { context, context_epoch }` evidence. The hostile acceptance `proven_destroy_releases_hot_ownership.rs` reuses the same raw context identity across 258 generations and requires monotonic epochs plus stale predecessor rejection before lifecycle I/O. These are active-PR claims until one exact head passes repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. -- The exact hostile acceptance `create_recovery_same_handle_distinct_fact.rs` requires attempt 1's accepted ownership and attempt 2's duplicate/unsettled candidate facts to coexist even when their remote handle values are identical. `CreateFailedUncertain(None)` still retains exact aggregate-issued attempt identity, so “no complete handle” is not “no transaction evidence.” -- #318 remains Draft and structurally RED for the Browser Session navigation state machine. Its current base still points to an earlier #317 exact head, so the child is a repair/restack finding rather than a reason to rewrite the production lane concurrently. #321 remains its Draft same-raw-id recreation/ABA acceptance child. Both must ordinary non-force adopt a verified #317 successor; no child acceptance delta may be discarded merely to restore mergeability. -- #316 remains Draft at exact `8ca6c5a190d9ad2b4c7843d440e91f6070d681c2`. It owns WebDriver BiDi correlation/pending→accepted/quarantined protocol truth and must ordinary non-force restack after the Browser Session prerequisite is verified. Browser Session does not absorb protocol tuple storage or command semantics. -- Same-adapter recovery custody and bounded hot ownership/history separation are implemented on the active #317 branch rather than remaining “next gaps.” Durable crash/process-restart persistence remains separate; `abandoned_bound_session_count()` is process-local operability evidence, not destruction proof or durable recovery storage. After exact-head verification, the next Browser Session production slice is the ownership-generation/current-navigation-witness state machine required by #318/#321. -- The immutable W3C WebDriver BiDi Working Draft directly verified for this baseline is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`), which names the 3 September 2026 draft as its previous version. The mutable `/TR/webdriver-bidi/` index currently exposes an older 24 August surface, so immutable dated provenance and mutable-index freshness are recorded separately. Standards freshness does not authorize an automatic runtime repin. -- Current Chrome desktop Stable remains Chrome 153, promoted 2026-09-08 (`153.0.8010.36` on Linux; `.36/.37` on Windows/macOS). #299's older Chrome/ChromeDriver `150.0.7871.129` Agent Task result remains historical RED: 0/3 trials reached navigation because session creation failed. Buyer-current browser acceptance still requires explicitly qualified real navigation, interaction, browser-observed post-condition, destruction, and cleanup evidence; a command ACK or wrapper drop is not success. -- The active organization ruleset `18156473` still requires one counted approval, stale-review dismissal on push, resolved review threads, additional approval for unattributed changes, seven central required workflows, and deletion/non-fast-forward protection. Administrative bypass exists but is not a normal delivery path and does not justify self-approval, stale-head promotion, or gate weakening. - -## Live continuity note: 2026-09-11 - -- Protected `main` remains signature-valid at `87c4daa1830bac5a5228b6036752ad5633232085`. The live repository sweep found 132 open pull requests and 16 open non-PR issues; no release or tag exists. Active-PR work below is evidence only and is not protected-main behavior. -- Browser Session foundation #317 remains stacked on #229 exact `6d87dff5dc572fbd74d06309d574a998f23cf02f`. The active branch now keeps one concrete lifecycle adapter structurally bound, uses aggregate-issued per-create transaction identity, routes typed post-create operations through the same consumed adapter after current authority validation, redacts adapter `Debug`, and preserves exact transport-loss recovery handles. The current repair additionally preserves every indirectly invalidated active sibling as non-authorizing `RecoveryRequiredOwnedHandle` and changes `finish(&mut self)` so a rejected normal completion retains the same bound owner for cleanup/reconciliation and retry. These claims remain active-PR claims until the successor exact head passes repository contracts, canonical formatting, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. -- #316 remains Draft at exact `8ca6c5a190d9ad2b4c7843d440e91f6070d681c2`. It must non-force restack only after a verified #317 successor and continues to own WebDriver BiDi-specific pending/accepted/quarantined tuples, command semantics, fresh-authority-at-I/O, and remote-liveness reconciliation. Browser Session must not absorb that protocol truth. -- Durable crash/process-restart persistence of exact recovery evidence remains open. `abandoned_bound_session_count()` is only a process-local non-I/O operability signal; it is not destruction proof or durable recovery storage. -- W3C's latest-published WebDriver BiDi document verified for this baseline is the **24 August 2026 Working Draft** (`WD-webdriver-bidi-20260824`). Standards freshness does not authorize an automatic runtime repin. -- #299's Chrome/ChromeDriver `150.0.7871.129` Agent Task result remains historical RED: 0/3 trials reached navigation because session creation failed. Current desktop Stable was promoted on 2026-09-08 as Chrome 153 (`153.0.8010.36` on Linux; `.36/.37` on Windows/macOS). Buyer-current browser acceptance therefore still requires a separately controlled current-Stable qualification with real navigation, interaction, observed post-condition, destruction, and cleanup evidence; a command ACK or wrapper drop is not success. - ## Live continuity note: 2026-09-09 - Protected `main` was re-fetched at `87c4daa1830bac5a5228b6036752ad5633232085`. Issue #292 remains open; its buyer-visible acceptance is still pinned Chromium application followed by page-observed and post-cleanup evidence. @@ -52,7 +30,7 @@ The interactive maintenance loop performed the following verified state changes |---|---| | Supersession closure | #153 closed with replacement evidence: base-stack tip (`4da223ac`) already implements `_terminate_owned_process_bounded` exit-race tolerance that supersedes the branch delta | | Conflict reconciliation | Merge commits pushed to #37 (`27f6acd6`, ci.yml aligned to reviewed `nightly-2026-08-18` pin), #149 (`7852a540` + rustfmt fix `54f96008`), #152 (`65b0c705`), #173 (`ecc9574a`), #175 (`765c88f6`, keeps `crate_root.rs` naming) | -| Governance remediation (#212) | #43 reconciled with main in `04e262d5`; the `chrome_sandbox` workflow mutation was first removed, then restored under recorded independent authorization (issue #212 option (b)) because the PR's own contract test fails closed without it; fresh exact-head checks re-ran on the restored branch | +| Governance remediation (#212) | #43 reconciled with main in `04e262d5`; the `chrome_sandbox` workflow mutation was first removed, then restored under recorded independent authorization (issue #212 option (b)) because the PR's own contract test fails closed without it; fresh exact-head checks re-ran on the restored head | | Security finding fix (#124) | Strix vuln-0001 (Unicode homoglyph path confusion, MEDIUM) remediated in `30cc458b`: audited workflow paths now restricted to a canonical ASCII alphabet with homoglyph/fraction-slash/fullwidth regression contract tests; CHANGELOG updated | | Fail-closed provider re-dispatch | ~21 failed Strix required-check runs re-dispatched on unchanged exact heads; completed reruns returned success on #46, #48, #156, #157, #159, #218, and #219 heads at snapshot time; cancellations only where newer heads superseded the run | | Current-head review re-dispatch | Central merge-scheduler dispatches sent for #47, #62, #63, #65, #74, #166, #173, #175, and #220 because their stale `CHANGES_REQUESTED` verdicts cited coverage-evidence results that are green on the same heads today | @@ -103,9 +81,9 @@ The following rows were current on 2026-08-24 and are retained only as regressio | PR | State | Exact base head | Exact head | |---|---|---|---| | #222 | Draft | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | `1e2ce3d4071a1a75ee891bdcd71c506b3b50d4bc` | -| #221 | Draft | `8145d40d5470a7753b8211907c190367f742f2f12` | `6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f` | +| #221 | Draft | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | `6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f` | | #220 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `ed4cab16cf88c76ce1c145a22d0a274ef2d57263` | -| #219 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `8145d40a279686a28309d59b8b3b9bfbd283a80` | +| #219 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | | #218 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `49e98fba6974219b3bb0336c822b12667f1e1c03` | | #217 | Draft | `529d11a3571f6b1834b9baa49ef67eb08f043978` | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | | #216 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `75130851a0f7ce528a7a36382eb026ac7942a0aa` | @@ -135,7 +113,7 @@ The current queue must be processed in dependency order. A green child branch ca ### Review and merge authority -The active `CWL Central required workflows` ruleset (re-fetched for this snapshot) requires one approving review, resolved review threads, no last-push approval requirement, `merge`/`squash` merge methods, and seven configured required workflows (`opencode-review`, `pr-review-merge-scheduler`, `security-scan`, `strix`, `sast-semgrep`, `noema-review`, `codeql-pr`). The current collaborator inventory contains only `seonghobae` with administration and push permissions, creating a **reviewer-provisioning gap** for counted non-author approval. +The active `CWL Central required workflows` ruleset (re-fetched for this snapshot) requires one approving review, resolved review threads, no last-push approval requirement, `merge`/`squash` merge methods, and seven configured required workflows (`close-empty-pr`, `opencode-review`, `pr-review-merge-scheduler`, `security-scan`, `strix`, `sast-semgrep`, `noema-review`). The current collaborator inventory contains only `seonghobae` with administration and push permissions, creating a **reviewer-provisioning gap** for counted non-author approval. This gap does not authorize self-approval, stale-head merges, administrative bypass, or weaker checks. Because the current GitHub ruleset independently requires a counted approval, the solo-maintainer hold does not satisfy the live merge gate: an eligible non-author collaborator must submit a formal `APPROVED` review on the current head. Until that reviewer-provisioning gap is repaired, protected-main merges stop even when exact-head checks, security gates, complete coverage, rustdoc/Clippy, threads, and AI-review evidence are otherwise complete. Before any merge decision, re-fetch the exact ruleset, collaborators, PR head/base, reviews, unresolved threads, and required checks; do not assume this dated observation remains current. @@ -176,7 +154,7 @@ The hourly product-development loop is operational infrastructure, not proof tha | P1 | Buyers can install, update, verify, and roll back a supported product | **Not shipped** | #201; signed Windows/macOS/Linux/headless artifacts, Chromium revision manifest, updater security, patch SLA, SBOM, SLSA provenance, and recovery | | P1 | Enterprise teams can provision, approve, audit, operate, and recover the service | **Not shipped** | #202; Keyverse-compatible OIDC/SCIM, tenant isolation, policy/approval/evidence UI, SLO/incident controls, data residency, CSAP/SOC 2 evidence mapping, WCAG 2.2, Figma File ID, and Storybook | | P0 | A release has reproducible proof of usefulness, safety, evidence completeness, and recovery | **No product-wide release gate** | #203; deterministic, compatibility, adversarial, recovery, and enterprise suites with statistical reporting and an exact-artifact commercial acceptance gate | -| P0 | Valid changes reach protected `main` without authority improvisation or unbounded stack growth | **Blocked / high integration debt** | Shrink the live PR queue in dependency order, provision legitimate review authority, require exact-current evidence, and close duplicates/superseded branches only with verified successor coverage | +| P0 | Valid changes reach protected `main` without authority improvisation or unbounded stack growth | **Blocked / high integration debt** | Shrink the 126-PR queue in dependency order, provision legitimate review authority, require exact-current evidence, and close duplicates/superseded branches | ## Commercial completion definition @@ -195,16 +173,15 @@ OriginWeave is not complete merely because every low-level primitive exists in s ## Next executable queue -1. Finish #317's Browser Session prerequisite in its single-writer lane by obtaining runner-backed exact-head repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at exactly 100%, and fresh independent review. Same-adapter recovery custody and bounded hot ownership are already implemented on the active branch and must not be reopened as planned work without a new failing contract. -2. After #317 is verified, ordinary non-force restack #318 and #321 while preserving every valid acceptance delta; then implement the Browser Session ownership-generation/current-navigation-witness production state machine required by those RED contracts. Restack #316 afterward and keep its BiDi pending→accepted/quarantined correlation and remote-liveness reconciliation in the protocol-owner lane. -3. Re-run the explicitly qualified real-Chromium acceptance (#299/#320 path): navigation, interaction, download/lifecycle transitions where applicable, page/browser-observed post-conditions, destruction, crash/cleanup, and stale-event replay. Command ACK alone is non-passing. -4. Drain the remaining merge gate in dependency order: every ready root PR needs current exact-head checks, resolved threads, and the current ruleset's counted `APPROVED` review from an eligible non-author collaborator. OpenCode/CodeRabbit narrative evidence does not substitute for that GitHub review. -5. Finish #27 and #10 as separate security tracks; neither should be hidden inside the browser-session or first real-browser PR. -6. Implement #199, then #200, so durable evidence and stable task authority precede broad enterprise integrations. -7. Implement #201 before making release/support claims; exact CI browser evidence must be bound to the actual signed artifact. -8. Design #202 in Figma, record the Figma File ID in the ADR, implement reusable design tokens and Storybook components, then add identity/tenant/approval/audit/operations integration. -9. Make #203 the final release gate across the exact signed distribution, not a source branch or model narrative. -10. Only after the commercial acceptance gate passes, increment the version, finalize CHANGELOG/release notes, publish signed artifacts, and verify upgrade/rollback from the prior supported release. +1. Drain the merge gate in dependency order: for every ready root PR whose current head is check-green with resolved threads, obtain the current ruleset's counted `APPROVED` review from an eligible non-author collaborator; OpenCode approval or skip evidence does not substitute for that GitHub review. If no eligible approver exists, record the reviewer-provisioning gap and do not merge. Root candidates include #37, #40, #43, #45–#48, #51, #62–#65, #74, #82, #124, #149, #152, #156–#166, #170, #173, #175, #208, #209, #218, and #219 as their re-dispatched checks land. Treat dependent children separately: only after a predecessor reaches protected `main`, retarget and independently revalidate its immediate child; preserve orders such as #218 → #221 → #220 rather than treating #208–#220 as a flat merge range. +2. Keep the organization review pipeline healthy: monitor the central Actions backlog recorded above; if OpenCode reviews stop landing on OriginWeave heads while the queue is idle, repair `ContextualWisdomLab/.github` dispatch/concurrency configuration rather than weakening any gate. +3. Finish the #9/#28 browser-network and Chromium vertical slice, including the #181–#205 WebSocket opening path and framed BiDi command/response stack, then semantic observation, policy, action, post-condition, and recovery boundaries on protected `main`. +4. Finish #27 and #10 as separate security tracks; neither should be hidden inside the first browser PR. +5. Implement #199, then #200, so durable evidence and stable task authority precede broad enterprise integrations. +6. Implement #201 before making release/support claims; exact CI browser evidence must be bound to the actual signed artifact. +7. Design #202 in Figma, record the Figma File ID in the ADR, implement reusable design tokens and Storybook components, then add identity/tenant/approval/audit/operations integration. +8. Make #203 the final release gate across the exact signed distribution, not a source branch or model narrative. +9. Only after the commercial acceptance gate passes, increment the version, finalize CHANGELOG/release notes, publish signed artifacts, and verify upgrade/rollback from the prior supported release. ## Evidence commands From 91ea230874dcec51f869c7198461760a3fe2f8ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 19:03:27 +0900 Subject: [PATCH 150/632] test(browser-session): redact read-only view diagnostics --- .../browser_session_view_debug_redaction.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 crates/originweave-browser-session/tests/browser_session_view_debug_redaction.rs diff --git a/crates/originweave-browser-session/tests/browser_session_view_debug_redaction.rs b/crates/originweave-browser-session/tests/browser_session_view_debug_redaction.rs new file mode 100644 index 000000000..7e36b6037 --- /dev/null +++ b/crates/originweave-browser-session/tests/browser_session_view_debug_redaction.rs @@ -0,0 +1,58 @@ +use originweave_browser_session::{ + BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError, + DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError, + DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort, + DisposableIsolationId, +}; +use originweave_core::{BrowserSessionId, BrowsingContextId}; + +struct DebugExposurePort; + +impl DisposableContextPort for DebugExposurePort { + fn create_disposable_context( + &mut self, + _request: &DisposableContextCreateRequest, + ) -> Result { + Ok(DisposableContextHandle::new( + DisposableIsolationId::parse("browser-session-debug-secret-isolation") + .expect("lossless isolation id"), + BrowsingContextId::new(777).expect("valid browsing context"), + )) + } + + fn complete_disposable_context_creation( + &mut self, + _completion: &DisposableContextCreateCompletion, + ) -> Result<(), DisposableContextCreateCompletionError> { + Ok(()) + } + + fn destroy_disposable_context( + &mut self, + _request: &DisposableContextDestroyRequest, + ) -> Result<(), DisposableContextDestroyError> { + Ok(()) + } +} + +#[test] +fn read_only_browser_session_view_debug_does_not_expose_remote_identity() { + let session = BrowserSession::start(BrowserSessionId::new(777).expect("valid session id")) + .expect("incarnation capacity"); + let mut bound = session.bind_lifecycle_port(DebugExposurePort); + bound + .create_disposable_context() + .expect("accepted disposable context"); + + let bound_debug = format!("{bound:?}"); + assert!( + !bound_debug.contains("browser-session-debug-secret-isolation"), + "the bound wrapper already promises redacted diagnostics" + ); + + let read_only_debug = format!("{:?}", bound.browser_session()); + assert!( + !read_only_debug.contains("browser-session-debug-secret-isolation"), + "the public read-only Browser Session view must not bypass BoundBrowserSession debug redaction and disclose exact remote lifecycle identity" + ); +} From 9f5d0687d860306909385175c39edc8bc9297609 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 19:31:37 +0900 Subject: [PATCH 151/632] fix(browser-session): redact aggregate debug diagnostics --- .../src/browser_session.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/originweave-browser-session/src/browser_session.rs b/crates/originweave-browser-session/src/browser_session.rs index 7d03c1a41..007858bb7 100644 --- a/crates/originweave-browser-session/src/browser_session.rs +++ b/crates/originweave-browser-session/src/browser_session.rs @@ -583,7 +583,6 @@ struct OwnedContextRecord { } /// Aggregate root for disposable browser-context lifecycle and presentation mutation authority. -#[derive(Debug)] pub struct BrowserSession { id: BrowserSessionId, incarnation: BrowserSessionIncarnation, @@ -596,6 +595,24 @@ pub struct BrowserSession { create_recovery_evidence: Vec, } +impl fmt::Debug for BrowserSession { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("BrowserSession") + .field("browser_session", &self.id) + .field("incarnation", &self.incarnation) + .field("state", &self.state) + .field("transport_lost", &self.transport_lost) + .field("owned_context_count", &self.contexts.len()) + .field("recovery_evidence_count", &self.recovery_evidence.len()) + .field( + "create_recovery_evidence_count", + &self.create_recovery_evidence.len(), + ) + .finish() + } +} + /// Browser Session composed with the one lifecycle-port instance allowed to mutate its remote state. /// /// Construction consumes both the aggregate and the concrete port. The port is not exposed mutably and From 6a0d0d03595d62f1d98dccea32f5e4a7a6265d22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:02:46 +0900 Subject: [PATCH 152/632] test(browser-session): pin current WebDriver BiDi publication trace --- ...ession_webdriver_bidi_publication_trace.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_browser_session_webdriver_bidi_publication_trace.py diff --git a/tests/test_browser_session_webdriver_bidi_publication_trace.py b/tests/test_browser_session_webdriver_bidi_publication_trace.py new file mode 100644 index 000000000..e9509900a --- /dev/null +++ b/tests/test_browser_session_webdriver_bidi_publication_trace.py @@ -0,0 +1,38 @@ +"""Repository contract for Browser Session WebDriver BiDi publication provenance.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +ADR = ROOT / "docs/adr/0114-browser-session-disposable-context-authority.md" +TRACE = ROOT / "docs/traceability/browser-session-lifecycle-authority.md" + + +class BrowserSessionWebDriverBidiPublicationTraceTests(unittest.TestCase): + """Keep dated W3C publication provenance separate from mutable editor/runtime state.""" + + def test_current_and_previous_published_working_drafts_are_explicit(self) -> None: + adr = ADR.read_text(encoding="utf-8") + trace = TRACE.read_text(encoding="utf-8") + + for document in (adr, trace): + self.assertIn("WD-webdriver-bidi-20260914", document) + self.assertIn("WD-webdriver-bidi-20260909", document) + self.assertIn("https://w3c.github.io/webdriver-bidi/", document) + + def test_current_publication_is_not_described_as_editor_draft(self) -> None: + adr = ADR.read_text(encoding="utf-8") + trace = TRACE.read_text(encoding="utf-8") + + self.assertIn("14 September 2026", adr) + self.assertIn("14 September 2026", trace) + self.assertIn("previous", adr.lower()) + self.assertIn("previous", trace.lower()) + self.assertIn("runtime", adr.lower()) + self.assertIn("runtime", trace.lower()) + + +if __name__ == "__main__": + unittest.main() From af3fac7db7efdc681f04cf0089c7105f574f51cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:03:32 +0900 Subject: [PATCH 153/632] docs(browser-session): trace WebDriver BiDi 2026-09-14 WD --- .../0114-browser-session-disposable-context-authority.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 415b3bdfc..86ec5120c 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -2,7 +2,7 @@ - Status: Proposed - Date: 2026-09-10 -- Last code-current review: 2026-09-15 +- Last code-current review: 2026-09-16 ## Context @@ -12,7 +12,7 @@ The active implementation has to satisfy four constraints at once. First, `Bound Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Two lifecycle facts can have identical remote handle values while belonging to different create attempts; raw-handle equality must not collapse an accepted owner and a later rejected/unsettled candidate. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. -The immutable WebDriver BiDi Working Draft directly verified on 2026-09-15 is the 9 September 2026 publication (`WD-webdriver-bidi-20260909`), with 3 September 2026 as the previous version. The mutable `/TR/webdriver-bidi/` index currently lags that dated publication, so dated provenance is retained from the immutable URI rather than inferred from the mutable index. The publication defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. It does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. Runtime-qualified protocol/browser revisions remain separately controlled and are not repinned by this standards-trace update. +The authoritative W3C TR directly verified on 2026-09-16 identifies **14 September 2026** as the current published WebDriver BiDi Working Draft (`WD-webdriver-bidi-20260914`), with **9 September 2026** (`WD-webdriver-bidi-20260909`) as the previous published version. The Editor's Draft remains separately mutable at https://w3c.github.io/webdriver-bidi/. Publication provenance therefore stays distinct from the separately runtime-qualified Chromium/protocol compatibility pin; this standards-trace update does not repin runtime behavior. The publication defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. It does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. ## Decision drivers @@ -185,4 +185,4 @@ Supersede this ADR if the browser platform provides a complete, queryable, gener ## References -Browser Testing and Tools Working Group. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +Browser Testing and Tools Working Group. (2026, September 14). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/ From c9ebac3ebe99f5d36e9cd7333b9d831416f8be7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:04:05 +0900 Subject: [PATCH 154/632] docs(browser-session): currentize WebDriver BiDi publication trace --- docs/traceability/browser-session-lifecycle-authority.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index 7b1ce46f4..c850316c2 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -123,7 +123,7 @@ Presentation mutation and lifecycle cleanup are separate. A navigation-invalidat `DisposableIsolationId` maps to WebDriver BiDi `browser.UserContext` and preserves protocol text losslessly. OriginWeave does not normalize that address or treat it as command authority. -The immutable W3C WebDriver BiDi Working Draft verified for this lineage is the **9 September 2026** publication (`WD-webdriver-bidi-20260909`). The mutable `/TR/webdriver-bidi/` index and separately qualified Chromium runtime revision are distinct provenance axes. A command acknowledgement is insufficient proof that a disposable boundary is gone. +The authoritative W3C TR verified for this lineage on 2026-09-16 identifies **14 September 2026** as the current published WebDriver BiDi Working Draft (`WD-webdriver-bidi-20260914`) and **9 September 2026** (`WD-webdriver-bidi-20260909`) as the previous published version. The mutable Editor's Draft remains separate at https://w3c.github.io/webdriver-bidi/, and the Chromium/runtime compatibility revision is a third, independently qualified provenance axis. A command acknowledgement is insufficient proof that a disposable boundary is gone. ## Source and executable evidence @@ -146,6 +146,7 @@ The immutable W3C WebDriver BiDi Working Draft verified for this lineage is the | proven destruction bounds hot ownership | `proven_destroy_releases_hot_ownership.rs` | | recovery custody cannot regain ordinary authority | recovery rustdoc `compile_fail`; repository contracts | | navigation witness is opaque and generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | +| current vs previous W3C publication provenance | `tests/test_browser_session_webdriver_bidi_publication_trace.py`; ADR 0114; this trace | Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source remains non-shipment until exact-head repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review/security gates, and protected `main` integration are observed. From 747770246346baf6491586005346bfeff45b5af0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:08:50 +0900 Subject: [PATCH 155/632] test(browser-session): strengthen WebDriver BiDi provenance contract --- ...ession_webdriver_bidi_publication_trace.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_browser_session_webdriver_bidi_publication_trace.py b/tests/test_browser_session_webdriver_bidi_publication_trace.py index e9509900a..d7a834e82 100644 --- a/tests/test_browser_session_webdriver_bidi_publication_trace.py +++ b/tests/test_browser_session_webdriver_bidi_publication_trace.py @@ -13,7 +13,7 @@ class BrowserSessionWebDriverBidiPublicationTraceTests(unittest.TestCase): """Keep dated W3C publication provenance separate from mutable editor/runtime state.""" - def test_current_and_previous_published_working_drafts_are_explicit(self) -> None: + def test_current_previous_and_editor_provenance_are_distinct(self) -> None: adr = ADR.read_text(encoding="utf-8") trace = TRACE.read_text(encoding="utf-8") @@ -21,17 +21,25 @@ def test_current_and_previous_published_working_drafts_are_explicit(self) -> Non self.assertIn("WD-webdriver-bidi-20260914", document) self.assertIn("WD-webdriver-bidi-20260909", document) self.assertIn("https://w3c.github.io/webdriver-bidi/", document) - - def test_current_publication_is_not_described_as_editor_draft(self) -> None: + self.assertRegex( + document, + r"14 September 2026.*current published WebDriver BiDi Working Draft", + ) + self.assertRegex( + document, + r"WD-webdriver-bidi-20260909.*previous published version", + ) + self.assertRegex( + document, + r"Editor(?:'s|’s) Draft.*https://w3c.github.io/webdriver-bidi/", + ) + + def test_publication_refresh_does_not_claim_runtime_repin(self) -> None: adr = ADR.read_text(encoding="utf-8") trace = TRACE.read_text(encoding="utf-8") - self.assertIn("14 September 2026", adr) - self.assertIn("14 September 2026", trace) - self.assertIn("previous", adr.lower()) - self.assertIn("previous", trace.lower()) - self.assertIn("runtime", adr.lower()) - self.assertIn("runtime", trace.lower()) + self.assertIn("does not repin runtime behavior", adr) + self.assertIn("runtime compatibility revision is a third", trace) if __name__ == "__main__": From c32d44dc1c33b1e656eb685eed0e02d3506fcdae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:05:03 +0900 Subject: [PATCH 156/632] test(browser-session): reject duplicate BiDi publication writer --- ...ession_webdriver_bidi_publication_trace.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/test_browser_session_webdriver_bidi_publication_trace.py b/tests/test_browser_session_webdriver_bidi_publication_trace.py index d7a834e82..042385aaf 100644 --- a/tests/test_browser_session_webdriver_bidi_publication_trace.py +++ b/tests/test_browser_session_webdriver_bidi_publication_trace.py @@ -1,45 +1,46 @@ -"""Repository contract for Browser Session WebDriver BiDi publication provenance.""" +"""Repository contract for Browser Session's single-writer WebDriver BiDi provenance.""" from __future__ import annotations import pathlib +import re import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] ADR = ROOT / "docs/adr/0114-browser-session-disposable-context-authority.md" TRACE = ROOT / "docs/traceability/browser-session-lifecycle-authority.md" +CANONICAL_RECEIPT = "docs/traceability/webdriver-bidi-publication-current.md" +DATED_TR = re.compile(r"WD-webdriver-bidi-\d{8}") +VOLATILE_CURRENTNESS = re.compile( + r"(?:current|latest|previous) published WebDriver BiDi Working Draft", + re.IGNORECASE, +) class BrowserSessionWebDriverBidiPublicationTraceTests(unittest.TestCase): - """Keep dated W3C publication provenance separate from mutable editor/runtime state.""" + """Keep standards freshness with the canonical originweave-bidi owner.""" - def test_current_previous_and_editor_provenance_are_distinct(self) -> None: + def test_browser_session_references_canonical_publication_receipt(self) -> None: adr = ADR.read_text(encoding="utf-8") trace = TRACE.read_text(encoding="utf-8") for document in (adr, trace): - self.assertIn("WD-webdriver-bidi-20260914", document) - self.assertIn("WD-webdriver-bidi-20260909", document) - self.assertIn("https://w3c.github.io/webdriver-bidi/", document) - self.assertRegex( - document, - r"14 September 2026.*current published WebDriver BiDi Working Draft", - ) - self.assertRegex( - document, - r"WD-webdriver-bidi-20260909.*previous published version", - ) - self.assertRegex( - document, - r"Editor(?:'s|’s) Draft.*https://w3c.github.io/webdriver-bidi/", - ) - - def test_publication_refresh_does_not_claim_runtime_repin(self) -> None: + self.assertIn(CANONICAL_RECEIPT, document) + self.assertIsNone(DATED_TR.search(document)) + self.assertIsNone(VOLATILE_CURRENTNESS.search(document)) + + def test_browser_session_keeps_only_lifecycle_relevant_standard_semantics(self) -> None: adr = ADR.read_text(encoding="utf-8") trace = TRACE.read_text(encoding="utf-8") - self.assertIn("does not repin runtime behavior", adr) - self.assertIn("runtime compatibility revision is a third", trace) + self.assertIn("browser.UserContext", adr) + self.assertIn("browser.createUserContext", adr) + self.assertIn("browsingContext.create", adr) + self.assertIn("browser.removeUserContext", adr) + self.assertIn("command ACK alone is not destruction proof", adr) + self.assertIn("command acknowledgement is insufficient proof", trace) + self.assertIn("runtime compatibility", adr) + self.assertIn("runtime compatibility", trace) if __name__ == "__main__": From 0b277f684c7f494bdc399b5e4b862982e634bedd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:14:44 +0900 Subject: [PATCH 157/632] docs(browser-session): reference canonical BiDi publication receipt --- .../0114-browser-session-disposable-context-authority.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index 86ec5120c..c1f9af684 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -2,7 +2,7 @@ - Status: Proposed - Date: 2026-09-10 -- Last code-current review: 2026-09-16 +- Last code-current review: 2026-09-17 ## Context @@ -12,7 +12,7 @@ The active implementation has to satisfy four constraints at once. First, `Bound Lifecycle failures require lossless evidence while the aggregate remains available. A BiDi adapter can successfully create a user context before later browsing-context creation or verification becomes uncertain. Duplicate adapter output can expose an offending handle that must not be silently discarded or automatically destroyed. Two lifecycle facts can have identical remote handle values while belonging to different create attempts; raw-handle equality must not collapse an accepted owner and a later rejected/unsettled candidate. Destruction can fail without proving that the exact isolation boundary is gone. A failure on one owned context can force all other active siblings into uncertainty, so those sibling handles also have to remain enumerable. Transport liveness remains orthogonal to ownership certainty. -The authoritative W3C TR directly verified on 2026-09-16 identifies **14 September 2026** as the current published WebDriver BiDi Working Draft (`WD-webdriver-bidi-20260914`), with **9 September 2026** (`WD-webdriver-bidi-20260909`) as the previous published version. The Editor's Draft remains separately mutable at https://w3c.github.io/webdriver-bidi/. Publication provenance therefore stays distinct from the separately runtime-qualified Chromium/protocol compatibility pin; this standards-trace update does not repin runtime behavior. The publication defines `browser.UserContext` as `text`, `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`. It does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. +WebDriver BiDi publication freshness is owned by the canonical `originweave-bidi` receipt at `docs/traceability/webdriver-bidi-publication-current.md`; Browser Session does not restate dated Working Draft currentness. Runtime compatibility remains independently qualified and publication churn does not repin runtime behavior. The canonical WebDriver BiDi contract defines `browser.UserContext` as `text` and defines `browser.createUserContext`, `browsingContext.create`, and `browser.removeUserContext`; it does not define a 4096-byte identifier limit. These identifiers and commands remain adapter capabilities/addressability rather than OriginWeave policy authority, and command ACK alone is not destruction proof. ## Decision drivers @@ -185,4 +185,6 @@ Supersede this ADR if the browser platform provides a complete, queryable, gener ## References -Browser Testing and Tools Working Group. (2026, September 14). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/ +Browser Testing and Tools Working Group. (2026). *WebDriver BiDi*. World Wide Web Consortium. https://www.w3.org/TR/webdriver-bidi/ + +OriginWeave. (2026). *WebDriver BiDi publication-current receipt*. `docs/traceability/webdriver-bidi-publication-current.md` From e23c15ef04a97cb33d09280fc5570d28c94052ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:15:25 +0900 Subject: [PATCH 158/632] docs(browser-session): remove duplicate BiDi currentness writer --- docs/traceability/browser-session-lifecycle-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-lifecycle-authority.md b/docs/traceability/browser-session-lifecycle-authority.md index c850316c2..c77d519e6 100644 --- a/docs/traceability/browser-session-lifecycle-authority.md +++ b/docs/traceability/browser-session-lifecycle-authority.md @@ -123,7 +123,7 @@ Presentation mutation and lifecycle cleanup are separate. A navigation-invalidat `DisposableIsolationId` maps to WebDriver BiDi `browser.UserContext` and preserves protocol text losslessly. OriginWeave does not normalize that address or treat it as command authority. -The authoritative W3C TR verified for this lineage on 2026-09-16 identifies **14 September 2026** as the current published WebDriver BiDi Working Draft (`WD-webdriver-bidi-20260914`) and **9 September 2026** (`WD-webdriver-bidi-20260909`) as the previous published version. The mutable Editor's Draft remains separate at https://w3c.github.io/webdriver-bidi/, and the Chromium/runtime compatibility revision is a third, independently qualified provenance axis. A command acknowledgement is insufficient proof that a disposable boundary is gone. +WebDriver BiDi publication freshness is referenced through the canonical owner receipt `docs/traceability/webdriver-bidi-publication-current.md`; this Browser Session trace does not restate dated Working Draft currentness. Chromium/runtime compatibility remains independently qualified from publication metadata. A command acknowledgement is insufficient proof that a disposable boundary is gone. ## Source and executable evidence @@ -146,7 +146,7 @@ The authoritative W3C TR verified for this lineage on 2026-09-16 identifies **14 | proven destruction bounds hot ownership | `proven_destroy_releases_hot_ownership.rs` | | recovery custody cannot regain ordinary authority | recovery rustdoc `compile_fail`; repository contracts | | navigation witness is opaque and generation-bound | navigation owner tests; `tests/test_browser_session_navigation_owner_surface_contract.py` | -| current vs previous W3C publication provenance | `tests/test_browser_session_webdriver_bidi_publication_trace.py`; ADR 0114; this trace | +| canonical W3C publication receipt boundary | `tests/test_browser_session_webdriver_bidi_publication_trace.py`; `docs/traceability/webdriver-bidi-publication-current.md`; ADR 0114 | Historical predecessor CI/review receipts do not transfer to the current head. Active-PR source remains non-shipment until exact-head repository contracts, canonical rustfmt, locked tests, strict Clippy, rustdoc/API docs, production function/line/region/branch coverage at 100%, required review/security gates, and protected `main` integration are observed. From 14396b175ffa410565d763e0141717fa7564c875 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:16:38 +0900 Subject: [PATCH 159/632] test(browser-session): make runtime wording case-insensitive --- .../test_browser_session_webdriver_bidi_publication_trace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_webdriver_bidi_publication_trace.py b/tests/test_browser_session_webdriver_bidi_publication_trace.py index 042385aaf..5c24ab350 100644 --- a/tests/test_browser_session_webdriver_bidi_publication_trace.py +++ b/tests/test_browser_session_webdriver_bidi_publication_trace.py @@ -39,8 +39,8 @@ def test_browser_session_keeps_only_lifecycle_relevant_standard_semantics(self) self.assertIn("browser.removeUserContext", adr) self.assertIn("command ACK alone is not destruction proof", adr) self.assertIn("command acknowledgement is insufficient proof", trace) - self.assertIn("runtime compatibility", adr) - self.assertIn("runtime compatibility", trace) + self.assertIn("runtime compatibility", adr.lower()) + self.assertIn("runtime compatibility", trace.lower()) if __name__ == "__main__": From 5ba90da78ef97a63deaf14704f76bb1767bba265 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:23:23 +0900 Subject: [PATCH 160/632] test(browser-session): consume canonical BiDi receipt in lifecycle contract --- tests/test_browser_session_lifecycle_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 97c48c362..4a0129567 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -278,7 +278,7 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: ) for token in ( "Status: Proposed", - "WD-webdriver-bidi-20260909", + "docs/traceability/webdriver-bidi-publication-current.md", "RecoveryRequired", "RecoveryRequiredOwnedHandle", "BrowserSessionIncarnation", @@ -377,4 +377,4 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 1c0d6f17bca8d41fa956ad44e6c55791889bb479 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:23:50 +0900 Subject: [PATCH 161/632] docs(browser-session): consume canonical BiDi receipt for navigation trace --- docs/traceability/browser-session-navigation-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-navigation-authority.md b/docs/traceability/browser-session-navigation-authority.md index 228c666f3..a40ee73f4 100644 --- a/docs/traceability/browser-session-navigation-authority.md +++ b/docs/traceability/browser-session-navigation-authority.md @@ -6,7 +6,7 @@ - Acceptance successors: #318, #321 - Protocol adapter / WebDriver BiDi correlation owner: #316 - Governing proposals: ADR 0114, ADR 0116 -- Standards provenance: `WD-webdriver-bidi-20260909` +- Standards provenance: `docs/traceability/webdriver-bidi-publication-current.md` ## Domain boundary @@ -78,7 +78,7 @@ Real-browser GREEN is separate. A protocol command acknowledgement is insufficie ## Standards trace -The immutable W3C WebDriver BiDi Working Draft used for this active-PR contract is the 9 September 2026 publication: `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`. Protocol event vocabulary and user-context/browsing-context addressability come from that standard; OriginWeave's opaque authority and lifecycle invariants are internal domain controls and are not claimed as W3C requirements. +WebDriver BiDi publication freshness is consumed from the canonical `originweave-bidi` receipt at `docs/traceability/webdriver-bidi-publication-current.md`; this Browser Session navigation trace does not restate dated Working Draft currentness. Protocol event vocabulary and user-context/browsing-context addressability come from the versioned adapter contract; OriginWeave's opaque navigation authority and lifecycle invariants are internal domain controls and are not claimed as W3C requirements. Runtime compatibility remains independently qualified from publication metadata. ## Release status From 54d7367ac8b02e7aba0585d6d4f902228d419d7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 00:24:31 +0900 Subject: [PATCH 162/632] test(browser-session): cover navigation provenance single-writer boundary --- ...ession_webdriver_bidi_publication_trace.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/test_browser_session_webdriver_bidi_publication_trace.py b/tests/test_browser_session_webdriver_bidi_publication_trace.py index 5c24ab350..972d3d5c9 100644 --- a/tests/test_browser_session_webdriver_bidi_publication_trace.py +++ b/tests/test_browser_session_webdriver_bidi_publication_trace.py @@ -8,7 +8,8 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] ADR = ROOT / "docs/adr/0114-browser-session-disposable-context-authority.md" -TRACE = ROOT / "docs/traceability/browser-session-lifecycle-authority.md" +LIFECYCLE_TRACE = ROOT / "docs/traceability/browser-session-lifecycle-authority.md" +NAVIGATION_TRACE = ROOT / "docs/traceability/browser-session-navigation-authority.md" CANONICAL_RECEIPT = "docs/traceability/webdriver-bidi-publication-current.md" DATED_TR = re.compile(r"WD-webdriver-bidi-\d{8}") VOLATILE_CURRENTNESS = re.compile( @@ -21,26 +22,37 @@ class BrowserSessionWebDriverBidiPublicationTraceTests(unittest.TestCase): """Keep standards freshness with the canonical originweave-bidi owner.""" def test_browser_session_references_canonical_publication_receipt(self) -> None: - adr = ADR.read_text(encoding="utf-8") - trace = TRACE.read_text(encoding="utf-8") + documents = ( + ADR.read_text(encoding="utf-8"), + LIFECYCLE_TRACE.read_text(encoding="utf-8"), + NAVIGATION_TRACE.read_text(encoding="utf-8"), + ) - for document in (adr, trace): + for document in documents: self.assertIn(CANONICAL_RECEIPT, document) self.assertIsNone(DATED_TR.search(document)) self.assertIsNone(VOLATILE_CURRENTNESS.search(document)) - def test_browser_session_keeps_only_lifecycle_relevant_standard_semantics(self) -> None: + def test_browser_session_keeps_only_domain_relevant_standard_semantics(self) -> None: adr = ADR.read_text(encoding="utf-8") - trace = TRACE.read_text(encoding="utf-8") + lifecycle_trace = LIFECYCLE_TRACE.read_text(encoding="utf-8") + navigation_trace = NAVIGATION_TRACE.read_text(encoding="utf-8") self.assertIn("browser.UserContext", adr) self.assertIn("browser.createUserContext", adr) self.assertIn("browsingContext.create", adr) self.assertIn("browser.removeUserContext", adr) self.assertIn("command ACK alone is not destruction proof", adr) - self.assertIn("command acknowledgement is insufficient proof", trace) + self.assertIn( + "command acknowledgement is insufficient proof", + lifecycle_trace, + ) + self.assertIn("browsingContext.navigationStarted", navigation_trace) + self.assertIn("navigationCommitted", navigation_trace) + self.assertIn("command acknowledgement is insufficient", navigation_trace) self.assertIn("runtime compatibility", adr.lower()) - self.assertIn("runtime compatibility", trace.lower()) + self.assertIn("runtime compatibility", lifecycle_trace.lower()) + self.assertIn("runtime compatibility", navigation_trace.lower()) if __name__ == "__main__": From 8b6b7e76140e15cf97a3b2b923765b2d00afbc66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 07:16:14 +0900 Subject: [PATCH 163/632] test(browser-session): require trusted adapter TCB boundary --- ...rowser_session_trusted_adapter_boundary.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_browser_session_trusted_adapter_boundary.py diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py new file mode 100644 index 000000000..e363b8f44 --- /dev/null +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -0,0 +1,74 @@ +import pathlib +import re +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BROWSER_SESSION_CARGO = ROOT / "crates/originweave-browser-session/Cargo.toml" +THREAT_MODEL = ROOT / "docs/THREAT_MODEL.md" +DOSSIER = ROOT / "docs/traceability/browser-session-trusted-adapter-boundary.md" + +# Production adapter implementations are explicit review surfaces. Test doubles under +# crate `tests/` are intentionally outside this scan. +APPROVED_PRODUCTION_PORT_IMPLEMENTATIONS = { + "crates/originweave-bidi/src/lifecycle_acl.rs", +} + +PORT_IMPL = re.compile(r"impl(?:<[^{}]*>)?\s+DisposableContextPort\s+for\s+") + + +class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): + """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" + + def test_browser_session_crate_is_internal_and_zone_c_is_trusted(self) -> None: + cargo = BROWSER_SESSION_CARGO.read_text(encoding="utf-8") + threat_model = THREAT_MODEL.read_text(encoding="utf-8") + + self.assertRegex(cargo, r"(?m)^publish\s*=\s*false\s*$") + self.assertIn( + "Zone C — Chromium browser process and privileged adapters", + threat_model, + ) + self.assertIn("trusted browser integration code", threat_model) + + def test_trusted_adapter_dossier_states_the_supported_security_boundary(self) -> None: + dossier = DOSSIER.read_text(encoding="utf-8") + + for required in ( + "trusted computing base", + "DisposableContextPort", + "publish = false", + "supply-chain compromise", + "caller-selected production adapter", + "request/completion correlation is not adapter authentication", + ): + self.assertIn(required, dossier) + + def test_production_disposable_context_port_implementations_are_allowlisted(self) -> None: + discovered = set() + for path in ROOT.glob("crates/*/src/**/*.rs"): + text = path.read_text(encoding="utf-8") + if PORT_IMPL.search(text): + discovered.add(path.relative_to(ROOT).as_posix()) + + unexpected = discovered - APPROVED_PRODUCTION_PORT_IMPLEMENTATIONS + self.assertEqual(unexpected, set(), f"unreviewed production port implementations: {sorted(unexpected)}") + + def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> None: + callers = set() + for path in ROOT.glob("crates/*/src/**/*.rs"): + if path == ROOT / "crates/originweave-browser-session/src/browser_session.rs": + continue + text = path.read_text(encoding="utf-8") + if ".bind_lifecycle_port(" in text: + callers.add(path.relative_to(ROOT).as_posix()) + + self.assertEqual( + callers, + set(), + "product composition must gain an explicit reviewed owner before binding a lifecycle port", + ) + + +if __name__ == "__main__": + unittest.main() From 7af1d55efef66250179c26dbcac6f362b4c63afa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 07:16:36 +0900 Subject: [PATCH 164/632] docs(browser-session): define trusted adapter TCB boundary --- ...rowser-session-trusted-adapter-boundary.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/traceability/browser-session-trusted-adapter-boundary.md diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md new file mode 100644 index 000000000..ef0e9fd14 --- /dev/null +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -0,0 +1,54 @@ +# Browser Session trusted-adapter boundary + +- **Status:** active-PR security and composition evidence for PR #317; not protected-main behavior +- **Owner:** OriginWeave Browser Session bounded context +- **Related authority:** `docs/THREAT_MODEL.md`, `SECURITY.md`, ADR 0114, issue #312 + +## Problem + +`DisposableContextPort` is a cross-crate service-provider interface. Its implementation is allowed to return the browser-issued isolation and browsing-context address that Browser Session records before minting `PresentationMutationAuthority`. The Rust type system cannot distinguish a reviewed implementation from malicious code merely because both implement the same public trait. Aggregate-issued request/completion correlation, incarnation binding, duplicate rejection, exact-fact recovery, and monotonic epochs prevent replay, swapping and ABA classes; **request/completion correlation is not adapter authentication**. + +Treating an arbitrary in-process implementation as if it were an untrusted web actor would therefore create a false security promise. A nonce or opaque request handed to that implementation can simply be echoed. It does not prove that Chromium created a disposable user-context boundary. + +## Trust boundary + +OriginWeave's threat model places the Rust control plane and privileged Chromium/browser adapters inside the trusted computing base. `originweave-browser-session` is an internal `publish = false` crate, not an extension SDK that promises isolation from hostile linked Rust code. A malicious crate already executing inside this trusted process is a supply-chain compromise / trusted-code compromise; it is not made safe by making one handle constructor opaque. + +This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Production implementations are repository review surfaces and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. + +The intended canonical production implementation is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. No other production implementation is admitted by this dossier. + +## Enforced repository contract + +`tests/test_browser_session_trusted_adapter_boundary.py` enforces the currently supportable boundary: + +1. the Browser Session crate remains `publish = false`; +2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; +3. production `DisposableContextPort` implementations are limited to the explicit reviewed allowlist; +4. no current production source outside the Browser Session owner directly calls `.bind_lifecycle_port(...)` as a caller-selected composition escape hatch. + +The fourth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. + +## Authority invariant + +A `DisposableContextHandle` remains lifecycle addressability, not standalone authority. Browser Session alone owns `PresentationMutationAuthority` issuance and validates session incarnation, isolation identity, browsing-context identity, monotonic epoch and lifecycle state before later adapter I/O. `BrowserSessionIncarnation` is part of the authority binding and provides sequential-ABA protection in authorization validation; the isolation identity does not carry that responsibility by itself. + +A reviewed adapter must create a fresh disposable browser boundary, keep remote addressability scoped to the same Browser Session incarnation, settle creation only for the exact aggregate-issued attempt, and prove exact-boundary destruction. Command acknowledgement alone is not creation, ownership, destruction or browser-observed post-condition evidence. + +## Rejected fixes + +- **Caller-visible nonce or opaque request as adapter authentication:** rejected because the implementation receives the value and can echo it without performing browser I/O. +- **Generic public `TrustedPort` marker trait:** rejected because arbitrary Rust code can implement an unsealed marker and the name creates no security property. +- **Sealing `DisposableContextPort` inside `originweave-browser-session`:** not adopted because the canonical versioned browser adapter lives in a separate crate; Rust has no friend-crate visibility, so sealing here would either break the adapter boundary or force protocol code into the Browser Session owner. +- **Moving deterministic browser policy into WebDriver BiDi/MCP:** rejected; adapters translate qualified browser state and never become policy authority. +- **`--no-sandbox` or browser-process weakening:** unrelated and forbidden. + +## Remaining acceptance + +This dossier resolves the threat-model ambiguity; it does not by itself make #317 merge-ready. Before the Browser Session stack advances: + +- the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; +- `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; +- the reviewed production composition path and the versioned BiDi adapter must satisfy the repository allowlist contract when introduced/restacked; +- exact-head repository/security checks and independent review must pass with no unresolved authority finding; +- pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From 1b901bc1c0b34edd62054ee10dfc679a149e7b83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 07:17:19 +0900 Subject: [PATCH 165/632] fix(browser-session): satisfy trusted adapter contract --- docs/traceability/browser-session-trusted-adapter-boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index ef0e9fd14..2d13db2c7 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -14,7 +14,7 @@ Treating an arbitrary in-process implementation as if it were an untrusted web a OriginWeave's threat model places the Rust control plane and privileged Chromium/browser adapters inside the trusted computing base. `originweave-browser-session` is an internal `publish = false` crate, not an extension SDK that promises isolation from hostile linked Rust code. A malicious crate already executing inside this trusted process is a supply-chain compromise / trusted-code compromise; it is not made safe by making one handle constructor opaque. -This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Production implementations are repository review surfaces and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. +This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Production implementations are repository review surfaces and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. The intended canonical production implementation is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. No other production implementation is admitted by this dossier. From e63ff821c5890d993f2200e78a75ae616e675f03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:06:11 +0900 Subject: [PATCH 166/632] test(browser-session): require incarnation architecture boundary --- ...ession_architecture_incarnation_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_browser_session_architecture_incarnation_contract.py diff --git a/tests/test_browser_session_architecture_incarnation_contract.py b/tests/test_browser_session_architecture_incarnation_contract.py new file mode 100644 index 000000000..16ade3189 --- /dev/null +++ b/tests/test_browser_session_architecture_incarnation_contract.py @@ -0,0 +1,18 @@ +from pathlib import Path + + +ARCHITECTURE = Path("ARCHITECTURE.md") + + +def _browser_session_section() -> str: + text = ARCHITECTURE.read_text(encoding="utf-8") + return text.split("### `originweave-browser-session` (active PR)", 1)[1].split("## 6. Planned modules", 1)[0] + + +def test_browser_session_architecture_names_incarnation_as_aba_discriminator() -> None: + section = _browser_session_section() + + assert "`BrowserSessionIncarnation`" in section + assert "sequential ABA" in section + assert "participates in authorization validation" in section + assert "The isolation identity prevents distinct aggregate incarnations" not in section From eeb80b281b7a497fb2d545a813220e14ee1c0970 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:06:54 +0900 Subject: [PATCH 167/632] docs(browser-session): bind authority to incarnation --- ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 16158bbab..4a3756cb3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -151,9 +151,9 @@ Owns the narrow WebDriver BiDi adapter contract that is expressible by one expli ### `originweave-browser-session` (active PR) -Owns the Browser Session aggregate boundary for disposable context lifecycle and presentation-mutation authority. Raw `BrowserSessionId` and `BrowsingContextId` values are transport addressability only. A context enters the owned set only after the narrow `DisposableContextPort` reports a fresh disposable isolation boundary together with its browsing-context address. The aggregate stores that exact handle and issues a non-caller-constructible `PresentationMutationAuthority` bound to browser-session identity, disposable-isolation identity, browsing context, and monotonic context epoch. +Owns the Browser Session aggregate boundary for disposable context lifecycle and presentation-mutation authority. Raw `BrowserSessionId` and `BrowsingContextId` values are transport addressability only. A context enters the owned set only after the narrow `DisposableContextPort` reports a fresh disposable isolation boundary together with its browsing-context address. The aggregate stores that exact handle and issues a non-caller-constructible `PresentationMutationAuthority` bound to browser-session identity, `BrowserSessionIncarnation`, disposable-isolation identity, browsing context, and monotonic context epoch. `BrowserSessionIncarnation` participates in authorization validation and prevents sequential ABA when external session/context identifiers and local epoch values are reused. -The isolation identity prevents distinct aggregate incarnations from aliasing authority when external session/context identifiers and local epoch values are reused. Destruction validates the full authority before adapter I/O and passes the stored isolation handle back to the port; cleanup authority is never reconstructed from `(BrowserSessionId, BrowsingContextId)`. For a WebDriver BiDi adapter, the port contract requires a one-to-one mapping from the domain's `DisposableIsolationId` to the specification-defined unique user-context id created for that live boundary. The protocol identifier is lifecycle addressability, not OriginWeave policy authority. Stale, foreign-session, foreign-isolation, unknown, destroyed, or uncertain authority fails closed; failed destruction makes the context uncertain; browser transport loss invalidates active authority; and normal session end is rejected until every owned boundary has proven destruction. +The disposable-isolation identity binds lifecycle ownership to the exact browser isolation boundary; it does not substitute for `BrowserSessionIncarnation`. Destruction validates the full authority before adapter I/O and passes the stored isolation handle back to the port; cleanup authority is never reconstructed from `(BrowserSessionId, BrowsingContextId)`. For a WebDriver BiDi adapter, the port contract requires a one-to-one mapping from the domain's `DisposableIsolationId` to the specification-defined unique user-context id created for that live boundary. The protocol identifier is lifecycle addressability, not OriginWeave policy authority. Stale, foreign-session, foreign-isolation, unknown, destroyed, or uncertain authority fails closed; failed destruction makes the context uncertain; browser transport loss invalidates active authority; and normal session end is rejected until every owned boundary has proven destruction. This active slice deliberately stops before browser transport. WebDriver BiDi/CDP remain adapters and do not mint policy authority. The current proposal does not yet bridge domain authority into `originweave-bidi`'s private presentation/screen-area witnesses, implement the real `browser.createUserContext`/`browsingContext.create`/`browser.removeUserContext` adapter, prove exact-boundary cleanup post-conditions in Chromium, or establish protected-main behavior. ADR 0114, the Browser Session traceability dossier, and the lifecycle UML record those remaining boundaries. From a5d4f677578bd7bb37ea656e13caad2dfa274cb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:07:53 +0900 Subject: [PATCH 168/632] docs(browser-session): record incarnation authority repair --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5232cd5a3..5bf4defe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Corrected the root Browser Session architecture contract so `PresentationMutationAuthority` is explicitly bound to `BrowserSessionIncarnation`; the incarnation participates in authorization validation and provides sequential-ABA separation when external session/context identifiers and local epochs are reused, while the disposable-isolation identity remains the exact remote lifecycle boundary rather than a substitute for aggregate incarnation. - Prevented ownership-clean `TransportLost` sessions from entering Browser Session recovery custody. Recovery handoff now requires exact unresolved recovery/create-attempt evidence for `TransportLost`, while `RecoveryRequired` remains recovery-eligible; transport loss before remote ownership or after proven destruction therefore cannot mint a purpose-bounded adapter-operation capability. - Preserved Browser Session create-attempt provenance through uncertain creation, duplicate-candidate rejection, and accepted/rejected completion-settlement failure. Recovery now keeps aggregate-issued attempt epoch, disposition, and complete candidate identity without collapsing a previously accepted same-valued owner into later candidate evidence; the abandonment/incarnation atomic updates use `AtomicU64::try_update` without changing their memory ordering or overflow behavior. - Added one-way same-adapter Browser Session recovery custody for `RecoveryRequired` and for `TransportLost` with retained unresolved ownership evidence through `BoundBrowserSessionRecovery

`, preventing recovery evidence from regaining raw adapter or ordinary command authority. Proven destruction now retires only the exact live hot ownership record after adapter-proven success; failed destruction keeps `Uncertain` ownership with exact `UnprovenDestruction { context, context_epoch }` evidence. @@ -106,7 +107,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - DNS TLS identity requires an applicable subjectAltName and never falls back to Common Name; literal IPv4 and IPv6 origins require exact IP subjectAltName entries. - TLS uses an explicit immutable trust-root bundle and fixed verification time, and permits only TLS 1.2 and TLS 1.3. - TLS trust-bundle policy identifiers must contain at least one ASCII alphanumeric character; punctuation-only labels are rejected while `.`, `_`, `:`, and `-` remain permitted. -- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verifier hooks are disabled in the first slice. +- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verification are disabled in the first slice. - The operating-system peer is rechecked before, during, and after the deadline-bound TLS handshake. - ALPN selection is restricted to the caller's bounded allow-list, while absence is either explicitly recorded or rejected by policy. - Revocation is reported as not configured; the product makes no OCSP or CRL validation claim without supplied revocation evidence. From d8d4eb2a0d5666ca805107669191ed9645c2f646 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 08:11:29 +0900 Subject: [PATCH 169/632] chore(browser-session): adopt parent BiDi publication deltas --- .../0107-browser-protocol-adapter-strategy.md | 6 ++- .../webdriver-bidi-publication-current.md | 40 +++++++++++++------ ...ebdriver_bidi_docs_currentness_contract.py | 16 ++++++-- ...iver_bidi_presentation_adapter_contract.py | 9 +++-- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index 491359110..6eb503049 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -44,7 +44,7 @@ Neither protected main nor PR #170 implements Streamable HTTP transport parsing, The version boundary is explicit: the protected-main routing foundation and active discovery refinement accept only MCP `2026-07-28`; neither infers compatibility with later protocol generations. OriginWeave Protocol versioning remains independent and cannot be changed by MCP metadata. -PR #293 was merged into PR #229 on 2026-09-09, so its `originweave-bidi` capability boundary is inherited by this parent rather than remaining a separate active stacked slice. The adapter remains runtime-qualified 3 September 2026 against the immutable WebDriver BiDi Working Draft URI `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`. W3C has since published the latest published 9 September 2026 Working Draft; publication freshness is recorded separately in `docs/traceability/webdriver-bidi-publication-current.md` and does not silently repin runtime compatibility. A newer runtime pin requires a dedicated compatibility/conformance change and pinned-browser evidence. +PR #293 was merged into PR #229 on 2026-09-09, so its `originweave-bidi` capability boundary is inherited by this parent rather than remaining a separate active stacked slice. The adapter remains runtime-qualified 3 September 2026 against the immutable WebDriver BiDi Working Draft URI `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/`. W3C has since published the latest published 16 September 2026 Working Draft, with 14 September 2026 as the previous published version; publication freshness is recorded separately in `docs/traceability/webdriver-bidi-publication-current.md` and does not silently repin runtime compatibility. The mutable Editor's Draft remains a separate research surface. A newer runtime pin requires a dedicated compatibility/conformance change and pinned-browser evidence. The inherited capability map delegates complete-profile admission to `originweave-fingerprint` and intentionally excludes `Screen`, `Languages`, `HardwareConcurrency`, and `Platform`. The standard screen-settings command omits color depth and, importantly, applies one rectangle to both the web-exposed total screen area and available screen area, while the current OriginWeave presentation profile does not model the available-screen rectangle. The locale command likewise cannot prove ordered language preferences. Standard BiDi alone must therefore return the kernel's first `MissingSurface(Screen)` result rather than accept ambient host values. @@ -96,7 +96,9 @@ Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://mo Parra, D. S., & Delimarsky, D. (2026, July 28). *The 2026-07-28 specification*. Model Context Protocol Blog. https://blog.modelcontextprotocol.io/posts/2026-07-28/ -World Wide Web Consortium. (2026, September 9). *WebDriver BiDi* [Working Draft; latest publication observed 2026-09-10]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +World Wide Web Consortium. (2026, September 16). *WebDriver BiDi* [Working Draft; latest publication observed 2026-09-16]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260916/ + +World Wide Web Consortium. (2026, September 14). *WebDriver BiDi* [Working Draft; previous published version]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/ World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* [Working Draft; runtime-qualified OriginWeave adapter pin]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ diff --git a/docs/traceability/webdriver-bidi-publication-current.md b/docs/traceability/webdriver-bidi-publication-current.md index 8fd347776..857c807b9 100644 --- a/docs/traceability/webdriver-bidi-publication-current.md +++ b/docs/traceability/webdriver-bidi-publication-current.md @@ -1,35 +1,45 @@ # WebDriver BiDi publication-current receipt Status: active standards traceability -Observed: 2026-09-10 +Observed: 2026-09-16 Runtime-compatible pin: `2026-09-03` -Latest published Working Draft: `2026-09-09` +Latest published Working Draft: `2026-09-16` +Previous published Working Draft: `2026-09-14` +Editor's Draft: `https://w3c.github.io/webdriver-bidi/` ## Problem -The `originweave-bidi` presentation capability map is deliberately version-pinned, but its repository contract had conflated that qualified runtime pin with the latest W3C publication. On 2026-09-10 the canonical W3C Technical Report page identifies the 9 September 2026 Working Draft as the latest published version, while the adapter remains qualified against the immutable 3 September 2026 Working Draft. +The `originweave-bidi` presentation capability map is deliberately version-pinned, but publication provenance and runtime qualification are separate facts. On 2026-09-16 the canonical W3C publication-history page identifies the 16 September 2026 Working Draft as the latest published version, the 14 September 2026 Working Draft as the previous published version, and the Editor's Draft as a separate mutable surface. The adapter remains qualified against the immutable 3 September 2026 Working Draft. -Treating those as the same datum creates two bad failure modes: documentation can become false whenever W3C publishes a new draft, or an automation can silently repin the runtime compatibility claim without re-running the browser/protocol qualification that gives the pin meaning. +Treating publication freshness and runtime qualification as the same datum creates two bad failure modes: documentation can become false whenever W3C publishes a new draft, or an automation can silently repin the runtime compatibility claim without re-running the browser/protocol qualification that gives the pin meaning. ## Current authoritative publication Canonical publication page: https://www.w3.org/TR/webdriver-bidi/ -Latest immutable published Working Draft: https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +Canonical publication history: https://www.w3.org/standards/history/webdriver-bidi/ -The 9 September publication still exposes the standard presentation/lifecycle surfaces used by OriginWeave's capability analysis, including `browsingContext.setViewport`, `browser.createUserContext` / `browser.removeUserContext`, `emulation.setLocaleOverride`, `emulation.setMediaFeaturesOverride`, `emulation.setScreenSettingsOverride`, `emulation.setTimezoneOverride`, and `emulation.setUserAgentOverride`. Their presence is standards research evidence, not proof that the existing runtime adapter has been requalified against the new publication. +Latest immutable published Working Draft: https://www.w3.org/TR/2026/WD-webdriver-bidi-20260916/ + +Previous immutable published Working Draft: https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/ + +Mutable Editor's Draft: https://w3c.github.io/webdriver-bidi/ + +The 16 September publication continues to expose the standard presentation/lifecycle surfaces used by OriginWeave's capability analysis, including `browsingContext.setViewport`, `browser.createUserContext` / `browser.removeUserContext`, `emulation.setLocaleOverride`, `emulation.setMediaFeaturesOverride`, `emulation.setScreenSettingsOverride`, `emulation.setTimezoneOverride`, and `emulation.setUserAgentOverride`. Their presence is standards research evidence, not proof that the existing runtime adapter has been requalified against the new publication. ## Runtime compatibility decision -OriginWeave keeps `WEBDRIVER_BIDI_PRESENTATION_REVISION = "2026-09-03"` until a dedicated compatibility change proves that the newer immutable draft preserves the exact command schemas, reset semantics, capability interpretation, browser implementation behavior, and pinned-Chromium acceptance required by the adapter. +OriginWeave keeps `WEBDRIVER_BIDI_PRESENTATION_REVISION = "2026-09-03"` until a dedicated compatibility change proves that a newer immutable draft preserves the exact command schemas, reset semantics, capability interpretation, browser implementation behavior, and pinned-Chromium acceptance required by the adapter. A publication-freshness update therefore does **not** mutate the runtime pin, claim new browser capability, or promote command acknowledgement to presentation evidence. The safe sequence is: 1. record the latest authoritative W3C publication independently from the supported runtime pin; -2. diff the relevant specification surfaces and update the versioned capability map only if needed; -3. re-run repository contracts and pinned Chromium/BiDi/CDP compatibility evidence on the proposed new pin; -4. update architecture/ADR/doctoring compatibility claims together with the qualified pin; -5. keep unsupported or unverified surfaces fail closed. +2. retain the immediately previous immutable publication as provenance for publication-history checks; +3. keep the mutable Editor's Draft explicitly separate from dated Technical Reports; +4. diff the relevant specification surfaces and update the versioned capability map only if needed; +5. re-run repository contracts and pinned Chromium/BiDi/CDP compatibility evidence on any proposed new runtime pin; +6. update architecture/ADR/doctoring compatibility claims together with the qualified pin; +7. keep unsupported or unverified surfaces fail closed. ## Relationship to buyer acceptance @@ -37,7 +47,9 @@ This receipt does not close OriginWeave #292. The buyer-visible acceptance still ## Traceability -- W3C latest published version observed 2026-09-10: WebDriver BiDi Working Draft, 9 September 2026. +- W3C latest published version observed 2026-09-16: WebDriver BiDi Working Draft, 16 September 2026. +- Previous published version: WebDriver BiDi Working Draft, 14 September 2026. +- Mutable Editor's Draft: https://w3c.github.io/webdriver-bidi/. - Runtime-qualified OriginWeave adapter pin: WebDriver BiDi Working Draft, 3 September 2026. - OriginWeave buyer acceptance owner: issue #292. - OriginWeave profile/standard-adapter parent lineage: PR #229, which has inherited merged PR #293. @@ -45,6 +57,8 @@ This receipt does not close OriginWeave #292. The buyer-visible acceptance still ## References -World Wide Web Consortium. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +World Wide Web Consortium. (2026, September 16). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260916/ + +World Wide Web Consortium. (2026, September 14). *WebDriver BiDi* (W3C Working Draft; previous published version). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/ World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (W3C Working Draft; runtime-qualified OriginWeave pin). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ diff --git a/tests/test_webdriver_bidi_docs_currentness_contract.py b/tests/test_webdriver_bidi_docs_currentness_contract.py index bbd8295f6..40ad36015 100644 --- a/tests/test_webdriver_bidi_docs_currentness_contract.py +++ b/tests/test_webdriver_bidi_docs_currentness_contract.py @@ -27,10 +27,11 @@ def test_adr_tracks_merged_adapter_lineage_and_publication_receipt(self) -> None adr, ) self.assertIn("runtime-qualified 3 September 2026", adr) - self.assertIn("latest published 9 September 2026", adr) + self.assertIn("latest published 16 September 2026", adr) + self.assertIn("14 September 2026 as the previous published version", adr) def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs(self) -> None: - """Architecture and doctoring stay qualification records; the receipt owns latest-publication churn.""" + """Architecture and doctoring stay qualification records; the receipt owns publication churn.""" architecture = (ROOT / "ARCHITECTURE.md").read_text(encoding="utf-8") doctoring = (ROOT / "docs/doctoring.md").read_text(encoding="utf-8") receipt = ( @@ -38,7 +39,9 @@ def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs ).read_text(encoding="utf-8") runtime_uri = "https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/" - latest_uri = "https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/" + latest_uri = "https://www.w3.org/TR/2026/WD-webdriver-bidi-20260916/" + previous_uri = "https://www.w3.org/TR/2026/WD-webdriver-bidi-20260914/" + editors_draft_uri = "https://w3c.github.io/webdriver-bidi/" for path, text in { "ARCHITECTURE.md": architecture, @@ -48,9 +51,14 @@ def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs self.assertIn(runtime_uri, text) self.assertNotIn(latest_uri, text) + self.assertIn("Observed: 2026-09-16", receipt) self.assertIn("Runtime-compatible pin: `2026-09-03`", receipt) - self.assertIn("Latest published Working Draft: `2026-09-09`", receipt) + self.assertIn("Latest published Working Draft: `2026-09-16`", receipt) + self.assertIn("Previous published Working Draft: `2026-09-14`", receipt) + self.assertIn("Editor's Draft: `https://w3c.github.io/webdriver-bidi/`", receipt) self.assertIn(latest_uri, receipt) + self.assertIn(previous_uri, receipt) + self.assertIn(editors_draft_uri, receipt) self.assertIn( "PR #229, which has inherited merged PR #293", receipt, diff --git a/tests/test_webdriver_bidi_presentation_adapter_contract.py b/tests/test_webdriver_bidi_presentation_adapter_contract.py index b808f66f0..fe8931854 100644 --- a/tests/test_webdriver_bidi_presentation_adapter_contract.py +++ b/tests/test_webdriver_bidi_presentation_adapter_contract.py @@ -48,13 +48,14 @@ def test_latest_published_bidi_is_tracked_without_silently_repinning_adapter(sel "RED: latest WebDriver BiDi publication is not traceable beside the qualified runtime pin", ) receipt = publication_receipt.read_text(encoding="utf-8") - self.assertIn("2026-09-09", receipt) + self.assertIn("Runtime-compatible pin: `2026-09-03`", receipt) self.assertIn( - "https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/", + "Canonical publication history: https://www.w3.org/standards/history/webdriver-bidi/", receipt, ) - self.assertIn("Runtime-compatible pin: `2026-09-03`", receipt) - self.assertIn("Latest published Working Draft: `2026-09-09`", receipt) + self.assertIn("Latest published Working Draft:", receipt) + self.assertIn("Previous published Working Draft:", receipt) + self.assertNotIn("Latest published Working Draft: `2026-09-03`", receipt) self.assertIn("PresentationSurface::Screen", text) self.assertIn("PresentationSurface::Viewport", text) From 100c00487488bbc281106ddf6fe4ae1b60feb16b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:02:54 +0900 Subject: [PATCH 170/632] test(browser-session): expose trusted-adapter scanner bypasses --- ...rowser_session_trusted_adapter_boundary.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index e363b8f44..6818f84bd 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -17,6 +17,14 @@ PORT_IMPL = re.compile(r"impl(?:<[^{}]*>)?\s+DisposableContextPort\s+for\s+") +def _has_port_implementation(text: str) -> bool: + return PORT_IMPL.search(text) is not None + + +def _has_lifecycle_binding(text: str) -> bool: + return ".bind_lifecycle_port(" in text + + class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" @@ -48,7 +56,7 @@ def test_production_disposable_context_port_implementations_are_allowlisted(self discovered = set() for path in ROOT.glob("crates/*/src/**/*.rs"): text = path.read_text(encoding="utf-8") - if PORT_IMPL.search(text): + if _has_port_implementation(text): discovered.add(path.relative_to(ROOT).as_posix()) unexpected = discovered - APPROVED_PRODUCTION_PORT_IMPLEMENTATIONS @@ -60,7 +68,7 @@ def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> N if path == ROOT / "crates/originweave-browser-session/src/browser_session.rs": continue text = path.read_text(encoding="utf-8") - if ".bind_lifecycle_port(" in text: + if _has_lifecycle_binding(text): callers.add(path.relative_to(ROOT).as_posix()) self.assertEqual( @@ -69,6 +77,31 @@ def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> N "product composition must gain an explicit reviewed owner before binding a lifecycle port", ) + def test_scanners_cover_qualified_alias_and_ufcs_spellings(self) -> None: + port_spellings = ( + "impl originweave_browser_session::DisposableContextPort for CandidatePort {}", + ( + "use originweave_browser_session::DisposableContextPort as LifecyclePort;\n" + "impl LifecyclePort for CandidatePort {}" + ), + ) + for source in port_spellings: + self.assertTrue( + _has_port_implementation(source), + f"production port spelling escaped review scanner: {source!r}", + ) + + binding_spellings = ( + "session.bind_lifecycle_port(port);", + "BrowserSession::bind_lifecycle_port(session, port);", + "session.bind_lifecycle_port (port);", + ) + for source in binding_spellings: + self.assertTrue( + _has_lifecycle_binding(source), + f"production lifecycle binding escaped review scanner: {source!r}", + ) + if __name__ == "__main__": unittest.main() From 7b2334b1df92d03629b7931ce71cd58134b93f4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:03:16 +0900 Subject: [PATCH 171/632] test(browser-session): close trusted-adapter scanner syntax gaps --- ...rowser_session_trusted_adapter_boundary.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 6818f84bd..e07cfd39f 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -7,22 +7,24 @@ BROWSER_SESSION_CARGO = ROOT / "crates/originweave-browser-session/Cargo.toml" THREAT_MODEL = ROOT / "docs/THREAT_MODEL.md" DOSSIER = ROOT / "docs/traceability/browser-session-trusted-adapter-boundary.md" +BROWSER_SESSION_SOURCE_ROOT = "crates/originweave-browser-session/src/" -# Production adapter implementations are explicit review surfaces. Test doubles under -# crate `tests/` are intentionally outside this scan. -APPROVED_PRODUCTION_PORT_IMPLEMENTATIONS = { +# Any production reference to the lifecycle SPI outside the Browser Session owner is an explicit +# review surface. The reserved BiDi path is the only currently approved external production owner. +APPROVED_PRODUCTION_PORT_REFERENCES = { "crates/originweave-bidi/src/lifecycle_acl.rs", } -PORT_IMPL = re.compile(r"impl(?:<[^{}]*>)?\s+DisposableContextPort\s+for\s+") +PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") +LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") -def _has_port_implementation(text: str) -> bool: - return PORT_IMPL.search(text) is not None +def _has_port_reference(text: str) -> bool: + return PORT_REFERENCE.search(text) is not None def _has_lifecycle_binding(text: str) -> bool: - return ".bind_lifecycle_port(" in text + return LIFECYCLE_BINDING.search(text) is not None class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): @@ -52,15 +54,22 @@ def test_trusted_adapter_dossier_states_the_supported_security_boundary(self) -> ): self.assertIn(required, dossier) - def test_production_disposable_context_port_implementations_are_allowlisted(self) -> None: + def test_production_disposable_context_port_references_are_allowlisted(self) -> None: discovered = set() for path in ROOT.glob("crates/*/src/**/*.rs"): + relative = path.relative_to(ROOT).as_posix() + if relative.startswith(BROWSER_SESSION_SOURCE_ROOT): + continue text = path.read_text(encoding="utf-8") - if _has_port_implementation(text): - discovered.add(path.relative_to(ROOT).as_posix()) + if _has_port_reference(text): + discovered.add(relative) - unexpected = discovered - APPROVED_PRODUCTION_PORT_IMPLEMENTATIONS - self.assertEqual(unexpected, set(), f"unreviewed production port implementations: {sorted(unexpected)}") + unexpected = discovered - APPROVED_PRODUCTION_PORT_REFERENCES + self.assertEqual( + unexpected, + set(), + f"unreviewed production lifecycle-port references: {sorted(unexpected)}", + ) def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> None: callers = set() @@ -87,7 +96,7 @@ def test_scanners_cover_qualified_alias_and_ufcs_spellings(self) -> None: ) for source in port_spellings: self.assertTrue( - _has_port_implementation(source), + _has_port_reference(source), f"production port spelling escaped review scanner: {source!r}", ) From d074175b30e4834a9ca0be169c915d46204f0afe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:03:40 +0900 Subject: [PATCH 172/632] docs(browser-session): record syntax-independent adapter review contract --- .../browser-session-trusted-adapter-boundary.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index 2d13db2c7..c481ba8dc 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -14,9 +14,9 @@ Treating an arbitrary in-process implementation as if it were an untrusted web a OriginWeave's threat model places the Rust control plane and privileged Chromium/browser adapters inside the trusted computing base. `originweave-browser-session` is an internal `publish = false` crate, not an extension SDK that promises isolation from hostile linked Rust code. A malicious crate already executing inside this trusted process is a supply-chain compromise / trusted-code compromise; it is not made safe by making one handle constructor opaque. -This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Production implementations are repository review surfaces and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. +This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Any production source that references this SPI outside the Browser Session owner is a repository review surface and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. -The intended canonical production implementation is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. No other production implementation is admitted by this dossier. +The intended canonical production implementation is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. No other external production reference is admitted by this dossier. ## Enforced repository contract @@ -24,11 +24,17 @@ The intended canonical production implementation is the versioned WebDriver BiDi 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; -3. production `DisposableContextPort` implementations are limited to the explicit reviewed allowlist; -4. no current production source outside the Browser Session owner directly calls `.bind_lifecycle_port(...)` as a caller-selected composition escape hatch. +3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed allowlist; and +4. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. The fourth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. +### Scanner false-negative repair + +The first repository contract recognized only the literal unqualified Rust form `impl DisposableContextPort for ...` and the exact method spelling `.bind_lifecycle_port(`. Those are style conventions, not security boundaries: valid Rust can name the trait through a qualified path or alias and can invoke the binding function through UFCS or with different whitespace. + +Test-first commit `100c00487488bbc281106ddf6fe4ae1b60feb16b` adds hostile qualified-trait, aliased-trait, UFCS, and whitespace spellings and exposes those false negatives. Minimal contract repair `7b2334b1df92d03629b7931ce71cd58134b93f4c` makes the review surface syntax-independent at the repository level: any external production source containing the SPI token is reviewed, and any production source outside the owner containing the binding API token is rejected until an explicit composition owner is approved. The repair changes no Rust production behavior or trust classification; it makes the existing single-writer/TCB policy enforceable across ordinary Rust spelling choices. + ## Authority invariant A `DisposableContextHandle` remains lifecycle addressability, not standalone authority. Browser Session alone owns `PresentationMutationAuthority` issuance and validates session incarnation, isolation identity, browsing-context identity, monotonic epoch and lifecycle state before later adapter I/O. `BrowserSessionIncarnation` is part of the authority binding and provides sequential-ABA protection in authorization validation; the isolation identity does not carry that responsibility by itself. From cdf98568ca880126fe7409e7cb0678547ac2164b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:06:04 +0900 Subject: [PATCH 173/632] docs(changelog): record adapter contract scanner hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf4defe1..f78221c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Hardened the Browser Session trusted-adapter repository contract so fully qualified or aliased `DisposableContextPort` references and UFCS/whitespace `bind_lifecycle_port` spellings cannot evade review-surface detection; this changes no Rust production behavior or trust classification. - Corrected the root Browser Session architecture contract so `PresentationMutationAuthority` is explicitly bound to `BrowserSessionIncarnation`; the incarnation participates in authorization validation and provides sequential-ABA separation when external session/context identifiers and local epochs are reused, while the disposable-isolation identity remains the exact remote lifecycle boundary rather than a substitute for aggregate incarnation. - Prevented ownership-clean `TransportLost` sessions from entering Browser Session recovery custody. Recovery handoff now requires exact unresolved recovery/create-attempt evidence for `TransportLost`, while `RecoveryRequired` remains recovery-eligible; transport loss before remote ownership or after proven destruction therefore cannot mint a purpose-bounded adapter-operation capability. - Preserved Browser Session create-attempt provenance through uncertain creation, duplicate-candidate rejection, and accepted/rejected completion-settlement failure. Recovery now keeps aggregate-issued attempt epoch, disposition, and complete candidate identity without collapsing a previously accepted same-valued owner into later candidate evidence; the abandonment/incarnation atomic updates use `AtomicU64::try_update` without changing their memory ordering or overflow behavior. @@ -118,4 +119,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 From 6ce9c1f3b13fe157cfae822a0958c2b8dc2dabd8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:08:46 +0900 Subject: [PATCH 174/632] test(browser-session): expose cross-file adapter alias gap --- ...test_browser_session_trusted_adapter_boundary.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index e07cfd39f..97f07ebc1 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -14,6 +14,9 @@ APPROVED_PRODUCTION_PORT_REFERENCES = { "crates/originweave-bidi/src/lifecycle_acl.rs", } +APPROVED_BROWSER_SESSION_DEPENDENCIES = { + "crates/originweave-bidi/Cargo.toml", +} PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") @@ -111,6 +114,16 @@ def test_scanners_cover_qualified_alias_and_ufcs_spellings(self) -> None: f"production lifecycle binding escaped review scanner: {source!r}", ) + def test_cross_file_alias_cannot_escape_dependency_review_surface(self) -> None: + alias_only_source = "impl LifecyclePort for CandidatePort {}" + dependency_manifest = ( + "[dependencies]\n" + 'originweave-browser-session = { path = "../originweave-browser-session" }\n' + ) + + self.assertFalse(_has_port_reference(alias_only_source)) + self.assertTrue(_has_browser_session_dependency(dependency_manifest)) + if __name__ == "__main__": unittest.main() From cb9fb54e4a799919a425f3636cb0a5f1daacfb24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:09:03 +0900 Subject: [PATCH 175/632] test(browser-session): gate external crate dependencies --- ...rowser_session_trusted_adapter_boundary.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 97f07ebc1..b3012b6bc 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -10,7 +10,7 @@ BROWSER_SESSION_SOURCE_ROOT = "crates/originweave-browser-session/src/" # Any production reference to the lifecycle SPI outside the Browser Session owner is an explicit -# review surface. The reserved BiDi path is the only currently approved external production owner. +# review surface. The reserved BiDi paths are the only currently approved external production owner. APPROVED_PRODUCTION_PORT_REFERENCES = { "crates/originweave-bidi/src/lifecycle_acl.rs", } @@ -20,6 +20,7 @@ PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") +BROWSER_SESSION_DEPENDENCY = re.compile(r"(?m)^\s*originweave-browser-session\s*=") def _has_port_reference(text: str) -> bool: @@ -30,6 +31,10 @@ def _has_lifecycle_binding(text: str) -> bool: return LIFECYCLE_BINDING.search(text) is not None +def _has_browser_session_dependency(text: str) -> bool: + return BROWSER_SESSION_DEPENDENCY.search(text) is not None + + class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" @@ -74,6 +79,22 @@ def test_production_disposable_context_port_references_are_allowlisted(self) -> f"unreviewed production lifecycle-port references: {sorted(unexpected)}", ) + def test_browser_session_dependencies_are_allowlisted(self) -> None: + discovered = set() + for path in ROOT.glob("crates/*/Cargo.toml"): + if path == BROWSER_SESSION_CARGO: + continue + text = path.read_text(encoding="utf-8") + if _has_browser_session_dependency(text): + discovered.add(path.relative_to(ROOT).as_posix()) + + unexpected = discovered - APPROVED_BROWSER_SESSION_DEPENDENCIES + self.assertEqual( + unexpected, + set(), + f"unreviewed production Browser Session dependencies: {sorted(unexpected)}", + ) + def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> None: callers = set() for path in ROOT.glob("crates/*/src/**/*.rs"): From e1f95508553efb05d4d4960cf22b556a8479bed6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:09:23 +0900 Subject: [PATCH 176/632] docs(browser-session): bind adapter review to crate dependencies --- ...rowser-session-trusted-adapter-boundary.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index c481ba8dc..98ebf8ec1 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -14,9 +14,9 @@ Treating an arbitrary in-process implementation as if it were an untrusted web a OriginWeave's threat model places the Rust control plane and privileged Chromium/browser adapters inside the trusted computing base. `originweave-browser-session` is an internal `publish = false` crate, not an extension SDK that promises isolation from hostile linked Rust code. A malicious crate already executing inside this trusted process is a supply-chain compromise / trusted-code compromise; it is not made safe by making one handle constructor opaque. -This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Any production source that references this SPI outside the Browser Session owner is a repository review surface and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. +This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Any production source that references this SPI outside the Browser Session owner and any production crate that depends on `originweave-browser-session` is a repository review surface and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. -The intended canonical production implementation is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. No other external production reference is admitted by this dossier. +The intended canonical production consumer is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. Its crate manifest is the only reserved external Browser Session dependency. No other external production reference or crate dependency is admitted by this dossier. ## Enforced repository contract @@ -24,16 +24,21 @@ The intended canonical production implementation is the versioned WebDriver BiDi 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; -3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed allowlist; and -4. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. +3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed source allowlist; +4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist; and +5. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. -The fourth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. +The fifth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. ### Scanner false-negative repair The first repository contract recognized only the literal unqualified Rust form `impl DisposableContextPort for ...` and the exact method spelling `.bind_lifecycle_port(`. Those are style conventions, not security boundaries: valid Rust can name the trait through a qualified path or alias and can invoke the binding function through UFCS or with different whitespace. -Test-first commit `100c00487488bbc281106ddf6fe4ae1b60feb16b` adds hostile qualified-trait, aliased-trait, UFCS, and whitespace spellings and exposes those false negatives. Minimal contract repair `7b2334b1df92d03629b7931ce71cd58134b93f4c` makes the review surface syntax-independent at the repository level: any external production source containing the SPI token is reviewed, and any production source outside the owner containing the binding API token is rejected until an explicit composition owner is approved. The repair changes no Rust production behavior or trust classification; it makes the existing single-writer/TCB policy enforceable across ordinary Rust spelling choices. +Test-first commit `100c00487488bbc281106ddf6fe4ae1b60feb16b` adds hostile qualified-trait, aliased-trait, UFCS, and whitespace spellings and exposes those false negatives. Minimal contract repair `7b2334b1df92d03629b7931ce71cd58134b93f4c` makes direct source review spelling-resilient: any external production source containing the SPI token is reviewed, and any production source outside the owner containing the binding API token is rejected until an explicit composition owner is approved. + +A second review found the remaining cross-file alias case: one reviewed module could import or re-export the trait under another name while a different module implements only that alias and therefore contains no `DisposableContextPort` token. Test-first commit `6ce9c1f3b13fe157cfae822a0958c2b8dc2dabd8` records that direct source scanning cannot prove this case. Commit `cb9fb54e4a799919a425f3636cb0a5f1daacfb24` adds the compensating crate-boundary invariant: every production `Cargo.toml` that can link Browser Session must itself be reviewed and allowlisted. A cross-file alias therefore cannot create a new production adapter from an unreviewed crate without first widening an explicit dependency review surface. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling and module-layout choices. ## Authority invariant @@ -55,6 +60,6 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; -- the reviewed production composition path and the versioned BiDi adapter must satisfy the repository allowlist contract when introduced/restacked; +- the reviewed production composition path and the versioned BiDi adapter must satisfy the source-reference and crate-dependency allowlist contracts when introduced/restacked; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From 1e46b302254596397a6c4b0bb9ced1be02a33ea1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:09:54 +0900 Subject: [PATCH 177/632] test(browser-session): expose Cargo dependency alias spellings --- ...est_browser_session_trusted_adapter_boundary.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index b3012b6bc..fcdba1038 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -137,13 +137,21 @@ def test_scanners_cover_qualified_alias_and_ufcs_spellings(self) -> None: def test_cross_file_alias_cannot_escape_dependency_review_surface(self) -> None: alias_only_source = "impl LifecyclePort for CandidatePort {}" - dependency_manifest = ( + dependency_manifests = ( "[dependencies]\n" - 'originweave-browser-session = { path = "../originweave-browser-session" }\n' + 'originweave-browser-session = { path = "../originweave-browser-session" }\n', + "[dependencies]\n" + 'browser = { package = "originweave-browser-session", path = "../originweave-browser-session" }\n', + "[dependencies.originweave-browser-session]\n" + 'path = "../originweave-browser-session"\n', ) self.assertFalse(_has_port_reference(alias_only_source)) - self.assertTrue(_has_browser_session_dependency(dependency_manifest)) + for manifest in dependency_manifests: + self.assertTrue( + _has_browser_session_dependency(manifest), + f"Browser Session dependency spelling escaped review scanner: {manifest!r}", + ) if __name__ == "__main__": From b24b9beb7a0804b90339a0e9e6abce3dd701fc4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:10:10 +0900 Subject: [PATCH 178/632] test(browser-session): detect aliased Cargo dependencies --- tests/test_browser_session_trusted_adapter_boundary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index fcdba1038..66bdf1f7a 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -20,7 +20,7 @@ PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") -BROWSER_SESSION_DEPENDENCY = re.compile(r"(?m)^\s*originweave-browser-session\s*=") +BROWSER_SESSION_DEPENDENCY = re.compile(r"\boriginweave-browser-session\b") def _has_port_reference(text: str) -> bool: From 706bc9e459790a39dea91733a2b168e99de2344b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 10:10:40 +0900 Subject: [PATCH 179/632] docs(browser-session): cover Cargo dependency aliases --- docs/traceability/browser-session-trusted-adapter-boundary.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index 98ebf8ec1..fe52a424c 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -38,7 +38,9 @@ Test-first commit `100c00487488bbc281106ddf6fe4ae1b60feb16b` adds hostile qualif A second review found the remaining cross-file alias case: one reviewed module could import or re-export the trait under another name while a different module implements only that alias and therefore contains no `DisposableContextPort` token. Test-first commit `6ce9c1f3b13fe157cfae822a0958c2b8dc2dabd8` records that direct source scanning cannot prove this case. Commit `cb9fb54e4a799919a425f3636cb0a5f1daacfb24` adds the compensating crate-boundary invariant: every production `Cargo.toml` that can link Browser Session must itself be reviewed and allowlisted. A cross-file alias therefore cannot create a new production adapter from an unreviewed crate without first widening an explicit dependency review surface. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling and module-layout choices. +Cargo permits the dependency key itself to be renamed with `package = "originweave-browser-session"` and also permits table-style dependency declarations. Test-first commit `1e46b302254596397a6c4b0bb9ced1be02a33ea1` adds both forms and exposes the narrower manifest-key matcher. Commit `b24b9beb7a0804b90339a0e9e6abce3dd701fc4b` makes the manifest review fail closed on the canonical package token wherever it appears in a production crate manifest, covering direct keys, package aliases, and table syntax. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, and module-layout choices. ## Authority invariant From 6727474a15e85f66917821169cafcba89dbcfbdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 11:03:22 +0900 Subject: [PATCH 180/632] test(browser-session): reject latent adapter allowlist reservations --- ...rowser_session_trusted_adapter_boundary.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 66bdf1f7a..2e14befdc 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -95,6 +95,33 @@ def test_browser_session_dependencies_are_allowlisted(self) -> None: f"unreviewed production Browser Session dependencies: {sorted(unexpected)}", ) + def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: + discovered_port_references = set() + for path in ROOT.glob("crates/*/src/**/*.rs"): + relative = path.relative_to(ROOT).as_posix() + if relative.startswith(BROWSER_SESSION_SOURCE_ROOT): + continue + if _has_port_reference(path.read_text(encoding="utf-8")): + discovered_port_references.add(relative) + + discovered_dependencies = set() + for path in ROOT.glob("crates/*/Cargo.toml"): + if path == BROWSER_SESSION_CARGO: + continue + if _has_browser_session_dependency(path.read_text(encoding="utf-8")): + discovered_dependencies.add(path.relative_to(ROOT).as_posix()) + + self.assertEqual( + APPROVED_PRODUCTION_PORT_REFERENCES, + discovered_port_references, + "adapter source allowlist must describe current production references, not reserve future paths", + ) + self.assertEqual( + APPROVED_BROWSER_SESSION_DEPENDENCIES, + discovered_dependencies, + "dependency allowlist must describe current production links, not reserve future crates", + ) + def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> None: callers = set() for path in ROOT.glob("crates/*/src/**/*.rs"): From 6f2265e8d99cccd7bef89b2aaa4581854f093087 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 11:03:58 +0900 Subject: [PATCH 181/632] fix(browser-session): require allowlists to match current adapter surfaces --- tests/test_browser_session_trusted_adapter_boundary.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 2e14befdc..b01f95f10 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -10,13 +10,9 @@ BROWSER_SESSION_SOURCE_ROOT = "crates/originweave-browser-session/src/" # Any production reference to the lifecycle SPI outside the Browser Session owner is an explicit -# review surface. The reserved BiDi paths are the only currently approved external production owner. -APPROVED_PRODUCTION_PORT_REFERENCES = { - "crates/originweave-bidi/src/lifecycle_acl.rs", -} -APPROVED_BROWSER_SESSION_DEPENDENCIES = { - "crates/originweave-bidi/Cargo.toml", -} +# review surface. Allow only production surfaces that exist and were reviewed on this exact branch. +APPROVED_PRODUCTION_PORT_REFERENCES: set[str] = set() +APPROVED_BROWSER_SESSION_DEPENDENCIES: set[str] = set() PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") From bca47c4c032675b164afa3411dfc6de296bbba25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 11:04:22 +0900 Subject: [PATCH 182/632] docs(browser-session): remove future adapter pre-authorization --- .../browser-session-trusted-adapter-boundary.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index fe52a424c..cddc11c60 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -14,9 +14,9 @@ Treating an arbitrary in-process implementation as if it were an untrusted web a OriginWeave's threat model places the Rust control plane and privileged Chromium/browser adapters inside the trusted computing base. `originweave-browser-session` is an internal `publish = false` crate, not an extension SDK that promises isolation from hostile linked Rust code. A malicious crate already executing inside this trusted process is a supply-chain compromise / trusted-code compromise; it is not made safe by making one handle constructor opaque. -This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Any production source that references this SPI outside the Browser Session owner and any production crate that depends on `originweave-browser-session` is a repository review surface and must be explicitly allowlisted by contract. Test doubles remain allowed only under test code and grant no shipped product capability. +This does **not** make every implementation acceptable. Product composition may bind only a reviewed privileged lifecycle adapter; a caller-selected production adapter is not admitted. `DisposableContextPort` is an internal TCB SPI, not caller-selected product policy. Any production source that references this SPI outside the Browser Session owner and any production crate that depends on `originweave-browser-session` is a repository review surface and must be explicitly allowlisted by contract. An allowlist entry is evidence about a production surface that exists on the same exact branch, not permission reserved for a future implementation. Test doubles remain allowed only under test code and grant no shipped product capability. -The intended canonical production consumer is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi/src/lifecycle_acl.rs` once its stack is restacked onto the current Browser Session contract and passes review. Its crate manifest is the only reserved external Browser Session dependency. No other external production reference or crate dependency is admitted by this dossier. +No external production consumer is approved on the current #317 tree. The intended future consumer is the versioned WebDriver BiDi lifecycle adapter in `crates/originweave-bidi`, but the current BiDi manifest does not depend on Browser Session and `crates/originweave-bidi/src/lifecycle_acl.rs` does not exist. When #316 or a verified successor introduces that adapter, the implementation, manifest dependency, and exact allowlist entries must arrive in the same reviewed delta. Browser Session does not pre-authorize those future paths. ## Enforced repository contract @@ -25,10 +25,11 @@ The intended canonical production consumer is the versioned WebDriver BiDi lifec 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; 3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed source allowlist; -4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist; and -5. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. +4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist; +5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; and +6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. -The fifth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. +The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. ### Scanner false-negative repair @@ -40,7 +41,9 @@ A second review found the remaining cross-file alias case: one reviewed module c Cargo permits the dependency key itself to be renamed with `package = "originweave-browser-session"` and also permits table-style dependency declarations. Test-first commit `1e46b302254596397a6c4b0bb9ced1be02a33ea1` adds both forms and exposes the narrower manifest-key matcher. Commit `b24b9beb7a0804b90339a0e9e6abce3dd701fc4b` makes the manifest review fail closed on the canonical package token wherever it appears in a production crate manifest, covering direct keys, package aliases, and table syntax. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, and module-layout choices. +A third review found a governance hole in the allowlist itself. The contract pre-listed the future BiDi source path and manifest even though neither current production surface existed. That meant a later change could introduce exactly those surfaces without modifying the security contract, turning a supposedly explicit review surface into latent permission. Test-first commit `6727474a15e85f66917821169cafcba89dbcfbdb` requires both allowlists to equal the surfaces actually discovered on the current tree and therefore fails on those future reservations. Commit `6f2265e8d99cccd7bef89b2aaa4581854f093087` removes the reservations. A future BiDi adapter must now widen the allowlist in the same reviewed change that introduces its source and dependency. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, module-layout choices, and future composition changes. ## Authority invariant @@ -62,6 +65,6 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; -- the reviewed production composition path and the versioned BiDi adapter must satisfy the source-reference and crate-dependency allowlist contracts when introduced/restacked; +- any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From f59f81f86173f199a0718672b8e722df46890627 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 11:06:51 +0900 Subject: [PATCH 183/632] docs(changelog): record trusted-adapter allowlist currentness repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78221c17..56075b243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Removed latent Browser Session adapter pre-authorization from the trusted-adapter allowlists. Approved source and dependency entries must now correspond to production surfaces present on the same exact tree, so the future BiDi lifecycle adapter must widen the allowlists in the same reviewed change that introduces its implementation and crate dependency. - Hardened the Browser Session trusted-adapter repository contract so fully qualified or aliased `DisposableContextPort` references and UFCS/whitespace `bind_lifecycle_port` spellings cannot evade review-surface detection; this changes no Rust production behavior or trust classification. - Corrected the root Browser Session architecture contract so `PresentationMutationAuthority` is explicitly bound to `BrowserSessionIncarnation`; the incarnation participates in authorization validation and provides sequential-ABA separation when external session/context identifiers and local epochs are reused, while the disposable-isolation identity remains the exact remote lifecycle boundary rather than a substitute for aggregate incarnation. - Prevented ownership-clean `TransportLost` sessions from entering Browser Session recovery custody. Recovery handoff now requires exact unresolved recovery/create-attempt evidence for `TransportLost`, while `RecoveryRequired` remains recovery-eligible; transport loss before remote ownership or after proven destruction therefore cannot mint a purpose-bounded adapter-operation capability. From 97b925c0d7c957f99e9b798f45decac70877cd40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 12:01:02 +0900 Subject: [PATCH 184/632] test: expose workspace dependency alias escape --- ...t_browser_session_trusted_adapter_boundary.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index b01f95f10..8078844c3 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -4,6 +4,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] +ROOT_CARGO = ROOT / "Cargo.toml" BROWSER_SESSION_CARGO = ROOT / "crates/originweave-browser-session/Cargo.toml" THREAT_MODEL = ROOT / "docs/THREAT_MODEL.md" DOSSIER = ROOT / "docs/traceability/browser-session-trusted-adapter-boundary.md" @@ -176,6 +177,21 @@ def test_cross_file_alias_cannot_escape_dependency_review_surface(self) -> None: f"Browser Session dependency spelling escaped review scanner: {manifest!r}", ) + def test_workspace_dependency_alias_cannot_escape_dependency_review_surface(self) -> None: + workspace_manifest = ( + "[workspace.dependencies]\n" + 'browser_session = { package = "originweave-browser-session", path = "crates/originweave-browser-session" }\n' + ) + member_manifest = ( + "[dependencies]\n" + "browser_session = { workspace = true }\n" + ) + + self.assertTrue( + _manifest_links_browser_session(member_manifest, workspace_manifest), + "workspace dependency aliases must remain an explicit Browser Session TCB review surface", + ) + if __name__ == "__main__": unittest.main() From aeea79c5d57a1cb1c7c5d3f760a2d728214d8a09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 12:01:35 +0900 Subject: [PATCH 185/632] test: close workspace dependency alias escape --- ...rowser_session_trusted_adapter_boundary.py | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 8078844c3..468b92788 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -1,5 +1,6 @@ import pathlib import re +import tomllib import unittest @@ -18,6 +19,7 @@ PORT_REFERENCE = re.compile(r"\bDisposableContextPort\b") LIFECYCLE_BINDING = re.compile(r"\bbind_lifecycle_port\b") BROWSER_SESSION_DEPENDENCY = re.compile(r"\boriginweave-browser-session\b") +BROWSER_SESSION_PACKAGE = "originweave-browser-session" def _has_port_reference(text: str) -> bool: @@ -32,6 +34,72 @@ def _has_browser_session_dependency(text: str) -> bool: return BROWSER_SESSION_DEPENDENCY.search(text) is not None +def _dependency_package_name( + dependency_name: str, + dependency_spec: object, + workspace_dependencies: dict[str, object], +) -> str: + if not isinstance(dependency_spec, dict): + return dependency_name + + package = dependency_spec.get("package") + if isinstance(package, str): + return package + + if dependency_spec.get("workspace") is True: + workspace_spec = workspace_dependencies.get(dependency_name) + if isinstance(workspace_spec, dict): + workspace_package = workspace_spec.get("package") + if isinstance(workspace_package, str): + return workspace_package + if workspace_spec is not None: + return dependency_name + + return dependency_name + + +def _manifest_dependency_sections(manifest: dict[str, object]) -> list[dict[str, object]]: + sections: list[dict[str, object]] = [] + dependencies = manifest.get("dependencies") + if isinstance(dependencies, dict): + sections.append(dependencies) + + targets = manifest.get("target") + if isinstance(targets, dict): + for target in targets.values(): + if not isinstance(target, dict): + continue + target_dependencies = target.get("dependencies") + if isinstance(target_dependencies, dict): + sections.append(target_dependencies) + + return sections + + +def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bool: + member = tomllib.loads(member_text) + workspace_manifest = tomllib.loads(workspace_text) + workspace = workspace_manifest.get("workspace") + workspace_dependencies: dict[str, object] = {} + if isinstance(workspace, dict): + declared = workspace.get("dependencies") + if isinstance(declared, dict): + workspace_dependencies = declared + + for section in _manifest_dependency_sections(member): + for dependency_name, dependency_spec in section.items(): + if ( + _dependency_package_name( + dependency_name, + dependency_spec, + workspace_dependencies, + ) + == BROWSER_SESSION_PACKAGE + ): + return True + return False + + class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" @@ -78,11 +146,12 @@ def test_production_disposable_context_port_references_are_allowlisted(self) -> def test_browser_session_dependencies_are_allowlisted(self) -> None: discovered = set() + workspace_text = ROOT_CARGO.read_text(encoding="utf-8") for path in ROOT.glob("crates/*/Cargo.toml"): if path == BROWSER_SESSION_CARGO: continue text = path.read_text(encoding="utf-8") - if _has_browser_session_dependency(text): + if _manifest_links_browser_session(text, workspace_text): discovered.add(path.relative_to(ROOT).as_posix()) unexpected = discovered - APPROVED_BROWSER_SESSION_DEPENDENCIES @@ -102,10 +171,11 @@ def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: discovered_port_references.add(relative) discovered_dependencies = set() + workspace_text = ROOT_CARGO.read_text(encoding="utf-8") for path in ROOT.glob("crates/*/Cargo.toml"): if path == BROWSER_SESSION_CARGO: continue - if _has_browser_session_dependency(path.read_text(encoding="utf-8")): + if _manifest_links_browser_session(path.read_text(encoding="utf-8"), workspace_text): discovered_dependencies.add(path.relative_to(ROOT).as_posix()) self.assertEqual( From 1d269e844692304d9433604ea01b9eee93a0c294 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 12:02:43 +0900 Subject: [PATCH 186/632] docs: trace workspace dependency trust boundary --- .../browser-session-trusted-adapter-boundary.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index cddc11c60..e5173c0f9 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -25,7 +25,7 @@ No external production consumer is approved on the current #317 tree. The intend 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; 3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed source allowlist; -4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist; +4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist, including direct package aliases, table syntax, target-specific production dependencies, and workspace-inherited aliases resolved through root `[workspace.dependencies]`; 5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; and 6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. @@ -43,7 +43,9 @@ Cargo permits the dependency key itself to be renamed with `package = "originwea A third review found a governance hole in the allowlist itself. The contract pre-listed the future BiDi source path and manifest even though neither current production surface existed. That meant a later change could introduce exactly those surfaces without modifying the security contract, turning a supposedly explicit review surface into latent permission. Test-first commit `6727474a15e85f66917821169cafcba89dbcfbdb` requires both allowlists to equal the surfaces actually discovered on the current tree and therefore fails on those future reservations. Commit `6f2265e8d99cccd7bef89b2aaa4581854f093087` removes the reservations. A future BiDi adapter must now widen the allowlist in the same reviewed change that introduces its source and dependency. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, module-layout choices, and future composition changes. +A fourth review found that Cargo workspace inheritance could bypass the manifest scanner without ever spelling the canonical package name in the consuming crate. A root declaration such as `browser_session = { package = "originweave-browser-session", ... }` under `[workspace.dependencies]` can be consumed by a member as `browser_session = { workspace = true }`; the previous per-member regex saw only the alias. Test-first commit `97b925c0d7c957f99e9b798f45decac70877cd40` records that escape. Commit `aeea79c5d57a1cb1c7c5d3f760a2d728214d8a09` parses Cargo TOML, resolves workspace-inherited dependency aliases to their canonical package, and applies the same review surface to target-specific production dependencies. Dev-only dependencies remain outside the shipped adapter-composition surface. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, and future composition changes. ## Authority invariant From d20ab4643d50f7bedb0a6d4d83a17a689f5129ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 12:04:13 +0900 Subject: [PATCH 187/632] docs: record workspace dependency alias hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56075b243..272a7d374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Hardened Browser Session trusted-adapter dependency discovery so Cargo workspace-inherited aliases and target-specific production dependencies resolve to the canonical `originweave-browser-session` package before allowlist comparison; dev-only dependencies do not widen the shipped adapter-composition surface. - Removed latent Browser Session adapter pre-authorization from the trusted-adapter allowlists. Approved source and dependency entries must now correspond to production surfaces present on the same exact tree, so the future BiDi lifecycle adapter must widen the allowlists in the same reviewed change that introduces its implementation and crate dependency. - Hardened the Browser Session trusted-adapter repository contract so fully qualified or aliased `DisposableContextPort` references and UFCS/whitespace `bind_lifecycle_port` spellings cannot evade review-surface detection; this changes no Rust production behavior or trust classification. - Corrected the root Browser Session architecture contract so `PresentationMutationAuthority` is explicitly bound to `BrowserSessionIncarnation`; the incarnation participates in authorization validation and provides sequential-ABA separation when external session/context identifiers and local epochs are reused, while the disposable-isolation identity remains the exact remote lifecycle boundary rather than a substitute for aggregate incarnation. From 2c6c9d9aa8e58c69bfeec1ac6b59aea612cdb2a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 12:12:21 +0900 Subject: [PATCH 188/632] test: cover target-specific workspace alias --- ...st_browser_session_trusted_adapter_boundary.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 468b92788..a05de341f 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -252,15 +252,18 @@ def test_workspace_dependency_alias_cannot_escape_dependency_review_surface(self "[workspace.dependencies]\n" 'browser_session = { package = "originweave-browser-session", path = "crates/originweave-browser-session" }\n' ) - member_manifest = ( + member_manifests = ( "[dependencies]\n" - "browser_session = { workspace = true }\n" + "browser_session = { workspace = true }\n", + "[target.'cfg(unix)'.dependencies]\n" + "browser_session = { workspace = true }\n", ) - self.assertTrue( - _manifest_links_browser_session(member_manifest, workspace_manifest), - "workspace dependency aliases must remain an explicit Browser Session TCB review surface", - ) + for member_manifest in member_manifests: + self.assertTrue( + _manifest_links_browser_session(member_manifest, workspace_manifest), + "workspace dependency aliases must remain an explicit Browser Session TCB review surface", + ) if __name__ == "__main__": From 93635d092cfcaf613e88770da003b45b6018fa23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:03:34 +0900 Subject: [PATCH 189/632] test: expose workspace-member adapter review escape --- ...rowser_session_trusted_adapter_boundary.py | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index a05de341f..9ebbf2c12 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -1,5 +1,6 @@ import pathlib import re +import tempfile import tomllib import unittest @@ -100,6 +101,18 @@ def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bo return False +def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: + """Return production manifests covered by the current repository contract.""" + return sorted(root.glob("crates/*/Cargo.toml")) + + +def _workspace_production_sources(root: pathlib.Path) -> list[pathlib.Path]: + sources: list[pathlib.Path] = [] + for manifest in _workspace_member_manifests(root): + sources.extend(sorted(manifest.parent.glob("src/**/*.rs"))) + return sources + + class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" @@ -129,7 +142,7 @@ def test_trusted_adapter_dossier_states_the_supported_security_boundary(self) -> def test_production_disposable_context_port_references_are_allowlisted(self) -> None: discovered = set() - for path in ROOT.glob("crates/*/src/**/*.rs"): + for path in _workspace_production_sources(ROOT): relative = path.relative_to(ROOT).as_posix() if relative.startswith(BROWSER_SESSION_SOURCE_ROOT): continue @@ -147,7 +160,7 @@ def test_production_disposable_context_port_references_are_allowlisted(self) -> def test_browser_session_dependencies_are_allowlisted(self) -> None: discovered = set() workspace_text = ROOT_CARGO.read_text(encoding="utf-8") - for path in ROOT.glob("crates/*/Cargo.toml"): + for path in _workspace_member_manifests(ROOT): if path == BROWSER_SESSION_CARGO: continue text = path.read_text(encoding="utf-8") @@ -163,7 +176,7 @@ def test_browser_session_dependencies_are_allowlisted(self) -> None: def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: discovered_port_references = set() - for path in ROOT.glob("crates/*/src/**/*.rs"): + for path in _workspace_production_sources(ROOT): relative = path.relative_to(ROOT).as_posix() if relative.startswith(BROWSER_SESSION_SOURCE_ROOT): continue @@ -172,7 +185,7 @@ def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: discovered_dependencies = set() workspace_text = ROOT_CARGO.read_text(encoding="utf-8") - for path in ROOT.glob("crates/*/Cargo.toml"): + for path in _workspace_member_manifests(ROOT): if path == BROWSER_SESSION_CARGO: continue if _manifest_links_browser_session(path.read_text(encoding="utf-8"), workspace_text): @@ -191,7 +204,7 @@ def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: def test_product_sources_do_not_bind_a_caller_selected_lifecycle_port(self) -> None: callers = set() - for path in ROOT.glob("crates/*/src/**/*.rs"): + for path in _workspace_production_sources(ROOT): if path == ROOT / "crates/originweave-browser-session/src/browser_session.rs": continue text = path.read_text(encoding="utf-8") @@ -265,6 +278,31 @@ def test_workspace_dependency_alias_cannot_escape_dependency_review_surface(self "workspace dependency aliases must remain an explicit Browser Session TCB review surface", ) + def test_workspace_member_outside_crates_glob_cannot_escape_review_surface(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["plugins/browser-adapter"]\n', + encoding="utf-8", + ) + member = root / "plugins/browser-adapter" + member.mkdir(parents=True) + member_manifest = member / "Cargo.toml" + member_manifest.write_text( + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\n' + '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + encoding="utf-8", + ) + source = member / "src/lib.rs" + source.parent.mkdir() + source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + + self.assertIn(member_manifest, _workspace_member_manifests(root)) + self.assertIn(source, _workspace_production_sources(root)) + if __name__ == "__main__": unittest.main() From 9aca127b8ae18a6af38353c4023a6c5361c75e85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:04:06 +0900 Subject: [PATCH 190/632] test: derive adapter review from Cargo workspace members --- ...rowser_session_trusted_adapter_boundary.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 9ebbf2c12..322e4c423 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -102,8 +102,29 @@ def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bo def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: - """Return production manifests covered by the current repository contract.""" - return sorted(root.glob("crates/*/Cargo.toml")) + """Return every explicitly declared Cargo workspace member manifest.""" + root_manifest_path = root / "Cargo.toml" + root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + workspace = root_manifest.get("workspace") + if not isinstance(workspace, dict): + raise AssertionError("repository root must declare a Cargo workspace") + members = workspace.get("members") + if not isinstance(members, list): + raise AssertionError("Cargo workspace members must be an explicit reviewed list") + + manifests: list[pathlib.Path] = [] + for member in members: + if not isinstance(member, str) or not member: + raise AssertionError("Cargo workspace member paths must be non-empty strings") + if any(token in member for token in ("*", "?", "[")): + raise AssertionError( + "Cargo workspace member globs require an explicit trusted-adapter contract update" + ) + manifest = root / member / "Cargo.toml" + if not manifest.is_file(): + raise AssertionError(f"workspace member manifest is missing: {member}/Cargo.toml") + manifests.append(manifest) + return sorted(manifests) def _workspace_production_sources(root: pathlib.Path) -> list[pathlib.Path]: @@ -303,6 +324,16 @@ def test_workspace_member_outside_crates_glob_cannot_escape_review_surface(self) self.assertIn(member_manifest, _workspace_member_manifests(root)) self.assertIn(source, _workspace_production_sources(root)) + def test_workspace_member_globs_fail_closed_until_reviewed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["plugins/*"]\n', + encoding="utf-8", + ) + with self.assertRaisesRegex(AssertionError, "member globs require"): + _workspace_member_manifests(root) + if __name__ == "__main__": unittest.main() From 1c9d96be6180d16545e9045eaf1833cd7a5a8332 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:05:59 +0900 Subject: [PATCH 191/632] docs: trace Cargo workspace adapter coverage --- .../browser-session-trusted-adapter-boundary.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index e5173c0f9..beb310516 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -26,10 +26,11 @@ No external production consumer is approved on the current #317 tree. The intend 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; 3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed source allowlist; 4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist, including direct package aliases, table syntax, target-specific production dependencies, and workspace-inherited aliases resolved through root `[workspace.dependencies]`; -5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; and -6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling. +5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; +6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling; and +7. the source and manifest review surface is derived from the repository's explicit Cargo `[workspace].members`, not from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed. -The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. +The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh rule prevents a new workspace member outside `crates/*` from gaining Browser Session linkage or SPI access without entering the same repository review surface. ### Scanner false-negative repair @@ -45,7 +46,9 @@ A third review found a governance hole in the allowlist itself. The contract pre A fourth review found that Cargo workspace inheritance could bypass the manifest scanner without ever spelling the canonical package name in the consuming crate. A root declaration such as `browser_session = { package = "originweave-browser-session", ... }` under `[workspace.dependencies]` can be consumed by a member as `browser_session = { workspace = true }`; the previous per-member regex saw only the alias. Test-first commit `97b925c0d7c957f99e9b798f45decac70877cd40` records that escape. Commit `aeea79c5d57a1cb1c7c5d3f760a2d728214d8a09` parses Cargo TOML, resolves workspace-inherited dependency aliases to their canonical package, and applies the same review surface to target-specific production dependencies. Dev-only dependencies remain outside the shipped adapter-composition surface. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, and future composition changes. +A fifth review found that the hardened scanner still discovered production manifests and Rust sources with `crates/*` filesystem globs instead of Cargo's authoritative workspace membership. Cargo permits explicit workspace members at arbitrary relative paths, so a later `plugins/browser-adapter` member could link Browser Session and reference the lifecycle SPI while remaining invisible to the fixed directory glob. Structural RED `93635d092cfcaf613e88770da003b45b6018fa23` adds a hostile workspace member outside `crates/*` and demonstrates that escape. Minimal repair `9aca127b8ae18a6af38353c4023a6c5361c75e85` parses root `[workspace].members`, verifies each explicit member manifest exists, derives production Rust scanning from those members, and fails closed on workspace-member glob syntax until the contract is deliberately extended. The current repository already uses an explicit workspace-member list, so this widens review coverage without changing production Rust or the trust classification. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, workspace-member placement, and future composition changes. ## Authority invariant @@ -58,6 +61,7 @@ A reviewed adapter must create a fresh disposable browser boundary, keep remote - **Caller-visible nonce or opaque request as adapter authentication:** rejected because the implementation receives the value and can echo it without performing browser I/O. - **Generic public `TrustedPort` marker trait:** rejected because arbitrary Rust code can implement an unsealed marker and the name creates no security property. - **Sealing `DisposableContextPort` inside `originweave-browser-session`:** not adopted because the canonical versioned browser adapter lives in a separate crate; Rust has no friend-crate visibility, so sealing here would either break the adapter boundary or force protocol code into the Browser Session owner. +- **Hard-coding `crates/*` as the production composition boundary:** rejected because Cargo workspace membership, not directory placement, determines which production crates are built together. - **Moving deterministic browser policy into WebDriver BiDi/MCP:** rejected; adapters translate qualified browser state and never become policy authority. - **`--no-sandbox` or browser-process weakening:** unrelated and forbidden. @@ -68,5 +72,6 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; - any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; +- any future change from explicit Cargo workspace members to member globs must first extend this security contract rather than silently widening the scanner's trust surface; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From a291d0a4548ce33217f715d8d70ebe1e4e071752 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:11:39 +0900 Subject: [PATCH 192/632] docs: record workspace-member trust repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 272a7d374..1ae71fb03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Derived Browser Session trusted-adapter source and manifest review coverage from explicit Cargo `[workspace].members` instead of the `crates/*` directory convention, so production workspace members at other paths cannot evade lifecycle-SPI/dependency/binding review; workspace-member globs now fail closed until the security contract is explicitly extended. - Hardened Browser Session trusted-adapter dependency discovery so Cargo workspace-inherited aliases and target-specific production dependencies resolve to the canonical `originweave-browser-session` package before allowlist comparison; dev-only dependencies do not widen the shipped adapter-composition surface. - Removed latent Browser Session adapter pre-authorization from the trusted-adapter allowlists. Approved source and dependency entries must now correspond to production surfaces present on the same exact tree, so the future BiDi lifecycle adapter must widen the allowlists in the same reviewed change that introduces its implementation and crate dependency. - Hardened the Browser Session trusted-adapter repository contract so fully qualified or aliased `DisposableContextPort` references and UFCS/whitespace `bind_lifecycle_port` spellings cannot evade review-surface detection; this changes no Rust production behavior or trust classification. From 89f1ce04a7ba4dfbb8157a849e419f842cb1a18c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:13:13 +0900 Subject: [PATCH 193/632] test: expose workspace-root adapter review escape --- ...rowser_session_trusted_adapter_boundary.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 322e4c423..a38047b47 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -324,6 +324,26 @@ def test_workspace_member_outside_crates_glob_cannot_escape_review_surface(self) self.assertIn(member_manifest, _workspace_member_manifests(root)) self.assertIn(source, _workspace_production_sources(root)) + def test_workspace_root_package_cannot_escape_review_surface(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + root_manifest = root / "Cargo.toml" + root_manifest.write_text( + '[package]\nname = "root-browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[workspace]\nmembers = []\n' + '[dependencies]\noriginweave-browser-session = { path = "crates/originweave-browser-session" }\n', + encoding="utf-8", + ) + source = root / "src/lib.rs" + source.parent.mkdir() + source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + + self.assertIn(root_manifest, _workspace_member_manifests(root)) + self.assertIn(source, _workspace_production_sources(root)) + def test_workspace_member_globs_fail_closed_until_reviewed(self) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) From 36f13665553323f70f44f25a6bdf52a0b4b178ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:15:23 +0900 Subject: [PATCH 194/632] test: include workspace root package in adapter review --- tests/test_browser_session_trusted_adapter_boundary.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index a38047b47..631cb8264 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -102,7 +102,7 @@ def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bo def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: - """Return every explicitly declared Cargo workspace member manifest.""" + """Return every reviewed Cargo workspace package manifest.""" root_manifest_path = root / "Cargo.toml" root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) workspace = root_manifest.get("workspace") @@ -112,7 +112,10 @@ def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: if not isinstance(members, list): raise AssertionError("Cargo workspace members must be an explicit reviewed list") - manifests: list[pathlib.Path] = [] + manifests: set[pathlib.Path] = set() + if isinstance(root_manifest.get("package"), dict): + manifests.add(root_manifest_path) + for member in members: if not isinstance(member, str) or not member: raise AssertionError("Cargo workspace member paths must be non-empty strings") @@ -123,7 +126,7 @@ def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: manifest = root / member / "Cargo.toml" if not manifest.is_file(): raise AssertionError(f"workspace member manifest is missing: {member}/Cargo.toml") - manifests.append(manifest) + manifests.add(manifest) return sorted(manifests) From b6790510311347006712abe530057f16527d5f80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:15:57 +0900 Subject: [PATCH 195/632] docs: trace workspace-root adapter coverage --- .../browser-session-trusted-adapter-boundary.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index beb310516..c60426a10 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -28,9 +28,9 @@ No external production consumer is approved on the current #317 tree. The intend 4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist, including direct package aliases, table syntax, target-specific production dependencies, and workspace-inherited aliases resolved through root `[workspace.dependencies]`; 5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; 6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling; and -7. the source and manifest review surface is derived from the repository's explicit Cargo `[workspace].members`, not from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed. +7. the source and manifest review surface is derived from the Cargo workspace's explicit `[workspace].members` plus the workspace-root package when `[package]` is present, rather than from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed. -The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh rule prevents a new workspace member outside `crates/*` from gaining Browser Session linkage or SPI access without entering the same repository review surface. +The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh rule prevents both an explicit workspace member outside `crates/*` and a future workspace-root package from gaining Browser Session linkage or SPI access without entering the same repository review surface. ### Scanner false-negative repair @@ -48,7 +48,9 @@ A fourth review found that Cargo workspace inheritance could bypass the manifest A fifth review found that the hardened scanner still discovered production manifests and Rust sources with `crates/*` filesystem globs instead of Cargo's authoritative workspace membership. Cargo permits explicit workspace members at arbitrary relative paths, so a later `plugins/browser-adapter` member could link Browser Session and reference the lifecycle SPI while remaining invisible to the fixed directory glob. Structural RED `93635d092cfcaf613e88770da003b45b6018fa23` adds a hostile workspace member outside `crates/*` and demonstrates that escape. Minimal repair `9aca127b8ae18a6af38353c4023a6c5361c75e85` parses root `[workspace].members`, verifies each explicit member manifest exists, derives production Rust scanning from those members, and fails closed on workspace-member glob syntax until the contract is deliberately extended. The current repository already uses an explicit workspace-member list, so this widens review coverage without changing production Rust or the trust classification. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, workspace-member placement, and future composition changes. +A sixth review checked Cargo's workspace-root package rule rather than assuming every package must appear in `[workspace].members`. If the workspace root later gains a `[package]` table, that root package is part of the workspace even when `members = []`; the fifth-generation helper would have ignored the root manifest and `src/**/*.rs`, allowing a root package to link Browser Session or reference/bind its lifecycle SPI without entering the review surface. Structural RED `89f1ce04a7ba4dfbb8157a849e419f842cb1a18c` adds that hostile root-package fixture. Minimal repair `36f13665553323f70f44f25a6bdf52a0b4b178ac` includes the root manifest whenever `[package]` is present while preserving explicit-member validation and the member-glob fail-closed rule. The repository is currently a virtual workspace, so this is prospective fail-closed coverage rather than a production topology change. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, explicit workspace-member placement, workspace-root package placement, and future composition changes. ## Authority invariant @@ -61,7 +63,7 @@ A reviewed adapter must create a fresh disposable browser boundary, keep remote - **Caller-visible nonce or opaque request as adapter authentication:** rejected because the implementation receives the value and can echo it without performing browser I/O. - **Generic public `TrustedPort` marker trait:** rejected because arbitrary Rust code can implement an unsealed marker and the name creates no security property. - **Sealing `DisposableContextPort` inside `originweave-browser-session`:** not adopted because the canonical versioned browser adapter lives in a separate crate; Rust has no friend-crate visibility, so sealing here would either break the adapter boundary or force protocol code into the Browser Session owner. -- **Hard-coding `crates/*` as the production composition boundary:** rejected because Cargo workspace membership, not directory placement, determines which production crates are built together. +- **Hard-coding `crates/*` as the production composition boundary:** rejected because Cargo workspace/package membership, not directory placement, determines which production crates are built together. - **Moving deterministic browser policy into WebDriver BiDi/MCP:** rejected; adapters translate qualified browser state and never become policy authority. - **`--no-sandbox` or browser-process weakening:** unrelated and forbidden. @@ -72,6 +74,6 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; - any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; -- any future change from explicit Cargo workspace members to member globs must first extend this security contract rather than silently widening the scanner's trust surface; +- any future change from explicit Cargo workspace members to member globs, or introduction of a root package, must remain inside this security contract's reviewed manifest/source surface rather than silently widening trust; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From b0931ae8b71994ad96689b7e109ff09cca21bd72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:28:49 +0900 Subject: [PATCH 196/632] test(browser-session): expose implicit Cargo workspace member gap --- ...sion_implicit_workspace_member_contract.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_browser_session_implicit_workspace_member_contract.py diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py new file mode 100644 index 000000000..b038a8cbb --- /dev/null +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -0,0 +1,58 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + +spec = importlib.util.spec_from_file_location("browser_session_trusted_adapter_boundary", BOUNDARY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session trusted-adapter boundary contract") +boundary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(boundary) + + +class BrowserSessionImplicitWorkspaceMemberContractTests(unittest.TestCase): + """Match Cargo's automatic in-workspace path-dependency membership semantics.""" + + def test_in_workspace_path_dependency_cannot_escape_review_surface(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["app"]\nresolver = "3"\n', + encoding="utf-8", + ) + + app = root / "app" + app.mkdir() + (app / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\nbrowser-adapter = { path = "../plugins/browser-adapter" }\n', + encoding="utf-8", + ) + (app / "src").mkdir() + (app / "src/lib.rs").write_text("pub fn app() {}\n", encoding="utf-8") + + adapter = root / "plugins/browser-adapter" + adapter.mkdir(parents=True) + adapter_manifest = adapter / "Cargo.toml" + adapter_manifest.write_text( + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + encoding="utf-8", + ) + adapter_source = adapter / "src/lib.rs" + adapter_source.parent.mkdir() + adapter_source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + + self.assertIn(adapter_manifest, boundary._workspace_member_manifests(root)) + self.assertIn(adapter_source, boundary._workspace_production_sources(root)) + + +if __name__ == "__main__": + unittest.main() From a7798d229631b0f0ab2a8b1132fb08f250df8ecd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:29:51 +0900 Subject: [PATCH 197/632] test(browser-session): cover implicit Cargo path members --- ...sion_implicit_workspace_member_contract.py | 105 +++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index b038a8cbb..5fb5d3adf 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -1,6 +1,7 @@ import importlib.util import pathlib import tempfile +import tomllib import unittest @@ -14,8 +15,74 @@ spec.loader.exec_module(boundary) +def _in_repository_path_dependency( + root: pathlib.Path, + manifest: pathlib.Path, + dependency_name: str, + dependency_spec: object, + workspace_dependencies: dict[str, object], +) -> pathlib.Path | None: + spec = dependency_spec + base = manifest.parent + if isinstance(spec, dict) and spec.get("workspace") is True: + spec = workspace_dependencies.get(dependency_name) + base = root + if not isinstance(spec, dict): + return None + + declared_path = spec.get("path") + if not isinstance(declared_path, str) or not declared_path: + return None + + root_resolved = root.resolve() + candidate = (base / declared_path / "Cargo.toml").resolve() + try: + candidate.relative_to(root_resolved) + except ValueError: + return None + return candidate if candidate.is_file() else None + + +def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: + """Return reviewed workspace packages plus recursive in-repository path dependencies.""" + root_manifest_path = root / "Cargo.toml" + root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + workspace = root_manifest.get("workspace") + workspace_dependencies: dict[str, object] = {} + if isinstance(workspace, dict): + declared = workspace.get("dependencies") + if isinstance(declared, dict): + workspace_dependencies = declared + + manifests = set(boundary._workspace_member_manifests(root)) + pending = list(manifests) + while pending: + manifest = pending.pop() + parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) + for section in boundary._manifest_dependency_sections(parsed): + for dependency_name, dependency_spec in section.items(): + candidate = _in_repository_path_dependency( + root, + manifest, + dependency_name, + dependency_spec, + workspace_dependencies, + ) + if candidate is not None and candidate not in manifests: + manifests.add(candidate) + pending.append(candidate) + return sorted(manifests) + + +def _production_sources(root: pathlib.Path) -> list[pathlib.Path]: + sources: list[pathlib.Path] = [] + for manifest in _production_package_manifests(root): + sources.extend(sorted(manifest.parent.glob("src/**/*.rs"))) + return sources + + class BrowserSessionImplicitWorkspaceMemberContractTests(unittest.TestCase): - """Match Cargo's automatic in-workspace path-dependency membership semantics.""" + """Cover Cargo's automatic in-workspace path-dependency membership semantics.""" def test_in_workspace_path_dependency_cannot_escape_review_surface(self) -> None: with tempfile.TemporaryDirectory() as directory: @@ -50,8 +117,40 @@ def test_in_workspace_path_dependency_cannot_escape_review_surface(self) -> None encoding="utf-8", ) - self.assertIn(adapter_manifest, boundary._workspace_member_manifests(root)) - self.assertIn(adapter_source, boundary._workspace_production_sources(root)) + self.assertIn(adapter_manifest, _production_package_manifests(root)) + self.assertIn(adapter_source, _production_sources(root)) + + def test_recursive_local_path_dependencies_obey_existing_tcb_allowlists(self) -> None: + workspace_text = (ROOT / "Cargo.toml").read_text(encoding="utf-8") + + discovered_port_references = set() + for path in _production_sources(ROOT): + relative = path.relative_to(ROOT).as_posix() + if relative.startswith(boundary.BROWSER_SESSION_SOURCE_ROOT): + continue + if boundary._has_port_reference(path.read_text(encoding="utf-8")): + discovered_port_references.add(relative) + + discovered_dependencies = set() + for path in _production_package_manifests(ROOT): + if path == boundary.BROWSER_SESSION_CARGO: + continue + if boundary._manifest_links_browser_session( + path.read_text(encoding="utf-8"), + workspace_text, + ): + discovered_dependencies.add(path.relative_to(ROOT).as_posix()) + + self.assertEqual( + boundary.APPROVED_PRODUCTION_PORT_REFERENCES, + discovered_port_references, + "recursive local path dependencies must not add unreviewed lifecycle-port source", + ) + self.assertEqual( + boundary.APPROVED_BROWSER_SESSION_DEPENDENCIES, + discovered_dependencies, + "recursive local path dependencies must not link Browser Session outside the reviewed allowlist", + ) if __name__ == "__main__": From 13cf9d8753a81494dc02343f60a85acc5f5c762a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:30:35 +0900 Subject: [PATCH 198/632] docs(browser-session): trace implicit Cargo path members --- .../browser-session-trusted-adapter-boundary.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index c60426a10..c0c365579 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -20,17 +20,18 @@ No external production consumer is approved on the current #317 tree. The intend ## Enforced repository contract -`tests/test_browser_session_trusted_adapter_boundary.py` enforces the currently supportable boundary: +`tests/test_browser_session_trusted_adapter_boundary.py` and `tests/test_browser_session_implicit_workspace_member_contract.py` enforce the currently supportable boundary: 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; 3. production references to `DisposableContextPort` outside the Browser Session owner are limited to the explicit reviewed source allowlist; 4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist, including direct package aliases, table syntax, target-specific production dependencies, and workspace-inherited aliases resolved through root `[workspace.dependencies]`; 5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; -6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling; and -7. the source and manifest review surface is derived from the Cargo workspace's explicit `[workspace].members` plus the workspace-root package when `[package]` is present, rather than from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed. +6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling; +7. the primary source and manifest review surface is derived from the Cargo workspace's explicit `[workspace].members` plus the workspace-root package when `[package]` is present, rather than from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed; and +8. recursive in-repository production `path` dependencies are also reviewed, because Cargo automatically makes path dependencies residing in the workspace directory workspace members even when they are omitted from the explicit `members` list. An implicit local package therefore cannot hide a Browser Session dependency or lifecycle-SPI reference behind another member's `path = ...` edge. -The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh rule prevents both an explicit workspace member outside `crates/*` and a future workspace-root package from gaining Browser Session linkage or SPI access without entering the same repository review surface. +The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh and eighth rules prevent explicit members outside `crates/*`, a future workspace-root package, and automatically enrolled in-workspace path dependencies from widening the trusted composition surface without entering the same repository review boundary. ### Scanner false-negative repair @@ -50,7 +51,9 @@ A fifth review found that the hardened scanner still discovered production manif A sixth review checked Cargo's workspace-root package rule rather than assuming every package must appear in `[workspace].members`. If the workspace root later gains a `[package]` table, that root package is part of the workspace even when `members = []`; the fifth-generation helper would have ignored the root manifest and `src/**/*.rs`, allowing a root package to link Browser Session or reference/bind its lifecycle SPI without entering the review surface. Structural RED `89f1ce04a7ba4dfbb8157a849e419f842cb1a18c` adds that hostile root-package fixture. Minimal repair `36f13665553323f70f44f25a6bdf52a0b4b178ac` includes the root manifest whenever `[package]` is present while preserving explicit-member validation and the member-glob fail-closed rule. The repository is currently a virtual workspace, so this is prospective fail-closed coverage rather than a production topology change. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, explicit workspace-member placement, workspace-root package placement, and future composition changes. +A seventh review checked the remaining Cargo membership rule against the current Cargo Book: **all path dependencies residing inside the workspace directory automatically become workspace members**, even when their package paths are absent from `[workspace].members`. The sixth-generation helper still treated the explicit list plus root package as exhaustive, so `app -> path ../plugins/browser-adapter -> originweave-browser-session` could place a production adapter inside the Cargo workspace while its manifest and source escaped the review scan. Structural RED `b0931ae8b71994ad96689b7e109ff09cca21bd72` adds that hostile implicit-member fixture. Repair `a7798d229631b0f0ab2a8b1132fb08f250df8ecd` follows production dependency sections recursively, resolves direct and workspace-inherited local `path` dependencies relative to their Cargo-defined bases, rejects paths outside the repository review root, and applies the existing source/dependency allowlists to the resulting production-package closure. This changes no Rust runtime behavior; it aligns the security review surface with Cargo's automatic local path-membership semantics. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, explicit workspace-member placement, workspace-root package placement, automatic local path-dependency membership, and future composition changes. ## Authority invariant @@ -64,6 +67,7 @@ A reviewed adapter must create a fresh disposable browser boundary, keep remote - **Generic public `TrustedPort` marker trait:** rejected because arbitrary Rust code can implement an unsealed marker and the name creates no security property. - **Sealing `DisposableContextPort` inside `originweave-browser-session`:** not adopted because the canonical versioned browser adapter lives in a separate crate; Rust has no friend-crate visibility, so sealing here would either break the adapter boundary or force protocol code into the Browser Session owner. - **Hard-coding `crates/*` as the production composition boundary:** rejected because Cargo workspace/package membership, not directory placement, determines which production crates are built together. +- **Treating explicit `[workspace].members` as exhaustive:** rejected because Cargo automatically enrolls in-workspace path dependencies; repository review must follow those production path edges as well. - **Moving deterministic browser policy into WebDriver BiDi/MCP:** rejected; adapters translate qualified browser state and never become policy authority. - **`--no-sandbox` or browser-process weakening:** unrelated and forbidden. @@ -74,6 +78,6 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; - any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; -- any future change from explicit Cargo workspace members to member globs, or introduction of a root package, must remain inside this security contract's reviewed manifest/source surface rather than silently widening trust; +- any future change involving Cargo member globs, a workspace-root package, or an in-repository production path dependency must remain inside this security contract's reviewed manifest/source surface rather than silently widening trust; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From bbeb5d1888815b3bb9cd635df78a3796e3b2cf82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:32:04 +0900 Subject: [PATCH 199/632] test(browser-session): cover inherited implicit path members --- ...sion_implicit_workspace_member_contract.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index 5fb5d3adf..a591f3f3c 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -120,6 +120,43 @@ def test_in_workspace_path_dependency_cannot_escape_review_surface(self) -> None self.assertIn(adapter_manifest, _production_package_manifests(root)) self.assertIn(adapter_source, _production_sources(root)) + def test_workspace_inherited_path_dependency_cannot_escape_review_surface(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["app"]\nresolver = "3"\n' + '[workspace.dependencies]\nbrowser_adapter = { path = "plugins/browser-adapter" }\n', + encoding="utf-8", + ) + + app = root / "app" + app.mkdir() + (app / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2024"\n' + '[target.\'cfg(unix)\'.dependencies]\nbrowser_adapter = { workspace = true }\n', + encoding="utf-8", + ) + (app / "src").mkdir() + (app / "src/lib.rs").write_text("pub fn app() {}\n", encoding="utf-8") + + adapter = root / "plugins/browser-adapter" + adapter.mkdir(parents=True) + adapter_manifest = adapter / "Cargo.toml" + adapter_manifest.write_text( + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + encoding="utf-8", + ) + adapter_source = adapter / "src/lib.rs" + adapter_source.parent.mkdir() + adapter_source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + + self.assertIn(adapter_manifest, _production_package_manifests(root)) + self.assertIn(adapter_source, _production_sources(root)) + def test_recursive_local_path_dependencies_obey_existing_tcb_allowlists(self) -> None: workspace_text = (ROOT / "Cargo.toml").read_text(encoding="utf-8") From 40c9fb3b452d50fdb1b688e5e7634a1b3858c5e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 13:59:42 +0900 Subject: [PATCH 200/632] test(browser-session): expose custom Cargo target source gap --- ...r_session_custom_target_source_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_browser_session_custom_target_source_contract.py diff --git a/tests/test_browser_session_custom_target_source_contract.py b/tests/test_browser_session_custom_target_source_contract.py new file mode 100644 index 000000000..e72d34cc4 --- /dev/null +++ b/tests/test_browser_session_custom_target_source_contract.py @@ -0,0 +1,60 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +IMPLICIT_WORKSPACE_CONTRACT = ROOT / "tests/test_browser_session_implicit_workspace_member_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_implicit_workspace_member_contract", + IMPLICIT_WORKSPACE_CONTRACT, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session implicit-workspace contract") +implicit_workspace = importlib.util.module_from_spec(spec) +spec.loader.exec_module(implicit_workspace) + + +class BrowserSessionCustomTargetSourceContractTests(unittest.TestCase): + """Keep non-standard Cargo production target paths inside the TCB review surface.""" + + def test_custom_lib_and_bin_paths_cannot_escape_production_source_review(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["plugins/browser-adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + + adapter = root / "plugins/browser-adapter" + adapter.mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[lib]\npath = "runtime/lifecycle_adapter.rs"\n' + '[[bin]]\nname = "browser-adapter-cli"\npath = "command/adapter_cli.rs"\n' + '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + encoding="utf-8", + ) + + library_source = adapter / "runtime/lifecycle_adapter.rs" + library_source.parent.mkdir() + library_source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + binary_source = adapter / "command/adapter_cli.rs" + binary_source.parent.mkdir() + binary_source.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + + production_sources = implicit_workspace._production_sources(root) + self.assertIn(library_source, production_sources) + self.assertIn(binary_source, production_sources) + + +if __name__ == "__main__": + unittest.main() From b66bb5cd05999f569460c76e173bcf1fd44a2499 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 14:00:06 +0900 Subject: [PATCH 201/632] test(browser-session): review custom Cargo target paths --- ...sion_implicit_workspace_member_contract.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index a591f3f3c..3376588e3 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -74,11 +74,51 @@ def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: return sorted(manifests) +def _declared_production_target_sources( + root: pathlib.Path, + manifest: pathlib.Path, +) -> set[pathlib.Path]: + """Return explicitly configured library and binary target sources under the review root.""" + parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) + declared_paths: list[str] = [] + + library = parsed.get("lib") + if isinstance(library, dict): + library_path = library.get("path") + if isinstance(library_path, str) and library_path: + declared_paths.append(library_path) + + binaries = parsed.get("bin") + if isinstance(binaries, list): + for binary in binaries: + if not isinstance(binary, dict): + continue + binary_path = binary.get("path") + if isinstance(binary_path, str) and binary_path: + declared_paths.append(binary_path) + + root_resolved = root.resolve() + sources: set[pathlib.Path] = set() + for declared_path in declared_paths: + candidate = (manifest.parent / declared_path).resolve() + try: + candidate.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"production Cargo target source escapes repository review root: {declared_path}" + ) from exc + if not candidate.is_file(): + raise AssertionError(f"declared production Cargo target source is missing: {declared_path}") + sources.add(candidate) + return sources + + def _production_sources(root: pathlib.Path) -> list[pathlib.Path]: - sources: list[pathlib.Path] = [] + sources: set[pathlib.Path] = set() for manifest in _production_package_manifests(root): - sources.extend(sorted(manifest.parent.glob("src/**/*.rs"))) - return sources + sources.update(manifest.parent.glob("src/**/*.rs")) + sources.update(_declared_production_target_sources(root, manifest)) + return sorted(sources) class BrowserSessionImplicitWorkspaceMemberContractTests(unittest.TestCase): From 65f23f0d0baf86bfa892bde3e53dc8bca814dece Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 14:01:35 +0900 Subject: [PATCH 202/632] docs(browser-session): trace custom Cargo target coverage --- ...er-session-cargo-target-source-coverage.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-target-source-coverage.md diff --git a/docs/traceability/browser-session-cargo-target-source-coverage.md b/docs/traceability/browser-session-cargo-target-source-coverage.md new file mode 100644 index 000000000..eac3e8d49 --- /dev/null +++ b/docs/traceability/browser-session-cargo-target-source-coverage.md @@ -0,0 +1,48 @@ +# Browser Session Cargo target-source coverage + +- **Status:** active-PR repository-security evidence for PR #317; not protected-main behavior +- **Owner:** OriginWeave Browser Session bounded context +- **Related authority:** `docs/traceability/browser-session-trusted-adapter-boundary.md`, `docs/THREAT_MODEL.md`, ADR 0114 + +## Problem + +The trusted-adapter repository contract already derives its package review surface from Cargo workspace membership and recursive in-repository production `path` dependencies. Its Rust source scan, however, still treated `src/**/*.rs` as exhaustive. Cargo does not require production library and binary targets to live under `src/`: `[lib].path` and `[[bin]].path` may point at other files relative to the package manifest. + +That difference matters after a production crate becomes an approved Browser Session dependency. A later change could place another `DisposableContextPort` reference in a custom production target outside `src/`; the manifest would remain on the already-reviewed dependency allowlist while the file-level lifecycle-SPI allowlist would never see the new source. The crate-level dependency gate is therefore necessary but not sufficient for exact source-surface review. + +## Authoritative standard + +The Cargo Book, **Cargo Targets**, states that Cargo packages consist of targets corresponding to source files, that target configuration is controlled by `[lib]`, `[[bin]]`, `[[example]]`, `[[test]]`, and `[[bench]]`, and that the `path` field specifies a target source file relative to `Cargo.toml`. Library targets default to `src/lib.rs`, while configured targets may use non-standard paths. OriginWeave treats library and binary targets as the shipped production source surface for this Browser Session composition contract; examples, integration tests, and benches do not grant shipped runtime adapter authority. + +Primary source: Rust Project Developers. (2026). *Cargo Targets*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/cargo-targets.html + +## RED and repair + +- **Structural RED `40c9fb3b452d50fdb1b688e5e7634a1b3858c5e4`** adds `tests/test_browser_session_custom_target_source_contract.py`. Its hostile package declares `[lib] path = "runtime/lifecycle_adapter.rs"` and `[[bin]] path = "command/adapter_cli.rs"`; both files reference `DisposableContextPort` and intentionally live outside `src/`. The prior `_production_sources` helper could not discover either file. +- **Minimal causal repair `b66bb5cd05999f569460c76e173bcf1fd44a2499`** extends the existing production-source closure with explicitly configured library and binary target paths from each reviewed production manifest. Declared paths are resolved relative to their package manifest, must remain inside the repository review root, and must identify an existing file. The existing `src/**/*.rs` scan remains as conservative coverage for default and auto-discovered production sources. + +No Rust runtime, browser-policy, WebDriver BiDi, navigation, or lifecycle semantics changed. This is a repository-security contract repair that makes the existing TCB review policy match Cargo's actual production-target topology. + +## Invariant + +For every production package in the Browser Session trusted-composition closure: + +1. the package manifest is reviewed if it links `originweave-browser-session`; +2. ordinary `src/**/*.rs` production sources remain inside lifecycle-SPI review; +3. every explicitly configured `[lib].path` and `[[bin]].path` is also inside lifecycle-SPI review even when it lives outside `src/`; +4. a configured production target path outside the repository review root fails closed; +5. a configured production target path naming a missing file fails closed; and +6. source and dependency allowlists continue to describe only surfaces present on the same exact tree. + +A future #316 BiDi lifecycle adapter therefore cannot use a custom Cargo target path to widen the Browser Session TCB after its crate dependency has already been approved. Adapter source, Browser Session dependency, target topology, and allowlist widening must remain one reviewed exact-tree delta. + +## Rejected alternatives + +- **Treat `src/**/*.rs` as the production source boundary:** rejected because Cargo target `path` is authoritative and may point elsewhere. +- **Rely only on the crate dependency allowlist:** rejected because it admits a crate, not every future source file or target added inside that crate. +- **Scan every `.rs` file in the repository:** rejected because tests/examples/tooling are different authority surfaces and would collapse production composition with non-shipped code rather than model Cargo targets. +- **Move the Browser Session trust decision into the BiDi adapter:** rejected because deterministic Browser Session composition policy remains owned by Browser Session; protocol adapters consume that contract. + +## Evidence state + +The two commits above are structural/source-contract evidence on Draft PR #317. Draft policy does not provide executable exact-head repository/security GREEN. Parent #229 remains the executable prerequisite, and #317 still requires authorized ordinary/non-force ancestry reconciliation followed by fresh exact-head checks and review before this contract can be treated as merge evidence. From 983304c81e54fae1a37405092dc8affca66d4cad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 14:02:23 +0900 Subject: [PATCH 203/632] docs(browser-session): record custom Cargo target coverage --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae71fb03..d629b0b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Extended the Browser Session trusted-adapter source review to Cargo production targets declared through custom `[lib].path` and `[[bin]].path`, so an already reviewed Browser Session-dependent crate cannot add lifecycle-SPI source outside `src/` without entering the exact source allowlist; declared production target paths outside the repository review root or naming missing files fail closed. - Derived Browser Session trusted-adapter source and manifest review coverage from explicit Cargo `[workspace].members` instead of the `crates/*` directory convention, so production workspace members at other paths cannot evade lifecycle-SPI/dependency/binding review; workspace-member globs now fail closed until the security contract is explicitly extended. - Hardened Browser Session trusted-adapter dependency discovery so Cargo workspace-inherited aliases and target-specific production dependencies resolve to the canonical `originweave-browser-session` package before allowlist comparison; dev-only dependencies do not widen the shipped adapter-composition surface. - Removed latent Browser Session adapter pre-authorization from the trusted-adapter allowlists. Approved source and dependency entries must now correspond to production surfaces present on the same exact tree, so the future BiDi lifecycle adapter must widen the allowlists in the same reviewed change that introduces its implementation and crate dependency. From f1596358b3dd849c8022bfcb1715b8a271992032 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 14:02:52 +0900 Subject: [PATCH 204/632] docs(browser-session): include Cargo target source boundary --- ...browser-session-trusted-adapter-boundary.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-trusted-adapter-boundary.md b/docs/traceability/browser-session-trusted-adapter-boundary.md index c0c365579..2945d7788 100644 --- a/docs/traceability/browser-session-trusted-adapter-boundary.md +++ b/docs/traceability/browser-session-trusted-adapter-boundary.md @@ -20,7 +20,7 @@ No external production consumer is approved on the current #317 tree. The intend ## Enforced repository contract -`tests/test_browser_session_trusted_adapter_boundary.py` and `tests/test_browser_session_implicit_workspace_member_contract.py` enforce the currently supportable boundary: +`tests/test_browser_session_trusted_adapter_boundary.py`, `tests/test_browser_session_implicit_workspace_member_contract.py`, and `tests/test_browser_session_custom_target_source_contract.py` enforce the currently supportable boundary: 1. the Browser Session crate remains `publish = false`; 2. the canonical threat model continues to classify privileged browser integration as trusted Zone C code; @@ -28,10 +28,11 @@ No external production consumer is approved on the current #317 tree. The intend 4. production crate dependencies on `originweave-browser-session` are limited to the explicit reviewed manifest allowlist, including direct package aliases, table syntax, target-specific production dependencies, and workspace-inherited aliases resolved through root `[workspace.dependencies]`; 5. both allowlists exactly describe production surfaces that exist on the current tree, so an absent future source path or dependency cannot be pre-approved; 6. no current production source outside the Browser Session owner references `bind_lifecycle_port` as a caller-selected composition escape hatch, regardless of method-call, UFCS, or whitespace spelling; -7. the primary source and manifest review surface is derived from the Cargo workspace's explicit `[workspace].members` plus the workspace-root package when `[package]` is present, rather than from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed; and -8. recursive in-repository production `path` dependencies are also reviewed, because Cargo automatically makes path dependencies residing in the workspace directory workspace members even when they are omitted from the explicit `members` list. An implicit local package therefore cannot hide a Browser Session dependency or lifecycle-SPI reference behind another member's `path = ...` edge. +7. the primary source and manifest review surface is derived from the Cargo workspace's explicit `[workspace].members` plus the workspace-root package when `[package]` is present, rather than from a `crates/*` directory convention. Workspace-member globs fail closed until this contract is explicitly extended and reviewed; +8. recursive in-repository production `path` dependencies are also reviewed, because Cargo automatically makes path dependencies residing in the workspace directory workspace members even when they are omitted from the explicit `members` list. An implicit local package therefore cannot hide a Browser Session dependency or lifecycle-SPI reference behind another member's `path = ...` edge; and +9. production source review includes explicitly configured `[lib].path` and `[[bin]].path` targets as well as the ordinary `src/**/*.rs` surface. Custom production target paths must remain inside the repository review root and identify existing files, so an already approved Browser Session-dependent crate cannot hide a new lifecycle-SPI source outside `src/`. -The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh and eighth rules prevent explicit members outside `crates/*`, a future workspace-root package, and automatically enrolled in-workspace path dependencies from widening the trusted composition surface without entering the same repository review boundary. +The sixth rule intentionally leaves the product composition owner unclaimed until a reviewed runtime/composition lane exists. When that owner is introduced, its exact path must be added deliberately with architecture and security review rather than discovered implicitly through a new call site. The seventh through ninth rules prevent explicit members outside `crates/*`, a future workspace-root package, automatically enrolled in-workspace path dependencies, and custom Cargo production-target paths from widening the trusted composition surface without entering the same repository review boundary. ### Scanner false-negative repair @@ -53,7 +54,9 @@ A sixth review checked Cargo's workspace-root package rule rather than assuming A seventh review checked the remaining Cargo membership rule against the current Cargo Book: **all path dependencies residing inside the workspace directory automatically become workspace members**, even when their package paths are absent from `[workspace].members`. The sixth-generation helper still treated the explicit list plus root package as exhaustive, so `app -> path ../plugins/browser-adapter -> originweave-browser-session` could place a production adapter inside the Cargo workspace while its manifest and source escaped the review scan. Structural RED `b0931ae8b71994ad96689b7e109ff09cca21bd72` adds that hostile implicit-member fixture. Repair `a7798d229631b0f0ab2a8b1132fb08f250df8ecd` follows production dependency sections recursively, resolves direct and workspace-inherited local `path` dependencies relative to their Cargo-defined bases, rejects paths outside the repository review root, and applies the existing source/dependency allowlists to the resulting production-package closure. This changes no Rust runtime behavior; it aligns the security review surface with Cargo's automatic local path-membership semantics. -These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, explicit workspace-member placement, workspace-root package placement, automatic local path-dependency membership, and future composition changes. +An eighth review checked Cargo target topology rather than assuming every shipped Rust source lives under `src/`. The Cargo Book permits `[lib].path` and `[[bin]].path` to name source files elsewhere relative to `Cargo.toml`. After a crate becomes an approved Browser Session dependency, a later custom target outside `src/` could therefore add another lifecycle-SPI source while the manifest remains approved and the file-level allowlist never sees the new file. Structural RED `40c9fb3b452d50fdb1b688e5e7634a1b3858c5e4` adds hostile custom library and binary targets outside `src/`. Minimal repair `b66bb5cd05999f569460c76e173bcf1fd44a2499` extends the production-source closure with configured library and binary target paths, resolves them relative to the package manifest, and fails closed when they escape the repository review root or name a missing file. Detailed standard and decision evidence is recorded in `docs/traceability/browser-session-cargo-target-source-coverage.md`. + +These repairs change no Rust production behavior or trust classification; they make the existing single-writer/TCB policy enforceable across ordinary Rust spelling, Cargo aliasing, workspace inheritance, module-layout choices, explicit workspace-member placement, workspace-root package placement, automatic local path-dependency membership, custom production-target source placement, and future composition changes. ## Authority invariant @@ -68,6 +71,7 @@ A reviewed adapter must create a fresh disposable browser boundary, keep remote - **Sealing `DisposableContextPort` inside `originweave-browser-session`:** not adopted because the canonical versioned browser adapter lives in a separate crate; Rust has no friend-crate visibility, so sealing here would either break the adapter boundary or force protocol code into the Browser Session owner. - **Hard-coding `crates/*` as the production composition boundary:** rejected because Cargo workspace/package membership, not directory placement, determines which production crates are built together. - **Treating explicit `[workspace].members` as exhaustive:** rejected because Cargo automatically enrolls in-workspace path dependencies; repository review must follow those production path edges as well. +- **Treating `src/**/*.rs` as exhaustive production source coverage:** rejected because Cargo may locate library and binary targets at manifest-declared custom paths outside `src/`. - **Moving deterministic browser policy into WebDriver BiDi/MCP:** rejected; adapters translate qualified browser state and never become policy authority. - **`--no-sandbox` or browser-process weakening:** unrelated and forbidden. @@ -77,7 +81,7 @@ This dossier resolves the threat-model ambiguity; it does not by itself make #31 - the active branch must inherit every still-valid #229 delta by ordinary non-force adoption; - `ARCHITECTURE.md` must explicitly include `BrowserSessionIncarnation` in `PresentationMutationAuthority` binding and sequential-ABA responsibility; -- any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; -- any future change involving Cargo member globs, a workspace-root package, or an in-repository production path dependency must remain inside this security contract's reviewed manifest/source surface rather than silently widening trust; +- any reviewed production composition path and versioned BiDi adapter must introduce its source, crate dependency, target topology, and allowlist widening together on the exact reviewed tree rather than relying on a reserved future entry; +- any future change involving Cargo member globs, a workspace-root package, an in-repository production path dependency, or a custom production library/binary target path must remain inside this security contract's reviewed manifest/source surface rather than silently widening trust; - exact-head repository/security checks and independent review must pass with no unresolved authority finding; - pinned Chromium must later prove create/use/post-condition/destroy behavior rather than treating a command ACK as success. From a0fb0765d7df8303df490c97dbb5945b1682d837 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:05:26 +0900 Subject: [PATCH 205/632] test: expose recursive lifecycle binding review gap --- ...sion_implicit_workspace_member_contract.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index 3376588e3..2c194146f 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -160,6 +160,45 @@ def test_in_workspace_path_dependency_cannot_escape_review_surface(self) -> None self.assertIn(adapter_manifest, _production_package_manifests(root)) self.assertIn(adapter_source, _production_sources(root)) + def test_recursive_path_dependency_enters_canonical_binding_review_surface(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["app"]\nresolver = "3"\n', + encoding="utf-8", + ) + + app = root / "app" + app.mkdir() + (app / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\nbrowser-adapter = { path = "../plugins/browser-adapter" }\n', + encoding="utf-8", + ) + (app / "src").mkdir() + (app / "src/lib.rs").write_text("pub fn app() {}\n", encoding="utf-8") + + adapter = root / "plugins/browser-adapter" + adapter.mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + adapter_source = adapter / "src/lib.rs" + adapter_source.parent.mkdir() + adapter_source.write_text( + "pub fn attach(session: &mut Session, port: Port) { session.bind_lifecycle_port(port); }\n", + encoding="utf-8", + ) + + self.assertIn(adapter_source, _production_sources(root)) + self.assertTrue(boundary._has_lifecycle_binding(adapter_source.read_text(encoding="utf-8"))) + self.assertIn( + adapter_source, + boundary._workspace_production_sources(root), + "canonical lifecycle-binding review must include recursive in-repository path dependencies", + ) + def test_workspace_inherited_path_dependency_cannot_escape_review_surface(self) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) From 6b050ee820a29342a9a180638a38b85b33cd66a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:06:06 +0900 Subject: [PATCH 206/632] test: unify Browser Session production topology review --- ...rowser_session_trusted_adapter_boundary.py | 118 ++++++++++++++++-- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 631cb8264..61b89f0e1 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -102,7 +102,7 @@ def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bo def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: - """Return every reviewed Cargo workspace package manifest.""" + """Return every explicitly reviewed Cargo workspace package manifest.""" root_manifest_path = root / "Cargo.toml" root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) workspace = root_manifest.get("workspace") @@ -130,13 +130,113 @@ def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: return sorted(manifests) -def _workspace_production_sources(root: pathlib.Path) -> list[pathlib.Path]: - sources: list[pathlib.Path] = [] - for manifest in _workspace_member_manifests(root): - sources.extend(sorted(manifest.parent.glob("src/**/*.rs"))) +def _in_repository_path_dependency( + root: pathlib.Path, + manifest: pathlib.Path, + dependency_name: str, + dependency_spec: object, + workspace_dependencies: dict[str, object], +) -> pathlib.Path | None: + spec = dependency_spec + base = manifest.parent + if isinstance(spec, dict) and spec.get("workspace") is True: + spec = workspace_dependencies.get(dependency_name) + base = root + if not isinstance(spec, dict): + return None + + declared_path = spec.get("path") + if not isinstance(declared_path, str) or not declared_path: + return None + + root_resolved = root.resolve() + candidate = (base / declared_path / "Cargo.toml").resolve() + try: + candidate.relative_to(root_resolved) + except ValueError: + return None + return candidate if candidate.is_file() else None + + +def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: + """Return workspace packages plus recursive in-repository production path dependencies.""" + root_manifest_path = root / "Cargo.toml" + root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + workspace = root_manifest.get("workspace") + workspace_dependencies: dict[str, object] = {} + if isinstance(workspace, dict): + declared = workspace.get("dependencies") + if isinstance(declared, dict): + workspace_dependencies = declared + + manifests = set(_workspace_member_manifests(root)) + pending = list(manifests) + while pending: + manifest = pending.pop() + parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) + for section in _manifest_dependency_sections(parsed): + for dependency_name, dependency_spec in section.items(): + candidate = _in_repository_path_dependency( + root, + manifest, + dependency_name, + dependency_spec, + workspace_dependencies, + ) + if candidate is not None and candidate not in manifests: + manifests.add(candidate) + pending.append(candidate) + return sorted(manifests) + + +def _declared_production_target_sources( + root: pathlib.Path, + manifest: pathlib.Path, +) -> set[pathlib.Path]: + """Return explicitly configured library and binary sources under the repository review root.""" + parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) + declared_paths: list[str] = [] + + library = parsed.get("lib") + if isinstance(library, dict): + library_path = library.get("path") + if isinstance(library_path, str) and library_path: + declared_paths.append(library_path) + + binaries = parsed.get("bin") + if isinstance(binaries, list): + for binary in binaries: + if not isinstance(binary, dict): + continue + binary_path = binary.get("path") + if isinstance(binary_path, str) and binary_path: + declared_paths.append(binary_path) + + root_resolved = root.resolve() + sources: set[pathlib.Path] = set() + for declared_path in declared_paths: + candidate = (manifest.parent / declared_path).resolve() + try: + candidate.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"production Cargo target source escapes repository review root: {declared_path}" + ) from exc + if not candidate.is_file(): + raise AssertionError(f"declared production Cargo target source is missing: {declared_path}") + sources.add(candidate) return sources +def _workspace_production_sources(root: pathlib.Path) -> list[pathlib.Path]: + """Return the canonical production Rust source closure reviewed by the TCB contract.""" + sources: set[pathlib.Path] = set() + for manifest in _production_package_manifests(root): + sources.update(manifest.parent.glob("src/**/*.rs")) + sources.update(_declared_production_target_sources(root, manifest)) + return sorted(sources) + + class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): """Keep the privileged lifecycle adapter inside the reviewed product TCB.""" @@ -184,7 +284,7 @@ def test_production_disposable_context_port_references_are_allowlisted(self) -> def test_browser_session_dependencies_are_allowlisted(self) -> None: discovered = set() workspace_text = ROOT_CARGO.read_text(encoding="utf-8") - for path in _workspace_member_manifests(ROOT): + for path in _production_package_manifests(ROOT): if path == BROWSER_SESSION_CARGO: continue text = path.read_text(encoding="utf-8") @@ -209,7 +309,7 @@ def test_allowlists_do_not_preapprove_absent_production_surfaces(self) -> None: discovered_dependencies = set() workspace_text = ROOT_CARGO.read_text(encoding="utf-8") - for path in _workspace_member_manifests(ROOT): + for path in _production_package_manifests(ROOT): if path == BROWSER_SESSION_CARGO: continue if _manifest_links_browser_session(path.read_text(encoding="utf-8"), workspace_text): @@ -324,7 +424,7 @@ def test_workspace_member_outside_crates_glob_cannot_escape_review_surface(self) encoding="utf-8", ) - self.assertIn(member_manifest, _workspace_member_manifests(root)) + self.assertIn(member_manifest, _production_package_manifests(root)) self.assertIn(source, _workspace_production_sources(root)) def test_workspace_root_package_cannot_escape_review_surface(self) -> None: @@ -344,7 +444,7 @@ def test_workspace_root_package_cannot_escape_review_surface(self) -> None: encoding="utf-8", ) - self.assertIn(root_manifest, _workspace_member_manifests(root)) + self.assertIn(root_manifest, _production_package_manifests(root)) self.assertIn(source, _workspace_production_sources(root)) def test_workspace_member_globs_fail_closed_until_reviewed(self) -> None: From 18b560c869c5e967a3226d5407bc7216b80f9c80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:06:27 +0900 Subject: [PATCH 207/632] test: keep Browser Session Cargo topology single-sourced --- ...sion_implicit_workspace_member_contract.py | 111 +----------------- 1 file changed, 5 insertions(+), 106 deletions(-) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index 2c194146f..61d7e1586 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -1,7 +1,6 @@ import importlib.util import pathlib import tempfile -import tomllib import unittest @@ -14,111 +13,11 @@ boundary = importlib.util.module_from_spec(spec) spec.loader.exec_module(boundary) - -def _in_repository_path_dependency( - root: pathlib.Path, - manifest: pathlib.Path, - dependency_name: str, - dependency_spec: object, - workspace_dependencies: dict[str, object], -) -> pathlib.Path | None: - spec = dependency_spec - base = manifest.parent - if isinstance(spec, dict) and spec.get("workspace") is True: - spec = workspace_dependencies.get(dependency_name) - base = root - if not isinstance(spec, dict): - return None - - declared_path = spec.get("path") - if not isinstance(declared_path, str) or not declared_path: - return None - - root_resolved = root.resolve() - candidate = (base / declared_path / "Cargo.toml").resolve() - try: - candidate.relative_to(root_resolved) - except ValueError: - return None - return candidate if candidate.is_file() else None - - -def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: - """Return reviewed workspace packages plus recursive in-repository path dependencies.""" - root_manifest_path = root / "Cargo.toml" - root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) - workspace = root_manifest.get("workspace") - workspace_dependencies: dict[str, object] = {} - if isinstance(workspace, dict): - declared = workspace.get("dependencies") - if isinstance(declared, dict): - workspace_dependencies = declared - - manifests = set(boundary._workspace_member_manifests(root)) - pending = list(manifests) - while pending: - manifest = pending.pop() - parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) - for section in boundary._manifest_dependency_sections(parsed): - for dependency_name, dependency_spec in section.items(): - candidate = _in_repository_path_dependency( - root, - manifest, - dependency_name, - dependency_spec, - workspace_dependencies, - ) - if candidate is not None and candidate not in manifests: - manifests.add(candidate) - pending.append(candidate) - return sorted(manifests) - - -def _declared_production_target_sources( - root: pathlib.Path, - manifest: pathlib.Path, -) -> set[pathlib.Path]: - """Return explicitly configured library and binary target sources under the review root.""" - parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) - declared_paths: list[str] = [] - - library = parsed.get("lib") - if isinstance(library, dict): - library_path = library.get("path") - if isinstance(library_path, str) and library_path: - declared_paths.append(library_path) - - binaries = parsed.get("bin") - if isinstance(binaries, list): - for binary in binaries: - if not isinstance(binary, dict): - continue - binary_path = binary.get("path") - if isinstance(binary_path, str) and binary_path: - declared_paths.append(binary_path) - - root_resolved = root.resolve() - sources: set[pathlib.Path] = set() - for declared_path in declared_paths: - candidate = (manifest.parent / declared_path).resolve() - try: - candidate.relative_to(root_resolved) - except ValueError as exc: - raise AssertionError( - f"production Cargo target source escapes repository review root: {declared_path}" - ) from exc - if not candidate.is_file(): - raise AssertionError(f"declared production Cargo target source is missing: {declared_path}") - sources.add(candidate) - return sources - - -def _production_sources(root: pathlib.Path) -> list[pathlib.Path]: - sources: set[pathlib.Path] = set() - for manifest in _production_package_manifests(root): - sources.update(manifest.parent.glob("src/**/*.rs")) - sources.update(_declared_production_target_sources(root, manifest)) - return sorted(sources) +# The trusted-adapter boundary is the single writer for production Cargo topology. +# These aliases keep the hostile topology fixtures on that exact scanner rather than +# maintaining a second implementation that can drift away from the security gate. +_production_package_manifests = boundary._production_package_manifests +_production_sources = boundary._workspace_production_sources class BrowserSessionImplicitWorkspaceMemberContractTests(unittest.TestCase): From 25c992ed1c416a91e080e003acf0168fc5198868 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:06:56 +0900 Subject: [PATCH 208/632] docs: trace Browser Session topology single writer --- ...ssion-production-topology-single-writer.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/traceability/browser-session-production-topology-single-writer.md diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md new file mode 100644 index 000000000..77b61cc74 --- /dev/null +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -0,0 +1,27 @@ +# Browser Session production-topology single writer + +- **Status:** active-PR security evidence for PR #317; not protected-main behavior +- **Owner:** OriginWeave Browser Session bounded context +- **Related contract:** `tests/test_browser_session_trusted_adapter_boundary.py` + +## Problem + +The Browser Session trusted-adapter gate had two different definitions of the production Cargo topology. The canonical boundary test scanned only explicit workspace members for `bind_lifecycle_port`, while the later implicit-workspace contract separately followed recursive in-repository `path` dependencies and custom `[lib].path` / `[[bin]].path` targets. + +That split left a concrete composition escape: an implicit local path dependency could contain `bind_lifecycle_port` without a `DisposableContextPort` token or direct Browser Session dependency in that source file. The supplemental topology test would discover the file, but the canonical caller-selected binding gate would not inspect it. A lifecycle binding in such a package could therefore widen product composition without entering the same fail-closed check that protects explicit workspace members. + +## RED and repair + +- **Structural RED `a0fb0765d7df8303df490c97dbb5945b1682d837`** adds a hostile `app -> ../plugins/browser-adapter` fixture whose implicit local package calls `bind_lifecycle_port`. The enhanced supplemental production-source closure finds the source, while the canonical `_workspace_production_sources` scanner does not. +- **Minimal causal repair `6b050ee820a29342a9a180638a38b85b33cd66a3`** moves recursive in-repository production `path` dependency traversal and custom production target discovery into the canonical trusted-adapter boundary. The same source closure now drives lifecycle-SPI references, Browser Session dependency allowlists, dormant-entry equality, and caller-selected lifecycle-binding rejection. +- **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. `tests/test_browser_session_custom_target_source_contract.py` continues to consume that same helper through the implicit-workspace contract. + +## Invariant + +There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. The closure fails closed for unsupported workspace-member globs, missing declared production targets, and declared target paths outside the repository review root. + +Every source in that closure is subject to the same lifecycle-SPI and caller-selected binding checks. A supplemental hostile fixture may exercise the scanner, but it must not maintain a second production-topology algorithm. + +## Scope + +This repair changes repository security coverage only. It does not change Browser Session runtime semantics, WebDriver BiDi protocol authority, Chromium behavior, or the trust classification of privileged in-process adapters. Exact-head executable repository/security evidence remains required after the parent lineage is reconciled and the PR becomes runnable. From 0b0204b30585a2a5921a7b1e193121daf23aff50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:11:30 +0900 Subject: [PATCH 209/632] test: reject external production path dependencies --- ...sion_implicit_workspace_member_contract.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index 61d7e1586..cb084687c 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -98,6 +98,41 @@ def test_recursive_path_dependency_enters_canonical_binding_review_surface(self) "canonical lifecycle-binding review must include recursive in-repository path dependencies", ) + def test_external_production_path_dependency_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + parent = pathlib.Path(directory) + root = parent / "repo" + root.mkdir() + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["app"]\nresolver = "3"\n', + encoding="utf-8", + ) + + app = root / "app" + app.mkdir() + (app / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\nexternal-adapter = { path = "../../external-adapter" }\n', + encoding="utf-8", + ) + (app / "src").mkdir() + (app / "src/lib.rs").write_text("pub fn app() {}\n", encoding="utf-8") + + external = parent / "external-adapter" + external.mkdir() + (external / "Cargo.toml").write_text( + '[package]\nname = "external-adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (external / "src").mkdir() + (external / "src/lib.rs").write_text("pub fn external() {}\n", encoding="utf-8") + + with self.assertRaisesRegex( + AssertionError, + "production Cargo path dependency escapes repository review root", + ): + _production_package_manifests(root) + def test_workspace_inherited_path_dependency_cannot_escape_review_surface(self) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) From 95c49d22e557466c063124989808d02ae363c609 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:12:40 +0900 Subject: [PATCH 210/632] test: fail closed on external production path dependencies --- tests/test_browser_session_trusted_adapter_boundary.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 61b89f0e1..1f231af2d 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -153,9 +153,13 @@ def _in_repository_path_dependency( candidate = (base / declared_path / "Cargo.toml").resolve() try: candidate.relative_to(root_resolved) - except ValueError: - return None - return candidate if candidate.is_file() else None + except ValueError as exc: + raise AssertionError( + f"production Cargo path dependency escapes repository review root: {declared_path}" + ) from exc + if not candidate.is_file(): + raise AssertionError(f"production Cargo path dependency manifest is missing: {declared_path}") + return candidate def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: From c6b0f4bcb760697f9a116104a35a254bf7f7b5e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:13:01 +0900 Subject: [PATCH 211/632] docs: fail closed on unreviewable Cargo path edges --- .../browser-session-production-topology-single-writer.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md index 77b61cc74..c629bea5f 100644 --- a/docs/traceability/browser-session-production-topology-single-writer.md +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -10,15 +10,21 @@ The Browser Session trusted-adapter gate had two different definitions of the pr That split left a concrete composition escape: an implicit local path dependency could contain `bind_lifecycle_port` without a `DisposableContextPort` token or direct Browser Session dependency in that source file. The supplemental topology test would discover the file, but the canonical caller-selected binding gate would not inspect it. A lifecycle binding in such a package could therefore widen product composition without entering the same fail-closed check that protects explicit workspace members. +A second review found that the recursive path resolver silently returned `None` when a production `path` dependency resolved outside the repository review root. Cargo permits local path dependencies outside the repository, but this security contract cannot inspect such a package's source, manifest evolution, or lifecycle binding. Silently omitting it would treat an unreviewable production dependency as if no production edge existed. + ## RED and repair - **Structural RED `a0fb0765d7df8303df490c97dbb5945b1682d837`** adds a hostile `app -> ../plugins/browser-adapter` fixture whose implicit local package calls `bind_lifecycle_port`. The enhanced supplemental production-source closure finds the source, while the canonical `_workspace_production_sources` scanner does not. - **Minimal causal repair `6b050ee820a29342a9a180638a38b85b33cd66a3`** moves recursive in-repository production `path` dependency traversal and custom production target discovery into the canonical trusted-adapter boundary. The same source closure now drives lifecycle-SPI references, Browser Session dependency allowlists, dormant-entry equality, and caller-selected lifecycle-binding rejection. - **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. `tests/test_browser_session_custom_target_source_contract.py` continues to consume that same helper through the implicit-workspace contract. +- **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root and requires the canonical production-package closure to reject it instead of silently dropping the edge. +- **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. A declared local dependency whose `Cargo.toml` is missing also fails closed. Registry and Git dependencies remain outside this local-path traversal; their package/source integrity is governed by the normal locked dependency and supply-chain controls rather than being misclassified as repository-local source. ## Invariant -There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. The closure fails closed for unsupported workspace-member globs, missing declared production targets, and declared target paths outside the repository review root. +There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. + +The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, and declared target paths outside the repository review root. It must never convert an unreviewable production local dependency into absence. Every source in that closure is subject to the same lifecycle-SPI and caller-selected binding checks. A supplemental hostile fixture may exercise the scanner, but it must not maintain a second production-topology algorithm. From 600547a4f7cef3a9a22744e13e2890538f21b68a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:17:28 +0900 Subject: [PATCH 212/632] test: cover missing production path manifest --- ...sion_implicit_workspace_member_contract.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_browser_session_implicit_workspace_member_contract.py b/tests/test_browser_session_implicit_workspace_member_contract.py index cb084687c..143160117 100644 --- a/tests/test_browser_session_implicit_workspace_member_contract.py +++ b/tests/test_browser_session_implicit_workspace_member_contract.py @@ -133,6 +133,30 @@ def test_external_production_path_dependency_fails_closed(self) -> None: ): _production_package_manifests(root) + def test_missing_production_path_dependency_manifest_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["app"]\nresolver = "3"\n', + encoding="utf-8", + ) + + app = root / "app" + app.mkdir() + (app / "Cargo.toml").write_text( + '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2024"\n' + '[dependencies]\nmissing-adapter = { path = "../plugins/missing-adapter" }\n', + encoding="utf-8", + ) + (app / "src").mkdir() + (app / "src/lib.rs").write_text("pub fn app() {}\n", encoding="utf-8") + + with self.assertRaisesRegex( + AssertionError, + "production Cargo path dependency manifest is missing", + ): + _production_package_manifests(root) + def test_workspace_inherited_path_dependency_cannot_escape_review_surface(self) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) From e09b3b30ffffcb095d9ee29f29df06ecd095b611 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 15:18:39 +0900 Subject: [PATCH 213/632] docs: trace missing path manifest regression --- .../browser-session-production-topology-single-writer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md index c629bea5f..facbda07f 100644 --- a/docs/traceability/browser-session-production-topology-single-writer.md +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -19,6 +19,7 @@ A second review found that the recursive path resolver silently returned `None` - **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. `tests/test_browser_session_custom_target_source_contract.py` continues to consume that same helper through the implicit-workspace contract. - **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root and requires the canonical production-package closure to reject it instead of silently dropping the edge. - **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. A declared local dependency whose `Cargo.toml` is missing also fails closed. Registry and Git dependencies remain outside this local-path traversal; their package/source integrity is governed by the normal locked dependency and supply-chain controls rather than being misclassified as repository-local source. +- **Review-driven coverage `600547a4f7cef3a9a22744e13e2890538f21b68a`** adds the direct hostile regression for the second branch of that fail-closed behavior: an in-repository production `path` dependency whose declared `Cargo.toml` is absent must raise instead of disappearing from the package closure. This was requested by the current-head independent review and does not change production behavior. ## Invariant From 44b6d2716ade55c3793698080bfda01ff51a23c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:05:21 +0900 Subject: [PATCH 214/632] test(security): reject workspace members outside review root --- ...rowser_session_trusted_adapter_boundary.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 1f231af2d..0fd90c052 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -451,6 +451,25 @@ def test_workspace_root_package_cannot_escape_review_surface(self) -> None: self.assertIn(root_manifest, _production_package_manifests(root)) self.assertIn(source, _workspace_production_sources(root)) + def test_workspace_member_outside_repository_root_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + sandbox = pathlib.Path(directory) + root = sandbox / "workspace" + external = sandbox / "external-browser-adapter" + root.mkdir() + external.mkdir() + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["../external-browser-adapter"]\n', + encoding="utf-8", + ) + (external / "Cargo.toml").write_text( + '[package]\nname = "external-browser-adapter"\nversion = "0.1.0"\n', + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "member escapes repository review root"): + _workspace_member_manifests(root) + def test_workspace_member_globs_fail_closed_until_reviewed(self) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) From a807accfddea31a5739f47dcfb992057f7292c03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:05:57 +0900 Subject: [PATCH 215/632] fix(security): fail closed on external workspace members --- tests/test_browser_session_trusted_adapter_boundary.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 0fd90c052..811186d46 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -112,6 +112,7 @@ def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: if not isinstance(members, list): raise AssertionError("Cargo workspace members must be an explicit reviewed list") + root_resolved = root.resolve() manifests: set[pathlib.Path] = set() if isinstance(root_manifest.get("package"), dict): manifests.add(root_manifest_path) @@ -123,7 +124,13 @@ def _workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: raise AssertionError( "Cargo workspace member globs require an explicit trusted-adapter contract update" ) - manifest = root / member / "Cargo.toml" + manifest = (root / member / "Cargo.toml").resolve() + try: + manifest.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"Cargo workspace member escapes repository review root: {member}" + ) from exc if not manifest.is_file(): raise AssertionError(f"workspace member manifest is missing: {member}/Cargo.toml") manifests.add(manifest) From afb8ab82adcb1da564930798f682f10b38cf818b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:06:24 +0900 Subject: [PATCH 216/632] docs(security): trace workspace-member containment repair --- ...er-session-workspace-member-containment.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/traceability/browser-session-workspace-member-containment.md diff --git a/docs/traceability/browser-session-workspace-member-containment.md b/docs/traceability/browser-session-workspace-member-containment.md new file mode 100644 index 000000000..e89889507 --- /dev/null +++ b/docs/traceability/browser-session-workspace-member-containment.md @@ -0,0 +1,39 @@ +# Browser Session workspace-member containment + +- **Status:** active-PR security evidence for PR #317; not protected-main behavior +- **Owner:** OriginWeave Browser Session bounded context +- **Canonical contract:** `tests/test_browser_session_trusted_adapter_boundary.py` +- **Related evidence:** `docs/traceability/browser-session-trusted-adapter-boundary.md`, `docs/THREAT_MODEL.md` + +## Problem + +The trusted-adapter contract treats repository-local Cargo package/source topology as the review boundary for code that can participate in Browser Session lifecycle composition. Local production `path` dependencies and custom target sources already fail closed when their resolved paths escape the repository review root, but explicit `[workspace].members` did not apply the same containment check. + +Cargo's workspace contract allows `members` to name package directories rather than restricting them to a `crates/*` convention. The Cargo Book also documents `package.workspace` specifically for member packages that are not under the workspace root. A relative member such as `../external-browser-adapter` can therefore identify code outside the repository tree. Merely checking that its `Cargo.toml` exists is insufficient for a repository-scoped TCB review contract. + +## RED and repair + +- **Structural RED `44b6d2716ade55c3793698080bfda01ff51a23c1`** adds a hostile workspace fixture whose explicit member resolves to `../external-browser-adapter`. The previous `_workspace_member_manifests()` accepted the external manifest because it checked only existence. +- **Minimal causal repair `a807accfddea31a5739f47dcfb992057f7292c03`** resolves each explicit member manifest and requires it to remain under the repository review root before it may enter the canonical production package closure. A member that escapes the root now raises instead of silently expanding the trusted composition surface. + +The repair changes repository security coverage only. No production Rust, Browser Session runtime semantics, WebDriver BiDi authority, Chromium behavior, or existing allowlist entry changed. + +## Decision + +External Cargo workspace members are rejected by this repository contract even if Cargo itself can model such membership. The product security boundary is intentionally narrower than Cargo's general project-layout flexibility: code outside the repository cannot be proven by OriginWeave's exact-head review, provenance, branch protection, or release evidence. + +Alternative approaches were rejected: + +- **Accept the external member because Cargo accepts it:** rejected because repository review/provenance cannot establish the external source generation. +- **Copy external code into the scanner's evidence:** rejected because that turns mutable external source into an implicit dependency instead of a released/versioned owner contract. +- **Permit an allowlisted filesystem path outside the repository:** rejected because the path is not an immutable release identity and would bypass normal PR/release provenance. + +If OriginWeave later needs an external browser adapter, it must arrive through a released/versioned dependency or another canonical owner boundary rather than an unversioned workspace-member filesystem edge. + +## Authoritative reference + +The Cargo Book, *Workspaces*, documents `[workspace].members` as package-directory entries, automatic in-workspace path-dependency membership, and `package.workspace` for explicitly identifying a workspace root when the member is not beneath it: https://doc.rust-lang.org/cargo/reference/workspaces.html + +## Remaining acceptance + +This repair is structural security evidence on the Draft #317 branch. It does not transfer executable GREEN from another generation. #229 current exact-head repository/security evidence remains the lineage prerequisite; after authorized ordinary/non-force ancestry reconciliation, #317 must obtain fresh exact-head checks and review before #318 → #321 → #316 restacking. Real Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner path. From ddf17ba21581473c20fdd5c7f7b3223240edd262 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:10:32 +0900 Subject: [PATCH 217/632] test(security): cover symlinked external workspace member --- ...ssion_workspace_member_symlink_contract.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_browser_session_workspace_member_symlink_contract.py diff --git a/tests/test_browser_session_workspace_member_symlink_contract.py b/tests/test_browser_session_workspace_member_symlink_contract.py new file mode 100644 index 000000000..d3685429d --- /dev/null +++ b/tests/test_browser_session_workspace_member_symlink_contract.py @@ -0,0 +1,42 @@ +import pathlib +import runpy +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CANONICAL_CONTRACT = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + + +def _canonical_workspace_member_manifests(root: pathlib.Path) -> list[pathlib.Path]: + """Load the single-writer workspace-member scanner from the canonical security contract.""" + namespace = runpy.run_path(str(CANONICAL_CONTRACT)) + return namespace["_workspace_member_manifests"](root) + + +class BrowserSessionWorkspaceMemberSymlinkContractTests(unittest.TestCase): + """Keep symlinked workspace members inside the repository review root.""" + + def test_symlinked_external_workspace_member_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + sandbox = pathlib.Path(directory) + root = sandbox / "workspace" + external = sandbox / "external-browser-adapter" + root.mkdir() + external.mkdir() + (external / "Cargo.toml").write_text( + '[package]\nname = "external-browser-adapter"\nversion = "0.1.0"\n', + encoding="utf-8", + ) + (root / "linked-browser-adapter").symlink_to(external, target_is_directory=True) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["linked-browser-adapter"]\n', + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "member escapes repository review root"): + _canonical_workspace_member_manifests(root) + + +if __name__ == "__main__": + unittest.main() From 00059590351b69499440dbddd72c4fb36c11305b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:10:50 +0900 Subject: [PATCH 218/632] docs(security): record symlink escape regression --- .../browser-session-workspace-member-containment.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-workspace-member-containment.md b/docs/traceability/browser-session-workspace-member-containment.md index e89889507..d3ab9075a 100644 --- a/docs/traceability/browser-session-workspace-member-containment.md +++ b/docs/traceability/browser-session-workspace-member-containment.md @@ -15,6 +15,8 @@ Cargo's workspace contract allows `members` to name package directories rather t - **Structural RED `44b6d2716ade55c3793698080bfda01ff51a23c1`** adds a hostile workspace fixture whose explicit member resolves to `../external-browser-adapter`. The previous `_workspace_member_manifests()` accepted the external manifest because it checked only existence. - **Minimal causal repair `a807accfddea31a5739f47dcfb992057f7292c03`** resolves each explicit member manifest and requires it to remain under the repository review root before it may enter the canonical production package closure. A member that escapes the root now raises instead of silently expanding the trusted composition surface. +- **Independent-review finding on `afb8ab82adcb1da564930798f682f10b38cf818b`** accepted the containment implementation, including canonical path resolution, but identified one P2 coverage gap: the committed hostile fixture covered `..` escape but not a repository-local symlink whose target resolves outside the review root. +- **Review-driven regression `ddf17ba21581473c20fdd5c7f7b3223240edd262`** adds `tests/test_browser_session_workspace_member_symlink_contract.py`. The fixture creates a member path inside the workspace that is a directory symlink to an external package and requires the canonical scanner to reject the resolved external manifest. It imports the single-writer scanner rather than creating another topology implementation. The repair changes repository security coverage only. No production Rust, Browser Session runtime semantics, WebDriver BiDi authority, Chromium behavior, or existing allowlist entry changed. @@ -36,4 +38,4 @@ The Cargo Book, *Workspaces*, documents `[workspace].members` as package-directo ## Remaining acceptance -This repair is structural security evidence on the Draft #317 branch. It does not transfer executable GREEN from another generation. #229 current exact-head repository/security evidence remains the lineage prerequisite; after authorized ordinary/non-force ancestry reconciliation, #317 must obtain fresh exact-head checks and review before #318 → #321 → #316 restacking. Real Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner path. +This repair is structural security evidence on the Draft #317 branch. It does not transfer executable GREEN from another generation. The current exact head still requires focused independent review of the review-driven symlink regression, and #229 current exact-head repository/security evidence remains the lineage prerequisite. After authorized ordinary/non-force ancestry reconciliation, #317 must obtain fresh exact-head checks and review before #318 → #321 → #316 restacking. Real Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner path. From 38fc81680a9ab0014f241862176fffc444419af3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 16:13:08 +0900 Subject: [PATCH 219/632] docs(security): close workspace containment review finding --- .../browser-session-workspace-member-containment.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-workspace-member-containment.md b/docs/traceability/browser-session-workspace-member-containment.md index d3ab9075a..b5504aaf7 100644 --- a/docs/traceability/browser-session-workspace-member-containment.md +++ b/docs/traceability/browser-session-workspace-member-containment.md @@ -17,6 +17,7 @@ Cargo's workspace contract allows `members` to name package directories rather t - **Minimal causal repair `a807accfddea31a5739f47dcfb992057f7292c03`** resolves each explicit member manifest and requires it to remain under the repository review root before it may enter the canonical production package closure. A member that escapes the root now raises instead of silently expanding the trusted composition surface. - **Independent-review finding on `afb8ab82adcb1da564930798f682f10b38cf818b`** accepted the containment implementation, including canonical path resolution, but identified one P2 coverage gap: the committed hostile fixture covered `..` escape but not a repository-local symlink whose target resolves outside the review root. - **Review-driven regression `ddf17ba21581473c20fdd5c7f7b3223240edd262`** adds `tests/test_browser_session_workspace_member_symlink_contract.py`. The fixture creates a member path inside the workspace that is a directory symlink to an external package and requires the canonical scanner to reject the resolved external manifest. It imports the single-writer scanner rather than creating another topology implementation. +- **Focused independent review of exact `00059590351b69499440dbddd72c4fb36c11305b`** found no remaining defect in this scope. The review verified that the symlink fixture invokes the canonical `_workspace_member_manifests()` scanner, that resolved-path containment rejects the external target, that the traceability chain is accurate, and that the review-driven delta does not modify Browser Session production source, `Cargo.toml`, or `Cargo.lock`. The review explicitly classified this as structural evidence rather than repository-test/CI execution. The repair changes repository security coverage only. No production Rust, Browser Session runtime semantics, WebDriver BiDi authority, Chromium behavior, or existing allowlist entry changed. @@ -38,4 +39,4 @@ The Cargo Book, *Workspaces*, documents `[workspace].members` as package-directo ## Remaining acceptance -This repair is structural security evidence on the Draft #317 branch. It does not transfer executable GREEN from another generation. The current exact head still requires focused independent review of the review-driven symlink regression, and #229 current exact-head repository/security evidence remains the lineage prerequisite. After authorized ordinary/non-force ancestry reconciliation, #317 must obtain fresh exact-head checks and review before #318 → #321 → #316 restacking. Real Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner path. +The current branch has focused structural review closure for this workspace-member containment slice, but no executable exact-head GREEN is transferred or implied. #229 current exact-head repository/security evidence remains the lineage prerequisite; after authorized ordinary/non-force ancestry reconciliation, #317 must obtain fresh exact-head repository/security checks and full current-head review before #318 → #321 → #316 restacking. Real Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner path. From e05381c00b76a96f2b3932941597941eac90a4f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 17:05:15 +0900 Subject: [PATCH 220/632] test(browser-session): expose external source symlink escape --- ..._production_source_containment_contract.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_browser_session_production_source_containment_contract.py diff --git a/tests/test_browser_session_production_source_containment_contract.py b/tests/test_browser_session_production_source_containment_contract.py new file mode 100644 index 000000000..c3842d835 --- /dev/null +++ b/tests/test_browser_session_production_source_containment_contract.py @@ -0,0 +1,50 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + +spec = importlib.util.spec_from_file_location("browser_session_trusted_adapter_boundary", BOUNDARY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session trusted-adapter boundary contract") +boundary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(boundary) + + +class BrowserSessionProductionSourceContainmentContractTests(unittest.TestCase): + """Keep every Cargo production source inside exact-head repository provenance.""" + + def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + sandbox = pathlib.Path(directory) + root = sandbox / "workspace" + external = sandbox / "external-lifecycle-adapter.rs" + root.mkdir() + external.write_text( + "use originweave_browser_session::DisposableContextPort;\n", + encoding="utf-8", + ) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").symlink_to(external) + + with self.assertRaisesRegex( + AssertionError, + "production Cargo source escapes repository review root", + ): + boundary._workspace_production_sources(root) + + +if __name__ == "__main__": + unittest.main() From e37adeeeb9ace7e42707f14286841fda5c2ab239 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 17:05:45 +0900 Subject: [PATCH 221/632] fix(browser-session): fail closed on external production source symlinks --- ..._production_source_containment_contract.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_production_source_containment_contract.py b/tests/test_browser_session_production_source_containment_contract.py index c3842d835..e8e1837c0 100644 --- a/tests/test_browser_session_production_source_containment_contract.py +++ b/tests/test_browser_session_production_source_containment_contract.py @@ -14,9 +14,31 @@ spec.loader.exec_module(boundary) +def _reviewed_production_sources(root: pathlib.Path) -> list[pathlib.Path]: + """Validate canonical Cargo production sources against exact-head repository provenance.""" + root_resolved = root.resolve() + reviewed: list[pathlib.Path] = [] + for source in boundary._workspace_production_sources(root): + resolved = source.resolve() + try: + resolved.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"production Cargo source escapes repository review root: {source}" + ) from exc + if not resolved.is_file(): + raise AssertionError(f"production Cargo source is missing: {source}") + reviewed.append(source) + return reviewed + + class BrowserSessionProductionSourceContainmentContractTests(unittest.TestCase): """Keep every Cargo production source inside exact-head repository provenance.""" + def test_current_production_source_closure_stays_under_repository_root(self) -> None: + reviewed = _reviewed_production_sources(ROOT) + self.assertEqual(reviewed, boundary._workspace_production_sources(ROOT)) + def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> None: with tempfile.TemporaryDirectory() as directory: sandbox = pathlib.Path(directory) @@ -37,13 +59,15 @@ def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> No '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', encoding="utf-8", ) - (adapter / "src/lib.rs").symlink_to(external) + source = adapter / "src/lib.rs" + source.symlink_to(external) + self.assertIn(source, boundary._workspace_production_sources(root)) with self.assertRaisesRegex( AssertionError, "production Cargo source escapes repository review root", ): - boundary._workspace_production_sources(root) + _reviewed_production_sources(root) if __name__ == "__main__": From 3fd55acfaa58fde61f1ca9236db91df2b18320ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 17:06:47 +0900 Subject: [PATCH 222/632] docs(browser-session): trace external source containment --- ...r-session-production-source-containment.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/traceability/browser-session-production-source-containment.md diff --git a/docs/traceability/browser-session-production-source-containment.md b/docs/traceability/browser-session-production-source-containment.md new file mode 100644 index 000000000..10a33ab5e --- /dev/null +++ b/docs/traceability/browser-session-production-source-containment.md @@ -0,0 +1,65 @@ +# Browser Session production-source containment + +Status: Draft evidence on PR #317; this file does not claim protected-main shipment or executable exact-head GREEN. + +## Problem + +OriginWeave's Browser Session trusted-adapter contract derives one Cargo production package/source closure and uses it to review `DisposableContextPort` references, Browser Session dependencies, and caller-selected lifecycle binding. The closure already rejects workspace members, local path dependencies, and explicit Cargo target paths that resolve outside the repository review root. + +The remaining gap was default Rust source discovery. The canonical scanner used `manifest.parent.glob("src/**/*.rs")`, which returns a lexically in-repository path even when the Rust file itself is a symlink whose resolved target is outside the repository. That external file can therefore participate in Cargo's default production target while its bytes are not fixed by the OriginWeave exact Git head. This is a provenance and TCB-review defect even when the scanner happens to read the external bytes on one runner. + +Cargo's current target reference documents `src/lib.rs`, `src/main.rs`, and `src/bin/` as default production source locations and permits manifest-relative explicit target paths. OriginWeave therefore treats the resolved filesystem object behind every source returned by the canonical Cargo topology scanner as part of the exact-head review boundary, not only the lexical path. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for Cargo package/source discovery. +- The containment guard must consume that canonical closure rather than reimplement workspace membership, dependency recursion, or target discovery. +- Repository-external production source is rejected; it is not made trusted by a symlink placed inside the repository. +- Registry and released external dependencies remain dependency provenance concerns and are not reclassified as repository-local source. +- No future BiDi lifecycle adapter path is pre-authorized. + +## RED + +Commit `e05381c00b76a96f2b3932941597941eac90a4f5` added a hostile fixture in `tests/test_browser_session_production_source_containment_contract.py`: + +1. create an in-repository Cargo workspace member; +2. make its default `src/lib.rs` a symlink to `../external-lifecycle-adapter.rs` outside the repository root; +3. call the canonical `_workspace_production_sources(root)` scanner; +4. require fail-closed repository containment. + +The current canonical scanner returns the lexical `adapter/src/lib.rs` path instead of rejecting the external resolved source. No PR-triggered workflow run was emitted for that Draft exact head, so this is structural RED evidence rather than runner-backed RED. + +## Minimal repair + +Commit `e37adeeeb9ace7e42707f14286841fda5c2ab239` adds a provenance postcondition over the canonical source closure. `_reviewed_production_sources(root)` resolves every discovered source, requires the resolved object to remain below `root.resolve()`, requires it to be a file, and otherwise raises `AssertionError`. It does not duplicate Cargo topology discovery. + +Two contracts now apply: + +- the current exact OriginWeave production-source closure must satisfy the provenance postcondition; +- the hostile default-source symlink must still be discovered by the canonical scanner and then be rejected by the provenance guard. + +This repair changes no Rust Browser Session semantics. It tightens the evidence boundary that determines which source bytes are eligible to participate in the privileged browser-integration TCB. + +## Alternatives considered + +### Resolve every path inside the canonical topology scanner + +This would make containment inseparable from discovery but requires modifying the existing single-writer scanner and all downstream path-identity expectations. It remains a valid future consolidation if the contract is moved into reusable repository tooling. + +### Ignore symlinks because CI reads the external target + +Rejected. Reading bytes from an external filesystem object during one run is not immutable exact-head provenance and makes review/reproduction depend on runner state. + +### Ban all source symlinks + +Rejected as broader than necessary. A symlink whose resolved target remains inside the repository can still be covered by the exact-head review boundary; the security invariant is containment of the resolved source object. + +## Evidence and follow-up + +Current repair exact: `e37adeeeb9ace7e42707f14286841fda5c2ab239`. + +The branch remains Draft and diverged from canonical parent #229. This file is therefore structural/source-contract evidence only. Required follow-up is exact-head independent review, then the existing parent-first lineage sequence: terminal #229 evidence, ordinary/non-force #229→#317 ancestry reconciliation with zero valid-delta loss, fresh #317 executable evidence, then #318 → #321 → #316. Real pinned-Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner. + +## Reference + +The Cargo Project Developers. (2026). *Cargo targets*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/cargo-targets.html From f157c215a49d203be2fbe146460ddf22c9081002 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:05:16 +0900 Subject: [PATCH 223/632] test(browser-session): require canonical source containment --- ..._production_source_containment_contract.py | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/tests/test_browser_session_production_source_containment_contract.py b/tests/test_browser_session_production_source_containment_contract.py index e8e1837c0..16343019c 100644 --- a/tests/test_browser_session_production_source_containment_contract.py +++ b/tests/test_browser_session_production_source_containment_contract.py @@ -14,30 +14,13 @@ spec.loader.exec_module(boundary) -def _reviewed_production_sources(root: pathlib.Path) -> list[pathlib.Path]: - """Validate canonical Cargo production sources against exact-head repository provenance.""" - root_resolved = root.resolve() - reviewed: list[pathlib.Path] = [] - for source in boundary._workspace_production_sources(root): - resolved = source.resolve() - try: - resolved.relative_to(root_resolved) - except ValueError as exc: - raise AssertionError( - f"production Cargo source escapes repository review root: {source}" - ) from exc - if not resolved.is_file(): - raise AssertionError(f"production Cargo source is missing: {source}") - reviewed.append(source) - return reviewed - - class BrowserSessionProductionSourceContainmentContractTests(unittest.TestCase): """Keep every Cargo production source inside exact-head repository provenance.""" - def test_current_production_source_closure_stays_under_repository_root(self) -> None: - reviewed = _reviewed_production_sources(ROOT) - self.assertEqual(reviewed, boundary._workspace_production_sources(ROOT)) + def test_current_production_source_closure_is_canonical_and_nonempty(self) -> None: + reviewed = boundary._workspace_production_sources(ROOT) + self.assertGreater(len(reviewed), 0) + self.assertTrue(all(source.is_file() for source in reviewed)) def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> None: with tempfile.TemporaryDirectory() as directory: @@ -62,12 +45,11 @@ def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> No source = adapter / "src/lib.rs" source.symlink_to(external) - self.assertIn(source, boundary._workspace_production_sources(root)) with self.assertRaisesRegex( AssertionError, "production Cargo source escapes repository review root", ): - _reviewed_production_sources(root) + boundary._workspace_production_sources(root) if __name__ == "__main__": From 7cc1cfaec705c6980a59af84bf4459b3e358873a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:06:12 +0900 Subject: [PATCH 224/632] fix(browser-session): centralize source provenance guard --- ...t_browser_session_trusted_adapter_boundary.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 811186d46..bc32b154b 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -241,11 +241,25 @@ def _declared_production_target_sources( def _workspace_production_sources(root: pathlib.Path) -> list[pathlib.Path]: """Return the canonical production Rust source closure reviewed by the TCB contract.""" + root_resolved = root.resolve() sources: set[pathlib.Path] = set() for manifest in _production_package_manifests(root): sources.update(manifest.parent.glob("src/**/*.rs")) sources.update(_declared_production_target_sources(root, manifest)) - return sorted(sources) + + reviewed: list[pathlib.Path] = [] + for source in sources: + resolved = source.resolve() + try: + resolved.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"production Cargo source escapes repository review root: {source}" + ) from exc + if not resolved.is_file(): + raise AssertionError(f"production Cargo source is missing: {source}") + reviewed.append(source) + return sorted(reviewed) class BrowserSessionTrustedAdapterBoundaryTests(unittest.TestCase): From c2a3c1abd4ec01eb91df77c90e865049e607d21c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:06:52 +0900 Subject: [PATCH 225/632] docs(browser-session): record canonical source provenance guard --- ...r-session-production-source-containment.md | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/traceability/browser-session-production-source-containment.md b/docs/traceability/browser-session-production-source-containment.md index 10a33ab5e..a583b7c9a 100644 --- a/docs/traceability/browser-session-production-source-containment.md +++ b/docs/traceability/browser-session-production-source-containment.md @@ -12,39 +12,37 @@ Cargo's current target reference documents `src/lib.rs`, `src/main.rs`, and `src ## Constraints -- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for Cargo package/source discovery. -- The containment guard must consume that canonical closure rather than reimplement workspace membership, dependency recursion, or target discovery. +- `tests/test_browser_session_trusted_adapter_boundary.py` is the single writer for Cargo package/source discovery **and** repository-containment validation. +- Supplemental hostile-fixture tests must delegate to that canonical function rather than wrap it with a second provenance implementation. - Repository-external production source is rejected; it is not made trusted by a symlink placed inside the repository. - Registry and released external dependencies remain dependency provenance concerns and are not reclassified as repository-local source. - No future BiDi lifecycle adapter path is pre-authorized. ## RED -Commit `e05381c00b76a96f2b3932941597941eac90a4f5` added a hostile fixture in `tests/test_browser_session_production_source_containment_contract.py`: +Commit `f157c215a49d203be2fbe146460ddf22c9081002` rewired `tests/test_browser_session_production_source_containment_contract.py` so its hostile default-source symlink fixture calls canonical `_workspace_production_sources(root)` directly and requires that function itself to fail closed. -1. create an in-repository Cargo workspace member; -2. make its default `src/lib.rs` a symlink to `../external-lifecycle-adapter.rs` outside the repository root; -3. call the canonical `_workspace_production_sources(root)` scanner; -4. require fail-closed repository containment. - -The current canonical scanner returns the lexical `adapter/src/lib.rs` path instead of rejecting the external resolved source. No PR-triggered workflow run was emitted for that Draft exact head, so this is structural RED evidence rather than runner-backed RED. +At predecessor exact `3fd55acfaa58fde61f1ca9236db91df2b18320ad`, canonical `_workspace_production_sources(root)` only collected lexical `src/**/*.rs` paths plus explicit target paths. Repository containment lived in supplemental `_reviewed_production_sources(root)`, so the direct canonical call returned the external-target symlink instead of raising. The new test therefore exposes a real single-writer violation. No PR-triggered workflow run is emitted for the Draft head, so this remains structural RED evidence rather than runner-backed RED. ## Minimal repair -Commit `e37adeeeb9ace7e42707f14286841fda5c2ab239` adds a provenance postcondition over the canonical source closure. `_reviewed_production_sources(root)` resolves every discovered source, requires the resolved object to remain below `root.resolve()`, requires it to be a file, and otherwise raises `AssertionError`. It does not duplicate Cargo topology discovery. +Commit `7cc1cfaec705c6980a59af84bf4459b3e358873a` moves the provenance postcondition into canonical `_workspace_production_sources(root)`: -Two contracts now apply: +1. gather the existing Cargo production source closure without changing workspace, dependency, or target discovery; +2. resolve every discovered source; +3. require the resolved source to remain beneath `root.resolve()`; +4. require the resolved object to be a file; +5. return the reviewed lexical paths only after those checks succeed. -- the current exact OriginWeave production-source closure must satisfy the provenance postcondition; -- the hostile default-source symlink must still be discovered by the canonical scanner and then be rejected by the provenance guard. +The supplemental production-source containment test now contains only current-tree and hostile fixtures. It no longer defines a second `_reviewed_production_sources` policy function. Lifecycle-SPI reference review, Browser Session dependency review, lifecycle binding review, and the hostile provenance fixture therefore consume one canonical source closure and one containment decision. This repair changes no Rust Browser Session semantics. It tightens the evidence boundary that determines which source bytes are eligible to participate in the privileged browser-integration TCB. ## Alternatives considered -### Resolve every path inside the canonical topology scanner +### Keep containment in a supplemental wrapper -This would make containment inseparable from discovery but requires modifying the existing single-writer scanner and all downstream path-identity expectations. It remains a valid future consolidation if the contract is moved into reusable repository tooling. +Rejected after the current review. It produced two policy writers: canonical trusted-adapter tests consumed `_workspace_production_sources` directly while only the supplemental containment test consumed `_reviewed_production_sources`. Future callers could therefore bypass the provenance guard without noticing. ### Ignore symlinks because CI reads the external target @@ -56,7 +54,7 @@ Rejected as broader than necessary. A symlink whose resolved target remains insi ## Evidence and follow-up -Current repair exact: `e37adeeeb9ace7e42707f14286841fda5c2ab239`. +Current repair exact: `7cc1cfaec705c6980a59af84bf4459b3e358873a`. The branch remains Draft and diverged from canonical parent #229. This file is therefore structural/source-contract evidence only. Required follow-up is exact-head independent review, then the existing parent-first lineage sequence: terminal #229 evidence, ordinary/non-force #229→#317 ancestry reconciliation with zero valid-delta loss, fresh #317 executable evidence, then #318 → #321 → #316. Real pinned-Chromium acceptance remains downstream under #299 and the canonical `.github` MV3 workflow/sandbox owner. From 022b63d130bb0901c8ef8bc18e5062eb987350f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:07:35 +0900 Subject: [PATCH 226/632] test(browser-session): reject unreviewed Cargo build surfaces --- ..._browser_session_build_surface_contract.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_browser_session_build_surface_contract.py diff --git a/tests/test_browser_session_build_surface_contract.py b/tests/test_browser_session_build_surface_contract.py new file mode 100644 index 000000000..3f810c953 --- /dev/null +++ b/tests/test_browser_session_build_surface_contract.py @@ -0,0 +1,70 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + +spec = importlib.util.spec_from_file_location("browser_session_trusted_adapter_boundary", BOUNDARY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session trusted-adapter boundary contract") +boundary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(boundary) + + +class BrowserSessionBuildSurfaceContractTests(unittest.TestCase): + """Keep generated-code build surfaces out of the Browser Session TCB closure.""" + + def _workspace(self, manifest_suffix: str = "") -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n' + + manifest_suffix, + encoding="utf-8", + ) + return directory, root + + def test_default_build_rs_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + (root / "adapter/build.rs").write_text("fn main() {}\n", encoding="utf-8") + with self.assertRaisesRegex(AssertionError, "production Cargo build script"): + boundary._production_package_manifests(root) + + def test_custom_package_build_path_fails_closed(self) -> None: + directory, root = self._workspace('build = "tools/generate.rs"\n') + with directory: + (root / "adapter/tools").mkdir() + (root / "adapter/tools/generate.rs").write_text("fn main() {}\n", encoding="utf-8") + with self.assertRaisesRegex(AssertionError, "production Cargo build script"): + boundary._production_package_manifests(root) + + def test_build_dependencies_fail_closed(self) -> None: + directory, root = self._workspace( + '[build-dependencies]\nserde = "1"\n' + ) + with directory: + with self.assertRaisesRegex(AssertionError, "production Cargo build dependencies"): + boundary._production_package_manifests(root) + + def test_target_specific_build_dependencies_fail_closed(self) -> None: + directory, root = self._workspace( + "[target.'cfg(unix)'.build-dependencies]\nserde = \"1\"\n" + ) + with directory: + with self.assertRaisesRegex(AssertionError, "production Cargo build dependencies"): + boundary._production_package_manifests(root) + + +if __name__ == "__main__": + unittest.main() From c917970fb939e35ccb0adb920754f8208aed46c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:08:22 +0900 Subject: [PATCH 227/632] fix(browser-session): fail closed on Cargo build surfaces --- ...rowser_session_trusted_adapter_boundary.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index bc32b154b..26fd3c7db 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -77,6 +77,61 @@ def _manifest_dependency_sections(manifest: dict[str, object]) -> list[dict[str, return sections +def _manifest_build_dependency_sections(manifest: dict[str, object]) -> list[dict[str, object]]: + """Return build dependency sections that can influence generated production code.""" + sections: list[dict[str, object]] = [] + build_dependencies = manifest.get("build-dependencies") + if isinstance(build_dependencies, dict): + sections.append(build_dependencies) + + targets = manifest.get("target") + if isinstance(targets, dict): + for target in targets.values(): + if not isinstance(target, dict): + continue + target_build_dependencies = target.get("build-dependencies") + if isinstance(target_build_dependencies, dict): + sections.append(target_build_dependencies) + + return sections + + +def _assert_no_production_build_surfaces(root: pathlib.Path, manifest: pathlib.Path) -> None: + """Fail closed on Cargo build surfaces until generated-source provenance is modeled.""" + parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) + build_dependency_sections = _manifest_build_dependency_sections(parsed) + if any(section for section in build_dependency_sections): + relative = manifest.relative_to(root).as_posix() + raise AssertionError( + f"production Cargo build dependencies require an explicit trusted-adapter contract: {relative}" + ) + + package = parsed.get("package") + build_setting: object = None + if isinstance(package, dict): + build_setting = package.get("build") + + if build_setting is False: + return + if isinstance(build_setting, str) and build_setting: + relative = manifest.relative_to(root).as_posix() + raise AssertionError( + f"production Cargo build script requires an explicit trusted-adapter contract: {relative}" + ) + if build_setting is not None: + relative = manifest.relative_to(root).as_posix() + raise AssertionError( + f"unsupported Cargo package.build setting in production package: {relative}" + ) + + default_build_script = manifest.parent / "build.rs" + if default_build_script.exists() or default_build_script.is_symlink(): + relative = manifest.relative_to(root).as_posix() + raise AssertionError( + f"production Cargo build script requires an explicit trusted-adapter contract: {relative}" + ) + + def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bool: member = tomllib.loads(member_text) workspace_manifest = tomllib.loads(workspace_text) @@ -184,6 +239,7 @@ def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: pending = list(manifests) while pending: manifest = pending.pop() + _assert_no_production_build_surfaces(root, manifest) parsed = tomllib.loads(manifest.read_text(encoding="utf-8")) for section in _manifest_dependency_sections(parsed): for dependency_name, dependency_spec in section.items(): From 80a6aa0ae958e6514b2576f46d5b17dd685d7349 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:08:37 +0900 Subject: [PATCH 228/632] test(browser-session): isolate custom target fixture --- ...browser_session_custom_target_source_contract.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/test_browser_session_custom_target_source_contract.py b/tests/test_browser_session_custom_target_source_contract.py index e72d34cc4..68957557d 100644 --- a/tests/test_browser_session_custom_target_source_contract.py +++ b/tests/test_browser_session_custom_target_source_contract.py @@ -33,23 +33,16 @@ def test_custom_lib_and_bin_paths_cannot_escape_production_source_review(self) - (adapter / "Cargo.toml").write_text( '[package]\nname = "browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' '[lib]\npath = "runtime/lifecycle_adapter.rs"\n' - '[[bin]]\nname = "browser-adapter-cli"\npath = "command/adapter_cli.rs"\n' - '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + '[[bin]]\nname = "browser-adapter-cli"\npath = "command/adapter_cli.rs"\n', encoding="utf-8", ) library_source = adapter / "runtime/lifecycle_adapter.rs" library_source.parent.mkdir() - library_source.write_text( - "use originweave_browser_session::DisposableContextPort;\n", - encoding="utf-8", - ) + library_source.write_text("pub fn lifecycle_adapter() {}\n", encoding="utf-8") binary_source = adapter / "command/adapter_cli.rs" binary_source.parent.mkdir() - binary_source.write_text( - "use originweave_browser_session::DisposableContextPort;\n", - encoding="utf-8", - ) + binary_source.write_text("fn main() {}\n", encoding="utf-8") production_sources = implicit_workspace._production_sources(root) self.assertIn(library_source, production_sources) From 900912a34995917efe82adb104e0b8f8e4095cbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:09:22 +0900 Subject: [PATCH 229/632] test(browser-session): pin delegated settlement validation --- ...rowser_session_recovery_settlement_contract.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_recovery_settlement_contract.py b/tests/test_browser_session_recovery_settlement_contract.py index 8e6ce1fb0..5e4053550 100644 --- a/tests/test_browser_session_recovery_settlement_contract.py +++ b/tests/test_browser_session_recovery_settlement_contract.py @@ -74,20 +74,23 @@ def test_settlement_order_pins_pre_io_validation_and_post_proof_commit(self) -> """Fact validation must precede proof I/O, which must precede aggregate mutation.""" source = RECOVERY.read_text(encoding="utf-8") + selector = source.split("fn select_recovery_fact", 1)[1].split("\n }\n}", 1)[0] method = source.split("pub fn settle_recovery_fact", 1)[1].split("\n }\n}", 1)[0] - authority = method.index("RecoverySettlementError::AuthorityMismatch") - revision = method.index("fact.revision != self.revision") - exact_fact = method.index(".get(fact.index)") + selector_authority = selector.index("RecoveryFactValidationError::AuthorityMismatch") + selector_revision = selector.index("fact.revision != self.revision") + selector_exact_fact = selector.index(".get(fact.index)") + selection = method.index(".select_recovery_fact(fact)") verifier = method.index("verify_recovery_settlement") retirement = method.index("settle_recovery_evidence_at") revision_commit = method.index("self.revision = next_revision") - self.assertLess(authority, verifier) - self.assertLess(revision, verifier) - self.assertLess(exact_fact, verifier) + self.assertLess(selector_authority, selector_revision) + self.assertLess(selector_revision, selector_exact_fact) + self.assertLess(selection, verifier) self.assertLess(verifier, retirement) self.assertLess(retirement, revision_commit) + self.assertIn("RecoveryFactLedger::CreateAttempt", selector) self.assertIn("RecoveryFactLedger::CreateAttempt", method) self.assertIn("settle_create_attempt_recovery_evidence_at", method) From a7c0ef2b20f6c4d82a3dcda54d567544fe75fbe5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:10:11 +0900 Subject: [PATCH 230/632] test(browser-session): scope recovery custody surface checks --- ...test_browser_session_lifecycle_contract.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 4a0129567..75d5df238 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -3,6 +3,7 @@ from __future__ import annotations import pathlib +import re import tomllib import unittest @@ -10,6 +11,20 @@ CRATE = ROOT / "crates/originweave-browser-session" +def _inherent_impl_surface(source: str, type_name: str) -> str: + """Return every inherent impl segment for one Rust type without matching sibling request types.""" + + starts = [match.start() for match in re.finditer(r"(?m)^impl<", source)] + starts.append(len(source)) + segments: list[str] = [] + for index, start in enumerate(starts[:-1]): + segment = source[start : starts[index + 1]] + header = segment.split("{", 1)[0] + if re.search(rf"\b{re.escape(type_name)}<[^>]+>\s*$", header.strip()): + segments.append(segment) + return "\n".join(segments) + + class BrowserSessionLifecycleContractTests(unittest.TestCase): """Keep presentation mutation authority in an explicit Browser Session domain.""" @@ -31,6 +46,10 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: """Raw driver identifiers must never become caller-mintable authority tokens.""" recovery_source = (CRATE / "src/recovery.rs").read_text(encoding="utf-8") + recovery_custody_surface = _inherent_impl_surface( + recovery_source, + "BoundBrowserSessionRecovery", + ) source = "\n".join( (CRATE / relative_path).read_text(encoding="utf-8") for relative_path in ( @@ -106,11 +125,13 @@ def test_domain_source_mints_authority_only_from_owned_lifecycle(self) -> None: self.assertNotIn("pub const fn port", source) self.assertNotIn("pub fn port", source) - self.assertIn("pub const fn state(&self) -> BrowserSessionState", recovery_source) - self.assertIn("pub fn recovery_evidence(&self)", recovery_source) - self.assertIn("pub fn create_attempt_recovery_evidence(&self)", recovery_source) - self.assertNotIn("pub const fn browser_session(&self)", recovery_source) - self.assertNotIn("pub fn browser_session(&self)", recovery_source) + self.assertIn("pub const fn state(&self) -> BrowserSessionState", recovery_custody_surface) + self.assertIn("pub fn recovery_evidence(&self)", recovery_custody_surface) + self.assertIn("pub fn create_attempt_recovery_evidence(&self)", recovery_custody_surface) + self.assertNotIn("pub const fn browser_session(&self)", recovery_custody_surface) + self.assertNotIn("pub fn browser_session(&self)", recovery_custody_surface) + self.assertNotIn("pub const fn port", recovery_custody_surface) + self.assertNotIn("pub fn port", recovery_custody_surface) self.assertIn( "recovery.browser_session().presentation_authority(context)", recovery_source, @@ -377,4 +398,4 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 123c0ac70ed062f7d26b4e1f9038e3a12db2f6d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:12:20 +0900 Subject: [PATCH 231/632] test(browser-session): reject dangling production source symlink --- ..._production_source_containment_contract.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_browser_session_production_source_containment_contract.py b/tests/test_browser_session_production_source_containment_contract.py index 16343019c..e5bd8e8d1 100644 --- a/tests/test_browser_session_production_source_containment_contract.py +++ b/tests/test_browser_session_production_source_containment_contract.py @@ -51,6 +51,28 @@ def test_default_rust_source_symlink_outside_repository_fails_closed(self) -> No ): boundary._workspace_production_sources(root) + def test_dangling_default_rust_source_symlink_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + source = adapter / "src/lib.rs" + source.symlink_to(adapter / "missing-generated.rs") + + with self.assertRaisesRegex( + AssertionError, + "production Cargo source is missing", + ): + boundary._workspace_production_sources(root) + if __name__ == "__main__": unittest.main() From 9955ebd5e0f2dc9eeecded255e7335dea8def521 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:14:22 +0900 Subject: [PATCH 232/632] docs(browser-session): trace Cargo build-surface boundary --- ...er-session-cargo-build-surface-boundary.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-build-surface-boundary.md diff --git a/docs/traceability/browser-session-cargo-build-surface-boundary.md b/docs/traceability/browser-session-cargo-build-surface-boundary.md new file mode 100644 index 000000000..ecbbd6e00 --- /dev/null +++ b/docs/traceability/browser-session-cargo-build-surface-boundary.md @@ -0,0 +1,56 @@ +# Browser Session Cargo build-surface boundary + +Status: Draft evidence on PR #317. This document does not claim protected-main shipment or executable exact-head GREEN. + +## Problem + +The Browser Session trusted-adapter contract reviews production Cargo package topology and Rust source provenance before allowing lifecycle-SPI references, Browser Session dependencies, or caller-selected lifecycle binding. That closure previously covered normal dependencies, target-specific dependencies, default Rust source, and explicit `[lib].path` / `[[bin]].path`, but did not govern Cargo build scripts or build dependencies. + +Cargo runs a package-root `build.rs` by default before compiling the package. `[package] build` can select another build-script path or disable build scripts, and `[build-dependencies]` plus target-specific build dependencies supply code to that build script. Build scripts may generate Rust source in `OUT_DIR` that the package later compiles with `include!`. A future production package could therefore introduce privileged generated code without that code being fixed by the repository source-closure contract. + +## Decision + +OriginWeave fails closed on Cargo build surfaces in the Browser Session production package closure until generated-source provenance is explicitly modeled. + +For every manifest returned by canonical `_production_package_manifests(root)`: + +- non-empty top-level `[build-dependencies]` is rejected; +- non-empty target-specific `build-dependencies` is rejected; +- a default package-root `build.rs`, including a symlink, is rejected; +- a non-empty string `[package] build = "..."` is rejected; +- unsupported non-null `package.build` values are rejected; +- `package.build = false` is allowed because it explicitly disables Cargo build-script discovery. + +This is a repository-security contract, not a statement that Cargo build scripts are inherently unsafe. OriginWeave is declining a source-generation surface until it can bind generated bytes, generator inputs, build dependencies, toolchain, target/host distinction, and resulting artifacts to reproducible exact-head evidence. + +## RED and repair + +Structural RED: `022b63d130bb0901c8ef8bc18e5062eb987350f5` adds hostile fixtures for default `build.rs`, custom `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies. Before the repair, canonical production package discovery admits each fixture. + +Minimal repair: `c917970fb939e35ccb0adb920754f8208aed46c1` adds `_manifest_build_dependency_sections` and `_assert_no_production_build_surfaces` to the correction-owning `tests/test_browser_session_trusted_adapter_boundary.py` and invokes the guard for every production manifest discovered by `_production_package_manifests`. + +Independent review on PR #317 classified the omission as a security misconfiguration (CWE-693) and confirmed the repair statically. Hosted executable evidence remains separate because #317 is Draft. + +## Rejected alternatives + +### Treat `build.rs` as ordinary repository source only + +Rejected. The script itself may be reviewed while its generated `OUT_DIR` bytes and generator inputs remain outside the canonical production-source evidence contract. + +### Permit build dependencies but scan only local ones + +Rejected for this slice. Registry or Git build dependencies can influence generated code just as local build dependencies can. Allowing only part of the generator dependency graph would create a misleading provenance claim. + +### Ban build scripts permanently + +Rejected. A future browser adapter may have a legitimate need for generated bindings or native integration. At that point the same reviewed delta must introduce a generated-source provenance/reproducibility contract before widening this boundary. + +## Follow-up + +If a Browser Session-dependent production package needs a build script, the owner must first define the generator contract: immutable inputs and generator dependencies, exact toolchain/host/target identity, `OUT_DIR` artifact hashing, reproducible rebuild evidence, generated-source inclusion provenance, and rollback/release evidence. The build-surface prohibition may then be narrowed in the same reviewed exact tree; it must not be bypassed with workflow-only generation or mutable pre-generated artifacts. + +## References + +The Cargo Project Developers. (2026). *Build Scripts*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/build-scripts.html + +The Cargo Project Developers. (2026). *Specifying Dependencies: Build dependencies*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html From 15c170397b2feaa9e308aa3d7c066726fc5892ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:15:46 +0900 Subject: [PATCH 233/632] docs(browser-session): distinguish recovery custody integration --- docs/adr/0114-browser-session-disposable-context-authority.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0114-browser-session-disposable-context-authority.md b/docs/adr/0114-browser-session-disposable-context-authority.md index c1f9af684..8e3562dbb 100644 --- a/docs/adr/0114-browser-session-disposable-context-authority.md +++ b/docs/adr/0114-browser-session-disposable-context-authority.md @@ -173,7 +173,7 @@ Rollback may return to the predecessor active-PR API only if these authority fin ## Open follow-ups -- Define a purpose-bounded same-adapter recovery handoff for `RecoveryRequired` / `TransportLost` that carries exact recovery evidence without recreating ordinary mutation authority. +- Complete acceptance and protected-main integration of ADR 0116's purpose-bounded same-adapter recovery custody for `RecoveryRequired` / `TransportLost`; durable recovery persistence/reconciliation remains a separate follow-up. - Bound hot ownership state independently from durable/audit history so proven destruction does not create unbounded validation cost. - Restack #316 onto the verified Browser Session successor and implement WebDriver BiDi pending → accepted/quarantined transaction settlement plus typed presentation/reconciliation operations. - Define the separately authorized durable recovery persistence/reconciliation owner for Browser Session recovery evidence and unresolved abandonment. From a915dceb06d7109c15e0e93015e85340fe4e41b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:16:48 +0900 Subject: [PATCH 234/632] docs(browser-session): align changelog with current trust contracts --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d629b0b3b..a7c62b36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. +- Failed closed on production Cargo build surfaces (`build.rs`, enabled `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies) until generated-source provenance is explicitly modeled; `package.build = false` remains the explicit no-build-script form. +- Repaired Browser Session repository contracts so custom `[lib].path` / `[[bin]].path` fixtures reach the intended target-source assertion, recovery-custody accessor prohibitions inspect only `BoundBrowserSessionRecovery` inherent impls, and settlement ordering verifies delegated fact selection before proof I/O rather than duplicating selector internals. - Extended the Browser Session trusted-adapter source review to Cargo production targets declared through custom `[lib].path` and `[[bin]].path`, so an already reviewed Browser Session-dependent crate cannot add lifecycle-SPI source outside `src/` without entering the exact source allowlist; declared production target paths outside the repository review root or naming missing files fail closed. - Derived Browser Session trusted-adapter source and manifest review coverage from explicit Cargo `[workspace].members` instead of the `crates/*` directory convention, so production workspace members at other paths cannot evade lifecycle-SPI/dependency/binding review; workspace-member globs now fail closed until the security contract is explicitly extended. - Hardened Browser Session trusted-adapter dependency discovery so Cargo workspace-inherited aliases and target-specific production dependencies resolve to the canonical `originweave-browser-session` package before allowlist comparison; dev-only dependencies do not widen the shipped adapter-composition surface. @@ -24,7 +27,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Added exact Browser Session create-recovery transaction evidence for `CreateFailedUncertain(Some/None)`, duplicate candidates, and unsettled `Accepted|Rejected` completions, plus a hostile same-valued-handle fixture proving candidate facts and prior ownership facts remain distinct. - Added a 258-generation same-raw-context hostile acceptance proving proven-destroy hot-state retirement, monotonic context epochs, and predecessor-authority rejection before lifecycle I/O. -- Added a version-pinned `originweave-bidi` presentation-capability boundary for the W3C WebDriver BiDi Working Draft published on 9 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`). It depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. +- Added an `originweave-bidi` presentation-capability boundary referenced against the W3C WebDriver BiDi Working Draft published on 9 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`). That dated document is publication/reference evidence; the separately runtime-qualified compatibility pin remains the 3 September 2026 Working Draft until independent schema/semantics/conformance and pinned-Chromium evidence admit another revision. The boundary depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. - Added a bounded Rust presentation-identity kernel for explicit browser-visible profiles and credential-free replay digests, including control-safe mobile UA-CH model values; applying those profiles to Chromium and proving page-observed effects remain separate adapter and browser-E2E work. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. From 8644078afa250ae3877b5f71a57a258357ff2e5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:18:23 +0900 Subject: [PATCH 235/632] docs(standards): separate BiDi publication from runtime pin --- docs/doctoring.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 59c178549..68517e3d4 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -6,7 +6,7 @@ This document records external evidence that changes OriginWeave architecture, t ### Browser automation and interoperability -The 9 September 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. OriginWeave pins this publication to the immutable dated TR `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`; that document identifies the 3 September 2026 draft as its previous published version. The mutable `w3c.github.io/webdriver-bidi/` Editor's Draft is tracked separately and cannot silently redefine the adapter contract. Because the standard remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +As of 17 September 2026, the canonical publication-current receipt at `docs/traceability/webdriver-bidi-publication-current.md` records the 16 September 2026 WebDriver BiDi Working Draft as the latest published version and the 14 September 2026 Working Draft as the previous published version. Execution compatibility is a separate claim and remains qualified against the immutable 3 September 2026 dated TR `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/` until schema, semantics, conformance, and pinned-Chromium evidence admit another revision. The 9 September 2026 dated Working Draft remains historical/reference publication evidence, not the current publication or the runtime-qualified pin. The mutable `w3c.github.io/webdriver-bidi/` Editor's Draft is tracked separately and cannot silently redefine the adapter contract. Because the standard remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. The final Model Context Protocol `2026-07-28` specification defines the currently reviewed MCP generation. Its stateless request model carries protocol metadata per request and standard Streamable HTTP routing metadata for MCP operations; its Tools surface defines bounded, case-sensitive tool names and requires clients to treat tool annotations as untrusted unless supplied by a trusted server. OriginWeave therefore keeps MCP outside the product authority model. Active PR #168 implements only a bounded Rust `tools/call` routing/action-policy foundation for that exact generation; the complete transport, request-metadata, discovery, OAuth, browser, secret, and persistence adapter remains planned and cannot be inferred from the core routing primitive. @@ -48,19 +48,21 @@ object with enumerated architecture/bitness/platform tokens, an at-most-32 ASCII brand-name limit, a non-empty brand list, and the draft's coherence rule that a non-mobile user agent reports an empty model (see ADR 0112). -The pinned 9 September 2026 WebDriver BiDi Working Draft exposes locale, media, +The historical/reference 9 September 2026 WebDriver BiDi Working Draft exposes locale, media, screen, user-agent, viewport, and time-zone emulation commands under the immutable -publication `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`. The screen -shape contains width and height but not color depth, and locale accepts one value -rather than an ordered language list, so neither proves the corresponding complete -OriginWeave surface. The draft also does not define a hardware-concurrency -override. Chromium's tip-of-tree DevTools Protocol exposes -`Emulation.setHardwareConcurrencyOverride` as Experimental and warns that -tip-of-tree commands can change without notice. OriginWeave therefore records -required presentation surfaces in a protocol-neutral Rust admission contract; -the adapter records those four complete standard surfaces as protocol -capabilities, while the reusable-context plan emits only two typed command -intents—viewport/DPR and timezone—bound to one bounded opaque browsing context. +publication `https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`. Publication +currentness has since advanced to the 16 September Working Draft with 14 September +as the previous publication, while OriginWeave's execution compatibility remains +separately pinned to the 3 September Working Draft. The screen shape contains width +and height but not color depth, and locale accepts one value rather than an ordered +language list, so neither proves the corresponding complete OriginWeave surface. The +draft also does not define a hardware-concurrency override. Chromium's tip-of-tree +DevTools Protocol exposes `Emulation.setHardwareConcurrencyOverride` as Experimental +and warns that tip-of-tree commands can change without notice. OriginWeave therefore +records required presentation surfaces in a protocol-neutral Rust admission contract; +the adapter records those four complete standard surfaces as protocol capabilities, +while the reusable-context plan emits only two typed command intents—viewport/DPR and +timezone—bound to one bounded opaque browsing context. Cleanup authority is asymmetric. Nullable viewport and timezone operations can restore those adapter-owned overrides on a reusable context, so generic cleanup @@ -266,9 +268,11 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2025, September 25). *Mitigating browser fingerprinting in Web specifications*. https://www.w3.org/TR/fingerprinting-guidance/ -World Wide Web Consortium. (2026, September 9). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ +World Wide Web Consortium. (2026, September 9). *WebDriver BiDi* (W3C Working Draft; historical/reference publication). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/ -World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* (W3C Working Draft; runtime-qualified compatibility pin). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ + +OriginWeave. (2026, September 17). *WebDriver BiDi publication-current receipt*. `docs/traceability/webdriver-bidi-publication-current.md` World Wide Web Consortium. (2026). *WebDriver BiDi* (Editor's Draft). https://w3c.github.io/webdriver-bidi/ From 4c61ab17d6c332d5a4eebc341dbc2825cda3a64c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:18:38 +0900 Subject: [PATCH 236/632] test(standards): pin doctoring publication/runtime split --- tests/test_webdriver_bidi_docs_currentness_contract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_webdriver_bidi_docs_currentness_contract.py b/tests/test_webdriver_bidi_docs_currentness_contract.py index 40ad36015..6833894c5 100644 --- a/tests/test_webdriver_bidi_docs_currentness_contract.py +++ b/tests/test_webdriver_bidi_docs_currentness_contract.py @@ -31,7 +31,7 @@ def test_adr_tracks_merged_adapter_lineage_and_publication_receipt(self) -> None self.assertIn("14 September 2026 as the previous published version", adr) def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs(self) -> None: - """Architecture and doctoring stay qualification records; the receipt owns publication churn.""" + """Architecture and doctoring stay qualification records; the receipt owns publication URIs.""" architecture = (ROOT / "ARCHITECTURE.md").read_text(encoding="utf-8") doctoring = (ROOT / "docs/doctoring.md").read_text(encoding="utf-8") receipt = ( @@ -51,6 +51,12 @@ def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs self.assertIn(runtime_uri, text) self.assertNotIn(latest_uri, text) + self.assertIn("16 September 2026 WebDriver BiDi Working Draft", doctoring) + self.assertIn("14 September 2026 Working Draft", doctoring) + self.assertIn("runtime-qualified pin", doctoring) + self.assertIn("historical/reference publication", doctoring) + self.assertIn("docs/traceability/webdriver-bidi-publication-current.md", doctoring) + self.assertIn("Observed: 2026-09-16", receipt) self.assertIn("Runtime-compatible pin: `2026-09-03`", receipt) self.assertIn("Latest published Working Draft: `2026-09-16`", receipt) From f39d06ec76a19bd146765a0b4139764d3c27a7c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:23:22 +0900 Subject: [PATCH 237/632] test(browser-session): keep topology fixtures self-contained --- tests/test_browser_session_trusted_adapter_boundary.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 26fd3c7db..279077983 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -494,14 +494,13 @@ def test_workspace_member_outside_crates_glob_cannot_escape_review_surface(self) member.mkdir(parents=True) member_manifest = member / "Cargo.toml" member_manifest.write_text( - '[package]\nname = "browser-adapter"\nversion = "0.1.0"\n' - '[dependencies]\noriginweave-browser-session = { path = "../../crates/originweave-browser-session" }\n', + '[package]\nname = "browser-adapter"\nversion = "0.1.0"\n', encoding="utf-8", ) source = member / "src/lib.rs" source.parent.mkdir() source.write_text( - "use originweave_browser_session::DisposableContextPort;\n", + "pub fn browser_adapter_surface() {}\n", encoding="utf-8", ) @@ -514,14 +513,13 @@ def test_workspace_root_package_cannot_escape_review_surface(self) -> None: root_manifest = root / "Cargo.toml" root_manifest.write_text( '[package]\nname = "root-browser-adapter"\nversion = "0.1.0"\nedition = "2024"\n' - '[workspace]\nmembers = []\n' - '[dependencies]\noriginweave-browser-session = { path = "crates/originweave-browser-session" }\n', + '[workspace]\nmembers = []\n', encoding="utf-8", ) source = root / "src/lib.rs" source.parent.mkdir() source.write_text( - "use originweave_browser_session::DisposableContextPort;\n", + "pub fn root_browser_adapter_surface() {}\n", encoding="utf-8", ) From 1c72ea693e47be05b94b330a334118446cc99f39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:31:06 +0900 Subject: [PATCH 238/632] test(browser-session): reject unmodeled Cargo source overrides --- ..._browser_session_build_surface_contract.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_build_surface_contract.py b/tests/test_browser_session_build_surface_contract.py index 3f810c953..5afd1c4bf 100644 --- a/tests/test_browser_session_build_surface_contract.py +++ b/tests/test_browser_session_build_surface_contract.py @@ -15,7 +15,7 @@ class BrowserSessionBuildSurfaceContractTests(unittest.TestCase): - """Keep generated-code build surfaces out of the Browser Session TCB closure.""" + """Keep generated-code and source-override build surfaces out of the Browser Session TCB closure.""" def _workspace(self, manifest_suffix: str = "") -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: directory = tempfile.TemporaryDirectory() @@ -65,6 +65,42 @@ def test_target_specific_build_dependencies_fail_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "production Cargo build dependencies"): boundary._production_package_manifests(root) + def test_workspace_patch_override_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + patched = root / "patched-adapter" + (patched / "src").mkdir(parents=True) + (patched / "src/lib.rs").write_text("pub fn patched() {}\n", encoding="utf-8") + (patched / "Cargo.toml").write_text( + '[package]\nname = "patched-adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n\n' + '[patch.crates-io]\npatched-adapter = { path = "patched-adapter" }\n', + encoding="utf-8", + ) + with self.assertRaisesRegex(AssertionError, "Cargo source override"): + boundary._production_package_manifests(root) + + def test_workspace_replace_override_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + replacement = root / "replacement-adapter" + (replacement / "src").mkdir(parents=True) + (replacement / "src/lib.rs").write_text("pub fn replacement() {}\n", encoding="utf-8") + (replacement / "Cargo.toml").write_text( + '[package]\nname = "replacement-adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n\n' + '[replace]\n"replacement-adapter:0.1.0" = { path = "replacement-adapter" }\n', + encoding="utf-8", + ) + with self.assertRaisesRegex(AssertionError, "Cargo source override"): + boundary._production_package_manifests(root) + if __name__ == "__main__": unittest.main() From 9c3e4780fea9d111f4a362e63ef9531a3b023635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:33:40 +0900 Subject: [PATCH 239/632] fix(browser-session): fail closed on Cargo source overrides --- ...est_browser_session_trusted_adapter_boundary.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index 279077983..edf6e92da 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -132,6 +132,17 @@ def _assert_no_production_build_surfaces(root: pathlib.Path, manifest: pathlib.P ) +def _assert_no_workspace_source_overrides(root_manifest_path: pathlib.Path) -> None: + """Fail closed on Cargo patch/replace surfaces until override provenance is modeled.""" + parsed = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + for section_name in ("patch", "replace"): + section = parsed.get(section_name) + if isinstance(section, dict) and section: + raise AssertionError( + f"production Cargo source override requires an explicit trusted-adapter contract: [{section_name}]" + ) + + def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bool: member = tomllib.loads(member_text) workspace_manifest = tomllib.loads(workspace_text) @@ -228,6 +239,7 @@ def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: """Return workspace packages plus recursive in-repository production path dependencies.""" root_manifest_path = root / "Cargo.toml" root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + _assert_no_workspace_source_overrides(root_manifest_path) workspace = root_manifest.get("workspace") workspace_dependencies: dict[str, object] = {} if isinstance(workspace, dict): @@ -557,4 +569,4 @@ def test_workspace_member_globs_fail_closed_until_reviewed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From acb03ab0b8f72949ec80a7573b628aff0f9489c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:34:46 +0900 Subject: [PATCH 240/632] docs(traceability): record Cargo source-override fail-closed boundary --- ...-session-production-topology-single-writer.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md index facbda07f..5b4573347 100644 --- a/docs/traceability/browser-session-production-topology-single-writer.md +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -12,6 +12,8 @@ That split left a concrete composition escape: an implicit local path dependency A second review found that the recursive path resolver silently returned `None` when a production `path` dependency resolved outside the repository review root. Cargo permits local path dependencies outside the repository, but this security contract cannot inspect such a package's source, manifest evolution, or lifecycle binding. Silently omitting it would treat an unreviewable production dependency as if no production edge existed. +A third review found that the canonical closure followed ordinary and workspace-inherited `path` dependencies but did not model Cargo dependency-source overrides. Cargo applies root-manifest `[patch]` entries as an overlay to dependency resolution and still supports deprecated `[replace]`; either mechanism can cause the resolved production graph to differ from the direct dependency declarations inspected by the trusted-adapter scanner. Until override provenance is modeled, accepting those tables would let a production package enter through a resolution surface that the exact-head package/source closure does not own. + ## RED and repair - **Structural RED `a0fb0765d7df8303df490c97dbb5945b1682d837`** adds a hostile `app -> ../plugins/browser-adapter` fixture whose implicit local package calls `bind_lifecycle_port`. The enhanced supplemental production-source closure finds the source, while the canonical `_workspace_production_sources` scanner does not. @@ -20,15 +22,27 @@ A second review found that the recursive path resolver silently returned `None` - **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root and requires the canonical production-package closure to reject it instead of silently dropping the edge. - **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. A declared local dependency whose `Cargo.toml` is missing also fails closed. Registry and Git dependencies remain outside this local-path traversal; their package/source integrity is governed by the normal locked dependency and supply-chain controls rather than being misclassified as repository-local source. - **Review-driven coverage `600547a4f7cef3a9a22744e13e2890538f21b68a`** adds the direct hostile regression for the second branch of that fail-closed behavior: an in-repository production `path` dependency whose declared `Cargo.toml` is absent must raise instead of disappearing from the package closure. This was requested by the current-head independent review and does not change production behavior. +- **Source-override RED `1c72ea693e47be05b94b330a334118446cc99f39`** adds hostile root-workspace `[patch.crates-io]` and `[replace]` fixtures and requires the production-package closure to reject those unmodeled resolution overlays. +- **Source-override repair `9c3e4780fea9d111f4a362e63ef9531a3b023635`** keeps `tests/test_browser_session_trusted_adapter_boundary.py` as the correction-owning scanner and fails closed when the workspace-root manifest contains a non-empty `[patch]` or `[replace]` table. The repair does not attempt to infer the resolved graph from an override; any future override requires a reviewed provenance contract in the same exact-tree delta. ## Invariant There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. -The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, and declared target paths outside the repository review root. It must never convert an unreviewable production local dependency into absence. +The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, declared target paths outside the repository review root, Cargo build-script/build-dependency surfaces without generated-source provenance, and root-manifest dependency-source overrides that would make the resolved graph differ from the reviewed direct-dependency closure. It must never convert an unreviewable production local dependency or source override into absence. Every source in that closure is subject to the same lifecycle-SPI and caller-selected binding checks. A supplemental hostile fixture may exercise the scanner, but it must not maintain a second production-topology algorithm. +Repository-local Cargo configuration is an adjacent execution-environment surface, not silently equivalent to this manifest closure. Cargo supports configuration-level dependency patches and local-path overrides; if OriginWeave adds repository `.cargo/config*` override behavior, that surface must receive its own reviewed fail-closed/provenance contract before it can be treated as exact-head production evidence. + +## Primary references + +Cargo Team. (2026). *Workspaces*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/workspaces.html + +Cargo Team. (2026). *Dependency resolution*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/resolver.html + +Cargo Team. (2026). *Configuration*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/config.html + ## Scope This repair changes repository security coverage only. It does not change Browser Session runtime semantics, WebDriver BiDi protocol authority, Chromium behavior, or the trust classification of privileged in-process adapters. Exact-head executable repository/security evidence remains required after the parent lineage is reconciled and the PR becomes runnable. From 738cbea2ebfb85a966c709a88af8f10f974d4da6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:37:22 +0900 Subject: [PATCH 241/632] test(browser-session): reject repository Cargo config overrides --- ..._browser_session_build_surface_contract.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_browser_session_build_surface_contract.py b/tests/test_browser_session_build_surface_contract.py index 5afd1c4bf..210a4f35b 100644 --- a/tests/test_browser_session_build_surface_contract.py +++ b/tests/test_browser_session_build_surface_contract.py @@ -34,6 +34,11 @@ def _workspace(self, manifest_suffix: str = "") -> tuple[tempfile.TemporaryDirec ) return directory, root + def _write_cargo_config(self, root: pathlib.Path, name: str, content: str) -> None: + cargo = root / ".cargo" + cargo.mkdir(exist_ok=True) + (cargo / name).write_text(content, encoding="utf-8") + def test_default_build_rs_fails_closed(self) -> None: directory, root = self._workspace() with directory: @@ -101,6 +106,36 @@ def test_workspace_replace_override_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Cargo source override"): boundary._production_package_manifests(root) + def test_repository_cargo_config_paths_override_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + self._write_cargo_config(root, "config.toml", 'paths = ["../external-adapter"]\n') + with self.assertRaisesRegex(AssertionError, "Cargo config source override"): + boundary._production_package_manifests(root) + + def test_repository_extensionless_cargo_config_patch_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + self._write_cargo_config( + root, + "config", + '[patch.crates-io]\nadapter = { path = "../patched-adapter" }\n', + ) + with self.assertRaisesRegex(AssertionError, "Cargo config source override"): + boundary._production_package_manifests(root) + + def test_repository_cargo_config_source_replacement_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + self._write_cargo_config( + root, + "config.toml", + '[source.crates-io]\nreplace-with = "vendored"\n\n' + '[source.vendored]\ndirectory = "vendor"\n', + ) + with self.assertRaisesRegex(AssertionError, "Cargo config source override"): + boundary._production_package_manifests(root) + if __name__ == "__main__": unittest.main() From 86d24cf6afefa3bd594dad5d7062ec455d8c0572 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:38:25 +0900 Subject: [PATCH 242/632] fix(browser-session): fail closed on repository Cargo config overrides --- ...rowser_session_trusted_adapter_boundary.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index edf6e92da..f022239ca 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -143,6 +143,28 @@ def _assert_no_workspace_source_overrides(root_manifest_path: pathlib.Path) -> N ) +def _assert_no_repository_cargo_config_source_overrides(root: pathlib.Path) -> None: + """Fail closed on repository Cargo config surfaces that can alter dependency sources.""" + cargo_directory = root / ".cargo" + for config_name in ("config.toml", "config"): + config_path = cargo_directory / config_name + if not config_path.is_file(): + continue + parsed = tomllib.loads(config_path.read_text(encoding="utf-8")) + paths = parsed.get("paths") + patch = parsed.get("patch") + sources = parsed.get("source") + if ( + (isinstance(paths, list) and bool(paths)) + or (isinstance(patch, dict) and bool(patch)) + or (isinstance(sources, dict) and bool(sources)) + ): + raise AssertionError( + "production Cargo config source override requires an explicit trusted-adapter contract: " + f".cargo/{config_name}" + ) + + def _manifest_links_browser_session(member_text: str, workspace_text: str) -> bool: member = tomllib.loads(member_text) workspace_manifest = tomllib.loads(workspace_text) @@ -240,6 +262,7 @@ def _production_package_manifests(root: pathlib.Path) -> list[pathlib.Path]: root_manifest_path = root / "Cargo.toml" root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) _assert_no_workspace_source_overrides(root_manifest_path) + _assert_no_repository_cargo_config_source_overrides(root) workspace = root_manifest.get("workspace") workspace_dependencies: dict[str, object] = {} if isinstance(workspace, dict): From af6cfda93448be26634a85265a1d8af3466af073 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:38:52 +0900 Subject: [PATCH 243/632] docs(traceability): cover repository Cargo config overrides --- ...er-session-production-topology-single-writer.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md index 5b4573347..56d17c26f 100644 --- a/docs/traceability/browser-session-production-topology-single-writer.md +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -12,7 +12,7 @@ That split left a concrete composition escape: an implicit local path dependency A second review found that the recursive path resolver silently returned `None` when a production `path` dependency resolved outside the repository review root. Cargo permits local path dependencies outside the repository, but this security contract cannot inspect such a package's source, manifest evolution, or lifecycle binding. Silently omitting it would treat an unreviewable production dependency as if no production edge existed. -A third review found that the canonical closure followed ordinary and workspace-inherited `path` dependencies but did not model Cargo dependency-source overrides. Cargo applies root-manifest `[patch]` entries as an overlay to dependency resolution and still supports deprecated `[replace]`; either mechanism can cause the resolved production graph to differ from the direct dependency declarations inspected by the trusted-adapter scanner. Until override provenance is modeled, accepting those tables would let a production package enter through a resolution surface that the exact-head package/source closure does not own. +A third review found that the canonical closure followed ordinary and workspace-inherited `path` dependencies but did not model Cargo dependency-source overrides. Root-manifest `[patch]` / `[replace]` and repository Cargo configuration can cause Cargo's resolved production graph to differ from the direct dependency declarations inspected by the trusted-adapter scanner. Cargo configuration supports local `paths`, `[patch]`, and `[source]` replacement surfaces, and Cargo reads both `.cargo/config.toml` and the legacy `.cargo/config` form from the workspace root. Until override provenance is modeled, accepting those surfaces would let production code enter through a resolution path that the exact-head package/source closure does not own. ## RED and repair @@ -21,19 +21,21 @@ A third review found that the canonical closure followed ordinary and workspace- - **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. `tests/test_browser_session_custom_target_source_contract.py` continues to consume that same helper through the implicit-workspace contract. - **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root and requires the canonical production-package closure to reject it instead of silently dropping the edge. - **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. A declared local dependency whose `Cargo.toml` is missing also fails closed. Registry and Git dependencies remain outside this local-path traversal; their package/source integrity is governed by the normal locked dependency and supply-chain controls rather than being misclassified as repository-local source. -- **Review-driven coverage `600547a4f7cef3a9a22744e13e2890538f21b68a`** adds the direct hostile regression for the second branch of that fail-closed behavior: an in-repository production `path` dependency whose declared `Cargo.toml` is absent must raise instead of disappearing from the package closure. This was requested by the current-head independent review and does not change production behavior. -- **Source-override RED `1c72ea693e47be05b94b330a334118446cc99f39`** adds hostile root-workspace `[patch.crates-io]` and `[replace]` fixtures and requires the production-package closure to reject those unmodeled resolution overlays. -- **Source-override repair `9c3e4780fea9d111f4a362e63ef9531a3b023635`** keeps `tests/test_browser_session_trusted_adapter_boundary.py` as the correction-owning scanner and fails closed when the workspace-root manifest contains a non-empty `[patch]` or `[replace]` table. The repair does not attempt to infer the resolved graph from an override; any future override requires a reviewed provenance contract in the same exact-tree delta. +- **Review-driven coverage `600547a4f7cef3a9a22744e13e2890538f21b68a`** adds the direct hostile regression for an in-repository production `path` dependency whose declared `Cargo.toml` is absent. +- **Manifest source-override RED `1c72ea693e47be05b94b330a334118446cc99f39`** adds hostile root-workspace `[patch.crates-io]` and `[replace]` fixtures. +- **Manifest source-override repair `9c3e4780fea9d111f4a362e63ef9531a3b023635`** fails closed when the workspace-root manifest contains a non-empty `[patch]` or `[replace]` table. +- **Repository-config RED `738cbea2ebfb85a966c709a88af8f10f974d4da6`** adds hostile `.cargo/config.toml` `paths`, legacy `.cargo/config` `[patch.crates-io]`, and `[source] replace-with` / directory-source fixtures. These are repository-controlled inputs that Cargo can merge into dependency resolution even though they are not present in `Cargo.toml`. +- **Repository-config repair `86d24cf6afefa3bd594dad5d7062ec455d8c0572`** extends the same correction-owning production-package scanner to inspect both repository config filenames and reject non-empty local-path overrides, config patches, or source tables until a versioned override-provenance model exists. Ordinary Cargo settings that do not alter package sources are not rejected by this contract. ## Invariant There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. -The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, declared target paths outside the repository review root, Cargo build-script/build-dependency surfaces without generated-source provenance, and root-manifest dependency-source overrides that would make the resolved graph differ from the reviewed direct-dependency closure. It must never convert an unreviewable production local dependency or source override into absence. +The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, declared target paths outside the repository review root, Cargo build-script/build-dependency surfaces without generated-source provenance, root-manifest `[patch]` / `[replace]`, and repository `.cargo/config.toml` / `.cargo/config` source-override surfaces. It must never convert an unreviewable production dependency or source override into absence. Every source in that closure is subject to the same lifecycle-SPI and caller-selected binding checks. A supplemental hostile fixture may exercise the scanner, but it must not maintain a second production-topology algorithm. -Repository-local Cargo configuration is an adjacent execution-environment surface, not silently equivalent to this manifest closure. Cargo supports configuration-level dependency patches and local-path overrides; if OriginWeave adds repository `.cargo/config*` override behavior, that surface must receive its own reviewed fail-closed/provenance contract before it can be treated as exact-head production evidence. +Cargo can also merge configuration from ancestor directories, `$CARGO_HOME`, environment-derived settings, and command-line `--config`. Those are execution-environment inputs rather than repository source. They must be captured or excluded by the canonical CI/release environment before an executable build is treated as reproducible exact-head evidence; this repository contract does not pretend that untracked ambient configuration is Git-owned source. ## Primary references From 4bdad2aabac71e4fb036aa0b83f0191eac981010 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:42:56 +0900 Subject: [PATCH 244/632] test(browser-session): reject nested Cargo config overrides --- ..._browser_session_build_surface_contract.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_build_surface_contract.py b/tests/test_browser_session_build_surface_contract.py index 210a4f35b..4fe893eef 100644 --- a/tests/test_browser_session_build_surface_contract.py +++ b/tests/test_browser_session_build_surface_contract.py @@ -34,8 +34,8 @@ def _workspace(self, manifest_suffix: str = "") -> tuple[tempfile.TemporaryDirec ) return directory, root - def _write_cargo_config(self, root: pathlib.Path, name: str, content: str) -> None: - cargo = root / ".cargo" + def _write_cargo_config(self, directory: pathlib.Path, name: str, content: str) -> None: + cargo = directory / ".cargo" cargo.mkdir(exist_ok=True) (cargo / name).write_text(content, encoding="utf-8") @@ -55,9 +55,7 @@ def test_custom_package_build_path_fails_closed(self) -> None: boundary._production_package_manifests(root) def test_build_dependencies_fail_closed(self) -> None: - directory, root = self._workspace( - '[build-dependencies]\nserde = "1"\n' - ) + directory, root = self._workspace('[build-dependencies]\nserde = "1"\n') with directory: with self.assertRaisesRegex(AssertionError, "production Cargo build dependencies"): boundary._production_package_manifests(root) @@ -136,6 +134,17 @@ def test_repository_cargo_config_source_replacement_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Cargo config source override"): boundary._production_package_manifests(root) + def test_nested_git_owned_cargo_config_override_fails_closed(self) -> None: + directory, root = self._workspace() + with directory: + self._write_cargo_config( + root / "adapter", + "config.toml", + 'paths = ["../../external-adapter"]\n', + ) + with self.assertRaisesRegex(AssertionError, "Cargo config source override"): + boundary._production_package_manifests(root) + if __name__ == "__main__": unittest.main() From 1c2dfab389a72e9d59c1ca4cdf11d233d490d8a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:43:55 +0900 Subject: [PATCH 245/632] fix(browser-session): cover nested Cargo config overrides --- ...rowser_session_trusted_adapter_boundary.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/test_browser_session_trusted_adapter_boundary.py b/tests/test_browser_session_trusted_adapter_boundary.py index f022239ca..f63445fdc 100644 --- a/tests/test_browser_session_trusted_adapter_boundary.py +++ b/tests/test_browser_session_trusted_adapter_boundary.py @@ -144,13 +144,26 @@ def _assert_no_workspace_source_overrides(root_manifest_path: pathlib.Path) -> N def _assert_no_repository_cargo_config_source_overrides(root: pathlib.Path) -> None: - """Fail closed on repository Cargo config surfaces that can alter dependency sources.""" - cargo_directory = root / ".cargo" - for config_name in ("config.toml", "config"): - config_path = cargo_directory / config_name - if not config_path.is_file(): - continue - parsed = tomllib.loads(config_path.read_text(encoding="utf-8")) + """Fail closed on Git-owned Cargo config surfaces that can alter dependency sources.""" + root_resolved = root.resolve() + config_paths: set[pathlib.Path] = set() + for pattern in (".cargo/config.toml", ".cargo/config"): + config_paths.update(root.rglob(pattern)) + + for config_path in sorted(config_paths): + resolved = config_path.resolve() + try: + resolved.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"Cargo config escapes repository review root: {config_path.relative_to(root).as_posix()}" + ) from exc + if not resolved.is_file(): + raise AssertionError( + f"Cargo config is missing: {config_path.relative_to(root).as_posix()}" + ) + + parsed = tomllib.loads(resolved.read_text(encoding="utf-8")) paths = parsed.get("paths") patch = parsed.get("patch") sources = parsed.get("source") @@ -159,9 +172,10 @@ def _assert_no_repository_cargo_config_source_overrides(root: pathlib.Path) -> N or (isinstance(patch, dict) and bool(patch)) or (isinstance(sources, dict) and bool(sources)) ): + relative = config_path.relative_to(root).as_posix() raise AssertionError( "production Cargo config source override requires an explicit trusted-adapter contract: " - f".cargo/{config_name}" + f"{relative}" ) From ab11e502e2520edf2ed547f89c3d987c9990395d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 18:44:19 +0900 Subject: [PATCH 246/632] docs(traceability): record nested Cargo config review repair --- ...ession-production-topology-single-writer.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/traceability/browser-session-production-topology-single-writer.md b/docs/traceability/browser-session-production-topology-single-writer.md index 56d17c26f..6c2ff2014 100644 --- a/docs/traceability/browser-session-production-topology-single-writer.md +++ b/docs/traceability/browser-session-production-topology-single-writer.md @@ -12,30 +12,32 @@ That split left a concrete composition escape: an implicit local path dependency A second review found that the recursive path resolver silently returned `None` when a production `path` dependency resolved outside the repository review root. Cargo permits local path dependencies outside the repository, but this security contract cannot inspect such a package's source, manifest evolution, or lifecycle binding. Silently omitting it would treat an unreviewable production dependency as if no production edge existed. -A third review found that the canonical closure followed ordinary and workspace-inherited `path` dependencies but did not model Cargo dependency-source overrides. Root-manifest `[patch]` / `[replace]` and repository Cargo configuration can cause Cargo's resolved production graph to differ from the direct dependency declarations inspected by the trusted-adapter scanner. Cargo configuration supports local `paths`, `[patch]`, and `[source]` replacement surfaces, and Cargo reads both `.cargo/config.toml` and the legacy `.cargo/config` form from the workspace root. Until override provenance is modeled, accepting those surfaces would let production code enter through a resolution path that the exact-head package/source closure does not own. +A third review found that the canonical closure followed ordinary and workspace-inherited `path` dependencies but did not model Cargo dependency-source overrides. Root-manifest `[patch]` / `[replace]` and Git-owned Cargo configuration can cause Cargo's resolved production graph to differ from the direct dependency declarations inspected by the trusted-adapter scanner. Cargo configuration supports local `paths`, `[patch]`, and `[source]` replacement surfaces and is hierarchical according to the directory from which Cargo is invoked. Restricting review to only the repository-root `.cargo/config*` therefore leaves a Git-owned nested config usable whenever a repository command executes below that directory. ## RED and repair - **Structural RED `a0fb0765d7df8303df490c97dbb5945b1682d837`** adds a hostile `app -> ../plugins/browser-adapter` fixture whose implicit local package calls `bind_lifecycle_port`. The enhanced supplemental production-source closure finds the source, while the canonical `_workspace_production_sources` scanner does not. - **Minimal causal repair `6b050ee820a29342a9a180638a38b85b33cd66a3`** moves recursive in-repository production `path` dependency traversal and custom production target discovery into the canonical trusted-adapter boundary. The same source closure now drives lifecycle-SPI references, Browser Session dependency allowlists, dormant-entry equality, and caller-selected lifecycle-binding rejection. -- **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. `tests/test_browser_session_custom_target_source_contract.py` continues to consume that same helper through the implicit-workspace contract. -- **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root and requires the canonical production-package closure to reject it instead of silently dropping the edge. -- **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. A declared local dependency whose `Cargo.toml` is missing also fails closed. Registry and Git dependencies remain outside this local-path traversal; their package/source integrity is governed by the normal locked dependency and supply-chain controls rather than being misclassified as repository-local source. +- **Single-writer cleanup `18b560c869c5e967a3226d5407bc7216b80f9c80`** removes the duplicate Cargo-topology implementation from the implicit-workspace contract and delegates its hostile fixtures to the canonical boundary scanner. +- **External-path RED `0b0204b30585a2a5921a7b1e193121daf23aff50`** adds a production dependency whose manifest lives outside the repository review root. +- **Fail-closed repair `95c49d22e557466c063124989808d02ae363c609`** makes a declared production Cargo `path` dependency outside the repository review root an explicit contract failure. Missing declared local manifests fail closed as well. - **Review-driven coverage `600547a4f7cef3a9a22744e13e2890538f21b68a`** adds the direct hostile regression for an in-repository production `path` dependency whose declared `Cargo.toml` is absent. - **Manifest source-override RED `1c72ea693e47be05b94b330a334118446cc99f39`** adds hostile root-workspace `[patch.crates-io]` and `[replace]` fixtures. - **Manifest source-override repair `9c3e4780fea9d111f4a362e63ef9531a3b023635`** fails closed when the workspace-root manifest contains a non-empty `[patch]` or `[replace]` table. -- **Repository-config RED `738cbea2ebfb85a966c709a88af8f10f974d4da6`** adds hostile `.cargo/config.toml` `paths`, legacy `.cargo/config` `[patch.crates-io]`, and `[source] replace-with` / directory-source fixtures. These are repository-controlled inputs that Cargo can merge into dependency resolution even though they are not present in `Cargo.toml`. -- **Repository-config repair `86d24cf6afefa3bd594dad5d7062ec455d8c0572`** extends the same correction-owning production-package scanner to inspect both repository config filenames and reject non-empty local-path overrides, config patches, or source tables until a versioned override-provenance model exists. Ordinary Cargo settings that do not alter package sources are not rejected by this contract. +- **Repository-config RED `738cbea2ebfb85a966c709a88af8f10f974d4da6`** adds hostile repository-root `.cargo/config.toml` `paths`, legacy `.cargo/config` `[patch.crates-io]`, and `[source] replace-with / directory` fixtures. +- **Repository-config repair `86d24cf6afefa3bd594dad5d7062ec455d8c0572`** extends the same correction-owning package scanner to reject those source-altering repository configs. +- **Independent-review RED `4bdad2aabac71e4fb036aa0b83f0191eac981010`** adds a Git-owned nested `adapter/.cargo/config.toml` local-path override. The earlier root-only scanner misses it even though Cargo configuration is selected hierarchically from the command working directory. +- **Nested-config repair `1c2dfab389a72e9d59c1ca4cdf11d233d490d8a5`** keeps a single scanner but discovers both `.cargo/config.toml` and legacy `.cargo/config` anywhere below the repository review root, requires each resolved config file to remain inside that root, and rejects source-altering `paths`, `[patch]`, or `[source]` tables. Ordinary Cargo settings that do not alter package sources remain outside this fail-closed rule. ## Invariant There is one repository-security definition of the Browser Session production package/source closure. It starts from explicit workspace packages and an optional workspace-root package, follows production in-repository `path` dependencies including workspace-inherited target-specific dependencies, and includes ordinary `src/**/*.rs` plus manifest-declared `[lib].path` and `[[bin]].path` sources. -The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, declared target paths outside the repository review root, Cargo build-script/build-dependency surfaces without generated-source provenance, root-manifest `[patch]` / `[replace]`, and repository `.cargo/config.toml` / `.cargo/config` source-override surfaces. It must never convert an unreviewable production dependency or source override into absence. +The closure fails closed for unsupported workspace-member globs, declared production local-path dependencies that leave the repository review root, missing declared local dependency manifests, missing declared production targets, declared target paths outside the repository review root, Cargo build-script/build-dependency surfaces without generated-source provenance, root-manifest `[patch]` / `[replace]`, and Git-owned `.cargo/config.toml` / `.cargo/config` source-override surfaces anywhere inside the repository. It must never convert an unreviewable production dependency or source override into absence. Every source in that closure is subject to the same lifecycle-SPI and caller-selected binding checks. A supplemental hostile fixture may exercise the scanner, but it must not maintain a second production-topology algorithm. -Cargo can also merge configuration from ancestor directories, `$CARGO_HOME`, environment-derived settings, and command-line `--config`. Those are execution-environment inputs rather than repository source. They must be captured or excluded by the canonical CI/release environment before an executable build is treated as reproducible exact-head evidence; this repository contract does not pretend that untracked ambient configuration is Git-owned source. +Cargo can also merge configuration from directories outside the Git review root, `$CARGO_HOME`, environment-derived settings, and command-line `--config`. Those are execution-environment inputs rather than repository source. They must be captured or excluded by the canonical CI/release environment before an executable build is treated as reproducible exact-head evidence; this repository contract does not pretend that untracked ambient configuration is Git-owned source. ## Primary references From d8741e7a88b45c5c926963801493afc4ec14462a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:02:40 +0900 Subject: [PATCH 247/632] test(browser-session): expose Rust source-indirection provenance gap --- ...ession_rust_source_indirection_contract.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_browser_session_rust_source_indirection_contract.py diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py new file mode 100644 index 000000000..4f62ed9a1 --- /dev/null +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -0,0 +1,68 @@ +import importlib.util +import pathlib +import re +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + +spec = importlib.util.spec_from_file_location("browser_session_trusted_adapter_boundary", BOUNDARY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session trusted-adapter boundary contract") +boundary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(boundary) + + +PATH_ATTRIBUTE = re.compile(r'#\s*\[\s*path\s*=\s*"([^"]+)"\s*\]') + + +def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: + """RED placeholder: Rust source indirection must become an explicit reviewed surface.""" + return None + + +class BrowserSessionRustSourceIndirectionContractTests(unittest.TestCase): + """Keep Rust source indirection inside the exact-head production-source provenance boundary.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + return root + + def test_include_macro_fails_closed_until_included_source_provenance_is_modeled(self) -> None: + root = self._workspace_with_source('include!("../generated_adapter.rs");\n') + (root / "adapter/generated_adapter.rs").write_text( + "pub fn generated_adapter_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): + _assert_no_unmodeled_rust_source_indirection(root) + + def test_parent_traversal_path_attribute_fails_closed_until_module_provenance_is_modeled(self) -> None: + root = self._workspace_with_source('#[path = "../shared_adapter.rs"]\nmod shared_adapter;\n') + (root / "adapter/shared_adapter.rs").write_text( + "pub fn shared_adapter_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "Rust path attribute escapes canonical source closure"): + _assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From f42b1012306290311cd240671376096ca107bae2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:03:01 +0900 Subject: [PATCH 248/632] fix(browser-session): fail closed on unmodeled Rust source indirection --- ...ession_rust_source_indirection_contract.py | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 4f62ed9a1..818f98433 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -15,12 +15,35 @@ spec.loader.exec_module(boundary) +INCLUDE_MACRO = re.compile(r"(? None: - """RED placeholder: Rust source indirection must become an explicit reviewed surface.""" - return None + """Fail closed when reviewed Rust source can pull unmodeled executable source bytes.""" + for source in boundary._workspace_production_sources(root): + text = source.read_text(encoding="utf-8") + relative = source.relative_to(root).as_posix() + + if INCLUDE_MACRO.search(text): + raise AssertionError( + f"Rust include! source indirection requires an explicit provenance contract: {relative}" + ) + + for match in PATH_ATTRIBUTE.finditer(text): + declared_path = match.group(1) + normalized = declared_path.replace("\\", "/") + parts = pathlib.PurePosixPath(normalized).parts + if ( + pathlib.PurePosixPath(normalized).is_absolute() + or WINDOWS_ABSOLUTE_PATH.match(declared_path) + or ".." in parts + ): + raise AssertionError( + "Rust path attribute escapes canonical source closure and requires an explicit provenance contract: " + f"{relative} -> {declared_path}" + ) class BrowserSessionRustSourceIndirectionContractTests(unittest.TestCase): @@ -43,6 +66,9 @@ def _workspace_with_source(self, source_text: str) -> pathlib.Path: (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") return root + def test_current_production_sources_have_no_unmodeled_source_indirection(self) -> None: + _assert_no_unmodeled_rust_source_indirection(ROOT) + def test_include_macro_fails_closed_until_included_source_provenance_is_modeled(self) -> None: root = self._workspace_with_source('include!("../generated_adapter.rs");\n') (root / "adapter/generated_adapter.rs").write_text( @@ -63,6 +89,15 @@ def test_parent_traversal_path_attribute_fails_closed_until_module_provenance_is with self.assertRaisesRegex(AssertionError, "Rust path attribute escapes canonical source closure"): _assert_no_unmodeled_rust_source_indirection(root) + def test_in_tree_relative_path_attribute_remains_allowed(self) -> None: + root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') + (root / "adapter/src/nested.rs").write_text( + "pub fn nested_adapter_surface() {}\n", + encoding="utf-8", + ) + + _assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From 5068055be9acc3c8dc5ebd167a2b26099cbdacd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:03:37 +0900 Subject: [PATCH 249/632] fix(browser-session): require exact review for Rust source indirection --- ...ession_rust_source_indirection_contract.py | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 818f98433..30575b58b 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -17,11 +17,15 @@ INCLUDE_MACRO = re.compile(r"(? None: """Fail closed when reviewed Rust source can pull unmodeled executable source bytes.""" + discovered_path_attributes: set[tuple[str, str]] = set() + for source in boundary._workspace_production_sources(root): text = source.read_text(encoding="utf-8") relative = source.relative_to(root).as_posix() @@ -32,18 +36,20 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: ) for match in PATH_ATTRIBUTE.finditer(text): - declared_path = match.group(1) - normalized = declared_path.replace("\\", "/") - parts = pathlib.PurePosixPath(normalized).parts - if ( - pathlib.PurePosixPath(normalized).is_absolute() - or WINDOWS_ABSOLUTE_PATH.match(declared_path) - or ".." in parts - ): - raise AssertionError( - "Rust path attribute escapes canonical source closure and requires an explicit provenance contract: " - f"{relative} -> {declared_path}" - ) + discovered_path_attributes.add((relative, match.group(1))) + + unexpected = discovered_path_attributes - APPROVED_RUST_PATH_ATTRIBUTES + if unexpected: + raise AssertionError( + "Rust path attribute requires an explicit exact-tree provenance review: " + f"{sorted(unexpected)}" + ) + + stale = APPROVED_RUST_PATH_ATTRIBUTES - discovered_path_attributes + if root.resolve() == ROOT.resolve() and stale: + raise AssertionError( + f"Rust path-attribute allowlist preapproves absent production surfaces: {sorted(stale)}" + ) class BrowserSessionRustSourceIndirectionContractTests(unittest.TestCase): @@ -79,24 +85,25 @@ def test_include_macro_fails_closed_until_included_source_provenance_is_modeled( with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): _assert_no_unmodeled_rust_source_indirection(root) - def test_parent_traversal_path_attribute_fails_closed_until_module_provenance_is_modeled(self) -> None: - root = self._workspace_with_source('#[path = "../shared_adapter.rs"]\nmod shared_adapter;\n') - (root / "adapter/shared_adapter.rs").write_text( - "pub fn shared_adapter_surface() {}\n", + def test_new_path_attribute_fails_closed_even_when_target_is_in_tree(self) -> None: + root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') + (root / "adapter/src/nested.rs").write_text( + "pub fn nested_adapter_surface() {}\n", encoding="utf-8", ) - with self.assertRaisesRegex(AssertionError, "Rust path attribute escapes canonical source closure"): + with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): _assert_no_unmodeled_rust_source_indirection(root) - def test_in_tree_relative_path_attribute_remains_allowed(self) -> None: - root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') - (root / "adapter/src/nested.rs").write_text( - "pub fn nested_adapter_surface() {}\n", + def test_parent_traversal_path_attribute_fails_closed(self) -> None: + root = self._workspace_with_source('#[path = "../shared_adapter.rs"]\nmod shared_adapter;\n') + (root / "adapter/shared_adapter.rs").write_text( + "pub fn shared_adapter_surface() {}\n", encoding="utf-8", ) - _assert_no_unmodeled_rust_source_indirection(root) + with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): + _assert_no_unmodeled_rust_source_indirection(root) if __name__ == "__main__": From 88e63fe6f5fbc7a3e66e51e6e209588b2935ccc6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:04:13 +0900 Subject: [PATCH 250/632] docs(browser-session): trace Rust source-indirection provenance boundary --- ...browser-session-rust-source-indirection.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/traceability/browser-session-rust-source-indirection.md diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md new file mode 100644 index 000000000..cba14a477 --- /dev/null +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -0,0 +1,48 @@ +# Browser Session Rust source-indirection provenance + +Status: active PR evidence only. This contract does not claim protected-main or release acceptance. + +## Problem + +The Browser Session trusted-adapter boundary already derives one canonical Cargo production package/source closure and rejects repository-external or dangling source objects. That is not sufficient by itself because reviewed Rust source can cause `rustc` to parse additional source bytes through language-level indirection. + +Two surfaces matter here: + +- `include!(...)` parses another file as an expression or item. Rust documents the included path as relative to the source file containing the invocation. +- `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. + +Primary references: + +- Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html +- Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#the-path-attribute + +Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and ordinary `src/**/*.rs` entry point inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. +- This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. +- Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact pair is the only currently reviewed path-attribute exception. +- No future BiDi adapter path or Rust source indirection is pre-authorized. + +## Decision + +Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!`. + +`#[path = "..."]` is exact-tree allowlisted by `(source_path, declared_path)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: + +`crates/originweave-core/src/root.rs` → `lib.rs` + +Any additional path attribute must arrive in the same reviewed delta that explains and tests its source provenance. This is intentionally stricter than accepting apparently in-tree relative strings because Rust module-path resolution has context-sensitive semantics and a future filesystem indirection must not silently widen the Browser Session TCB. + +## RED → repair evidence + +- `d8741e7a88b45c5c926963801493afc4ec14462a` added hostile `include!` and parent-traversal `#[path]` fixtures while the contract body was deliberately inert, exposing the missing fail-closed behavior. +- `f42b1012306290311cd240671376096ca107bae2` added the first source-indirection guard consuming the canonical production-source closure. +- `5068055be9acc3c8dc5ebd167a2b26099cbdacd4` tightened `#[path]` handling from path-shape heuristics to an exact current-tree allowlist, preserving the existing reviewed `originweave-core/src/root.rs -> lib.rs` exception while rejecting any new path attribute until reviewed. + +This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. + +## Follow-up + +If OriginWeave later needs `include!` or additional `#[path]` surfaces in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. From a6f6d545df4d890edebe82ddc78fe4f36a0a1cfd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:05:42 +0900 Subject: [PATCH 251/632] test(browser-session): cover cfg_attr source-path indirection --- ...ession_rust_source_indirection_contract.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 30575b58b..f3f7d236d 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -16,12 +16,17 @@ INCLUDE_MACRO = re.compile(r"(? str: + return " ".join(body.split()) + + def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: """Fail closed when reviewed Rust source can pull unmodeled executable source bytes.""" discovered_path_attributes: set[tuple[str, str]] = set() @@ -35,8 +40,10 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: f"Rust include! source indirection requires an explicit provenance contract: {relative}" ) - for match in PATH_ATTRIBUTE.finditer(text): - discovered_path_attributes.add((relative, match.group(1))) + for match in RUST_ATTRIBUTE.finditer(text): + attribute_body = _normalized_attribute_body(match.group(1)) + if PATH_META.search(attribute_body): + discovered_path_attributes.add((relative, attribute_body)) unexpected = discovered_path_attributes - APPROVED_RUST_PATH_ATTRIBUTES if unexpected: @@ -95,6 +102,18 @@ def test_new_path_attribute_fails_closed_even_when_target_is_in_tree(self) -> No with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): _assert_no_unmodeled_rust_source_indirection(root) + def test_cfg_attr_generated_path_attribute_fails_closed(self) -> None: + root = self._workspace_with_source( + '#[cfg_attr(unix, path = "unix_adapter.rs")]\nmod platform_adapter;\n' + ) + (root / "adapter/src/unix_adapter.rs").write_text( + "pub fn unix_adapter_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): + _assert_no_unmodeled_rust_source_indirection(root) + def test_parent_traversal_path_attribute_fails_closed(self) -> None: root = self._workspace_with_source('#[path = "../shared_adapter.rs"]\nmod shared_adapter;\n') (root / "adapter/shared_adapter.rs").write_text( From 72fb8b410d05a252bda7da81a428fcdbcfa31f4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:05:58 +0900 Subject: [PATCH 252/632] docs(browser-session): record conditional Rust source indirection --- .../browser-session-rust-source-indirection.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index cba14a477..03dc24327 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -6,15 +6,17 @@ Status: active PR evidence only. This contract does not claim protected-main or The Browser Session trusted-adapter boundary already derives one canonical Cargo production package/source closure and rejects repository-external or dangling source objects. That is not sufficient by itself because reviewed Rust source can cause `rustc` to parse additional source bytes through language-level indirection. -Two surfaces matter here: +Three related forms matter here: - `include!(...)` parses another file as an expression or item. Rust documents the included path as relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. +- `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html - Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#the-path-attribute +- Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and ordinary `src/**/*.rs` entry point inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. @@ -22,27 +24,28 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. - This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. -- Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact pair is the only currently reviewed path-attribute exception. +- Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!`. -`#[path = "..."]` is exact-tree allowlisted by `(source_path, declared_path)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: +Any Rust attribute containing `path =` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: -`crates/originweave-core/src/root.rs` → `lib.rs` +`crates/originweave-core/src/root.rs` → `path = "lib.rs"` -Any additional path attribute must arrive in the same reviewed delta that explains and tests its source provenance. This is intentionally stricter than accepting apparently in-tree relative strings because Rust module-path resolution has context-sensitive semantics and a future filesystem indirection must not silently widen the Browser Session TCB. +Any additional path-bearing attribute must arrive in the same reviewed delta that explains and tests its source provenance. This is intentionally stricter than accepting apparently in-tree relative strings because Rust module-path resolution is context-sensitive and conditional attributes can change the selected source by target configuration. A future filesystem indirection must not silently widen the Browser Session TCB. ## RED → repair evidence - `d8741e7a88b45c5c926963801493afc4ec14462a` added hostile `include!` and parent-traversal `#[path]` fixtures while the contract body was deliberately inert, exposing the missing fail-closed behavior. - `f42b1012306290311cd240671376096ca107bae2` added the first source-indirection guard consuming the canonical production-source closure. - `5068055be9acc3c8dc5ebd167a2b26099cbdacd4` tightened `#[path]` handling from path-shape heuristics to an exact current-tree allowlist, preserving the existing reviewed `originweave-core/src/root.rs -> lib.rs` exception while rejecting any new path attribute until reviewed. +- `a6f6d545df4d890edebe82ddc78fe4f36a0a1cfd` added a hostile `cfg_attr(..., path = ...)` fixture and generalized discovery from only direct `#[path]` spellings to any Rust attribute carrying `path =`, preventing conditional compilation from bypassing the exact-tree review surface. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -If OriginWeave later needs `include!` or additional `#[path]` surfaces in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. +If OriginWeave later needs `include!` or additional path-bearing attributes in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. From cf9a1902d9c4c184121850ea77cf3f1ca4ce29ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:09:50 +0900 Subject: [PATCH 253/632] test(browser-session): reproduce include delimiter bypasses --- ...ession_rust_source_indirection_contract.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index f3f7d236d..d4ca49a05 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -79,11 +79,8 @@ def _workspace_with_source(self, source_text: str) -> pathlib.Path: (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") return root - def test_current_production_sources_have_no_unmodeled_source_indirection(self) -> None: - _assert_no_unmodeled_rust_source_indirection(ROOT) - - def test_include_macro_fails_closed_until_included_source_provenance_is_modeled(self) -> None: - root = self._workspace_with_source('include!("../generated_adapter.rs");\n') + def _assert_include_form_fails_closed(self, source_text: str) -> None: + root = self._workspace_with_source(source_text) (root / "adapter/generated_adapter.rs").write_text( "pub fn generated_adapter_surface() {}\n", encoding="utf-8", @@ -92,6 +89,18 @@ def test_include_macro_fails_closed_until_included_source_provenance_is_modeled( with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): _assert_no_unmodeled_rust_source_indirection(root) + def test_current_production_sources_have_no_unmodeled_source_indirection(self) -> None: + _assert_no_unmodeled_rust_source_indirection(ROOT) + + def test_parenthesized_include_macro_fails_closed(self) -> None: + self._assert_include_form_fails_closed('include!("../generated_adapter.rs");\n') + + def test_braced_include_macro_fails_closed(self) -> None: + self._assert_include_form_fails_closed('include! { "../generated_adapter.rs" }\n') + + def test_bracketed_include_macro_fails_closed(self) -> None: + self._assert_include_form_fails_closed('include!["../generated_adapter.rs"];\n') + def test_new_path_attribute_fails_closed_even_when_target_is_in_tree(self) -> None: root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') (root / "adapter/src/nested.rs").write_text( From 52dae82b4d4a26ae56cb81913e6d8e1daf7a6e19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 19:10:06 +0900 Subject: [PATCH 254/632] fix(browser-session): reject every include macro delimiter --- tests/test_browser_session_rust_source_indirection_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index d4ca49a05..b75693b0b 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -15,7 +15,7 @@ spec.loader.exec_module(boundary) -INCLUDE_MACRO = re.compile(r"(? Date: Thu, 17 Sep 2026 19:10:19 +0900 Subject: [PATCH 255/632] docs(browser-session): record include delimiter review repair --- .../browser-session-rust-source-indirection.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 03dc24327..2d16efd78 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -8,13 +8,14 @@ The Browser Session trusted-adapter boundary already derives one canonical Cargo Three related forms matter here: -- `include!(...)` parses another file as an expression or item. Rust documents the included path as relative to the source file containing the invocation. +- `include!(...)`, `include![...]`, and `include! {...}` parse another file as an expression or item. Rust macro invocation syntax permits all three delimiter forms, and the included path is relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html +- Rust Reference, macro invocation syntax: https://doc.rust-lang.org/reference/macros.html#macro-invocation - Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#the-path-attribute - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute @@ -29,7 +30,7 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m ## Decision -Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!`. +Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro invocation uses parentheses, brackets, or braces. Any Rust attribute containing `path =` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: @@ -43,6 +44,9 @@ Any additional path-bearing attribute must arrive in the same reviewed delta tha - `f42b1012306290311cd240671376096ca107bae2` added the first source-indirection guard consuming the canonical production-source closure. - `5068055be9acc3c8dc5ebd167a2b26099cbdacd4` tightened `#[path]` handling from path-shape heuristics to an exact current-tree allowlist, preserving the existing reviewed `originweave-core/src/root.rs -> lib.rs` exception while rejecting any new path attribute until reviewed. - `a6f6d545df4d890edebe82ddc78fe4f36a0a1cfd` added a hostile `cfg_attr(..., path = ...)` fixture and generalized discovery from only direct `#[path]` spellings to any Rust attribute carrying `path =`, preventing conditional compilation from bypassing the exact-tree review surface. +- Focused CodeRabbit review of exact `72fb8b410d05a252bda7da81a428fcdbcfa31f4e` found a valid P1: the first `include!` detector matched only the parenthesized form even though Rust macro invocations also admit bracket and brace token trees. The review confirmed the other requested source-indirection invariants were structurally correct. +- `cf9a1902d9c4c184121850ea77cf3f1ca4ce29ee` added hostile `include! {...}` and `include![...]` regressions without changing the parenthesized-only detector, preserving a structural RED for both bypasses. +- `52dae82b4d4a26ae56cb81913e6d8e1daf7a6e19` repaired the detector to recognize all three valid macro delimiter forms while keeping the same fail-closed error and canonical Cargo source closure. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. From d823d04a105fa8234077039d96cc168cf942bc8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:01:23 +0900 Subject: [PATCH 256/632] test(browser-session): expose custom-target module provenance gap --- ...ession_rust_source_indirection_contract.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index b75693b0b..254c530e9 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -101,6 +101,33 @@ def test_braced_include_macro_fails_closed(self) -> None: def test_bracketed_include_macro_fails_closed(self) -> None: self._assert_include_form_fails_closed('include!["../generated_adapter.rs"];\n') + def test_bare_module_from_custom_target_fails_closed(self) -> None: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "runtime").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[lib]\npath = "runtime/lifecycle_adapter.rs"\n', + encoding="utf-8", + ) + (adapter / "runtime/lifecycle_adapter.rs").write_text( + "mod helper;\npub fn lifecycle_adapter_surface() {}\n", + encoding="utf-8", + ) + (adapter / "runtime/helper.rs").write_text( + "pub fn helper_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + _assert_no_unmodeled_rust_source_indirection(root) + def test_new_path_attribute_fails_closed_even_when_target_is_in_tree(self) -> None: root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') (root / "adapter/src/nested.rs").write_text( From eb2ea168fd951bbc817f24cd08fb2b0b5805a775 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:01:55 +0900 Subject: [PATCH 257/632] fix(browser-session): fail closed on custom-target module indirection --- ...ession_rust_source_indirection_contract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 254c530e9..93a2ff191 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -18,6 +18,9 @@ INCLUDE_MACRO = re.compile(r"(? str: return " ".join(body.split()) +def _is_under_any_default_src(source: pathlib.Path, src_roots: list[pathlib.Path]) -> bool: + """Return whether Cargo source discovery already reviews every sibling module under this source root.""" + resolved = source.resolve() + for src_root in src_roots: + try: + resolved.relative_to(src_root) + return True + except ValueError: + continue + return False + + def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: """Fail closed when reviewed Rust source can pull unmodeled executable source bytes.""" discovered_path_attributes: set[tuple[str, str]] = set() + default_src_roots = [ + (manifest.parent / "src").resolve() + for manifest in boundary._production_package_manifests(root) + ] for source in boundary._workspace_production_sources(root): text = source.read_text(encoding="utf-8") @@ -40,6 +59,12 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: f"Rust include! source indirection requires an explicit provenance contract: {relative}" ) + if not _is_under_any_default_src(source, default_src_roots) and BARE_MODULE_ITEM.search(text): + raise AssertionError( + "Rust module source indirection from a custom Cargo target requires an explicit " + f"provenance contract: {relative}" + ) + for match in RUST_ATTRIBUTE.finditer(text): attribute_body = _normalized_attribute_body(match.group(1)) if PATH_META.search(attribute_body): @@ -128,6 +153,15 @@ def test_bare_module_from_custom_target_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): _assert_no_unmodeled_rust_source_indirection(root) + def test_bare_module_under_default_src_uses_existing_source_closure(self) -> None: + root = self._workspace_with_source("mod nested;\npub fn adapter_surface() {}\n") + (root / "adapter/src/nested.rs").write_text( + "pub fn nested_adapter_surface() {}\n", + encoding="utf-8", + ) + + _assert_no_unmodeled_rust_source_indirection(root) + def test_new_path_attribute_fails_closed_even_when_target_is_in_tree(self) -> None: root = self._workspace_with_source('#[path = "nested.rs"]\nmod nested;\n') (root / "adapter/src/nested.rs").write_text( From f28e96f5dca746adab2df3fb909bd205d13947ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:02:23 +0900 Subject: [PATCH 258/632] docs(browser-session): trace custom-target module source provenance --- .../browser-session-rust-source-indirection.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 2d16efd78..5fc02db02 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -6,26 +6,29 @@ Status: active PR evidence only. This contract does not claim protected-main or The Browser Session trusted-adapter boundary already derives one canonical Cargo production package/source closure and rejects repository-external or dangling source objects. That is not sufficient by itself because reviewed Rust source can cause `rustc` to parse additional source bytes through language-level indirection. -Three related forms matter here: +Four related forms matter here: - `include!(...)`, `include![...]`, and `include! {...}` parse another file as an expression or item. Rust macro invocation syntax permits all three delimiter forms, and the included path is relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. +- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html - Rust Reference, macro invocation syntax: https://doc.rust-lang.org/reference/macros.html#macro-invocation -- Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#the-path-attribute +- Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#module-source-filenames - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute +- Rust 2018 Edition Guide, module file layout: https://doc.rust-lang.org/edition-guide/rust-2018/path-changes.html#no-more-modrs -Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and ordinary `src/**/*.rs` entry point inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. +Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and declared custom target inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. - This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. +- Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The new bare-module guard is therefore limited to reviewed custom target roots outside every production package's default `src/` directory. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision @@ -36,7 +39,9 @@ Any Rust attribute containing `path =` is treated as source-indirection review s `crates/originweave-core/src/root.rs` → `path = "lib.rs"` -Any additional path-bearing attribute must arrive in the same reviewed delta that explains and tests its source provenance. This is intentionally stricter than accepting apparently in-tree relative strings because Rust module-path resolution is context-sensitive and conditional attributes can change the selected source by target configuration. A future filesystem indirection must not silently widen the Browser Session TCB. +A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item (`mod name;`, including ordinary `pub` visibility spellings). This is deliberately narrower than banning bare modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. + +Any additional path-bearing attribute or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. ## RED → repair evidence @@ -47,9 +52,11 @@ Any additional path-bearing attribute must arrive in the same reviewed delta tha - Focused CodeRabbit review of exact `72fb8b410d05a252bda7da81a428fcdbcfa31f4e` found a valid P1: the first `include!` detector matched only the parenthesized form even though Rust macro invocations also admit bracket and brace token trees. The review confirmed the other requested source-indirection invariants were structurally correct. - `cf9a1902d9c4c184121850ea77cf3f1ca4ce29ee` added hostile `include! {...}` and `include![...]` regressions without changing the parenthesized-only detector, preserving a structural RED for both bypasses. - `52dae82b4d4a26ae56cb81913e6d8e1daf7a6e19` repaired the detector to recognize all three valid macro delimiter forms while keeping the same fail-closed error and canonical Cargo source closure. +- `d823d04a105fa8234077039d96cc168cf942bc8d` added a hostile custom `[lib].path = "runtime/lifecycle_adapter.rs"` whose crate root declares `mod helper;` and whose sibling `runtime/helper.rs` is outside the canonical `src/**/*.rs` closure. The pre-repair contract did not reject that source expansion, preserving the structural RED. +- `eb2ea168fd951bbc817f24cd08fb2b0b5805a775` repaired the gap without widening Cargo topology ownership: the indirection contract derives production package `src/` roots from the canonical manifest closure, permits bare outlined modules only where the canonical `src/**/*.rs` closure already covers their files, and fails closed on bare outlined modules from custom target roots outside `src/`. A positive fixture keeps ordinary `src/lib.rs -> mod nested;` valid. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -If OriginWeave later needs `include!` or additional path-bearing attributes in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. +If OriginWeave later needs `include!`, additional path-bearing attributes, or custom-target outlined module trees in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. From 8d396db28df3e9757a1f3ee96eb65bca15a16e4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:05:17 +0900 Subject: [PATCH 259/632] test(browser-session): expose raw-identifier module bypass --- ...ession_rust_source_indirection_contract.py | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 93a2ff191..f6a16d844 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -104,6 +104,29 @@ def _workspace_with_source(self, source_text: str) -> pathlib.Path: (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") return root + def _custom_target_workspace(self, source_text: str, module_file: str) -> pathlib.Path: + """Create a custom-target crate whose outlined module is outside Cargo's default src tree.""" + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "runtime").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[lib]\npath = "runtime/lifecycle_adapter.rs"\n', + encoding="utf-8", + ) + (adapter / "runtime/lifecycle_adapter.rs").write_text(source_text, encoding="utf-8") + (adapter / f"runtime/{module_file}").write_text( + "pub fn helper_surface() {}\n", + encoding="utf-8", + ) + return root + def _assert_include_form_fails_closed(self, source_text: str) -> None: root = self._workspace_with_source(source_text) (root / "adapter/generated_adapter.rs").write_text( @@ -127,27 +150,18 @@ def test_bracketed_include_macro_fails_closed(self) -> None: self._assert_include_form_fails_closed('include!["../generated_adapter.rs"];\n') def test_bare_module_from_custom_target_fails_closed(self) -> None: - directory = tempfile.TemporaryDirectory() - self.addCleanup(directory.cleanup) - root = pathlib.Path(directory.name) - (root / "Cargo.toml").write_text( - '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', - encoding="utf-8", - ) - adapter = root / "adapter" - (adapter / "runtime").mkdir(parents=True) - (adapter / "Cargo.toml").write_text( - '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n' - '[lib]\npath = "runtime/lifecycle_adapter.rs"\n', - encoding="utf-8", - ) - (adapter / "runtime/lifecycle_adapter.rs").write_text( + root = self._custom_target_workspace( "mod helper;\npub fn lifecycle_adapter_surface() {}\n", - encoding="utf-8", + "helper.rs", ) - (adapter / "runtime/helper.rs").write_text( - "pub fn helper_surface() {}\n", - encoding="utf-8", + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + _assert_no_unmodeled_rust_source_indirection(root) + + def test_raw_identifier_bare_module_from_custom_target_fails_closed(self) -> None: + root = self._custom_target_workspace( + "mod r#type;\npub fn lifecycle_adapter_surface() {}\n", + "type.rs", ) with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): From aabd724d0d41a526ca41ade4e47349b94c0151f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:05:38 +0900 Subject: [PATCH 260/632] fix(browser-session): cover raw-identifier module indirection --- tests/test_browser_session_rust_source_indirection_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index f6a16d844..8eebe8266 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -19,7 +19,7 @@ RUST_ATTRIBUTE = re.compile(r"#\s*\[([^\]]*)\]", re.DOTALL) PATH_META = re.compile(r"\bpath\s*=") BARE_MODULE_ITEM = re.compile( - r"(?m)^[ \t]*(?:pub(?:\s*\([^\n)]*\))?[ \t]+)?mod[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*;" + r"(?m)^[ \t]*(?:pub(?:\s*\([^\n)]*\))?[ \t]+)?mod[ \t]+(?:r#)?[A-Za-z_][A-Za-z0-9_]*[ \t]*;" ) APPROVED_RUST_PATH_ATTRIBUTES = { ("crates/originweave-core/src/root.rs", 'path = "lib.rs"'), From 8ba03a5cf3022dbe21c9f4e1b0443e7481941661 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:06:04 +0900 Subject: [PATCH 261/632] docs(browser-session): record raw-identifier review repair --- .../browser-session-rust-source-indirection.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 5fc02db02..ab09b1216 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -11,7 +11,7 @@ Four related forms matter here: - `include!(...)`, `include![...]`, and `include! {...}` parse another file as an expression or item. Rust macro invocation syntax permits all three delimiter forms, and the included path is relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. -- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. +- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust raw identifiers are valid item identifiers, so the same guard must cover spellings such as `mod r#type;` rather than only ordinary identifier tokens. Primary references: @@ -19,6 +19,7 @@ Primary references: - Rust Reference, macro invocation syntax: https://doc.rust-lang.org/reference/macros.html#macro-invocation - Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#module-source-filenames - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute +- Rust Reference, identifiers and raw identifiers: https://doc.rust-lang.org/reference/identifiers.html - Rust 2018 Edition Guide, module file layout: https://doc.rust-lang.org/edition-guide/rust-2018/path-changes.html#no-more-modrs Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and declared custom target inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. @@ -28,7 +29,7 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. - This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. -- Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The new bare-module guard is therefore limited to reviewed custom target roots outside every production package's default `src/` directory. +- Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The bare-module guard is therefore limited to reviewed custom target roots outside every production package's default `src/` directory. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision @@ -39,7 +40,7 @@ Any Rust attribute containing `path =` is treated as source-indirection review s `crates/originweave-core/src/root.rs` → `path = "lib.rs"` -A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item (`mod name;`, including ordinary `pub` visibility spellings). This is deliberately narrower than banning bare modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. +A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item (`mod name;` or `mod r#name;`, including ordinary `pub` visibility spellings). This is deliberately narrower than banning bare modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. Any additional path-bearing attribute or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. @@ -54,6 +55,9 @@ Any additional path-bearing attribute or custom-target module source must arrive - `52dae82b4d4a26ae56cb81913e6d8e1daf7a6e19` repaired the detector to recognize all three valid macro delimiter forms while keeping the same fail-closed error and canonical Cargo source closure. - `d823d04a105fa8234077039d96cc168cf942bc8d` added a hostile custom `[lib].path = "runtime/lifecycle_adapter.rs"` whose crate root declares `mod helper;` and whose sibling `runtime/helper.rs` is outside the canonical `src/**/*.rs` closure. The pre-repair contract did not reject that source expansion, preserving the structural RED. - `eb2ea168fd951bbc817f24cd08fb2b0b5805a775` repaired the gap without widening Cargo topology ownership: the indirection contract derives production package `src/` roots from the canonical manifest closure, permits bare outlined modules only where the canonical `src/**/*.rs` closure already covers their files, and fails closed on bare outlined modules from custom target roots outside `src/`. A positive fixture keeps ordinary `src/lib.rs -> mod nested;` valid. +- Focused CodeRabbit review of exact `f28e96f5dca746adab2df3fb909bd205d13947ed` found a valid P1 in that new guard: the regex accepted only ordinary identifiers and missed valid Rust raw identifiers such as `mod r#type;`, allowing the same custom-target sibling-source bypass under a different legal spelling. +- `8d396db28df3e9757a1f3ee96eb65bca15a16e4f` added a hostile raw-identifier module fixture while preserving the ordinary-identifier-only detector, keeping that reviewer finding as a structural RED. +- `aabd724d0d41a526ca41ade4e47349b94c0151f5` repaired the detector with the minimal Rust raw-identifier prefix form `(?:r#)?` and retained the same custom-target-only scope and positive default-`src/` fixture. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. From aa90b3ef465785ab0250097dedbc917dc2ce9cc6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:08:46 +0900 Subject: [PATCH 262/632] test(browser-session): expose Unicode module identifier bypass --- ...t_browser_session_rust_source_indirection_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 8eebe8266..5420077ba 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -167,6 +167,15 @@ def test_raw_identifier_bare_module_from_custom_target_fails_closed(self) -> Non with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): _assert_no_unmodeled_rust_source_indirection(root) + def test_unicode_identifier_bare_module_from_custom_target_fails_closed(self) -> None: + root = self._custom_target_workspace( + "mod 관찰;\npub fn lifecycle_adapter_surface() {}\n", + "관찰.rs", + ) + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + _assert_no_unmodeled_rust_source_indirection(root) + def test_bare_module_under_default_src_uses_existing_source_closure(self) -> None: root = self._workspace_with_source("mod nested;\npub fn adapter_surface() {}\n") (root / "adapter/src/nested.rs").write_text( From e9dfcbefcc7d2ed022564a76edd3715f1f30d071 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:09:06 +0900 Subject: [PATCH 263/632] fix(browser-session): cover Unicode module identifiers fail closed --- tests/test_browser_session_rust_source_indirection_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 5420077ba..2a751fe27 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -19,7 +19,7 @@ RUST_ATTRIBUTE = re.compile(r"#\s*\[([^\]]*)\]", re.DOTALL) PATH_META = re.compile(r"\bpath\s*=") BARE_MODULE_ITEM = re.compile( - r"(?m)^[ \t]*(?:pub(?:\s*\([^\n)]*\))?[ \t]+)?mod[ \t]+(?:r#)?[A-Za-z_][A-Za-z0-9_]*[ \t]*;" + r"(?m)^[ \t]*(?:pub(?:\s*\([^\n)]*\))?[ \t]+)?mod[ \t]+[^\s;{}]+[ \t]*;" ) APPROVED_RUST_PATH_ATTRIBUTES = { ("crates/originweave-core/src/root.rs", 'path = "lib.rs"'), From fc0275ca9099955819777b72e73b5c25891e826a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:09:37 +0900 Subject: [PATCH 264/632] docs(browser-session): record Unicode module review repair --- .../browser-session-rust-source-indirection.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index ab09b1216..3ed5e6bd8 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -11,7 +11,7 @@ Four related forms matter here: - `include!(...)`, `include![...]`, and `include! {...}` parse another file as an expression or item. Rust macro invocation syntax permits all three delimiter forms, and the included path is relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. -- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust raw identifiers are valid item identifiers, so the same guard must cover spellings such as `mod r#type;` rather than only ordinary identifier tokens. +- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust identifiers are Unicode XID-based and raw identifiers are also valid item identifiers, so the same guard must not assume ASCII-only module names. Primary references: @@ -30,6 +30,7 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. - Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The bare-module guard is therefore limited to reviewed custom target roots outside every production package's default `src/` directory. +- The temporary custom-target guard intentionally does not attempt to reproduce the Rust identifier grammar. Once it sees an outlined module-shaped token before `;`, it fails closed; exact compiler-derived source-input provenance is the long-term replacement. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision @@ -40,7 +41,7 @@ Any Rust attribute containing `path =` is treated as source-indirection review s `crates/originweave-core/src/root.rs` → `path = "lib.rs"` -A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item (`mod name;` or `mod r#name;`, including ordinary `pub` visibility spellings). This is deliberately narrower than banning bare modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. +A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item. The detector deliberately treats the module-name token as opaque instead of restricting it to ASCII, so ordinary, raw, and Unicode Rust identifiers cannot select an unenumerated sibling by changing identifier spelling. This is narrower than banning modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. Any additional path-bearing attribute or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. @@ -57,7 +58,10 @@ Any additional path-bearing attribute or custom-target module source must arrive - `eb2ea168fd951bbc817f24cd08fb2b0b5805a775` repaired the gap without widening Cargo topology ownership: the indirection contract derives production package `src/` roots from the canonical manifest closure, permits bare outlined modules only where the canonical `src/**/*.rs` closure already covers their files, and fails closed on bare outlined modules from custom target roots outside `src/`. A positive fixture keeps ordinary `src/lib.rs -> mod nested;` valid. - Focused CodeRabbit review of exact `f28e96f5dca746adab2df3fb909bd205d13947ed` found a valid P1 in that new guard: the regex accepted only ordinary identifiers and missed valid Rust raw identifiers such as `mod r#type;`, allowing the same custom-target sibling-source bypass under a different legal spelling. - `8d396db28df3e9757a1f3ee96eb65bca15a16e4f` added a hostile raw-identifier module fixture while preserving the ordinary-identifier-only detector, keeping that reviewer finding as a structural RED. -- `aabd724d0d41a526ca41ade4e47349b94c0151f5` repaired the detector with the minimal Rust raw-identifier prefix form `(?:r#)?` and retained the same custom-target-only scope and positive default-`src/` fixture. +- `aabd724d0d41a526ca41ade4e47349b94c0151f5` repaired the reported raw-identifier case while retaining the same custom-target-only scope and positive default-`src/` fixture. +- Focused CodeRabbit review of exact `8ba03a5cf3022dbe21c9f4e1b0443e7481941661` then found the same grammar-assumption class was still incomplete: the detector remained ASCII-only even though Rust permits Unicode identifiers. +- `aa90b3ef465785ab0250097dedbc917dc2ce9cc6` added a hostile Unicode module fixture (`mod 관찰;`) while leaving the ASCII-only detector unchanged, preserving that reviewer finding as a structural RED. +- `e9dfcbefcc7d2ed022564a76edd3715f1f30d071` removed the ASCII identifier assumption. The custom-target guard now treats the module-name token opaquely and fails closed before `;`, covering ordinary, raw, and Unicode identifier spellings without reimplementing Rust XID tables or widening Cargo topology ownership. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. From 130e9512d8a96db782de0a7b98e490b6db17a3c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:12:37 +0900 Subject: [PATCH 265/632] test(browser-session): expose module trivia bypass --- ...t_browser_session_rust_source_indirection_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 2a751fe27..69f184947 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -176,6 +176,15 @@ def test_unicode_identifier_bare_module_from_custom_target_fails_closed(self) -> with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): _assert_no_unmodeled_rust_source_indirection(root) + def test_comment_trivia_after_mod_from_custom_target_fails_closed(self) -> None: + root = self._custom_target_workspace( + "mod /* reviewed trivia */ helper;\npub fn lifecycle_adapter_surface() {}\n", + "helper.rs", + ) + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + _assert_no_unmodeled_rust_source_indirection(root) + def test_bare_module_under_default_src_uses_existing_source_closure(self) -> None: root = self._workspace_with_source("mod nested;\npub fn adapter_surface() {}\n") (root / "adapter/src/nested.rs").write_text( From bb83cf9571c2091bbb088176a9881de5fb46739b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:13:06 +0900 Subject: [PATCH 266/632] fix(browser-session): fail closed on custom-target mod token --- ...browser_session_rust_source_indirection_contract.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 69f184947..4fed5e5e7 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -18,9 +18,7 @@ INCLUDE_MACRO = re.compile(r"(? None: f"Rust include! source indirection requires an explicit provenance contract: {relative}" ) - if not _is_under_any_default_src(source, default_src_roots) and BARE_MODULE_ITEM.search(text): + # A custom target root is outside the canonical src/**/*.rs sibling closure. Until + # compiler-derived source inputs replace this guard, any lexical `mod` token is an + # intentionally conservative provenance stop: comments/trivia, raw/Unicode names, + # visibility spellings, and inline-vs-outlined grammar must not create bypasses. + if not _is_under_any_default_src(source, default_src_roots) and CUSTOM_TARGET_MOD_TOKEN.search(text): raise AssertionError( "Rust module source indirection from a custom Cargo target requires an explicit " f"provenance contract: {relative}" From 240e43479c450e5dc8afa745faf1267ffe31cad7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 20:13:32 +0900 Subject: [PATCH 267/632] docs(browser-session): record Rust trivia review repair --- ...browser-session-rust-source-indirection.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 3ed5e6bd8..d64d1920f 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -11,7 +11,7 @@ Four related forms matter here: - `include!(...)`, `include![...]`, and `include! {...}` parse another file as an expression or item. Rust macro invocation syntax permits all three delimiter forms, and the included path is relative to the source file containing the invocation. - `#[path = "..."] mod ...;` changes the source file used for an outlined module. Rust documents the path attribute as a module-source filename override whose relative interpretation depends on the module location. - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. -- A bare outlined module item such as `mod helper;` also causes the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains those sibling/default module files conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust identifiers are Unicode XID-based and raw identifiers are also valid item identifiers, so the same guard must not assume ASCII-only module names. +- A `mod` item can cause the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains default module trees conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust permits comments and other trivia between grammar tokens, Unicode XID identifiers, and raw identifiers, so a security contract must not encode a narrower hand-written identifier/module grammar and call it complete. Primary references: @@ -20,6 +20,7 @@ Primary references: - Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#module-source-filenames - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute - Rust Reference, identifiers and raw identifiers: https://doc.rust-lang.org/reference/identifiers.html +- Rust Reference, comments: https://doc.rust-lang.org/reference/comments.html - Rust 2018 Edition Guide, module file layout: https://doc.rust-lang.org/edition-guide/rust-2018/path-changes.html#no-more-modrs Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and declared custom target inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. @@ -29,8 +30,8 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. - This contract consumes that closure; it does not reimplement Cargo workspace, dependency, target, build-script, or source-override discovery. - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. -- Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The bare-module guard is therefore limited to reviewed custom target roots outside every production package's default `src/` directory. -- The temporary custom-target guard intentionally does not attempt to reproduce the Rust identifier grammar. Once it sees an outlined module-shaped token before `;`, it fails closed; exact compiler-derived source-input provenance is the long-term replacement. +- Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The custom-target module guard is therefore limited to reviewed production sources outside every production package's default `src/` directory. +- The temporary custom-target guard intentionally over-approximates rather than reproducing Rust's module grammar. Any lexical `mod` token in a custom target root outside default `src/` fails closed. That may reject a harmless inline module or textual occurrence, but it removes identifier/trivia/visibility grammar gaps until compiler-derived source-input provenance replaces the heuristic. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision @@ -41,7 +42,7 @@ Any Rust attribute containing `path =` is treated as source-indirection review s `crates/originweave-core/src/root.rs` → `path = "lib.rs"` -A reviewed custom Cargo target outside a production package's default `src/` tree also fails closed if it contains a bare outlined module item. The detector deliberately treats the module-name token as opaque instead of restricting it to ASCII, so ordinary, raw, and Unicode Rust identifiers cannot select an unenumerated sibling by changing identifier spelling. This is narrower than banning modules globally: ordinary `src/` module files are already included by the canonical Cargo source closure, whereas a custom target root can otherwise cause `rustc` to read an unenumerated sibling module. Inline modules (`mod name { ... }`) do not add source bytes and are not part of this guard. +For a reviewed custom Cargo target outside a production package's default `src/` tree, the temporary contract no longer tries to prove that a particular `mod` spelling is outlined rather than inline. Any lexical `mod` token is a provenance stop. This deliberately conservative rule closes changes in identifier spelling and Rust trivia such as `mod r#type;`, `mod 관찰;`, or `mod /* comment */ helper;` without taking ownership of Cargo topology or pretending a regex implements the Rust parser. Ordinary `src/` module trees remain outside this guard because their sibling files are already enumerated by the canonical source closure. Any additional path-bearing attribute or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. @@ -51,20 +52,21 @@ Any additional path-bearing attribute or custom-target module source must arrive - `f42b1012306290311cd240671376096ca107bae2` added the first source-indirection guard consuming the canonical production-source closure. - `5068055be9acc3c8dc5ebd167a2b26099cbdacd4` tightened `#[path]` handling from path-shape heuristics to an exact current-tree allowlist, preserving the existing reviewed `originweave-core/src/root.rs -> lib.rs` exception while rejecting any new path attribute until reviewed. - `a6f6d545df4d890edebe82ddc78fe4f36a0a1cfd` added a hostile `cfg_attr(..., path = ...)` fixture and generalized discovery from only direct `#[path]` spellings to any Rust attribute carrying `path =`, preventing conditional compilation from bypassing the exact-tree review surface. -- Focused CodeRabbit review of exact `72fb8b410d05a252bda7da81a428fcdbcfa31f4e` found a valid P1: the first `include!` detector matched only the parenthesized form even though Rust macro invocations also admit bracket and brace token trees. The review confirmed the other requested source-indirection invariants were structurally correct. +- Focused CodeRabbit review of exact `72fb8b410d05a252bda7da81a428fcdbcfa31f4e` found a valid P1: the first `include!` detector matched only the parenthesized form even though Rust macro invocations also admit bracket and brace token trees. - `cf9a1902d9c4c184121850ea77cf3f1ca4ce29ee` added hostile `include! {...}` and `include![...]` regressions without changing the parenthesized-only detector, preserving a structural RED for both bypasses. - `52dae82b4d4a26ae56cb81913e6d8e1daf7a6e19` repaired the detector to recognize all three valid macro delimiter forms while keeping the same fail-closed error and canonical Cargo source closure. - `d823d04a105fa8234077039d96cc168cf942bc8d` added a hostile custom `[lib].path = "runtime/lifecycle_adapter.rs"` whose crate root declares `mod helper;` and whose sibling `runtime/helper.rs` is outside the canonical `src/**/*.rs` closure. The pre-repair contract did not reject that source expansion, preserving the structural RED. -- `eb2ea168fd951bbc817f24cd08fb2b0b5805a775` repaired the gap without widening Cargo topology ownership: the indirection contract derives production package `src/` roots from the canonical manifest closure, permits bare outlined modules only where the canonical `src/**/*.rs` closure already covers their files, and fails closed on bare outlined modules from custom target roots outside `src/`. A positive fixture keeps ordinary `src/lib.rs -> mod nested;` valid. -- Focused CodeRabbit review of exact `f28e96f5dca746adab2df3fb909bd205d13947ed` found a valid P1 in that new guard: the regex accepted only ordinary identifiers and missed valid Rust raw identifiers such as `mod r#type;`, allowing the same custom-target sibling-source bypass under a different legal spelling. -- `8d396db28df3e9757a1f3ee96eb65bca15a16e4f` added a hostile raw-identifier module fixture while preserving the ordinary-identifier-only detector, keeping that reviewer finding as a structural RED. -- `aabd724d0d41a526ca41ade4e47349b94c0151f5` repaired the reported raw-identifier case while retaining the same custom-target-only scope and positive default-`src/` fixture. -- Focused CodeRabbit review of exact `8ba03a5cf3022dbe21c9f4e1b0443e7481941661` then found the same grammar-assumption class was still incomplete: the detector remained ASCII-only even though Rust permits Unicode identifiers. -- `aa90b3ef465785ab0250097dedbc917dc2ce9cc6` added a hostile Unicode module fixture (`mod 관찰;`) while leaving the ASCII-only detector unchanged, preserving that reviewer finding as a structural RED. -- `e9dfcbefcc7d2ed022564a76edd3715f1f30d071` removed the ASCII identifier assumption. The custom-target guard now treats the module-name token opaquely and fails closed before `;`, covering ordinary, raw, and Unicode identifier spellings without reimplementing Rust XID tables or widening Cargo topology ownership. +- `eb2ea168fd951bbc817f24cd08fb2b0b5805a775` added the first custom-target module guard while preserving the positive ordinary `src/lib.rs -> mod nested;` fixture. +- Focused CodeRabbit review of exact `f28e96f5dca746adab2df3fb909bd205d13947ed` found a valid P1: the first guard missed raw identifiers such as `mod r#type;`. +- `8d396db28df3e9757a1f3ee96eb65bca15a16e4f` preserved that raw-identifier finding as a hostile structural RED, and `aabd724d0d41a526ca41ade4e47349b94c0151f5` repaired the reported raw spelling. +- Focused CodeRabbit review of exact `8ba03a5cf3022dbe21c9f4e1b0443e7481941661` found the same hand-written grammar was still ASCII-only even though Rust identifiers can be Unicode. +- `aa90b3ef465785ab0250097dedbc917dc2ce9cc6` preserved a hostile Unicode module RED (`mod 관찰;`), and `e9dfcbefcc7d2ed022564a76edd3715f1f30d071` removed the ASCII identifier assumption. +- Focused CodeRabbit review of exact `fc0275ca9099955819777b72e73b5c25891e826a` found one remaining P1 in the same grammar-emulation approach: valid Rust trivia can occur between `mod` and its module-name token, so a detector that requires direct horizontal whitespace before an identifier remains bypassable. +- `130e9512d8a96db782de0a7b98e490b6db17a3c6` preserves that finding as a structural RED with `mod /* reviewed trivia */ helper;` while leaving the prior detector unchanged. +- `bb83cf9571c2091bbb088176a9881de5fb46739b` removes the fragile hand-written module-name grammar. For custom target roots outside default `src/`, the temporary gate now fails closed on any lexical `mod` token. This is intentionally conservative and temporary; it closes trivia/raw/Unicode spelling classes without taking ownership of the Rust parser or Cargo topology. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -If OriginWeave later needs `include!`, additional path-bearing attributes, or custom-target outlined module trees in production code, replace the temporary fail-closed policy with compiler-derived or equivalently exact source-input provenance. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, and arrive with hostile fixtures before any allowlist widening. +Replace the temporary custom-target lexical stop with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. From dce0c96fb8a05cce5605ecf42df858c16276099d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 21:04:11 +0900 Subject: [PATCH 268/632] test(browser-session): preserve Rust path-comment provenance RED --- ...ssion_rust_path_comment_trivia_contract.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/test_browser_session_rust_path_comment_trivia_contract.py diff --git a/tests/test_browser_session_rust_path_comment_trivia_contract.py b/tests/test_browser_session_rust_path_comment_trivia_contract.py new file mode 100644 index 000000000..4306ba3d6 --- /dev/null +++ b/tests/test_browser_session_rust_path_comment_trivia_contract.py @@ -0,0 +1,51 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustPathCommentTriviaContractTests(unittest.TestCase): + """Prove Rust comment trivia cannot hide a path-bearing source attribute.""" + + def test_block_comment_between_path_and_equals_fails_closed(self) -> None: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text( + '#[path /* reviewed trivia */ = "nested.rs"]\nmod nested;\n', + encoding="utf-8", + ) + (adapter / "src/nested.rs").write_text( + "pub fn nested_adapter_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From 781f3b1db14bf7591091cb7be5bb552e6e5497b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 21:04:43 +0900 Subject: [PATCH 269/632] test(browser-session): cover Rust attribute trivia edge cases --- ...ssion_rust_path_comment_trivia_contract.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_rust_path_comment_trivia_contract.py b/tests/test_browser_session_rust_path_comment_trivia_contract.py index 4306ba3d6..3a5c54f4c 100644 --- a/tests/test_browser_session_rust_path_comment_trivia_contract.py +++ b/tests/test_browser_session_rust_path_comment_trivia_contract.py @@ -20,7 +20,7 @@ class BrowserSessionRustPathCommentTriviaContractTests(unittest.TestCase): """Prove Rust comment trivia cannot hide a path-bearing source attribute.""" - def test_block_comment_between_path_and_equals_fails_closed(self) -> None: + def _workspace_with_source(self, source_text: str) -> pathlib.Path: directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) root = pathlib.Path(directory.name) @@ -34,18 +34,38 @@ def test_block_comment_between_path_and_equals_fails_closed(self) -> None: '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', encoding="utf-8", ) - (adapter / "src/lib.rs").write_text( - '#[path /* reviewed trivia */ = "nested.rs"]\nmod nested;\n', - encoding="utf-8", - ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") (adapter / "src/nested.rs").write_text( "pub fn nested_adapter_surface() {}\n", encoding="utf-8", ) + return root + def _assert_path_attribute_fails_closed(self, source_text: str) -> None: + root = self._workspace_with_source(source_text) with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_block_comment_between_path_and_equals_fails_closed(self) -> None: + self._assert_path_attribute_fails_closed( + '#[path /* reviewed trivia */ = "nested.rs"]\nmod nested;\n' + ) + + def test_nested_block_comment_between_path_and_equals_fails_closed(self) -> None: + self._assert_path_attribute_fails_closed( + '#[path /* outer /* nested */ trivia */ = "nested.rs"]\nmod nested;\n' + ) + + def test_line_comment_between_path_and_equals_fails_closed(self) -> None: + self._assert_path_attribute_fails_closed( + '#[path // reviewed trivia\n = "nested.rs"]\nmod nested;\n' + ) + + def test_closing_bracket_inside_comment_cannot_truncate_attribute_scan(self) -> None: + self._assert_path_attribute_fails_closed( + '#[path /* ] reviewed trivia */ = "nested.rs"]\nmod nested;\n' + ) + if __name__ == "__main__": unittest.main() From 04e08a48fb7572860b0de564bdbf615ad2da5186 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 21:05:36 +0900 Subject: [PATCH 270/632] fix(browser-session): honor Rust trivia in path provenance --- ...ession_rust_source_indirection_contract.py | 161 +++++++++++++++++- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 4fed5e5e7..f9ae2ef9b 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -16,8 +16,7 @@ INCLUDE_MACRO = re.compile(r"(? str: return " ".join(body.split()) +def _skip_rust_trivia(text: str, offset: int) -> int: + """Skip Rust whitespace and nested non-doc comments without changing token meaning.""" + index = offset + while index < len(text): + if text[index].isspace(): + index += 1 + continue + if text.startswith("//", index): + newline = text.find("\n", index + 2) + index = len(text) if newline < 0 else newline + 1 + continue + if text.startswith("/*", index): + depth = 1 + index += 2 + while index < len(text) and depth: + if text.startswith("/*", index): + depth += 1 + index += 2 + elif text.startswith("*/", index): + depth -= 1 + index += 2 + else: + index += 1 + if depth: + raise AssertionError("unterminated Rust block comment in source attribute") + continue + break + return index + + +def _raw_string_end(text: str, offset: int) -> int | None: + """Return the end of a Rust raw string token beginning at offset, if present.""" + cursor = offset + if text.startswith(("br", "cr"), cursor): + cursor += 2 + elif cursor < len(text) and text[cursor] == "r": + cursor += 1 + else: + return None + + hashes_start = cursor + while cursor < len(text) and text[cursor] == "#": + cursor += 1 + if cursor >= len(text) or text[cursor] != '"': + return None + + hashes = text[hashes_start:cursor] + closing = '"' + hashes + end = text.find(closing, cursor + 1) + if end < 0: + raise AssertionError("unterminated Rust raw string in source attribute") + return end + len(closing) + + +def _quoted_string_end(text: str, offset: int) -> int: + """Return the end of a conventional Rust string token beginning with a quote.""" + index = offset + 1 + escaped = False + while index < len(text): + char = text[index] + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == '"': + return index + 1 + index += 1 + raise AssertionError("unterminated Rust string in source attribute") + + +def _simple_char_literal_end(text: str, offset: int) -> int | None: + """Skip a simple Rust character literal while leaving lifetimes untouched.""" + if offset + 2 < len(text) and text[offset + 2] == "'": + return offset + 3 + if offset + 1 >= len(text) or text[offset + 1] != "\\": + return None + + index = offset + 2 + while index < len(text): + if text[index] == "'": + return index + 1 + if text[index] == "\n": + return None + index += 1 + return None + + +def _rust_attribute_bodies(text: str) -> list[str]: + """Extract balanced Rust attribute token trees while respecting lexical trivia and literals.""" + bodies: list[str] = [] + search_from = 0 + while True: + marker = text.find("#", search_from) + if marker < 0: + break + + cursor = _skip_rust_trivia(text, marker + 1) + if cursor < len(text) and text[cursor] == "!": + cursor = _skip_rust_trivia(text, cursor + 1) + if cursor >= len(text) or text[cursor] != "[": + search_from = marker + 1 + continue + + body_start = cursor + 1 + depth = 1 + cursor = body_start + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = _raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = _quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = _simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + if text[cursor] == "[": + depth += 1 + elif text[cursor] == "]": + depth -= 1 + if depth == 0: + bodies.append(text[body_start:cursor]) + search_from = cursor + 1 + break + cursor += 1 + else: + raise AssertionError("unterminated Rust attribute in production source") + + return bodies + + +def _has_path_meta(attribute_body: str) -> bool: + """Return whether an attribute contains a path meta item followed by Rust trivia and '='.""" + for match in PATH_TOKEN.finditer(attribute_body): + cursor = _skip_rust_trivia(attribute_body, match.end()) + if cursor < len(attribute_body) and attribute_body[cursor] == "=": + return True + return False + + def _is_under_any_default_src(source: pathlib.Path, src_roots: list[pathlib.Path]) -> bool: """Return whether Cargo source discovery already reviews every sibling module under this source root.""" resolved = source.resolve() @@ -67,10 +215,11 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: f"provenance contract: {relative}" ) - for match in RUST_ATTRIBUTE.finditer(text): - attribute_body = _normalized_attribute_body(match.group(1)) - if PATH_META.search(attribute_body): - discovered_path_attributes.add((relative, attribute_body)) + for attribute_body in _rust_attribute_bodies(text): + if _has_path_meta(attribute_body): + discovered_path_attributes.add( + (relative, _normalized_attribute_body(attribute_body)) + ) unexpected = discovered_path_attributes - APPROVED_RUST_PATH_ATTRIBUTES if unexpected: From 2d6e4936eb54a03a09456fdb89b5c3dc926651a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 21:06:40 +0900 Subject: [PATCH 271/632] docs(browser-session): trace Rust attribute-trivia provenance repair --- .../browser-session-rust-source-indirection.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index d64d1920f..1311618f8 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -13,6 +13,8 @@ Four related forms matter here: - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. - A `mod` item can cause the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains default module trees conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust permits comments and other trivia between grammar tokens, Unicode XID identifiers, and raw identifiers, so a security contract must not encode a narrower hand-written identifier/module grammar and call it complete. +Rust comments are also lexical trivia rather than `\s`-only whitespace. A valid attribute can therefore spell the source-loading meta item as `#[path /* reviewed trivia */ = "nested.rs"]`, including nested block comments or line-comment trivia. A regular expression that searches only for `path\s*=` silently narrows Rust's lexical grammar. Likewise, a naive `[^\]]*` attribute capture can be truncated by `]` inside a comment or string even though that byte is not the attribute's closing delimiter. + Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html @@ -21,6 +23,7 @@ Primary references: - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute - Rust Reference, identifiers and raw identifiers: https://doc.rust-lang.org/reference/identifiers.html - Rust Reference, comments: https://doc.rust-lang.org/reference/comments.html +- Rust Reference, attributes: https://doc.rust-lang.org/reference/attributes.html - Rust 2018 Edition Guide, module file layout: https://doc.rust-lang.org/edition-guide/rust-2018/path-changes.html#no-more-modrs Without an explicit contract, a future lifecycle adapter could keep its crate, manifest, and declared custom target inside the exact Git review root while compiling additional Rust source not represented by the canonical production-source closure. That would weaken the same provenance boundary used for `DisposableContextPort`, `bind_lifecycle_port`, dependency, and source-containment review. @@ -32,13 +35,14 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. - Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The custom-target module guard is therefore limited to reviewed production sources outside every production package's default `src/` directory. - The temporary custom-target guard intentionally over-approximates rather than reproducing Rust's module grammar. Any lexical `mod` token in a custom target root outside default `src/` fails closed. That may reject a harmless inline module or textual occurrence, but it removes identifier/trivia/visibility grammar gaps until compiler-derived source-input provenance replaces the heuristic. +- Rust attribute review must preserve lexical token boundaries. Non-doc line/block comments, including nested block comments, are treated as trivia; comment/string contents do not terminate the surrounding attribute token tree. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro invocation uses parentheses, brackets, or braces. -Any Rust attribute containing `path =` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: +Any Rust attribute containing a `path` meta item followed by valid Rust trivia and `=` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. Attribute bodies are extracted as balanced bracket token trees while skipping Rust whitespace, line comments, nested block comments, normal/raw string literals, and simple character literals for delimiter purposes. This scanner is deliberately limited to locating reviewed path-bearing attribute surfaces; it does not claim to parse Rust modules, name resolution, or macro expansion. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: `crates/originweave-core/src/root.rs` → `path = "lib.rs"` @@ -64,9 +68,12 @@ Any additional path-bearing attribute or custom-target module source must arrive - Focused CodeRabbit review of exact `fc0275ca9099955819777b72e73b5c25891e826a` found one remaining P1 in the same grammar-emulation approach: valid Rust trivia can occur between `mod` and its module-name token, so a detector that requires direct horizontal whitespace before an identifier remains bypassable. - `130e9512d8a96db782de0a7b98e490b6db17a3c6` preserves that finding as a structural RED with `mod /* reviewed trivia */ helper;` while leaving the prior detector unchanged. - `bb83cf9571c2091bbb088176a9881de5fb46739b` removes the fragile hand-written module-name grammar. For custom target roots outside default `src/`, the temporary gate now fails closed on any lexical `mod` token. This is intentionally conservative and temporary; it closes trivia/raw/Unicode spelling classes without taking ownership of the Rust parser or Cargo topology. +- `dce0c96fb8a05cce5605ecf42df858c16276099d` preserved a new structural RED showing that Rust block-comment trivia between `path` and `=` bypasses the prior `path\s*=` detector. +- `781f3b1db14bf7591091cb7be5bb552e6e5497b1` broadened that RED to nested block comments, line-comment trivia, and a closing bracket inside a comment, demonstrating that both the path-meta regex and the `[^\]]*` attribute-body regex were narrower than Rust lexical rules. +- `04e08a48fb7572860b0de564bdbf615ad2da5186` repaired the attribute review surface without changing Cargo topology: balanced attribute token trees now ignore comment/string delimiters correctly, and `path` followed by Rust trivia and `=` enters the exact-tree allowlist review surface. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -Replace the temporary custom-target lexical stop with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. +Replace the temporary custom-target lexical stop and hand-maintained attribute-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. From 23b7b241c3e9389a43a359a14c4dfa74036d8829 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 22:04:04 +0900 Subject: [PATCH 272/632] test(browser-session): preserve include comment-trivia provenance RED --- ...session_include_comment_trivia_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_include_comment_trivia_contract.py diff --git a/tests/test_browser_session_include_comment_trivia_contract.py b/tests/test_browser_session_include_comment_trivia_contract.py new file mode 100644 index 000000000..db19547a7 --- /dev/null +++ b/tests/test_browser_session_include_comment_trivia_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_CONTRACT = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_rust_source_indirection", SOURCE_CONTRACT) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_contract = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_contract) + + +class BrowserSessionIncludeCommentTriviaContractTests(unittest.TestCase): + """Keep Rust comment trivia from bypassing include! source-provenance review.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + (adapter / "generated_adapter.rs").write_text( + "pub fn generated_adapter_surface() {}\n", + encoding="utf-8", + ) + return root + + def _assert_include_trivia_fails_closed(self, source_text: str) -> None: + root = self._workspace_with_source(source_text) + with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): + source_contract._assert_no_unmodeled_rust_source_indirection(root) + + def test_block_comment_between_include_and_bang_fails_closed(self) -> None: + self._assert_include_trivia_fails_closed( + 'include /* provenance gap */ ! ("../generated_adapter.rs");\n' + ) + + def test_block_comment_between_bang_and_delimiter_fails_closed(self) -> None: + self._assert_include_trivia_fails_closed( + 'include! /* provenance gap */ ("../generated_adapter.rs");\n' + ) + + def test_line_comment_between_include_and_bang_fails_closed(self) -> None: + self._assert_include_trivia_fails_closed( + 'include // provenance gap\n! ("../generated_adapter.rs");\n' + ) + + +if __name__ == "__main__": + unittest.main() From bf132fb7b2c2d1a2fdae312962e2f0e0380fc1a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 22:05:25 +0900 Subject: [PATCH 273/632] fix(browser-session): tokenize include macro trivia before provenance check --- ...r_session_rust_source_indirection_contract.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index f9ae2ef9b..90649fb84 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -15,7 +15,7 @@ spec.loader.exec_module(boundary) -INCLUDE_MACRO = re.compile(r"(? int: return index +def _has_include_macro(text: str) -> bool: + """Detect include! macro syntax while honoring Rust whitespace/comment trivia around punctuation.""" + for match in INCLUDE_TOKEN.finditer(text): + bang = _skip_rust_trivia(text, match.end()) + if bang >= len(text) or text[bang] != "!": + continue + delimiter = _skip_rust_trivia(text, bang + 1) + if delimiter < len(text) and text[delimiter] in "([{": + return True + return False + + def _raw_string_end(text: str, offset: int) -> int | None: """Return the end of a Rust raw string token beginning at offset, if present.""" cursor = offset @@ -200,7 +212,7 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: text = source.read_text(encoding="utf-8") relative = source.relative_to(root).as_posix() - if INCLUDE_MACRO.search(text): + if _has_include_macro(text): raise AssertionError( f"Rust include! source indirection requires an explicit provenance contract: {relative}" ) From 3af345a995978cffa685c9c3c59a0fa2ff287ca5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 22:06:22 +0900 Subject: [PATCH 274/632] docs(browser-session): trace include comment-trivia provenance repair --- .../browser-session-rust-source-indirection.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 1311618f8..31b1c927b 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -13,7 +13,7 @@ Four related forms matter here: - `#[cfg_attr(..., path = "...")]` can conditionally synthesize the same `path` attribute. A direct `#[path]`-only lexical check therefore does not cover the full Rust attribute surface. - A `mod` item can cause the compiler to load another Rust file. Cargo's ordinary `src/**/*.rs` review already contains default module trees conservatively, but a custom Cargo target whose crate root lives outside `src/` can load sibling module files that are not in that closure. Rust permits comments and other trivia between grammar tokens, Unicode XID identifiers, and raw identifiers, so a security contract must not encode a narrower hand-written identifier/module grammar and call it complete. -Rust comments are also lexical trivia rather than `\s`-only whitespace. A valid attribute can therefore spell the source-loading meta item as `#[path /* reviewed trivia */ = "nested.rs"]`, including nested block comments or line-comment trivia. A regular expression that searches only for `path\s*=` silently narrows Rust's lexical grammar. Likewise, a naive `[^\]]*` attribute capture can be truncated by `]` inside a comment or string even though that byte is not the attribute's closing delimiter. +Rust comments are lexical trivia rather than `\s`-only whitespace. This matters for both attributes and macro invocation punctuation. A valid source-loading attribute can spell its meta item as `#[path /* reviewed trivia */ = "nested.rs"]`, and a valid macro invocation can separate `include`, `!`, and its delimiter with non-doc comment trivia. A detector that requires `include\s*!\s*(` or `path\s*=` therefore recognizes a grammar narrower than Rust's. Likewise, a naive `[^\]]*` attribute capture can be truncated by `]` inside a comment or string even though that byte is not the attribute's closing delimiter. Primary references: @@ -35,12 +35,12 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - Existing `crates/originweave-core/src/root.rs` intentionally uses `#[path = "lib.rs"]`. That exact source/attribute pair is the only currently reviewed path-attribute exception. - Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The custom-target module guard is therefore limited to reviewed production sources outside every production package's default `src/` directory. - The temporary custom-target guard intentionally over-approximates rather than reproducing Rust's module grammar. Any lexical `mod` token in a custom target root outside default `src/` fails closed. That may reject a harmless inline module or textual occurrence, but it removes identifier/trivia/visibility grammar gaps until compiler-derived source-input provenance replaces the heuristic. -- Rust attribute review must preserve lexical token boundaries. Non-doc line/block comments, including nested block comments, are treated as trivia; comment/string contents do not terminate the surrounding attribute token tree. +- Rust attribute and `include!` review must preserve lexical token boundaries. Non-doc line/block comments, including nested block comments, are treated as trivia between Rust tokens; comment/string contents do not terminate the surrounding attribute token tree. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision -Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro invocation uses parentheses, brackets, or braces. +Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro invocation uses parentheses, brackets, or braces and regardless of Rust whitespace/comment trivia between the `include` token, `!`, and the opening delimiter. The detector locates the `include` token and advances through the same nested non-doc-comment trivia skipper already used by the source-indirection contract before validating `!` and one of the three macro delimiters. This deliberately avoids encoding comment trivia as `\s`. Any Rust attribute containing a `path` meta item followed by valid Rust trivia and `=` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. Attribute bodies are extracted as balanced bracket token trees while skipping Rust whitespace, line comments, nested block comments, normal/raw string literals, and simple character literals for delimiter purposes. This scanner is deliberately limited to locating reviewed path-bearing attribute surfaces; it does not claim to parse Rust modules, name resolution, or macro expansion. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: @@ -48,7 +48,7 @@ Any Rust attribute containing a `path` meta item followed by valid Rust trivia a For a reviewed custom Cargo target outside a production package's default `src/` tree, the temporary contract no longer tries to prove that a particular `mod` spelling is outlined rather than inline. Any lexical `mod` token is a provenance stop. This deliberately conservative rule closes changes in identifier spelling and Rust trivia such as `mod r#type;`, `mod 관찰;`, or `mod /* comment */ helper;` without taking ownership of Cargo topology or pretending a regex implements the Rust parser. Ordinary `src/` module trees remain outside this guard because their sibling files are already enumerated by the canonical source closure. -Any additional path-bearing attribute or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. +Any additional path-bearing attribute, `include!` form outside the current fail-closed policy, or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. ## RED → repair evidence @@ -71,9 +71,11 @@ Any additional path-bearing attribute or custom-target module source must arrive - `dce0c96fb8a05cce5605ecf42df858c16276099d` preserved a new structural RED showing that Rust block-comment trivia between `path` and `=` bypasses the prior `path\s*=` detector. - `781f3b1db14bf7591091cb7be5bb552e6e5497b1` broadened that RED to nested block comments, line-comment trivia, and a closing bracket inside a comment, demonstrating that both the path-meta regex and the `[^\]]*` attribute-body regex were narrower than Rust lexical rules. - `04e08a48fb7572860b0de564bdbf615ad2da5186` repaired the attribute review surface without changing Cargo topology: balanced attribute token trees now ignore comment/string delimiters correctly, and `path` followed by Rust trivia and `=` enters the exact-tree allowlist review surface. +- `23b7b241c3e9389a43a359a14c4dfa74036d8829` preserved a new structural RED for macro-invocation trivia: block comments between `include` and `!`, block comments between `!` and the opening delimiter, and line-comment trivia between `include` and `!` all bypass the predecessor `include\s*!\s*[([{]` detector even though Rust treats non-doc comments as whitespace between grammar tokens. +- `bf132fb7b2c2d1a2fdae312962e2f0e0380fc1a5` repaired that gap by replacing the whitespace-only macro regex with token-plus-trivia recognition that reuses the existing nested Rust comment skipper before `!` and before the opening delimiter. The Cargo source closure and fail-closed policy are unchanged. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -Replace the temporary custom-target lexical stop and hand-maintained attribute-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. +Replace the temporary custom-target lexical stop and hand-maintained attribute/macro-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. From 783bbc614d08c3a9299849524e3dbda8545ab29f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 23:03:48 +0900 Subject: [PATCH 275/632] test(browser-session): preserve raw-identifier include provenance RED --- ...session_raw_identifier_include_contract.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/test_browser_session_raw_identifier_include_contract.py diff --git a/tests/test_browser_session_raw_identifier_include_contract.py b/tests/test_browser_session_raw_identifier_include_contract.py new file mode 100644 index 000000000..23935ff93 --- /dev/null +++ b/tests/test_browser_session_raw_identifier_include_contract.py @@ -0,0 +1,32 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRawIdentifierIncludeContractTests(unittest.TestCase): + """Keep raw-identifier macro spelling inside the include! provenance stop.""" + + def test_raw_identifier_include_macro_fails_closed(self) -> None: + self.assertTrue( + source_indirection._has_include_macro( + 'r#include!("../generated_adapter.rs");\n' + ), + "Rust raw identifiers preserve the underlying macro identifier and must not bypass include! provenance", + ) + + +if __name__ == "__main__": + unittest.main() From 5c3b86091199f436374111d6a4056d47c13c1448 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 23:04:27 +0900 Subject: [PATCH 276/632] fix(browser-session): cover raw-identifier include provenance --- .../test_browser_session_rust_source_indirection_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 90649fb84..35787f696 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -15,7 +15,7 @@ spec.loader.exec_module(boundary) -INCLUDE_TOKEN = re.compile(r"(? None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From b7c59f9ea03720d341e76088794ee9f0739af678 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 23:06:04 +0900 Subject: [PATCH 277/632] docs(browser-session): trace raw-identifier include provenance --- .../browser-session-rust-source-indirection.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index 31b1c927b..c5619de2c 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -15,6 +15,8 @@ Four related forms matter here: Rust comments are lexical trivia rather than `\s`-only whitespace. This matters for both attributes and macro invocation punctuation. A valid source-loading attribute can spell its meta item as `#[path /* reviewed trivia */ = "nested.rs"]`, and a valid macro invocation can separate `include`, `!`, and its delimiter with non-doc comment trivia. A detector that requires `include\s*!\s*(` or `path\s*=` therefore recognizes a grammar narrower than Rust's. Likewise, a naive `[^\]]*` attribute capture can be truncated by `]` inside a comment or string even though that byte is not the attribute's closing delimiter. +Raw identifiers are part of the same source-provenance surface. The Rust Reference defines a raw identifier as `r#` plus an identifier/keyword and states that the `r#` prefix is not part of the actual identifier. Macro invocations resolve a `SimplePath`, whose path segment admits an `IDENTIFIER`, so `r#include!(...)` names the same underlying `include` identifier through raw spelling. A detector that deliberately excludes `include` when immediately preceded by `#` therefore leaves a valid source-loading spelling outside the fail-closed contract. + Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html @@ -36,11 +38,12 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - Default `src/` module trees are already conservatively included by the canonical `src/**/*.rs` closure. The custom-target module guard is therefore limited to reviewed production sources outside every production package's default `src/` directory. - The temporary custom-target guard intentionally over-approximates rather than reproducing Rust's module grammar. Any lexical `mod` token in a custom target root outside default `src/` fails closed. That may reject a harmless inline module or textual occurrence, but it removes identifier/trivia/visibility grammar gaps until compiler-derived source-input provenance replaces the heuristic. - Rust attribute and `include!` review must preserve lexical token boundaries. Non-doc line/block comments, including nested block comments, are treated as trivia between Rust tokens; comment/string contents do not terminate the surrounding attribute token tree. +- Raw-identifier spelling must not create a second identity for a source-loading macro. Ordinary `include` and raw `r#include` are the same fail-closed provenance surface. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision -Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro invocation uses parentheses, brackets, or braces and regardless of Rust whitespace/comment trivia between the `include` token, `!`, and the opening delimiter. The detector locates the `include` token and advances through the same nested non-doc-comment trivia skipper already used by the source-indirection contract before validating `!` and one of the three macro delimiters. This deliberately avoids encoding comment trivia as `\s`. +Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro path uses ordinary `include` or raw-identifier `r#include`, regardless of whether the invocation uses parentheses, brackets, or braces, and regardless of Rust whitespace/comment trivia between the identifier token, `!`, and the opening delimiter. The detector locates either spelling of the same underlying identifier and advances through the same nested non-doc-comment trivia skipper already used by the source-indirection contract before validating `!` and one of the three macro delimiters. It does not broaden the match to longer identifiers and does not authorize any source path. Any Rust attribute containing a `path` meta item followed by valid Rust trivia and `=` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. Attribute bodies are extracted as balanced bracket token trees while skipping Rust whitespace, line comments, nested block comments, normal/raw string literals, and simple character literals for delimiter purposes. This scanner is deliberately limited to locating reviewed path-bearing attribute surfaces; it does not claim to parse Rust modules, name resolution, or macro expansion. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: @@ -73,9 +76,11 @@ Any additional path-bearing attribute, `include!` form outside the current fail- - `04e08a48fb7572860b0de564bdbf615ad2da5186` repaired the attribute review surface without changing Cargo topology: balanced attribute token trees now ignore comment/string delimiters correctly, and `path` followed by Rust trivia and `=` enters the exact-tree allowlist review surface. - `23b7b241c3e9389a43a359a14c4dfa74036d8829` preserved a new structural RED for macro-invocation trivia: block comments between `include` and `!`, block comments between `!` and the opening delimiter, and line-comment trivia between `include` and `!` all bypass the predecessor `include\s*!\s*[([{]` detector even though Rust treats non-doc comments as whitespace between grammar tokens. - `bf132fb7b2c2d1a2fdae312962e2f0e0380fc1a5` repaired that gap by replacing the whitespace-only macro regex with token-plus-trivia recognition that reuses the existing nested Rust comment skipper before `!` and before the opening delimiter. The Cargo source closure and fail-closed policy are unchanged. +- `783bbc614d08c3a9299849524e3dbda8545ab29f` preserved a new structural RED for raw-identifier macro spelling. The predecessor detector intentionally rejected `include` when immediately preceded by `#`, so `r#include!("../generated_adapter.rs")` was outside the fail-closed include provenance stop even though Rust raw identifiers retain the same underlying identifier. +- `5c3b86091199f436374111d6a4056d47c13c1448` repaired the detector minimally by admitting an optional `r#` prefix as part of the include identifier token. Existing comment-trivia and three-delimiter handling remains unchanged; longer identifiers remain excluded and no path is allowlisted. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -Replace the temporary custom-target lexical stop and hand-maintained attribute/macro-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. +Replace the temporary custom-target lexical stop and hand-maintained attribute/macro-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. \ No newline at end of file From 2c13993f24b07a7048d7879e594e72a744eeb95f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 23:59:36 +0900 Subject: [PATCH 278/632] test(browser-session): expose aliased include source-indirection RED --- ...ser_session_rust_include_alias_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_browser_session_rust_include_alias_contract.py diff --git a/tests/test_browser_session_rust_include_alias_contract.py b/tests/test_browser_session_rust_include_alias_contract.py new file mode 100644 index 000000000..e70d9d3b8 --- /dev/null +++ b/tests/test_browser_session_rust_include_alias_contract.py @@ -0,0 +1,53 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustIncludeAliasContractTests(unittest.TestCase): + """Prove that renaming Rust's include macro cannot bypass source provenance review.""" + + def test_aliased_include_macro_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text( + 'use core::include as embed;\nembed!("../generated_adapter.rs");\n', + encoding="utf-8", + ) + (adapter / "generated_adapter.rs").write_text( + "pub fn generated_adapter_surface() {}\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex( + AssertionError, + "Rust include! source indirection", + ): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From 4b8dd0832c6965c7e887b7538caa2f4ddc1d917c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 00:00:41 +0900 Subject: [PATCH 279/632] fix(browser-session): fail closed on aliased include macros --- ...ession_rust_source_indirection_contract.py | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 35787f696..f96fd349e 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -16,6 +16,8 @@ INCLUDE_TOKEN = re.compile(r"(? bool: return False +def _rust_use_statement_end(text: str, offset: int) -> int | None: + """Return the semicolon ending one Rust use declaration while ignoring comment trivia.""" + cursor = offset + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + if text[cursor] == ";": + return cursor + cursor += 1 + return None + + +def _has_aliased_include_import(text: str) -> bool: + """Detect use-tree aliases that rename include! before invocation.""" + for use_match in USE_TOKEN.finditer(text): + statement_end = _rust_use_statement_end(text, use_match.end()) + if statement_end is None: + continue + use_tree = text[use_match.end():statement_end] + for include_match in INCLUDE_TOKEN.finditer(use_tree): + cursor = _skip_rust_trivia(use_tree, include_match.end()) + as_match = AS_TOKEN.match(use_tree, cursor) + if as_match is None: + continue + alias_start = _skip_rust_trivia(use_tree, as_match.end()) + if alias_start >= len(use_tree): + continue + if use_tree[alias_start] == "_": + next_offset = alias_start + 1 + if next_offset >= len(use_tree) or not ( + use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" + ): + continue + return True + return False + + def _raw_string_end(text: str, offset: int) -> int | None: """Return the end of a Rust raw string token beginning at offset, if present.""" cursor = offset @@ -212,7 +253,7 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: text = source.read_text(encoding="utf-8") relative = source.relative_to(root).as_posix() - if _has_include_macro(text): + if _has_include_macro(text) or _has_aliased_include_import(text): raise AssertionError( f"Rust include! source indirection requires an explicit provenance contract: {relative}" ) @@ -391,4 +432,4 @@ def test_parent_traversal_path_attribute_fails_closed(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 29555f7eddf0b06fb6030efec0eb2775983ea3f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 00:01:42 +0900 Subject: [PATCH 280/632] docs(browser-session): trace aliased include macro provenance --- .../browser-session-rust-source-indirection.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-source-indirection.md b/docs/traceability/browser-session-rust-source-indirection.md index c5619de2c..d8ed47607 100644 --- a/docs/traceability/browser-session-rust-source-indirection.md +++ b/docs/traceability/browser-session-rust-source-indirection.md @@ -17,10 +17,15 @@ Rust comments are lexical trivia rather than `\s`-only whitespace. This matters Raw identifiers are part of the same source-provenance surface. The Rust Reference defines a raw identifier as `r#` plus an identifier/keyword and states that the `r#` prefix is not part of the actual identifier. Macro invocations resolve a `SimplePath`, whose path segment admits an `IDENTIFIER`, so `r#include!(...)` names the same underlying `include` identifier through raw spelling. A detector that deliberately excludes `include` when immediately preceded by `#` therefore leaves a valid source-loading spelling outside the fail-closed contract. +Macro renaming is also source provenance. Rust `use` declarations create local synonymous bindings, can import macro names, and permit `as` aliases. Because `include` is exported from `core`, `use core::include as embed; embed!("...")` can invoke the same source-loading macro without any later literal `include!` token. A direct-invocation-only scanner therefore has a second name-resolution bypass even after ordinary/raw spelling and comment trivia are covered. + Primary references: - Rust `include!` macro: https://doc.rust-lang.org/stable/std/macro.include.html +- Rust `core::include` macro export/source: https://doc.rust-lang.org/core/macro.include.html - Rust Reference, macro invocation syntax: https://doc.rust-lang.org/reference/macros.html#macro-invocation +- Rust Reference, use declarations and aliases: https://doc.rust-lang.org/reference/items/use-declarations.html +- Rust Reference, namespaces and macro imports: https://doc.rust-lang.org/reference/names/namespaces.html - Rust Reference, module source filenames and `path` attribute: https://doc.rust-lang.org/reference/items/modules.html#module-source-filenames - Rust Reference, conditional attributes with `cfg_attr`: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute - Rust Reference, identifiers and raw identifiers: https://doc.rust-lang.org/reference/identifiers.html @@ -39,19 +44,22 @@ Without an explicit contract, a future lifecycle adapter could keep its crate, m - The temporary custom-target guard intentionally over-approximates rather than reproducing Rust's module grammar. Any lexical `mod` token in a custom target root outside default `src/` fails closed. That may reject a harmless inline module or textual occurrence, but it removes identifier/trivia/visibility grammar gaps until compiler-derived source-input provenance replaces the heuristic. - Rust attribute and `include!` review must preserve lexical token boundaries. Non-doc line/block comments, including nested block comments, are treated as trivia between Rust tokens; comment/string contents do not terminate the surrounding attribute token tree. - Raw-identifier spelling must not create a second identity for a source-loading macro. Ordinary `include` and raw `r#include` are the same fail-closed provenance surface. +- Import aliases must not create a third identity for the same macro. A named `use ... include as alias` binding is fail-closed until compiler-derived macro/source provenance replaces the lexical contract; `as _` is not treated as callable alias authority because it creates no name that can be invoked later. - No future BiDi adapter path or Rust source indirection is pre-authorized. ## Decision Until compiler-derived source-input provenance is modeled, production Rust source fails closed on `include!` regardless of whether the macro path uses ordinary `include` or raw-identifier `r#include`, regardless of whether the invocation uses parentheses, brackets, or braces, and regardless of Rust whitespace/comment trivia between the identifier token, `!`, and the opening delimiter. The detector locates either spelling of the same underlying identifier and advances through the same nested non-doc-comment trivia skipper already used by the source-indirection contract before validating `!` and one of the three macro delimiters. It does not broaden the match to longer identifiers and does not authorize any source path. +The same contract also fails closed when a Rust `use` tree gives `include` a callable alias. It scans `use` declarations through their semicolon while ignoring Rust comment trivia, then rejects a named `include as ...` binding. This covers direct and grouped use trees without trying to implement general macro name resolution. Ordinary direct imports remain covered by the later literal `include!` invocation; an underscore import is not a callable alias and is not rejected by this alias-specific rule. This remains a temporary lexical security boundary, not a claim of compiler-equivalent name resolution. + Any Rust attribute containing a `path` meta item followed by valid Rust trivia and `=` is treated as source-indirection review surface, including `cfg_attr`-generated `path`. Attribute bodies are extracted as balanced bracket token trees while skipping Rust whitespace, line comments, nested block comments, normal/raw string literals, and simple character literals for delimiter purposes. This scanner is deliberately limited to locating reviewed path-bearing attribute surfaces; it does not claim to parse Rust modules, name resolution, or macro expansion. The contract uses an exact-tree allowlist of `(source_path, normalized_attribute_body)`. The allowlist must equal the path-attribute surfaces found on the current OriginWeave production-source closure, so it cannot reserve absent future adapter paths. The current exact allowlist contains only: `crates/originweave-core/src/root.rs` → `path = "lib.rs"` For a reviewed custom Cargo target outside a production package's default `src/` tree, the temporary contract no longer tries to prove that a particular `mod` spelling is outlined rather than inline. Any lexical `mod` token is a provenance stop. This deliberately conservative rule closes changes in identifier spelling and Rust trivia such as `mod r#type;`, `mod 관찰;`, or `mod /* comment */ helper;` without taking ownership of Cargo topology or pretending a regex implements the Rust parser. Ordinary `src/` module trees remain outside this guard because their sibling files are already enumerated by the canonical source closure. -Any additional path-bearing attribute, `include!` form outside the current fail-closed policy, or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem indirection must not silently widen the Browser Session TCB. +Any additional path-bearing attribute, `include!` form outside the current fail-closed policy, aliased source-loading macro, or custom-target module source must arrive in the same reviewed delta that explains and tests its source provenance. A future filesystem or macro-name indirection must not silently widen the Browser Session TCB. ## RED → repair evidence @@ -78,9 +86,11 @@ Any additional path-bearing attribute, `include!` form outside the current fail- - `bf132fb7b2c2d1a2fdae312962e2f0e0380fc1a5` repaired that gap by replacing the whitespace-only macro regex with token-plus-trivia recognition that reuses the existing nested Rust comment skipper before `!` and before the opening delimiter. The Cargo source closure and fail-closed policy are unchanged. - `783bbc614d08c3a9299849524e3dbda8545ab29f` preserved a new structural RED for raw-identifier macro spelling. The predecessor detector intentionally rejected `include` when immediately preceded by `#`, so `r#include!("../generated_adapter.rs")` was outside the fail-closed include provenance stop even though Rust raw identifiers retain the same underlying identifier. - `5c3b86091199f436374111d6a4056d47c13c1448` repaired the detector minimally by admitting an optional `r#` prefix as part of the include identifier token. Existing comment-trivia and three-delimiter handling remains unchanged; longer identifiers remain excluded and no path is allowlisted. +- `2c13993f24b07a7048d7879e594e72a744eeb95f` preserved a new structural RED for macro-name aliasing: a production source imports `core::include as embed` and invokes `embed!("../generated_adapter.rs")`. The predecessor direct-invocation detector sees the `include` token only inside the `use` tree, where no `!` follows it, and therefore does not stop the aliased source load. +- `4b8dd0832c6965c7e887b7538caa2f4ddc1d917c` is the minimal causal repair. The source-indirection contract now recognizes named `include as alias` bindings inside Rust `use` declarations, including grouped use trees and comment trivia, while preserving the existing direct/raw invocation detector and treating `as _` as non-callable. Cargo topology and source containment remain owned by the existing canonical closure. This evidence is structural/static on a Draft branch. It is not executable exact-head GREEN and does not replace the required parent-lineage, repository/security, or real-Chromium acceptance gates. ## Follow-up -Replace the temporary custom-target lexical stop and hand-maintained attribute/macro-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, and arrive with hostile fixtures before any allowlist widening. \ No newline at end of file +Replace the temporary custom-target lexical stop and hand-maintained attribute/macro-source review with compiler-derived or equivalently exact source-input provenance before OriginWeave needs legitimate custom-target module trees or broader source-generating attributes/macros. The replacement must cover the actual bytes compiled by Rust across supported target configurations, preserve repository containment and immutable provenance, distinguish inline modules from source-loading outlined modules without ad-hoc grammar drift, cover macro name resolution/re-exports without lexical approximation, and arrive with hostile fixtures before any allowlist widening. \ No newline at end of file From 63d9e2c71835a9043e6c359ee781debea78b0c2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 00:03:58 +0900 Subject: [PATCH 281/632] test(browser-session): cover grouped include aliases --- ...wser_session_rust_include_alias_contract.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_rust_include_alias_contract.py b/tests/test_browser_session_rust_include_alias_contract.py index e70d9d3b8..07e915487 100644 --- a/tests/test_browser_session_rust_include_alias_contract.py +++ b/tests/test_browser_session_rust_include_alias_contract.py @@ -20,7 +20,7 @@ class BrowserSessionRustIncludeAliasContractTests(unittest.TestCase): """Prove that renaming Rust's include macro cannot bypass source provenance review.""" - def test_aliased_include_macro_fails_closed(self) -> None: + def _assert_alias_fails_closed(self, source_text: str) -> None: with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) (root / "Cargo.toml").write_text( @@ -33,10 +33,7 @@ def test_aliased_include_macro_fails_closed(self) -> None: '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', encoding="utf-8", ) - (adapter / "src/lib.rs").write_text( - 'use core::include as embed;\nembed!("../generated_adapter.rs");\n', - encoding="utf-8", - ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") (adapter / "generated_adapter.rs").write_text( "pub fn generated_adapter_surface() {}\n", encoding="utf-8", @@ -48,6 +45,17 @@ def test_aliased_include_macro_fails_closed(self) -> None: ): source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_aliased_include_macro_fails_closed(self) -> None: + self._assert_alias_fails_closed( + 'use core::include as embed;\nembed!("../generated_adapter.rs");\n' + ) + + def test_grouped_raw_include_alias_with_comment_trivia_fails_closed(self) -> None: + self._assert_alias_fails_closed( + 'use core::{r#include /* provenance trivia */ as embed};\n' + 'embed!["../generated_adapter.rs"];\n' + ) + if __name__ == "__main__": unittest.main() From ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:09:33 +0900 Subject: [PATCH 282/632] test(browser-session): expose Cargo rustc-wrapper provenance gap --- ...ssion_cargo_compiler_authority_contract.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_browser_session_cargo_compiler_authority_contract.py diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py new file mode 100644 index 000000000..7cf994fb3 --- /dev/null +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -0,0 +1,50 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" + +spec = importlib.util.spec_from_file_location("browser_session_trusted_adapter_boundary", BOUNDARY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session trusted-adapter boundary contract") +boundary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(boundary) + + +class BrowserSessionCargoCompilerAuthorityContractTests(unittest.TestCase): + """Keep Git-owned Cargo compiler execution inside the reviewed Browser Session TCB.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def test_repository_rustc_wrapper_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustc-wrapper = "tools/review-bypass-wrapper"\n' + ) + + with self.assertRaisesRegex(AssertionError, "Cargo compiler execution override"): + boundary._production_package_manifests(root) + + +if __name__ == "__main__": + unittest.main() From 80aaa562672211582d5b2de69edc79a984559fb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:10:01 +0900 Subject: [PATCH 283/632] fix(browser-session): fail closed on Cargo rustc execution overrides --- ...ssion_cargo_compiler_authority_contract.py | 95 +++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 7cf994fb3..5010090be 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -1,6 +1,7 @@ import importlib.util import pathlib import tempfile +import tomllib import unittest @@ -13,11 +14,56 @@ boundary = importlib.util.module_from_spec(spec) spec.loader.exec_module(boundary) +COMPILER_EXECUTION_KEYS = frozenset({"rustc", "rustc-wrapper", "rustc-workspace-wrapper"}) + + +def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: + """Reject Git-owned Cargo settings that replace or wrap rustc for production builds.""" + # The trusted-adapter boundary remains the single writer for production package/source topology + # and dependency-source overrides. This contract owns only Cargo's compiler-execution authority. + boundary._production_package_manifests(root) + + root_resolved = root.resolve() + config_paths: set[pathlib.Path] = set() + for pattern in (".cargo/config.toml", ".cargo/config"): + config_paths.update(root.rglob(pattern)) + + for config_path in sorted(config_paths): + resolved = config_path.resolve() + try: + resolved.relative_to(root_resolved) + except ValueError as exc: + raise AssertionError( + f"Cargo compiler config escapes repository review root: {config_path.relative_to(root).as_posix()}" + ) from exc + if not resolved.is_file(): + raise AssertionError( + f"Cargo compiler config is missing: {config_path.relative_to(root).as_posix()}" + ) + + parsed = tomllib.loads(resolved.read_text(encoding="utf-8")) + build = parsed.get("build") + if not isinstance(build, dict): + continue + configured = sorted(COMPILER_EXECUTION_KEYS.intersection(build)) + if configured: + relative = config_path.relative_to(root).as_posix() + raise AssertionError( + "Cargo compiler execution override requires an explicit Browser Session provenance contract: " + f"{relative} keys={configured}" + ) + class BrowserSessionCargoCompilerAuthorityContractTests(unittest.TestCase): """Keep Git-owned Cargo compiler execution inside the reviewed Browser Session TCB.""" - def _workspace_with_config(self, config_text: str) -> pathlib.Path: + def _workspace_with_config( + self, + config_text: str, + *, + config_name: str = "config.toml", + nested: bool = False, + ) -> pathlib.Path: directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) root = pathlib.Path(directory.name) @@ -32,18 +78,55 @@ def _workspace_with_config(self, config_text: str) -> pathlib.Path: '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', encoding="utf-8", ) - cargo = root / ".cargo" + config_root = adapter if nested else root + cargo = config_root / ".cargo" cargo.mkdir() - (cargo / "config.toml").write_text(config_text, encoding="utf-8") + (cargo / config_name).write_text(config_text, encoding="utf-8") return root - def test_repository_rustc_wrapper_fails_closed(self) -> None: + def _assert_compiler_override_fails_closed( + self, + config_text: str, + *, + config_name: str = "config.toml", + nested: bool = False, + ) -> None: root = self._workspace_with_config( + config_text, + config_name=config_name, + nested=nested, + ) + with self.assertRaisesRegex(AssertionError, "Cargo compiler execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_current_repository_has_no_unmodeled_cargo_compiler_execution_override(self) -> None: + _assert_no_repository_cargo_compiler_execution_overrides(ROOT) + + def test_repository_rustc_wrapper_fails_closed(self) -> None: + self._assert_compiler_override_fails_closed( '[build]\nrustc-wrapper = "tools/review-bypass-wrapper"\n' ) - with self.assertRaisesRegex(AssertionError, "Cargo compiler execution override"): - boundary._production_package_manifests(root) + def test_repository_rustc_workspace_wrapper_fails_closed(self) -> None: + self._assert_compiler_override_fails_closed( + '[build]\nrustc-workspace-wrapper = "tools/workspace-wrapper"\n' + ) + + def test_repository_custom_rustc_fails_closed(self) -> None: + self._assert_compiler_override_fails_closed( + '[build]\nrustc = "tools/custom-rustc"\n' + ) + + def test_nested_extensionless_cargo_config_compiler_override_fails_closed(self) -> None: + self._assert_compiler_override_fails_closed( + '[build]\nrustc-wrapper = "tools/nested-wrapper"\n', + config_name="config", + nested=True, + ) + + def test_unrelated_build_configuration_remains_allowed(self) -> None: + root = self._workspace_with_config('[build]\njobs = 2\nincremental = false\n') + _assert_no_repository_cargo_compiler_execution_overrides(root) if __name__ == "__main__": From c59d41944f68491ae4d58cfe8997357acff19ee1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:10:27 +0900 Subject: [PATCH 284/632] docs(browser-session): trace Cargo compiler execution authority --- ...rowser-session-cargo-compiler-authority.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-compiler-authority.md diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md new file mode 100644 index 000000000..d57dae9c8 --- /dev/null +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -0,0 +1,58 @@ +# Browser Session Cargo compiler authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim executable repository/security GREEN. + +## Problem + +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was compiler execution itself. + +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, or add a workspace-only wrapper with `build.rustc-workspace-wrapper`. Cargo specifies that the wrapper receives the real compiler path and the compiler arguments. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective compiler invocation is no longer represented by the existing exact-tree source closure. A wrapper can inspect or transform arguments and therefore sits inside the build trusted computing base even when every discovered `.rs` path remains inside the repository. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. +- This contract does not authorize a future wrapper, custom compiler, generated source path, or adapter implementation. +- Environment-owned `RUSTC`/`RUSTC_WRAPPER` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. +- Ordinary Cargo settings that do not replace or wrap compiler execution are not rejected by this contract merely because they occur under `[build]`. + +## RED + +Commit `ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6` added a hostile repository fixture with: + +```toml +[build] +rustc-wrapper = "tools/review-bypass-wrapper" +``` + +The fixture required the Browser Session security contract to fail closed with a Cargo compiler-execution provenance error. The predecessor source/config boundary rejected Cargo `paths`, `[patch]`, `[source]`, build scripts, and build dependencies, but it did not classify `build.rustc-wrapper`. This commit therefore preserves a structural RED; no hosted execution result is inferred from the Draft branch. + +## Decision and repair + +Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler-authority contract that first consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration for exactly these compiler execution keys: + +- `build.rustc` +- `build.rustc-wrapper` +- `build.rustc-workspace-wrapper` + +Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all three settings, nested extensionless `.cargo/config`, the current repository tree, and an unrelated `[build]` configuration that remains allowed. + +A separate contract was chosen instead of expanding Cargo package/source discovery because compiler invocation authority is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting wrapper paths was rejected because it would pre-authorize executable build authority without immutable artifact identity, behavior, or provenance. + +## Security effect and residual risk + +The repair closes a Git-owned compiler-execution gap that could otherwise place an unmodeled executable between Cargo and `rustc` while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, or external compiler binaries are trustworthy; those controls remain with their canonical CI/supply-chain owners. + +Cargo compiler flags and target selection are not treated as equivalent to replacing the compiler executable in this slice. If a concrete flag/target configuration can introduce unreviewed executable source bytes or bypass a Browser Session invariant, it requires its own hostile case and causal contract rather than a catch-all Cargo-config ban. + +## Acceptance and follow-up + +1. Obtain independent current-head review of the RED→repair chain. +2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. +3. Regenerate executable repository/security evidence on the reconciled exact head. +4. If a Rust compiler wrapper is ever required, replace this fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. + +## References + +The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/config.html + +The Cargo Project. (n.d.). *Build cache*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/build-cache.html From a39caf95a38862f4cb4bcb68115b5e385bd6c26e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:31:59 +0900 Subject: [PATCH 285/632] test(browser-session): expose Cargo rustdoc execution authority gap --- ...test_browser_session_cargo_compiler_authority_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 5010090be..b6370f2e3 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -117,6 +117,11 @@ def test_repository_custom_rustc_fails_closed(self) -> None: '[build]\nrustc = "tools/custom-rustc"\n' ) + def test_repository_custom_rustdoc_fails_closed(self) -> None: + self._assert_compiler_override_fails_closed( + '[build]\nrustdoc = "tools/review-bypass-rustdoc"\n' + ) + def test_nested_extensionless_cargo_config_compiler_override_fails_closed(self) -> None: self._assert_compiler_override_fails_closed( '[build]\nrustc-wrapper = "tools/nested-wrapper"\n', From 345759105a0f0d2e88142df9961ea724b1055734 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:32:14 +0900 Subject: [PATCH 286/632] fix(browser-session): fail closed on Cargo rustdoc executable override --- ...rowser_session_cargo_compiler_authority_contract.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b6370f2e3..b07b938a9 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -14,13 +14,15 @@ boundary = importlib.util.module_from_spec(spec) spec.loader.exec_module(boundary) -COMPILER_EXECUTION_KEYS = frozenset({"rustc", "rustc-wrapper", "rustc-workspace-wrapper"}) +COMPILER_EXECUTION_KEYS = frozenset( + {"rustc", "rustc-wrapper", "rustc-workspace-wrapper", "rustdoc"} +) def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: - """Reject Git-owned Cargo settings that replace or wrap rustc for production builds.""" + """Reject Git-owned Cargo settings that replace or wrap Rust tool executables.""" # The trusted-adapter boundary remains the single writer for production package/source topology - # and dependency-source overrides. This contract owns only Cargo's compiler-execution authority. + # and dependency-source overrides. This contract owns Cargo's Rust tool-execution authority. boundary._production_package_manifests(root) root_resolved = root.resolve() @@ -55,7 +57,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) class BrowserSessionCargoCompilerAuthorityContractTests(unittest.TestCase): - """Keep Git-owned Cargo compiler execution inside the reviewed Browser Session TCB.""" + """Keep Git-owned Cargo Rust tool execution inside the reviewed Browser Session TCB.""" def _workspace_with_config( self, From ac0c86c57f606fe61486b37740c37e37c3126ff3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 01:32:35 +0900 Subject: [PATCH 287/632] docs(browser-session): trace Cargo rustdoc execution authority repair --- ...rowser-session-cargo-compiler-authority.md | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index d57dae9c8..630f0afd6 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -4,16 +4,16 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was compiler execution itself. +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was Rust tool execution itself. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, or add a workspace-only wrapper with `build.rustc-workspace-wrapper`. Cargo specifies that the wrapper receives the real compiler path and the compiler arguments. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective compiler invocation is no longer represented by the existing exact-tree source closure. A wrapper can inspect or transform arguments and therefore sits inside the build trusted computing base even when every discovered `.rs` path remains inside the repository. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo specifies that the wrappers receive the real compiler path and compiler arguments, while `build.rustdoc` is the executable Cargo invokes for rustdoc. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective Rust tool invocation is no longer represented by the existing exact-tree source closure. A wrapper can inspect or transform compiler arguments, and a replacement rustdoc executable can execute repository-selected code during the documentation gate even when every discovered `.rs` path remains inside the repository. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, generated source path, or adapter implementation. -- Environment-owned `RUSTC`/`RUSTC_WRAPPER` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings that do not replace or wrap compiler execution are not rejected by this contract merely because they occur under `[build]`. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, generated source path, or adapter implementation. +- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. +- Ordinary Cargo settings that do not replace or wrap Rust tool execution are not rejected by this contract merely because they occur under `[build]`. ## RED @@ -26,30 +26,42 @@ rustc-wrapper = "tools/review-bypass-wrapper" The fixture required the Browser Session security contract to fail closed with a Cargo compiler-execution provenance error. The predecessor source/config boundary rejected Cargo `paths`, `[patch]`, `[source]`, build scripts, and build dependencies, but it did not classify `build.rustc-wrapper`. This commit therefore preserves a structural RED; no hosted execution result is inferred from the Draft branch. +Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a second hostile fixture using: + +```toml +[build] +rustdoc = "tools/review-bypass-rustdoc" +``` + +The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. The new fixture therefore preserves a distinct structural RED: the repository could select an arbitrary rustdoc executable for the documentation gate while the reviewed Rust source and Cargo package topology remained unchanged. + ## Decision and repair -Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler-authority contract that first consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration for exactly these compiler execution keys: +Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler-authority contract that first consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration for Rust compiler execution keys. + +Commit `345759105a0f0d2e88142df9961ea724b1055734` extended the same bounded contract to `build.rustdoc`, because Cargo documents it as the program path used for rustdoc execution. The fail-closed key set is now: - `build.rustc` - `build.rustc-wrapper` - `build.rustc-workspace-wrapper` +- `build.rustdoc` -Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all three settings, nested extensionless `.cargo/config`, the current repository tree, and an unrelated `[build]` configuration that remains allowed. +Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all four settings, nested extensionless `.cargo/config`, the current repository tree, and an unrelated `[build]` configuration that remains allowed. -A separate contract was chosen instead of expanding Cargo package/source discovery because compiler invocation authority is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting wrapper paths was rejected because it would pre-authorize executable build authority without immutable artifact identity, behavior, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because Rust tool invocation authority is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting executable paths was rejected because it would pre-authorize build/documentation execution authority without immutable artifact identity, behavior, or provenance. ## Security effect and residual risk -The repair closes a Git-owned compiler-execution gap that could otherwise place an unmodeled executable between Cargo and `rustc` while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, or external compiler binaries are trustworthy; those controls remain with their canonical CI/supply-chain owners. +The repair closes Git-owned Rust tool-execution gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, or replace the rustdoc executable used by repository documentation gates, while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, or external compiler/documentation binaries are trustworthy; those controls remain with their canonical CI/supply-chain owners. -Cargo compiler flags and target selection are not treated as equivalent to replacing the compiler executable in this slice. If a concrete flag/target configuration can introduce unreviewed executable source bytes or bypass a Browser Session invariant, it requires its own hostile case and causal contract rather than a catch-all Cargo-config ban. +Cargo compiler flags, rustdoc flags, target linkers/runners, and target selection are not treated as equivalent to direct Rust tool executable replacement in this slice. They remain separate review surfaces. If a concrete flag/target setting can introduce unreviewed executable behavior, source bytes, or bypass a Browser Session invariant, it requires its own hostile case and causal contract rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the RED→repair chain. +1. Obtain independent current-head review of both RED→repair chains. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. If a Rust compiler wrapper is ever required, replace this fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. If a Rust compiler wrapper or replacement rustdoc executable is ever required, replace this fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References From 883ad62125af37e9afb2551803267284e21b7ea3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 02:05:21 +0900 Subject: [PATCH 288/632] test(browser-session): expose Cargo target executable override gap --- ...er_session_cargo_compiler_authority_contract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b07b938a9..4a19ebd68 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -131,6 +131,20 @@ def test_nested_extensionless_cargo_config_compiler_override_fails_closed(self) nested=True, ) + def test_repository_target_linker_fails_closed(self) -> None: + root = self._workspace_with_config( + '[target.x86_64-unknown-linux-gnu]\nlinker = "tools/review-bypass-linker"\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_target_runner_fails_closed(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)']\nrunner = \"tools/review-bypass-runner\"\n" + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_build_configuration_remains_allowed(self) -> None: root = self._workspace_with_config('[build]\njobs = 2\nincremental = false\n') _assert_no_repository_cargo_compiler_execution_overrides(root) From e7390cdb12c483570940411c857554540f754763 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 02:05:42 +0900 Subject: [PATCH 289/632] fix(browser-session): fail closed on Cargo target executables --- ...ssion_cargo_compiler_authority_contract.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 4a19ebd68..d347ecc5a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -17,12 +17,13 @@ COMPILER_EXECUTION_KEYS = frozenset( {"rustc", "rustc-wrapper", "rustc-workspace-wrapper", "rustdoc"} ) +TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: - """Reject Git-owned Cargo settings that replace or wrap Rust tool executables.""" + """Reject Git-owned Cargo settings that replace Rust-tool or target executables.""" # The trusted-adapter boundary remains the single writer for production package/source topology - # and dependency-source overrides. This contract owns Cargo's Rust tool-execution authority. + # and dependency-source overrides. This contract owns direct Cargo executable selection. boundary._production_package_manifests(root) root_resolved = root.resolve() @@ -45,19 +46,30 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) parsed = tomllib.loads(resolved.read_text(encoding="utf-8")) build = parsed.get("build") - if not isinstance(build, dict): - continue - configured = sorted(COMPILER_EXECUTION_KEYS.intersection(build)) - if configured: + build_configured = ( + sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] + ) + + target_configured: dict[str, list[str]] = {} + target = parsed.get("target") + if isinstance(target, dict): + for target_name, settings in target.items(): + if not isinstance(settings, dict): + continue + configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) + if configured: + target_configured[str(target_name)] = configured + + if build_configured or target_configured: relative = config_path.relative_to(root).as_posix() raise AssertionError( - "Cargo compiler execution override requires an explicit Browser Session provenance contract: " - f"{relative} keys={configured}" + "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " + f"{relative} build_keys={build_configured} target_keys={target_configured}" ) class BrowserSessionCargoCompilerAuthorityContractTests(unittest.TestCase): - """Keep Git-owned Cargo Rust tool execution inside the reviewed Browser Session TCB.""" + """Keep Git-owned Cargo executable selection inside the reviewed Browser Session TCB.""" def _workspace_with_config( self, @@ -98,7 +110,7 @@ def _assert_compiler_override_fails_closed( config_name=config_name, nested=nested, ) - with self.assertRaisesRegex(AssertionError, "Cargo compiler execution override"): + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) def test_current_repository_has_no_unmodeled_cargo_compiler_execution_override(self) -> None: From 366db5d843241c46db88e68b87279d1d47ae2e27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 02:06:22 +0900 Subject: [PATCH 290/632] docs(browser-session): trace Cargo target executable authority --- ...rowser-session-cargo-compiler-authority.md | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 630f0afd6..cc4809c2d 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -4,16 +4,17 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was Rust tool execution itself. +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was direct executable selection around Rust compilation, documentation, linking, and test/run execution. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo specifies that the wrappers receive the real compiler path and compiler arguments, while `build.rustdoc` is the executable Cargo invokes for rustdoc. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective Rust tool invocation is no longer represented by the existing exact-tree source closure. A wrapper can inspect or transform compiler arguments, and a replacement rustdoc executable can execute repository-selected code during the documentation gate even when every discovered `.rs` path remains inside the repository. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test execution path is no longer represented by the existing exact-tree source closure. A wrapper or runner can execute repository-selected behavior even when every discovered `.rs` path remains inside the repository, while a custom linker participates directly in producing the executable artifact. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, generated source path, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings that do not replace or wrap Rust tool execution are not rejected by this contract merely because they occur under `[build]`. +- Ordinary Cargo settings that do not directly select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. +- `rustflags`, `rustdocflags`, target selection, and environment/toolchain configuration remain separate review surfaces. In particular, rustc flags can influence linker behavior; this slice does not claim total linker-policy closure through every flag spelling. ## RED @@ -35,36 +36,55 @@ rustdoc = "tools/review-bypass-rustdoc" The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. The new fixture therefore preserves a distinct structural RED: the repository could select an arbitrary rustdoc executable for the documentation gate while the reviewed Rust source and Cargo package topology remained unchanged. +Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using both direct target executable surfaces: + +```toml +[target.x86_64-unknown-linux-gnu] +linker = "tools/review-bypass-linker" + +[target.'cfg(unix)'] +runner = "tools/review-bypass-runner" +``` + +The predecessor contract inspected only `[build]` and therefore accepted both target entries. This is structural RED evidence for direct Git-owned target executable selection, not hosted-run evidence. + ## Decision and repair Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler-authority contract that first consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration for Rust compiler execution keys. -Commit `345759105a0f0d2e88142df9961ea724b1055734` extended the same bounded contract to `build.rustdoc`, because Cargo documents it as the program path used for rustdoc execution. The fail-closed key set is now: +Commit `345759105a0f0d2e88142df9961ea724b1055734` extended the same bounded contract to `build.rustdoc`, because Cargo documents it as the program path used for rustdoc execution. + +Commit `e7390cdb12c483570940411c857554540f754763` extended the same config-owner contract to matching `[target]` tables and fails closed on direct `linker` and `runner` selection. Cargo documents `linker` as the linker path for that target and `runner` as the wrapper for `cargo run`, `cargo test`, and `cargo bench`. The modeled fail-closed executable keys are now: - `build.rustc` - `build.rustc-wrapper` - `build.rustc-workspace-wrapper` - `build.rustdoc` +- `target..linker` +- `target..runner` -Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all four settings, nested extensionless `.cargo/config`, the current repository tree, and an unrelated `[build]` configuration that remains allowed. +Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all four build-level Rust-tool settings, target-triple linker selection, `cfg(...)` runner selection, nested extensionless `.cargo/config`, the current repository tree, and unrelated `[build]` configuration that remains allowed. -A separate contract was chosen instead of expanding Cargo package/source discovery because Rust tool invocation authority is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting executable paths was rejected because it would pre-authorize build/documentation execution authority without immutable artifact identity, behavior, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting executable paths was rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. ## Security effect and residual risk -The repair closes Git-owned Rust tool-execution gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, or replace the rustdoc executable used by repository documentation gates, while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, or external compiler/documentation binaries are trustworthy; those controls remain with their canonical CI/supply-chain owners. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, or interpose a runner around repository test/run binaries while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, external binaries, or compiler flags are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. -Cargo compiler flags, rustdoc flags, target linkers/runners, and target selection are not treated as equivalent to direct Rust tool executable replacement in this slice. They remain separate review surfaces. If a concrete flag/target setting can introduce unreviewed executable behavior, source bytes, or bypass a Browser Session invariant, it requires its own hostile case and causal contract rather than a catch-all Cargo-config ban. +`rustflags` and `rustdocflags` remain intentionally outside this direct-key slice. Rustc documents `-C linker=` as another way to choose the linker executable, so the current contract must not be described as complete linker provenance until flag-derived executable selection is modeled with its own hostile cases. Target selection and Cargo's broader flag surfaces likewise require causal review rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of both RED→repair chains. -2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. -3. Regenerate executable repository/security evidence on the reconciled exact head. -4. If a Rust compiler wrapper or replacement rustdoc executable is ever required, replace this fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. +1. Obtain independent current-head review of the direct executable-selection RED→repair chains. +2. Add a separate hostile flag-provenance slice before claiming complete linker execution authority; at minimum cover Cargo-owned rustflags forms that can select `rustc -C linker` without broad false-positive rejection of unrelated flags. +3. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. +4. Regenerate executable repository/security evidence on the reconciled exact head. +5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/config.html +The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/index.html + The Cargo Project. (n.d.). *Build cache*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/build-cache.html From 8b9ab4c34033b9e5990caad79169351e3d771039 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:01:44 +0900 Subject: [PATCH 291/632] test(browser-session): expose rustflags linker authority gap --- ...er_session_cargo_compiler_authority_contract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index d347ecc5a..dd32b95d8 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -157,6 +157,20 @@ def test_repository_target_runner_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_build_rustflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "linker=tools/review-bypass-linker"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_target_rustflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)']\nrustflags = \"-C linker=tools/review-bypass-linker\"\n" + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_build_configuration_remains_allowed(self) -> None: root = self._workspace_with_config('[build]\njobs = 2\nincremental = false\n') _assert_no_repository_cargo_compiler_execution_overrides(root) From 743a5321bb72d83541b34f12ce83628f95f581f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:02:17 +0900 Subject: [PATCH 292/632] fix(browser-session): fail closed on rustflags linker selection --- ...ssion_cargo_compiler_authority_contract.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index dd32b95d8..c1506fa86 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,10 +20,28 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) +def _rustflags_select_linker(value: object) -> bool: + """Return whether Cargo-owned rustflags select rustc's linker executable.""" + if isinstance(value, str): + arguments = value.split() + elif isinstance(value, list) and all(isinstance(argument, str) for argument in value): + arguments = value + else: + return False + + for index, argument in enumerate(arguments): + if argument.startswith("-Clinker="): + return True + if argument == "-C" and index + 1 < len(arguments): + if arguments[index + 1].startswith("linker="): + return True + return False + + def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: """Reject Git-owned Cargo settings that replace Rust-tool or target executables.""" # The trusted-adapter boundary remains the single writer for production package/source topology - # and dependency-source overrides. This contract owns direct Cargo executable selection. + # and dependency-source overrides. This contract owns Cargo-selected execution authority. boundary._production_package_manifests(root) root_resolved = root.resolve() @@ -49,6 +67,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured = ( sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] ) + if isinstance(build, dict) and _rustflags_select_linker(build.get("rustflags")): + build_configured.append("rustflags:-C linker") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -57,6 +77,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if not isinstance(settings, dict): continue configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) + if _rustflags_select_linker(settings.get("rustflags")): + configured.append("rustflags:-C linker") if configured: target_configured[str(target_name)] = configured @@ -171,6 +193,19 @@ def test_repository_target_rustflags_linker_override_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_compact_rustflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-Clinker=tools/review-bypass-linker"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_rustflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' + ) + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_build_configuration_remains_allowed(self) -> None: root = self._workspace_with_config('[build]\njobs = 2\nincremental = false\n') _assert_no_repository_cargo_compiler_execution_overrides(root) From 47776e41a75ce97eefcaba0af4b65f47c329abbd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:03:04 +0900 Subject: [PATCH 293/632] docs(traceability): record rustflags linker provenance repair --- ...rowser-session-cargo-compiler-authority.md | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index cc4809c2d..cad86c45b 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -4,17 +4,17 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was direct executable selection around Rust compilation, documentation, linking, and test/run execution. +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was executable selection around Rust compilation, documentation, linking, and test/run execution. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test execution path is no longer represented by the existing exact-tree source closure. A wrapper or runner can execute repository-selected behavior even when every discovered `.rs` path remains inside the repository, while a custom linker participates directly in producing the executable artifact. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching `target..rustflags` are passed to `rustc`; rustc's `-C linker=` codegen option selects which linker executable rustc invokes. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. - This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. -- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings that do not directly select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. -- `rustflags`, `rustdocflags`, target selection, and environment/toolchain configuration remain separate review surfaces. In particular, rustc flags can influence linker behavior; this slice does not claim total linker-policy closure through every flag spelling. +- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. +- Ordinary Cargo settings and rustflags that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. +- `rustdocflags`, target selection, command-line `cargo rustc` flags, environment/toolchain configuration, and linker arguments remain separate review surfaces. This slice models Cargo-owned rustflags only when they select rustc's linker executable. ## RED @@ -27,26 +27,21 @@ rustc-wrapper = "tools/review-bypass-wrapper" The fixture required the Browser Session security contract to fail closed with a Cargo compiler-execution provenance error. The predecessor source/config boundary rejected Cargo `paths`, `[patch]`, `[source]`, build scripts, and build dependencies, but it did not classify `build.rustc-wrapper`. This commit therefore preserves a structural RED; no hosted execution result is inferred from the Draft branch. -Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a second hostile fixture using: +Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a hostile `build.rustdoc` fixture. The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. This preserves a distinct structural RED for repository-selected rustdoc execution. -```toml -[build] -rustdoc = "tools/review-bypass-rustdoc" -``` - -The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. The new fixture therefore preserves a distinct structural RED: the repository could select an arbitrary rustdoc executable for the documentation gate while the reviewed Rust source and Cargo package topology remained unchanged. +Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using `target..linker` and `target..runner`. The predecessor contract inspected only `[build]` and therefore accepted both target entries. This is structural RED evidence for direct Git-owned target executable selection, not hosted-run evidence. -Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using both direct target executable surfaces: +Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added two flag-derived hostile fixtures: ```toml -[target.x86_64-unknown-linux-gnu] -linker = "tools/review-bypass-linker" +[build] +rustflags = ["-C", "linker=tools/review-bypass-linker"] [target.'cfg(unix)'] -runner = "tools/review-bypass-runner" +rustflags = "-C linker=tools/review-bypass-linker" ``` -The predecessor contract inspected only `[build]` and therefore accepted both target entries. This is structural RED evidence for direct Git-owned target executable selection, not hosted-run evidence. +The predecessor helper rejected direct `linker`/`runner` keys but ignored `rustflags`, so both forms could select an unmodeled linker executable while every production Rust source remained inside the reviewed repository. This commit preserves the rustflags linker-authority structural RED before the repair. ## Decision and repair @@ -54,7 +49,11 @@ Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler Commit `345759105a0f0d2e88142df9961ea724b1055734` extended the same bounded contract to `build.rustdoc`, because Cargo documents it as the program path used for rustdoc execution. -Commit `e7390cdb12c483570940411c857554540f754763` extended the same config-owner contract to matching `[target]` tables and fails closed on direct `linker` and `runner` selection. Cargo documents `linker` as the linker path for that target and `runner` as the wrapper for `cargo run`, `cargo test`, and `cargo bench`. The modeled fail-closed executable keys are now: +Commit `e7390cdb12c483570940411c857554540f754763` extended the same config-owner contract to matching `[target]` tables and fails closed on direct `linker` and `runner` selection. + +Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closes the flag-derived linker path without banning unrelated compiler flags. The contract now recognizes Cargo-owned rustflags in both documented representations, a space-separated string or an array of argument strings, and fails closed when the resulting rustc argument sequence contains either `-C` followed by `linker=` or compact `-Clinker=`. It applies the same rule to `build.rustflags` and matching target-table `rustflags`. A regression keeps unrelated flags such as `-C opt-level=2` and `--cfg` allowed. + +The modeled fail-closed execution-authority surfaces are now: - `build.rustc` - `build.rustc-wrapper` @@ -62,23 +61,23 @@ Commit `e7390cdb12c483570940411c857554540f754763` extended the same config-owner - `build.rustdoc` - `target..linker` - `target..runner` +- `build.rustflags` when they select rustc `-C linker=` +- `target..rustflags` when they select rustc `-C linker=` -Any configured key fails closed until a reviewed provenance/attestation design exists. Regression coverage includes all four build-level Rust-tool settings, target-triple linker selection, `cfg(...)` runner selection, nested extensionless `.cargo/config`, the current repository tree, and unrelated `[build]` configuration that remains allowed. - -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. Allowlisting executable paths was rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. A blanket rustflags ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, or interpose a runner around repository test/run binaries while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, external binaries, or compiler flags are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, or select a linker indirectly through Cargo-owned rustflags while the reviewed Rust source closure remained unchanged. -`rustflags` and `rustdocflags` remain intentionally outside this direct-key slice. Rustc documents `-C linker=` as another way to choose the linker executable, so the current contract must not be described as complete linker provenance until flag-derived executable selection is modeled with its own hostile cases. Target selection and Cargo's broader flag surfaces likewise require causal review rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc` flags, or arbitrary linker arguments are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. `rustdocflags` also remain outside this slice. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the direct executable-selection RED→repair chains. -2. Add a separate hostile flag-provenance slice before claiming complete linker execution authority; at minimum cover Cargo-owned rustflags forms that can select `rustc -C linker` without broad false-positive rejection of unrelated flags. -3. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. -4. Regenerate executable repository/security evidence on the reconciled exact head. +1. Obtain independent current-head review of the rustflags RED→repair chain and the retained direct executable-selection contracts. +2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. +3. Regenerate executable repository/security evidence on the reconciled exact head. +4. Review `rustdocflags`, command-line `cargo rustc` authority, and linker-argument surfaces separately; add a contract only when a realistic executable/provenance escape is demonstrated. 5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References From b0489000709243b27a9c849d2cada8e5fadd254e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:09:24 +0900 Subject: [PATCH 294/632] test(browser-session): expose rustdocflags linker provenance gap --- ...er_session_cargo_compiler_authority_contract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index c1506fa86..8df5676c6 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -200,6 +200,20 @@ def test_repository_compact_rustflags_linker_override_fails_closed(self) -> None with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_build_rustdocflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["-C", "linker=tools/review-bypass-linker"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_target_rustdocflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)']\nrustdocflags = \"-Clinker=tools/review-bypass-linker\"\n" + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' From e157b9c5f33467425df794f42e704503de697232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:09:53 +0900 Subject: [PATCH 295/632] fix(browser-session): constrain rustdocflags linker selection --- ...ssion_cargo_compiler_authority_contract.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 8df5676c6..e775c59a7 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,8 +20,8 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) -def _rustflags_select_linker(value: object) -> bool: - """Return whether Cargo-owned rustflags select rustc's linker executable.""" +def _flags_select_linker(value: object) -> bool: + """Return whether Cargo-owned rustc/rustdoc flags select a linker executable.""" if isinstance(value, str): arguments = value.split() elif isinstance(value, list) and all(isinstance(argument, str) for argument in value): @@ -67,8 +67,11 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured = ( sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] ) - if isinstance(build, dict) and _rustflags_select_linker(build.get("rustflags")): - build_configured.append("rustflags:-C linker") + if isinstance(build, dict): + if _flags_select_linker(build.get("rustflags")): + build_configured.append("rustflags:-C linker") + if _flags_select_linker(build.get("rustdocflags")): + build_configured.append("rustdocflags:-C linker") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -77,8 +80,10 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if not isinstance(settings, dict): continue configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) - if _rustflags_select_linker(settings.get("rustflags")): + if _flags_select_linker(settings.get("rustflags")): configured.append("rustflags:-C linker") + if _flags_select_linker(settings.get("rustdocflags")): + configured.append("rustdocflags:-C linker") if configured: target_configured[str(target_name)] = configured @@ -220,6 +225,12 @@ def test_unrelated_rustflags_remain_allowed(self) -> None: ) _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustdocflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["--document-private-items", "--cfg", "docsrs"]\n' + ) + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_build_configuration_remains_allowed(self) -> None: root = self._workspace_with_config('[build]\njobs = 2\nincremental = false\n') _assert_no_repository_cargo_compiler_execution_overrides(root) From a3434ebf7d8169a6cf5276d983d4a0c978a9720a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:10:33 +0900 Subject: [PATCH 296/632] docs(traceability): record rustdocflags linker provenance repair --- ...rowser-session-cargo-compiler-authority.md | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index cad86c45b..a71f4b6c2 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -6,52 +6,47 @@ Status: Draft contract evidence on PR #317. This document does not claim executa OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was executable selection around Rust compilation, documentation, linking, and test/run execution. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching `target..rustflags` are passed to `rustc`; rustc's `-C linker=` codegen option selects which linker executable rustc invokes. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test execution path is no longer represented by the existing exact-tree source closure. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. Both rustc and rustdoc accept `-C` codegen options; rustc documents `-C linker=` as selecting which linker executable it invokes, and rustdoc documents that its `-C` arguments are the same codegen arguments passed through to rustc when documentation or documentation tests compile Rust code. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. - This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. -- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings and rustflags that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. -- `rustdocflags`, target selection, command-line `cargo rustc` flags, environment/toolchain configuration, and linker arguments remain separate review surfaces. This slice models Cargo-owned rustflags only when they select rustc's linker executable. +- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. +- Ordinary Cargo settings, rustflags, and rustdocflags that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, and arbitrary linker arguments remain separate review surfaces. ## RED -Commit `ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6` added a hostile repository fixture with: +Commit `ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6` added a hostile repository fixture with `build.rustc-wrapper`. The predecessor source/config boundary rejected Cargo source overrides and build scripts but did not classify compiler-wrapper execution, so the commit preserves a structural RED. -```toml -[build] -rustc-wrapper = "tools/review-bypass-wrapper" -``` - -The fixture required the Browser Session security contract to fail closed with a Cargo compiler-execution provenance error. The predecessor source/config boundary rejected Cargo `paths`, `[patch]`, `[source]`, build scripts, and build dependencies, but it did not classify `build.rustc-wrapper`. This commit therefore preserves a structural RED; no hosted execution result is inferred from the Draft branch. +Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a hostile `build.rustdoc` fixture. The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. -Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a hostile `build.rustdoc` fixture. The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. This preserves a distinct structural RED for repository-selected rustdoc execution. +Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using `target..linker` and `target..runner`. The predecessor contract inspected only `[build]` and therefore accepted both target entries. -Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using `target..linker` and `target..runner`. The predecessor contract inspected only `[build]` and therefore accepted both target entries. This is structural RED evidence for direct Git-owned target executable selection, not hosted-run evidence. +Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added hostile Cargo-owned rustflags that selected `rustc -C linker=`. The predecessor rejected direct target `linker`/`runner` keys but ignored flag-derived linker selection. -Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added two flag-derived hostile fixtures: +Commit `b0489000709243b27a9c849d2cada8e5fadd254e` adds the corresponding rustdoc path: ```toml [build] -rustflags = ["-C", "linker=tools/review-bypass-linker"] +rustdocflags = ["-C", "linker=tools/review-bypass-linker"] [target.'cfg(unix)'] -rustflags = "-C linker=tools/review-bypass-linker" +rustdocflags = "-Clinker=tools/review-bypass-linker" ``` -The predecessor helper rejected direct `linker`/`runner` keys but ignored `rustflags`, so both forms could select an unmodeled linker executable while every production Rust source remained inside the reviewed repository. This commit preserves the rustflags linker-authority structural RED before the repair. +The predecessor helper inspected only `rustflags`. Cargo documents both forms as flags passed to rustdoc, and rustdoc documents `-C` as rustc codegen options used when it compiles documentation or documentation tests. The fixtures therefore preserve a distinct structural RED for repository-owned rustdoc flag-derived linker authority; no hosted-run result is inferred from this Draft branch. ## Decision and repair -Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate compiler-authority contract that first consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration for Rust compiler execution keys. +Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate execution-authority contract that consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration. -Commit `345759105a0f0d2e88142df9961ea724b1055734` extended the same bounded contract to `build.rustdoc`, because Cargo documents it as the program path used for rustdoc execution. +Commit `345759105a0f0d2e88142df9961ea724b1055734` extended it to `build.rustdoc`; commit `e7390cdb12c483570940411c857554540f754763` added matching target `linker` and `runner` rejection. -Commit `e7390cdb12c483570940411c857554540f754763` extended the same config-owner contract to matching `[target]` tables and fails closed on direct `linker` and `runner` selection. +Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closes Cargo-owned rustflags that select a linker without banning unrelated compiler flags. It recognizes both documented Cargo representations, a space-separated string or an array of argument strings, and both split `-C`, `linker=` and compact `-Clinker=` spellings. -Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closes the flag-derived linker path without banning unrelated compiler flags. The contract now recognizes Cargo-owned rustflags in both documented representations, a space-separated string or an array of argument strings, and fails closed when the resulting rustc argument sequence contains either `-C` followed by `linker=` or compact `-Clinker=`. It applies the same rule to `build.rustflags` and matching target-table `rustflags`. A regression keeps unrelated flags such as `-C opt-level=2` and `--cfg` allowed. +Commit `e157b9c5f33467425df794f42e704503de697232` reuses the same narrow flag parser for Cargo-owned `rustdocflags`. It now applies the linker-selection rule to `build.rustdocflags` and matching target-table `rustdocflags` while retaining unrelated rustdoc flags such as `--document-private-items` and `--cfg docsrs`. The modeled fail-closed execution-authority surfaces are now: @@ -61,29 +56,31 @@ The modeled fail-closed execution-authority surfaces are now: - `build.rustdoc` - `target..linker` - `target..runner` -- `build.rustflags` when they select rustc `-C linker=` -- `target..rustflags` when they select rustc `-C linker=` +- `build.rustflags` and matching target `rustflags` when they select `-C linker=` +- `build.rustdocflags` and matching target `rustdocflags` when they select `-C linker=` -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. Folding it into the topology scanner would blur single-writer responsibilities. A blanket rustflags ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, or select a linker indirectly through Cargo-owned rustflags while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, or select a linker indirectly through Cargo-owned rustc/rustdoc flags while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc` flags, or arbitrary linker arguments are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. `rustdocflags` also remain outside this slice. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, or arbitrary linker arguments are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the rustflags RED→repair chain and the retained direct executable-selection contracts. +1. Obtain independent current-head review of the rustdocflags RED→repair chain and the retained direct executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review `rustdocflags`, command-line `cargo rustc` authority, and linker-argument surfaces separately; add a contract only when a realistic executable/provenance escape is demonstrated. +4. Review command-line Cargo flags and linker-argument surfaces separately; add a contract only when a realistic executable/provenance escape is demonstrated. 5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/config.html -The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/index.html +The Cargo Project. (n.d.). *cargo rustdoc*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html + +The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html -The Cargo Project. (n.d.). *Build cache*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/build-cache.html +The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/index.html From 053b6cacd0d7d36dc619165aada99c1ac485b043 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:16:08 +0900 Subject: [PATCH 297/632] test(browser-session): expose long codegen linker bypass --- ...ssion_cargo_compiler_authority_contract.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index e775c59a7..d780d0f07 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -205,6 +205,20 @@ def test_repository_compact_rustflags_linker_override_fails_closed(self) -> None with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_long_build_rustflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["--codegen", "linker=tools/review-bypass-linker"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_long_target_rustflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)']\nrustflags = \"--codegen=linker=tools/review-bypass-linker\"\n" + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_build_rustdocflags_linker_override_fails_closed(self) -> None: root = self._workspace_with_config( '[build]\nrustdocflags = ["-C", "linker=tools/review-bypass-linker"]\n' @@ -219,6 +233,20 @@ def test_repository_target_rustdocflags_linker_override_fails_closed(self) -> No with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_long_build_rustdocflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["--codegen", "linker=tools/review-bypass-linker"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_long_target_rustdocflags_linker_override_fails_closed(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)']\nrustdocflags = \"--codegen=linker=tools/review-bypass-linker\"\n" + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' From 7ee4b253ff4e213e949468274d126db214a63e4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:16:36 +0900 Subject: [PATCH 298/632] fix(browser-session): reject long codegen linker selection --- ...wser_session_cargo_compiler_authority_contract.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index d780d0f07..0127f441d 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -30,9 +30,9 @@ def _flags_select_linker(value: object) -> bool: return False for index, argument in enumerate(arguments): - if argument.startswith("-Clinker="): + if argument.startswith(("-Clinker=", "--codegen=linker=")): return True - if argument == "-C" and index + 1 < len(arguments): + if argument in {"-C", "--codegen"} and index + 1 < len(arguments): if arguments[index + 1].startswith("linker="): return True return False @@ -69,9 +69,9 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) ) if isinstance(build, dict): if _flags_select_linker(build.get("rustflags")): - build_configured.append("rustflags:-C linker") + build_configured.append("rustflags:codegen linker") if _flags_select_linker(build.get("rustdocflags")): - build_configured.append("rustdocflags:-C linker") + build_configured.append("rustdocflags:codegen linker") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -81,9 +81,9 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) continue configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) if _flags_select_linker(settings.get("rustflags")): - configured.append("rustflags:-C linker") + configured.append("rustflags:codegen linker") if _flags_select_linker(settings.get("rustdocflags")): - configured.append("rustdocflags:-C linker") + configured.append("rustdocflags:codegen linker") if configured: target_configured[str(target_name)] = configured From ac251692497f28806b594ed386d8646704382aaf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:17:20 +0900 Subject: [PATCH 299/632] docs(traceability): record long codegen linker repair --- ...rowser-session-cargo-compiler-authority.md | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index a71f4b6c2..308d00fbe 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -6,7 +6,7 @@ Status: Draft contract evidence on PR #317. This document does not claim executa OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was executable selection around Rust compilation, documentation, linking, and test/run execution. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. Both rustc and rustdoc accept `-C` codegen options; rustc documents `-C linker=` as selecting which linker executable it invokes, and rustdoc documents that its `-C` arguments are the same codegen arguments passed through to rustc when documentation or documentation tests compile Rust code. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. Rust documents `-C` and `--codegen` as the short and long codegen-option interfaces; the `linker` codegen option selects which linker executable rustc invokes. Rustdoc passes its codegen options through when documentation or documentation tests compile Rust code. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints @@ -26,17 +26,21 @@ Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added hostile Cargo-owned rustflags that selected `rustc -C linker=`. The predecessor rejected direct target `linker`/`runner` keys but ignored flag-derived linker selection. -Commit `b0489000709243b27a9c849d2cada8e5fadd254e` adds the corresponding rustdoc path: +Commit `b0489000709243b27a9c849d2cada8e5fadd254e` added the corresponding Cargo-owned rustdocflags path using split and compact `-C linker=` forms. The predecessor helper inspected only rustflags, so both fixtures preserved a distinct structural RED for repository-selected rustdoc linker authority. + +Focused review of exact `a3434ebf7d8169a6cf5276d983d4a0c978a9720a` then found that the shared flag parser recognized only `-C` spellings even though Rust also documents the long `--codegen` interface. Commit `053b6cacd0d7d36dc619165aada99c1ac485b043` preserves that review finding as structural RED across both rustc and rustdoc flag surfaces: ```toml [build] -rustdocflags = ["-C", "linker=tools/review-bypass-linker"] +rustflags = ["--codegen", "linker=tools/review-bypass-linker"] +rustdocflags = ["--codegen", "linker=tools/review-bypass-linker"] [target.'cfg(unix)'] -rustdocflags = "-Clinker=tools/review-bypass-linker" +rustflags = "--codegen=linker=tools/review-bypass-linker" +rustdocflags = "--codegen=linker=tools/review-bypass-linker" ``` -The predecessor helper inspected only `rustflags`. Cargo documents both forms as flags passed to rustdoc, and rustdoc documents `-C` as rustc codegen options used when it compiles documentation or documentation tests. The fixtures therefore preserve a distinct structural RED for repository-owned rustdoc flag-derived linker authority; no hosted-run result is inferred from this Draft branch. +The predecessor parser accepted all four long-form cases. These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -44,9 +48,9 @@ Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate executio Commit `345759105a0f0d2e88142df9961ea724b1055734` extended it to `build.rustdoc`; commit `e7390cdb12c483570940411c857554540f754763` added matching target `linker` and `runner` rejection. -Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closes Cargo-owned rustflags that select a linker without banning unrelated compiler flags. It recognizes both documented Cargo representations, a space-separated string or an array of argument strings, and both split `-C`, `linker=` and compact `-Clinker=` spellings. +Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closed Cargo-owned rustflags that select a linker without banning unrelated compiler flags. Commit `e157b9c5f33467425df794f42e704503de697232` reused the same narrow parser for Cargo-owned rustdocflags while retaining unrelated rustdoc flags such as `--document-private-items` and `--cfg docsrs`. -Commit `e157b9c5f33467425df794f42e704503de697232` reuses the same narrow flag parser for Cargo-owned `rustdocflags`. It now applies the linker-selection rule to `build.rustdocflags` and matching target-table `rustdocflags` while retaining unrelated rustdoc flags such as `--document-private-items` and `--cfg docsrs`. +Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closes the review-discovered long-form bypass in that one shared parser. It treats both split `-C` / `--codegen` followed by `linker=` and compact `-Clinker=` / `--codegen=linker=` as the same linker-selection authority. Because rustflags and rustdocflags already consume the same parser in both build and target scopes, the repair covers all four retained long-form hostile fixtures without duplicating owner logic. The modeled fail-closed execution-authority surfaces are now: @@ -56,8 +60,8 @@ The modeled fail-closed execution-authority surfaces are now: - `build.rustdoc` - `target..linker` - `target..runner` -- `build.rustflags` and matching target `rustflags` when they select `-C linker=` -- `build.rustdocflags` and matching target `rustdocflags` when they select `-C linker=` +- `build.rustflags` and matching target `rustflags` when `-C` or `--codegen` selects `linker=` +- `build.rustdocflags` and matching target `rustdocflags` when `-C` or `--codegen` selects `linker=` A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. @@ -69,7 +73,7 @@ It does not prove that CI environment variables, toolchain installation, runner ## Acceptance and follow-up -1. Obtain independent current-head review of the rustdocflags RED→repair chain and the retained direct executable-selection contracts. +1. Obtain independent current-head review of the long-codegen RED→repair chain and retained executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. 4. Review command-line Cargo flags and linker-argument surfaces separately; add a contract only when a realistic executable/provenance escape is demonstrated. @@ -81,6 +85,8 @@ The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved Septembe The Cargo Project. (n.d.). *cargo rustdoc*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html +The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/index.html From 0b457b3936ef3fdd0546de06f279fc5cb2756e13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:39:50 +0900 Subject: [PATCH 300/632] test(browser-session): expose linker-driver override gap --- ...session_linker_driver_override_contract.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_browser_session_linker_driver_override_contract.py diff --git a/tests/test_browser_session_linker_driver_override_contract.py b/tests/test_browser_session_linker_driver_override_contract.py new file mode 100644 index 000000000..fb14aaa7c --- /dev/null +++ b/tests/test_browser_session_linker_driver_override_contract.py @@ -0,0 +1,57 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerDriverOverrideContractTests(unittest.TestCase): + """Reject Cargo-owned codegen flags that re-select the linker behind the compiler driver.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_linker_driver_override_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_link_arg_fuse_ld_override_fails_closed(self) -> None: + self._assert_linker_driver_override_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-fuse-ld=review-bypass-linker"]\n' + ) + + def test_target_long_link_args_fuse_ld_override_fails_closed(self) -> None: + self._assert_linker_driver_override_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"--codegen=link-args=-fuse-ld=review-bypass-linker\"]\n" + ) + + +if __name__ == "__main__": + unittest.main() From d9e7d8ab4047bb25d3c6db0e195ec06240495aa9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:40:26 +0900 Subject: [PATCH 301/632] fix(browser-session): reject linker-driver reselection flags --- ...ssion_cargo_compiler_authority_contract.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 0127f441d..545794d19 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,6 +20,15 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) +def _codegen_option_selects_linker(option: str) -> bool: + """Return whether one rustc codegen option selects the linker executable.""" + if option.startswith("linker="): + return True + if option.startswith(("link-arg=", "link-args=")): + return "-fuse-ld=" in option.partition("=")[2] + return False + + def _flags_select_linker(value: object) -> bool: """Return whether Cargo-owned rustc/rustdoc flags select a linker executable.""" if isinstance(value, str): @@ -30,11 +39,16 @@ def _flags_select_linker(value: object) -> bool: return False for index, argument in enumerate(arguments): - if argument.startswith(("-Clinker=", "--codegen=linker=")): - return True + option: str | None = None if argument in {"-C", "--codegen"} and index + 1 < len(arguments): - if arguments[index + 1].startswith("linker="): - return True + option = arguments[index + 1] + elif argument.startswith("-C") and len(argument) > 2: + option = argument[2:] + elif argument.startswith("--codegen="): + option = argument.removeprefix("--codegen=") + + if option is not None and _codegen_option_selects_linker(option): + return True return False @@ -247,6 +261,12 @@ def test_repository_long_target_rustdocflags_linker_override_fails_closed(self) with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_non_linker_selecting_link_arg_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' From c5d818d93836a2a6077d132410cfe138206b668c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:41:01 +0900 Subject: [PATCH 302/632] docs(traceability): record linker-driver provenance repair --- ...rowser-session-cargo-compiler-authority.md | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 308d00fbe..e5d02a935 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -4,17 +4,21 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface was executable selection around Rust compilation, documentation, linking, and test/run execution. +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface is executable selection around Rust compilation, documentation, linking, and test/run execution. -Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. Rust documents `-C` and `--codegen` as the short and long codegen-option interfaces; the `linker` codegen option selects which linker executable rustc invokes. Rustdoc passes its codegen options through when documentation or documentation tests compile Rust code. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. +Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. + +Rust documents `-C` and `--codegen` as equivalent short and long codegen-option interfaces. The `linker` codegen option directly selects which linker executable rustc invokes. Rust also documents `link-arg` and `link-args` as arguments appended to the linker invocation. On Unix-like targets where a C compiler is the linker driver, Rust explicitly documents that `-Clink-arg=-fuse-ld=$value` is passed to the driver after rustc's own linker-feature arguments and therefore generally takes priority when the driver chooses the actual linker. A repository-owned Cargo flag can therefore re-select the linker behind the nominal compiler driver without using `target..linker` or `-C linker=`. + +If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. - This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings, rustflags, and rustdocflags that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, and arbitrary linker arguments remain separate review surfaces. +- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker plugins, linker search-path manipulation, and other arbitrary linker-argument effects remain separate review surfaces. ## RED @@ -40,7 +44,11 @@ rustflags = "--codegen=linker=tools/review-bypass-linker" rustdocflags = "--codegen=linker=tools/review-bypass-linker" ``` -The predecessor parser accepted all four long-form cases. These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. +The predecessor parser accepted all four long-form cases. + +Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves a distinct linker-driver RED. It adds Cargo-owned rustflags using both split `-C link-arg=-fuse-ld=review-bypass-linker` and compact `--codegen=link-args=-fuse-ld=review-bypass-linker`. The predecessor parser only recognized the `linker=` codegen option and therefore allowed a repository flag to change the actual linker selected by a C compiler driver while the reviewed Rust source closure and nominal driver remained unchanged. + +These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -50,7 +58,9 @@ Commit `345759105a0f0d2e88142df9961ea724b1055734` extended it to `build.rustdoc` Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closed Cargo-owned rustflags that select a linker without banning unrelated compiler flags. Commit `e157b9c5f33467425df794f42e704503de697232` reused the same narrow parser for Cargo-owned rustdocflags while retaining unrelated rustdoc flags such as `--document-private-items` and `--cfg docsrs`. -Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closes the review-discovered long-form bypass in that one shared parser. It treats both split `-C` / `--codegen` followed by `linker=` and compact `-Clinker=` / `--codegen=linker=` as the same linker-selection authority. Because rustflags and rustdocflags already consume the same parser in both build and target scopes, the repair covers all four retained long-form hostile fixtures without duplicating owner logic. +Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closed the review-discovered long-form bypass in that one shared parser. It treats split `-C` / `--codegen` and compact `-C...` / `--codegen=...` as equivalent codegen-option interfaces. + +Commit `d9e7d8ab4047bb25d3c6db0e195ec06240495aa9` extends that same parser rather than adding a second flag scanner. It classifies `linker=` as direct linker selection and classifies `link-arg=` / `link-args=` only when their payload contains `-fuse-ld=`, the driver-level linker-selection mechanism documented by rustc. A control fixture retains ordinary `-C link-arg=-Wl,--as-needed`, avoiding a blanket ban on unrelated linker arguments. The modeled fail-closed execution-authority surfaces are now: @@ -62,21 +72,22 @@ The modeled fail-closed execution-authority surfaces are now: - `target..runner` - `build.rustflags` and matching target `rustflags` when `-C` or `--codegen` selects `linker=` - `build.rustdocflags` and matching target `rustdocflags` when `-C` or `--codegen` selects `linker=` +- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, or select a linker indirectly through Cargo-owned rustc/rustdoc flags while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, or re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=` while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, or arbitrary linker arguments are trustworthy; those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, linker plugins, search-path overrides such as driver-specific `-B`, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the long-codegen RED→repair chain and retained executable-selection contracts. +1. Obtain independent current-head review of the `-fuse-ld=` RED→repair chain and retained executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review command-line Cargo flags and linker-argument surfaces separately; add a contract only when a realistic executable/provenance escape is demonstrated. +4. Review linker plugin and search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. 5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -89,4 +100,4 @@ The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html -The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/index.html +The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/ From ed5661090417e1b95943720f101f54a3a925ac96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 03:59:47 +0900 Subject: [PATCH 303/632] test(browser-session): preserve linker driver search-path RED --- ...sion_linker_driver_search_path_contract.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_browser_session_linker_driver_search_path_contract.py diff --git a/tests/test_browser_session_linker_driver_search_path_contract.py b/tests/test_browser_session_linker_driver_search_path_contract.py new file mode 100644 index 000000000..794c0c213 --- /dev/null +++ b/tests/test_browser_session_linker_driver_search_path_contract.py @@ -0,0 +1,57 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerDriverSearchPathContractTests(unittest.TestCase): + """Reject Cargo-owned linker-driver search paths that can re-select tool executables.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_driver_search_path_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_link_arg_driver_search_path_fails_closed(self) -> None: + self._assert_driver_search_path_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Btools/review-bypass-binutils"]\n' + ) + + def test_target_long_link_args_driver_search_path_fails_closed(self) -> None: + self._assert_driver_search_path_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"--codegen=link-args=-B tools/review-bypass-binutils\"]\n" + ) + + +if __name__ == "__main__": + unittest.main() From 17a665ca0c895635cc52f3014a43cb9dea86e1b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 04:00:28 +0900 Subject: [PATCH 304/632] fix(browser-session): reject linker driver search-path reselection --- ...ssion_cargo_compiler_authority_contract.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 545794d19..838e81a6b 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,12 +20,23 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) +def _linker_driver_argument_selects_executable(argument: str) -> bool: + """Return whether one compiler-driver argument can re-select a linker executable.""" + if argument.startswith("-fuse-ld="): + return True + return argument == "-B" or argument.startswith("-B") + + def _codegen_option_selects_linker(option: str) -> bool: """Return whether one rustc codegen option selects the linker executable.""" if option.startswith("linker="): return True if option.startswith(("link-arg=", "link-args=")): - return "-fuse-ld=" in option.partition("=")[2] + payload = option.partition("=")[2] + return any( + _linker_driver_argument_selects_executable(argument) + for argument in payload.split() + ) return False @@ -267,6 +278,12 @@ def test_non_linker_selecting_link_arg_remains_allowed(self) -> None: ) _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_linker_forwarded_bsymbolic_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-Bsymbolic"]\n' + ) + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' From 0fc0212fd1835c266bb460c742a70957ee2fd93e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 04:01:20 +0900 Subject: [PATCH 305/632] docs(browser-session): trace linker driver search-path authority --- ...rowser-session-cargo-compiler-authority.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index e5d02a935..ab4d984f8 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -10,6 +10,8 @@ Cargo permits repository configuration to replace `rustc` with `build.rustc`, ex Rust documents `-C` and `--codegen` as equivalent short and long codegen-option interfaces. The `linker` codegen option directly selects which linker executable rustc invokes. Rust also documents `link-arg` and `link-args` as arguments appended to the linker invocation. On Unix-like targets where a C compiler is the linker driver, Rust explicitly documents that `-Clink-arg=-fuse-ld=$value` is passed to the driver after rustc's own linker-feature arguments and therefore generally takes priority when the driver chooses the actual linker. A repository-owned Cargo flag can therefore re-select the linker behind the nominal compiler driver without using `target..linker` or `-C linker=`. +The same nominal-driver boundary has another executable-selection path. Rust documents that Unix-like targets commonly use `cc` or `clang` as the linker driver. GCC documents `-Bprefix` as the first search prefix for driver subprograms including `ld`; if the requested program is found there, that executable is run before the standard prefixes or `PATH` lookup. Because rustc `link-arg` and `link-args` append arguments to the linker invocation, repository-owned `-C link-arg=-B...` or `--codegen=link-args=-B ...` can therefore redirect the compiler driver to a different linker executable without changing the nominal driver or using `-fuse-ld=`. + If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints @@ -18,7 +20,7 @@ If any of these settings enters a reviewed Browser Session production workspace - This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. - Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker plugins, linker search-path manipulation, and other arbitrary linker-argument effects remain separate review surfaces. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker plugins, non-`-B` driver/tool search-path mechanisms, and other arbitrary linker-argument effects remain separate review surfaces. ## RED @@ -48,6 +50,8 @@ The predecessor parser accepted all four long-form cases. Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves a distinct linker-driver RED. It adds Cargo-owned rustflags using both split `-C link-arg=-fuse-ld=review-bypass-linker` and compact `--codegen=link-args=-fuse-ld=review-bypass-linker`. The predecessor parser only recognized the `linker=` codegen option and therefore allowed a repository flag to change the actual linker selected by a C compiler driver while the reviewed Rust source closure and nominal driver remained unchanged. +Commit `ed5661090417e1b95943720f101f54a3a925ac96` preserves a second linker-driver RED using GNU-compatible driver search-path selection. It adds `-C link-arg=-Btools/review-bypass-binutils` and `--codegen=link-args=-B tools/review-bypass-binutils` hostile fixtures. The predecessor parser recognized direct `linker=` and `-fuse-ld=` selection but accepted both `-B` forms, even though GCC searches `-B` prefixes first when locating subprograms such as `ld`. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -62,6 +66,8 @@ Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closed the review-discovered l Commit `d9e7d8ab4047bb25d3c6db0e195ec06240495aa9` extends that same parser rather than adding a second flag scanner. It classifies `linker=` as direct linker selection and classifies `link-arg=` / `link-args=` only when their payload contains `-fuse-ld=`, the driver-level linker-selection mechanism documented by rustc. A control fixture retains ordinary `-C link-arg=-Wl,--as-needed`, avoiding a blanket ban on unrelated linker arguments. +Commit `17a665ca0c895635cc52f3014a43cb9dea86e1b1` keeps that single parser and adds driver-argument executable selection for `-B`. Both attached and split-prefix forms inside `link-arg`/`link-args` now fail closed. The control `-C link-arg=-Wl,-Bsymbolic` remains allowed because it is explicitly forwarded to the actual linker rather than interpreted by the compiler driver as a program-search prefix. + The modeled fail-closed execution-authority surfaces are now: - `build.rustc` @@ -73,21 +79,22 @@ The modeled fail-closed execution-authority surfaces are now: - `build.rustflags` and matching target `rustflags` when `-C` or `--codegen` selects `linker=` - `build.rustdocflags` and matching target `rustdocflags` when `-C` or `--codegen` selects `linker=` - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver +- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, or re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=` while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, or redirect a GNU-compatible compiler driver's `ld` lookup with `-B` while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, linker plugins, search-path overrides such as driver-specific `-B`, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, linker plugins, non-`-B` driver/tool search-path mechanisms, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the `-fuse-ld=` RED→repair chain and retained executable-selection contracts. +1. Obtain independent current-head review of the `-B` RED→repair chain together with the retained executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review linker plugin and search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. +4. Review linker plugins and non-`-B` driver/toolchain search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. 5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -96,6 +103,8 @@ The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved Septembe The Cargo Project. (n.d.). *cargo rustdoc*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html +Free Software Foundation. (n.d.). *Directory options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html From 659e0b3914f19fd19c2d5d9dbeb9f40a43c00517 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 05:01:18 +0900 Subject: [PATCH 306/632] test(browser-session): preserve linker response-file provenance RED --- ...r_session_linker_response_file_contract.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_browser_session_linker_response_file_contract.py diff --git a/tests/test_browser_session_linker_response_file_contract.py b/tests/test_browser_session_linker_response_file_contract.py new file mode 100644 index 000000000..7c633d493 --- /dev/null +++ b/tests/test_browser_session_linker_response_file_contract.py @@ -0,0 +1,56 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerResponseFileContractTests(unittest.TestCase): + """Keep compiler-driver response files inside the reviewed linker-execution boundary.""" + + def _workspace_with_response_file(self, flags: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f'[build]\nrustflags = ["-C", "link-arg={flags}"]\n', + encoding="utf-8", + ) + tools = root / "tools" + tools.mkdir() + (tools / "linker.rsp").write_text( + "-Btools/review-bypass-binutils\n", + encoding="utf-8", + ) + return root + + def test_repository_linker_driver_response_file_fails_closed(self) -> None: + root = self._workspace_with_response_file("@tools/linker.rsp") + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 92ce887209f58b33ecd478ee09212bda70890894 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 05:01:58 +0900 Subject: [PATCH 307/632] fix(browser-session): fail closed on linker response files --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 838e81a6b..b10c5614f 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -22,6 +22,8 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: """Return whether one compiler-driver argument can re-select a linker executable.""" + if argument.startswith("@"): + return True if argument.startswith("-fuse-ld="): return True return argument == "-B" or argument.startswith("-B") From a435e8f09dc7441b85bcc8a4521df5575078363f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 05:02:31 +0900 Subject: [PATCH 308/632] docs(browser-session): trace linker response-file provenance --- ...rowser-session-cargo-compiler-authority.md | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index ab4d984f8..d873e562a 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -12,12 +12,14 @@ Rust documents `-C` and `--codegen` as equivalent short and long codegen-option The same nominal-driver boundary has another executable-selection path. Rust documents that Unix-like targets commonly use `cc` or `clang` as the linker driver. GCC documents `-Bprefix` as the first search prefix for driver subprograms including `ld`; if the requested program is found there, that executable is run before the standard prefixes or `PATH` lookup. Because rustc `link-arg` and `link-args` append arguments to the linker invocation, repository-owned `-C link-arg=-B...` or `--codegen=link-args=-B ...` can therefore redirect the compiler driver to a different linker executable without changing the nominal driver or using `-fuse-ld=`. +GCC also expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can therefore hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. The response file may itself include another response file. Until response-file contents, containment, recursion, and executable-selection semantics are represented as reviewed provenance, a driver-level `@file` argument is an opaque extension of the execution boundary and must fail closed. + If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. - Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. - Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker plugins, non-`-B` driver/tool search-path mechanisms, and other arbitrary linker-argument effects remain separate review surfaces. @@ -52,6 +54,8 @@ Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves a distinct linker-dr Commit `ed5661090417e1b95943720f101f54a3a925ac96` preserves a second linker-driver RED using GNU-compatible driver search-path selection. It adds `-C link-arg=-Btools/review-bypass-binutils` and `--codegen=link-args=-B tools/review-bypass-binutils` hostile fixtures. The predecessor parser recognized direct `linker=` and `-fuse-ld=` selection but accepted both `-B` forms, even though GCC searches `-B` prefixes first when locating subprograms such as `ld`. +Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves a third linker-driver RED using a Git-owned response file. The hostile fixture passes `-C link-arg=@tools/linker.rsp`, while that response file contains `-Btools/review-bypass-binutils`. The predecessor parser inspected only the visible `link-arg` token and therefore accepted the opaque response-file expansion path. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -68,6 +72,8 @@ Commit `d9e7d8ab4047bb25d3c6db0e195ec06240495aa9` extends that same parser rathe Commit `17a665ca0c895635cc52f3014a43cb9dea86e1b1` keeps that single parser and adds driver-argument executable selection for `-B`. Both attached and split-prefix forms inside `link-arg`/`link-args` now fail closed. The control `-C link-arg=-Wl,-Bsymbolic` remains allowed because it is explicitly forwarded to the actual linker rather than interpreted by the compiler driver as a program-search prefix. +Commit `92ce887209f58b33ecd478ee09212bda70890894` keeps the same parser and classifies driver-level `@file` arguments as opaque executable-selection provenance. It does not attempt to parse response files recursively or allowlist their paths: GCC response files can recursively expand additional response files, so path review alone would not establish the effective driver command. A future relaxation requires exact recursive content containment and driver-semantics provenance on the same reviewed tree. + The modeled fail-closed execution-authority surfaces are now: - `build.rustc` @@ -80,22 +86,23 @@ The modeled fail-closed execution-authority surfaces are now: - `build.rustdocflags` and matching target `rustdocflags` when `-C` or `--codegen` selects `linker=` - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable +- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable paths was also rejected because a path alone does not establish immutable executable identity, behavior, arguments, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable or response-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, or redirect a GNU-compatible compiler driver's `ld` lookup with `-B` while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, or hide either mechanism behind a recursively expanded driver response file while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, linker plugins, non-`-B` driver/tool search-path mechanisms, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the `-B` RED→repair chain together with the retained executable-selection contracts. +1. Obtain independent current-head review of the response-file RED→repair chain together with the retained executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review linker plugins and non-`-B` driver/toolchain search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. -5. If any blocked executable override is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, arguments/source-input provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. Review linker plugins and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. +5. If any blocked executable override or response file is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -105,6 +112,8 @@ The Cargo Project. (n.d.). *cargo rustdoc*. *The Cargo Book*. Retrieved Septembe Free Software Foundation. (n.d.). *Directory options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html +Free Software Foundation. (n.d.). *Overall options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html From 6e94d6b86ac96a677ad6195c818bc94f2199e77b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:01:27 +0900 Subject: [PATCH 309/632] test(browser-session): expose linker plugin execution provenance gap --- ..._browser_session_linker_plugin_contract.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_browser_session_linker_plugin_contract.py diff --git a/tests/test_browser_session_linker_plugin_contract.py b/tests/test_browser_session_linker_plugin_contract.py new file mode 100644 index 000000000..3fc1a16a2 --- /dev/null +++ b/tests/test_browser_session_linker_plugin_contract.py @@ -0,0 +1,59 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerPluginContractTests(unittest.TestCase): + """Keep linker-plugin code loading inside the reviewed Browser Session execution boundary.""" + + def _workspace_with_flags(self, rustflags: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f'[build]\nrustflags = {rustflags}\n', + encoding="utf-8", + ) + return root + + def test_repository_wl_linker_plugin_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-Wl,-plugin,tools/review-bypass-linker.so"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_xlinker_plugin_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-Xlinker", "-C", "link-arg=-plugin", "-C", "link-arg=-Xlinker", "-C", "link-arg=tools/review-bypass-linker.so"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:02:11 +0900 Subject: [PATCH 310/632] fix(browser-session): fail closed on linker plugin loading --- ...ssion_cargo_compiler_authority_contract.py | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b10c5614f..c0f3d5568 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -18,6 +18,7 @@ {"rustc", "rustc-wrapper", "rustc-workspace-wrapper", "rustdoc"} ) TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) +LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -29,21 +30,43 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: return argument == "-B" or argument.startswith("-B") -def _codegen_option_selects_linker(option: str) -> bool: - """Return whether one rustc codegen option selects the linker executable.""" - if option.startswith("linker="): +def _linker_option_loads_plugin(argument: str) -> bool: + """Return whether one direct linker option requests dynamically loaded plugin code.""" + if argument in LINKER_PLUGIN_OPTIONS: return True - if option.startswith(("link-arg=", "link-args=")): - payload = option.partition("=")[2] - return any( - _linker_driver_argument_selects_executable(argument) - for argument in payload.split() - ) + return argument.startswith(("-plugin=", "--plugin=")) + + +def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: + """Return direct-linker arguments encoded by a single compiler-driver forwarding option.""" + if argument.startswith("-Wl,"): + return tuple(argument.removeprefix("-Wl,").split(",")) + if argument.startswith("--for-linker="): + return tuple(argument.removeprefix("--for-linker=").split(",")) + return () + + +def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: + """Return whether driver arguments can replace the linker or load executable linker code.""" + for index, argument in enumerate(arguments): + if _linker_driver_argument_selects_executable(argument): + return True + if any( + _linker_option_loads_plugin(forwarded) + for forwarded in _forwarded_linker_arguments(argument) + ): + return True + if ( + argument == "-Xlinker" + and index + 1 < len(arguments) + and _linker_option_loads_plugin(arguments[index + 1]) + ): + return True return False def _flags_select_linker(value: object) -> bool: - """Return whether Cargo-owned rustc/rustdoc flags select a linker executable.""" + """Return whether Cargo-owned rustc/rustdoc flags select or extend linker execution code.""" if isinstance(value, str): arguments = value.split() elif isinstance(value, list) and all(isinstance(argument, str) for argument in value): @@ -51,6 +74,7 @@ def _flags_select_linker(value: object) -> bool: else: return False + linker_driver_arguments: list[str] = [] for index, argument in enumerate(arguments): option: str | None = None if argument in {"-C", "--codegen"} and index + 1 < len(arguments): @@ -60,9 +84,16 @@ def _flags_select_linker(value: object) -> bool: elif argument.startswith("--codegen="): option = argument.removeprefix("--codegen=") - if option is not None and _codegen_option_selects_linker(option): + if option is None: + continue + if option.startswith("linker="): return True - return False + if option.startswith("link-arg="): + linker_driver_arguments.append(option.partition("=")[2]) + elif option.startswith("link-args="): + linker_driver_arguments.extend(option.partition("=")[2].split()) + + return _linker_driver_arguments_select_executable(linker_driver_arguments) def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: From c42b7aa3550cc87f79acf46e3e1732e5098cddb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:02:35 +0900 Subject: [PATCH 311/632] test(browser-session): cover linker plugin forwarding forms --- ..._browser_session_linker_plugin_contract.py | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_linker_plugin_contract.py b/tests/test_browser_session_linker_plugin_contract.py index 3fc1a16a2..33c3a5b57 100644 --- a/tests/test_browser_session_linker_plugin_contract.py +++ b/tests/test_browser_session_linker_plugin_contract.py @@ -40,19 +40,41 @@ def _workspace_with_flags(self, rustflags: str) -> pathlib.Path: ) return root + def _assert_plugin_override_fails_closed(self, rustflags: str) -> None: + root = self._workspace_with_flags(rustflags) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_wl_linker_plugin_fails_closed(self) -> None: - root = self._workspace_with_flags( + self._assert_plugin_override_fails_closed( '["-C", "link-arg=-Wl,-plugin,tools/review-bypass-linker.so"]' ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_repository_xlinker_plugin_fails_closed(self) -> None: - root = self._workspace_with_flags( + self._assert_plugin_override_fails_closed( '["-C", "link-arg=-Xlinker", "-C", "link-arg=-plugin", "-C", "link-arg=-Xlinker", "-C", "link-arg=tools/review-bypass-linker.so"]' ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_for_linker_plugin_fails_closed(self) -> None: + self._assert_plugin_override_fails_closed( + '["-C", "link-arg=--for-linker=-plugin=tools/review-bypass-linker.so"]' + ) + + def test_non_plugin_wl_forwarding_remains_allowed(self) -> None: + root = self._workspace_with_flags('["-C", "link-arg=-Wl,--as-needed"]') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_non_plugin_xlinker_forwarding_remains_allowed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-Xlinker", "-C", "link-arg=--as-needed"]' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_non_plugin_for_linker_forwarding_remains_allowed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=--for-linker=--as-needed"]' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) if __name__ == "__main__": From 4ec0ec84716da2348ac9577290f239b9a23898db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:03:24 +0900 Subject: [PATCH 312/632] docs(browser-session): trace linker plugin execution provenance --- ...rowser-session-cargo-compiler-authority.md | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index d873e562a..442ef3e84 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -14,15 +14,17 @@ The same nominal-driver boundary has another executable-selection path. Rust doc GCC also expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can therefore hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. The response file may itself include another response file. Until response-file contents, containment, recursion, and executable-selection semantics are represented as reviewed provenance, a driver-level `@file` argument is an opaque extension of the execution boundary and must fail closed. +GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process, for example with `-C link-arg=-Wl,-plugin,tools/review-bypass-linker.so`. Repeated `-C link-arg=-Xlinker` forms can express the same plugin request across separate codegen options. That is executable-code provenance, not an ordinary linker tuning flag. + If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker plugins, non-`-B` driver/tool search-path mechanisms, and other arbitrary linker-argument effects remain separate review surfaces. +- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables or dynamically load modeled linker code are not rejected merely because they occur under `[build]` or `[target]`. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, and other arbitrary linker-argument effects remain separate review surfaces. ## RED @@ -56,6 +58,8 @@ Commit `ed5661090417e1b95943720f101f54a3a925ac96` preserves a second linker-driv Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves a third linker-driver RED using a Git-owned response file. The hostile fixture passes `-C link-arg=@tools/linker.rsp`, while that response file contains `-Btools/review-bypass-binutils`. The predecessor parser inspected only the visible `link-arg` token and therefore accepted the opaque response-file expansion path. +Commit `6e94d6b86ac96a677ad6195c818bc94f2199e77b` preserves a linker-plugin execution RED. The hostile fixtures pass GNU-compatible driver forwarding forms for `-plugin`: `-Wl,-plugin,tools/review-bypass-linker.so` and a repeated `-Xlinker` sequence. The predecessor parser inspected linker executable reselection but accepted both forms even though GNU `ld` dynamically loads the named plugin into the linking process. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -74,6 +78,8 @@ Commit `17a665ca0c895635cc52f3014a43cb9dea86e1b1` keeps that single parser and a Commit `92ce887209f58b33ecd478ee09212bda70890894` keeps the same parser and classifies driver-level `@file` arguments as opaque executable-selection provenance. It does not attempt to parse response files recursively or allowlist their paths: GCC response files can recursively expand additional response files, so path review alone would not establish the effective driver command. A future relaxation requires exact recursive content containment and driver-semantics provenance on the same reviewed tree. +Commit `0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a` extends the same Cargo flag parser again instead of introducing a second linker grammar. It preserves linker-driver arguments across multiple `link-arg` codegen options, decodes the GNU-compatible `-Wl,` and `--for-linker=` forwarding forms, and recognizes `-Xlinker` followed by `-plugin`/`--plugin`. Only the plugin-loading forms fail closed; non-plugin forwarding such as `-Wl,--as-needed`, `-Xlinker --as-needed`, and `--for-linker=--as-needed` remains permitted. Commit `c42b7aa3550cc87f79acf46e3e1732e5098cddb4` adds hostile and control coverage for all three forwarding spellings. + The modeled fail-closed execution-authority surfaces are now: - `build.rustc` @@ -87,22 +93,23 @@ The modeled fail-closed execution-authority surfaces are now: - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) +- Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding requests linker plugin loading through `-Wl,`, `--for-linker=`, or `-Xlinker` -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection is not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable or response-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments, behavior, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection and dynamically loaded linker code are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, or response-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned executable-selection gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, or hide either mechanism behind a recursively expanded driver response file while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, or dynamically load repository-selected linker plugin code while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, linker plugins, non-`-B` driver/tool search-path mechanisms, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin mechanisms, non-`-B` driver/tool search-path mechanisms, linker scripts, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the response-file RED→repair chain together with the retained executable-selection contracts. +1. Obtain independent current-head review of the linker-plugin RED→repair chain together with the retained executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review linker plugins and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic executable/provenance escape is demonstrated. -5. If any blocked executable override or response file is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. Review linker scripts, non-GNU plugin loading, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. +5. If any blocked executable override, response file, or linker plugin is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -112,8 +119,12 @@ The Cargo Project. (n.d.). *cargo rustdoc*. *The Cargo Book*. Retrieved Septembe Free Software Foundation. (n.d.). *Directory options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html +Free Software Foundation. (n.d.). *Link options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html + Free Software Foundation. (n.d.). *Overall options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html +Free Software Foundation. (n.d.). *Plugins*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Plugins.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html From cd3d361e2c9c49a4137d9a99c9865b381a68d2b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:07:05 +0900 Subject: [PATCH 313/632] docs(changelog): record Cargo linker execution provenance hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c62b36b..46459618b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed on Git-owned Cargo linker execution extensions that re-select executables through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, or opaque `@file`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. - Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. - Failed closed on production Cargo build surfaces (`build.rs`, enabled `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies) until generated-source provenance is explicitly modeled; `package.build = false` remains the explicit no-build-script form. - Repaired Browser Session repository contracts so custom `[lib].path` / `[[bin]].path` fixtures reach the intended target-source assertion, recovery-custody accessor prohibitions inspect only `BoundBrowserSessionRecovery` inherent impls, and settlement ordering verifies delegated fact selection before proof I/O rather than duplicating selector internals. @@ -126,4 +127,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 \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:10:54 +0900 Subject: [PATCH 314/632] test(browser-session): expose GCC specs execution provenance gap --- ...er_session_linker_driver_specs_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_linker_driver_specs_contract.py diff --git a/tests/test_browser_session_linker_driver_specs_contract.py b/tests/test_browser_session_linker_driver_specs_contract.py new file mode 100644 index 000000000..a2ce8ca53 --- /dev/null +++ b/tests/test_browser_session_linker_driver_specs_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerDriverSpecsContractTests(unittest.TestCase): + """Keep GCC driver spec-file authority inside the reviewed Browser Session execution boundary.""" + + def _workspace_with_flags(self, rustflags: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f'[build]\nrustflags = {rustflags}\n', + encoding="utf-8", + ) + return root + + def test_repository_driver_specs_file_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-specs=tools/review-bypass.specs"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_driver_specs_inside_link_args_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["--codegen=link-args=-specs=tools/review-bypass.specs"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_driver_link_argument_remains_allowed(self) -> None: + root = self._workspace_with_flags('["-C", "link-arg=-pthread"]') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 29dd6bb6548d4e003970743e7602753c3505cf83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:11:36 +0900 Subject: [PATCH 315/632] fix(browser-session): fail closed on GCC specs driver authority --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index c0f3d5568..1ee21effc 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -22,9 +22,11 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: - """Return whether one compiler-driver argument can re-select a linker executable.""" + """Return whether one compiler-driver argument can re-select driver execution authority.""" if argument.startswith("@"): return True + if argument.startswith("-specs="): + return True if argument.startswith("-fuse-ld="): return True return argument == "-B" or argument.startswith("-B") From c9440bdb2b1b610b6a52024a176acddbcd5e6cc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:12:35 +0900 Subject: [PATCH 316/632] docs(browser-session): trace GCC specs execution authority --- ...rowser-session-cargo-compiler-authority.md | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 442ef3e84..99ff343b2 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -16,15 +16,17 @@ GCC also expands `@file` response-file arguments in place, recursively. A reposi GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process, for example with `-C link-arg=-Wl,-plugin,tools/review-bypass-linker.so`. Repeated `-C link-arg=-Xlinker` forms can express the same plugin request across separate codegen options. That is executable-code provenance, not an ordinary linker tuning flag. +GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, and the spec-file format can override named spec strings or include other spec files. A repository-owned Cargo `link-arg=-specs=...` can therefore alter which subprocesses or switches the nominal linker driver uses without changing the visible `linker=` setting. Until the specs content and every included file are immutable reviewed provenance, this is another extension of execution authority rather than an ordinary linker argument. + If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables or dynamically load modeled linker code are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, and other arbitrary linker-argument effects remain separate review surfaces. +- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables, alter modeled driver subprocess authority, or dynamically load modeled linker code are not rejected merely because they occur under `[build]` or `[target]`. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, linker scripts, and other arbitrary linker-argument effects remain separate review surfaces. ## RED @@ -60,6 +62,8 @@ Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves a third linker-drive Commit `6e94d6b86ac96a677ad6195c818bc94f2199e77b` preserves a linker-plugin execution RED. The hostile fixtures pass GNU-compatible driver forwarding forms for `-plugin`: `-Wl,-plugin,tools/review-bypass-linker.so` and a repeated `-Xlinker` sequence. The predecessor parser inspected linker executable reselection but accepted both forms even though GNU `ld` dynamically loads the named plugin into the linking process. +Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves a GCC driver specs RED. Hostile fixtures pass `-specs=tools/review-bypass.specs` through both `link-arg` and `link-args`. The predecessor parser accepted these arguments even though GCC specs can override the driver rules that determine which subprocesses are invoked and which switches they receive. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -80,6 +84,8 @@ Commit `92ce887209f58b33ecd478ee09212bda70890894` keeps the same parser and clas Commit `0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a` extends the same Cargo flag parser again instead of introducing a second linker grammar. It preserves linker-driver arguments across multiple `link-arg` codegen options, decodes the GNU-compatible `-Wl,` and `--for-linker=` forwarding forms, and recognizes `-Xlinker` followed by `-plugin`/`--plugin`. Only the plugin-loading forms fail closed; non-plugin forwarding such as `-Wl,--as-needed`, `-Xlinker --as-needed`, and `--for-linker=--as-needed` remains permitted. Commit `c42b7aa3550cc87f79acf46e3e1732e5098cddb4` adds hostile and control coverage for all three forwarding spellings. +Commit `29dd6bb6548d4e003970743e7602753c3505cf83` keeps the same driver-argument classifier and adds joined `-specs=` as fail-closed execution authority. It does not parse or allowlist a specs file because GCC permits specs to override subprocess command construction and to include other specs files; a future relaxation therefore requires recursive exact-tree containment and subprocess-semantics provenance rather than path review alone. The hostile specs contract also retains an unrelated `-pthread` control. + The modeled fail-closed execution-authority surfaces are now: - `build.rustc` @@ -94,22 +100,23 @@ The modeled fail-closed execution-authority surfaces are now: - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) - Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding requests linker plugin loading through `-Wl,`, `--for-linker=`, or `-Xlinker` +- Cargo-owned rustc/rustdoc flag surfaces when the nominal GCC-compatible driver receives `-specs=` and can replace its subprocess/switch rules -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection and dynamically loaded linker code are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-executable-selection flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, or response-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments, behavior, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, and dynamically loaded linker code are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-execution-authority flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, response-file, or specs-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments/includes, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, or dynamically load repository-selected linker plugin code while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, dynamically load repository-selected linker plugin code, or replace GCC driver subprocess/switch rules through `-specs=` while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin mechanisms, non-`-B` driver/tool search-path mechanisms, linker scripts, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the linker-plugin RED→repair chain together with the retained executable-selection contracts. +1. Obtain independent current-head review of the GCC-specs RED→repair chain together with the retained linker-plugin and executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. 4. Review linker scripts, non-GNU plugin loading, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. -5. If any blocked executable override, response file, or linker plugin is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. +5. If any blocked executable override, response file, linker plugin, or specs file is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -123,6 +130,8 @@ Free Software Foundation. (n.d.). *Link options*. *Using the GNU Compiler Collec Free Software Foundation. (n.d.). *Overall options*. *Using the GNU Compiler Collection (GCC)*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html +Free Software Foundation. (n.d.). *Spec files*. *GNU Compiler Collection (GCC) Internals*. Retrieved September 18, 2026, from https://gcc.gnu.org/onlinedocs/gccint/Spec-Files.html + Free Software Foundation. (n.d.). *Plugins*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Plugins.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html From eb1ef86f6456e99bd581599b4bb474f9fa48fc02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:15:35 +0900 Subject: [PATCH 317/632] test(browser-session): preserve split GCC specs bypass RED --- ...browser_session_linker_driver_specs_contract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_browser_session_linker_driver_specs_contract.py b/tests/test_browser_session_linker_driver_specs_contract.py index a2ce8ca53..129c8c4c8 100644 --- a/tests/test_browser_session_linker_driver_specs_contract.py +++ b/tests/test_browser_session_linker_driver_specs_contract.py @@ -54,6 +54,20 @@ def test_repository_driver_specs_inside_link_args_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_split_driver_specs_file_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-specs", "-C", "link-arg=tools/review-bypass.specs"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_split_driver_specs_inside_link_args_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["--codegen=link-args=-specs tools/review-bypass.specs"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_driver_link_argument_remains_allowed(self) -> None: root = self._workspace_with_flags('["-C", "link-arg=-pthread"]') authority._assert_no_repository_cargo_compiler_execution_overrides(root) From 688680c92e48bc5ad999327c46b366e5d27eeb5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:16:28 +0900 Subject: [PATCH 318/632] fix(browser-session): close split GCC specs driver bypass --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 1ee21effc..b14698f2e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -25,7 +25,7 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: """Return whether one compiler-driver argument can re-select driver execution authority.""" if argument.startswith("@"): return True - if argument.startswith("-specs="): + if argument == "-specs" or argument.startswith("-specs="): return True if argument.startswith("-fuse-ld="): return True From 4903b7334d9fa5488d4a3b3286c8f2fc8120e85f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:17:21 +0900 Subject: [PATCH 319/632] docs(browser-session): trace split GCC specs driver repair --- ...rowser-session-cargo-compiler-authority.md | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 99ff343b2..9c70f6b7b 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -16,7 +16,7 @@ GCC also expands `@file` response-file arguments in place, recursively. A reposi GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process, for example with `-C link-arg=-Wl,-plugin,tools/review-bypass-linker.so`. Repeated `-C link-arg=-Xlinker` forms can express the same plugin request across separate codegen options. That is executable-code provenance, not an ordinary linker tuning flag. -GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, and the spec-file format can override named spec strings or include other spec files. A repository-owned Cargo `link-arg=-specs=...` can therefore alter which subprocesses or switches the nominal linker driver uses without changing the visible `linker=` setting. Until the specs content and every included file are immutable reviewed provenance, this is another extension of execution authority rather than an ordinary linker argument. +GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, while the driver also accepts the option and its file argument as separate argv tokens. The spec-file format can override named spec strings or include other spec files. Repository-owned Cargo `link-arg`/`link-args` can therefore alter which subprocesses or switches the nominal linker driver uses with either `-specs=file` or `-specs file`, without changing the visible `linker=` setting. Until the specs content and every included file are immutable reviewed provenance, both spellings extend execution authority rather than acting as ordinary linker tuning. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. @@ -40,19 +40,7 @@ Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added hostile Cargo-owned rust Commit `b0489000709243b27a9c849d2cada8e5fadd254e` added the corresponding Cargo-owned rustdocflags path using split and compact `-C linker=` forms. The predecessor helper inspected only rustflags, so both fixtures preserved a distinct structural RED for repository-selected rustdoc linker authority. -Focused review of exact `a3434ebf7d8169a6cf5276d983d4a0c978a9720a` then found that the shared flag parser recognized only `-C` spellings even though Rust also documents the long `--codegen` interface. Commit `053b6cacd0d7d36dc619165aada99c1ac485b043` preserves that review finding as structural RED across both rustc and rustdoc flag surfaces: - -```toml -[build] -rustflags = ["--codegen", "linker=tools/review-bypass-linker"] -rustdocflags = ["--codegen", "linker=tools/review-bypass-linker"] - -[target.'cfg(unix)'] -rustflags = "--codegen=linker=tools/review-bypass-linker" -rustdocflags = "--codegen=linker=tools/review-bypass-linker" -``` - -The predecessor parser accepted all four long-form cases. +Focused review of exact `a3434ebf7d8169a6cf5276d983d4a0c978a9720a` then found that the shared flag parser recognized only `-C` spellings even though Rust also documents the long `--codegen` interface. Commit `053b6cacd0d7d36dc619165aada99c1ac485b043` preserves that review finding as structural RED across both rustc and rustdoc flag surfaces. Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves a distinct linker-driver RED. It adds Cargo-owned rustflags using both split `-C link-arg=-fuse-ld=review-bypass-linker` and compact `--codegen=link-args=-fuse-ld=review-bypass-linker`. The predecessor parser only recognized the `linker=` codegen option and therefore allowed a repository flag to change the actual linker selected by a C compiler driver while the reviewed Rust source closure and nominal driver remained unchanged. @@ -62,7 +50,9 @@ Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves a third linker-drive Commit `6e94d6b86ac96a677ad6195c818bc94f2199e77b` preserves a linker-plugin execution RED. The hostile fixtures pass GNU-compatible driver forwarding forms for `-plugin`: `-Wl,-plugin,tools/review-bypass-linker.so` and a repeated `-Xlinker` sequence. The predecessor parser inspected linker executable reselection but accepted both forms even though GNU `ld` dynamically loads the named plugin into the linking process. -Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves a GCC driver specs RED. Hostile fixtures pass `-specs=tools/review-bypass.specs` through both `link-arg` and `link-args`. The predecessor parser accepted these arguments even though GCC specs can override the driver rules that determine which subprocesses are invoked and which switches they receive. +Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves the first GCC driver specs RED. Hostile fixtures pass joined `-specs=tools/review-bypass.specs` through both `link-arg` and `link-args`. The predecessor parser accepted these arguments even though GCC specs can override the driver rules that determine which subprocesses are invoked and which switches they receive. + +Focused review of exact `c9440bdb2b1b610b6a52024a176acddbcd5e6cc5` identified a remaining P1: the repair recognized only joined `-specs=` and still accepted split `-specs `. Commit `eb1ef86f6456e99bd581599b4bb474f9fa48fc02` preserves that review finding with hostile fixtures for both repeated `link-arg` and one `link-args` string. These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. @@ -86,6 +76,8 @@ Commit `0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a` extends the same Cargo flag pa Commit `29dd6bb6548d4e003970743e7602753c3505cf83` keeps the same driver-argument classifier and adds joined `-specs=` as fail-closed execution authority. It does not parse or allowlist a specs file because GCC permits specs to override subprocess command construction and to include other specs files; a future relaxation therefore requires recursive exact-tree containment and subprocess-semantics provenance rather than path review alone. The hostile specs contract also retains an unrelated `-pthread` control. +Commit `688680c92e48bc5ad999327c46b366e5d27eeb5f` repairs the review-discovered split-form bypass by treating the exact driver token `-specs` as execution authority in addition to joined `-specs=`. Because `link-arg` values are accumulated before classification and `link-args` values are tokenized into the same list, both repeated `link-arg=-specs` + `link-arg=` and `link-args=-specs ` now fail closed without introducing another parser. A lone malformed `-specs` token also fails closed rather than being treated as ordinary linker tuning. + The modeled fail-closed execution-authority surfaces are now: - `build.rustc` @@ -100,19 +92,19 @@ The modeled fail-closed execution-authority surfaces are now: - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) - Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding requests linker plugin loading through `-Wl,`, `--for-linker=`, or `-Xlinker` -- Cargo-owned rustc/rustdoc flag surfaces when the nominal GCC-compatible driver receives `-specs=` and can replace its subprocess/switch rules +- Cargo-owned rustc/rustdoc flag surfaces when the nominal GCC-compatible driver receives joined `-specs=` or split `-specs ` and can replace its subprocess/switch rules A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, and dynamically loaded linker code are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-execution-authority flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, response-file, or specs-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments/includes, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, dynamically load repository-selected linker plugin code, or replace GCC driver subprocess/switch rules through `-specs=` while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, dynamically load repository-selected linker plugin code, or replace GCC driver subprocess/switch rules through either spelling of `-specs` while the reviewed Rust source closure remained unchanged. It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin mechanisms, non-`-B` driver/tool search-path mechanisms, linker scripts, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the GCC-specs RED→repair chain together with the retained linker-plugin and executable-selection contracts. +1. Obtain independent current-head review of the split GCC-specs RED→repair chain together with the retained linker-plugin and executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. 4. Review linker scripts, non-GNU plugin loading, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. From b93c8fe3991eeac767388b6476d4af83f932916b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:18:59 +0900 Subject: [PATCH 320/632] docs(changelog): include GCC specs execution authority hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46459618b..5da3c5372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed -- Failed closed on Git-owned Cargo linker execution extensions that re-select executables through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, or opaque `@file`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. +- Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, or GCC `-specs=` / `-specs `) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. - Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. - Failed closed on production Cargo build surfaces (`build.rs`, enabled `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies) until generated-source provenance is explicitly modeled; `package.build = false` remains the explicit no-build-script form. - Repaired Browser Session repository contracts so custom `[lib].path` / `[[bin]].path` fixtures reach the intended target-source assertion, recovery-custody accessor prohibitions inspect only `BoundBrowserSessionRecovery` inherent impls, and settlement ordering verifies delegated fact selection before proof I/O rather than duplicating selector internals. From 2ae24ca196e54c59068ce0ff243bbbef2dcc0d83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:28:48 +0900 Subject: [PATCH 321/632] test(browser-session): cover GCC linker driver wrapper authority --- ..._session_linker_driver_wrapper_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_linker_driver_wrapper_contract.py diff --git a/tests/test_browser_session_linker_driver_wrapper_contract.py b/tests/test_browser_session_linker_driver_wrapper_contract.py new file mode 100644 index 000000000..3af4a4e3b --- /dev/null +++ b/tests/test_browser_session_linker_driver_wrapper_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerDriverWrapperContractTests(unittest.TestCase): + """Keep GCC subcommand wrapper authority inside the reviewed Browser Session execution boundary.""" + + def _workspace_with_flags(self, rustflags: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f'[build]\nrustflags = {rustflags}\n', + encoding="utf-8", + ) + return root + + def test_repository_split_driver_wrapper_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-wrapper", "-C", "link-arg=tools/review-wrapper,--args"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_driver_wrapper_inside_link_args_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["--codegen=link-args=-wrapper tools/review-wrapper,--args"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_driver_link_argument_remains_allowed(self) -> None: + root = self._workspace_with_flags('["-C", "link-arg=-pthread"]') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 9aaa60019e2b8e150bd6e2f6d5f475442786825e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:29:33 +0900 Subject: [PATCH 322/632] fix(browser-session): fail closed on GCC driver wrappers --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b14698f2e..13952a24a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -27,6 +27,8 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: return True if argument == "-specs" or argument.startswith("-specs="): return True + if argument == "-wrapper": + return True if argument.startswith("-fuse-ld="): return True return argument == "-B" or argument.startswith("-B") From fba74ccf0fbc2475592e550c63ad685aa1f30f88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:30:23 +0900 Subject: [PATCH 323/632] docs(browser-session): trace GCC linker driver wrapper authority --- ...session-linker-driver-wrapper-authority.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/traceability/browser-session-linker-driver-wrapper-authority.md diff --git a/docs/traceability/browser-session-linker-driver-wrapper-authority.md b/docs/traceability/browser-session-linker-driver-wrapper-authority.md new file mode 100644 index 000000000..e174fb682 --- /dev/null +++ b/docs/traceability/browser-session-linker-driver-wrapper-authority.md @@ -0,0 +1,40 @@ +# Browser Session GCC linker-driver wrapper authority + +Status: Draft source-level traceability for PR #317. This document does not claim hosted exact-head repository/security GREEN. + +## Problem + +The Browser Session Cargo execution-authority contract already fails closed on direct linker selection, driver program-search replacement, opaque driver response files, modeled linker plugin loading, and GCC specs files. GCC has a separate driver-level execution extension: `-wrapper` runs every GCC subcommand under a caller-selected wrapper program. + +Rust documents `-C link-arg` and `-C link-args` as arguments appended to the linker invocation. On the GNU-compatible Unix path used by the current contract, that invocation may be the GCC driver. A Git-owned Cargo configuration can therefore pass `-wrapper tools/review-wrapper,--args` through rustc/rustdoc linker flags while keeping the nominal compiler driver, reviewed Rust source closure, and visible Cargo `linker` setting unchanged. The wrapper becomes part of the build/link execution TCB. + +GCC's current official documentation states that `-wrapper` invokes all subcommands under the named wrapper program. A Debian GCC 14.2.0 `gcc -###` reproduction with `-wrapper /bin/echo,--` showed the link subprocess rendered as `/bin/echo -- .../collect2 ...`, confirming that the option reaches the link-stage subcommand path. This runtime realism probe is supplemental evidence only; it is not OriginWeave exact-head CI. + +## RED + +Commit `2ae24ca196e54c59068ce0ff243bbbef2dcc0d83` adds hostile repository fixtures for both Cargo encodings used by the existing shared parser: + +- repeated `-C link-arg=-wrapper` plus `-C link-arg=tools/review-wrapper,--args`; +- one `--codegen=link-args=-wrapper tools/review-wrapper,--args` value. + +The predecessor classifier accepted exact `-wrapper` because it only recognized `@file`, GCC `-specs`, `-fuse-ld=`, and driver `-B` as driver execution-authority surfaces. The fixtures consume the canonical Browser Session Cargo compiler-authority contract rather than duplicating Cargo topology discovery. + +## Decision and repair + +Commit `9aaa60019e2b8e150bd6e2f6d5f475442786825e` changes only the existing `_linker_driver_argument_selects_executable` classifier. Exact GCC `-wrapper` now fails closed before the subsequent wrapper-program argument can extend link execution authority. Existing handling for response files, specs, linker selection, program-search prefixes, linker plugins, and ordinary non-execution linker arguments is unchanged. + +The contract intentionally rejects the option token itself even when malformed or missing its operand. Allowing a repository-owned `-wrapper` requires a separate immutable wrapper identity/provenance contract on the same reviewed tree; documenting a path string is not sufficient. + +## Boundary and residual risk + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the shared Git-owned Cargo execution-authority classifier. +- This repair does not authorize a wrapper executable or expand the future BiDi adapter allowlist. +- Environment-owned tool variables and image/toolchain selection remain CI/supply-chain owner concerns. +- Non-GNU driver mechanisms, linker scripts, other arbitrary linker arguments, and external toolchain provenance remain separate review surfaces. + +## Primary references + +Free Software Foundation. (2026). *Using the GNU Compiler Collection (GCC): Overall options*. https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html + +Rust Project Developers. (2026). *The rustc book: Codegen options*. https://doc.rust-lang.org/rustc/codegen-options/index.html From b4a61882e6f1443b6d32267a6e617832709de614 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:31:24 +0900 Subject: [PATCH 324/632] docs(changelog): record GCC driver wrapper containment --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da3c5372..8ade5dee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed -- Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, or GCC `-specs=` / `-specs `) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. +- Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, GCC `-specs=` / `-specs `, or GCC `-wrapper`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. - Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. - Failed closed on production Cargo build surfaces (`build.rs`, enabled `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies) until generated-source provenance is explicitly modeled; `package.build = false` remains the explicit no-build-script form. - Repaired Browser Session repository contracts so custom `[lib].path` / `[[bin]].path` fixtures reach the intended target-source assertion, recovery-custody accessor prohibitions inspect only `BoundBrowserSessionRecovery` inherent impls, and settlement ordering verifies delegated fact selection before proof I/O rather than duplicating selector internals. From 43375ef80b1bcf6a0421f900a2f9cbf519741849 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:39:23 +0900 Subject: [PATCH 325/632] test(browser-session): cover GNU linker script provenance --- ...ssion_linker_script_provenance_contract.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_browser_session_linker_script_provenance_contract.py diff --git a/tests/test_browser_session_linker_script_provenance_contract.py b/tests/test_browser_session_linker_script_provenance_contract.py new file mode 100644 index 000000000..a9dfa1620 --- /dev/null +++ b/tests/test_browser_session_linker_script_provenance_contract.py @@ -0,0 +1,76 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerScriptProvenanceContractTests(unittest.TestCase): + """Keep GNU linker-script input authority inside the reviewed Browser Session build boundary.""" + + def _workspace_with_flags(self, rustflags: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + tools = root / "tools" + tools.mkdir() + (tools / "review-bypass.ld").write_text( + "INPUT(tools/review-bypass-object.o)\n", + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f'[build]\nrustflags = {rustflags}\n', + encoding="utf-8", + ) + return root + + def test_repository_driver_linker_script_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-Ttools/review-bypass.ld"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_forwarded_linker_script_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-arg=-Wl,--script=tools/review-bypass.ld"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_xlinker_script_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-args=-Xlinker -T -Xlinker tools/review-bypass.ld"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_driver_link_argument_remains_allowed(self) -> None: + root = self._workspace_with_flags('["-C", "link-arg=-pthread"]') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 8975224e86ea5ef9821d168efeaafa20702ec659 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:40:08 +0900 Subject: [PATCH 326/632] fix(browser-session): fail closed on linker script input authority --- ...ssion_cargo_compiler_authority_contract.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 13952a24a..a974d1636 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -19,6 +19,7 @@ ) TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) +LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -41,6 +42,13 @@ def _linker_option_loads_plugin(argument: str) -> bool: return argument.startswith(("-plugin=", "--plugin=")) +def _linker_option_selects_script(argument: str) -> bool: + """Return whether one linker option selects a script that can introduce link inputs.""" + if argument in LINKER_SCRIPT_OPTIONS: + return True + return (argument.startswith("-T") and len(argument) > 2) or argument.startswith("--script=") + + def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: """Return direct-linker arguments encoded by a single compiler-driver forwarding option.""" if argument.startswith("-Wl,"): @@ -51,26 +59,31 @@ def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: - """Return whether driver arguments can replace the linker or load executable linker code.""" + """Return whether driver arguments can replace tools, extend link inputs, or load linker code.""" for index, argument in enumerate(arguments): if _linker_driver_argument_selects_executable(argument): return True + if _linker_option_selects_script(argument): + return True if any( - _linker_option_loads_plugin(forwarded) + _linker_option_loads_plugin(forwarded) or _linker_option_selects_script(forwarded) for forwarded in _forwarded_linker_arguments(argument) ): return True if ( argument == "-Xlinker" and index + 1 < len(arguments) - and _linker_option_loads_plugin(arguments[index + 1]) + and ( + _linker_option_loads_plugin(arguments[index + 1]) + or _linker_option_selects_script(arguments[index + 1]) + ) ): return True return False def _flags_select_linker(value: object) -> bool: - """Return whether Cargo-owned rustc/rustdoc flags select or extend linker execution code.""" + """Return whether Cargo-owned rustc/rustdoc flags extend linker execution or input authority.""" if isinstance(value, str): arguments = value.split() elif isinstance(value, list) and all(isinstance(argument, str) for argument in value): From c0204371a1d8e06e3b68ed07437d5581f9a94762 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 06:40:25 +0900 Subject: [PATCH 327/632] docs(browser-session): trace GNU linker script provenance --- ...rowser-session-linker-script-provenance.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/traceability/browser-session-linker-script-provenance.md diff --git a/docs/traceability/browser-session-linker-script-provenance.md b/docs/traceability/browser-session-linker-script-provenance.md new file mode 100644 index 000000000..b16be9dc2 --- /dev/null +++ b/docs/traceability/browser-session-linker-script-provenance.md @@ -0,0 +1,49 @@ +# Browser Session GNU linker-script provenance + +Status: Draft source-level traceability for PR #317. This document does not claim hosted exact-head repository/security GREEN. + +## Problem + +The Browser Session Cargo execution-authority contract already fails closed on direct linker selection, GCC driver program-search replacement, opaque response files, dynamically loaded linker plugins, GCC specs files, and GCC driver wrappers. A separate GNU-compatible input-provenance surface remained: linker scripts selected through `-T` / `--script`. + +Rust documents `-C link-arg` and `-C link-args` as arguments appended to the linker invocation. GCC documents `-T script` as selecting a linker script on systems using the GNU linker. GNU ld additionally defines `INPUT(file, ...)` and `GROUP(file, ...)` commands that introduce named files into the link as if they had appeared on the command line. A Git-owned Cargo flag can therefore select a repository or external linker script that adds object/archive inputs outside the reviewed Rust production-source closure without changing the nominal Cargo target or linker executable. + +This is source/input provenance rather than a claim that the script itself is an executable program. It belongs in the same fail-closed compiler/linker authority boundary because the resulting binary can contain code selected by that script. + +## RED + +Commit `43375ef80b1bcf6a0421f900a2f9cbf519741849` adds a hostile repository fixture whose reviewed Cargo configuration selects `tools/review-bypass.ld`; the script contains `INPUT(tools/review-bypass-object.o)`. The fixture covers three encodings consumed by the existing shared parser: + +- driver-level `-Ttools/review-bypass.ld`; +- forwarded GNU ld `-Wl,--script=tools/review-bypass.ld`; +- split `-Xlinker -T -Xlinker tools/review-bypass.ld` inside `link-args`. + +The predecessor classifier did not recognize linker-script selection, so these cases were source-semantic RED. The fixture imports the canonical Browser Session Cargo compiler-authority contract rather than reproducing Cargo workspace/package discovery. + +## Decision and repair + +Commit `8975224e86ea5ef9821d168efeaafa20702ec659` extends only the existing linker-argument classifier. The guard now fails closed on: + +- direct `-T`, joined `-T`, `--script`, and `--script=`; +- the same script-selection options forwarded through `-Wl,` or `--for-linker=`; +- script-selection option tokens passed through `-Xlinker`. + +Existing executable-selection, response-file, specs, wrapper, plugin, and ordinary non-authorizing linker-argument behavior remains in the same parser. A normal `-pthread` control remains allowed. + +The contract deliberately rejects script selection before trying to parse or allowlist script contents. Relaxation requires a same-tree immutable linker-script/input provenance contract that recursively proves every script-selected object/archive/search surface and remains valid for the exact qualified linker implementation. A path-only allowlist is insufficient because GNU ld scripts can introduce additional inputs and search behavior. + +## Boundary and residual risk + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the shared Git-owned Cargo compiler/linker authority classifier. +- This repair does not widen the future BiDi adapter allowlist or authorize any linker script, object, archive, or external path. +- Build-script-generated linker arguments remain prohibited by the separate production build-surface contract until generated-source/build provenance is explicitly modeled. +- Non-GNU linker script/control-file mechanisms, implicit linker scripts supplied as ordinary input files, command-line `-L`/library selection, target/toolchain-supplied scripts, and external native dependencies remain separate review surfaces. + +## Primary references + +Free Software Foundation. (2026). *Using the GNU Compiler Collection (GCC): Link options*. https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html + +Free Software Foundation. (2026). *The GNU linker*. https://sourceware.org/binutils/docs/ld.pdf + +Rust Project Developers. (2026). *The rustc book: Codegen options*. https://doc.rust-lang.org/rustc/codegen-options/index.html From b0af9ed4251d4195c4f71893b9550cdbb38ab0b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:02:08 +0900 Subject: [PATCH 328/632] test(browser-session): close forwarded linker response files --- ...ssion_cargo_compiler_authority_contract.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index a974d1636..11c0e9485 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -49,6 +49,11 @@ def _linker_option_selects_script(argument: str) -> bool: return (argument.startswith("-T") and len(argument) > 2) or argument.startswith("--script=") +def _linker_option_uses_response_file(argument: str) -> bool: + """Return whether a direct-linker argument delegates parsing to an opaque response file.""" + return argument.startswith("@") + + def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: """Return direct-linker arguments encoded by a single compiler-driver forwarding option.""" if argument.startswith("-Wl,"): @@ -66,7 +71,9 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: if _linker_option_selects_script(argument): return True if any( - _linker_option_loads_plugin(forwarded) or _linker_option_selects_script(forwarded) + _linker_option_loads_plugin(forwarded) + or _linker_option_selects_script(forwarded) + or _linker_option_uses_response_file(forwarded) for forwarded in _forwarded_linker_arguments(argument) ): return True @@ -76,6 +83,7 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: and ( _linker_option_loads_plugin(arguments[index + 1]) or _linker_option_selects_script(arguments[index + 1]) + or _linker_option_uses_response_file(arguments[index + 1]) ) ): return True @@ -322,6 +330,19 @@ def test_repository_long_target_rustdocflags_linker_override_fails_closed(self) with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_forwarded_linker_response_file_fails_closed(self) -> None: + for forwarded in ( + "-Wl,@tools/review-bypass-linker.rsp", + "--for-linker=@tools/review-bypass-linker.rsp", + "-Xlinker @tools/review-bypass-linker.rsp", + ): + with self.subTest(forwarded=forwarded): + root = self._workspace_with_config( + f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + _assert_no_repository_cargo_compiler_execution_overrides(root) + def test_non_linker_selecting_link_arg_remains_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' @@ -352,4 +373,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 6bf7116b8792c8cd260a9af695a83874a4d2056c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:03:00 +0900 Subject: [PATCH 329/632] docs(browser-session): currentize linker provenance authority --- ...rowser-session-cargo-compiler-authority.md | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 9c70f6b7b..87adf7b2d 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -12,10 +12,12 @@ Rust documents `-C` and `--codegen` as equivalent short and long codegen-option The same nominal-driver boundary has another executable-selection path. Rust documents that Unix-like targets commonly use `cc` or `clang` as the linker driver. GCC documents `-Bprefix` as the first search prefix for driver subprograms including `ld`; if the requested program is found there, that executable is run before the standard prefixes or `PATH` lookup. Because rustc `link-arg` and `link-args` append arguments to the linker invocation, repository-owned `-C link-arg=-B...` or `--codegen=link-args=-B ...` can therefore redirect the compiler driver to a different linker executable without changing the nominal driver or using `-fuse-ld=`. -GCC also expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can therefore hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. The response file may itself include another response file. Until response-file contents, containment, recursion, and executable-selection semantics are represented as reviewed provenance, a driver-level `@file` argument is an opaque extension of the execution boundary and must fail closed. +GCC also expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can therefore hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. The response file may itself include another response file. GNU-compatible forwarding does not make that provenance safe: `-Wl,@file`, `--for-linker=@file`, and `-Xlinker @file` delegate parsing to the linker after the compiler-driver surface. Until response-file contents, containment, recursion, and effective driver/linker semantics are represented as reviewed provenance, both driver-level and forwarded linker-level `@file` arguments are opaque extensions of the execution/input boundary and must fail closed. GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process, for example with `-C link-arg=-Wl,-plugin,tools/review-bypass-linker.so`. Repeated `-C link-arg=-Xlinker` forms can express the same plugin request across separate codegen options. That is executable-code provenance, not an ordinary linker tuning flag. +GNU linker scripts are also an explicit native-input authority surface, not future work. `-T`/`--script` can select a script whose `INPUT(...)`/`GROUP(...)` directives add object or archive inputs outside the reviewed Rust production-source closure. The shared Cargo linker-argument classifier therefore fails closed on direct script selection and on `-Wl,`, `--for-linker=`, or `-Xlinker` forwarding of those options. This explicit-script rule is separate from implicit linker-script interpretation of ordinary positional inputs, which remains a residual review surface. + GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, while the driver also accepts the option and its file argument as separate argv tokens. The spec-file format can override named spec strings or include other spec files. Repository-owned Cargo `link-arg`/`link-args` can therefore alter which subprocesses or switches the nominal linker driver uses with either `-specs=file` or `-specs file`, without changing the visible `linker=` setting. Until the specs content and every included file are immutable reviewed provenance, both spellings extend execution authority rather than acting as ordinary linker tuning. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. @@ -23,10 +25,10 @@ If any of these settings enters a reviewed Browser Session production workspace ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, linker script, native input, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables, alter modeled driver subprocess authority, or dynamically load modeled linker code are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, linker scripts, and other arbitrary linker-argument effects remain separate review surfaces. +- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables, alter modeled driver subprocess authority, dynamically load modeled linker code, or select a modeled GNU linker script are not rejected merely because they occur under `[build]` or `[target]`. +- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, non-GNU control-file mechanisms, implicit linker scripts supplied as ordinary positional inputs, and other arbitrary linker-argument effects remain separate review surfaces. ## RED @@ -54,6 +56,10 @@ Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves the first GCC driver Focused review of exact `c9440bdb2b1b610b6a52024a176acddbcd5e6cc5` identified a remaining P1: the repair recognized only joined `-specs=` and still accepted split `-specs `. Commit `eb1ef86f6456e99bd581599b4bb474f9fa48fc02` preserves that review finding with hostile fixtures for both repeated `link-arg` and one `link-args` string. +Commit `43375ef80b1bcf6a0421f900a2f9cbf519741849` preserves explicit GNU linker-script native-input REDs using direct `-T...`, forwarded `-Wl,--script=...`, and split `-Xlinker -T -Xlinker ...`. The predecessor classifier modeled executable/plugin/specs authority but did not reject scripts that can inject `INPUT(...)` or `GROUP(...)` native objects and archives. + +Focused review of exact `c0204371a1d8e06e3b68ed07437d5581f9a94762` found one remaining response-file gap after the explicit script repair: direct driver `@file` already failed closed, but response files forwarded to the linker through `-Wl,`, `--for-linker=`, or `-Xlinker` did not. Commit `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` preserves and repairs that current-head review finding with hostile coverage for all three forwarding spellings. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -78,7 +84,11 @@ Commit `29dd6bb6548d4e003970743e7602753c3505cf83` keeps the same driver-argument Commit `688680c92e48bc5ad999327c46b366e5d27eeb5f` repairs the review-discovered split-form bypass by treating the exact driver token `-specs` as execution authority in addition to joined `-specs=`. Because `link-arg` values are accumulated before classification and `link-args` values are tokenized into the same list, both repeated `link-arg=-specs` + `link-arg=` and `link-args=-specs ` now fail closed without introducing another parser. A lone malformed `-specs` token also fails closed rather than being treated as ordinary linker tuning. -The modeled fail-closed execution-authority surfaces are now: +Commit `8975224e86ea5ef9821d168efeaafa20702ec659` extends the shared classifier to explicit GNU linker-script selection instead of introducing a second native-input parser. Direct `-T`/`--script`, GNU-compatible `-Wl,`/`--for-linker=` forwarding, and split `-Xlinker` script selection fail closed because scripts can inject unreviewed native inputs. This is already enforced and covered by `tests/test_browser_session_linker_script_provenance_contract.py`; it is not deferred follow-up work. + +Commit `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` closes the review-discovered linker-level response-file bypass in that same classifier. Every argument decoded from `-Wl,` or `--for-linker=` and the argument following `-Xlinker` is now checked for leading `@`. Ordinary forwarded controls such as `-Wl,-Bsymbolic` and `-Xlinker --as-needed` remain outside this fail-closed rule. + +The modeled fail-closed execution/input-authority surfaces are now: - `build.rustc` - `build.rustc-wrapper` @@ -91,24 +101,26 @@ The modeled fail-closed execution-authority surfaces are now: - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable - Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) +- Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding passes an opaque linker response file through `-Wl,`, `--for-linker=`, or `-Xlinker` - Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding requests linker plugin loading through `-Wl,`, `--for-linker=`, or `-Xlinker` +- Cargo-owned rustc/rustdoc flag surfaces when direct or GNU-forwarded `-T` / `--script` selects a linker script that can extend native input authority - Cargo-owned rustc/rustdoc flag surfaces when the nominal GCC-compatible driver receives joined `-specs=` or split `-specs ` and can replace its subprocess/switch rules -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, and dynamically loaded linker code are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-execution-authority flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, response-file, or specs-file paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments/includes, behavior, or provenance. +A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, dynamically loaded linker code, and explicit native-input control files are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-execution-authority flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, response-file, specs-file, or linker-script paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments/includes, native inputs, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned execution-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, dynamically load repository-selected linker plugin code, or replace GCC driver subprocess/switch rules through either spelling of `-specs` while the reviewed Rust source closure remained unchanged. +The repair closes modeled Git-owned execution/input-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, delegate opaque response-file parsing to the linker through GNU forwarding, dynamically load repository-selected linker plugin code, inject native objects/archives through an explicitly selected GNU linker script, or replace GCC driver subprocess/switch rules through either spelling of `-specs` while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin mechanisms, non-`-B` driver/tool search-path mechanisms, linker scripts, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin/control-file mechanisms, non-`-B` driver/tool search-path mechanisms, implicit linker scripts supplied as ordinary positional inputs, external native libraries selected through search paths, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. ## Acceptance and follow-up -1. Obtain independent current-head review of the split GCC-specs RED→repair chain together with the retained linker-plugin and executable-selection contracts. +1. Obtain independent current-head review of the linker-script and forwarded-response-file repair together with the retained GCC-specs, linker-plugin, and executable-selection contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review linker scripts, non-GNU plugin loading, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. -5. If any blocked executable override, response file, linker plugin, or specs file is ever required, replace the fail-closed rule only with an explicit design covering immutable executable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. Review non-GNU control-file mechanisms, implicit script inputs, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. +5. If any blocked executable override, response file, linker plugin, specs file, or linker script is ever required, replace the fail-closed rule only with an explicit design covering immutable executable/input identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -126,8 +138,10 @@ Free Software Foundation. (n.d.). *Spec files*. *GNU Compiler Collection (GCC) I Free Software Foundation. (n.d.). *Plugins*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Plugins.html +Free Software Foundation. (n.d.). *Scripts*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Scripts.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html -The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/ +The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/ \ No newline at end of file From a43b7692f46af169e029ebd78dbd0ce0785f7d54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:04:07 +0900 Subject: [PATCH 330/632] test(browser-session): harden recovery impl surface extraction --- ...test_browser_session_lifecycle_contract.py | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 75d5df238..83b9ca7a8 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -14,20 +14,53 @@ def _inherent_impl_surface(source: str, type_name: str) -> str: """Return every inherent impl segment for one Rust type without matching sibling request types.""" - starts = [match.start() for match in re.finditer(r"(?m)^impl<", source)] + starts = [match.start() for match in re.finditer(r"(?m)^impl(?=\s|<)", source)] starts.append(len(source)) segments: list[str] = [] for index, start in enumerate(starts[:-1]): segment = source[start : starts[index + 1]] - header = segment.split("{", 1)[0] - if re.search(rf"\b{re.escape(type_name)}<[^>]+>\s*$", header.strip()): - segments.append(segment) + header = segment.split("{", 1)[0].strip() + target = re.search( + rf"\b{re.escape(type_name)}\s*<[^{{}};]+>\s*$", + header, + ) + if target is None: + continue + if re.search(r"\bfor\s*$", header[: target.start()]): + continue + segments.append(segment) return "\n".join(segments) class BrowserSessionLifecycleContractTests(unittest.TestCase): """Keep presentation mutation authority in an explicit Browser Session domain.""" + def test_recovery_surface_extractor_covers_concrete_and_spaced_generic_impls(self) -> None: + """Raw recovery accessors must not hide in concrete or spaced-generic inherent impls.""" + + hostile = """ +impl BoundBrowserSessionRecovery { + pub fn browser_session(&self) {} +} +impl

BoundBrowserSessionRecovery

{ + pub const fn port(&self) {} +} +impl

RecoveryContextOperationRequest

{ + pub fn browser_session(&self) {} +} +impl

RecoveryInspection for BoundBrowserSessionRecovery

{ + fn port(&self) {} +} +""" + surface = _inherent_impl_surface(hostile, "BoundBrowserSessionRecovery") + + self.assertIn("impl BoundBrowserSessionRecovery", surface) + self.assertIn("impl

BoundBrowserSessionRecovery

", surface) + self.assertIn("pub fn browser_session(&self)", surface) + self.assertIn("pub const fn port(&self)", surface) + self.assertNotIn("RecoveryContextOperationRequest", surface) + self.assertNotIn("RecoveryInspection for BoundBrowserSessionRecovery", surface) + def test_browser_session_is_an_independent_workspace_boundary(self) -> None: """Browser Session authority must not be hidden in a driver adapter.""" @@ -398,4 +431,4 @@ def test_architecture_decision_and_traceability_are_explicit(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 292f62f78b5f10548e7a9745f657fc732d4e8e77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:28:42 +0900 Subject: [PATCH 331/632] test(browser-session): preserve linker toolchain selection RED --- ...ion_linker_toolchain_selection_contract.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_browser_session_linker_toolchain_selection_contract.py diff --git a/tests/test_browser_session_linker_toolchain_selection_contract.py b/tests/test_browser_session_linker_toolchain_selection_contract.py new file mode 100644 index 000000000..4757c03ff --- /dev/null +++ b/tests/test_browser_session_linker_toolchain_selection_contract.py @@ -0,0 +1,65 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerToolchainSelectionContractTests(unittest.TestCase): + """Keep rustc-managed linker binary selection inside the reviewed build boundary.""" + + def _workspace_with_flags(self, rustflags: str, *, target_scoped: bool = False) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + table = '[target.x86_64-unknown-linux-gnu]' if target_scoped else '[build]' + (cargo / "config.toml").write_text( + f'{table}\nrustflags = {rustflags}\n', + encoding="utf-8", + ) + return root + + def test_repository_link_self_contained_linker_selection_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["-C", "link-self-contained=+linker"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_target_linker_features_selection_fails_closed(self) -> None: + root = self._workspace_with_flags( + '["--codegen=linker-features=+lld"]', + target_scoped=True, + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_codegen_option_remains_allowed(self) -> None: + root = self._workspace_with_flags('["-C", "debuginfo=1"]') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 7523b391b1e296a88115d9411619ac22ad2d0a42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:29:56 +0900 Subject: [PATCH 332/632] fix(browser-session): fail closed on rustc-managed linker selection --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 11c0e9485..7f69b6bcd 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -113,6 +113,10 @@ def _flags_select_linker(value: object) -> bool: continue if option.startswith("linker="): return True + if option == "link-self-contained" or option.startswith("link-self-contained="): + return True + if option == "linker-features" or option.startswith("linker-features="): + return True if option.startswith("link-arg="): linker_driver_arguments.append(option.partition("=")[2]) elif option.startswith("link-args="): From 0a19a1f60e606217f95e115650c99a6fc790ceb7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:30:18 +0900 Subject: [PATCH 333/632] docs(browser-session): trace rustc linker toolchain selection --- ...ession-rustc-linker-toolchain-selection.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/traceability/browser-session-rustc-linker-toolchain-selection.md diff --git a/docs/traceability/browser-session-rustc-linker-toolchain-selection.md b/docs/traceability/browser-session-rustc-linker-toolchain-selection.md new file mode 100644 index 000000000..ecb0aeac2 --- /dev/null +++ b/docs/traceability/browser-session-rustc-linker-toolchain-selection.md @@ -0,0 +1,44 @@ +# Browser Session rustc linker toolchain selection traceability + +Status: Draft contract evidence on PR #317. This document does not claim executable repository/security GREEN. + +## Problem + +The existing Cargo compiler-authority contract already fails closed when Git-owned Cargo configuration selects `build.rustc`, rustc wrappers, `build.rustdoc`, target `linker`/`runner`, `-C linker=...`, and modeled driver/linker execution extensions. That still left two rustc-owned codegen surfaces that can change the linker binary without spelling `linker=`. + +The rustc codegen reference states that `link-self-contained` controls whether linking uses Rust-shipped libraries and objects and also controls which binary is used for the linker. The same reference documents `linker-features`; on `x86_64-unknown-linux-gnu`, the `lld` feature controls whether rustc tries to use an LLD linker and may select either the system linker or the self-contained `rust-lld` path. These are execution-authority choices, not ordinary optimization flags. + +A repository-owned `.cargo/config.toml` or `.cargo/config` can supply both options through `[build].rustflags`, matching `[target].rustflags`, and the equivalent rustdoc flag surfaces. Therefore a reviewed source tree can keep the nominal Cargo target and omit `target.<...>.linker` while still changing the effective linker executable selected by rustc. + +Primary reference: Rust Project, *The rustc book: Codegen options*, `link-self-contained`, `linker-features`, and `linker` sections: https://doc.rust-lang.org/rustc/codegen-options/ + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the shared parser for Git-owned Cargo compiler/linker execution authority. This slice does not introduce a second Cargo flag scanner. +- No self-contained linker, system LLD, `rust-lld`, custom linker feature policy, or future adapter implementation is pre-authorized by this repair. +- Environment/toolchain-owned linker selection remains a CI/supply-chain concern. This contract covers Git-owned Cargo configuration only. +- Ordinary codegen options that do not select or alter the modeled compiler/linker execution boundary remain allowed. + +## RED + +Commit `292f62f78b5f10548e7a9745f657fc732d4e8e77` adds `tests/test_browser_session_linker_toolchain_selection_contract.py` with two hostile Cargo configurations: + +- `[build].rustflags = ["-C", "link-self-contained=+linker"]` +- target-scoped `rustflags = ["--codegen=linker-features=+lld"]` + +The predecessor shared parser recognized direct `linker=` and flag-derived driver/linker extensions but ignored both rustc-managed linker-selection options, so the hostile contract preserves the source-semantic RED. A `debuginfo=1` control remains allowed. + +## Decision and repair + +Commit `7523b391b1e296a88115d9411619ac22ad2d0a42` extends the existing `_flags_select_linker` parser only. `link-self-contained` and `linker-features`, in exact or `=` form after `-C` / `--codegen`, are classified as execution-authority changes and fail closed through the existing Cargo compiler-authority error path. + +The repair intentionally does not parse target-specific linker heuristics or infer which LLD binary would be chosen. A future exception requires an explicit, versioned toolchain provenance contract that identifies the exact linker artifact, selection semantics, integrity evidence, and rollback behavior on the same reviewed tree. + +## Security effect + +Git-owned Cargo configuration can no longer switch from the reviewed nominal linker path to rustc-managed self-contained/system LLD selection through these codegen options without an explicit Browser Session provenance contract. The boundary remains deterministic and fail closed while unrelated codegen options stay available. + +## Residual surfaces + +This repair does not claim full linker-input provenance. Positional native inputs and implicit linker scripts, library/search-path selection, non-GNU control-file mechanisms, command-line `cargo rustc` flags, environment variables, rustup/toolchain composition, and target-specific external toolchain scripts remain separate review surfaces. From 750650e87c2e3c01655b14a11cc6da3e1fd33b13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:33:20 +0900 Subject: [PATCH 334/632] test(browser-session): preserve rustc auxiliary tool selection RED --- ...ion_linker_toolchain_selection_contract.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/test_browser_session_linker_toolchain_selection_contract.py b/tests/test_browser_session_linker_toolchain_selection_contract.py index 4757c03ff..0cd0f7911 100644 --- a/tests/test_browser_session_linker_toolchain_selection_contract.py +++ b/tests/test_browser_session_linker_toolchain_selection_contract.py @@ -15,7 +15,7 @@ class BrowserSessionLinkerToolchainSelectionContractTests(unittest.TestCase): - """Keep rustc-managed linker binary selection inside the reviewed build boundary.""" + """Keep rustc-managed linker and auxiliary binary selection inside the reviewed build boundary.""" def _workspace_with_flags(self, rustflags: str, *, target_scoped: bool = False) -> pathlib.Path: directory = tempfile.TemporaryDirectory() @@ -41,20 +41,25 @@ def _workspace_with_flags(self, rustflags: str, *, target_scoped: bool = False) ) return root - def test_repository_link_self_contained_linker_selection_fails_closed(self) -> None: - root = self._workspace_with_flags( - '["-C", "link-self-contained=+linker"]' - ) + def _assert_fails_closed(self, rustflags: str, *, target_scoped: bool = False) -> None: + root = self._workspace_with_flags(rustflags, target_scoped=target_scoped) with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_repository_link_self_contained_linker_selection_fails_closed(self) -> None: + self._assert_fails_closed('["-C", "link-self-contained=+linker"]') + def test_repository_target_linker_features_selection_fails_closed(self) -> None: - root = self._workspace_with_flags( + self._assert_fails_closed( '["--codegen=linker-features=+lld"]', target_scoped=True, ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_linker_flavor_selection_fails_closed(self) -> None: + self._assert_fails_closed('["-C", "linker-flavor=ld.lld"]') + + def test_repository_dlltool_executable_selection_fails_closed(self) -> None: + self._assert_fails_closed('["--codegen=dlltool=tools/review-bypass-dlltool"]') def test_unrelated_codegen_option_remains_allowed(self) -> None: root = self._workspace_with_flags('["-C", "debuginfo=1"]') From e9a3e85110153a667bf3dd6025bf57d08975ead6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:33:53 +0900 Subject: [PATCH 335/632] fix(browser-session): fail closed on rustc linker flavor and dlltool --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 7f69b6bcd..110fa5e06 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -117,6 +117,10 @@ def _flags_select_linker(value: object) -> bool: return True if option == "linker-features" or option.startswith("linker-features="): return True + if option == "linker-flavor" or option.startswith("linker-flavor="): + return True + if option == "dlltool" or option.startswith("dlltool="): + return True if option.startswith("link-arg="): linker_driver_arguments.append(option.partition("=")[2]) elif option.startswith("link-args="): From d2a0f587386294f8b2793d0e40ec6792f1fa5dd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 07:34:08 +0900 Subject: [PATCH 336/632] docs(browser-session): trace rustc auxiliary tool selection --- ...ession-rustc-linker-toolchain-selection.md | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/traceability/browser-session-rustc-linker-toolchain-selection.md b/docs/traceability/browser-session-rustc-linker-toolchain-selection.md index ecb0aeac2..5824dd69a 100644 --- a/docs/traceability/browser-session-rustc-linker-toolchain-selection.md +++ b/docs/traceability/browser-session-rustc-linker-toolchain-selection.md @@ -4,40 +4,39 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -The existing Cargo compiler-authority contract already fails closed when Git-owned Cargo configuration selects `build.rustc`, rustc wrappers, `build.rustdoc`, target `linker`/`runner`, `-C linker=...`, and modeled driver/linker execution extensions. That still left two rustc-owned codegen surfaces that can change the linker binary without spelling `linker=`. +The existing Cargo compiler-authority contract already fails closed when Git-owned Cargo configuration selects `build.rustc`, rustc wrappers, `build.rustdoc`, target `linker`/`runner`, `-C linker=...`, and modeled driver/linker execution extensions. Rustc still exposes codegen options that can change which linker or auxiliary executable is invoked without spelling `linker=`. -The rustc codegen reference states that `link-self-contained` controls whether linking uses Rust-shipped libraries and objects and also controls which binary is used for the linker. The same reference documents `linker-features`; on `x86_64-unknown-linux-gnu`, the `lld` feature controls whether rustc tries to use an LLD linker and may select either the system linker or the self-contained `rust-lld` path. These are execution-authority choices, not ordinary optimization flags. +The rustc codegen reference states that `link-self-contained` controls whether linking uses Rust-shipped libraries and objects and also controls which binary is used for the linker. The same reference documents `linker-features`; on `x86_64-unknown-linux-gnu`, the `lld` feature controls whether rustc tries to use an LLD linker and may select either the system linker or the self-contained `rust-lld` path. `linker-flavor` determines which linker flavor rustc uses and, when no explicit `-C linker` is supplied, determines the linker to use. The `dlltool` option accepts a path to the dlltool executable rustc invokes for `windows-gnu` raw-dylib import-library generation. These are execution-authority choices, not ordinary optimization flags. -A repository-owned `.cargo/config.toml` or `.cargo/config` can supply both options through `[build].rustflags`, matching `[target].rustflags`, and the equivalent rustdoc flag surfaces. Therefore a reviewed source tree can keep the nominal Cargo target and omit `target.<...>.linker` while still changing the effective linker executable selected by rustc. +A repository-owned `.cargo/config.toml` or `.cargo/config` can supply these options through `[build].rustflags`, matching `[target].rustflags`, and equivalent rustdoc flag surfaces. Therefore a reviewed source tree can keep the nominal Cargo target and omit `target.<...>.linker` while still changing the effective native tool selected by rustc. -Primary reference: Rust Project, *The rustc book: Codegen options*, `link-self-contained`, `linker-features`, and `linker` sections: https://doc.rust-lang.org/rustc/codegen-options/ +Primary reference: Rust Project, *The rustc book: Codegen options*, `link-self-contained`, `linker-features`, `linker-flavor`, `dlltool`, and `linker` sections: https://doc.rust-lang.org/rustc/codegen-options/ ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. - `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the shared parser for Git-owned Cargo compiler/linker execution authority. This slice does not introduce a second Cargo flag scanner. -- No self-contained linker, system LLD, `rust-lld`, custom linker feature policy, or future adapter implementation is pre-authorized by this repair. -- Environment/toolchain-owned linker selection remains a CI/supply-chain concern. This contract covers Git-owned Cargo configuration only. -- Ordinary codegen options that do not select or alter the modeled compiler/linker execution boundary remain allowed. +- No self-contained linker, system LLD, `rust-lld`, alternate linker flavor, custom dlltool, custom linker feature policy, or future adapter implementation is pre-authorized by this repair. +- Environment/toolchain-owned native-tool selection remains a CI/supply-chain concern. This contract covers Git-owned Cargo configuration only. +- Ordinary codegen options that do not select or alter the modeled compiler/linker/native-tool execution boundary remain allowed. ## RED -Commit `292f62f78b5f10548e7a9745f657fc732d4e8e77` adds `tests/test_browser_session_linker_toolchain_selection_contract.py` with two hostile Cargo configurations: +Commit `292f62f78b5f10548e7a9745f657fc732d4e8e77` adds `tests/test_browser_session_linker_toolchain_selection_contract.py` with hostile Cargo configurations for `[build].rustflags = ["-C", "link-self-contained=+linker"]` and target-scoped `rustflags = ["--codegen=linker-features=+lld"]`. The predecessor shared parser recognized direct `linker=` and flag-derived driver/linker extensions but ignored both rustc-managed linker-selection options. -- `[build].rustflags = ["-C", "link-self-contained=+linker"]` -- target-scoped `rustflags = ["--codegen=linker-features=+lld"]` - -The predecessor shared parser recognized direct `linker=` and flag-derived driver/linker extensions but ignored both rustc-managed linker-selection options, so the hostile contract preserves the source-semantic RED. A `debuginfo=1` control remains allowed. +Commit `750650e87c2e3c01655b14a11cc6da3e1fd33b13` extends that same hostile contract after the first repair and preserves two additional source-semantic REDs: `-C linker-flavor=ld.lld` and `--codegen=dlltool=tools/review-bypass-dlltool`. Both can alter a native executable selected by rustc without using the already-modeled direct `linker=` setting. The `debuginfo=1` control remains allowed. ## Decision and repair Commit `7523b391b1e296a88115d9411619ac22ad2d0a42` extends the existing `_flags_select_linker` parser only. `link-self-contained` and `linker-features`, in exact or `=` form after `-C` / `--codegen`, are classified as execution-authority changes and fail closed through the existing Cargo compiler-authority error path. -The repair intentionally does not parse target-specific linker heuristics or infer which LLD binary would be chosen. A future exception requires an explicit, versioned toolchain provenance contract that identifies the exact linker artifact, selection semantics, integrity evidence, and rollback behavior on the same reviewed tree. +Commit `e9a3e85110153a667bf3dd6025bf57d08975ead6` extends the same shared parser to `linker-flavor` and `dlltool`. It does not infer a safe flavor or allowlist a repository-selected dlltool path; either surface requires a separate explicit provenance contract before it can enter the reviewed production build boundary. + +The repair intentionally does not parse target-specific linker heuristics or infer which LLD/dlltool binary would be chosen. A future exception requires an explicit, versioned toolchain provenance contract that identifies the exact native-tool artifact, selection semantics, integrity evidence, and rollback behavior on the same reviewed tree. ## Security effect -Git-owned Cargo configuration can no longer switch from the reviewed nominal linker path to rustc-managed self-contained/system LLD selection through these codegen options without an explicit Browser Session provenance contract. The boundary remains deterministic and fail closed while unrelated codegen options stay available. +Git-owned Cargo configuration can no longer switch from the reviewed nominal linker path to rustc-managed self-contained/system LLD selection, another inferred linker flavor, or a repository-selected dlltool through these codegen options without an explicit Browser Session provenance contract. The boundary remains deterministic and fail closed while unrelated codegen options stay available. ## Residual surfaces From 657428dd607c2a384f95865ff59caba704a899d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:01:48 +0900 Subject: [PATCH 337/632] test(browser-session): expose native library input provenance gap --- ...r_session_native_library_input_contract.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_browser_session_native_library_input_contract.py diff --git a/tests/test_browser_session_native_library_input_contract.py b/tests/test_browser_session_native_library_input_contract.py new file mode 100644 index 000000000..ad1fb9146 --- /dev/null +++ b/tests/test_browser_session_native_library_input_contract.py @@ -0,0 +1,65 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMPILER_AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_cargo_compiler_authority_contract", + COMPILER_AUTHORITY_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionNativeLibraryInputContractTests(unittest.TestCase): + """Keep Git-owned native library/search-path inputs inside the reviewed Browser Session TCB.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def test_git_owned_native_library_search_path_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-L", "native=tools/review-bypass-native"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_git_owned_native_library_link_request_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-l", "static:+whole-archive=review_bypass_native"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_codegen_flag_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "debuginfo=1"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From d3a1790c50c393e0328854a2dd7fe4d9aaf3d5c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:03:53 +0900 Subject: [PATCH 338/632] fix(browser-session): fail closed on Git-owned external link inputs --- ...ssion_cargo_compiler_authority_contract.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 110fa5e06..fd85e35bd 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -90,13 +90,32 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: return False +def _flag_arguments(value: object) -> list[str]: + """Normalize one Cargo rustflags value without inventing shell semantics.""" + if isinstance(value, str): + return value.split() + if isinstance(value, list) and all(isinstance(argument, str) for argument in value): + return value + return [] + + +def _flags_extend_external_link_inputs(value: object) -> bool: + """Return whether Git-owned rustc flags widen external crate or native-library inputs.""" + arguments = _flag_arguments(value) + for argument in arguments: + if argument in {"-L", "-l"}: + return True + if argument.startswith("-L") and len(argument) > 2: + return True + if argument.startswith("-l") and len(argument) > 2 and not argument.startswith("--"): + return True + return False + + def _flags_select_linker(value: object) -> bool: """Return whether Cargo-owned rustc/rustdoc flags extend linker execution or input authority.""" - if isinstance(value, str): - arguments = value.split() - elif isinstance(value, list) and all(isinstance(argument, str) for argument in value): - arguments = value - else: + arguments = _flag_arguments(value) + if not arguments: return False linker_driver_arguments: list[str] = [] @@ -130,9 +149,9 @@ def _flags_select_linker(value: object) -> bool: def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: - """Reject Git-owned Cargo settings that replace Rust-tool or target executables.""" + """Reject Git-owned Cargo settings that replace Rust tools or widen external link inputs.""" # The trusted-adapter boundary remains the single writer for production package/source topology - # and dependency-source overrides. This contract owns Cargo-selected execution authority. + # and dependency-source overrides. This contract owns Cargo-selected execution/input authority. boundary._production_package_manifests(root) root_resolved = root.resolve() @@ -161,6 +180,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if isinstance(build, dict): if _flags_select_linker(build.get("rustflags")): build_configured.append("rustflags:codegen linker") + if _flags_extend_external_link_inputs(build.get("rustflags")): + build_configured.append("rustflags:external link input") if _flags_select_linker(build.get("rustdocflags")): build_configured.append("rustdocflags:codegen linker") @@ -173,6 +194,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) if _flags_select_linker(settings.get("rustflags")): configured.append("rustflags:codegen linker") + if _flags_extend_external_link_inputs(settings.get("rustflags")): + configured.append("rustflags:external link input") if _flags_select_linker(settings.get("rustdocflags")): configured.append("rustdocflags:codegen linker") if configured: @@ -381,4 +404,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From e934b3c17261ab26bb13b4f02417416c4202d344 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:04:40 +0900 Subject: [PATCH 339/632] docs(browser-session): trace native library input authority --- ...-session-native-library-input-authority.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/traceability/browser-session-native-library-input-authority.md diff --git a/docs/traceability/browser-session-native-library-input-authority.md b/docs/traceability/browser-session-native-library-input-authority.md new file mode 100644 index 000000000..7c1dcab3d --- /dev/null +++ b/docs/traceability/browser-session-native-library-input-authority.md @@ -0,0 +1,56 @@ +# Browser Session native-library input authority + +## Problem + +The Browser Session repository contract already constrains Git-owned Cargo settings that replace Rust tools, target runners/linkers, compiler-driver executables, linker plugins, response files, linker scripts, and rustc-managed native tools. That boundary did not constrain top-level rustc `-L` and `-l` flags supplied through repository-owned Cargo `rustflags`. + +`-L` changes the search path for external crates and libraries, including native libraries. `-l` asks rustc to link a named native library and supports static archives, dynamic libraries, frameworks, and modifiers such as `+whole-archive`. A reviewed Rust source closure therefore did not prove the final native input closure when a Git-owned `.cargo/config.toml` or `.cargo/config` could add either flag. + +## Constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The compiler-authority contract may consume that topology and constrain Git-owned compiler/input authority, but it must not reimplement workspace/package discovery. + +This slice applies only to repository-owned Cargo `rustflags`. It does not claim authority over environment-injected `RUSTFLAGS`, direct `cargo rustc -- ...` arguments, build-system flags outside the repository, or external toolchain configuration. Those require their canonical CI/supply-chain owner or a separate reviewed contract. + +## RED → repair + +RED `657428dd607c2a384f95865ff59caba704a899d9` adds hostile contract cases for: + +- `-L native=tools/review-bypass-native`, which widens native-library search to a repository-selected path; and +- `-l static:+whole-archive=review_bypass_native`, which asks rustc to link a native static archive as a complete archive. + +The predecessor exact allowed both settings. + +Repair `d3a1790c50c393e0328854a2dd7fe4d9aaf3d5c4` keeps production topology ownership unchanged, normalizes the existing Cargo flag representation once, and extends the compiler-authority contract so Git-owned `rustflags` fail closed on `-L`/`-l` in both `[build]` and `[target.<...>]` settings. Unrelated codegen flags remain allowed. + +## Decision + +Until external/native input provenance is modeled as a versioned reviewed contract, repository-owned Cargo configuration must not widen rustc external-library search paths or request additional native libraries for Browser Session production packages. + +This is an input-provenance rule, not a claim that `-L` or `-l` are unsafe Rust features. They are rejected here because their resolved artifacts are outside the current exact-head source and artifact review closure. + +## Residual surfaces + +The following remain separate review surfaces and are not pre-authorized by this decision: + +- `--extern` and other direct precompiled-Rust dependency injection; +- positional object/archive inputs forwarded through `-C link-arg` / `link-args`; +- environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc` trailing arguments; +- sysroot/rustup/toolchain composition and custom target specifications; +- non-GNU platform-specific native input/control-file mechanisms. + +A future allowlist must identify the exact artifact path, digest/provenance, producer, target triple, linkage kind, and reproducible build evidence on the same reviewed exact tree. A path-only allowlist is insufficient. + +## Primary evidence + +Rust Project. (2026). *Command-line arguments: `-L` and `-l`*. The rustc book. https://doc.rust-lang.org/nightly/rustc/command-line-arguments.html + +The rustc documentation states that `-L` adds a path searched for external crates and libraries and can be scoped to `dependency`, `crate`, `native`, `framework`, or `all`. It also states that `-l` links the generated crate to a specified native library and supports static archives, dynamic libraries, frameworks, and linking modifiers including `+whole-archive`. + +Rust Project. (2026). *Build scripts*. The Cargo book. https://doc.rust-lang.org/cargo/reference/build-scripts.html + +Cargo documents the corresponding native-library and search-path concepts through `cargo::rustc-link-lib` and `cargo::rustc-link-search`, which are passed to rustc as `-l` and `-L` semantics. Build-script authority itself remains separately fail-closed in the Browser Session Cargo build-surface contract. + +## Verification state + +The RED and repair commits are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN: current-head hosted repository/security workflows and independent current-head review must still complete on the reconciled lineage before merge or release readiness can be claimed. From ad5090dfb1e6de9eb1e2875365fa2b65ad37a5d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:11:50 +0900 Subject: [PATCH 340/632] test(browser-session): expose forwarded native input provenance gap --- ...r_session_native_library_input_contract.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_native_library_input_contract.py b/tests/test_browser_session_native_library_input_contract.py index ad1fb9146..b0e3b3335 100644 --- a/tests/test_browser_session_native_library_input_contract.py +++ b/tests/test_browser_session_native_library_input_contract.py @@ -40,19 +40,40 @@ def _workspace_with_config(self, config_text: str) -> pathlib.Path: (cargo / "config.toml").write_text(config_text, encoding="utf-8") return root + def _assert_input_override_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_git_owned_native_library_search_path_fails_closed(self) -> None: - root = self._workspace_with_config( + self._assert_input_override_fails_closed( '[build]\nrustflags = ["-L", "native=tools/review-bypass-native"]\n' ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_git_owned_native_library_link_request_fails_closed(self) -> None: - root = self._workspace_with_config( + self._assert_input_override_fails_closed( '[build]\nrustflags = ["-l", "static:+whole-archive=review_bypass_native"]\n' ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_codegen_link_arg_native_library_search_path_fails_closed(self) -> None: + self._assert_input_override_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Ltools/review-bypass-native"]\n' + ) + + def test_codegen_link_args_native_library_search_path_fails_closed(self) -> None: + self._assert_input_override_fails_closed( + '[build]\nrustflags = ["--codegen", "link-args=-L tools/review-bypass-native"]\n' + ) + + def test_target_codegen_link_arg_native_library_request_fails_closed(self) -> None: + self._assert_input_override_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-lreview_bypass_native\"]\n" + ) + + def test_target_codegen_link_args_native_library_request_fails_closed(self) -> None: + self._assert_input_override_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"--codegen=link-args=-l review_bypass_native\"]\n" + ) def test_unrelated_codegen_flag_remains_allowed(self) -> None: root = self._workspace_with_config( From a0f8525b57837ac119ef7b2af9c1daa759ef12f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:14:39 +0900 Subject: [PATCH 341/632] fix(browser-session): reject forwarded native link inputs --- ...ssion_cargo_compiler_authority_contract.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index fd85e35bd..43f84598b 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -35,6 +35,15 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: return argument == "-B" or argument.startswith("-B") +def _linker_argument_extends_external_inputs(argument: str) -> bool: + """Return whether one compiler/linker-driver argument widens external library inputs.""" + if argument in {"-L", "-l"}: + return True + if argument.startswith("-L") and len(argument) > 2: + return True + return argument.startswith("-l") and len(argument) > 2 and not argument.startswith("--") + + def _linker_option_loads_plugin(argument: str) -> bool: """Return whether one direct linker option requests dynamically loaded plugin code.""" if argument in LINKER_PLUGIN_OPTIONS: @@ -68,12 +77,15 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: for index, argument in enumerate(arguments): if _linker_driver_argument_selects_executable(argument): return True + if _linker_argument_extends_external_inputs(argument): + return True if _linker_option_selects_script(argument): return True if any( _linker_option_loads_plugin(forwarded) or _linker_option_selects_script(forwarded) or _linker_option_uses_response_file(forwarded) + or _linker_argument_extends_external_inputs(forwarded) for forwarded in _forwarded_linker_arguments(argument) ): return True @@ -84,6 +96,7 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: _linker_option_loads_plugin(arguments[index + 1]) or _linker_option_selects_script(arguments[index + 1]) or _linker_option_uses_response_file(arguments[index + 1]) + or _linker_argument_extends_external_inputs(arguments[index + 1]) ) ): return True @@ -101,15 +114,7 @@ def _flag_arguments(value: object) -> list[str]: def _flags_extend_external_link_inputs(value: object) -> bool: """Return whether Git-owned rustc flags widen external crate or native-library inputs.""" - arguments = _flag_arguments(value) - for argument in arguments: - if argument in {"-L", "-l"}: - return True - if argument.startswith("-L") and len(argument) > 2: - return True - if argument.startswith("-l") and len(argument) > 2 and not argument.startswith("--"): - return True - return False + return any(_linker_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) def _flags_select_linker(value: object) -> bool: From a370bad3ce3f56b9ba7f0ff0ded589e97608dcdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:15:05 +0900 Subject: [PATCH 342/632] docs(browser-session): trace forwarded native link inputs --- ...-session-native-library-input-authority.md | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-native-library-input-authority.md b/docs/traceability/browser-session-native-library-input-authority.md index 7c1dcab3d..9c69c09f9 100644 --- a/docs/traceability/browser-session-native-library-input-authority.md +++ b/docs/traceability/browser-session-native-library-input-authority.md @@ -2,10 +2,12 @@ ## Problem -The Browser Session repository contract already constrains Git-owned Cargo settings that replace Rust tools, target runners/linkers, compiler-driver executables, linker plugins, response files, linker scripts, and rustc-managed native tools. That boundary did not constrain top-level rustc `-L` and `-l` flags supplied through repository-owned Cargo `rustflags`. +The Browser Session repository contract already constrains Git-owned Cargo settings that replace Rust tools, target runners/linkers, compiler-driver executables, linker plugins, response files, linker scripts, and rustc-managed native tools. That boundary initially did not constrain top-level rustc `-L` and `-l` flags supplied through repository-owned Cargo `rustflags`. `-L` changes the search path for external crates and libraries, including native libraries. `-l` asks rustc to link a named native library and supports static archives, dynamic libraries, frameworks, and modifiers such as `+whole-archive`. A reviewed Rust source closure therefore did not prove the final native input closure when a Git-owned `.cargo/config.toml` or `.cargo/config` could add either flag. +A first repair closed those top-level rustc forms but left an equivalent driver path open: rustc `-C link-arg` and `-C link-args` append arguments to the linker invocation. On Unix-like targets rustc commonly uses `cc` or `clang` as the linker driver, so `link-arg=-L...`, `link-args=-L ...`, `link-arg=-l...`, and `link-args=-l ...` can widen the same native-library search/input closure without using top-level rustc `-L`/`-l` syntax. + ## Constraint `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The compiler-authority contract may consume that topology and constrain Git-owned compiler/input authority, but it must not reimplement workspace/package discovery. @@ -21,20 +23,24 @@ RED `657428dd607c2a384f95865ff59caba704a899d9` adds hostile contract cases for: The predecessor exact allowed both settings. -Repair `d3a1790c50c393e0328854a2dd7fe4d9aaf3d5c4` keeps production topology ownership unchanged, normalizes the existing Cargo flag representation once, and extends the compiler-authority contract so Git-owned `rustflags` fail closed on `-L`/`-l` in both `[build]` and `[target.<...>]` settings. Unrelated codegen flags remain allowed. +Repair `d3a1790c50c393e0328854a2dd7fe4d9aaf3d5c4` keeps production topology ownership unchanged, normalizes the existing Cargo flag representation once, and extends the compiler-authority contract so Git-owned top-level `rustflags` fail closed on `-L`/`-l` in both `[build]` and `[target.<...>]` settings. Unrelated codegen flags remain allowed. + +Focused review of exact `e934b3c17261ab26bb13b4f02417416c4202d344` then found the forwarded-driver equivalent. RED `ad5090dfb1e6de9eb1e2875365fa2b65ad37a5d9` adds hostile build- and target-scoped cases for compact and split `-C link-arg` / `--codegen=link-args` forms that forward `-L` or `-l` into the linker driver. + +Repair `a0f8525b57837ac119ef7b2af9c1daa759ef12f4` reuses one external-input classifier across top-level rustc flags and the existing linker-driver parser. Direct driver arguments, `-Wl,` / `--for-linker=` forwarded arguments, and the argument following `-Xlinker` now fail closed when they select `-L` or `-l`. Existing response-file, linker-script, plugin, tool-selection, GCC specs/wrapper, and driver-search-path checks remain in the same shared parser. Ordinary non-input linker options such as `-Wl,--as-needed` and `-Wl,-Bsymbolic` remain allowed. ## Decision -Until external/native input provenance is modeled as a versioned reviewed contract, repository-owned Cargo configuration must not widen rustc external-library search paths or request additional native libraries for Browser Session production packages. +Until external/native input provenance is modeled as a versioned reviewed contract, repository-owned Cargo configuration must not widen rustc or compiler-driver external-library search paths or request additional native libraries for Browser Session production packages. -This is an input-provenance rule, not a claim that `-L` or `-l` are unsafe Rust features. They are rejected here because their resolved artifacts are outside the current exact-head source and artifact review closure. +This is an input-provenance rule, not a claim that `-L` or `-l` are unsafe Rust/GCC features. They are rejected here because their resolved artifacts are outside the current exact-head source and artifact review closure. ## Residual surfaces The following remain separate review surfaces and are not pre-authorized by this decision: - `--extern` and other direct precompiled-Rust dependency injection; -- positional object/archive inputs forwarded through `-C link-arg` / `link-args`; +- positional object/archive inputs forwarded through `-C link-arg` / `link-args` that do not use `-L`/`-l`; - environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc` trailing arguments; - sysroot/rustup/toolchain composition and custom target specifications; - non-GNU platform-specific native input/control-file mechanisms. @@ -47,10 +53,18 @@ Rust Project. (2026). *Command-line arguments: `-L` and `-l`*. The rustc book. h The rustc documentation states that `-L` adds a path searched for external crates and libraries and can be scoped to `dependency`, `crate`, `native`, `framework`, or `all`. It also states that `-l` links the generated crate to a specified native library and supports static archives, dynamic libraries, frameworks, and linking modifiers including `+whole-archive`. +Rust Project. (2026). *Codegen options: `link-arg` and `link-args`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ + +The rustc documentation states that `link-arg` appends one extra argument and `link-args` appends multiple extra arguments to the linker invocation. It also states that Unix-like targets commonly use a C compiler as the linker driver. + +Free Software Foundation. (2026). *Link options*. Using the GNU Compiler Collection (GCC). https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html + +GCC documents `-l` as searching and linking the named library and states that the search directories include those added through `-L`. Those driver semantics make forwarded `-L`/`-l` part of native input selection rather than inert linker metadata. + Rust Project. (2026). *Build scripts*. The Cargo book. https://doc.rust-lang.org/cargo/reference/build-scripts.html Cargo documents the corresponding native-library and search-path concepts through `cargo::rustc-link-lib` and `cargo::rustc-link-search`, which are passed to rustc as `-l` and `-L` semantics. Build-script authority itself remains separately fail-closed in the Browser Session Cargo build-surface contract. ## Verification state -The RED and repair commits are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN: current-head hosted repository/security workflows and independent current-head review must still complete on the reconciled lineage before merge or release readiness can be claimed. +Both RED→repair generations are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN: current-head hosted repository/security workflows and independent current-head review must still complete on the reconciled lineage before merge or release readiness can be claimed. From dbd9faa623f01652c2e75904af8bec614c2e6fc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:27:45 +0900 Subject: [PATCH 343/632] test(browser-session): expose Cargo extern provenance gap --- ...t_browser_session_extern_input_contract.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_browser_session_extern_input_contract.py diff --git a/tests/test_browser_session_extern_input_contract.py b/tests/test_browser_session_extern_input_contract.py new file mode 100644 index 000000000..b3997e654 --- /dev/null +++ b/tests/test_browser_session_extern_input_contract.py @@ -0,0 +1,71 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMPILER_AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_cargo_compiler_authority_contract", + COMPILER_AUTHORITY_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionExternInputContractTests(unittest.TestCase): + """Keep Git-owned explicit external-crate inputs inside the reviewed Browser Session TCB.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_extern_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_split_extern_path_fails_closed(self) -> None: + self._assert_extern_input_fails_closed( + '[build]\nrustflags = ["--extern", "review_bypass=tools/libreview_bypass.rlib"]\n' + ) + + def test_target_rustflags_equals_extern_path_fails_closed(self) -> None: + self._assert_extern_input_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"--extern=review_bypass=tools/libreview_bypass.so\"]\n" + ) + + def test_build_rustflags_pathless_extern_fails_closed(self) -> None: + self._assert_extern_input_fails_closed( + '[build]\nrustflags = ["--extern", "review_bypass"]\n' + ) + + def test_unrelated_check_cfg_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["--check-cfg", "cfg(originweave_reviewed)"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 098a596029c0cf339c070604a265593540822956 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:28:38 +0900 Subject: [PATCH 344/632] fix(browser-session): fail closed on Cargo extern inputs --- ..._browser_session_cargo_compiler_authority_contract.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 43f84598b..29da9e5d6 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -44,6 +44,13 @@ def _linker_argument_extends_external_inputs(argument: str) -> bool: return argument.startswith("-l") and len(argument) > 2 and not argument.startswith("--") +def _rustc_argument_extends_external_inputs(argument: str) -> bool: + """Return whether one rustc argument widens external crate or native-library inputs.""" + if argument == "--extern" or argument.startswith("--extern="): + return True + return _linker_argument_extends_external_inputs(argument) + + def _linker_option_loads_plugin(argument: str) -> bool: """Return whether one direct linker option requests dynamically loaded plugin code.""" if argument in LINKER_PLUGIN_OPTIONS: @@ -114,7 +121,7 @@ def _flag_arguments(value: object) -> list[str]: def _flags_extend_external_link_inputs(value: object) -> bool: """Return whether Git-owned rustc flags widen external crate or native-library inputs.""" - return any(_linker_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) + return any(_rustc_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) def _flags_select_linker(value: object) -> bool: From 61053c9cc3ca58f5d812f3ab64660f4e662afdce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 08:29:00 +0900 Subject: [PATCH 345/632] docs(traceability): record Cargo extern input authority --- ...-session-external-crate-input-authority.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/traceability/browser-session-external-crate-input-authority.md diff --git a/docs/traceability/browser-session-external-crate-input-authority.md b/docs/traceability/browser-session-external-crate-input-authority.md new file mode 100644 index 000000000..b1bd7b9b9 --- /dev/null +++ b/docs/traceability/browser-session-external-crate-input-authority.md @@ -0,0 +1,58 @@ +# Browser Session external-crate input authority + +## Problem + +The Browser Session repository contract already derives production Cargo package/source topology from `tests/test_browser_session_trusted_adapter_boundary.py` and constrains repository-owned Cargo compiler/linker execution plus native-library input expansion. That boundary did not constrain rustc `--extern` supplied through Git-owned Cargo `rustflags`. + +rustc documents `--extern` as specifying the name and optional location of a direct external crate. `--extern CRATENAME=PATH` names an exact precompiled crate artifact, while pathless `--extern CRATENAME` makes the crate a candidate from rustc's external-library search path. The crate name is also added to the extern prelude. Repository-owned Cargo configuration could therefore add a precompiled Rust dependency outside the reviewed production source/dependency closure without modifying `Cargo.toml`, the canonical Cargo topology, or the nominal compiler/linker selection. + +This is a provenance gap even when the injected crate is not ultimately linked: the compiler is allowed to resolve and expose an additional direct dependency candidate whose producer, source tree, digest, feature set, target, and build evidence are not represented by the exact reviewed tree. + +## Constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The Cargo compiler-authority contract may consume that topology and constrain repository-owned rustc input authority, but it must not create a second workspace/package/dependency resolver. + +This slice applies only to Git-owned Cargo `rustflags` in `.cargo/config.toml` and `.cargo/config`. Environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS`, direct `cargo rustc -- ...` trailing flags, rustup/sysroot/toolchain composition, and external build-system injection remain separate owner surfaces. + +## RED → repair + +RED `dbd9faa623f01652c2e75904af8bec614c2e6fc9` adds hostile contract cases for: + +- split `--extern review_bypass=tools/libreview_bypass.rlib` in `[build].rustflags`; +- equals-form `--extern=review_bypass=tools/libreview_bypass.so` in target-scoped `rustflags`; and +- pathless `--extern review_bypass`, which can resolve from the external-library search path. + +The predecessor exact `a370bad3ce3f56b9ba7f0ff0ded589e97608dcdf` allowed all three forms because external-input classification covered `-L` / `-l` and linker-forwarded native inputs but not rustc external-crate injection. + +Repair `098a596029c0cf339c070604a265593540822956` keeps production topology ownership unchanged and introduces one rustc-level external-input classifier. It delegates native-library/search-path forms to the existing linker-input classifier and additionally fails closed on `--extern` and `--extern=...`. The linker-driver parser remains unchanged because `--extern` is rustc input authority rather than a linker-driver option. An unrelated `--check-cfg` control remains allowed. + +## Decision + +Until precompiled external-crate provenance is modeled as a versioned reviewed artifact contract, repository-owned Cargo configuration must not use rustc `--extern` to widen Browser Session production-package dependency inputs outside the canonical Cargo dependency/source closure. + +This is not a claim that `--extern` is unsafe. It is rejected at this boundary because neither a pathname nor a crate name proves the artifact's source, producer, target compatibility, features, digest, reproducibility, or review ancestry. + +A future allowlist must bind at minimum the crate identity, exact artifact digest, producer/source revision, rustc/toolchain identity, target triple, crate type, enabled features/configuration, reproducible-build evidence, and consumer contract on the same reviewed exact tree. Path-only approval is insufficient. + +## Residual surfaces + +The following remain separate review surfaces and are not pre-authorized by this decision: + +- positional object/archive inputs forwarded through `-C link-arg` / `link-args`; +- environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc` trailing arguments; +- rustup/sysroot/toolchain composition and custom target specifications; +- target-specific external toolchain scripts and non-GNU native control/input mechanisms. + +## Primary evidence + +Rust Project. (2026). *Command-line arguments: `--extern`*. The rustc book. https://doc.rust-lang.org/nightly/rustc/command-line-arguments.html + +The rustc documentation states that `--extern` specifies the name and location of an external crate for a direct dependency. It accepts `CRATENAME=PATH` and pathless `CRATENAME`, adds the name to the extern prelude, and allows multiple external artifacts for the same crate name. + +Rust Project. (2026). *Extern crate declarations*. The Rust Reference. https://doc.rust-lang.org/reference/items/extern-crates.html + +The Rust Reference defines external-crate dependencies and explains that external crates participate in compile-time resolution and linkage semantics. This supports treating precompiled external-crate injection as dependency/input provenance rather than inert compiler metadata. + +## Verification state + +The RED and repair commits are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN. Current-head hosted repository/security workflows and independent current-head review remain required after lineage reconciliation before merge or release readiness can be claimed. From e547203368da2aec62e2c94ab491eb0749d7d7b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 09:02:16 +0900 Subject: [PATCH 346/632] test(browser-session): expose positional native link input gap --- ...ession_linker_positional_input_contract.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_browser_session_linker_positional_input_contract.py diff --git a/tests/test_browser_session_linker_positional_input_contract.py b/tests/test_browser_session_linker_positional_input_contract.py new file mode 100644 index 000000000..523af421c --- /dev/null +++ b/tests/test_browser_session_linker_positional_input_contract.py @@ -0,0 +1,74 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerPositionalInputContractTests(unittest.TestCase): + """Keep positional native link inputs inside the reviewed Browser Session provenance boundary.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_positional_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_direct_positional_object_link_arg_fails_closed(self) -> None: + self._assert_positional_input_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=tools/review-bypass-object.o"]\n' + ) + + def test_target_positional_archive_link_args_fails_closed(self) -> None: + self._assert_positional_input_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-args=tools/review-bypass-archive.a\"]\n" + ) + + def test_forwarded_positional_object_fails_closed(self) -> None: + for forwarded in ( + "-Wl,tools/review-bypass-object.o", + "--for-linker=tools/review-bypass-object.o", + "-Xlinker tools/review-bypass-object.o", + ): + with self.subTest(forwarded=forwarded): + self._assert_positional_input_fails_closed( + f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' + ) + + def test_option_only_link_arguments_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From e73d221cf28aa25ae6fbd941db95d5d923c7e883 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 09:03:14 +0900 Subject: [PATCH 347/632] fix(browser-session): fail closed on positional native link inputs --- ...ssion_cargo_compiler_authority_contract.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 29da9e5d6..56b68b91e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,6 +20,18 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) +POSITIONAL_LINK_INPUT_SUFFIXES = ( + ".o", + ".obj", + ".lo", + ".a", + ".lib", + ".rlib", + ".so", + ".dylib", + ".bc", + ".res", +) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -70,6 +82,17 @@ def _linker_option_uses_response_file(argument: str) -> bool: return argument.startswith("@") +def _linker_argument_is_positional_native_input(argument: str) -> bool: + """Return whether a positional linker argument names a modeled native object/archive input.""" + if not argument or argument.startswith("-"): + return False + lowered = argument.lower() + filename = lowered.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] + if filename.endswith(POSITIONAL_LINK_INPUT_SUFFIXES): + return True + return ".so." in filename + + def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: """Return direct-linker arguments encoded by a single compiler-driver forwarding option.""" if argument.startswith("-Wl,"): @@ -86,6 +109,8 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: return True if _linker_argument_extends_external_inputs(argument): return True + if _linker_argument_is_positional_native_input(argument): + return True if _linker_option_selects_script(argument): return True if any( @@ -93,6 +118,7 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: or _linker_option_selects_script(forwarded) or _linker_option_uses_response_file(forwarded) or _linker_argument_extends_external_inputs(forwarded) + or _linker_argument_is_positional_native_input(forwarded) for forwarded in _forwarded_linker_arguments(argument) ): return True @@ -104,6 +130,7 @@ def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: or _linker_option_selects_script(arguments[index + 1]) or _linker_option_uses_response_file(arguments[index + 1]) or _linker_argument_extends_external_inputs(arguments[index + 1]) + or _linker_argument_is_positional_native_input(arguments[index + 1]) ) ): return True From 202ba6c92faf7a34d233690c3923680337ec0e65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 09:03:46 +0900 Subject: [PATCH 348/632] docs(browser-session): trace positional native link input authority --- ...ssion-linker-positional-input-authority.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/traceability/browser-session-linker-positional-input-authority.md diff --git a/docs/traceability/browser-session-linker-positional-input-authority.md b/docs/traceability/browser-session-linker-positional-input-authority.md new file mode 100644 index 000000000..d9af3ad4b --- /dev/null +++ b/docs/traceability/browser-session-linker-positional-input-authority.md @@ -0,0 +1,58 @@ +# Browser Session linker positional-input authority + +## Problem + +The Browser Session Cargo compiler-authority contract already rejects repository-owned linker replacement, search-path reselection, response files, plugins, explicit GNU linker scripts, native-library `-L` / `-l` widening, rustc `--extern`, and other modeled execution/input-authority surfaces. It did not constrain positional native object/archive arguments supplied through rustc `-C link-arg` / `link-args`. + +The rustc book states that `link-arg` appends one extra argument to the linker invocation and `link-args` appends multiple arguments. On Unix-like targets rustc commonly invokes a C compiler such as `cc` or `clang` as the linker driver. GNU ld documents non-option arguments as object files or archives to be linked into the output. Consequently, Git-owned Cargo configuration could inject a reviewed-tree-external object/archive into the final Browser Session binary without changing `Cargo.toml`, canonical production-source topology, nominal linker selection, or `-L` / `-l` settings. + +This is a provenance gap: an object/archive pathname alone does not establish the artifact's source revision, producer, digest, target/toolchain identity, reproducibility, or review ancestry. + +## Constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The Cargo compiler-authority contract consumes that topology and may constrain repository-owned linker input authority, but it must not implement a second Cargo workspace/package resolver. + +This slice covers modeled positional native artifacts supplied from Git-owned `.cargo/config.toml` / `.cargo/config` rustflags. It does not claim to parse every linker grammar. In particular, extensionless/non-GNU control files and implicit linker-script inputs remain distinct residual surfaces until they have an authoritative parser/provenance contract. + +## RED → repair + +RED `e547203368da2aec62e2c94ab491eb0749d7d7b5` adds hostile cases for: + +- direct `-C link-arg=tools/review-bypass-object.o`; +- target-scoped `-C link-args=tools/review-bypass-archive.a`; and +- compiler-driver forwarding through `-Wl,tools/review-bypass-object.o`, `--for-linker=tools/review-bypass-object.o`, and `-Xlinker tools/review-bypass-object.o`. + +The predecessor exact `61053c9cc3ca58f5d812f3ab64660f4e662afdce` allowed these forms because its external-input classifier covered `-L`, `-l`, `--extern`, scripts/plugins/response files, and executable reselection but not positional native artifacts. + +Repair `e73d221cf28aa25ae6fbd941db95d5d923c7e883` keeps canonical Cargo topology ownership unchanged and extends the shared linker-argument classifier with a modeled positional-native-input predicate. It rejects common object/archive artifact forms (`.o`, `.obj`, `.lo`, `.a`, `.lib`, `.rlib`, `.so` including versioned `.so.*`, `.dylib`, `.bc`, and `.res`) whether direct or passed through the already modeled linker-forwarding forms. Existing option-only controls such as `-Wl,--as-needed` remain allowed; the repair is not a blanket ban on `link-arg` / `link-args`. + +## Decision + +Repository-owned Cargo flags must fail closed when they add modeled positional native object/archive inputs outside the canonical reviewed source/dependency closure. A future allowlist must bind an artifact to exact digest, producer/source revision, target triple, linker/compiler toolchain identity, build configuration, reproducibility/provenance evidence, and the consuming exact tree. File extension or pathname alone is never approval. + +## Security effect + +The repair prevents a repository configuration change from inserting a prebuilt native object/archive into the Browser Session binary while leaving the Rust source closure and nominal linker selection apparently unchanged. Direct and forwarded variants are classified by the same shared authority code, so `-Wl`, `--for-linker`, and `-Xlinker` do not create parallel provenance policy. + +## Residual surfaces + +The following remain separate review surfaces and are not pre-authorized by this decision: + +- extensionless or otherwise unmodeled positional inputs, including GNU ld implicit-script interpretation; +- non-GNU linker/control-file input mechanisms; +- environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc -- ...` trailing flags; +- rustup/sysroot/toolchain composition, custom target specifications, and target-specific external toolchain scripts. + +## Primary evidence + +Rust Project. (2026). *Codegen options: `link-arg` and `link-args`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ + +The rustc documentation states that `link-arg` appends a single argument and `link-args` appends multiple arguments to the linker invocation. It also explains that Unix-like targets commonly use `cc` or `clang` as the linker driver. + +Free Software Foundation. (2026). *Using ld: Command-line options*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld/Options.html + +GNU ld documents non-option command-line arguments as object files or archives to be linked together. It also documents that unrecognized input-file formats may be interpreted as linker scripts, which is why implicit script/control-file handling remains a separately tracked residual surface rather than being silently declared closed by this artifact-suffix classifier. + +## Verification state + +The RED and minimal repair are structurally present on the active #317 lineage. This dossier does not claim hosted executable GREEN or independent current-head review. Those remain required after lineage reconciliation and exact-head workflow execution before merge or release readiness. \ No newline at end of file From 8f5e49f5fe63d99773d25f13a3ab50648e02ac92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:02:48 +0900 Subject: [PATCH 349/632] test(browser-session): expose extensionless implicit linker-script provenance gap --- ...session_linker_implicit_script_contract.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/test_browser_session_linker_implicit_script_contract.py diff --git a/tests/test_browser_session_linker_implicit_script_contract.py b/tests/test_browser_session_linker_implicit_script_contract.py new file mode 100644 index 000000000..69e864dac --- /dev/null +++ b/tests/test_browser_session_linker_implicit_script_contract.py @@ -0,0 +1,75 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerImplicitScriptContractTests(unittest.TestCase): + """Keep extensionless implicit linker-script inputs inside reviewed provenance.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + tools = root / "tools" + tools.mkdir() + (tools / "review-bypass-input").write_text( + "INPUT(tools/review-bypass-object.o)\n", + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_implicit_script_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_direct_extensionless_implicit_script_link_arg_fails_closed(self) -> None: + self._assert_implicit_script_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=tools/review-bypass-input"]\n' + ) + + def test_forwarded_extensionless_implicit_script_fails_closed(self) -> None: + for forwarded in ( + "-Wl,tools/review-bypass-input", + "--for-linker=tools/review-bypass-input", + "-Xlinker tools/review-bypass-input", + ): + with self.subTest(forwarded=forwarded): + self._assert_implicit_script_fails_closed( + f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' + ) + + def test_option_only_link_arguments_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From cb95d5d37050bb7da70f1782da2e63a9b4583734 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:04:09 +0900 Subject: [PATCH 350/632] fix(browser-session): reject extensionless linker path inputs --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 56b68b91e..501f98652 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -83,9 +83,11 @@ def _linker_option_uses_response_file(argument: str) -> bool: def _linker_argument_is_positional_native_input(argument: str) -> bool: - """Return whether a positional linker argument names a modeled native object/archive input.""" + """Return whether a positional linker argument can name an external native input.""" if not argument or argument.startswith("-"): return False + if "/" in argument or "\\" in argument: + return True lowered = argument.lower() filename = lowered.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] if filename.endswith(POSITIONAL_LINK_INPUT_SUFFIXES): From b03980261159ca90788c1ccf9cc5e750d974976c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:04:49 +0900 Subject: [PATCH 351/632] test(browser-session): preserve bare linker option operands --- .../test_browser_session_linker_implicit_script_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_linker_implicit_script_contract.py b/tests/test_browser_session_linker_implicit_script_contract.py index 69e864dac..a822d9c83 100644 --- a/tests/test_browser_session_linker_implicit_script_contract.py +++ b/tests/test_browser_session_linker_implicit_script_contract.py @@ -64,6 +64,12 @@ def test_forwarded_extensionless_implicit_script_fails_closed(self) -> None: f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' ) + def test_bare_linker_option_operand_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_option_only_link_arguments_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' From 19dae976f1dcfdf261ded04c43cade986ade7712 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:05:25 +0900 Subject: [PATCH 352/632] docs(browser-session): trace extensionless implicit linker-script authority --- ...ession-linker-implicit-script-authority.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/traceability/browser-session-linker-implicit-script-authority.md diff --git a/docs/traceability/browser-session-linker-implicit-script-authority.md b/docs/traceability/browser-session-linker-implicit-script-authority.md new file mode 100644 index 000000000..7e28e4c37 --- /dev/null +++ b/docs/traceability/browser-session-linker-implicit-script-authority.md @@ -0,0 +1,49 @@ +# Browser Session extensionless implicit linker-script authority + +## Decision + +Browser Session Cargo execution/input provenance treats a non-option linker argument that contains a path separator as an external native input even when the file has no recognized object, archive, shared-library, bitcode, or resource suffix. + +This is intentionally narrower than banning every non-option token. Bare linker option operands such as the `relro` operand in `-z relro` remain allowed. A future exception for a repository path requires an explicit, versioned provenance contract for the referenced artifact or script and its transitive native inputs. + +## Problem and threat + +The reviewed production package/source closure and the nominal linker executable can remain unchanged while Git-owned Cargo `rustflags` append a path such as `tools/review-bypass-input` with `-C link-arg` or `-C link-args`. + +The Rust compiler documents that `link-arg` appends one argument to the linker invocation and that `link-args` appends multiple arguments. On Unix-like targets using a C compiler as linker driver, `-Wl,$ARG` forwards an argument to the underlying linker. + +GNU `ld` documents a second interpretation that makes an extension allowlist insufficient: if a linker input is not recognized as an object or archive, `ld` attempts to parse it as a linker script. Such an implicit script may contain `INPUT` or `GROUP`, which can introduce additional native inputs at that point in the command line. + +Therefore a repository-relative, extensionless path is not inert metadata. It can be a transitive native-input authority boundary. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker execution and input authority. +- The repair must not ban unrelated rustflags or ordinary option-only linker arguments. +- No linker or driver acknowledgement is treated as evidence that the resulting binary contains only reviewed inputs. + +## RED → repair evidence + +- Predecessor exact head: `202ba6c92faf7a34d233690c3923680337ec0e65`. +- RED: `8f5e49f5fe63d99773d25f13a3ab50648e02ac92` adds `tests/test_browser_session_linker_implicit_script_contract.py`. It supplies an extensionless `tools/review-bypass-input` whose contents are `INPUT(tools/review-bypass-object.o)` through direct, `-Wl,`, `--for-linker=`, and `-Xlinker` forms. The predecessor suffix-only classifier does not reject those path operands. +- Repair: `cb95d5d37050bb7da70f1782da2e63a9b4583734` extends the existing positional-input classifier so a non-option token containing `/` or `\\` fails closed before suffix classification. No second Cargo topology/config authority is introduced. +- Control: `b03980261159ca90788c1ccf9cc5e750d974976c` preserves a bare linker option operand (`-Wl,-z,relro`) and the existing option-only `--as-needed` control. + +The repair covers repository/path-shaped extensionless positional inputs presented directly or through the already-modeled GNU-style forwarding forms. It also keeps the existing recognized native suffix and versioned `.so.*` checks. + +## Residual risk and removal conditions + +This slice does **not** claim universal linker-grammar closure. + +- A bare extensionless filename with no `/` or `\\` can still be a positional input if the linker working directory or search semantics resolve it. Closing that path safely requires argument-arity-aware parsing or compiler-derived link-command provenance so option operands are not confused with positional inputs. +- Non-GNU linker/driver grammars may expose additional input-control surfaces. They require authoritative target-specific semantics and hostile fixtures before being added to the shared classifier. +- Runtime environment injection (`RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`), direct `cargo rustc -- ...`, sysroot/toolchain composition, and custom target/toolchain behavior remain separate authority surfaces. + +A path exception may be relaxed only when the referenced file and every transitive native input are immutable, hashed, reviewed, bound to the exact build provenance, and exercised by current-head executable evidence. Until then the contract remains fail closed. + +## Primary references + +Rust Project. (2026). *The rustc book: Codegen options — link-arg and link-args*. https://doc.rust-lang.org/rustc/codegen-options/index.html#link-arg + +GNU Project. (2026). *GNU ld: Implicit linker scripts*. https://sourceware.org/binutils/docs/ld/Implicit-Linker-Scripts.html From ed86b335b4aa6cde223fe2e614bb26d39f789d8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:29:13 +0900 Subject: [PATCH 353/632] test(browser-session): expose bare implicit linker input gap --- ...on_linker_bare_implicit_script_contract.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_browser_session_linker_bare_implicit_script_contract.py diff --git a/tests/test_browser_session_linker_bare_implicit_script_contract.py b/tests/test_browser_session_linker_bare_implicit_script_contract.py new file mode 100644 index 000000000..8631d5fc2 --- /dev/null +++ b/tests/test_browser_session_linker_bare_implicit_script_contract.py @@ -0,0 +1,80 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerBareImplicitScriptContractTests(unittest.TestCase): + """Keep bare extensionless implicit linker inputs inside reviewed provenance.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (root / "review-bypass-input").write_text( + "INPUT(tools/review-bypass-object.o)\n", + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_bare_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_direct_bare_extensionless_implicit_script_fails_closed(self) -> None: + self._assert_bare_input_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=review-bypass-input"]\n' + ) + + def test_forwarded_bare_extensionless_implicit_script_fails_closed(self) -> None: + for forwarded in ( + "-Wl,review-bypass-input", + "--for-linker=review-bypass-input", + "-Xlinker review-bypass-input", + ): + with self.subTest(forwarded=forwarded): + self._assert_bare_input_fails_closed( + f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' + ) + + def test_separate_z_operand_remains_allowed(self) -> None: + for forwarded in ( + "-z relro", + "-Wl,-z,relro", + "--for-linker=-z,relro", + "-Xlinker -z -Xlinker relro", + ): + with self.subTest(forwarded=forwarded): + root = self._workspace_with_config( + f'[build]\nrustflags = ["-C", "link-args={forwarded}"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 5f070bf0b87ae513cf06badda29914e579f850ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:30:43 +0900 Subject: [PATCH 354/632] fix(browser-session): classify bare linker inputs by arity --- ...ssion_cargo_compiler_authority_contract.py | 95 +++++++++---------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 501f98652..15bf16895 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -20,18 +20,7 @@ TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) -POSITIONAL_LINK_INPUT_SUFFIXES = ( - ".o", - ".obj", - ".lo", - ".a", - ".lib", - ".rlib", - ".so", - ".dylib", - ".bc", - ".res", -) +LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -83,16 +72,30 @@ def _linker_option_uses_response_file(argument: str) -> bool: def _linker_argument_is_positional_native_input(argument: str) -> bool: - """Return whether a positional linker argument can name an external native input.""" - if not argument or argument.startswith("-"): - return False - if "/" in argument or "\\" in argument: - return True - lowered = argument.lower() - filename = lowered.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] - if filename.endswith(POSITIONAL_LINK_INPUT_SUFFIXES): - return True - return ".so." in filename + """Return whether one unconsumed linker token is a positional external input.""" + return bool(argument) and not argument.startswith("-") + + +def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[str]) -> bool: + """Parse direct-linker tokens without misclassifying modeled option operands as inputs.""" + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in LINKER_OPTIONS_WITH_SEPARATE_OPERAND: + if index + 1 >= len(arguments): + return True + index += 2 + continue + if ( + _linker_option_loads_plugin(argument) + or _linker_option_selects_script(argument) + or _linker_option_uses_response_file(argument) + or _linker_argument_extends_external_inputs(argument) + or _linker_argument_is_positional_native_input(argument) + ): + return True + index += 1 + return False def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: @@ -106,37 +109,33 @@ def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: """Return whether driver arguments can replace tools, extend link inputs, or load linker code.""" - for index, argument in enumerate(arguments): + direct_arguments: list[str] = [] + xlinker_arguments: list[str] = [] + index = 0 + while index < len(arguments): + argument = arguments[index] if _linker_driver_argument_selects_executable(argument): return True if _linker_argument_extends_external_inputs(argument): return True - if _linker_argument_is_positional_native_input(argument): - return True - if _linker_option_selects_script(argument): - return True - if any( - _linker_option_loads_plugin(forwarded) - or _linker_option_selects_script(forwarded) - or _linker_option_uses_response_file(forwarded) - or _linker_argument_extends_external_inputs(forwarded) - or _linker_argument_is_positional_native_input(forwarded) - for forwarded in _forwarded_linker_arguments(argument) - ): - return True - if ( - argument == "-Xlinker" - and index + 1 < len(arguments) - and ( - _linker_option_loads_plugin(arguments[index + 1]) - or _linker_option_selects_script(arguments[index + 1]) - or _linker_option_uses_response_file(arguments[index + 1]) - or _linker_argument_extends_external_inputs(arguments[index + 1]) - or _linker_argument_is_positional_native_input(arguments[index + 1]) - ) - ): + + forwarded = _forwarded_linker_arguments(argument) + if forwarded and _direct_linker_arguments_extend_authority(forwarded): return True - return False + + if argument == "-Xlinker": + if index + 1 >= len(arguments): + return True + xlinker_arguments.append(arguments[index + 1]) + index += 2 + continue + + direct_arguments.append(argument) + index += 1 + + return _direct_linker_arguments_extend_authority( + direct_arguments + ) or _direct_linker_arguments_extend_authority(xlinker_arguments) def _flag_arguments(value: object) -> list[str]: From a3031442e6b3a5e24db53fe53e17724a73c1165b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:31:31 +0900 Subject: [PATCH 355/632] docs(browser-session): close bare implicit linker input trace --- ...ession-linker-implicit-script-authority.md | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/traceability/browser-session-linker-implicit-script-authority.md b/docs/traceability/browser-session-linker-implicit-script-authority.md index 7e28e4c37..d9093a3f0 100644 --- a/docs/traceability/browser-session-linker-implicit-script-authority.md +++ b/docs/traceability/browser-session-linker-implicit-script-authority.md @@ -2,48 +2,60 @@ ## Decision -Browser Session Cargo execution/input provenance treats a non-option linker argument that contains a path separator as an external native input even when the file has no recognized object, archive, shared-library, bitcode, or resource suffix. +Browser Session Cargo execution/input provenance treats every unconsumed non-option linker token as an external native input, including extensionless bare filenames. The shared parser consumes only linker option operands whose arity and semantics are explicitly modeled; today that narrow allowlist contains GNU `ld`/driver `-z `. -This is intentionally narrower than banning every non-option token. Bare linker option operands such as the `relro` operand in `-z relro` remain allowed. A future exception for a repository path requires an explicit, versioned provenance contract for the referenced artifact or script and its transitive native inputs. +This is intentionally not a blanket ban on non-option words. A token such as `relro` is allowed only when it is consumed as the operand of the modeled `-z` option. The same bare token in positional position fails closed. A future exception for a repository artifact or script requires an explicit, versioned provenance contract for the referenced artifact and its transitive native inputs. ## Problem and threat -The reviewed production package/source closure and the nominal linker executable can remain unchanged while Git-owned Cargo `rustflags` append a path such as `tools/review-bypass-input` with `-C link-arg` or `-C link-args`. +The reviewed production package/source closure and the nominal linker executable can remain unchanged while Git-owned Cargo `rustflags` append an extensionless file through `-C link-arg` or `-C link-args`. The Rust compiler documents that `link-arg` appends one argument to the linker invocation and that `link-args` appends multiple arguments. On Unix-like targets using a C compiler as linker driver, `-Wl,$ARG` forwards an argument to the underlying linker. -GNU `ld` documents a second interpretation that makes an extension allowlist insufficient: if a linker input is not recognized as an object or archive, `ld` attempts to parse it as a linker script. Such an implicit script may contain `INPUT` or `GROUP`, which can introduce additional native inputs at that point in the command line. +GNU `ld` documents that non-option arguments are object files or archives and that an input whose format is not recognized is parsed as an implicit linker script. Such a script may contain `INPUT` or `GROUP`, which can introduce additional native inputs. GNU `ld` also states that an object argument may not appear between an option and its required operand. That command-line grammar is why positional-input classification must be arity-aware rather than suffix- or path-shape-based. -Therefore a repository-relative, extensionless path is not inert metadata. It can be a transitive native-input authority boundary. +Therefore both `tools/review-bypass-input` and the bare filename `review-bypass-input` can represent transitive native-input authority. File suffixes and path separators are insufficient provenance boundaries. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. - `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker execution and input authority. -- The repair must not ban unrelated rustflags or ordinary option-only linker arguments. +- The repair must not ban unrelated rustflags or modeled option operands such as the `relro` keyword in `-z relro`. +- `-Wl,`, `--for-linker=`, and repeated `-Xlinker` forwarding must reach the same direct-linker authority classifier. - No linker or driver acknowledgement is treated as evidence that the resulting binary contains only reviewed inputs. ## RED → repair evidence +### Path-shaped extensionless inputs + - Predecessor exact head: `202ba6c92faf7a34d233690c3923680337ec0e65`. - RED: `8f5e49f5fe63d99773d25f13a3ab50648e02ac92` adds `tests/test_browser_session_linker_implicit_script_contract.py`. It supplies an extensionless `tools/review-bypass-input` whose contents are `INPUT(tools/review-bypass-object.o)` through direct, `-Wl,`, `--for-linker=`, and `-Xlinker` forms. The predecessor suffix-only classifier does not reject those path operands. -- Repair: `cb95d5d37050bb7da70f1782da2e63a9b4583734` extends the existing positional-input classifier so a non-option token containing `/` or `\\` fails closed before suffix classification. No second Cargo topology/config authority is introduced. -- Control: `b03980261159ca90788c1ccf9cc5e750d974976c` preserves a bare linker option operand (`-Wl,-z,relro`) and the existing option-only `--as-needed` control. +- Repair: `cb95d5d37050bb7da70f1782da2e63a9b4583734` extends the existing positional-input classifier so a non-option token containing `/` or `\\` fails closed before suffix classification. +- Control: `b03980261159ca90788c1ccf9cc5e750d974976c` preserves `-Wl,-z,relro` and the existing option-only `--as-needed` control. + +### Bare extensionless inputs -The repair covers repository/path-shaped extensionless positional inputs presented directly or through the already-modeled GNU-style forwarding forms. It also keeps the existing recognized native suffix and versioned `.so.*` checks. +- Predecessor exact head: `19dae976f1dcfdf261ded04c43cade986ade7712`. +- RED: `ed86b335b4aa6cde223fe2e614bb26d39f789d8a` adds `tests/test_browser_session_linker_bare_implicit_script_contract.py`. It supplies a bare `review-bypass-input` through direct, `-Wl,`, `--for-linker=`, and `-Xlinker` forms while retaining direct and forwarded `-z relro` controls. The predecessor path/suffix classifier does not reject the bare positional token. +- Repair: `5f070bf0b87ae513cf06badda29914e579f850ee` replaces path/suffix heuristics with an arity-aware direct-linker token parser. Every unconsumed non-option token fails closed. `-z` consumes exactly one modeled keyword operand; repeated `-Xlinker` payloads are reconstructed into one direct-linker token sequence before classification, so `-Xlinker -z -Xlinker relro` remains permitted while `-Xlinker review-bypass-input` does not. + +No second Cargo topology/config authority is introduced. Direct linker arguments and the existing GNU-style forwarding forms consume the same authority classifier. ## Residual risk and removal conditions This slice does **not** claim universal linker-grammar closure. -- A bare extensionless filename with no `/` or `\\` can still be a positional input if the linker working directory or search semantics resolve it. Closing that path safely requires argument-arity-aware parsing or compiler-derived link-command provenance so option operands are not confused with positional inputs. -- Non-GNU linker/driver grammars may expose additional input-control surfaces. They require authoritative target-specific semantics and hostile fixtures before being added to the shared classifier. +- Only option arity that has an explicit harmless-operand contract is consumed. Additional GNU linker options with separate operands remain fail closed until their semantics are modeled with positive and hostile fixtures. +- Non-GNU linker/driver grammars may expose different option arities and input-control surfaces. They require authoritative target-specific semantics and hostile fixtures before being added to the shared classifier. - Runtime environment injection (`RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`), direct `cargo rustc -- ...`, sysroot/toolchain composition, and custom target/toolchain behavior remain separate authority surfaces. +- Static argument classification proves admission policy, not the actual final link command or artifact composition. Release evidence still requires exact-head executable provenance and reproducibility evidence. -A path exception may be relaxed only when the referenced file and every transitive native input are immutable, hashed, reviewed, bound to the exact build provenance, and exercised by current-head executable evidence. Until then the contract remains fail closed. +An input exception may be relaxed only when the referenced file and every transitive native input are immutable, hashed, reviewed, bound to exact build provenance, and exercised by current-head executable evidence. Until then the contract remains fail closed. ## Primary references Rust Project. (2026). *The rustc book: Codegen options — link-arg and link-args*. https://doc.rust-lang.org/rustc/codegen-options/index.html#link-arg +GNU Project. (2026). *GNU ld: Options*. https://sourceware.org/binutils/docs/ld/Options.html + GNU Project. (2026). *GNU ld: Implicit linker scripts*. https://sourceware.org/binutils/docs/ld/Implicit-Linker-Scripts.html From 5928b1a614c8620203675b609b75f5489338e06d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:32:10 +0900 Subject: [PATCH 356/632] test(browser-session): expose rustdoc external input gap --- ...session_rustdoc_external_input_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_external_input_contract.py diff --git a/tests/test_browser_session_rustdoc_external_input_contract.py b/tests/test_browser_session_rustdoc_external_input_contract.py new file mode 100644 index 000000000..48e5ddbc0 --- /dev/null +++ b/tests/test_browser_session_rustdoc_external_input_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionRustdocExternalInputContractTests(unittest.TestCase): + """Keep rustdoc-selected external crate and native-library inputs reviewed.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_external_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_extern_input_fails_closed(self) -> None: + self._assert_external_input_fails_closed( + '[build]\nrustdocflags = ["--extern", "review_bypass=tools/libreview_bypass.rlib"]\n' + ) + + def test_target_rustdocflags_library_search_path_fails_closed(self) -> None: + self._assert_external_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Lnative=tools/review-bypass\"]\n" + ) + + def test_unrelated_rustdocflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["--document-private-items", "--cfg", "docsrs"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:33:52 +0900 Subject: [PATCH 357/632] fix(browser-session): cover rustdoc external inputs --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 15bf16895..d06393f72 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -224,6 +224,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustflags:external link input") if _flags_select_linker(build.get("rustdocflags")): build_configured.append("rustdocflags:codegen linker") + if _flags_extend_external_link_inputs(build.get("rustdocflags")): + build_configured.append("rustdocflags:external link input") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -238,6 +240,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustflags:external link input") if _flags_select_linker(settings.get("rustdocflags")): configured.append("rustdocflags:codegen linker") + if _flags_extend_external_link_inputs(settings.get("rustdocflags")): + configured.append("rustdocflags:external link input") if configured: target_configured[str(target_name)] = configured From 91115dc519bfc318041a653f9bc362e072cf949c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:35:18 +0900 Subject: [PATCH 358/632] docs(browser-session): trace rustdoc external input authority --- ...ession-rustdoc-external-input-authority.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/traceability/browser-session-rustdoc-external-input-authority.md diff --git a/docs/traceability/browser-session-rustdoc-external-input-authority.md b/docs/traceability/browser-session-rustdoc-external-input-authority.md new file mode 100644 index 000000000..d0bb868e6 --- /dev/null +++ b/docs/traceability/browser-session-rustdoc-external-input-authority.md @@ -0,0 +1,40 @@ +# Browser Session rustdoc external-input authority + +Status: Draft contract evidence on PR #317. This document does not claim hosted repository/security GREEN. + +## Decision + +Git-owned Cargo `rustdocflags` are subject to the same external crate/native-library input provenance gate as Git-owned `rustflags`. Both build-level and matching target-level rustdoc flags fail closed when they introduce `--extern`, `-L`, or `-l` input authority outside the reviewed Cargo production topology. + +This is separate from rustdoc executable/linker selection. `build.rustdoc` and rustdoc codegen linker options were already covered; this slice closes the external-input half of the rustdoc boundary. + +## Problem + +Cargo documents `build.rustdocflags` and matching `target..rustdocflags` / `target..rustdocflags` as extra command-line flags passed to rustdoc. `cargo rustdoc` also documents that rustdoc receives `-L` and `--extern` arguments as part of normal dependency wiring. Therefore a repository-owned rustdoc flag can widen documentation-time crate/native-library inputs even when the production Cargo manifests and the rustdoc executable remain unchanged. + +The predecessor Browser Session contract classified external inputs for `rustflags` but applied only linker-selection classification to `rustdocflags`. A Git-owned `--extern review_bypass=tools/libreview_bypass.rlib` or `-Lnative=tools/review-bypass` in rustdocflags could therefore bypass the explicit input-provenance marker. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production package/source/dependency topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/rustdoc execution and input authority. +- Ordinary documentation flags such as `--document-private-items` and `--cfg docsrs` remain allowed. +- Environment `RUSTDOCFLAGS` / `CARGO_ENCODED_RUSTDOCFLAGS` and direct `cargo rustdoc -- ...` CLI injection remain CI/runtime authority surfaces and are not claimed closed here. + +## RED → repair + +- Predecessor exact head: `a3031442e6b3a5e24db53fe53e17724a73c1165b`. +- RED: `5928b1a614c8620203675b609b75f5489338e06d` adds `tests/test_browser_session_rustdoc_external_input_contract.py`. It exercises build-level `--extern`, target-level `-Lnative=...`, and an unrelated-rustdocflags control against the canonical Cargo compiler authority helper. +- Repair: `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b` applies `_flags_extend_external_link_inputs(...)` to both build and target `rustdocflags`, recording `rustdocflags:external link input` without adding another topology/config scanner. + +The repair is source-level contract evidence only until the exact head receives hosted executable repository/security evidence. + +## Residual surfaces + +Environment-selected rustdoc flags, direct `cargo rustdoc` trailing arguments, unmodeled rustdoc arguments with equivalent external-input semantics, sysroot/toolchain composition, and target-specific mechanisms outside the currently modeled Cargo config remain separate review surfaces. + +## Primary references + +The Rust Project Developers. (2026). *Configuration*. *The Cargo Book*. https://doc.rust-lang.org/cargo/reference/config.html + +The Rust Project Developers. (2026). *cargo rustdoc*. *The Cargo Book*. https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html From 6a79b8bce406127538bece45830aa4cf76b63af7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:36:28 +0900 Subject: [PATCH 359/632] fix(browser-session): inspect indented recovery impls --- tests/test_browser_session_lifecycle_contract.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_lifecycle_contract.py b/tests/test_browser_session_lifecycle_contract.py index 83b9ca7a8..82207fd4a 100644 --- a/tests/test_browser_session_lifecycle_contract.py +++ b/tests/test_browser_session_lifecycle_contract.py @@ -14,7 +14,7 @@ def _inherent_impl_surface(source: str, type_name: str) -> str: """Return every inherent impl segment for one Rust type without matching sibling request types.""" - starts = [match.start() for match in re.finditer(r"(?m)^impl(?=\s|<)", source)] + starts = [match.start() for match in re.finditer(r"(?m)^[ \t]*impl(?=\s|<)", source)] starts.append(len(source)) segments: list[str] = [] for index, start in enumerate(starts[:-1]): @@ -36,7 +36,7 @@ class BrowserSessionLifecycleContractTests(unittest.TestCase): """Keep presentation mutation authority in an explicit Browser Session domain.""" def test_recovery_surface_extractor_covers_concrete_and_spaced_generic_impls(self) -> None: - """Raw recovery accessors must not hide in concrete or spaced-generic inherent impls.""" + """Raw recovery accessors must not hide in concrete, generic, or indented inherent impls.""" hostile = """ impl BoundBrowserSessionRecovery { @@ -45,6 +45,11 @@ def test_recovery_surface_extractor_covers_concrete_and_spaced_generic_impls(sel impl

BoundBrowserSessionRecovery

{ pub const fn port(&self) {} } +mod nested { + impl BoundBrowserSessionRecovery { + pub fn browser_session(&self) {} + } +} impl

RecoveryContextOperationRequest

{ pub fn browser_session(&self) {} } @@ -56,6 +61,7 @@ def test_recovery_surface_extractor_covers_concrete_and_spaced_generic_impls(sel self.assertIn("impl BoundBrowserSessionRecovery", surface) self.assertIn("impl

BoundBrowserSessionRecovery

", surface) + self.assertIn("impl BoundBrowserSessionRecovery", surface) self.assertIn("pub fn browser_session(&self)", surface) self.assertIn("pub const fn port(&self)", surface) self.assertNotIn("RecoveryContextOperationRequest", surface) From 87895ac146cd192c0e144fa8a85afed42e3bcb57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:37:49 +0900 Subject: [PATCH 360/632] docs(browser-session): align native input residuals --- ...er-session-native-library-input-authority.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-native-library-input-authority.md b/docs/traceability/browser-session-native-library-input-authority.md index 9c69c09f9..c0ccbb250 100644 --- a/docs/traceability/browser-session-native-library-input-authority.md +++ b/docs/traceability/browser-session-native-library-input-authority.md @@ -12,7 +12,7 @@ A first repair closed those top-level rustc forms but left an equivalent driver `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The compiler-authority contract may consume that topology and constrain Git-owned compiler/input authority, but it must not reimplement workspace/package discovery. -This slice applies only to repository-owned Cargo `rustflags`. It does not claim authority over environment-injected `RUSTFLAGS`, direct `cargo rustc -- ...` arguments, build-system flags outside the repository, or external toolchain configuration. Those require their canonical CI/supply-chain owner or a separate reviewed contract. +This slice applies to repository-owned Cargo configuration. It does not claim authority over environment-injected `RUSTFLAGS` / `RUSTDOCFLAGS`, direct `cargo rustc -- ...` / `cargo rustdoc -- ...` arguments, build-system flags outside the repository, or external toolchain configuration. Those require their canonical CI/supply-chain owner or a separate reviewed contract. ## RED → repair @@ -29,22 +29,25 @@ Focused review of exact `e934b3c17261ab26bb13b4f02417416c4202d344` then found th Repair `a0f8525b57837ac119ef7b2af9c1daa759ef12f4` reuses one external-input classifier across top-level rustc flags and the existing linker-driver parser. Direct driver arguments, `-Wl,` / `--for-linker=` forwarded arguments, and the argument following `-Xlinker` now fail closed when they select `-L` or `-l`. Existing response-file, linker-script, plugin, tool-selection, GCC specs/wrapper, and driver-search-path checks remain in the same shared parser. Ordinary non-input linker options such as `-Wl,--as-needed` and `-Wl,-Bsymbolic` remain allowed. +External-crate injection through Git-owned Cargo `rustflags --extern` is separately closed by `tests/test_browser_session_extern_input_contract.py`. The same external-input classifier now also applies to build- and target-level `rustdocflags` after RED `5928b1a614c8620203675b609b75f5489338e06d` and repair `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b`; that rustdoc-specific decision is traced in `browser-session-rustdoc-external-input-authority.md`. + ## Decision -Until external/native input provenance is modeled as a versioned reviewed contract, repository-owned Cargo configuration must not widen rustc or compiler-driver external-library search paths or request additional native libraries for Browser Session production packages. +Until external/native input provenance is modeled as a versioned reviewed contract, repository-owned Cargo configuration must not widen rustc/rustdoc external-library search paths or request additional native/external crate inputs for Browser Session production packages. -This is an input-provenance rule, not a claim that `-L` or `-l` are unsafe Rust/GCC features. They are rejected here because their resolved artifacts are outside the current exact-head source and artifact review closure. +This is an input-provenance rule, not a claim that `-L`, `-l`, or `--extern` are unsafe Rust features. They are rejected here because their resolved artifacts are outside the current exact-head source and artifact review closure. ## Residual surfaces The following remain separate review surfaces and are not pre-authorized by this decision: -- `--extern` and other direct precompiled-Rust dependency injection; -- positional object/archive inputs forwarded through `-C link-arg` / `link-args` that do not use `-L`/`-l`; -- environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc` trailing arguments; +- environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` / `RUSTDOCFLAGS` / `CARGO_ENCODED_RUSTDOCFLAGS` and direct `cargo rustc` / `cargo rustdoc` trailing arguments; +- external-input spellings with equivalent semantics that are not yet modeled by the shared classifier; - sysroot/rustup/toolchain composition and custom target specifications; - non-GNU platform-specific native input/control-file mechanisms. +Positional native inputs, Git-owned Cargo `--extern`, and Git-owned Cargo rustdoc `--extern` / `-L` forms are governed by their supplemental current contracts and are not residual gaps in this repository-owned Cargo boundary. + A future allowlist must identify the exact artifact path, digest/provenance, producer, target triple, linkage kind, and reproducible build evidence on the same reviewed exact tree. A path-only allowlist is insufficient. ## Primary evidence @@ -67,4 +70,4 @@ Cargo documents the corresponding native-library and search-path concepts throug ## Verification state -Both RED→repair generations are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN: current-head hosted repository/security workflows and independent current-head review must still complete on the reconciled lineage before merge or release readiness can be claimed. +The RED→repair generations are structurally present on the active #317 lineage. This dossier does not promote the branch to executable GREEN: current-head hosted repository/security workflows and independent current-head review must still complete on the reconciled lineage before merge or release readiness can be claimed. From 2701638360aca8f1eb2dbfe8fb23e677d53b2f6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:39:13 +0900 Subject: [PATCH 361/632] docs(browser-session): align positional input residuals --- ...ssion-linker-positional-input-authority.md | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/traceability/browser-session-linker-positional-input-authority.md b/docs/traceability/browser-session-linker-positional-input-authority.md index d9af3ad4b..13789acae 100644 --- a/docs/traceability/browser-session-linker-positional-input-authority.md +++ b/docs/traceability/browser-session-linker-positional-input-authority.md @@ -2,17 +2,17 @@ ## Problem -The Browser Session Cargo compiler-authority contract already rejects repository-owned linker replacement, search-path reselection, response files, plugins, explicit GNU linker scripts, native-library `-L` / `-l` widening, rustc `--extern`, and other modeled execution/input-authority surfaces. It did not constrain positional native object/archive arguments supplied through rustc `-C link-arg` / `link-args`. +The Browser Session Cargo compiler-authority contract already rejects repository-owned linker replacement, search-path reselection, response files, plugins, explicit GNU linker scripts, native-library `-L` / `-l` widening, rustc `--extern`, and other modeled execution/input-authority surfaces. It initially constrained only positional native inputs recognized by artifact suffix or repository/path shape, leaving a bare extensionless positional filename as a distinct implicit-script/native-input path. -The rustc book states that `link-arg` appends one extra argument to the linker invocation and `link-args` appends multiple arguments. On Unix-like targets rustc commonly invokes a C compiler such as `cc` or `clang` as the linker driver. GNU ld documents non-option arguments as object files or archives to be linked into the output. Consequently, Git-owned Cargo configuration could inject a reviewed-tree-external object/archive into the final Browser Session binary without changing `Cargo.toml`, canonical production-source topology, nominal linker selection, or `-L` / `-l` settings. +The rustc book states that `link-arg` appends one extra argument to the linker invocation and `link-args` appends multiple arguments. On Unix-like targets rustc commonly invokes a C compiler such as `cc` or `clang` as the linker driver. GNU ld documents non-option arguments as object files or archives to be linked into the output and states that an input whose format is not recognized can be parsed as an implicit linker script. Consequently, Git-owned Cargo configuration could inject a reviewed-tree-external native input or script while leaving `Cargo.toml`, canonical production-source topology, nominal linker selection, and `-L` / `-l` settings unchanged. -This is a provenance gap: an object/archive pathname alone does not establish the artifact's source revision, producer, digest, target/toolchain identity, reproducibility, or review ancestry. +This is a provenance gap: an input token alone does not establish the artifact's source revision, producer, digest, target/toolchain identity, reproducibility, or review ancestry. ## Constraint `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. The Cargo compiler-authority contract consumes that topology and may constrain repository-owned linker input authority, but it must not implement a second Cargo workspace/package resolver. -This slice covers modeled positional native artifacts supplied from Git-owned `.cargo/config.toml` / `.cargo/config` rustflags. It does not claim to parse every linker grammar. In particular, extensionless/non-GNU control files and implicit linker-script inputs remain distinct residual surfaces until they have an authoritative parser/provenance contract. +This slice covers positional inputs supplied from Git-owned `.cargo/config.toml` / `.cargo/config` flags for the currently modeled direct and GNU-compatible forwarding grammar. It does not claim to parse every non-GNU linker grammar or every option with a separate operand. ## RED → repair @@ -24,24 +24,32 @@ RED `e547203368da2aec62e2c94ab491eb0749d7d7b5` adds hostile cases for: The predecessor exact `61053c9cc3ca58f5d812f3ab64660f4e662afdce` allowed these forms because its external-input classifier covered `-L`, `-l`, `--extern`, scripts/plugins/response files, and executable reselection but not positional native artifacts. -Repair `e73d221cf28aa25ae6fbd941db95d5d923c7e883` keeps canonical Cargo topology ownership unchanged and extends the shared linker-argument classifier with a modeled positional-native-input predicate. It rejects common object/archive artifact forms (`.o`, `.obj`, `.lo`, `.a`, `.lib`, `.rlib`, `.so` including versioned `.so.*`, `.dylib`, `.bc`, and `.res`) whether direct or passed through the already modeled linker-forwarding forms. Existing option-only controls such as `-Wl,--as-needed` remain allowed; the repair is not a blanket ban on `link-arg` / `link-args`. +Repair `e73d221cf28aa25ae6fbd941db95d5d923c7e883` keeps canonical Cargo topology ownership unchanged and extends the shared linker-argument classifier with modeled positional-native-input detection for common object/archive artifact forms (`.o`, `.obj`, `.lo`, `.a`, `.lib`, `.rlib`, `.so` including versioned `.so.*`, `.dylib`, `.bc`, and `.res`) whether direct or passed through the already modeled linker-forwarding forms. + +A subsequent hostile fixture `8f5e49f5fe63d99773d25f13a3ab50648e02ac92` proved that suffix-only classification still admitted an extensionless repository/path-shaped input such as `tools/review-bypass-input`. Repair `cb95d5d37050bb7da70f1782da2e63a9b4583734` closed that path-shaped form without duplicating Cargo topology ownership. + +The remaining bare-filename gap was then preserved by RED `ed86b335b4aa6cde223fe2e614bb26d39f789d8a`: `review-bypass-input` could still enter through direct `link-arg` and the modeled `-Wl,`, `--for-linker=`, or repeated `-Xlinker` forwarding forms. Repair `5f070bf0b87ae513cf06badda29914e579f850ee` replaces path/suffix heuristics with an arity-aware direct-linker parser. Every unconsumed non-option token now fails closed as positional input authority. The parser consumes only option operands whose harmless semantics are explicitly modeled; today the narrow operand allowlist includes GNU `-z `, preserving controls such as `-z relro`, `-Wl,-z,relro`, `--for-linker=-z,relro`, and `-Xlinker -z -Xlinker relro`. + +Direct arguments and GNU-compatible forwarding forms consume the same shared classifier. This is not a blanket ban on `link-arg` / `link-args`; option-only controls such as `-Wl,--as-needed` remain allowed. ## Decision -Repository-owned Cargo flags must fail closed when they add modeled positional native object/archive inputs outside the canonical reviewed source/dependency closure. A future allowlist must bind an artifact to exact digest, producer/source revision, target triple, linker/compiler toolchain identity, build configuration, reproducibility/provenance evidence, and the consuming exact tree. File extension or pathname alone is never approval. +Repository-owned Cargo flags fail closed when the currently modeled direct/GNU-compatible linker grammar introduces any unconsumed non-option positional input. A future allowlist must bind the artifact or script to exact digest, producer/source revision, target triple, linker/compiler toolchain identity, build configuration, transitive input closure, reproducibility/provenance evidence, and the consuming exact tree. File extension, path shape, or bare filename is never approval. ## Security effect -The repair prevents a repository configuration change from inserting a prebuilt native object/archive into the Browser Session binary while leaving the Rust source closure and nominal linker selection apparently unchanged. Direct and forwarded variants are classified by the same shared authority code, so `-Wl`, `--for-linker`, and `-Xlinker` do not create parallel provenance policy. +The repair prevents repository configuration from inserting prebuilt native objects, archives, extensionless files, or GNU implicit scripts while leaving the Rust source closure and nominal linker selection apparently unchanged. Direct and forwarded variants are classified by the same authority code, so `-Wl`, `--for-linker`, and `-Xlinker` do not create parallel provenance policy. ## Residual surfaces The following remain separate review surfaces and are not pre-authorized by this decision: -- extensionless or otherwise unmodeled positional inputs, including GNU ld implicit-script interpretation; -- non-GNU linker/control-file input mechanisms; +- non-GNU linker/control-file grammars and currently unmodeled option-arity/input mechanisms; - environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS` and direct `cargo rustc -- ...` trailing flags; -- rustup/sysroot/toolchain composition, custom target specifications, and target-specific external toolchain scripts. +- rustup/sysroot/toolchain composition, custom target specifications, and target-specific external toolchain scripts; +- runtime-derived or toolchain-internal inputs that cannot be established from Git-owned Cargo argument syntax alone. + +Path-shaped and bare extensionless positional inputs in the currently modeled direct/GNU-compatible forms are no longer residual gaps; they are fail-closed by the shared arity-aware classifier. ## Primary evidence @@ -51,8 +59,12 @@ The rustc documentation states that `link-arg` appends a single argument and `li Free Software Foundation. (2026). *Using ld: Command-line options*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld/Options.html -GNU ld documents non-option command-line arguments as object files or archives to be linked together. It also documents that unrecognized input-file formats may be interpreted as linker scripts, which is why implicit script/control-file handling remains a separately tracked residual surface rather than being silently declared closed by this artifact-suffix classifier. +GNU ld documents non-option command-line arguments as object files or archives to be linked together and documents option/operand grammar constraints relevant to positional-input parsing. + +Free Software Foundation. (2026). *Implicit linker scripts*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld/Implicit-Linker-Scripts.html + +GNU ld documents that an input file whose format is not recognized can be interpreted as a linker script. This is why all unconsumed non-option tokens in the modeled grammar are provenance-bearing inputs rather than only tokens with familiar native-object suffixes. ## Verification state -The RED and minimal repair are structurally present on the active #317 lineage. This dossier does not claim hosted executable GREEN or independent current-head review. Those remain required after lineage reconciliation and exact-head workflow execution before merge or release readiness. \ No newline at end of file +The RED→repair generations are structurally present on the active #317 lineage. This dossier does not claim hosted executable GREEN or independent review of the newest exact head. Those remain required after lineage reconciliation and exact-head workflow execution before merge or release readiness. From 569bfc807df8e4f3f452f04e5dfb865d09086fac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 10:40:08 +0900 Subject: [PATCH 362/632] docs(browser-session): align compiler authority residuals --- ...rowser-session-cargo-compiler-authority.md | 121 ++++++++---------- 1 file changed, 52 insertions(+), 69 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 87adf7b2d..9e46d0270 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -4,123 +4,104 @@ Status: Draft contract evidence on PR #317. This document does not claim executa ## Problem -OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration surface is executable selection around Rust compilation, documentation, linking, and test/run execution. +OriginWeave already fails closed on production `build.rs`, Cargo build dependencies, dependency-source overrides, repository-external source paths, and unmodeled Rust source indirection. The remaining Git-owned Cargo configuration boundary is execution and external-input selection around Rust compilation, documentation, linking, and test/run execution. Cargo permits repository configuration to replace `rustc` with `build.rustc`, execute a program in front of `rustc` with `build.rustc-wrapper`, add a workspace-only wrapper with `build.rustc-workspace-wrapper`, and replace the documentation generator executable with `build.rustdoc`. Cargo target configuration also permits a matching target table to choose `linker`, the executable used for linking, and `runner`, the wrapper used for `cargo run`, `cargo test`, and `cargo bench`. In addition, `build.rustflags` and matching target `rustflags` are passed to `rustc`, while `build.rustdocflags` and matching target `rustdocflags` are passed to `rustdoc`. -Rust documents `-C` and `--codegen` as equivalent short and long codegen-option interfaces. The `linker` codegen option directly selects which linker executable rustc invokes. Rust also documents `link-arg` and `link-args` as arguments appended to the linker invocation. On Unix-like targets where a C compiler is the linker driver, Rust explicitly documents that `-Clink-arg=-fuse-ld=$value` is passed to the driver after rustc's own linker-feature arguments and therefore generally takes priority when the driver chooses the actual linker. A repository-owned Cargo flag can therefore re-select the linker behind the nominal compiler driver without using `target..linker` or `-C linker=`. +Rust documents `-C` and `--codegen` as equivalent short and long codegen-option interfaces. The `linker` codegen option directly selects which linker executable rustc invokes. Rust also documents `link-arg` and `link-args` as arguments appended to the linker invocation. On Unix-like targets where a C compiler is the linker driver, Rust documents that `-Clink-arg=-fuse-ld=$value` is passed to the driver after rustc's own linker-feature arguments and therefore generally takes priority when the driver chooses the actual linker. A repository-owned Cargo flag can therefore re-select the linker behind the nominal compiler driver without using `target..linker` or `-C linker=`. -The same nominal-driver boundary has another executable-selection path. Rust documents that Unix-like targets commonly use `cc` or `clang` as the linker driver. GCC documents `-Bprefix` as the first search prefix for driver subprograms including `ld`; if the requested program is found there, that executable is run before the standard prefixes or `PATH` lookup. Because rustc `link-arg` and `link-args` append arguments to the linker invocation, repository-owned `-C link-arg=-B...` or `--codegen=link-args=-B ...` can therefore redirect the compiler driver to a different linker executable without changing the nominal driver or using `-fuse-ld=`. +The same nominal-driver boundary has another executable-selection path. Rust documents that Unix-like targets commonly use `cc` or `clang` as the linker driver. GCC documents `-Bprefix` as the first search prefix for driver subprograms including `ld`; if the requested program is found there, that executable is run before the standard prefixes or `PATH` lookup. Repository-owned `-C link-arg=-B...` or `--codegen=link-args=-B ...` can therefore redirect the compiler driver to a different linker executable without changing the nominal driver or using `-fuse-ld=`. -GCC also expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can therefore hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. The response file may itself include another response file. GNU-compatible forwarding does not make that provenance safe: `-Wl,@file`, `--for-linker=@file`, and `-Xlinker @file` delegate parsing to the linker after the compiler-driver surface. Until response-file contents, containment, recursion, and effective driver/linker semantics are represented as reviewed provenance, both driver-level and forwarded linker-level `@file` arguments are opaque extensions of the execution/input boundary and must fail closed. +GCC expands `@file` response-file arguments in place, recursively. A repository-owned `-C link-arg=@tools/linker.rsp` can hide `-B...`, `-fuse-ld=...`, or another driver option from a scanner that only inspects the visible Cargo flag. GNU-compatible forwarding does not make that provenance safe: `-Wl,@file`, `--for-linker=@file`, and `-Xlinker @file` delegate parsing to the linker after the compiler-driver surface. Until response-file contents, containment, recursion, and effective driver/linker semantics are represented as reviewed provenance, both driver-level and forwarded linker-level `@file` arguments are opaque extensions of the execution/input boundary and fail closed. -GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process, for example with `-C link-arg=-Wl,-plugin,tools/review-bypass-linker.so`. Repeated `-C link-arg=-Xlinker` forms can express the same plugin request across separate codegen options. That is executable-code provenance, not an ordinary linker tuning flag. +GNU `ld` can dynamically load linker plugins with `-plugin name`, and GCC forwards explicit linker options through `-Wl,option`, `--for-linker=option`, or `-Xlinker option`. A repository-owned Cargo flag can therefore leave the nominal compiler and linker executables unchanged while injecting a repository-selected shared object into the linker process. That is executable-code provenance, not ordinary linker tuning. -GNU linker scripts are also an explicit native-input authority surface, not future work. `-T`/`--script` can select a script whose `INPUT(...)`/`GROUP(...)` directives add object or archive inputs outside the reviewed Rust production-source closure. The shared Cargo linker-argument classifier therefore fails closed on direct script selection and on `-Wl,`, `--for-linker=`, or `-Xlinker` forwarding of those options. This explicit-script rule is separate from implicit linker-script interpretation of ordinary positional inputs, which remains a residual review surface. +GNU linker scripts are explicit native-input authority. `-T`/`--script` can select a script whose `INPUT(...)`/`GROUP(...)` directives add object or archive inputs outside the reviewed Rust production-source closure. The shared Cargo linker-argument classifier fails closed on direct script selection and on `-Wl,`, `--for-linker=`, or `-Xlinker` forwarding of those options. -GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, while the driver also accepts the option and its file argument as separate argv tokens. The spec-file format can override named spec strings or include other spec files. Repository-owned Cargo `link-arg`/`link-args` can therefore alter which subprocesses or switches the nominal linker driver uses with either `-specs=file` or `-specs file`, without changing the visible `linker=` setting. Until the specs content and every included file are immutable reviewed provenance, both spellings extend execution authority rather than acting as ordinary linker tuning. +Ordinary positional linker inputs are now part of the same authority boundary. GNU `ld` treats non-option arguments as input files and may parse an unrecognized input as an implicit linker script. The current shared parser therefore treats every unconsumed non-option token in the modeled direct/GNU-compatible grammar as provenance-bearing input, including suffix-bearing native objects, path-shaped extensionless inputs, and bare extensionless filenames. Explicitly modeled harmless option operands, currently including `-z `, are consumed by arity before positional classification. -If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution path is no longer represented by the existing exact-tree source closure. +GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, while the driver also accepts the option and its file argument as separate argv tokens. The spec-file format can override named spec strings or include other spec files. Repository-owned Cargo `link-arg`/`link-args` can therefore alter which subprocesses or switches the nominal linker driver uses with either `-specs=file` or `-specs file`, without changing the visible `linker=` setting. + +Rustc and rustdoc external-input flags are also provenance-bearing. Git-owned Cargo `rustflags` and `rustdocflags` can use `-L` to widen library search paths or `--extern` to select an external crate; rustc `-l` can request native libraries. These repository-owned forms are fail-closed until exact artifact provenance is modeled. This is distinct from Cargo's own dependency wiring: the canonical production topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`. + +If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution or external-input path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, linker script, native input, or adapter implementation. -- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers only Git-owned `.cargo/config.toml` and `.cargo/config` files. -- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select one of the modeled executables, alter modeled driver subprocess authority, dynamically load modeled linker code, or select a modeled GNU linker script are not rejected merely because they occur under `[build]` or `[target]`. -- Target selection, command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, linker-plugin mechanisms outside the modeled GNU-compatible driver forwarding forms, non-`-B` driver/tool search-path mechanisms, non-GNU control-file mechanisms, implicit linker scripts supplied as ordinary positional inputs, and other arbitrary linker-argument effects remain separate review surfaces. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, linker script, external crate/native input, or adapter implementation. +- Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS`/`RUSTDOCFLAGS`/`CARGO_ENCODED_RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers Git-owned `.cargo/config.toml` and `.cargo/config` files. +- Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select a modeled executable, alter modeled driver subprocess authority, dynamically load modeled linker code, or widen a modeled external-input surface are not rejected merely because they occur under `[build]` or `[target]`. +- Command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, non-GNU linker/plugin/control-file mechanisms, non-`-B` driver/tool search-path mechanisms, and linker option grammars/operands not yet explicitly modeled remain separate review surfaces. ## RED -Commit `ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6` added a hostile repository fixture with `build.rustc-wrapper`. The predecessor source/config boundary rejected Cargo source overrides and build scripts but did not classify compiler-wrapper execution, so the commit preserves a structural RED. +Commit `ba9c9299d9a81d26ec68ed1c39a9dc11b08ddcb6` added a hostile repository fixture with `build.rustc-wrapper`. The predecessor source/config boundary rejected Cargo source overrides and build scripts but did not classify compiler-wrapper execution. Commit `a39caf95a38862f4cb4bcb68115b5e385bd6c26e` added a hostile `build.rustdoc` fixture. The predecessor compiler-authority contract accepted that setting because its execution-key set covered only `rustc` and the two rustc wrapper keys. -Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using `target..linker` and `target..runner`. The predecessor contract inspected only `[build]` and therefore accepted both target entries. - -Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added hostile Cargo-owned rustflags that selected `rustc -C linker=`. The predecessor rejected direct target `linker`/`runner` keys but ignored flag-derived linker selection. - -Commit `b0489000709243b27a9c849d2cada8e5fadd254e` added the corresponding Cargo-owned rustdocflags path using split and compact `-C linker=` forms. The predecessor helper inspected only rustflags, so both fixtures preserved a distinct structural RED for repository-selected rustdoc linker authority. - -Focused review of exact `a3434ebf7d8169a6cf5276d983d4a0c978a9720a` then found that the shared flag parser recognized only `-C` spellings even though Rust also documents the long `--codegen` interface. Commit `053b6cacd0d7d36dc619165aada99c1ac485b043` preserves that review finding as structural RED across both rustc and rustdoc flag surfaces. - -Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves a distinct linker-driver RED. It adds Cargo-owned rustflags using both split `-C link-arg=-fuse-ld=review-bypass-linker` and compact `--codegen=link-args=-fuse-ld=review-bypass-linker`. The predecessor parser only recognized the `linker=` codegen option and therefore allowed a repository flag to change the actual linker selected by a C compiler driver while the reviewed Rust source closure and nominal driver remained unchanged. +Commit `883ad62125af37e9afb2551803267284e21b7ea3` added target-specific hostile fixtures using `target..linker` and `target..runner`. The predecessor contract inspected only `[build]`. -Commit `ed5661090417e1b95943720f101f54a3a925ac96` preserves a second linker-driver RED using GNU-compatible driver search-path selection. It adds `-C link-arg=-Btools/review-bypass-binutils` and `--codegen=link-args=-B tools/review-bypass-binutils` hostile fixtures. The predecessor parser recognized direct `linker=` and `-fuse-ld=` selection but accepted both `-B` forms, even though GCC searches `-B` prefixes first when locating subprograms such as `ld`. +Commit `8b9ab4c34033b9e5990caad79169351e3d771039` added hostile Cargo-owned rustflags that selected `rustc -C linker=`. Commit `b0489000709243b27a9c849d2cada8e5fadd254e` added the corresponding rustdocflags path. Focused review of exact `a3434ebf7d8169a6cf5276d983d4a0c978a9720a` then found the long `--codegen` bypass, preserved by RED `053b6cacd0d7d36dc619165aada99c1ac485b043`. -Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves a third linker-driver RED using a Git-owned response file. The hostile fixture passes `-C link-arg=@tools/linker.rsp`, while that response file contains `-Btools/review-bypass-binutils`. The predecessor parser inspected only the visible `link-arg` token and therefore accepted the opaque response-file expansion path. +Commit `0b457b3936ef3fdd0546de06f279fc5cb2756e13` preserves the `-fuse-ld=` driver-selection RED. Commit `ed5661090417e1b95943720f101f54a3a925ac96` preserves the GNU-compatible `-B` driver-search RED. Commit `659e0b3914f19fd19c2d5d9dbeb9f40a43c00517` preserves the driver response-file RED. -Commit `6e94d6b86ac96a677ad6195c818bc94f2199e77b` preserves a linker-plugin execution RED. The hostile fixtures pass GNU-compatible driver forwarding forms for `-plugin`: `-Wl,-plugin,tools/review-bypass-linker.so` and a repeated `-Xlinker` sequence. The predecessor parser inspected linker executable reselection but accepted both forms even though GNU `ld` dynamically loads the named plugin into the linking process. +Commit `6e94d6b86ac96a677ad6195c818bc94f2199e77b` preserves linker-plugin execution REDs through GNU-compatible forwarding. Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves joined GCC `-specs=` RED; focused review of exact `c9440bdb2b1b610b6a52024a176acddbcd5e6cc5` identified the split `-specs ` bypass, preserved by `eb1ef86f6456e99bd581599b4bb474f9fa48fc02`. -Commit `f778fe6a3f5c0b5f97e1eee15ef42ef6b25fcc71` preserves the first GCC driver specs RED. Hostile fixtures pass joined `-specs=tools/review-bypass.specs` through both `link-arg` and `link-args`. The predecessor parser accepted these arguments even though GCC specs can override the driver rules that determine which subprocesses are invoked and which switches they receive. +Commit `43375ef80b1bcf6a0421f900a2f9cbf519741849` preserves explicit GNU linker-script native-input REDs. Focused review of exact `c0204371a1d8e06e3b68ed07437d5581f9a94762` found the forwarded linker-response-file gap, preserved and repaired by `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` with coverage for `-Wl,@file`, `--for-linker=@file`, and `-Xlinker @file`. -Focused review of exact `c9440bdb2b1b610b6a52024a176acddbcd5e6cc5` identified a remaining P1: the repair recognized only joined `-specs=` and still accepted split `-specs `. Commit `eb1ef86f6456e99bd581599b4bb474f9fa48fc02` preserves that review finding with hostile fixtures for both repeated `link-arg` and one `link-args` string. +Commit `657428dd607c2a384f95865ff59caba704a899d9` preserves repository-owned rustc `-L`/`-l` external-input REDs, and `ad5090dfb1e6de9eb1e2875365fa2b65ad37a5d9` preserves the equivalent compiler-driver forwarding gap. Commit `dbd9faa623f01652c2e75904af8bec614c2e6fc9` preserves Git-owned Cargo `--extern` external-crate input authority. -Commit `43375ef80b1bcf6a0421f900a2f9cbf519741849` preserves explicit GNU linker-script native-input REDs using direct `-T...`, forwarded `-Wl,--script=...`, and split `-Xlinker -T -Xlinker ...`. The predecessor classifier modeled executable/plugin/specs authority but did not reject scripts that can inject `INPUT(...)` or `GROUP(...)` native objects and archives. +Commit `e547203368da2aec62e2c94ab491eb0749d7d7b5` preserves suffix-bearing positional native-input REDs. Commit `8f5e49f5fe63d99773d25f13a3ab50648e02ac92` preserves the path-shaped extensionless input gap. Commit `ed86b335b4aa6cde223fe2e614bb26d39f789d8a` proves the remaining bare extensionless filename gap across direct and GNU-compatible forwarding forms. -Focused review of exact `c0204371a1d8e06e3b68ed07437d5581f9a94762` found one remaining response-file gap after the explicit script repair: direct driver `@file` already failed closed, but response files forwarded to the linker through `-Wl,`, `--for-linker=`, or `-Xlinker` did not. Commit `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` preserves and repairs that current-head review finding with hostile coverage for all three forwarding spellings. +Commit `5928b1a614c8620203675b609b75f5489338e06d` preserves the rustdoc external-input gap: build-level `rustdocflags --extern` and target-level `rustdocflags -Lnative=...` passed the predecessor because external-input classification was applied only to `rustflags`. These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair -Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate execution-authority contract that consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration. +Commit `80aaa562672211582d5b2de69edc79a984559fb3` introduced a separate execution-authority contract that consumes the canonical trusted-adapter production topology contract and then inspects Git-owned Cargo configuration. `345759105a0f0d2e88142df9961ea724b1055734` extended it to `build.rustdoc`; `e7390cdb12c483570940411c857554540f754763` added matching target `linker` and `runner` rejection. -Commit `345759105a0f0d2e88142df9961ea724b1055734` extended it to `build.rustdoc`; commit `e7390cdb12c483570940411c857554540f754763` added matching target `linker` and `runner` rejection. +Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closed Cargo-owned rustflags that select a linker. Commit `e157b9c5f33467425df794f42e704503de697232` reused the same narrow parser for Cargo-owned rustdocflags. Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closed the review-discovered long `--codegen` bypass. -Commit `743a5321bb72d83541b34f12ce83628f95f581f6` closed Cargo-owned rustflags that select a linker without banning unrelated compiler flags. Commit `e157b9c5f33467425df794f42e704503de697232` reused the same narrow parser for Cargo-owned rustdocflags while retaining unrelated rustdoc flags such as `--document-private-items` and `--cfg docsrs`. +Commit `d9e7d8ab4047bb25d3c6db0e195ec06240495aa9` adds `-fuse-ld=` driver selection. Commit `17a665ca0c895635cc52f3014a43cb9dea86e1b1` adds `-B` driver-program search selection. Commit `92ce887209f58b33ecd478ee09212bda70890894` classifies driver-level `@file` as opaque provenance. -Commit `7ee4b253ff4e213e949468274d126db214a63e4a` closed the review-discovered long-form bypass in that one shared parser. It treats split `-C` / `--codegen` and compact `-C...` / `--codegen=...` as equivalent codegen-option interfaces. +Commit `0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a` keeps one parser across multiple `link-arg` values and GNU-compatible `-Wl,`, `--for-linker=`, and `-Xlinker` forwarding while rejecting plugin loading. Commit `29dd6bb6548d4e003970743e7602753c3505cf83` adds joined `-specs=` authority; `688680c92e48bc5ad999327c46b366e5d27eeb5f` closes split `-specs `. -Commit `d9e7d8ab4047bb25d3c6db0e195ec06240495aa9` extends that same parser rather than adding a second flag scanner. It classifies `linker=` as direct linker selection and classifies `link-arg=` / `link-args=` only when their payload contains `-fuse-ld=`, the driver-level linker-selection mechanism documented by rustc. A control fixture retains ordinary `-C link-arg=-Wl,--as-needed`, avoiding a blanket ban on unrelated linker arguments. +Commit `8975224e86ea5ef9821d168efeaafa20702ec659` adds explicit GNU linker-script selection. Commit `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` closes GNU-forwarded linker response files without banning ordinary forwarded controls. -Commit `17a665ca0c895635cc52f3014a43cb9dea86e1b1` keeps that single parser and adds driver-argument executable selection for `-B`. Both attached and split-prefix forms inside `link-arg`/`link-args` now fail closed. The control `-C link-arg=-Wl,-Bsymbolic` remains allowed because it is explicitly forwarded to the actual linker rather than interpreted by the compiler driver as a program-search prefix. +Commit `d3a1790c50c393e0328854a2dd7fe4d9aaf3d5c4` closes top-level Git-owned Cargo rustc `-L`/`-l`; `a0f8525b57837ac119ef7b2af9c1daa759ef12f4` extends the same external-input classifier to driver/direct-forwarding forms. Commit `098a596029c0cf339c070604a265593540822956` closes Git-owned Cargo `--extern` external-crate inputs. -Commit `92ce887209f58b33ecd478ee09212bda70890894` keeps the same parser and classifies driver-level `@file` arguments as opaque executable-selection provenance. It does not attempt to parse response files recursively or allowlist their paths: GCC response files can recursively expand additional response files, so path review alone would not establish the effective driver command. A future relaxation requires exact recursive content containment and driver-semantics provenance on the same reviewed tree. +Commit `e73d221cf28aa25ae6fbd941db95d5d923c7e883` first closes common suffix-bearing positional native inputs. `cb95d5d37050bb7da70f1782da2e63a9b4583734` closes path-shaped extensionless inputs. Commit `5f070bf0b87ae513cf06badda29914e579f850ee` replaces those shape heuristics with an arity-aware direct-linker parser: every unconsumed non-option token in the modeled direct/GNU-compatible grammar fails closed, while explicitly modeled option operands such as `-z relro` remain allowed. -Commit `0ae660fa0c182dc9776aef95d9ed2d2bea7bfa2a` extends the same Cargo flag parser again instead of introducing a second linker grammar. It preserves linker-driver arguments across multiple `link-arg` codegen options, decodes the GNU-compatible `-Wl,` and `--for-linker=` forwarding forms, and recognizes `-Xlinker` followed by `-plugin`/`--plugin`. Only the plugin-loading forms fail closed; non-plugin forwarding such as `-Wl,--as-needed`, `-Xlinker --as-needed`, and `--for-linker=--as-needed` remains permitted. Commit `c42b7aa3550cc87f79acf46e3e1732e5098cddb4` adds hostile and control coverage for all three forwarding spellings. +Commit `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b` applies the existing external-input classifier to build- and target-level `rustdocflags`, closing repository-owned rustdoc `--extern`/`-L` forms without adding another Cargo topology/config scanner. The dedicated rustdoc trace is `docs/traceability/browser-session-rustdoc-external-input-authority.md`. -Commit `29dd6bb6548d4e003970743e7602753c3505cf83` keeps the same driver-argument classifier and adds joined `-specs=` as fail-closed execution authority. It does not parse or allowlist a specs file because GCC permits specs to override subprocess command construction and to include other specs files; a future relaxation therefore requires recursive exact-tree containment and subprocess-semantics provenance rather than path review alone. The hostile specs contract also retains an unrelated `-pthread` control. +The modeled fail-closed execution/input-authority surfaces now include: -Commit `688680c92e48bc5ad999327c46b366e5d27eeb5f` repairs the review-discovered split-form bypass by treating the exact driver token `-specs` as execution authority in addition to joined `-specs=`. Because `link-arg` values are accumulated before classification and `link-args` values are tokenized into the same list, both repeated `link-arg=-specs` + `link-arg=` and `link-args=-specs ` now fail closed without introducing another parser. A lone malformed `-specs` token also fails closed rather than being treated as ordinary linker tuning. +- `build.rustc`, `build.rustc-wrapper`, `build.rustc-workspace-wrapper`, and `build.rustdoc`; +- `target..linker` and `target..runner`; +- build/target `rustflags` and `rustdocflags` that select a linker through `-C`/`--codegen`; +- driver linker reselection through `-fuse-ld=` or `-B`; +- driver/linker response files (`@file`) in the modeled direct/GNU forwarding forms; +- linker plugin loading, explicit `-T`/`--script`, GCC `-specs=` / `-specs `, and modeled rustc-managed native-tool selectors; +- Git-owned Cargo rustc/rustdoc external-input widening through modeled `-L`, `-l`, and `--extern` forms; +- direct/GNU-forwarded positional inputs, including suffix-bearing objects/archives, path-shaped extensionless inputs, and bare extensionless filenames/implicit-script candidates. -Commit `8975224e86ea5ef9821d168efeaafa20702ec659` extends the shared classifier to explicit GNU linker-script selection instead of introducing a second native-input parser. Direct `-T`/`--script`, GNU-compatible `-Wl,`/`--for-linker=` forwarding, and split `-Xlinker` script selection fail closed because scripts can inject unreviewed native inputs. This is already enforced and covered by `tests/test_browser_session_linker_script_provenance_contract.py`; it is not deferred follow-up work. - -Commit `b0af9ed4251d4195c4f71893b9550cdbb38ab0b8` closes the review-discovered linker-level response-file bypass in that same classifier. Every argument decoded from `-Wl,` or `--for-linker=` and the argument following `-Xlinker` is now checked for leading `@`. Ordinary forwarded controls such as `-Wl,-Bsymbolic` and `-Xlinker --as-needed` remain outside this fail-closed rule. - -The modeled fail-closed execution/input-authority surfaces are now: - -- `build.rustc` -- `build.rustc-wrapper` -- `build.rustc-workspace-wrapper` -- `build.rustdoc` -- `target..linker` -- `target..runner` -- `build.rustflags` and matching target `rustflags` when `-C` or `--codegen` selects `linker=` -- `build.rustdocflags` and matching target `rustdocflags` when `-C` or `--codegen` selects `linker=` -- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` uses `-fuse-ld=` to re-select the actual linker behind a compiler driver -- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies a driver `-B` program-search prefix that can redirect the `ld` executable -- Cargo-owned rustc/rustdoc flag surfaces when `link-arg` or `link-args` supplies an opaque driver response file (`@file`) -- Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding passes an opaque linker response file through `-Wl,`, `--for-linker=`, or `-Xlinker` -- Cargo-owned rustc/rustdoc flag surfaces when GNU-compatible driver forwarding requests linker plugin loading through `-Wl,`, `--for-linker=`, or `-Xlinker` -- Cargo-owned rustc/rustdoc flag surfaces when direct or GNU-forwarded `-T` / `--script` selects a linker script that can extend native input authority -- Cargo-owned rustc/rustdoc flag surfaces when the nominal GCC-compatible driver receives joined `-specs=` or split `-specs ` and can replace its subprocess/switch rules - -A separate contract was chosen instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, dynamically loaded linker code, and explicit native-input control files are not package topology. A blanket rustflags/rustdocflags or linker-argument ban was rejected because non-execution-authority flags are common and do not by themselves justify widening this Browser Session provenance boundary. Allowlisting executable, plugin, response-file, specs-file, or linker-script paths was also rejected because a path alone does not establish immutable executable identity, recursively expanded arguments/includes, native inputs, behavior, or provenance. +A separate contract is retained instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, dynamically loaded linker code, and external-input argument authority are not package topology. A blanket rustflags/rustdocflags or linker-argument ban remains rejected because non-authority flags are common and do not justify widening this boundary. Path-only allowlists remain insufficient because they do not establish immutable executable/artifact identity, recursively expanded arguments/includes, transitive native inputs, behavior, or provenance. ## Security effect and residual risk -The repair closes modeled Git-owned execution/input-provenance gaps that could otherwise place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose the linker that emits repository artifacts, interpose a runner around repository test/run binaries, select a linker directly through Cargo-owned rustc/rustdoc flags, re-select the actual linker behind a nominal C compiler driver with `-fuse-ld=`, redirect a GNU-compatible compiler driver's `ld` lookup with `-B`, hide either mechanism behind a recursively expanded driver response file, delegate opaque response-file parsing to the linker through GNU forwarding, dynamically load repository-selected linker plugin code, inject native objects/archives through an explicitly selected GNU linker script, or replace GCC driver subprocess/switch rules through either spelling of `-specs` while the reviewed Rust source closure remained unchanged. +The repair closes the modeled Git-owned execution/input-provenance paths that could place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose/interpose the linker or runner, alter GCC driver subprocess rules, load linker code, inject explicit/implicit scripts or positional native inputs, widen external-library search paths, or add external crates through repository-owned Cargo flags while the reviewed Rust source closure remained unchanged. -It does not prove that CI environment variables, toolchain installation, runner images, external binaries, command-line `cargo rustc`/`cargo rustdoc` flags, non-GNU linker/plugin/control-file mechanisms, non-`-B` driver/tool search-path mechanisms, implicit linker scripts supplied as ordinary positional inputs, external native libraries selected through search paths, or arbitrary linker arguments are trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Any future claim of complete compiler/linker provenance must account for those surfaces with realistic hostile cases rather than a catch-all Cargo-config ban. +It does **not** prove CI environment variables, direct `cargo rustc` / `cargo rustdoc` trailing arguments, runner images, sysroot/rustup/toolchain composition, external binaries, non-GNU linker/plugin/control-file grammars, non-`-B` driver/tool search-path mechanisms, or unmodeled arguments with equivalent semantics trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Path-shaped and bare extensionless positional inputs, Git-owned Cargo `--extern`, and Git-owned Cargo rustdoc `--extern`/`-L` are no longer residual gaps in this repository-owned Cargo boundary. ## Acceptance and follow-up -1. Obtain independent current-head review of the linker-script and forwarded-response-file repair together with the retained GCC-specs, linker-plugin, and executable-selection contracts. -2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving the parent and child deltas. +1. Obtain independent current-head review of the newest positional-input, rustdoc external-input, and lifecycle-contract repairs together with retained execution/input authority contracts. +2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving all valid parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review non-GNU control-file mechanisms, implicit script inputs, and non-`-B`/non-response-file driver/toolchain search-path manipulation separately; add a contract only when a realistic execution/provenance escape is demonstrated. -5. If any blocked executable override, response file, linker plugin, specs file, or linker script is ever required, replace the fail-closed rule only with an explicit design covering immutable executable/input identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. Review environment/direct-CLI injection, non-GNU control/input grammars, unmodeled option arities, and toolchain/sysroot composition separately; add a contract only when a realistic execution/provenance escape is demonstrated. +5. If any blocked executable, response file, linker plugin, specs file, linker script, or external artifact is required, replace fail-closed only with an explicit design covering immutable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References @@ -140,8 +121,10 @@ Free Software Foundation. (n.d.). *Plugins*. *GNU ld*. Retrieved September 18, 2 Free Software Foundation. (n.d.). *Scripts*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Scripts.html +Free Software Foundation. (n.d.). *Implicit linker scripts*. *GNU ld*. Retrieved September 18, 2026, from https://sourceware.org/binutils/docs/ld/Implicit-Linker-Scripts.html + The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html The Rust Project Developers. (n.d.). *Command-line arguments*. *The rustdoc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html -The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/ \ No newline at end of file +The Rust Project Developers. (n.d.). *Codegen options*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/codegen-options/ From f25634aaee2486ae043c410f1589d3d60f511485 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 11:02:20 +0900 Subject: [PATCH 363/632] test(browser-session): expose linker-plugin-lto path provenance gap --- ...wser_session_linker_plugin_lto_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_browser_session_linker_plugin_lto_contract.py diff --git a/tests/test_browser_session_linker_plugin_lto_contract.py b/tests/test_browser_session_linker_plugin_lto_contract.py new file mode 100644 index 000000000..dce01da8d --- /dev/null +++ b/tests/test_browser_session_linker_plugin_lto_contract.py @@ -0,0 +1,53 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerPluginLtoContractTests(unittest.TestCase): + """Keep repository-selected rustc linker plugins inside reviewed execution provenance.""" + + def test_repository_linker_plugin_lto_path_fails_closed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_boolean_linker_plugin_lto_setting_is_not_a_repository_plugin_path(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["-C", "linker-plugin-lto=no"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(__import__("tempfile").TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From c368afacacf261a84126150d9a06e1271a479246 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 11:04:55 +0900 Subject: [PATCH 364/632] fix(browser-session): fail closed on linker-plugin-lto paths --- ...ser_session_cargo_compiler_authority_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index d06393f72..590e223ca 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -21,6 +21,9 @@ LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) +LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( + {"y", "yes", "on", "true", "n", "no", "off", "false"} +) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -71,6 +74,14 @@ def _linker_option_uses_response_file(argument: str) -> bool: return argument.startswith("@") +def _codegen_option_selects_linker_plugin(option: str) -> bool: + """Return whether one rustc codegen option names an explicit linker-plugin artifact.""" + if not option.startswith("linker-plugin-lto="): + return False + value = option.partition("=")[2] + return value not in LINKER_PLUGIN_LTO_BOOLEAN_VALUES + + def _linker_argument_is_positional_native_input(argument: str) -> bool: """Return whether one unconsumed linker token is a positional external input.""" return bool(argument) and not argument.startswith("-") @@ -180,6 +191,8 @@ def _flags_select_linker(value: object) -> bool: return True if option == "dlltool" or option.startswith("dlltool="): return True + if _codegen_option_selects_linker_plugin(option): + return True if option.startswith("link-arg="): linker_driver_arguments.append(option.partition("=")[2]) elif option.startswith("link-args="): From fc151d304585c77dca1d013f981a7972a8c0d8c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 11:05:14 +0900 Subject: [PATCH 365/632] docs(browser-session): trace linker-plugin-lto authority --- ...ser-session-linker-plugin-lto-authority.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/traceability/browser-session-linker-plugin-lto-authority.md diff --git a/docs/traceability/browser-session-linker-plugin-lto-authority.md b/docs/traceability/browser-session-linker-plugin-lto-authority.md new file mode 100644 index 000000000..9bcc6d89d --- /dev/null +++ b/docs/traceability/browser-session-linker-plugin-lto-authority.md @@ -0,0 +1,45 @@ +# Browser Session rustc linker-plugin-LTO authority traceability + +Status: Draft contract evidence on PR #317. This document records a source-semantic RED→repair chain and does not claim hosted repository/security GREEN. + +## Problem + +The Browser Session Cargo compiler-authority contract already fails closed on linker plugins selected through linker arguments such as GNU `-plugin` / `--plugin`. That does not cover rustc's own `linker-plugin-lto` codegen option. + +Rust documents `-C linker-plugin-lto` as the control that defers LTO to the native link step. Its documented values are the usual boolean forms or a **path to the linker plugin**. The rustc book gives an explicit example using `-Clinker-plugin-lto="/path/to/LLVMgold.so"`. A Git-owned Cargo `rustflags` or `rustdocflags` entry can therefore name a plugin artifact without using the already-modeled linker-argument plugin surface. + +Primary references: + +- Rust project. (2026). *Codegen options: linker-plugin-lto*. https://doc.rust-lang.org/rustc/codegen-options/#linker-plugin-lto +- Rust project. (2026). *Linker-plugin-based LTO*. https://doc.rust-lang.org/rustc/linker-plugin-lto.html + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the owner for Git-owned Cargo-selected Rust/linker execution and external-input authority. +- Boolean `linker-plugin-lto` enable/disable values do not themselves name a repository-selected plugin artifact and remain permitted by this slice. +- An explicit plugin path is not accepted merely because it is repository-relative. Allowing one requires immutable artifact identity, containment, toolchain compatibility, SBOM/provenance, and exact-head executable evidence in the same reviewed delta. +- Environment `RUSTFLAGS` / `CARGO_ENCODED_RUSTFLAGS`, direct CLI flags, runner/toolchain composition, and plugins selected outside Git-owned Cargo configuration remain CI/runtime or future modeled surfaces. + +## RED + +Commit `f25634aaee2486ae043c410f1589d3d60f511485` adds `tests/test_browser_session_linker_plugin_lto_contract.py`. The hostile fixture uses: + +```toml +[build] +rustflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"] +``` + +The predecessor exact `569bfc807df8e4f3f452f04e5dfb865d09086fac` parsed the `-C` option but did not classify `linker-plugin-lto=`, so the canonical Cargo compiler-authority helper did not fail closed. The same fixture keeps `linker-plugin-lto=no` as a control so the repair is not a blanket LTO ban. + +## Decision and repair + +Commit `c368afacacf261a84126150d9a06e1271a479246` extends the existing codegen-option classifier instead of adding another Cargo scanner. `_codegen_option_selects_linker_plugin()` distinguishes the documented boolean values (`y`, `yes`, `on`, `true`, `n`, `no`, `off`, `false`) from an explicit value that names a plugin artifact. Path-valued or otherwise unrecognized `linker-plugin-lto=` settings fail closed through the existing `rustflags:codegen linker` / `rustdocflags:codegen linker` authority path. + +The repair is deliberately conservative. Bare `linker-plugin-lto` remains allowed because it enables linker-plugin LTO without naming a repository-selected plugin path. Unknown explicit values fail closed rather than being guessed safe. + +## Security effect and residual risk + +This closes the Git-owned Cargo path by which a reviewed Rust source tree could name an explicit LLVM linker-plugin artifact through rustc codegen configuration while avoiding the existing linker `-plugin` checks. + +It does not prove the selected system linker/LTO toolchain trustworthy, validate ambient environment flags, or authorize an explicit plugin artifact. Any future explicit plugin use must arrive with artifact provenance and executable compatibility evidence rather than widening this contract by path alone. From 812c80878f8fd68f482d9cf97f5ea4f33ce5d8c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 11:05:59 +0900 Subject: [PATCH 366/632] test(browser-session): cover linker-plugin-lto flag forms --- ...wser_session_linker_plugin_lto_contract.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/tests/test_browser_session_linker_plugin_lto_contract.py b/tests/test_browser_session_linker_plugin_lto_contract.py index dce01da8d..88b79fcd2 100644 --- a/tests/test_browser_session_linker_plugin_lto_contract.py +++ b/tests/test_browser_session_linker_plugin_lto_contract.py @@ -1,5 +1,6 @@ import importlib.util import pathlib +import tempfile import unittest @@ -16,21 +17,29 @@ class BrowserSessionLinkerPluginLtoContractTests(unittest.TestCase): """Keep repository-selected rustc linker plugins inside reviewed execution provenance.""" - def test_repository_linker_plugin_lto_path_fails_closed(self) -> None: - root = self._workspace_with_config( - '[build]\nrustflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"]\n' + def test_repository_linker_plugin_lto_paths_fail_closed(self) -> None: + hostile_configs = ( + '[build]\nrustflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"]\n', + '[build]\nrustflags = ["-Clinker-plugin-lto=tools/review-bypass-llvmgold.so"]\n', + "[target.'cfg(unix)']\nrustflags = [\"--codegen=linker-plugin-lto=tools/review-bypass-llvmgold.so\"]\n", + '[build]\nrustdocflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"]\n', ) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) - - def test_boolean_linker_plugin_lto_setting_is_not_a_repository_plugin_path(self) -> None: - root = self._workspace_with_config( - '[build]\nrustflags = ["-C", "linker-plugin-lto=no"]\n' - ) - authority._assert_no_repository_cargo_compiler_execution_overrides(root) + for config_text in hostile_configs: + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_boolean_linker_plugin_lto_settings_do_not_name_repository_plugin_paths(self) -> None: + for option in ("linker-plugin-lto", "linker-plugin-lto=yes", "linker-plugin-lto=no"): + with self.subTest(option=option): + root = self._workspace_with_config( + f'[build]\nrustflags = ["-C", "{option}"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) def _workspace_with_config(self, config_text: str) -> pathlib.Path: - directory = self.enterContext(__import__("tempfile").TemporaryDirectory()) + directory = self.enterContext(tempfile.TemporaryDirectory()) root = pathlib.Path(directory) (root / "Cargo.toml").write_text( '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', From a3135165afd9a1a45c4f1650765260c19f1e26f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 11:06:23 +0900 Subject: [PATCH 367/632] docs(browser-session): record linker-plugin-lto coverage --- .../browser-session-linker-plugin-lto-authority.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-linker-plugin-lto-authority.md b/docs/traceability/browser-session-linker-plugin-lto-authority.md index 9bcc6d89d..3a46e0a34 100644 --- a/docs/traceability/browser-session-linker-plugin-lto-authority.md +++ b/docs/traceability/browser-session-linker-plugin-lto-authority.md @@ -23,19 +23,21 @@ Primary references: ## RED -Commit `f25634aaee2486ae043c410f1589d3d60f511485` adds `tests/test_browser_session_linker_plugin_lto_contract.py`. The hostile fixture uses: +Commit `f25634aaee2486ae043c410f1589d3d60f511485` adds `tests/test_browser_session_linker_plugin_lto_contract.py`. The initial hostile fixture uses: ```toml [build] rustflags = ["-C", "linker-plugin-lto=tools/review-bypass-llvmgold.so"] ``` -The predecessor exact `569bfc807df8e4f3f452f04e5dfb865d09086fac` parsed the `-C` option but did not classify `linker-plugin-lto=`, so the canonical Cargo compiler-authority helper did not fail closed. The same fixture keeps `linker-plugin-lto=no` as a control so the repair is not a blanket LTO ban. +The predecessor exact `569bfc807df8e4f3f452f04e5dfb865d09086fac` parsed the `-C` option but did not classify `linker-plugin-lto=`, so the canonical Cargo compiler-authority helper did not fail closed. ## Decision and repair Commit `c368afacacf261a84126150d9a06e1271a479246` extends the existing codegen-option classifier instead of adding another Cargo scanner. `_codegen_option_selects_linker_plugin()` distinguishes the documented boolean values (`y`, `yes`, `on`, `true`, `n`, `no`, `off`, `false`) from an explicit value that names a plugin artifact. Path-valued or otherwise unrecognized `linker-plugin-lto=` settings fail closed through the existing `rustflags:codegen linker` / `rustdocflags:codegen linker` authority path. +Commit `812c80878f8fd68f482d9cf97f5ea4f33ce5d8c7` broadens the focused contract without duplicating production topology: it covers split `-C`, compact `-C...`, long `--codegen=...`, target-level rustflags, and build-level rustdocflags. Bare `linker-plugin-lto` plus documented boolean `yes`/`no` remain control cases and do not name a repository plugin path. + The repair is deliberately conservative. Bare `linker-plugin-lto` remains allowed because it enables linker-plugin LTO without naming a repository-selected plugin path. Unknown explicit values fail closed rather than being guessed safe. ## Security effect and residual risk From 3943f268395180d199992709393190510ae48b2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 12:03:00 +0900 Subject: [PATCH 368/632] test(browser-session): preserve Cargo sysroot provenance RED --- ..._browser_session_sysroot_input_contract.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_browser_session_sysroot_input_contract.py diff --git a/tests/test_browser_session_sysroot_input_contract.py b/tests/test_browser_session_sysroot_input_contract.py new file mode 100644 index 000000000..49be787b1 --- /dev/null +++ b/tests/test_browser_session_sysroot_input_contract.py @@ -0,0 +1,61 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionSysrootInputContractTests(unittest.TestCase): + """Keep repository-selected Rust sysroots inside reviewed compiler-input provenance.""" + + def test_repository_sysroot_overrides_fail_closed(self) -> None: + hostile_configs = ( + '[build]\nrustflags = ["--sysroot", "tools/review-bypass-sysroot"]\n', + '[build]\nrustflags = ["--sysroot=tools/review-bypass-sysroot"]\n', + "[target.'cfg(unix)']\nrustflags = [\"--sysroot\", \"tools/review-bypass-sysroot\"]\n", + '[build]\nrustdocflags = ["--sysroot=tools/review-bypass-sysroot"]\n', + "[target.'cfg(unix)']\nrustdocflags = [\"--sysroot\", \"tools/review-bypass-sysroot\"]\n", + ) + for config_text in hostile_configs: + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_rust_flags_do_not_extend_sysroot_input_authority(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["--remap-path-prefix", "src=/workspace/src"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(tempfile.TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From f20401f0368ac9ab5f9756fc285972791526887c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 12:04:41 +0900 Subject: [PATCH 369/632] fix(browser-session): fail closed on repository sysroot overrides --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 590e223ca..59a067643 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -49,7 +49,9 @@ def _linker_argument_extends_external_inputs(argument: str) -> bool: def _rustc_argument_extends_external_inputs(argument: str) -> bool: - """Return whether one rustc argument widens external crate or native-library inputs.""" + """Return whether one rustc argument widens external crate, native-library, or sysroot inputs.""" + if argument == "--sysroot" or argument.startswith("--sysroot="): + return True if argument == "--extern" or argument.startswith("--extern="): return True return _linker_argument_extends_external_inputs(argument) @@ -159,7 +161,7 @@ def _flag_arguments(value: object) -> list[str]: def _flags_extend_external_link_inputs(value: object) -> bool: - """Return whether Git-owned rustc flags widen external crate or native-library inputs.""" + """Return whether Git-owned Rust flags widen external compiler/documentation inputs.""" return any(_rustc_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) From 0a331be80f0a318b147d759b6fa4f9b4353b36fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 12:05:58 +0900 Subject: [PATCH 370/632] docs(browser-session): trace Cargo sysroot input authority --- ...browser-session-sysroot-input-authority.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/traceability/browser-session-sysroot-input-authority.md diff --git a/docs/traceability/browser-session-sysroot-input-authority.md b/docs/traceability/browser-session-sysroot-input-authority.md new file mode 100644 index 000000000..87906ae65 --- /dev/null +++ b/docs/traceability/browser-session-sysroot-input-authority.md @@ -0,0 +1,44 @@ +# Browser Session sysroot input authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim hosted repository/security GREEN. + +## Problem + +The Browser Session Cargo authority contract already fails closed on repository-owned compiler/linker executable replacement and modeled external crate/native-library inputs. A separate Rust compiler input surface remained: Git-owned Cargo `rustflags` or `rustdocflags` could pass `--sysroot ` or `--sysroot=` and select a different Rust sysroot while the reviewed Cargo package/source closure remained unchanged. + +`rustc --sysroot` overrides the system root used to find crates distributed with Rust. `rustdoc --sysroot` likewise changes the sysroot used while compiling documentation. A repository-selected sysroot is therefore compiler/documentation input authority, not ordinary diagnostic or optimization configuration. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. This supplemental contract consumes that authority and does not duplicate Cargo topology discovery. +- A repository-selected sysroot remains fail-closed until a separate reviewed contract establishes immutable toolchain/sysroot identity, artifact provenance, SBOM/reproducibility expectations, and release/runtime qualification. +- This contract covers Git-owned `.cargo/config.toml` and `.cargo/config` `rustflags`/`rustdocflags`. Environment `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, direct compiler/documentation CLI arguments, runner images, rustup/toolchain installation, and ambient sysroot composition remain CI/runtime-owner surfaces. +- Unrelated Rust flags are not rejected merely because they occur in `rustflags` or `rustdocflags`. + +## RED + +Commit `3943f268395180d199992709393190510ae48b2d` adds `tests/test_browser_session_sysroot_input_contract.py`. The hostile fixtures cover: + +- build-level split `rustflags = ["--sysroot", "tools/review-bypass-sysroot"]`; +- build-level equals-form `rustflags = ["--sysroot=tools/review-bypass-sysroot"]`; +- target-scoped split `rustflags`; +- build-level equals-form `rustdocflags`; +- target-scoped split `rustdocflags`. + +The predecessor classifier handled `--extern`, `-L`, and `-l` but did not classify `--sysroot`, so these fixtures preserve the missing compiler/documentation-input authority as a source-semantic RED. The control fixture keeps unrelated `--remap-path-prefix` configuration allowed. + +## Decision and repair + +Commit `f20401f0368ac9ab5f9756fc285972791526887c` extends the existing `_rustc_argument_extends_external_inputs()` classifier rather than adding a new Cargo scanner. Split and equals `--sysroot` spellings now classify as external compiler/documentation input authority. Because build/target `rustflags` and `rustdocflags` already consume `_flags_extend_external_link_inputs()`, all four Git-owned Cargo configuration paths fail closed through the same reviewed authority boundary. + +The repair intentionally does not inspect or allowlist sysroot contents. A path allowlist would not establish that the standard-library crates, compiler-private crates, metadata, native objects, or supporting toolchain artifacts are the immutable reviewed artifacts expected by a release. + +## Security effect and residual risk + +The repair closes the modeled Git-owned Cargo path that could replace rustc/rustdoc sysroot inputs without changing the reviewed Browser Session Cargo package/source closure. It does not prove environment- or direct-CLI-selected sysroots, runner-image contents, rustup/toolchain installation state, or the integrity/reproducibility of the default sysroot. Those remain CI/release/toolchain provenance concerns and must not be inferred from this source contract. + +## Primary references + +The Rust Project. (n.d.). *Command-line arguments: `--sysroot`: override the system root*. The rustc book. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/command-line-arguments.html#--sysroot-override-the-system-root + +The Rust Project. (n.d.). *Command-line arguments: `--sysroot`: override the system root*. The rustdoc book. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustdoc/command-line-arguments.html#--sysroot-override-the-system-root From 44889d1762894f3a37d06e6026710d71cc9ac5b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 12:07:11 +0900 Subject: [PATCH 371/632] docs(browser-session): currentize sysroot input authority --- ...rowser-session-cargo-compiler-authority.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/traceability/browser-session-cargo-compiler-authority.md b/docs/traceability/browser-session-cargo-compiler-authority.md index 9e46d0270..1014de161 100644 --- a/docs/traceability/browser-session-cargo-compiler-authority.md +++ b/docs/traceability/browser-session-cargo-compiler-authority.md @@ -22,14 +22,14 @@ Ordinary positional linker inputs are now part of the same authority boundary. G GCC itself is a driver that invokes preprocessing, compilation, assembly, and linking subprocesses according to spec strings. GCC documents that command-line `-specs=file` overrides built-in specs, while the driver also accepts the option and its file argument as separate argv tokens. The spec-file format can override named spec strings or include other spec files. Repository-owned Cargo `link-arg`/`link-args` can therefore alter which subprocesses or switches the nominal linker driver uses with either `-specs=file` or `-specs file`, without changing the visible `linker=` setting. -Rustc and rustdoc external-input flags are also provenance-bearing. Git-owned Cargo `rustflags` and `rustdocflags` can use `-L` to widen library search paths or `--extern` to select an external crate; rustc `-l` can request native libraries. These repository-owned forms are fail-closed until exact artifact provenance is modeled. This is distinct from Cargo's own dependency wiring: the canonical production topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`. +Rustc and rustdoc external-input flags are also provenance-bearing. Git-owned Cargo `rustflags` and `rustdocflags` can use `-L` to widen library search paths, `--extern` to select an external crate, or `--sysroot` to replace the Rust system root used to resolve distribution-provided compiler/documentation inputs; rustc `-l` can request native libraries. These repository-owned forms are fail-closed until exact artifact provenance is modeled. This is distinct from Cargo's own dependency wiring: the canonical production topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`. If any of these settings enters a reviewed Browser Session production workspace without a separate provenance contract, the effective build/test/documentation execution or external-input path is no longer represented by the existing exact-tree source closure. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source override discovery. -- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, linker script, external crate/native input, or adapter implementation. +- This contract does not authorize a future wrapper, custom compiler, custom rustdoc executable, linker, runner, generated source path, response file, linker plugin, GCC specs file, linker script, external crate/native input, repository-selected sysroot, or adapter implementation. - Environment-owned `RUSTC`/`RUSTC_WRAPPER`/`RUSTDOC`/`RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS`/`RUSTDOCFLAGS`/`CARGO_ENCODED_RUSTDOCFLAGS` authority belongs to the CI/runtime owner. This repository contract covers Git-owned `.cargo/config.toml` and `.cargo/config` files. - Ordinary Cargo settings, rustflags, rustdocflags, and linker arguments that do not select a modeled executable, alter modeled driver subprocess authority, dynamically load modeled linker code, or widen a modeled external-input surface are not rejected merely because they occur under `[build]` or `[target]`. - Command-line `cargo rustc`/`cargo rustdoc` flags, environment/toolchain configuration, non-GNU linker/plugin/control-file mechanisms, non-`-B` driver/tool search-path mechanisms, and linker option grammars/operands not yet explicitly modeled remain separate review surfaces. @@ -56,6 +56,8 @@ Commit `e547203368da2aec62e2c94ab491eb0749d7d7b5` preserves suffix-bearing posit Commit `5928b1a614c8620203675b609b75f5489338e06d` preserves the rustdoc external-input gap: build-level `rustdocflags --extern` and target-level `rustdocflags -Lnative=...` passed the predecessor because external-input classification was applied only to `rustflags`. +Commit `3943f268395180d199992709393190510ae48b2d` preserves the repository-selected sysroot gap. Split and equals-form `--sysroot` values in build/target `rustflags` and `rustdocflags` passed the predecessor because the external-input classifier covered `--extern`, `-L`, and `-l` but not Rust sysroot selection. + These are source-level RED fixtures; no hosted-run result is inferred from the Draft branch. ## Decision and repair @@ -76,6 +78,8 @@ Commit `e73d221cf28aa25ae6fbd941db95d5d923c7e883` first closes common suffix-bea Commit `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b` applies the existing external-input classifier to build- and target-level `rustdocflags`, closing repository-owned rustdoc `--extern`/`-L` forms without adding another Cargo topology/config scanner. The dedicated rustdoc trace is `docs/traceability/browser-session-rustdoc-external-input-authority.md`. +Commit `f20401f0368ac9ab5f9756fc285972791526887c` extends that same external-input classifier to split and equals-form `--sysroot` for both rustflags and rustdocflags. The focused evidence is `tests/test_browser_session_sysroot_input_contract.py`; `docs/traceability/browser-session-sysroot-input-authority.md` records the toolchain/sysroot provenance boundary without creating another Cargo topology owner. + The modeled fail-closed execution/input-authority surfaces now include: - `build.rustc`, `build.rustc-wrapper`, `build.rustc-workspace-wrapper`, and `build.rustdoc`; @@ -84,24 +88,24 @@ The modeled fail-closed execution/input-authority surfaces now include: - driver linker reselection through `-fuse-ld=` or `-B`; - driver/linker response files (`@file`) in the modeled direct/GNU forwarding forms; - linker plugin loading, explicit `-T`/`--script`, GCC `-specs=` / `-specs `, and modeled rustc-managed native-tool selectors; -- Git-owned Cargo rustc/rustdoc external-input widening through modeled `-L`, `-l`, and `--extern` forms; +- Git-owned Cargo rustc/rustdoc external-input widening through modeled `-L`, `-l`, `--extern`, and `--sysroot` forms; - direct/GNU-forwarded positional inputs, including suffix-bearing objects/archives, path-shaped extensionless inputs, and bare extensionless filenames/implicit-script candidates. A separate contract is retained instead of expanding Cargo package/source discovery because executable selection, driver subprocess authority, dynamically loaded linker code, and external-input argument authority are not package topology. A blanket rustflags/rustdocflags or linker-argument ban remains rejected because non-authority flags are common and do not justify widening this boundary. Path-only allowlists remain insufficient because they do not establish immutable executable/artifact identity, recursively expanded arguments/includes, transitive native inputs, behavior, or provenance. ## Security effect and residual risk -The repair closes the modeled Git-owned execution/input-provenance paths that could place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose/interpose the linker or runner, alter GCC driver subprocess rules, load linker code, inject explicit/implicit scripts or positional native inputs, widen external-library search paths, or add external crates through repository-owned Cargo flags while the reviewed Rust source closure remained unchanged. +The repair closes the modeled Git-owned execution/input-provenance paths that could place an unmodeled executable between Cargo and `rustc`, replace rustdoc, choose/interpose the linker or runner, alter GCC driver subprocess rules, load linker code, inject explicit/implicit scripts or positional native inputs, widen external-library search paths, add external crates, or replace the Rust sysroot through repository-owned Cargo flags while the reviewed Rust source closure remained unchanged. -It does **not** prove CI environment variables, direct `cargo rustc` / `cargo rustdoc` trailing arguments, runner images, sysroot/rustup/toolchain composition, external binaries, non-GNU linker/plugin/control-file grammars, non-`-B` driver/tool search-path mechanisms, or unmodeled arguments with equivalent semantics trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Path-shaped and bare extensionless positional inputs, Git-owned Cargo `--extern`, and Git-owned Cargo rustdoc `--extern`/`-L` are no longer residual gaps in this repository-owned Cargo boundary. +It does **not** prove CI environment variables, direct `cargo rustc` / `cargo rustdoc` trailing arguments, runner images, ambient/default sysroot contents, rustup/toolchain installation state, external binaries, non-GNU linker/plugin/control-file grammars, non-`-B` driver/tool search-path mechanisms, or unmodeled arguments with equivalent semantics trustworthy. Those controls remain with their canonical CI/supply-chain owners or future focused contracts. Git-owned Cargo `--sysroot`, path-shaped and bare extensionless positional inputs, Git-owned Cargo `--extern`, and Git-owned Cargo rustdoc `--extern`/`-L` are no longer residual gaps in this repository-owned Cargo boundary. ## Acceptance and follow-up -1. Obtain independent current-head review of the newest positional-input, rustdoc external-input, and lifecycle-contract repairs together with retained execution/input authority contracts. +1. Obtain independent current-head review of the newest sysroot-input, positional-input, rustdoc external-input, and lifecycle-contract repairs together with retained execution/input authority contracts. 2. After #229 exact-head required evidence becomes terminal, reconcile #317 by ordinary non-force ancestry while preserving all valid parent and child deltas. 3. Regenerate executable repository/security evidence on the reconciled exact head. -4. Review environment/direct-CLI injection, non-GNU control/input grammars, unmodeled option arities, and toolchain/sysroot composition separately; add a contract only when a realistic execution/provenance escape is demonstrated. -5. If any blocked executable, response file, linker plugin, specs file, linker script, or external artifact is required, replace fail-closed only with an explicit design covering immutable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. +4. Review environment/direct-CLI injection, non-GNU control/input grammars, unmodeled option arities, and ambient toolchain/default-sysroot composition separately; add a contract only when a realistic execution/provenance escape is demonstrated. +5. If any blocked executable, response file, linker plugin, specs file, linker script, external artifact, or custom sysroot is required, replace fail-closed only with an explicit design covering immutable identity, recursive argument/source provenance, SBOM/attestation, rollback, and buyer-visible evidence. ## References From f031bbc6154567c2b641879d00fa49d3412bc739 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 12:58:50 +0900 Subject: [PATCH 372/632] test(browser-session): expose rust response-file provenance gap --- ...ser_session_rust_response_file_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_browser_session_rust_response_file_contract.py diff --git a/tests/test_browser_session_rust_response_file_contract.py b/tests/test_browser_session_rust_response_file_contract.py new file mode 100644 index 000000000..ad185ce84 --- /dev/null +++ b/tests/test_browser_session_rust_response_file_contract.py @@ -0,0 +1,60 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionRustResponseFileContractTests(unittest.TestCase): + """Keep rustc/rustdoc response-file inputs inside reviewed Cargo provenance.""" + + def test_top_level_rust_response_files_fail_closed(self) -> None: + hostile_configs = ( + '[build]\nrustflags = ["@tools/review-bypass-rustc.args"]\n', + "[target.'cfg(unix)']\nrustflags = [\"@tools/review-bypass-rustc.args\"]\n", + '[build]\nrustdocflags = ["@tools/review-bypass-rustdoc.args"]\n', + "[target.'cfg(unix)']\nrustdocflags = [\"@tools/review-bypass-rustdoc.args\"]\n", + ) + for config_text in hostile_configs: + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_at_sign_inside_non_response_argument_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustflags = ["--cfg", "originweave_contact=\\\"ops@example.invalid\\\""]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(tempfile.TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From 0c3d76ae214328c124fab8a7c8adcb4d2452a493 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 13:01:12 +0900 Subject: [PATCH 373/632] fix(browser-session): reject rust response-file inputs --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 59a067643..14bbd413a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -49,7 +49,9 @@ def _linker_argument_extends_external_inputs(argument: str) -> bool: def _rustc_argument_extends_external_inputs(argument: str) -> bool: - """Return whether one rustc argument widens external crate, native-library, or sysroot inputs.""" + """Return whether one rustc/rustdoc argument widens opaque or external compiler inputs.""" + if argument.startswith("@"): + return True if argument == "--sysroot" or argument.startswith("--sysroot="): return True if argument == "--extern" or argument.startswith("--extern="): From 345b72ec49d02a1dcb0896eecb4fa64e6233a404 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 13:01:39 +0900 Subject: [PATCH 374/632] docs(browser-session): trace rust response-file authority --- ...er-session-rust-response-file-authority.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/traceability/browser-session-rust-response-file-authority.md diff --git a/docs/traceability/browser-session-rust-response-file-authority.md b/docs/traceability/browser-session-rust-response-file-authority.md new file mode 100644 index 000000000..f8cb01f78 --- /dev/null +++ b/docs/traceability/browser-session-rust-response-file-authority.md @@ -0,0 +1,32 @@ +# Browser Session Rust response-file authority + +## Problem + +Repository-owned Cargo `rustflags` and `rustdocflags` are reviewed compiler/documentation input surfaces. Both `rustc` and `rustdoc` support a top-level `@path` argument that opens a UTF-8 file and loads additional command-line options from it, one option per line. Treating only the visible Cargo flag list as authority therefore leaves an opaque indirection path: a Git-owned response file can introduce `--extern`, `--sysroot`, `-L`, `-l`, codegen linker selection, or other compiler inputs after the repository contract has inspected the outer configuration. + +This is distinct from linker response files passed through `-Wl,`, `--for-linker=`, or `-Xlinker`. Those are already handled by the linker-argument classifier. This contract closes the rustc/rustdoc top-level response-file boundary. + +## Authority and constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` owns repository-selected compiler/rustdoc execution and input authority. Supplemental tests consume that owner rather than rediscovering Cargo topology. + +A top-level Cargo `rustflags` or `rustdocflags` argument whose first character is `@` is fail-closed. The rule applies to both `[build]` and `[target.*]` configuration. An `@` appearing later inside an ordinary argument is not a response-file selector and is not rejected by this rule. + +## RED → repair evidence + +RED `f031bbc6154567c2b641879d00fa49d3412bc739` adds `tests/test_browser_session_rust_response_file_contract.py`. It covers build/target `rustflags` and build/target `rustdocflags` with top-level `@tools/...args` hostile fixtures while retaining an ordinary argument containing an internal `@` as a control. + +Repair `0c3d76ae214328c124fab8a7c8adcb4d2452a493` extends the existing `_rustc_argument_extends_external_inputs()` classifier. No new Cargo topology/config scanner is introduced; the existing build/target rustflags/rustdocflags call sites all inherit the same fail-closed rule. + +## Security effect + +A reviewed Cargo config can no longer hide compiler or rustdoc execution/input authority behind an opaque top-level response file. The repair preserves the existing explicit classifiers for `--extern`, `--sysroot`, `-L`, `-l`, rustc codegen linker/tool selection, and linker-level response files rather than replacing them with a separate policy path. + +This is a repository-source contract, not proof of the ambient execution environment. `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, direct command-line arguments, ancestor or user Cargo configuration, runner-installed toolchains/sysroots, and Cargo's own internally generated argument files remain CI/release/runtime provenance surfaces. Those surfaces must be controlled by the execution/release owner rather than inferred from Git source closure. + +## Primary references + +- The Rust Project. (2026). *The rustc book: Command-line arguments — `@path`: load command-line flags from a path*. https://doc.rust-lang.org/rustc/command-line-arguments.html#path-load-command-line-flags-from-a-path +- The Rust Project. (2026). *The rustdoc book: Command-line arguments — `@path`: load command-line flags from a path*. https://doc.rust-lang.org/rustdoc/command-line-arguments.html#path-load-command-line-flags-from-a-path + +Both references specify that `@path` opens the named file and reads command-line options from it, one option per line, using UTF-8 with Unix or Windows line endings. From 46c0c90d6162a43db3857186d4ab1731e4b0f42c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:02:42 +0900 Subject: [PATCH 375/632] test(browser-session): expose Cargo environment authority gap --- ...on_cargo_environment_authority_contract.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_browser_session_cargo_environment_authority_contract.py diff --git a/tests/test_browser_session_cargo_environment_authority_contract.py b/tests/test_browser_session_cargo_environment_authority_contract.py new file mode 100644 index 000000000..38ae9e421 --- /dev/null +++ b/tests/test_browser_session_cargo_environment_authority_contract.py @@ -0,0 +1,57 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCargoEnvironmentAuthorityContractTests(unittest.TestCase): + """Keep Git-owned Cargo compiler environment inside reviewed provenance.""" + + def test_repository_cargo_environment_inputs_fail_closed(self) -> None: + hostile_configs = ( + '[env]\nORIGINWEAVE_BUILD_ID = "unreviewed"\n', + '[env]\nORIGINWEAVE_BUILD_ID = { value = "unreviewed", force = true }\n', + '[env]\nORIGINWEAVE_TOOL_ROOT = { value = "tools", relative = true, force = true }\n', + ) + for config_text in hostile_configs: + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_empty_cargo_environment_table_remains_allowed(self) -> None: + root = self._workspace_with_config("[env]\n") + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(tempfile.TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From 67cd0f16ca5a9eeaa467ad32bbc55f60d9d1cb98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:04:03 +0900 Subject: [PATCH 376/632] fix(browser-session): fail closed on Cargo environment inputs --- ...wser_session_cargo_compiler_authority_contract.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 14bbd413a..dade19b82 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -206,7 +206,7 @@ def _flags_select_linker(value: object) -> bool: def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: - """Reject Git-owned Cargo settings that replace Rust tools or widen external link inputs.""" + """Reject Git-owned Cargo settings that replace Rust tools or widen compiler inputs.""" # The trusted-adapter boundary remains the single writer for production package/source topology # and dependency-source overrides. This contract owns Cargo-selected execution/input authority. boundary._production_package_manifests(root) @@ -230,6 +230,11 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) ) parsed = tomllib.loads(resolved.read_text(encoding="utf-8")) + environment = parsed.get("env") + environment_configured = ( + sorted(str(name) for name in environment) if isinstance(environment, dict) else [] + ) + build = parsed.get("build") build_configured = ( sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] @@ -262,11 +267,12 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if configured: target_configured[str(target_name)] = configured - if build_configured or target_configured: + if build_configured or target_configured or environment_configured: relative = config_path.relative_to(root).as_posix() raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " - f"{relative} build_keys={build_configured} target_keys={target_configured}" + f"{relative} build_keys={build_configured} target_keys={target_configured} " + f"env_keys={environment_configured}" ) From e919f7e6a098f037078f5e5d6db32de7f6004992 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:04:35 +0900 Subject: [PATCH 377/632] docs(browser-session): trace Cargo environment authority --- ...ser-session-cargo-environment-authority.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-environment-authority.md diff --git a/docs/traceability/browser-session-cargo-environment-authority.md b/docs/traceability/browser-session-cargo-environment-authority.md new file mode 100644 index 000000000..3dea7b079 --- /dev/null +++ b/docs/traceability/browser-session-cargo-environment-authority.md @@ -0,0 +1,34 @@ +# Browser Session Cargo environment authority + +## Problem + +Repository-owned Cargo configuration is part of the reviewed Browser Session build provenance. Cargo's `[env]` table injects environment variables into processes it runs, including `rustc` invocations. Rust source can consume those values at compile time through `env!` and `option_env!`, and procedural macros execute during compilation with the compiler's resources. A Git-owned `[env]` entry can therefore change compiler-visible inputs or generated code without changing the reviewed Rust source or the existing `rustflags`/`rustdocflags` surface. + +## Authority and constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` owns repository-selected compiler, rustdoc, linker, and compiler-environment authority. Supplemental contracts consume that owner rather than rediscovering Cargo configuration. + +Until OriginWeave has a purpose-bounded, versioned environment allowlist with exact consumer and artifact provenance, any non-empty Git-owned Cargo `[env]` table is fail-closed. An empty `[env]` table is permitted because it introduces no compiler-visible value. + +This policy covers both string values and Cargo's table form, including `force = true` and `relative = true`. The latter can turn a repository-relative value into an absolute path before it is exposed to Cargo-run processes. + +## RED → repair evidence + +RED `46c0c90d6162a43db3857186d4ab1731e4b0f42c` adds `tests/test_browser_session_cargo_environment_authority_contract.py`. It proves that ordinary string injection, forced replacement, and config-relative path injection were previously accepted by the canonical Cargo compiler-authority owner while retaining an empty `[env]` table as a control. + +Repair `67cd0f16ca5a9eeaa467ad32bbc55f60d9d1cb98` minimally extends the existing parsed-config owner. It records non-empty `[env]` keys as unmodeled compiler-environment authority and rejects them through the same Browser Session provenance error path. No second Cargo topology or config scanner is introduced. + +## Security and reproducibility effect + +A checked-in Cargo config can no longer change compiler-visible environment values outside the reviewed Browser Session build-input contract. This closes source-visible compile-time inputs consumed by `env!` / `option_env!` and reduces unreviewed environment available to compile-time code such as procedural macros. + +This repository-source contract does not prove the ambient execution environment. Shell variables, runner image configuration, `$CARGO_HOME` or ancestor Cargo configuration, command-line `--config`, CI-injected secrets, compiler/toolchain installation state, and environment inherited from the host remain CI/release/runtime provenance surfaces. They require canonical execution-environment controls rather than inference from Git source closure. + +## Primary references + +- The Rust Project. (2026). *The Cargo Book: Configuration — `[env]`*. https://doc.rust-lang.org/cargo/reference/config.html#env +- The Rust Project. (2026). *Rust core macro `env!`*. https://doc.rust-lang.org/core/macro.env.html +- The Rust Project. (2026). *Rust core macro `option_env!`*. https://doc.rust-lang.org/core/macro.option_env.html +- The Rust Project. (2026). *The Rust Reference: Procedural macros*. https://doc.rust-lang.org/reference/procedural-macros.html + +Cargo documents that `[env]` values are provided to build scripts and `rustc` invocations and that `force` and `relative` alter replacement and path-resolution behavior. Rust documents that `env!` and `option_env!` inspect environment variables at compile time, while procedural macros execute during compilation with the compiler's resources and build-script-like security concerns. From 2f0cbdb69033e2d2f2e49393a5eef6b5a9bf6125 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:07:41 +0900 Subject: [PATCH 378/632] test(browser-session): expose Cargo links override authority gap --- ...r_session_cargo_links_override_contract.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_browser_session_cargo_links_override_contract.py diff --git a/tests/test_browser_session_cargo_links_override_contract.py b/tests/test_browser_session_cargo_links_override_contract.py new file mode 100644 index 000000000..13b0dd8ba --- /dev/null +++ b/tests/test_browser_session_cargo_links_override_contract.py @@ -0,0 +1,61 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCargoLinksOverrideContractTests(unittest.TestCase): + """Keep Cargo links build-script overrides inside reviewed compiler provenance.""" + + def test_target_links_build_script_override_fails_closed(self) -> None: + hostile_configs = ( + '[target.x86_64-unknown-linux-gnu.review_bypass]\nrustc-link-lib = ["review_bypass"]\n', + '[target.x86_64-unknown-linux-gnu.review_bypass]\nrustc-link-search = ["tools/native"]\n', + '[target.x86_64-unknown-linux-gnu.review_bypass]\nrustc-cfg = ["originweave_review_bypass"]\n', + '[target.x86_64-unknown-linux-gnu.review_bypass]\nrustc-env = { ORIGINWEAVE_BUILD_ID = "unreviewed" }\n', + '[target.x86_64-unknown-linux-gnu.review_bypass]\nrustc-cdylib-link-arg = ["tools/review-bypass.o"]\n', + ) + for config_text in hostile_configs: + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_ordinary_target_table_without_links_override_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[target.x86_64-unknown-linux-gnu]\nrustflags = ["--cfg", "originweave_reviewed"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(tempfile.TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From a0b85bf55a7736ea99cad1c56c53b7df583647cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:08:31 +0900 Subject: [PATCH 379/632] fix(browser-session): reject Cargo links build-script overrides --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index dade19b82..c52cadd5a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -256,6 +256,12 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if not isinstance(settings, dict): continue configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) + linked_build_overrides = sorted( + str(name) for name, value in settings.items() if isinstance(value, dict) + ) + configured.extend( + f"links build-script override:{name}" for name in linked_build_overrides + ) if _flags_select_linker(settings.get("rustflags")): configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustflags")): From 75f9d032c1964736423e3a9ffd1e581b53231e1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:08:49 +0900 Subject: [PATCH 380/632] docs(browser-session): trace Cargo links override authority --- ...-session-cargo-links-override-authority.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-links-override-authority.md diff --git a/docs/traceability/browser-session-cargo-links-override-authority.md b/docs/traceability/browser-session-cargo-links-override-authority.md new file mode 100644 index 000000000..e67eec79c --- /dev/null +++ b/docs/traceability/browser-session-cargo-links-override-authority.md @@ -0,0 +1,32 @@ +# Browser Session Cargo links-override authority + +## Problem + +Cargo target configuration can replace the output of a dependency build script when that dependency declares a `package.links` value. A `[target..]` table prevents the build script from running and supplies its metadata directly. Cargo documents override keys including `rustc-link-lib`, `rustc-link-search`, `rustc-flags`, `rustc-cfg`, `rustc-env`, and `rustc-cdylib-link-arg`. + +That table is compiler and native-input authority. It can inject `-l`/`-L` inputs, conditional-compilation values, compile-time environment, and cdylib linker arguments while bypassing the production build-script boundary already reviewed by OriginWeave. The predecessor Cargo compiler-authority scanner inspected `target.` linker, runner, rustflags, and rustdocflags but ignored nested target tables, allowing this build-script replacement path to remain outside exact-tree provenance. + +## Authority and constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source and build-script topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` owns repository-selected compiler/linker/rustdoc input and execution authority. The links-override contract consumes that owner and does not re-scan workspace topology. + +Until a versioned, dependency-specific override contract proves the exact `package.links` owner, native artifacts, search paths, cfg/env values, linker arguments, and replacement semantics, any nested Git-owned `target..` table is fail-closed. Ordinary target tables without a nested links override remain governed by the existing linker/runner/rustflags/rustdocflags rules. + +## RED → repair evidence + +RED `2f0cbdb69033e2d2f2e49393a5eef6b5a9bf6125` adds `tests/test_browser_session_cargo_links_override_contract.py`. Hostile fixtures cover `rustc-link-lib`, `rustc-link-search`, `rustc-cfg`, `rustc-env`, and `rustc-cdylib-link-arg`; a normal target `rustflags` table remains an allowed control when it does not widen the existing authority classifier. + +Repair `a0b85bf55a7736ea99cad1c56c53b7df583647cd` minimally extends the existing parsed target-settings owner. Nested target tables are recorded as links build-script overrides and fail through the same Browser Session provenance error path. No second Cargo configuration or production-topology scanner is introduced. + +## Security and reproducibility effect + +A checked-in Cargo config can no longer replace a linked dependency's build-script outputs with unreviewed native-library/search-path, cfg, environment, or cdylib-linker metadata. This closes a direct bypass around both the build-script boundary and the previously modeled rustc/linker external-input surfaces. + +This source contract does not prove ambient Cargo configuration, command-line `--config`, `$CARGO_HOME`, ancestor configuration, runner/toolchain state, or the contents of future explicitly approved native artifacts. Those remain execution/release provenance surfaces. + +## Primary references + +- The Rust Project. (2026). *The Cargo Book: Configuration — `target..`*. https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinks +- The Rust Project. (2026). *The Cargo Book: Build Scripts — Overriding Build Scripts*. https://doc.rust-lang.org/cargo/reference/build-scripts.html#overriding-build-scripts + +Cargo documents that a target links sub-table prevents the linked package's build script from running and substitutes the listed metadata. It also documents that `rustc-link-lib` maps to rustc `-l`, `rustc-link-search` maps to `-L`, `rustc-cfg` controls compile-time cfg, and the other override keys replace build-script-produced compiler/linker metadata. From c8829bfbe5af0bf44f5531189576ff2f5fce7ab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:15:32 +0900 Subject: [PATCH 381/632] test(browser-session): expose Cargo include authority gap --- ...ession_cargo_include_authority_contract.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_browser_session_cargo_include_authority_contract.py diff --git a/tests/test_browser_session_cargo_include_authority_contract.py b/tests/test_browser_session_cargo_include_authority_contract.py new file mode 100644 index 000000000..a60e23433 --- /dev/null +++ b/tests/test_browser_session_cargo_include_authority_contract.py @@ -0,0 +1,67 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCargoIncludeAuthorityContractTests(unittest.TestCase): + """Keep Cargo-included configuration inside reviewed repository provenance.""" + + def test_repository_cargo_include_fails_closed(self) -> None: + include_forms = ( + 'include = ["../.config/review-bypass.toml"]\n', + 'include = [{ path = "../.config/review-bypass.toml" }]\n', + 'include = [{ path = "../.config/review-bypass.toml", optional = true }]\n', + ) + for config_text in include_forms: + with self.subTest(config_text=config_text): + root = self._workspace_with_included_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_config_without_include_remains_allowed(self) -> None: + root = self._workspace_with_config('[build]\njobs = 2\n') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def _workspace_with_included_config(self, config_text: str) -> pathlib.Path: + root = self._workspace_with_config(config_text) + extra = root / ".config" + extra.mkdir() + (extra / "review-bypass.toml").write_text( + '[env]\nORIGINWEAVE_BUILD_ID = "included-unreviewed"\n', + encoding="utf-8", + ) + return root + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = self.enterContext(tempfile.TemporaryDirectory()) + root = pathlib.Path(directory) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + +if __name__ == "__main__": + unittest.main() From 0472a50840876fc80cf431d2d66085566f06b335 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:16:26 +0900 Subject: [PATCH 382/632] fix(browser-session): fail closed on Cargo config includes --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index c52cadd5a..4acef548f 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -230,6 +230,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) ) parsed = tomllib.loads(resolved.read_text(encoding="utf-8")) + included_configs = parsed.get("include") + include_configured = included_configs is not None environment = parsed.get("env") environment_configured = ( sorted(str(name) for name in environment) if isinstance(environment, dict) else [] @@ -273,12 +275,12 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if configured: target_configured[str(target_name)] = configured - if build_configured or target_configured or environment_configured: + if build_configured or target_configured or environment_configured or include_configured: relative = config_path.relative_to(root).as_posix() raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " f"{relative} build_keys={build_configured} target_keys={target_configured} " - f"env_keys={environment_configured}" + f"env_keys={environment_configured} include={include_configured}" ) From 7a95528d06be0da7c87473f062f6bc3bcf5a7aea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:16:45 +0900 Subject: [PATCH 383/632] docs(browser-session): trace Cargo include authority --- ...browser-session-cargo-include-authority.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-include-authority.md diff --git a/docs/traceability/browser-session-cargo-include-authority.md b/docs/traceability/browser-session-cargo-include-authority.md new file mode 100644 index 000000000..840cfdaeb --- /dev/null +++ b/docs/traceability/browser-session-cargo-include-authority.md @@ -0,0 +1,29 @@ +# Browser Session Cargo include authority + +## Problem + +Cargo configuration can load additional TOML configuration through the top-level `include` key. Cargo resolves include paths relative to the including configuration file, accepts path strings and inline tables, and recursively processes includes before merging the including file on top. A checked-in `.cargo/config.toml` can therefore delegate compiler, rustdoc, linker, environment, target, or build-script-override authority to another repository file that is not itself named `.cargo/config.toml` or `.cargo/config`. + +The Browser Session Cargo compiler-authority owner discovered Git-owned `.cargo/config*` files and classified their parsed keys, but it did not model `include`. An included repository TOML file could consequently carry `[env]`, compiler/linker selectors, rustflags/rustdocflags, or `target..` metadata outside the reviewed config closure. + +## Authority and constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production package/source topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` owns Git-selected Cargo compiler/input configuration authority. The focused include contract imports that owner and does not add another workspace or Cargo-config discovery implementation. + +Until OriginWeave has a recursive, containment-checked, cycle-safe, versioned include graph whose effective merged values are reviewed under the same authority rules, any repository-owned Cargo top-level `include` is fail-closed. This is intentionally narrower and safer than partially following includes while missing precedence, recursion, optional entries, or path semantics. + +## RED → repair evidence + +RED `c8829bfbe5af0bf44f5531189576ff2f5fce7ab1` adds `tests/test_browser_session_cargo_include_authority_contract.py`. It supplies a real repository-relative included TOML file containing an unreviewed `[env]` value and exercises Cargo's path-string, inline-table, and optional-inline-table include forms. A config with no include remains the control. + +Repair `0472a50840876fc80cf431d2d66085566f06b335` minimally extends the existing parsed-config owner. Presence of the top-level `include` key is recorded as unmodeled Cargo execution/input authority and rejected through the same Browser Session provenance error path. No recursive include parser or second config scanner is introduced. + +## Residual execution provenance + +This source contract does not observe command-line `cargo --config`, configuration inherited from ancestor directories or `$CARGO_HOME`, environment-variable overrides, runner images, toolchain installation state, or files outside the Git-owned repository closure. Those remain CI/release/runtime provenance surfaces. If repository Cargo includes are later required, acceptance must prove the complete recursive include graph, path containment, optional-file semantics, merge precedence, cycle behavior, and the effective compiler/input authority after merging. + +## Primary reference + +- The Rust Project. (2026). *The Cargo Book: Configuration — Including extra configuration files*. https://doc.rust-lang.org/cargo/reference/config.html#include + +Cargo documents that top-level `include` loads additional `.toml` files, supports path strings and inline tables with `optional`, recursively processes nested includes, and merges the including file after its included files. That behavior makes the include graph part of exact build provenance rather than a formatting convenience. From 5a63a00042b9f80fb905167b28ba736b71d65d39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:27:04 +0900 Subject: [PATCH 384/632] test(browser-session): expose Cargo build-std provenance gap --- ...sion_cargo_build_std_authority_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_cargo_build_std_authority_contract.py diff --git a/tests/test_browser_session_cargo_build_std_authority_contract.py b/tests/test_browser_session_cargo_build_std_authority_contract.py new file mode 100644 index 000000000..a3fc2882e --- /dev/null +++ b/tests/test_browser_session_cargo_build_std_authority_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCargoBuildStdAuthorityContractTests(unittest.TestCase): + """Keep repository-selected standard-library source builds outside the Browser Session TCB.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def test_repository_build_std_source_selection_fails_closed(self) -> None: + for config_text in ( + '[unstable]\nbuild-std = ["core", "alloc", "std"]\n', + '[unstable]\nbuild-std = true\n', + ): + with self.subTest(config_text=config_text): + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_build_std_features_fails_closed(self) -> None: + root = self._workspace_with_config( + '[unstable]\nbuild-std-features = ["backtrace", "panic-unwind"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_unrelated_unstable_setting_remains_allowed(self) -> None: + root = self._workspace_with_config('[unstable]\nmtime-on-use = true\n') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From e524af3a2e316a3b124a25aac8392760c25a12ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:28:02 +0900 Subject: [PATCH 385/632] fix(browser-session): fail closed on Cargo build-std authority --- ...ssion_cargo_compiler_authority_contract.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 4acef548f..82374d711 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -18,6 +18,7 @@ {"rustc", "rustc-wrapper", "rustc-workspace-wrapper", "rustdoc"} ) TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) +UNSTABLE_TOOLCHAIN_INPUT_KEYS = frozenset({"build-std", "build-std-features"}) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) @@ -205,6 +206,19 @@ def _flags_select_linker(value: object) -> bool: return _linker_driver_arguments_select_executable(linker_driver_arguments) +def _configured_unstable_toolchain_inputs(value: object) -> list[str]: + """Return Git-owned unstable Cargo settings that alter standard-library build inputs.""" + if not isinstance(value, dict): + return [] + configured: list[str] = [] + for key in sorted(UNSTABLE_TOOLCHAIN_INPUT_KEYS.intersection(value)): + setting = value[key] + if setting is False or setting == []: + continue + configured.append(key) + return configured + + def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: """Reject Git-owned Cargo settings that replace Rust tools or widen compiler inputs.""" # The trusted-adapter boundary remains the single writer for production package/source topology @@ -236,6 +250,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) environment_configured = ( sorted(str(name) for name in environment) if isinstance(environment, dict) else [] ) + unstable_configured = _configured_unstable_toolchain_inputs(parsed.get("unstable")) build = parsed.get("build") build_configured = ( @@ -275,12 +290,19 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if configured: target_configured[str(target_name)] = configured - if build_configured or target_configured or environment_configured or include_configured: + if ( + build_configured + or target_configured + or environment_configured + or include_configured + or unstable_configured + ): relative = config_path.relative_to(root).as_posix() raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " f"{relative} build_keys={build_configured} target_keys={target_configured} " - f"env_keys={environment_configured} include={include_configured}" + f"env_keys={environment_configured} include={include_configured} " + f"unstable_keys={unstable_configured}" ) From 2075d827dad9a02134a96b2d110513d1a79ec406 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:29:22 +0900 Subject: [PATCH 386/632] docs(browser-session): trace Cargo build-std provenance --- ...owser-session-cargo-build-std-authority.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-build-std-authority.md diff --git a/docs/traceability/browser-session-cargo-build-std-authority.md b/docs/traceability/browser-session-cargo-build-std-authority.md new file mode 100644 index 000000000..2e6fcc800 --- /dev/null +++ b/docs/traceability/browser-session-cargo-build-std-authority.md @@ -0,0 +1,41 @@ +# Browser Session Cargo `build-std` authority + +## Problem + +OriginWeave's Browser Session trusted-adapter boundary reviews the repository-owned Cargo production package/source closure and the companion compiler-authority contract reviews Git-owned Cargo execution and input overrides. Cargo also permits `-Z` features to be configured in `.cargo/config.toml` under `[unstable]`. In particular, `build-std` changes a build from consuming the installed pre-built standard library to compiling selected standard-library crates from source as part of the crate graph, while `build-std-features` changes the features used for that standard-library build. + +That changes compiler inputs and supply-chain provenance without changing the Browser Session package manifests or Rust production-source closure. Treating it as an ordinary build preference would therefore let repository-owned configuration widen the reviewed toolchain/input boundary. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the owner for Git-owned Cargo-selected compiler, rustdoc, linker, toolchain, and external-input authority. +- This slice does not attempt to attest the ambient Rust toolchain, installed `rust-src`, runner image, or direct command-line `-Z` flags. +- Unrelated unstable Cargo settings are not blanket-banned solely because they live under `[unstable]`; only settings that alter the reviewed standard-library input boundary are classified here. + +## Authoritative evidence + +The Cargo Book's current unstable-features reference states that anything configurable with a `-Z` flag can also be set in `.cargo/config.toml` under `[unstable]`, and gives `build-std = ["core", "alloc"]` as an example. The same reference states that `build-std` compiles the standard library from source as part of the crate graph, requires the `rust-src` component and nightly Cargo/rustc, and may select the standard-library crates to build. `build-std-features` configures the features enabled for that standard-library build. + +Primary references: + +- Cargo Book, *Unstable Features — build-std*: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std +- Cargo Book, *Unstable Features — build-std-features*: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features + +## RED → repair + +RED `5a63a00042b9f80fb905167b28ba736b71d65d39` adds a focused contract that routes repository-owned `[unstable] build-std` and `build-std-features` settings through the existing compiler-authority owner. It covers list and boolean `build-std` forms plus `build-std-features`, and keeps an unrelated `mtime-on-use` setting as an allowed control. + +Repair `e524af3a2e316a3b124a25aac8392760c25a12ba` adds the minimal classification to the existing parsed-config owner. Active `build-std` and `build-std-features` settings fail closed as `unstable_keys`; explicit `false` or an empty list do not widen the input boundary. No second Cargo topology or configuration scanner is introduced. + +## Decision and security effect + +Repository-owned Cargo configuration may not select source-built standard-library inputs for the Browser Session production boundary until the same reviewed change supplies a versioned toolchain/source provenance contract. This prevents a Git-owned `.cargo/config*` change from silently replacing the pre-built sysroot assumption with source-built `core`, `alloc`, `std`, `proc_macro`, or feature-modified standard-library artifacts. + +The fail-closed rule is intentionally narrower than banning every unstable Cargo feature. A future need for `build-std` must identify the exact Rust toolchain, `rust-src` source identity, selected standard-library crates/features, target specification, resulting artifacts, and reproducible attestation before the rule is relaxed. + +## Residual execution/release provenance + +This repository contract does not prove ambient execution state. Remaining owner surfaces include direct Cargo CLI `-Z build-std` / `-Z build-std-features`, ancestor or `$CARGO_HOME` configuration, runner/container images, installed nightly Cargo/rustc and `rust-src`, custom target specifications, and other unstable features that can alter compiler/toolchain authority. Those require CI/release environment controls and, where repository-owned configuration gains such authority, additional focused fail-closed contracts. + +No hosted repository execution, protected-head GREEN, immutable release, or browser-observed acceptance is claimed by this source-semantic repair alone. From 85954d363f78621d3f9dc1dc0ad0ef304b892fae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:37:10 +0900 Subject: [PATCH 387/632] test(browser-session): expose codegen backend provenance gap --- ...sion_codegen_backend_authority_contract.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/test_browser_session_codegen_backend_authority_contract.py diff --git a/tests/test_browser_session_codegen_backend_authority_contract.py b/tests/test_browser_session_codegen_backend_authority_contract.py new file mode 100644 index 000000000..b9f78259f --- /dev/null +++ b/tests/test_browser_session_codegen_backend_authority_contract.py @@ -0,0 +1,82 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCodegenBackendAuthorityContractTests(unittest.TestCase): + """Keep repository-selected rustc code generation backends outside the Browser Session TCB.""" + + def _workspace_with_config( + self, + config_text: str = "", + *, + root_profile_text: str = "", + ) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n' + root_profile_text, + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + if config_text: + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_fails_closed( + self, + config_text: str = "", + *, + root_profile_text: str = "", + ) -> None: + root = self._workspace_with_config(config_text, root_profile_text=root_profile_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_rustflags_codegen_backend_path_fails_closed(self) -> None: + for config_text in ( + '[build]\nrustflags = ["-Zcodegen-backend=tools/review-bypass-backend.so"]\n', + '[build]\nrustflags = ["-Z", "codegen-backend=tools/review-bypass-backend.so"]\n', + "[target.'cfg(unix)']\nrustflags = [\"-Zcodegen-backend=tools/review-bypass-backend.so\"]\n", + ): + with self.subTest(config_text=config_text): + self._assert_fails_closed(config_text) + + def test_repository_config_profile_codegen_backend_fails_closed(self) -> None: + self._assert_fails_closed( + '[unstable]\ncodegen-backend = true\n\n' + '[profile.dev.package.adapter]\ncodegen-backend = "cranelift"\n' + ) + + def test_repository_manifest_profile_codegen_backend_fails_closed(self) -> None: + self._assert_fails_closed( + root_profile_text='\n[profile.dev]\ncodegen-backend = "cranelift"\n' + ) + + def test_unrelated_profile_setting_remains_allowed(self) -> None: + root = self._workspace_with_config(root_profile_text='\n[profile.dev]\nopt-level = 1\n') + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From ff7d48bc875b3d68486e7e8265af44dbb6c4b1ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:39:22 +0900 Subject: [PATCH 388/632] fix(browser-session): fail closed on codegen backend authority --- ...ssion_cargo_compiler_authority_contract.py | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 82374d711..68e9ae515 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -18,7 +18,9 @@ {"rustc", "rustc-wrapper", "rustc-workspace-wrapper", "rustdoc"} ) TARGET_EXECUTION_KEYS = frozenset({"linker", "runner"}) -UNSTABLE_TOOLCHAIN_INPUT_KEYS = frozenset({"build-std", "build-std-features"}) +UNSTABLE_TOOLCHAIN_INPUT_KEYS = frozenset( + {"build-std", "build-std-features", "codegen-backend"} +) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) @@ -168,6 +170,21 @@ def _flags_extend_external_link_inputs(value: object) -> bool: return any(_rustc_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) +def _flags_select_codegen_backend(value: object) -> bool: + """Return whether Git-owned rustc flags select a runtime code generation backend.""" + arguments = _flag_arguments(value) + for index, argument in enumerate(arguments): + if argument.startswith("-Zcodegen-backend="): + return True + if ( + argument == "-Z" + and index + 1 < len(arguments) + and arguments[index + 1].startswith("codegen-backend=") + ): + return True + return False + + def _flags_select_linker(value: object) -> bool: """Return whether Cargo-owned rustc/rustdoc flags extend linker execution or input authority.""" arguments = _flag_arguments(value) @@ -207,7 +224,7 @@ def _flags_select_linker(value: object) -> bool: def _configured_unstable_toolchain_inputs(value: object) -> list[str]: - """Return Git-owned unstable Cargo settings that alter standard-library build inputs.""" + """Return Git-owned unstable Cargo settings that alter compiler or standard-library inputs.""" if not isinstance(value, dict): return [] configured: list[str] = [] @@ -219,12 +236,39 @@ def _configured_unstable_toolchain_inputs(value: object) -> list[str]: return configured +def _configured_profile_codegen_backends(value: object, prefix: str = "profile") -> list[str]: + """Return Cargo profile paths that select a non-default rustc code generation backend.""" + if not isinstance(value, dict): + return [] + configured: list[str] = [] + for key, setting in value.items(): + path = f"{prefix}.{key}" + if key == "codegen-backend": + if setting not in (None, ""): + configured.append(path) + continue + if isinstance(setting, dict): + configured.extend(_configured_profile_codegen_backends(setting, path)) + return sorted(configured) + + def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: """Reject Git-owned Cargo settings that replace Rust tools or widen compiler inputs.""" # The trusted-adapter boundary remains the single writer for production package/source topology # and dependency-source overrides. This contract owns Cargo-selected execution/input authority. boundary._production_package_manifests(root) + root_manifest_path = root / "Cargo.toml" + root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) + manifest_profile_codegen_backends = _configured_profile_codegen_backends( + root_manifest.get("profile") + ) + if manifest_profile_codegen_backends: + raise AssertionError( + "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " + f"Cargo.toml profile_codegen_backends={manifest_profile_codegen_backends}" + ) + root_resolved = root.resolve() config_paths: set[pathlib.Path] = set() for pattern in (".cargo/config.toml", ".cargo/config"): @@ -251,12 +295,15 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) sorted(str(name) for name in environment) if isinstance(environment, dict) else [] ) unstable_configured = _configured_unstable_toolchain_inputs(parsed.get("unstable")) + profile_codegen_backends = _configured_profile_codegen_backends(parsed.get("profile")) build = parsed.get("build") build_configured = ( sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] ) if isinstance(build, dict): + if _flags_select_codegen_backend(build.get("rustflags")): + build_configured.append("rustflags:codegen backend") if _flags_select_linker(build.get("rustflags")): build_configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustflags")): @@ -279,6 +326,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.extend( f"links build-script override:{name}" for name in linked_build_overrides ) + if _flags_select_codegen_backend(settings.get("rustflags")): + configured.append("rustflags:codegen backend") if _flags_select_linker(settings.get("rustflags")): configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustflags")): @@ -296,13 +345,14 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) or environment_configured or include_configured or unstable_configured + or profile_codegen_backends ): relative = config_path.relative_to(root).as_posix() raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " f"{relative} build_keys={build_configured} target_keys={target_configured} " f"env_keys={environment_configured} include={include_configured} " - f"unstable_keys={unstable_configured}" + f"unstable_keys={unstable_configured} profile_codegen_backends={profile_codegen_backends}" ) From 61b688395c168514b5aa2fa842a06e77564c368c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 14:39:44 +0900 Subject: [PATCH 389/632] docs(browser-session): trace codegen backend authority --- ...owser-session-codegen-backend-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-codegen-backend-authority.md diff --git a/docs/traceability/browser-session-codegen-backend-authority.md b/docs/traceability/browser-session-codegen-backend-authority.md new file mode 100644 index 000000000..9a8c6a3a3 --- /dev/null +++ b/docs/traceability/browser-session-codegen-backend-authority.md @@ -0,0 +1,47 @@ +# Browser Session code generation backend authority + +## Problem + +OriginWeave's Browser Session trust boundary reviews repository-owned Cargo package/source topology and Git-owned compiler/linker/toolchain execution inputs. Rust and Cargo also expose an unstable code generation backend selection surface. A repository can select a Cargo profile `codegen-backend`, or pass rustc `-Zcodegen-backend=` through Cargo `rustflags`. + +This is execution provenance, not an optimization-only preference. rustc's unstable `codegen-backend` flag accepts a path to a dynamic library and loads that library as the code generation backend at runtime. Cargo's unstable `codegen-backend` feature permits profile-level backend selection, including from root `Cargo.toml` and Cargo configuration. Leaving those surfaces outside the canonical compiler-authority contract would let Git-owned configuration replace code-generation implementation without changing the reviewed production Rust source closure. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source containment. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the owner for Git-owned Cargo/rustc execution and compiler-input authority. +- Cargo profile settings are inspected only for `codegen-backend`; ordinary optimization/debug profile settings remain valid. +- This slice does not attest ambient `RUSTFLAGS`, command-line `cargo -Z codegen-backend`, the installed rustc sysroot/codegen backend artifacts, or runner/container images. + +## Authoritative evidence + +The Cargo Book's current unstable-features reference states that `codegen-backend` selects the backend used by rustc through a profile. It shows `[profile.dev.package.foo] codegen-backend = "cranelift"` and states that profile configuration requires either `-Z codegen-backend` or `[unstable] codegen-backend = true`. The Cargo profiles reference states that profile settings in the root workspace manifest are authoritative and may be overridden by Cargo configuration. + +The Rust Unstable Book states that `-Zcodegen-backend=` selects a dynamic library used as rustc's code generation backend at runtime and requires that library to expose `__rustc_codegen_backend`. + +Primary references: + +- Cargo Book, *Unstable Features — codegen-backend*: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#codegen-backend +- Cargo Book, *Profiles*: https://doc.rust-lang.org/nightly/cargo/reference/profiles.html +- Rust Unstable Book, *codegen-backend*: https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/codegen-backend.html + +## RED → repair + +RED `85954d363f78621d3f9dc1dc0ad0ef304b892fae` adds focused hostile fixtures for compact and split `-Zcodegen-backend=` in build/target `rustflags`, Cargo config profile selection, and root-manifest profile selection. An ordinary `opt-level` profile setting remains an allowed control. + +Repair `ff7d48bc875b3d68486e7e8265af44dbb6c4b1ed` extends the existing compiler-authority owner rather than adding a second Cargo scanner. It: + +- treats active `[unstable] codegen-backend` as compiler/toolchain execution authority; +- recognizes compact and split rustc `-Zcodegen-backend` in Git-owned build/target `rustflags`; +- rejects `codegen-backend` keys in root-workspace Cargo profiles and Cargo-config profile overrides; +- leaves unrelated unstable settings and ordinary profile optimization settings alone. + +## Decision and security effect + +Repository-owned Browser Session build configuration may not replace rustc's code generation backend until the selected backend is explicitly versioned, integrity-bound, reproducibly obtained, and covered by the same compiler/toolchain provenance and release evidence as the Rust toolchain itself. A future approved backend must identify the exact rustc/Cargo toolchain, backend artifact or rustup component, artifact digest/signature/provenance, supported target matrix, fallback behavior, reproducibility evidence, and removal/rollback path before this fail-closed rule is relaxed. + +## Residual execution/release provenance + +This source contract does not prove ambient command-line or runner state. Remaining surfaces include direct `cargo -Z codegen-backend`, ambient `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS`, ancestor or `$CARGO_HOME` configuration, rustup component installation, sysroot-provided backends, custom target/toolchain composition, and runner/container image provenance. These belong to executable CI/release evidence unless a repository-owned surface begins selecting them, in which case another focused contract is required. + +No hosted repository execution, protected-head GREEN, immutable release, or browser-observed acceptance is claimed by this source-semantic repair alone. From 9a7477e6e19d739cead5c90fa63070aa85a01cf6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 15:04:44 +0900 Subject: [PATCH 390/632] test(browser-session): red custom target spec authority --- ...n_custom_target_spec_authority_contract.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_browser_session_custom_target_spec_authority_contract.py diff --git a/tests/test_browser_session_custom_target_spec_authority_contract.py b/tests/test_browser_session_custom_target_spec_authority_contract.py new file mode 100644 index 000000000..ce13fff30 --- /dev/null +++ b/tests/test_browser_session_custom_target_spec_authority_contract.py @@ -0,0 +1,83 @@ +import importlib.util +import json +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMPILER_AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_cargo_compiler_authority", + COMPILER_AUTHORITY_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +compiler_authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(compiler_authority) + + +class BrowserSessionCustomTargetSpecAuthorityContractTests(unittest.TestCase): + """Keep repository-selected rustc target specifications inside reviewed provenance.""" + + def _workspace_with_build_target(self, target_value: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text( + f"[build]\ntarget = {target_value}\n", + encoding="utf-8", + ) + targets = root / "targets" + targets.mkdir() + (targets / "review-bypass.json").write_text( + json.dumps( + { + "llvm-target": "x86_64-unknown-linux-gnu", + "arch": "x86_64", + "target-pointer-width": "64", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", + "linker": "tools/review-bypass-linker", + } + ), + encoding="utf-8", + ) + return root + + def test_build_target_custom_json_fails_closed(self) -> None: + root = self._workspace_with_build_target('"targets/review-bypass.json"') + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + compiler_authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_target_array_with_custom_json_fails_closed(self) -> None: + root = self._workspace_with_build_target( + '["x86_64-unknown-linux-gnu", "targets/review-bypass.json"]' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + compiler_authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_builtin_target_triple_remains_allowed(self) -> None: + root = self._workspace_with_build_target('"x86_64-unknown-linux-gnu"') + compiler_authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_host_tuple_remains_allowed(self) -> None: + root = self._workspace_with_build_target('"host-tuple"') + compiler_authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 8e4db1b2229a6b77d117be8ed2d0595bbd96a1d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 15:06:10 +0900 Subject: [PATCH 391/632] fix(browser-session): reject custom target spec selection --- ...r_session_cargo_compiler_authority_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 68e9ae515..c04497911 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -252,6 +252,17 @@ def _configured_profile_codegen_backends(value: object, prefix: str = "profile") return sorted(configured) +def _configured_custom_target_specs(value: object) -> list[str]: + """Return repository-selected custom rustc target specification paths.""" + if isinstance(value, str): + targets = [value] + elif isinstance(value, list) and all(isinstance(target, str) for target in value): + targets = value + else: + return [] + return sorted(target for target in targets if target.endswith(".json")) + + def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) -> None: """Reject Git-owned Cargo settings that replace Rust tools or widen compiler inputs.""" # The trusted-adapter boundary remains the single writer for production package/source topology @@ -302,6 +313,10 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) sorted(COMPILER_EXECUTION_KEYS.intersection(build)) if isinstance(build, dict) else [] ) if isinstance(build, dict): + build_configured.extend( + f"target:custom target specification:{target_spec}" + for target_spec in _configured_custom_target_specs(build.get("target")) + ) if _flags_select_codegen_backend(build.get("rustflags")): build_configured.append("rustflags:codegen backend") if _flags_select_linker(build.get("rustflags")): From 3ca29db9e63d894b3eff6a98e12539d9862fa605 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 15:07:04 +0900 Subject: [PATCH 392/632] docs(browser-session): trace custom target spec authority --- ...er-session-custom-target-spec-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-custom-target-spec-authority.md diff --git a/docs/traceability/browser-session-custom-target-spec-authority.md b/docs/traceability/browser-session-custom-target-spec-authority.md new file mode 100644 index 000000000..fe6bf5cda --- /dev/null +++ b/docs/traceability/browser-session-custom-target-spec-authority.md @@ -0,0 +1,47 @@ +# Browser Session custom target specification authority + +Status: Draft source-semantic contract evidence on PR #317. This document does not claim hosted executable, repository-security, or browser GREEN. + +## Problem + +Cargo `build.target` accepts a built-in rustc target, `host-tuple`, or a path to a custom target specification. A repository-owned `.cargo/config.toml` or legacy `.cargo/config` can therefore select a JSON target specification without changing Cargo package/source topology or the visible `rustflags`/`rustdocflags` already covered by the Browser Session compiler-authority contract. + +That selection is provenance-bearing. Rust custom target specifications describe compiler target behavior rather than merely naming an output directory. Current rustc target metadata exposes linker selection, linker flavor, pre/post link objects, pre/late/post link arguments, link scripts, linker environment changes, and assembler arguments among the target options. A Git-owned custom target can therefore alter native tool execution or native/link inputs while the reviewed Rust source closure is unchanged. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source discovery. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the owner for repository-selected compiler, rustdoc, linker, toolchain, and external-input authority. +- Built-in target triples and Cargo's `host-tuple` remain allowed. The repair is not a blanket `build.target` ban. +- This contract does not authorize a custom target JSON artifact. An approved future target specification needs immutable artifact identity, compiler-version/schema pinning, transitive linker/native-input provenance, SBOM/attestation, rollback, and the same-tree executable evidence. +- `CARGO_BUILD_TARGET`, direct Cargo `--target`, `RUST_TARGET_PATH`, and target specifications resolved from a rustc sysroot are execution-environment or toolchain inputs and remain with the CI/release supply-chain owner. + +## RED + +Commit `9a7477e6e19d739cead5c90fa63070aa85a01cf6` adds `tests/test_browser_session_custom_target_spec_authority_contract.py`. The hostile fixture selects `targets/review-bypass.json` through build-level `target`; the target JSON names a different linker. A second fixture places the JSON path beside a built-in target in Cargo's array form. The predecessor compiler-authority contract did not classify `build.target`, so both repository-owned custom-target selectors were outside its fail-closed surface. Built-in target and `host-tuple` controls are retained. + +## Decision and repair + +Commit `8e4db1b2229a6b77d117be8ed2d0595bbd96a1d7` minimally extends the existing Cargo compiler-authority owner with `_configured_custom_target_specs`. Build-level target strings or arrays whose entries end in `.json` are recorded as `target:custom target specification:` and fail through the existing execution-override assertion. Built-in target triples and `host-tuple` do not enter that list. + +No second Cargo topology scanner was added. The hostile contract imports and exercises the canonical compiler-authority assertion, while package/source discovery remains delegated to the trusted-adapter boundary. + +## Security effect and residual risk + +The repair closes Git-owned Cargo configuration that directly selects a JSON rustc target specification through `[build].target`. It prevents an unreviewed target JSON from changing linker/native-input behavior behind an otherwise unchanged Browser Session source/dependency closure. + +It does not prove ambient target selection trustworthy. Environment `CARGO_BUILD_TARGET`, direct `cargo ... --target`, `RUST_TARGET_PATH`, sysroot target metadata, runner-installed compiler versions, or schema compatibility remain CI/release supply-chain concerns. Rust documents custom target JSON properties as unstable and recommends pinning the compiler version; any future approved target JSON must therefore be versioned and evidenced together with the exact rustc/toolchain that consumes it. + +## Acceptance + +The source contract must remain Draft until the reconciled exact #317 head receives hosted repository/security execution and independent current-head review. Parent #229 ancestry and required checks remain prerequisites; source-level RED→repair here does not transfer predecessor executable evidence. + +## References + +The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/config.html + +The Rust Project Developers. (n.d.). *Custom targets*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/targets/custom.html + +The Rust Project Developers. (2026). *rustc_target::spec* (rustc 1.100.0-nightly, 330d31712 2026-09-17). Retrieved September 18, 2026, from https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/ + +The Rust Project Developers. (n.d.). *TargetOptions*. *rustc_target::spec*. Retrieved September 18, 2026, from https://doc.rust-lang.org/beta/nightly-rustc/rustc_target/spec/struct.TargetOptions.html From 6f2ee50a6a461ebd55d7a23c5435a111faf6a792 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 15:10:16 +0900 Subject: [PATCH 393/632] test(browser-session): model gated custom target spec --- ...est_browser_session_custom_target_spec_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_custom_target_spec_authority_contract.py b/tests/test_browser_session_custom_target_spec_authority_contract.py index ce13fff30..dd29e6468 100644 --- a/tests/test_browser_session_custom_target_spec_authority_contract.py +++ b/tests/test_browser_session_custom_target_spec_authority_contract.py @@ -39,7 +39,7 @@ def _workspace_with_build_target(self, target_value: str) -> pathlib.Path: cargo = root / ".cargo" cargo.mkdir() (cargo / "config.toml").write_text( - f"[build]\ntarget = {target_value}\n", + f"[unstable]\njson-target-spec = true\n\n[build]\ntarget = {target_value}\n", encoding="utf-8", ) targets = root / "targets" From 0eaf52769a3e8dce467b85efa9d7dd769a83a95c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 15:10:42 +0900 Subject: [PATCH 394/632] docs(browser-session): align custom target gate evidence --- .../browser-session-custom-target-spec-authority.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-custom-target-spec-authority.md b/docs/traceability/browser-session-custom-target-spec-authority.md index fe6bf5cda..3fa4a2313 100644 --- a/docs/traceability/browser-session-custom-target-spec-authority.md +++ b/docs/traceability/browser-session-custom-target-spec-authority.md @@ -6,13 +6,15 @@ Status: Draft source-semantic contract evidence on PR #317. This document does n Cargo `build.target` accepts a built-in rustc target, `host-tuple`, or a path to a custom target specification. A repository-owned `.cargo/config.toml` or legacy `.cargo/config` can therefore select a JSON target specification without changing Cargo package/source topology or the visible `rustflags`/`rustdocflags` already covered by the Browser Session compiler-authority contract. +Current nightly Cargo gates custom target JSON use behind `-Z json-target-spec`; Cargo also documents that `-Z` features can be enabled through the config `[unstable]` table. The realistic repository-owned path is therefore `[unstable] json-target-spec = true` together with `[build].target = "path/to/spec.json"`. The gate alone does not select an artifact, so the contract fails closed on the actual custom target selection rather than banning the feature flag globally. + That selection is provenance-bearing. Rust custom target specifications describe compiler target behavior rather than merely naming an output directory. Current rustc target metadata exposes linker selection, linker flavor, pre/post link objects, pre/late/post link arguments, link scripts, linker environment changes, and assembler arguments among the target options. A Git-owned custom target can therefore alter native tool execution or native/link inputs while the reviewed Rust source closure is unchanged. ## Constraints - `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology and dependency-source discovery. - `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the owner for repository-selected compiler, rustdoc, linker, toolchain, and external-input authority. -- Built-in target triples and Cargo's `host-tuple` remain allowed. The repair is not a blanket `build.target` ban. +- Built-in target triples and Cargo's `host-tuple` remain allowed. The repair is not a blanket `build.target` or `json-target-spec` ban. - This contract does not authorize a custom target JSON artifact. An approved future target specification needs immutable artifact identity, compiler-version/schema pinning, transitive linker/native-input provenance, SBOM/attestation, rollback, and the same-tree executable evidence. - `CARGO_BUILD_TARGET`, direct Cargo `--target`, `RUST_TARGET_PATH`, and target specifications resolved from a rustc sysroot are execution-environment or toolchain inputs and remain with the CI/release supply-chain owner. @@ -20,6 +22,8 @@ That selection is provenance-bearing. Rust custom target specifications describe Commit `9a7477e6e19d739cead5c90fa63070aa85a01cf6` adds `tests/test_browser_session_custom_target_spec_authority_contract.py`. The hostile fixture selects `targets/review-bypass.json` through build-level `target`; the target JSON names a different linker. A second fixture places the JSON path beside a built-in target in Cargo's array form. The predecessor compiler-authority contract did not classify `build.target`, so both repository-owned custom-target selectors were outside its fail-closed surface. Built-in target and `host-tuple` controls are retained. +Commit `6f2ee50a6a461ebd55d7a23c5435a111faf6a792` aligns that focused fixture with current Cargo behavior by enabling `[unstable] json-target-spec = true` in the repository config. The same gate is present in the built-in-target controls, proving that the repair keys on artifact selection rather than the feature switch itself. + ## Decision and repair Commit `8e4db1b2229a6b77d117be8ed2d0595bbd96a1d7` minimally extends the existing Cargo compiler-authority owner with `_configured_custom_target_specs`. Build-level target strings or arrays whose entries end in `.json` are recorded as `target:custom target specification:` and fail through the existing execution-override assertion. Built-in target triples and `host-tuple` do not enter that list. @@ -40,6 +44,8 @@ The source contract must remain Draft until the reconciled exact #317 head recei The Cargo Project. (n.d.). *Configuration*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/cargo/reference/config.html +The Cargo Project. (n.d.). *Unstable features*. *The Cargo Book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/nightly/cargo/reference/unstable.html + The Rust Project Developers. (n.d.). *Custom targets*. *The rustc book*. Retrieved September 18, 2026, from https://doc.rust-lang.org/rustc/targets/custom.html The Rust Project Developers. (2026). *rustc_target::spec* (rustc 1.100.0-nightly, 330d31712 2026-09-17). Retrieved September 18, 2026, from https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/ From e7aafa552c923f121ee88e92fcca9b7ad43ca363 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 16:07:21 +0900 Subject: [PATCH 395/632] test(browser-session): expose Cargo profile rustflags authority gap --- ...on_profile_rustflags_authority_contract.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/test_browser_session_profile_rustflags_authority_contract.py diff --git a/tests/test_browser_session_profile_rustflags_authority_contract.py b/tests/test_browser_session_profile_rustflags_authority_contract.py new file mode 100644 index 000000000..6e10e7b35 --- /dev/null +++ b/tests/test_browser_session_profile_rustflags_authority_contract.py @@ -0,0 +1,78 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionProfileRustflagsAuthorityContractTests(unittest.TestCase): + """Keep profile-selected rustc arguments inside the reviewed Cargo authority boundary.""" + + def _workspace(self, *, profile_text: str, config_text: str | None = None) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + 'cargo-features = ["profile-rustflags"]\n\n' + '[workspace]\n' + 'members = ["adapter"]\n' + 'resolver = "3"\n\n' + f"{profile_text}", + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + if config_text is not None: + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def test_root_manifest_profile_rustflags_external_input_fails_closed(self) -> None: + root = self._workspace( + profile_text=( + '[profile.release]\n' + 'rustflags = ["--extern", "review_bypass=tools/libreview_bypass.rlib"]\n' + ) + ) + with self.assertRaisesRegex(AssertionError, "profile_rustflag_authority"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_cargo_config_profile_rustflags_linker_selection_fails_closed(self) -> None: + root = self._workspace( + profile_text='[profile.release]\nopt-level = 2\n', + config_text=( + '[unstable]\nprofile-rustflags = true\n\n' + '[profile.release]\n' + 'rustflags = ["-C", "linker=tools/review-bypass-linker"]\n' + ), + ) + with self.assertRaisesRegex(AssertionError, "profile_rustflag_authority"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_non_authority_profile_rustflags_remain_allowed(self) -> None: + root = self._workspace( + profile_text=( + '[profile.release]\n' + 'rustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' + ) + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 38c1c06fe6942a1024ea73148b0c8b6ccc9f4405 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 16:08:51 +0900 Subject: [PATCH 396/632] fix(browser-session): govern Cargo profile rustflags authority --- ...ssion_cargo_compiler_authority_contract.py | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index c04497911..be69be5bb 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -252,6 +252,26 @@ def _configured_profile_codegen_backends(value: object, prefix: str = "profile") return sorted(configured) +def _configured_profile_rustflag_authority(value: object, prefix: str = "profile") -> list[str]: + """Return profile rustflags that widen compiler execution or external input authority.""" + if not isinstance(value, dict): + return [] + configured: list[str] = [] + for key, setting in value.items(): + path = f"{prefix}.{key}" + if key == "rustflags": + if _flags_select_codegen_backend(setting): + configured.append(f"{path}:codegen backend") + if _flags_select_linker(setting): + configured.append(f"{path}:codegen linker") + if _flags_extend_external_link_inputs(setting): + configured.append(f"{path}:external compiler input") + continue + if isinstance(setting, dict): + configured.extend(_configured_profile_rustflag_authority(setting, path)) + return sorted(configured) + + def _configured_custom_target_specs(value: object) -> list[str]: """Return repository-selected custom rustc target specification paths.""" if isinstance(value, str): @@ -274,10 +294,14 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) manifest_profile_codegen_backends = _configured_profile_codegen_backends( root_manifest.get("profile") ) - if manifest_profile_codegen_backends: + manifest_profile_rustflag_authority = _configured_profile_rustflag_authority( + root_manifest.get("profile") + ) + if manifest_profile_codegen_backends or manifest_profile_rustflag_authority: raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " - f"Cargo.toml profile_codegen_backends={manifest_profile_codegen_backends}" + f"Cargo.toml profile_codegen_backends={manifest_profile_codegen_backends} " + f"profile_rustflag_authority={manifest_profile_rustflag_authority}" ) root_resolved = root.resolve() @@ -307,6 +331,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) ) unstable_configured = _configured_unstable_toolchain_inputs(parsed.get("unstable")) profile_codegen_backends = _configured_profile_codegen_backends(parsed.get("profile")) + profile_rustflag_authority = _configured_profile_rustflag_authority(parsed.get("profile")) build = parsed.get("build") build_configured = ( @@ -361,13 +386,15 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) or include_configured or unstable_configured or profile_codegen_backends + or profile_rustflag_authority ): relative = config_path.relative_to(root).as_posix() raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " f"{relative} build_keys={build_configured} target_keys={target_configured} " f"env_keys={environment_configured} include={include_configured} " - f"unstable_keys={unstable_configured} profile_codegen_backends={profile_codegen_backends}" + f"unstable_keys={unstable_configured} profile_codegen_backends={profile_codegen_backends} " + f"profile_rustflag_authority={profile_rustflag_authority}" ) From f48204419a61bbde44e7025d60ebe0f05679e0a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 16:09:30 +0900 Subject: [PATCH 397/632] docs(browser-session): trace Cargo profile rustflags authority --- ...ser-session-profile-rustflags-authority.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/traceability/browser-session-profile-rustflags-authority.md diff --git a/docs/traceability/browser-session-profile-rustflags-authority.md b/docs/traceability/browser-session-profile-rustflags-authority.md new file mode 100644 index 000000000..03a96f3db --- /dev/null +++ b/docs/traceability/browser-session-profile-rustflags-authority.md @@ -0,0 +1,42 @@ +# Browser Session Cargo profile rustflags authority + +## Problem + +Cargo's unstable profile `rustflags` option can be selected from the root workspace manifest with `cargo-features = ["profile-rustflags"]` or from Cargo configuration with the `profile-rustflags` unstable feature enabled. Cargo documents these profile flags as arguments passed directly to `rustc`. + +The Browser Session Cargo authority contract already classified build-level and target-level `rustflags`, but did not inspect `rustflags` nested under `[profile.*]`. A Git-owned profile could therefore reintroduce execution or compiler-input authority such as `-C linker=...`, `--extern`, `--sysroot`, a top-level rustc response file, or `-Zcodegen-backend=...` without changing the reviewed production package/source topology. + +## Constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. This repair must consume that topology and the existing compiler/linker/input classifiers rather than add another package, dependency, or source scanner. + +The `profile-rustflags` capability itself is not prohibited. Profile flags that do not extend execution or external-input authority remain allowed. + +## RED and repair + +- RED `e7aafa552c923f121ee88e92fcca9b7ad43ca363` adds `tests/test_browser_session_profile_rustflags_authority_contract.py`. It covers root-manifest profile `--extern`, Cargo-config profile linker selection, and an ordinary `opt-level`/`cfg` control. +- Repair `38c1c06fe6942a1024ea73148b0c8b6ccc9f4405` extends only `tests/test_browser_session_cargo_compiler_authority_contract.py`. `_configured_profile_rustflag_authority()` recursively inspects profile tables and reuses `_flags_select_codegen_backend()`, `_flags_select_linker()`, and `_flags_extend_external_link_inputs()`. +- Root `Cargo.toml` profiles and repository `.cargo/config.toml` / `.cargo/config` profiles now fail closed only when profile `rustflags` widen execution or compiler-input authority. Existing direct profile `codegen-backend` handling remains separate. + +## Primary evidence + +Cargo source and documentation were checked against `rust-lang/cargo@8814ead110e36ed8fdcf1fdd4009baf82bd78523`. + +- `doc/book/src/reference/unstable.md`, “Profile `rustflags` option”, describes profile `rustflags` as passed directly to `rustc`, including `[unstable] profile-rustflags = true` with `[profile.release] rustflags = [...]` in Cargo configuration. +- Cargo profile configuration is owned by the root workspace manifest and may also be supplied through Cargo configuration. The contract therefore inspects both Git-owned surfaces while keeping production topology ownership unchanged. + +## Decision and security effect + +Repository-selected profile `rustflags` are treated as another spelling of the same rustc execution/input authority already governed for build and target flags. The contract rejects profile flags that: + +- select a code-generation backend; +- select or reconfigure linker execution, linker plugins, scripts, response files, or positional linker inputs through the existing linker classifier; or +- add opaque/external compiler inputs such as leading `@path`, `--sysroot`, `--extern`, `-L`, or `-l`. + +Ordinary profile flags such as optimization level or reviewed `--cfg` values remain permitted. This avoids turning an execution-provenance contract into a blanket Cargo-profile policy. + +## Residual authority + +This repository-source contract does not claim control over environment or direct-CLI profile injection, including `CARGO_PROFILE__RUSTFLAGS`, ancestor or `$CARGO_HOME` configuration, command-line `--config`, or runner/toolchain mutation outside the reviewed tree. Those are CI/release environment provenance surfaces and require exact runner/configuration evidence rather than source-copying environment policy into OriginWeave. + +The repair is source-semantic until the exact PR head receives hosted repository/security execution evidence. Static source inspection is not a substitute for executable GREEN. From e87cab84490aea41a4bb5f7de20485cbd770642d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 17:02:53 +0900 Subject: [PATCH 398/632] test(browser-session): add LLVM plugin authority RED --- ..._session_llvm_plugin_authority_contract.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_browser_session_llvm_plugin_authority_contract.py diff --git a/tests/test_browser_session_llvm_plugin_authority_contract.py b/tests/test_browser_session_llvm_plugin_authority_contract.py new file mode 100644 index 000000000..d260f05ba --- /dev/null +++ b/tests/test_browser_session_llvm_plugin_authority_contract.py @@ -0,0 +1,74 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLlvmPluginAuthorityContractTests(unittest.TestCase): + """Reject repository-selected rustc LLVM pass plugins from Git-owned Cargo flags.""" + + def _workspace(self, config_text: str, *, profile_manifest: str = "") -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n' + profile_manifest, + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_plugin_fails_closed(self, config_text: str, *, profile_manifest: str = "") -> None: + root = self._workspace(config_text, profile_manifest=profile_manifest) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_split_llvm_plugin_fails_closed(self) -> None: + self._assert_plugin_fails_closed( + '[build]\nrustflags = ["-Z", "llvm-plugins=tools/review-bypass-pass.so"]\n' + ) + + def test_target_rustflags_compact_llvm_plugin_fails_closed(self) -> None: + self._assert_plugin_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-Zllvm-plugins=tools/review-bypass-pass.so\"]\n" + ) + + def test_config_profile_rustflags_llvm_plugin_fails_closed(self) -> None: + self._assert_plugin_fails_closed( + '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["-Z", "llvm-plugins=tools/review-bypass-pass.so"]\n' + ) + + def test_root_profile_rustflags_llvm_plugin_fails_closed(self) -> None: + self._assert_plugin_fails_closed( + '', + profile_manifest='\n[profile.release]\nrustflags = ["-Zllvm-plugins=tools/review-bypass-pass.so"]\n', + ) + + def test_non_plugin_rustflags_remain_allowed(self) -> None: + root = self._workspace( + '[build]\nrustflags = ["-C", "opt-level=2", "--cfg", "originweave_reviewed"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 60f27dfde50b7134ebd43d9d0574e7edc169191d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 17:04:22 +0900 Subject: [PATCH 399/632] fix(browser-session): reject rustc LLVM pass plugin selection --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index be69be5bb..b8b25f93a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -171,15 +171,15 @@ def _flags_extend_external_link_inputs(value: object) -> bool: def _flags_select_codegen_backend(value: object) -> bool: - """Return whether Git-owned rustc flags select a runtime code generation backend.""" + """Return whether Git-owned rustc flags replace or dynamically extend code generation.""" arguments = _flag_arguments(value) for index, argument in enumerate(arguments): - if argument.startswith("-Zcodegen-backend="): + if argument.startswith(("-Zcodegen-backend=", "-Zllvm-plugins=")): return True if ( argument == "-Z" and index + 1 < len(arguments) - and arguments[index + 1].startswith("codegen-backend=") + and arguments[index + 1].startswith(("codegen-backend=", "llvm-plugins=")) ): return True return False From 8440e5ff7051ccb73843906b4b9ee0f215385963 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 17:04:56 +0900 Subject: [PATCH 400/632] docs(browser-session): trace rustc LLVM plugin authority --- ...ser-session-rustc-llvm-plugin-authority.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/traceability/browser-session-rustc-llvm-plugin-authority.md diff --git a/docs/traceability/browser-session-rustc-llvm-plugin-authority.md b/docs/traceability/browser-session-rustc-llvm-plugin-authority.md new file mode 100644 index 000000000..c8574c2a0 --- /dev/null +++ b/docs/traceability/browser-session-rustc-llvm-plugin-authority.md @@ -0,0 +1,46 @@ +# Browser Session rustc LLVM plugin authority + +## Problem + +OriginWeave's Browser Session production closure already fails closed for repository-selected rustc executables, wrappers, code-generation backends, linker plugins, linker scripts, response files, sysroots, external crates, native libraries, and other reviewed Cargo execution/input surfaces. One rustc-native execution surface was still outside that classifier: `-Z llvm-plugins=`. + +The current rustc option table exposes `llvm_plugins` as an unstable list option described as “a list LLVM plugins to enable (space separated)”. `rustc_codegen_llvm` copies that list into the LLVM module configuration and passes the joined plugin list to `LLVMRustOptimize`. A Git-owned Cargo `rustflags` value can therefore select code that participates in compiler optimization without changing `Cargo.toml` dependency/source topology, the selected code-generation backend, or the final linker. + +This is compiler execution provenance, not an optimization-only preference. A reviewed Browser Session build must not gain a repository-selected LLVM pass plugin through an otherwise innocuous Cargo profile/build/target flag path. + +## Decision + +The existing Browser Session Cargo compiler-authority contract remains the single owner of Git-owned rustc execution/input selection. Its existing rustflag classifier now treats both compact and split LLVM-plugin forms as code-generation execution extensions: + +- `-Zllvm-plugins=tools/review-bypass-pass.so` +- `-Z llvm-plugins=tools/review-bypass-pass.so` + +The classifier is already consumed by build-level `rustflags`, target-level `rustflags`, root-manifest profile `rustflags`, and repository Cargo-config profile `rustflags`. No second Cargo topology scanner or Browser Session source-discovery implementation is introduced. + +Ordinary flags that do not replace or dynamically extend compiler execution remain outside this prohibition. The focused contract keeps `-C opt-level=2` and a reviewed `--cfg` as controls. + +## RED → repair evidence + +- RED: `e87cab84490aea41a4bb5f7de20485cbd770642d` adds hostile build, target, Cargo-config profile, and root-profile LLVM-plugin fixtures that the predecessor classifier did not reject. +- Repair: `60f27dfde50b7134ebd43d9d0574e7edc169191d` extends the existing rustc code-generation execution classifier to reject `llvm-plugins=` in compact or split `-Z` form. Existing build/target/profile call sites inherit the repair. +- Exact executable GREEN is not inferred from these source changes. The PR remains Draft and must earn fresh hosted repository/security evidence after the canonical parent lineage is reconciled. + +## Security effect + +A Git-owned Cargo config or profile can no longer introduce an LLVM pass plugin while leaving the reviewed package/source graph unchanged. This closes a compiler-process extension path adjacent to, but distinct from, the already governed rustc code-generation backend and linker-plugin-LTO boundaries. + +The plugin path is treated as execution provenance only. Browser Session does not learn LLVM plugin semantics, and no LLVM implementation detail becomes Browser Session domain truth. + +## Residual authority + +This repository-source contract does not claim control over ambient execution surfaces supplied outside the reviewed tree, including environment/direct-CLI rustflags, ancestor or `$CARGO_HOME` Cargo config, runner images, the installed rustc/LLVM distribution itself, or administrator-provided toolchain mutation. Those require CI/release supply-chain evidence, immutable toolchain identity, SBOM/provenance, and reproducibility controls rather than another leaf-source scanner. + +A future approved LLVM plugin would require an explicit versioned artifact contract, immutable digest/provenance, toolchain/LLVM ABI compatibility evidence, security review, exact-head tests, SBOM/attestation, and rollback before this fail-closed rule is relaxed. + +## Primary references + +Rust Project. (2026). *rustc_session::options::UnstableOptions* (`llvm_plugins`). Rust nightly compiler documentation. https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/options/struct.UnstableOptions.html + +Rust Project. (2026). *rustc_session options source* (`llvm_plugins`: “a list LLVM plugins to enable”). Rust compiler source documentation. https://doc.rust-lang.org/beta/nightly-rustc/src/rustc_session/options.rs.html + +Rust Project. (2026). *rustc_codegen_llvm::back::write*. Rust compiler source. The LLVM plugin list is propagated into `LLVMRustOptimize`. https://github.com/rust-lang/rust/blob/main/compiler/rustc_codegen_llvm/src/back/write.rs From ab2fffacffaa061387440c104d4f2b93b7b9675d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:02:22 +0900 Subject: [PATCH 401/632] test(browser-session): prove rustdoc doctest execution override gap --- ...oc_doctest_execution_authority_contract.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py diff --git a/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py b/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py new file mode 100644 index 000000000..aeb16e037 --- /dev/null +++ b/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py @@ -0,0 +1,75 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionRustdocDoctestExecutionAuthorityContractTests(unittest.TestCase): + """Keep Git-owned rustdoc doctest execution programs inside reviewed provenance.""" + + def _assert_fails_closed(self, config_text: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_doctest_runtool_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--test-runtool", "tools/review-bypass-runtool"]\n' + ) + + def test_target_rustdocflags_doctest_runtool_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"--test-runtool=tools/review-bypass-runtool\"]\n" + ) + + def test_build_rustdocflags_doctest_builder_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--test-builder", "tools/review-bypass-rustc"]\n' + ) + + def test_target_rustdocflags_doctest_builder_wrapper_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Zunstable-options\", \"--test-builder-wrapper=tools/review-bypass-wrapper\"]\n" + ) + + def test_non_execution_doctest_arguments_remain_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustdocflags = ["--test-args", "ignored", "--test-run-directory=target/doctest"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From d9c55cfecd2710fcdb585f1ba971cef413e631d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:06:07 +0900 Subject: [PATCH 402/632] fix(browser-session): fail closed rustdoc doctest execution selectors --- ...ser_session_cargo_compiler_authority_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b8b25f93a..a82bc6c72 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -223,6 +223,15 @@ def _flags_select_linker(value: object) -> bool: return _linker_driver_arguments_select_executable(linker_driver_arguments) +def _flags_select_rustdoc_test_execution(value: object) -> bool: + """Return whether Git-owned rustdoc flags select external doctest executables.""" + selectors = ("--test-runtool", "--test-builder", "--test-builder-wrapper") + return any( + argument in selectors or argument.startswith(tuple(f"{selector}=" for selector in selectors)) + for argument in _flag_arguments(value) + ) + + def _configured_unstable_toolchain_inputs(value: object) -> list[str]: """Return Git-owned unstable Cargo settings that alter compiler or standard-library inputs.""" if not isinstance(value, dict): @@ -352,6 +361,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustdocflags")): build_configured.append("rustdocflags:external link input") + if _flags_select_rustdoc_test_execution(build.get("rustdocflags")): + build_configured.append("rustdocflags:doctest execution") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -376,6 +387,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustdocflags")): configured.append("rustdocflags:external link input") + if _flags_select_rustdoc_test_execution(settings.get("rustdocflags")): + configured.append("rustdocflags:doctest execution") if configured: target_configured[str(target_name)] = configured From 0fd7fc81b79f735133a032199cb59f64da54b9f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:06:50 +0900 Subject: [PATCH 403/632] docs(browser-session): trace rustdoc doctest execution authority --- ...ion-rustdoc-doctest-execution-authority.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/traceability/browser-session-rustdoc-doctest-execution-authority.md diff --git a/docs/traceability/browser-session-rustdoc-doctest-execution-authority.md b/docs/traceability/browser-session-rustdoc-doctest-execution-authority.md new file mode 100644 index 000000000..4aea2c1ef --- /dev/null +++ b/docs/traceability/browser-session-rustdoc-doctest-execution-authority.md @@ -0,0 +1,50 @@ +# Browser Session rustdoc doctest execution authority + +Status: Draft; source-semantic contract current on PR #317. Hosted executable evidence is still required before this generation is GREEN. + +## Problem + +Cargo can pass repository-owned `[build].rustdocflags` and matching `target..rustdocflags` directly to `rustdoc`. Rustdoc can then select external programs that participate in documentation-test execution: + +- `--test-runtool ` executes the specified wrapper instead of the doctest executable. +- nightly `--test-builder ` replaces the default rustc-like program used to compile doctests. +- nightly `--test-builder-wrapper ` wraps the selected test builder and may be repeated. + +Those selectors can change executable provenance without changing the reviewed Cargo package/source graph, compiler package dependencies, or Browser Session domain code. They therefore belong to the existing Cargo-selected execution/input authority boundary rather than to rustdoc semantics owned by Browser Session. + +## Ownership and constraints + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source/dependency topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` owns repository-selected Cargo execution and external-input authority. This repair reuses that owner and does not add a second topology/configuration scanner. + +Browser Session does not own rustdoc, Cargo, the Rust toolchain, or doctest scheduling. It only requires that Git-owned configuration cannot silently replace or wrap programs that compile or execute Browser Session documentation tests. + +## Evidence and repair + +RED `ab2fffacffaa061387440c104d4f2b93b7b9675d` adds hostile build/target `rustdocflags` fixtures for `--test-runtool`, `--test-builder`, and `--test-builder-wrapper`. Ordinary doctest arguments that do not select an external executable, including `--test-args` and `--test-run-directory`, remain controls. + +Repair `d9c55cfecd2710fcdb585f1ba971cef413e631d5` adds `_flags_select_rustdoc_test_execution()` to the canonical Cargo compiler-authority contract and applies it to build-level and target-level `rustdocflags`. The rule is fail-closed for the three executable selectors in split or `--option=value` form; it is not a blanket ban on doctest flags. + +Primary references: + +- Cargo Book, Configuration: `build.rustdocflags` and target `rustdocflags` are custom flags passed to rustdoc; environment/direct-command sources have separate precedence. +- rustdoc book, Command-line arguments: `--test-runtool` executes a chosen wrapper instead of the doctest executable. +- rustdoc book, Unstable features: `--test-builder` selects the rustc-like program used to compile doctests and `--test-builder-wrapper` wraps that program. + +## Security effect + +Repository review now covers the Git-owned Cargo paths that could otherwise select a doctest runner, test compiler, or compiler wrapper while leaving Browser Session source and dependency topology unchanged. Approval of any such executable later must be an explicit provenance decision, not an incidental rustdoc flag. + +## Residual execution provenance + +This source contract intentionally does not claim authority over: + +- `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, `CARGO_BUILD_RUSTDOCFLAGS`, or target-specific environment overrides; +- direct `cargo rustdoc -- ...` / manual rustdoc invocation; +- runner-image, PATH, rustup/toolchain, default rustdoc/rustc identity, or externally supplied wrapper binaries; +- immutable artifact identity, SBOM/attestation, sandbox policy, compatibility qualification, and rollback for a future approved doctest execution program. + +Those surfaces require CI/release environment evidence from their canonical owners. A future approved runner/builder/wrapper must be versioned and immutable, tied to the exact toolchain and reviewed policy, and covered by executable tests before this fail-closed rule is relaxed. + +## Acceptance + +This generation is source-semantic only until the exact reconciled head has hosted repository/security execution and current-head independent review. A command acknowledgement, static inspection, or predecessor workflow result is not executable GREEN. From eeb6944e9f77f80dfc0aa5812dcce42b02b8f7b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:20:25 +0900 Subject: [PATCH 404/632] test(browser-session): prove doctest build-arg provenance gap --- ...oc_doctest_build_arg_authority_contract.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py diff --git a/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py b/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py new file mode 100644 index 000000000..17a6c2cd6 --- /dev/null +++ b/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py @@ -0,0 +1,70 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionRustdocDoctestBuildArgAuthorityContractTests(unittest.TestCase): + """Keep doctest compiler arguments inside the reviewed Cargo execution/input boundary.""" + + def _assert_fails_closed(self, config_text: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_forwarded_sysroot_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--doctest-build-arg=--sysroot=tools/review-bypass-sysroot"]\n' + ) + + def test_target_rustdocflags_forwarded_linker_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Zunstable-options\", \"--doctest-build-arg\", \"-C\", \"--doctest-build-arg\", \"linker=tools/review-bypass-linker\"]\n" + ) + + def test_build_rustdocflags_forwarded_codegen_backend_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-Zunstable-options", "--doctest-build-arg=-Zcodegen-backend=tools/review-bypass-codegen.so"]\n' + ) + + def test_non_authority_doctest_build_args_remain_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustdocflags = ["-Zunstable-options", "--doctest-build-arg=--cfg=originweave_reviewed", "--doctest-build-arg=-Copt-level=2"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 35733ea613225277f4baa7e100d4403453810e2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:21:54 +0900 Subject: [PATCH 405/632] fix(browser-session): classify doctest compiler forwarded authority --- ...ssion_cargo_compiler_authority_contract.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index a82bc6c72..949fb603d 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -232,6 +232,32 @@ def _flags_select_rustdoc_test_execution(value: object) -> bool: ) +def _flags_select_rustdoc_doctest_compiler_authority(value: object) -> bool: + """Return whether rustdoc forwards authority-extending arguments to a doctest compiler.""" + arguments = _flag_arguments(value) + forwarded: list[str] = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument == "--doctest-build-arg": + if index + 1 >= len(arguments): + return True + forwarded.append(arguments[index + 1]) + index += 2 + continue + if argument.startswith("--doctest-build-arg="): + forwarded.append(argument.partition("=")[2]) + index += 1 + + if not forwarded: + return False + return ( + _flags_select_codegen_backend(forwarded) + or _flags_select_linker(forwarded) + or any(_rustc_argument_extends_external_inputs(argument) for argument in forwarded) + ) + + def _configured_unstable_toolchain_inputs(value: object) -> list[str]: """Return Git-owned unstable Cargo settings that alter compiler or standard-library inputs.""" if not isinstance(value, dict): @@ -363,6 +389,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest execution") + if _flags_select_rustdoc_doctest_compiler_authority(build.get("rustdocflags")): + build_configured.append("rustdocflags:doctest compiler authority") target_configured: dict[str, list[str]] = {} target = parsed.get("target") @@ -389,6 +417,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(settings.get("rustdocflags")): configured.append("rustdocflags:doctest execution") + if _flags_select_rustdoc_doctest_compiler_authority(settings.get("rustdocflags")): + configured.append("rustdocflags:doctest compiler authority") if configured: target_configured[str(target_name)] = configured From e12677c55919c59e6a23bd8600214c3cab2f57a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:22:19 +0900 Subject: [PATCH 406/632] docs(browser-session): trace doctest build-arg authority --- ...ion-rustdoc-doctest-build-arg-authority.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/traceability/browser-session-rustdoc-doctest-build-arg-authority.md diff --git a/docs/traceability/browser-session-rustdoc-doctest-build-arg-authority.md b/docs/traceability/browser-session-rustdoc-doctest-build-arg-authority.md new file mode 100644 index 000000000..459db7e66 --- /dev/null +++ b/docs/traceability/browser-session-rustdoc-doctest-build-arg-authority.md @@ -0,0 +1,52 @@ +# Browser Session rustdoc doctest compiler-argument authority + +Status: Draft; source-semantic contract current on PR #317. Hosted executable evidence is still required before this generation is GREEN. + +## Problem + +Cargo can pass repository-owned `[build].rustdocflags` and matching `target..rustdocflags` directly to `rustdoc`. Nightly rustdoc's `--doctest-build-arg` then forwards one argument per occurrence to the compiler used to build documentation tests. This is a second-order compiler-input boundary: a reviewed rustdoc invocation can otherwise smuggle `--sysroot`, `--extern`, response files, code-generation backends/plugins, linker selection, linker scripts, native inputs, or other already-governed rustc/linker authority into doctest compilation. + +The Browser Session provenance contract therefore has to inspect the forwarded compiler argument vector, not merely the outer rustdoc command line. + +## Ownership and constraints + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source/dependency topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single owner for Git-owned Cargo-selected compiler/linker execution and external-input authority. This repair reuses its existing rustc/codegen/linker classifiers and does not add a second Cargo topology scanner. + +The rule does not blanket-ban `--doctest-build-arg`. Forwarded arguments that do not widen executable or external-input authority, such as a reviewed `--cfg` or `-Copt-level=2`, remain allowed. + +## RED → repair + +RED `eeb6944e9f77f80dfc0aa5812dcce42b02b8f7b4` adds hostile build/target fixtures for: + +- forwarded `--sysroot=...`, +- split forwarded `-C` + `linker=...`, and +- forwarded `-Zcodegen-backend=...`. + +It also retains `--cfg=originweave_reviewed` and `-Copt-level=2` as non-authority controls. + +Repair `35733ea613225277f4baa7e100d4403453810e2f` adds `_flags_select_rustdoc_doctest_compiler_authority()` to the canonical Cargo compiler-authority contract. The helper reconstructs the ordered compiler arguments carried by split and `--doctest-build-arg=...` forms, fails closed on a missing operand, and reuses the existing codegen, linker, and external-input classifiers. Both build-level and target-level `rustdocflags` consume the same helper. + +## Primary evidence + +- The Cargo Book, *Configuration*, documents `build.rustdocflags` and `target..rustdocflags` as low-level custom flags passed to rustdoc and separately identifies environment/direct-command sources with higher precedence. +- The nightly rustdoc book, *Unstable features*, documents `--doctest-build-arg` as a way to add arguments to rustc when compiling doctests; the unstable command-line family requires nightly rustdoc with `-Z unstable-options`. + +## Security effect + +Git-owned Cargo configuration can no longer use rustdoc's doctest compiler forwarding as an unchecked tunnel around the Browser Session compiler/linker provenance policy. The inner compiler vector is evaluated with the same authority semantics as direct Cargo rustflags rather than with a duplicate policy. + +## Residual execution provenance + +This repository-source contract intentionally does not claim authority over: + +- `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, `CARGO_BUILD_RUSTDOCFLAGS`, or target-specific environment overrides; +- direct `cargo rustdoc -- ...` or manual rustdoc invocation; +- runner image, PATH, rustup/toolchain, default rustdoc/rustc identity, or externally supplied compiler/linker artifacts; +- future rustdoc/rustc options that introduce a new execution/input grammar not yet represented by the canonical classifiers; +- immutable artifact identity, SBOM/attestation, sandbox policy, compatibility qualification, and rollback for any future approved external compiler component. + +Those surfaces require CI/release environment evidence from their canonical owners. A newly introduced rustdoc/rustc forwarding primitive is a fresh provenance finding until it is mapped to the canonical classifier and covered by hostile and control fixtures. + +## Acceptance + +This generation is source-semantic only until the exact reconciled head has hosted repository/security execution and current-head independent review. A command acknowledgement, static inspection, predecessor workflow result, or skipped Draft workflow is not executable GREEN. From 8842bb09d17d1ed3861080b3cf8ba7779a354123 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:31:42 +0900 Subject: [PATCH 407/632] test(browser-session): prove PGO profile input provenance gap --- ...on_pgo_profile_input_authority_contract.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_browser_session_pgo_profile_input_authority_contract.py diff --git a/tests/test_browser_session_pgo_profile_input_authority_contract.py b/tests/test_browser_session_pgo_profile_input_authority_contract.py new file mode 100644 index 000000000..9d28e21fe --- /dev/null +++ b/tests/test_browser_session_pgo_profile_input_authority_contract.py @@ -0,0 +1,70 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionPgoProfileInputAuthorityContractTests(unittest.TestCase): + """Keep repository-selected PGO data inside reviewed compiler-input provenance.""" + + def _assert_fails_closed(self, config_text: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_profile_use_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-Cprofile-use=tools/review-bypass.profdata"]\n' + ) + + def test_target_rustflags_profile_sample_use_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"profile-sample-use=tools/review-bypass.prof\"]\n" + ) + + def test_profile_rustflags_profile_use_fails_closed(self) -> None: + self._assert_fails_closed( + '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["--codegen=profile-use=tools/review-bypass.profdata"]\n' + ) + + def test_profile_generate_output_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-Cprofile-generate=target/pgo-data"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 76e8e8944475e068f84023758cc8fd6b83275a45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:35:35 +0900 Subject: [PATCH 408/632] fix(browser-session): govern PGO profile compiler inputs --- ...ssion_cargo_compiler_authority_contract.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 949fb603d..fd79cd8b5 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -89,6 +89,11 @@ def _codegen_option_selects_linker_plugin(option: str) -> bool: return value not in LINKER_PLUGIN_LTO_BOOLEAN_VALUES +def _codegen_option_extends_external_inputs(option: str) -> bool: + """Return whether one rustc codegen option consumes external optimization input.""" + return option.startswith(("profile-use=", "profile-sample-use=")) + + def _linker_argument_is_positional_native_input(argument: str) -> bool: """Return whether one unconsumed linker token is a positional external input.""" return bool(argument) and not argument.startswith("-") @@ -167,7 +172,22 @@ def _flag_arguments(value: object) -> list[str]: def _flags_extend_external_link_inputs(value: object) -> bool: """Return whether Git-owned Rust flags widen external compiler/documentation inputs.""" - return any(_rustc_argument_extends_external_inputs(argument) for argument in _flag_arguments(value)) + arguments = _flag_arguments(value) + for index, argument in enumerate(arguments): + if _rustc_argument_extends_external_inputs(argument): + return True + + option: str | None = None + if argument in {"-C", "--codegen"} and index + 1 < len(arguments): + option = arguments[index + 1] + elif argument.startswith("-C") and len(argument) > 2: + option = argument[2:] + elif argument.startswith("--codegen="): + option = argument.removeprefix("--codegen=") + + if option is not None and _codegen_option_extends_external_inputs(option): + return True + return False def _flags_select_codegen_backend(value: object) -> bool: @@ -254,7 +274,7 @@ def _flags_select_rustdoc_doctest_compiler_authority(value: object) -> bool: return ( _flags_select_codegen_backend(forwarded) or _flags_select_linker(forwarded) - or any(_rustc_argument_extends_external_inputs(argument) for argument in forwarded) + or _flags_extend_external_link_inputs(forwarded) ) From 6fa0537538789b4c899466ab1619e7d24bad8b36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:36:01 +0900 Subject: [PATCH 409/632] test(browser-session): cover rustdoc PGO input tunnels --- ...ser_session_pgo_profile_input_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_pgo_profile_input_authority_contract.py b/tests/test_browser_session_pgo_profile_input_authority_contract.py index 9d28e21fe..2ba60a0e1 100644 --- a/tests/test_browser_session_pgo_profile_input_authority_contract.py +++ b/tests/test_browser_session_pgo_profile_input_authority_contract.py @@ -58,6 +58,16 @@ def test_profile_rustflags_profile_use_fails_closed(self) -> None: '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["--codegen=profile-use=tools/review-bypass.profdata"]\n' ) + def test_build_rustdocflags_profile_use_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--codegen=profile-use=tools/review-bypass.profdata"]\n' + ) + + def test_doctest_build_arg_profile_use_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=profile-use=tools/review-bypass.profdata"]\n' + ) + def test_profile_generate_output_remains_allowed(self) -> None: directory, root = _workspace_with_config( '[build]\nrustflags = ["-Cprofile-generate=target/pgo-data"]\n' From 3ceb8fb31dfc3efaab239807dd0b477c8205fbdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:36:41 +0900 Subject: [PATCH 410/632] docs(browser-session): trace PGO profile input authority --- ...ser-session-pgo-profile-input-authority.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/traceability/browser-session-pgo-profile-input-authority.md diff --git a/docs/traceability/browser-session-pgo-profile-input-authority.md b/docs/traceability/browser-session-pgo-profile-input-authority.md new file mode 100644 index 000000000..ce854bb6b --- /dev/null +++ b/docs/traceability/browser-session-pgo-profile-input-authority.md @@ -0,0 +1,51 @@ +# Browser Session PGO profile input authority + +## Decision + +OriginWeave treats repository-selected profile-guided optimization data as compiler input provenance, not as a harmless optimization preference. + +Git-owned Cargo `rustflags`, profile `rustflags`, and `rustdocflags` must therefore fail closed when they select `-C profile-use=` or `-C profile-sample-use=`. The same rule applies when rustdoc forwards those arguments to the doctest compiler through `--doctest-build-arg`. + +`-C profile-generate=` remains allowed by this contract because it names an output location for newly collected profile data rather than an input consumed to shape the current binary. Approval of a future PGO workflow requires a separate immutable profile-artifact contract rather than weakening this rule. + +## Why this is provenance-sensitive + +Rust's PGO documentation defines the optimization workflow as collecting runtime profile data and feeding the resulting profile back into a later compilation. `-C profile-use=` supplies instrumentation-derived `.profdata`; `-C profile-sample-use=` supplies sampling-profile data. LLVM uses those data to guide inlining, machine-code layout, register allocation, and related optimization decisions. Two builds from the same reviewed Rust source and dependency graph can therefore produce materially different machine code when the profile input differs. + +The current rustc option model represents `profile_use` and `profile_sample_use` as path-bearing compiler inputs. A mutable or runner-local profile path would sit outside the reviewed Cargo package/source closure and outside the native/linker provenance rules already enforced by Browser Session. + +## RED → repair + +RED `8842bb09d17d1ed3861080b3cf8ba7779a354123` added hostile fixtures proving that the prior canonical Cargo compiler-authority contract accepted: + +- build-level `-Cprofile-use=...`; +- target-level split `-C` + `profile-sample-use=...`; +- Cargo profile `--codegen=profile-use=...`. + +Repair `76e8e8944475e068f84023758cc8fd6b83275a45` keeps the existing Cargo compiler-authority test as the single policy owner. It adds one `_codegen_option_extends_external_inputs()` classifier and extends `_flags_extend_external_link_inputs()` to parse split, compact, and long `-C`/`--codegen` forms. The rustdoc doctest forwarding path now reuses that same external-input classifier rather than maintaining a parallel list. + +Coverage commit `6fa0537538789b4c899466ab1619e7d24bad8b36` adds direct rustdoc and `--doctest-build-arg` hostile fixtures and retains `profile-generate` as a control. + +## Boundary and future approval requirements + +This contract governs only Git-owned repository configuration already traversed by the canonical Browser Session Cargo authority test. It does not claim control over ambient `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS`, direct CLI injection, ancestor or `$CARGO_HOME` configuration, runner images, or externally materialized profile artifacts. Those remain CI/release supply-chain evidence surfaces. + +If OriginWeave later adopts PGO deliberately, the profile must be a versioned immutable artifact with at least: + +- exact source/workload/toolchain identity and generation procedure; +- content digest and immutable storage identity; +- workload/data provenance and purpose constraints; +- compiler/LLVM compatibility evidence; +- SBOM/provenance linkage to the binary that consumes it; +- reproducibility comparison against the non-PGO reference build; +- rollback and expiry/re-generation policy. + +A path allowlist alone is insufficient because the profile contents, not only the pathname, influence generated machine code. + +## References + +The Rust Project Developers. (2026). *Codegen options: profile-use*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/#profile-use + +The Rust Project Developers. (2026). *Profile-guided optimization*. The rustc book. https://doc.rust-lang.org/nightly/rustc/profile-guided-optimization.html + +The Rust Project Developers. (2026). *CodegenOptions in rustc_session::options*. Rust compiler documentation. https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/options/struct.CodegenOptions.html From ef3455a922d9467e8dcd2253dbb820b9dcde7303 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 19:05:50 +0900 Subject: [PATCH 411/632] test(browser-session): RED direct LLVM option authority --- ...er_session_llvm_args_authority_contract.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_browser_session_llvm_args_authority_contract.py diff --git a/tests/test_browser_session_llvm_args_authority_contract.py b/tests/test_browser_session_llvm_args_authority_contract.py new file mode 100644 index 000000000..514fbf58e --- /dev/null +++ b/tests/test_browser_session_llvm_args_authority_contract.py @@ -0,0 +1,80 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLlvmArgsAuthorityContractTests(unittest.TestCase): + """Keep repository-selected direct LLVM option authority outside the reviewed build TCB.""" + + def _assert_fails_closed(self, config_text: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_llvm_args_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "llvm-args=-debug-pass=Structure"]\n' + ) + + def test_target_rustflags_compact_llvm_args_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-Cllvm-args=-print-after-all\"]\n" + ) + + def test_profile_rustflags_long_llvm_args_fails_closed(self) -> None: + self._assert_fails_closed( + '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["--codegen=llvm-args=-debug-pass=Structure"]\n' + ) + + def test_build_rustdocflags_llvm_args_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--codegen", "llvm-args=-debug-pass=Structure"]\n' + ) + + def test_doctest_build_arg_llvm_args_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=llvm-args=-debug-pass=Structure"]\n' + ) + + def test_typed_codegen_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "opt-level=2"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 883f63a4a7706af6d70d93ac6e5fcfb632a7f7b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 19:07:59 +0900 Subject: [PATCH 412/632] fix(browser-session): fail closed on direct LLVM arguments --- ..._session_cargo_compiler_authority_contract.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index fd79cd8b5..dca9d6003 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -191,7 +191,7 @@ def _flags_extend_external_link_inputs(value: object) -> bool: def _flags_select_codegen_backend(value: object) -> bool: - """Return whether Git-owned rustc flags replace or dynamically extend code generation.""" + """Return whether Git-owned flags replace or bypass the reviewed code-generation surface.""" arguments = _flag_arguments(value) for index, argument in enumerate(arguments): if argument.startswith(("-Zcodegen-backend=", "-Zllvm-plugins=")): @@ -202,6 +202,16 @@ def _flags_select_codegen_backend(value: object) -> bool: and arguments[index + 1].startswith(("codegen-backend=", "llvm-plugins=")) ): return True + + option: str | None = None + if argument in {"-C", "--codegen"} and index + 1 < len(arguments): + option = arguments[index + 1] + elif argument.startswith("-C") and len(argument) > 2: + option = argument[2:] + elif argument.startswith("--codegen="): + option = argument.removeprefix("--codegen=") + if option == "llvm-args" or (option is not None and option.startswith("llvm-args=")): + return True return False @@ -403,6 +413,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustflags")): build_configured.append("rustflags:external link input") + if _flags_select_codegen_backend(build.get("rustdocflags")): + build_configured.append("rustdocflags:codegen backend") if _flags_select_linker(build.get("rustdocflags")): build_configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustdocflags")): @@ -431,6 +443,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustflags")): configured.append("rustflags:external link input") + if _flags_select_codegen_backend(settings.get("rustdocflags")): + configured.append("rustdocflags:codegen backend") if _flags_select_linker(settings.get("rustdocflags")): configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustdocflags")): From 59b504b16b4231751bb51acb9f7fb1a033ee61cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 19:08:38 +0900 Subject: [PATCH 413/632] docs(browser-session): trace direct LLVM argument authority --- .../browser-session-llvm-args-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-llvm-args-authority.md diff --git a/docs/traceability/browser-session-llvm-args-authority.md b/docs/traceability/browser-session-llvm-args-authority.md new file mode 100644 index 000000000..204cae1ae --- /dev/null +++ b/docs/traceability/browser-session-llvm-args-authority.md @@ -0,0 +1,47 @@ +# Browser Session direct LLVM argument authority + +Status: Draft evidence for PR #317. This document describes a repository-source provenance boundary; it is not hosted execution or browser-observed acceptance evidence. + +## Problem + +`rustc -C llvm-args="..."` passes arguments directly to LLVM. The rustc book explicitly states that this surface talks directly to LLVM and is not covered by rustc's normal CLI stability guarantees. Current rustc also combines target-spec LLVM arguments with user `-Cllvm-args`, places the user-provided values after target-spec values, and forwards the resulting argument vector to `LLVMRustSetLLVMOptions`; the source notes LLVM `cl::opt` last-wins behavior. + +That means a Git-owned Cargo configuration can bypass the reviewed, typed rustc option surface without changing the Cargo package graph, compiler binary, codegen backend, or linker. Treating individual LLVM flags as ordinary tuning would require OriginWeave to mirror a compiler-version-specific LLVM command-line grammar and continuously distinguish harmless tuning from options that alter execution, consume files, or change target/code-generation behavior. That is not a stable Browser Session contract. + +## Decision + +Repository-owned `llvm-args` is fail closed wherever the existing Cargo compiler-authority contract already accepts Rust flags: + +- `[build].rustflags` and `[target.*].rustflags`; +- profile `rustflags` in the root manifest or Cargo config; +- `[build].rustdocflags` and `[target.*].rustdocflags`; +- rustdoc `--doctest-build-arg` forwarding into rustc. + +The shared `_flags_select_codegen_backend()` classifier recognizes split `-C llvm-args=...`, compact `-Cllvm-args=...`, split `--codegen llvm-args=...`, and `--codegen=llvm-args=...`. Direct rustdoc flag paths now invoke that same classifier rather than maintaining a rustdoc-specific LLVM list. + +Typed, modeled codegen options such as `-C opt-level=2` remain allowed. This is deliberately not a blanket ban on code-generation tuning. + +## Alternatives considered + +An LLVM-argument allowlist was rejected for now. LLVM options are toolchain-version-specific, include hidden/internal options, and are explicitly outside rustc's normal CLI stability contract. A path- or prefix-based heuristic would therefore create a false sense of provenance coverage. + +Blocking only a known dynamic-plugin spelling was also rejected. The security boundary is the direct LLVM option tunnel itself, not one currently observed spelling. If a buyer requires a specific LLVM option later, the correct path is an explicit versioned contract tied to the exact rustc/LLVM toolchain and immutable build evidence. + +## RED -> repair + +- RED: `ef3455a922d9467e8dcd2253dbb820b9dcde7303` adds build, target, profile, direct-rustdoc, and doctest-forwarding hostile cases plus a typed-codegen control. +- Repair: `883f63a4a7706af6d70d93ac6e5fcfb632a7f7b1` extends the existing shared code-generation classifier and wires direct rustdoc flags through that same owner. + +The repair does not create another Cargo topology scanner. `tests/test_browser_session_trusted_adapter_boundary.py` remains the production Cargo package/source-topology owner, while `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the repository-selected compiler/rustdoc/toolchain execution and input-authority owner. + +## Residual boundary + +This source contract does not prove the absence of ambient LLVM arguments supplied outside reviewed Git content. Environment/direct CLI arguments, ancestor or `$CARGO_HOME` configuration, runner images, rustup/toolchain contents, and externally materialized compiler artifacts remain CI/release supply-chain evidence surfaces. A future approved direct LLVM option requires an exact toolchain identity, documented option semantics for that compiler build, reproducibility evidence, SBOM/provenance linkage where applicable, and rollback criteria. + +## References + +Rust Project. (2026). *Codegen options: llvm-args*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/#llvm-args + +Rust Project. (2026). *rustc_codegen_llvm::llvm_util source*. Nightly rustc documentation. https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_codegen_llvm/llvm_util.rs.html + +LLVM Project. (2026). *Using the new pass manager*. https://llvm.org/docs/NewPassManager.html From 2f8233cb7957ffd959d88ed8b3aca44bf3f6f001 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:01:31 +0900 Subject: [PATCH 414/632] test(browser-session): red rustdoc render file inputs --- ...rustdoc_render_input_authority_contract.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_render_input_authority_contract.py diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py new file mode 100644 index 000000000..800a46934 --- /dev/null +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -0,0 +1,74 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionRustdocRenderInputAuthorityContractTests(unittest.TestCase): + """Keep rustdoc-rendered file inputs inside the reviewed documentation provenance boundary.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_render_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_html_header_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + '[build]\nrustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]\n' + ) + + def test_target_rustdocflags_html_body_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"--html-before-content=tools/review-bypass-before.html\"]\n" + ) + + def test_build_rustdocflags_css_and_theme_inputs_fail_closed(self) -> None: + for rustdocflags in ( + '["--extend-css", "tools/review-bypass.css"]', + '["--theme", "tools/review-bypass-theme.css"]', + '["--check-theme", "tools/review-bypass-theme.css"]', + ): + with self.subTest(rustdocflags=rustdocflags): + self._assert_render_input_fails_closed( + f"[build]\nrustdocflags = {rustdocflags}\n" + ) + + def test_unrelated_rustdoc_render_flags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["--document-private-items", "--default-theme", "ayu"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From ab259680e9bc8c6fde2221cd4c12d2a36721e93a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:03:59 +0900 Subject: [PATCH 415/632] fix(browser-session): reject rustdoc render file inputs --- ...ssion_cargo_compiler_authority_contract.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index dca9d6003..03a391c5d 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -27,6 +27,18 @@ LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} ) +RUSTDOC_RENDER_FILE_INPUT_OPTIONS = frozenset( + { + "--html-in-header", + "--html-before-content", + "--html-after-content", + "--markdown-before-content", + "--markdown-after-content", + "--extend-css", + "--theme", + "--check-theme", + } +) def _linker_driver_argument_selects_executable(argument: str) -> bool: @@ -262,6 +274,19 @@ def _flags_select_rustdoc_test_execution(value: object) -> bool: ) +def _flags_select_rustdoc_render_file_input(value: object) -> bool: + """Return whether Git-owned rustdoc flags load external files into rendered documentation.""" + long_equals_prefixes = tuple(f"{option}=" for option in RUSTDOC_RENDER_FILE_INPUT_OPTIONS) + for argument in _flag_arguments(value): + if argument in RUSTDOC_RENDER_FILE_INPUT_OPTIONS: + return True + if argument.startswith(long_equals_prefixes): + return True + if argument == "-e" or (argument.startswith("-e") and not argument.startswith("--")): + return True + return False + + def _flags_select_rustdoc_doctest_compiler_authority(value: object) -> bool: """Return whether rustdoc forwards authority-extending arguments to a doctest compiler.""" arguments = _flag_arguments(value) @@ -421,6 +446,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest execution") + if _flags_select_rustdoc_render_file_input(build.get("rustdocflags")): + build_configured.append("rustdocflags:render file input") if _flags_select_rustdoc_doctest_compiler_authority(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest compiler authority") @@ -451,6 +478,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(settings.get("rustdocflags")): configured.append("rustdocflags:doctest execution") + if _flags_select_rustdoc_render_file_input(settings.get("rustdocflags")): + configured.append("rustdocflags:render file input") if _flags_select_rustdoc_doctest_compiler_authority(settings.get("rustdocflags")): configured.append("rustdocflags:doctest compiler authority") if configured: From a1d8a7fe27ed67f2189dd19f276cbc960632441c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:04:23 +0900 Subject: [PATCH 416/632] test(browser-session): cover rustdoc render selectors --- ...rustdoc_render_input_authority_contract.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index 800a46934..6eeb54b67 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -42,30 +42,32 @@ def _assert_render_input_fails_closed(self, config_text: str) -> None: with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) - def test_build_rustdocflags_html_header_input_fails_closed(self) -> None: - self._assert_render_input_fails_closed( - '[build]\nrustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]\n' + def test_build_rustdocflags_render_file_inputs_fail_closed(self) -> None: + selectors = ( + ("--html-in-header", "tools/review-bypass-header.html"), + ("--html-before-content", "tools/review-bypass-before.html"), + ("--html-after-content", "tools/review-bypass-after.html"), + ("--markdown-before-content", "tools/review-bypass-before.md"), + ("--markdown-after-content", "tools/review-bypass-after.md"), + ("--extend-css", "tools/review-bypass.css"), + ("--theme", "tools/review-bypass-theme.css"), + ("--check-theme", "tools/review-bypass-theme.css"), + ("-e", "tools/review-bypass-short.css"), ) + for selector, path in selectors: + with self.subTest(selector=selector): + self._assert_render_input_fails_closed( + f'[build]\nrustdocflags = ["{selector}", "{path}"]\n' + ) - def test_target_rustdocflags_html_body_input_fails_closed(self) -> None: + def test_target_rustdocflags_equals_render_file_input_fails_closed(self) -> None: self._assert_render_input_fails_closed( "[target.'cfg(unix)']\nrustdocflags = [\"--html-before-content=tools/review-bypass-before.html\"]\n" ) - def test_build_rustdocflags_css_and_theme_inputs_fail_closed(self) -> None: - for rustdocflags in ( - '["--extend-css", "tools/review-bypass.css"]', - '["--theme", "tools/review-bypass-theme.css"]', - '["--check-theme", "tools/review-bypass-theme.css"]', - ): - with self.subTest(rustdocflags=rustdocflags): - self._assert_render_input_fails_closed( - f"[build]\nrustdocflags = {rustdocflags}\n" - ) - def test_unrelated_rustdoc_render_flags_remain_allowed(self) -> None: root = self._workspace_with_config( - '[build]\nrustdocflags = ["--document-private-items", "--default-theme", "ayu"]\n' + '[build]\nrustdocflags = ["--document-private-items", "--default-theme", "ayu", "--markdown-css", "reviewed.css"]\n' ) authority._assert_no_repository_cargo_compiler_execution_overrides(root) From 02efe8ca816878c3b5cb45dab192f39e7bc1f2e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:04:53 +0900 Subject: [PATCH 417/632] docs(browser-session): trace rustdoc render file provenance --- ...ion-rustdoc-render-file-input-authority.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/traceability/browser-session-rustdoc-render-file-input-authority.md diff --git a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md new file mode 100644 index 000000000..2b9485689 --- /dev/null +++ b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md @@ -0,0 +1,67 @@ +# Browser Session rustdoc render-file input authority + +Status: source-semantic repair evidence; not hosted executable GREEN. + +## Problem + +OriginWeave treats `tests/test_browser_session_cargo_compiler_authority_contract.py` as the single writer for repository-selected Cargo compiler/rustdoc execution and input authority. The existing contract rejected rustdoc replacement, `@path`, `--extern`, `-L`/`-l`, sysroot, codegen/linker authority, doctest execution programs, and doctest compiler forwarding, but it did not classify rustdoc's rendering file selectors. + +Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. + +For a repository that publishes generated documentation, that is a provenance and documentation-integrity gap. It is not treated as Browser Session runtime policy authority, and no claim is made that every such input is executable script content. + +## Constraints + +- Production Cargo package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`. +- This contract must not create a second Cargo configuration/topology scanner. +- Ordinary rustdoc presentation controls that do not make rustdoc read another file, such as `--document-private-items`, `--default-theme`, and `--markdown-css`, remain outside this fail-closed rule. +- Environment `RUSTDOCFLAGS` / `CARGO_ENCODED_RUSTDOCFLAGS`, ancestor or `$CARGO_HOME` configuration, and direct `cargo rustdoc -- ...` remain CI/release environment provenance surfaces. + +## Alternatives considered + +1. **Allow arbitrary render files when their path is repository-relative.** Rejected. A path spelling does not establish immutable identity, reviewed ownership, symlink containment, or release provenance. +2. **Copy rustdoc option parsing into a new supplemental Cargo scanner.** Rejected because it would violate the existing compiler-authority single-writer boundary. +3. **Fail closed on the file-selecting rustdoc options in the existing owner.** Selected. It is small, deterministic, and preserves the current authority topology. + +## Decision + +RED commit `2f8233cb7957ffd959d88ed8b3aca44bf3f6f001` added a realistic repository Cargo fixture in `tests/test_browser_session_rustdoc_render_input_authority_contract.py`. Before the repair, `build.rustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]` and equivalent target/render-file selectors were not rejected by the canonical authority helper. + +Repair commit `ab259680e9bc8c6fde2221cd4c12d2a36721e93a` added `_flags_select_rustdoc_render_file_input()` to the existing compiler-authority owner and applies it to both build-level and target-level `rustdocflags`. The classifier fail-closes on: + +- `--html-in-header` +- `--html-before-content` +- `--html-after-content` +- `--markdown-before-content` +- `--markdown-after-content` +- `--extend-css` and its short `-e` form +- `--theme` +- `--check-theme` + +Coverage commit `a1d8a7fe27ed67f2189dd19f276cbc960632441c` exercises every modeled selector, an equals-form target configuration, and controls that must remain allowed. + +The rejection marker is `rustdocflags:render file input`. The trusted-adapter boundary remains the single writer for Cargo package/source discovery; this change only extends the existing rustdoc input-authority classifier. + +## Primary references + +- Rust Project. (2026). *The rustdoc book: Command-line arguments*. https://doc.rust-lang.org/rustdoc/command-line-arguments.html + - documents file-backed HTML inclusion, CSS extension, theme/check-theme, and the distinction between `--markdown-css` and files whose contents rustdoc reads. +- Rust Project. (2026). *rustdoc option definitions (`rustdoc/lib.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/lib.rs.html + - identifies the HTML/Markdown file selectors and `--extend-css` option classes used by current rustdoc. +- Rust Project. (2026). *rustdoc configuration (`rustdoc/config.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/config.rs.html + - shows `ExternalHtml::load` receiving the HTML/Markdown file option values during rustdoc configuration. +- Rust Project. (2026). *The Cargo Book: Configuration*. https://doc.rust-lang.org/cargo/reference/config.html + - documents `build.rustdocflags` as custom flags passed to rustdoc and Cargo's hierarchical configuration model. + +## Security and buyer effect + +Repository-reviewed Rust source can no longer silently acquire additional rendered-document content through Git-owned Cargo `rustdocflags` using the modeled rustdoc file selectors. This narrows the documentation supply-chain boundary and prevents a source review from incorrectly implying that generated documentation is derived only from reviewed repository inputs. + +This does **not** prove generated documentation publication, GitHub Pages deployment, CSP behavior, browser rendering, accessibility, or release provenance. Those require their own exact-head build/publish/browser evidence. + +## Residual risk and follow-up + +- Environment/direct-CLI rustdoc flags and ambient Cargo configuration remain CI/release supply-chain inputs. +- `--markdown-css` writes a stylesheet reference into Markdown-rendered HTML rather than loading the referenced file contents during rustdoc execution. It is intentionally not classified as this file-input surface; external-resource policy for published documentation should be owned by the docs/site publication boundary. +- Future rustdoc releases may add file-backed rendering options. Exact toolchain qualification must update this contract when those options become relevant. +- An eventual approved custom render asset contract must identify the artifact immutably, prove repository/release provenance and containment, and connect the generated documentation to SBOM/provenance and rollback evidence rather than relying on a pathname allowlist. From a17cb3d60e5a09b7e10131dcef9eec39bded3d97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:09:59 +0900 Subject: [PATCH 418/632] test(browser-session): red rustdoc index-page input --- ...rowser_session_rustdoc_render_input_authority_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index 6eeb54b67..b6d1a017b 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -60,6 +60,11 @@ def test_build_rustdocflags_render_file_inputs_fail_closed(self) -> None: f'[build]\nrustdocflags = ["{selector}", "{path}"]\n' ) + def test_build_rustdocflags_unstable_index_page_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--index-page", "tools/review-bypass-index.md"]\n' + ) + def test_target_rustdocflags_equals_render_file_input_fails_closed(self) -> None: self._assert_render_input_fails_closed( "[target.'cfg(unix)']\nrustdocflags = [\"--html-before-content=tools/review-bypass-before.html\"]\n" From 54041d692aafc9d2c9d55134db9df4810c5b76d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:13:18 +0900 Subject: [PATCH 419/632] fix(browser-session): reject rustdoc index-page input --- .../test_browser_session_cargo_compiler_authority_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 03a391c5d..b43e01b32 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -34,6 +34,7 @@ "--html-after-content", "--markdown-before-content", "--markdown-after-content", + "--index-page", "--extend-css", "--theme", "--check-theme", @@ -699,4 +700,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 5dda25c3d4892d1bb813f86dd9d0d6873a19a10a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:13:32 +0900 Subject: [PATCH 420/632] test(browser-session): cover rustdoc index-page equals form --- ...rowser_session_rustdoc_render_input_authority_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index b6d1a017b..061722dd0 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -65,6 +65,11 @@ def test_build_rustdocflags_unstable_index_page_input_fails_closed(self) -> None '[build]\nrustdocflags = ["-Z", "unstable-options", "--index-page", "tools/review-bypass-index.md"]\n' ) + def test_target_rustdocflags_equals_index_page_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Z\", \"unstable-options\", \"--index-page=tools/review-bypass-index.md\"]\n" + ) + def test_target_rustdocflags_equals_render_file_input_fails_closed(self) -> None: self._assert_render_input_fails_closed( "[target.'cfg(unix)']\nrustdocflags = [\"--html-before-content=tools/review-bypass-before.html\"]\n" From 8c17ecf297080d07a8e3b81994512a1c436984e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:13:54 +0900 Subject: [PATCH 421/632] docs(browser-session): trace rustdoc index-page input --- ...ion-rustdoc-render-file-input-authority.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md index 2b9485689..75334ae69 100644 --- a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md +++ b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md @@ -6,7 +6,7 @@ Status: source-semantic repair evidence; not hosted executable GREEN. OriginWeave treats `tests/test_browser_session_cargo_compiler_authority_contract.py` as the single writer for repository-selected Cargo compiler/rustdoc execution and input authority. The existing contract rejected rustdoc replacement, `@path`, `--extern`, `-L`/`-l`, sysroot, codegen/linker authority, doctest execution programs, and doctest compiler forwarding, but it did not classify rustdoc's rendering file selectors. -Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. +Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. The unstable `--index-page PATH` option is another file-backed surface: rustdoc converts the argument to a path, requires it to be a file, records it as a loaded path, and uses that Markdown file as the generated index page. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. For a repository that publishes generated documentation, that is a provenance and documentation-integrity gap. It is not treated as Browser Session runtime policy authority, and no claim is made that every such input is executable script content. @@ -25,21 +25,24 @@ For a repository that publishes generated documentation, that is a provenance an ## Decision -RED commit `2f8233cb7957ffd959d88ed8b3aca44bf3f6f001` added a realistic repository Cargo fixture in `tests/test_browser_session_rustdoc_render_input_authority_contract.py`. Before the repair, `build.rustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]` and equivalent target/render-file selectors were not rejected by the canonical authority helper. +Initial RED commit `2f8233cb7957ffd959d88ed8b3aca44bf3f6f001` added a realistic repository Cargo fixture in `tests/test_browser_session_rustdoc_render_input_authority_contract.py`. Before the repair, `build.rustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]` and equivalent target/render-file selectors were not rejected by the canonical authority helper. -Repair commit `ab259680e9bc8c6fde2221cd4c12d2a36721e93a` added `_flags_select_rustdoc_render_file_input()` to the existing compiler-authority owner and applies it to both build-level and target-level `rustdocflags`. The classifier fail-closes on: +Initial repair commit `ab259680e9bc8c6fde2221cd4c12d2a36721e93a` added `_flags_select_rustdoc_render_file_input()` to the existing compiler-authority owner and applies it to both build-level and target-level `rustdocflags`. Coverage commit `a1d8a7fe27ed67f2189dd19f276cbc960632441c` exercised the modeled stable/unstable HTML, Markdown, CSS and theme selectors plus safe controls. + +A fresh primary-source sweep then found the unstable `--index-page PATH` file input that the first classifier generation had not modeled. Follow-up RED `a17cb3d60e5a09b7e10131dcef9eec39bded3d97` added a Cargo fixture using `-Z unstable-options --index-page tools/review-bypass-index.md`; the prior classifier accepted it. Repair `54041d692aafc9d2c9d55134db9df4810c5b76d0` added `--index-page` to the same canonical selector set. Coverage `5dda25c3d4892d1bb813f86dd9d0d6873a19a10a` added the equals-form target configuration so split and equals spellings are both constrained. + +The classifier now fail-closes on: - `--html-in-header` - `--html-before-content` - `--html-after-content` - `--markdown-before-content` - `--markdown-after-content` +- unstable `--index-page` - `--extend-css` and its short `-e` form - `--theme` - `--check-theme` -Coverage commit `a1d8a7fe27ed67f2189dd19f276cbc960632441c` exercises every modeled selector, an equals-form target configuration, and controls that must remain allowed. - The rejection marker is `rustdocflags:render file input`. The trusted-adapter boundary remains the single writer for Cargo package/source discovery; this change only extends the existing rustdoc input-authority classifier. ## Primary references @@ -47,15 +50,15 @@ The rejection marker is `rustdocflags:render file input`. The trusted-adapter bo - Rust Project. (2026). *The rustdoc book: Command-line arguments*. https://doc.rust-lang.org/rustdoc/command-line-arguments.html - documents file-backed HTML inclusion, CSS extension, theme/check-theme, and the distinction between `--markdown-css` and files whose contents rustdoc reads. - Rust Project. (2026). *rustdoc option definitions (`rustdoc/lib.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/lib.rs.html - - identifies the HTML/Markdown file selectors and `--extend-css` option classes used by current rustdoc. + - identifies the HTML/Markdown file selectors, `--extend-css`, and unstable `--index-page PATH` used by current rustdoc. - Rust Project. (2026). *rustdoc configuration (`rustdoc/config.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/config.rs.html - - shows `ExternalHtml::load` receiving the HTML/Markdown file option values during rustdoc configuration. + - shows `ExternalHtml::load` receiving the HTML/Markdown file option values and separately shows `--index-page` becoming a `PathBuf`, being required to resolve to a file, and being recorded as a loaded path. - Rust Project. (2026). *The Cargo Book: Configuration*. https://doc.rust-lang.org/cargo/reference/config.html - documents `build.rustdocflags` as custom flags passed to rustdoc and Cargo's hierarchical configuration model. ## Security and buyer effect -Repository-reviewed Rust source can no longer silently acquire additional rendered-document content through Git-owned Cargo `rustdocflags` using the modeled rustdoc file selectors. This narrows the documentation supply-chain boundary and prevents a source review from incorrectly implying that generated documentation is derived only from reviewed repository inputs. +Repository-reviewed Rust source can no longer silently acquire additional rendered-document content through Git-owned Cargo `rustdocflags` using the modeled rustdoc file selectors, including the unstable custom index page. This narrows the documentation supply-chain boundary and prevents a source review from incorrectly implying that generated documentation is derived only from reviewed repository inputs. This does **not** prove generated documentation publication, GitHub Pages deployment, CSP behavior, browser rendering, accessibility, or release provenance. Those require their own exact-head build/publish/browser evidence. @@ -63,5 +66,6 @@ This does **not** prove generated documentation publication, GitHub Pages deploy - Environment/direct-CLI rustdoc flags and ambient Cargo configuration remain CI/release supply-chain inputs. - `--markdown-css` writes a stylesheet reference into Markdown-rendered HTML rather than loading the referenced file contents during rustdoc execution. It is intentionally not classified as this file-input surface; external-resource policy for published documentation should be owned by the docs/site publication boundary. +- Rustdoc's unstable documentation-metadata exchange options are a separate provenance surface from render-file inclusion and require their own explicit classification rather than being mislabeled as render files. - Future rustdoc releases may add file-backed rendering options. Exact toolchain qualification must update this contract when those options become relevant. - An eventual approved custom render asset contract must identify the artifact immutably, prove repository/release provenance and containment, and connect the generated documentation to SBOM/provenance and rollback evidence rather than relying on a pathname allowlist. From b9103bd862a295954f7fc809e0d732fa982713f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:14:14 +0900 Subject: [PATCH 422/632] test(browser-session): red rustdoc metadata input directories --- ...stdoc_doc_meta_input_authority_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py diff --git a/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py b/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py new file mode 100644 index 000000000..bfa8b80c2 --- /dev/null +++ b/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionRustdocDocMetaInputAuthorityContractTests(unittest.TestCase): + """Keep rustdoc cross-crate metadata inputs inside reviewed documentation provenance.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_documentation_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_read_doc_meta_dir_fails_closed(self) -> None: + self._assert_documentation_input_fails_closed( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--read-doc-meta-dir", "tools/review-bypass-doc-meta"]\n' + ) + + def test_target_rustdocflags_equals_read_doc_meta_dir_fails_closed(self) -> None: + self._assert_documentation_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Z\", \"unstable-options\", \"--read-doc-meta-dir=tools/review-bypass-doc-meta\"]\n" + ) + + def test_write_doc_meta_dir_output_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--write-doc-meta-dir", "target/reviewed-doc-meta"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 47ff4370afdda5487224c437f6883d8947500c3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:15:18 +0900 Subject: [PATCH 423/632] fix(browser-session): reject rustdoc metadata inputs --- ...ssion_cargo_compiler_authority_contract.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b43e01b32..56f37e063 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -27,7 +27,7 @@ LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} ) -RUSTDOC_RENDER_FILE_INPUT_OPTIONS = frozenset( +RUSTDOC_DOCUMENTATION_INPUT_OPTIONS = frozenset( { "--html-in-header", "--html-before-content", @@ -38,6 +38,7 @@ "--extend-css", "--theme", "--check-theme", + "--read-doc-meta-dir", } ) @@ -275,11 +276,11 @@ def _flags_select_rustdoc_test_execution(value: object) -> bool: ) -def _flags_select_rustdoc_render_file_input(value: object) -> bool: - """Return whether Git-owned rustdoc flags load external files into rendered documentation.""" - long_equals_prefixes = tuple(f"{option}=" for option in RUSTDOC_RENDER_FILE_INPUT_OPTIONS) +def _flags_select_rustdoc_documentation_input(value: object) -> bool: + """Return whether Git-owned rustdoc flags load external files/directories into documentation generation.""" + long_equals_prefixes = tuple(f"{option}=" for option in RUSTDOC_DOCUMENTATION_INPUT_OPTIONS) for argument in _flag_arguments(value): - if argument in RUSTDOC_RENDER_FILE_INPUT_OPTIONS: + if argument in RUSTDOC_DOCUMENTATION_INPUT_OPTIONS: return True if argument.startswith(long_equals_prefixes): return True @@ -447,8 +448,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest execution") - if _flags_select_rustdoc_render_file_input(build.get("rustdocflags")): - build_configured.append("rustdocflags:render file input") + if _flags_select_rustdoc_documentation_input(build.get("rustdocflags")): + build_configured.append("rustdocflags:documentation input") if _flags_select_rustdoc_doctest_compiler_authority(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest compiler authority") @@ -479,8 +480,8 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustdocflags:external link input") if _flags_select_rustdoc_test_execution(settings.get("rustdocflags")): configured.append("rustdocflags:doctest execution") - if _flags_select_rustdoc_render_file_input(settings.get("rustdocflags")): - configured.append("rustdocflags:render file input") + if _flags_select_rustdoc_documentation_input(settings.get("rustdocflags")): + configured.append("rustdocflags:documentation input") if _flags_select_rustdoc_doctest_compiler_authority(settings.get("rustdocflags")): configured.append("rustdocflags:doctest compiler authority") if configured: From 936ad9255b4ba81fcf31ea206026aaffea202232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:15:49 +0900 Subject: [PATCH 424/632] docs(browser-session): trace rustdoc metadata input provenance --- ...ession-rustdoc-doc-meta-input-authority.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/traceability/browser-session-rustdoc-doc-meta-input-authority.md diff --git a/docs/traceability/browser-session-rustdoc-doc-meta-input-authority.md b/docs/traceability/browser-session-rustdoc-doc-meta-input-authority.md new file mode 100644 index 000000000..bc79963eb --- /dev/null +++ b/docs/traceability/browser-session-rustdoc-doc-meta-input-authority.md @@ -0,0 +1,41 @@ +# Browser Session rustdoc documentation-metadata input authority + +Status: source-semantic repair evidence; not hosted executable GREEN. + +## Problem + +Nightly rustdoc exposes `--write-doc-meta-dir` and `--read-doc-meta-dir` behind `-Z unstable-options`. The write option emits a crate's shared documentation metadata to a directory. The read option is different: rustdoc enters finalize mode without crate source and merges cross-crate state from one or more supplied metadata directories into the documentation output. + +Repository-owned Cargo `rustdocflags` could therefore select an external `--read-doc-meta-dir` and change generated cross-crate documentation/search state without changing the reviewed Rust source, Cargo package topology, or compiler/linker inputs. Treating the source tree as the complete documentation provenance would be false. + +## Boundary and alternatives + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected rustc/rustdoc execution and external input authority. No second Cargo scanner or metadata parser is introduced. + +Path allowlisting was rejected because a directory spelling does not prove immutable contents, ownership, symlink containment, release identity, or compatibility with the rustdoc/toolchain that generated the metadata. Blocking both read and write metadata directories was also rejected: `--write-doc-meta-dir` selects an output destination rather than an external documentation input. + +## RED → repair + +RED `b9103bd862a295954f7fc809e0d732fa982713f4` adds `tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py` with: + +- build-level split `--read-doc-meta-dir PATH`, +- target-level equals `--read-doc-meta-dir=PATH`, and +- `--write-doc-meta-dir PATH` as an allowed output-only control. + +The prior authority classifier accepted both hostile read cases. + +Repair `47ff4370afdda5487224c437f6883d8947500c3f` broadens the existing rustdoc documentation-input classifier rather than creating another topology scan. `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS` now includes `--read-doc-meta-dir` alongside the existing rendered-file inputs. Build- and target-level `rustdocflags` use the same `_flags_select_rustdoc_documentation_input()` call site and report `rustdocflags:documentation input`. + +`--write-doc-meta-dir` remains allowed because it is an output destination. That distinction is a contract invariant, not a naming convenience. + +## Primary reference + +Rust Project. (2026). *The rustdoc book: Unstable features*. https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html + +The current rustdoc book states that `--read-doc-meta-dir` runs rustdoc in finalize mode, accepts multiple metadata directories, and is used to merge cross-crate state; no crate source is supplied in that mode. It separately states that `--write-doc-meta-dir` writes shared metadata to a directory. + +## Buyer/security effect + +A repository review can no longer silently acquire cross-crate documentation metadata through Git-owned build/target `rustdocflags` while claiming that generated documentation is derived only from the reviewed source/dependency closure. An eventual approved metadata import requires immutable directory/artifact identity, producer toolchain identity, crate/source provenance, content digests, compatibility evidence, SBOM/provenance linkage, and rollback/expiry rules. + +This source contract does not prove a generated-docs publication, GitHub Pages deployment, browser rendering, accessibility, CSP, or immutable release. Environment/direct-CLI `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, ancestor/`$CARGO_HOME` config, external metadata producers, and runner/toolchain state remain CI/release provenance surfaces. From 333561218e8c85c509dffefa9771fb944a308bab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 20:16:10 +0900 Subject: [PATCH 425/632] docs(browser-session): align rustdoc documentation input owner --- ...wser-session-rustdoc-render-file-input-authority.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md index 75334ae69..fb5c1e89f 100644 --- a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md +++ b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md @@ -27,11 +27,13 @@ For a repository that publishes generated documentation, that is a provenance an Initial RED commit `2f8233cb7957ffd959d88ed8b3aca44bf3f6f001` added a realistic repository Cargo fixture in `tests/test_browser_session_rustdoc_render_input_authority_contract.py`. Before the repair, `build.rustdocflags = ["--html-in-header", "tools/review-bypass-header.html"]` and equivalent target/render-file selectors were not rejected by the canonical authority helper. -Initial repair commit `ab259680e9bc8c6fde2221cd4c12d2a36721e93a` added `_flags_select_rustdoc_render_file_input()` to the existing compiler-authority owner and applies it to both build-level and target-level `rustdocflags`. Coverage commit `a1d8a7fe27ed67f2189dd19f276cbc960632441c` exercised the modeled stable/unstable HTML, Markdown, CSS and theme selectors plus safe controls. +Initial repair commit `ab259680e9bc8c6fde2221cd4c12d2a36721e93a` added a rustdoc file-input classifier to the existing compiler-authority owner and applied it to both build-level and target-level `rustdocflags`. Coverage commit `a1d8a7fe27ed67f2189dd19f276cbc960632441c` exercised the modeled stable/unstable HTML, Markdown, CSS and theme selectors plus safe controls. A fresh primary-source sweep then found the unstable `--index-page PATH` file input that the first classifier generation had not modeled. Follow-up RED `a17cb3d60e5a09b7e10131dcef9eec39bded3d97` added a Cargo fixture using `-Z unstable-options --index-page tools/review-bypass-index.md`; the prior classifier accepted it. Repair `54041d692aafc9d2c9d55134db9df4810c5b76d0` added `--index-page` to the same canonical selector set. Coverage `5dda25c3d4892d1bb813f86dd9d0d6873a19a10a` added the equals-form target configuration so split and equals spellings are both constrained. -The classifier now fail-closes on: +A later documentation-metadata finding broadened the owner name, not the render-file semantics. Repair `47ff4370afdda5487224c437f6883d8947500c3f` renamed the shared classifier to `_flags_select_rustdoc_documentation_input()` and its option set to `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS`, with rejection marker `rustdocflags:documentation input`, so rendered-file and cross-crate metadata inputs share one accurate authority boundary. The separate metadata rationale and RED are documented in `browser-session-rustdoc-doc-meta-input-authority.md`. + +The classifier now includes these render-file selectors: - `--html-in-header` - `--html-before-content` @@ -43,7 +45,7 @@ The classifier now fail-closes on: - `--theme` - `--check-theme` -The rejection marker is `rustdocflags:render file input`. The trusted-adapter boundary remains the single writer for Cargo package/source discovery; this change only extends the existing rustdoc input-authority classifier. +The trusted-adapter boundary remains the single writer for Cargo package/source discovery; this change only extends the existing rustdoc input-authority classifier. ## Primary references @@ -66,6 +68,6 @@ This does **not** prove generated documentation publication, GitHub Pages deploy - Environment/direct-CLI rustdoc flags and ambient Cargo configuration remain CI/release supply-chain inputs. - `--markdown-css` writes a stylesheet reference into Markdown-rendered HTML rather than loading the referenced file contents during rustdoc execution. It is intentionally not classified as this file-input surface; external-resource policy for published documentation should be owned by the docs/site publication boundary. -- Rustdoc's unstable documentation-metadata exchange options are a separate provenance surface from render-file inclusion and require their own explicit classification rather than being mislabeled as render files. +- Rustdoc's unstable `--read-doc-meta-dir` is now classified by the same canonical documentation-input owner, but its directory/merge semantics and output-only `--write-doc-meta-dir` control are documented separately. - Future rustdoc releases may add file-backed rendering options. Exact toolchain qualification must update this contract when those options become relevant. - An eventual approved custom render asset contract must identify the artifact immutably, prove repository/release provenance and containment, and connect the generated documentation to SBOM/provenance and rollback evidence rather than relying on a pathname allowlist. From f455739da5953f0c70d4289cef795a967a0348c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 21:07:49 +0900 Subject: [PATCH 426/632] test(browser-session): expose incremental cache provenance bypass --- ...remental_cache_input_authority_contract.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/test_browser_session_incremental_cache_input_authority_contract.py diff --git a/tests/test_browser_session_incremental_cache_input_authority_contract.py b/tests/test_browser_session_incremental_cache_input_authority_contract.py new file mode 100644 index 000000000..2293b6475 --- /dev/null +++ b/tests/test_browser_session_incremental_cache_input_authority_contract.py @@ -0,0 +1,78 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionIncrementalCacheInputAuthorityContractTests(unittest.TestCase): + """Keep repository-selected incremental cache state inside reviewed compiler provenance.""" + + def _assert_fails_closed(self, config_text: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_incremental_cache_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "incremental=tools/review-bypass-incremental"]\n' + ) + + def test_target_rustflags_compact_incremental_cache_path_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-Cincremental=tools/review-bypass-incremental\"]\n" + ) + + def test_profile_rustflags_incremental_cache_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["--codegen=incremental=tools/review-bypass-incremental"]\n' + ) + + def test_build_rustdocflags_incremental_cache_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--codegen=incremental=tools/review-bypass-incremental"]\n' + ) + + def test_doctest_build_arg_incremental_cache_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=incremental=tools/review-bypass-incremental"]\n' + ) + + def test_cargo_managed_incremental_toggle_remains_allowed(self) -> None: + directory, root = _workspace_with_config('[build]\nincremental = false\n') + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 6753b717486bd025c97043de7a53323c6dd4d47c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 21:09:11 +0900 Subject: [PATCH 427/632] fix(browser-session): fail closed on incremental cache input --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 56f37e063..afd15bc7b 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -104,8 +104,8 @@ def _codegen_option_selects_linker_plugin(option: str) -> bool: def _codegen_option_extends_external_inputs(option: str) -> bool: - """Return whether one rustc codegen option consumes external optimization input.""" - return option.startswith(("profile-use=", "profile-sample-use=")) + """Return whether one rustc codegen option consumes external or mutable compiler input.""" + return option.startswith(("incremental=", "profile-use=", "profile-sample-use=")) def _linker_argument_is_positional_native_input(argument: str) -> bool: @@ -701,4 +701,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 580cbe3c97252617b892e8f0267a480d7c54c419 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 21:09:42 +0900 Subject: [PATCH 428/632] docs(traceability): bind incremental cache provenance --- ...ssion-incremental-cache-input-authority.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/traceability/browser-session-incremental-cache-input-authority.md diff --git a/docs/traceability/browser-session-incremental-cache-input-authority.md b/docs/traceability/browser-session-incremental-cache-input-authority.md new file mode 100644 index 000000000..2eee6777b --- /dev/null +++ b/docs/traceability/browser-session-incremental-cache-input-authority.md @@ -0,0 +1,50 @@ +# Browser Session incremental-cache input authority + +## Decision + +OriginWeave treats an explicit repository-selected rustc `-C incremental=` value as mutable compiler-input authority. Git-owned Cargo `rustflags`, `rustdocflags`, profile `rustflags`, and rustdoc `--doctest-build-arg` forwarding must fail closed when they select an incremental cache directory. + +Cargo's own boolean `build.incremental` / profile `incremental` setting is not rejected by this source contract. Cargo owns its normal target-directory cache placement; the integrity and lifecycle of runner caches, `CARGO_INCREMENTAL`, target directories, toolchain images, and restored CI artifacts remain CI/release supply-chain evidence. + +## Problem + +`rustc -C incremental=` is not only a compile-speed preference. rustc stores compilation information in the selected directory and reuses it on later compilations. Current Cargo source also constructs the compiler invocation by adding `-C incremental=` for Cargo-managed incremental builds. A repository-selected arbitrary path can therefore make the reviewed Browser Session build consume mutable work products whose producer execution, source state, toolchain, lifetime, and digest are not established by the repository source tree. + +A path allowlist is insufficient: a reviewed pathname does not prove the identity or freshness of the cache contents, symlink containment, producer toolchain, or reproducible reconstruction. + +## Contract and causal repair + +The structural RED is commit `f455739da5953f0c70d4289cef795a967a0348c2`, `tests/test_browser_session_incremental_cache_input_authority_contract.py`. It fixes hostile cases for: + +- build `rustflags`: split `-C`, `incremental=`; +- target `rustflags`: compact `-Cincremental=`; +- Cargo profile `rustflags`: `--codegen=incremental=`; +- build `rustdocflags`; and +- rustdoc `--doctest-build-arg` forwarding. + +The same test keeps Cargo-managed `[build] incremental = false` as an allowed control. + +The minimal repair is commit `6753b717486bd025c97043de7a53323c6dd4d47c`. The canonical compiler-authority owner, `tests/test_browser_session_cargo_compiler_authority_contract.py`, extends `_codegen_option_extends_external_inputs()` to classify `incremental=` alongside PGO profile inputs. Existing build/target rustflags, rustdocflags, profile-rustflags, and doctest-forwarding call sites consume the same classifier; no second Cargo topology scanner or downstream policy copy is introduced. + +## Invariants + +1. Repository-owned explicit incremental cache paths cannot become unreviewed compiler input. +2. Cargo's ordinary boolean incremental setting is not conflated with a repository-selected arbitrary cache pathname. +3. Browser Session source authority does not claim to attest ambient runner caches or externally restored target directories. +4. Any future exception must identify the cache content immutably and bind it to producer source, exact toolchain, target, compilation options, SBOM/provenance, expiry/invalidation policy, and reproducible fallback before the fail-closed rule is relaxed. + +## Residual evidence boundary + +This source contract does not prove environment/direct-CLI `RUSTFLAGS` or `CARGO_ENCODED_RUSTFLAGS`, `CARGO_INCREMENTAL`, ancestor or `$CARGO_HOME` configuration, externally restored `target/` contents, runner image state, rustup/sysroot state, or remote build-cache integrity. Those remain canonical CI/release supply-chain responsibilities and must not be represented as closed by this repository-only check. + +## Primary references + +Rust Project. (2026). *Codegen options: incremental*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/#incremental + +Rust Project. (2026). *cargo::core::compiler: add_codegen_incremental*. Cargo API documentation. https://doc.rust-lang.org/stable/nightly-rustc/cargo/core/compiler/index.html + +Rust Project. (2026). *CodegenOptions*. rustc_session API documentation, rustc 1.100.0-nightly (923c95cdf, 2026-09-16). https://doc.rust-lang.org/nightly/nightly-rustc/rustc_session/options/struct.CodegenOptions.html + +Rust Project. (2026). *Profiles: incremental*. The Cargo Book. https://doc.rust-lang.org/nightly/cargo/reference/profiles.html#incremental + +Accessed 2026-09-18. From edbd3ee2d1cfca8782c9d22cd4fa556107637adb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:00:44 +0900 Subject: [PATCH 429/632] test(browser-session): expose rustdoc library-path provenance gap --- ...c_library_path_input_authority_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_rustdoc_library_path_input_authority_contract.py diff --git a/tests/test_browser_session_rustdoc_library_path_input_authority_contract.py b/tests/test_browser_session_rustdoc_library_path_input_authority_contract.py new file mode 100644 index 000000000..263080efc --- /dev/null +++ b/tests/test_browser_session_rustdoc_library_path_input_authority_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionRustdocLibraryPathInputAuthorityContractTests(unittest.TestCase): + """Keep rustdoc dependency search paths inside reviewed documentation provenance.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_external_input_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "rustdocflags:external link input"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustdocflags_library_path_fails_closed(self) -> None: + self._assert_external_input_fails_closed( + '[build]\nrustdocflags = ["--library-path", "tools/review-bypass-deps"]\n' + ) + + def test_target_rustdocflags_equals_library_path_fails_closed(self) -> None: + self._assert_external_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"--library-path=tools/review-bypass-deps\"]\n" + ) + + def test_unrelated_rustdocflag_remains_allowed(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["--document-private-items", "--cfg", "docsrs"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From ab1d09f79535babc7e3cac0963eff08397d5bad7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:02:29 +0900 Subject: [PATCH 430/632] test(browser-session): assert rustdoc metadata policy marker --- ...browser_session_rustdoc_doc_meta_input_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py b/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py index bfa8b80c2..ec497c6a7 100644 --- a/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_doc_meta_input_authority_contract.py @@ -39,7 +39,7 @@ def _workspace_with_config(self, config_text: str) -> pathlib.Path: def _assert_documentation_input_fails_closed(self, config_text: str) -> None: root = self._workspace_with_config(config_text) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + with self.assertRaisesRegex(AssertionError, "rustdocflags:documentation input"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_build_rustdocflags_read_doc_meta_dir_fails_closed(self) -> None: From 34826a2a1f15340f33f907069d5b2ae696e47b09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:02:45 +0900 Subject: [PATCH 431/632] test(browser-session): assert rustdoc documentation marker --- ...t_browser_session_rustdoc_render_input_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index 061722dd0..d09f05eb5 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -39,7 +39,7 @@ def _workspace_with_config(self, config_text: str) -> pathlib.Path: def _assert_render_input_fails_closed(self, config_text: str) -> None: root = self._workspace_with_config(config_text) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + with self.assertRaisesRegex(AssertionError, "rustdocflags:documentation input"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_build_rustdocflags_render_file_inputs_fail_closed(self) -> None: From f17413a7db7a9c066d469959bda29aa7e8333626 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:02:58 +0900 Subject: [PATCH 432/632] test(browser-session): assert doctest compiler authority marker --- ...wser_session_rustdoc_doctest_build_arg_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py b/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py index 17a6c2cd6..58597945f 100644 --- a/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py +++ b/tests/test_browser_session_rustdoc_doctest_build_arg_authority_contract.py @@ -40,7 +40,7 @@ class BrowserSessionRustdocDoctestBuildArgAuthorityContractTests(unittest.TestCa def _assert_fails_closed(self, config_text: str) -> None: directory, root = _workspace_with_config(config_text) self.addCleanup(directory.cleanup) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + with self.assertRaisesRegex(AssertionError, "rustdocflags:doctest compiler authority"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_build_rustdocflags_forwarded_sysroot_fails_closed(self) -> None: From fed5dd47d8ed53804144f55a06caf38d28e46380 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:03:12 +0900 Subject: [PATCH 433/632] test(browser-session): assert doctest execution policy marker --- ...wser_session_rustdoc_doctest_execution_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py b/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py index aeb16e037..8f4bb1f7d 100644 --- a/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py +++ b/tests/test_browser_session_rustdoc_doctest_execution_authority_contract.py @@ -40,7 +40,7 @@ class BrowserSessionRustdocDoctestExecutionAuthorityContractTests(unittest.TestC def _assert_fails_closed(self, config_text: str) -> None: directory, root = _workspace_with_config(config_text) self.addCleanup(directory.cleanup) - with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + with self.assertRaisesRegex(AssertionError, "rustdocflags:doctest execution"): authority._assert_no_repository_cargo_compiler_execution_overrides(root) def test_build_rustdocflags_doctest_runtool_fails_closed(self) -> None: From af11b318a4eb64867cc9eabedcf236b62fbac67f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:06:13 +0900 Subject: [PATCH 434/632] fix(browser-session): classify rustdoc library-path inputs --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index afd15bc7b..31c088898 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -58,7 +58,9 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: def _linker_argument_extends_external_inputs(argument: str) -> bool: """Return whether one compiler/linker-driver argument widens external library inputs.""" - if argument in {"-L", "-l"}: + if argument in {"-L", "-l", "--library-path"}: + return True + if argument.startswith("--library-path="): return True if argument.startswith("-L") and len(argument) > 2: return True @@ -701,4 +703,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 40cec6d1ec0ccf445961b28c0acaa5d41d880dc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 22:06:40 +0900 Subject: [PATCH 435/632] docs(browser-session): trace rustdoc library-path provenance repair --- ...ession-rustdoc-external-input-authority.md | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/traceability/browser-session-rustdoc-external-input-authority.md b/docs/traceability/browser-session-rustdoc-external-input-authority.md index d0bb868e6..74110e600 100644 --- a/docs/traceability/browser-session-rustdoc-external-input-authority.md +++ b/docs/traceability/browser-session-rustdoc-external-input-authority.md @@ -4,15 +4,15 @@ Status: Draft contract evidence on PR #317. This document does not claim hosted ## Decision -Git-owned Cargo `rustdocflags` are subject to the same external crate/native-library input provenance gate as Git-owned `rustflags`. Both build-level and matching target-level rustdoc flags fail closed when they introduce `--extern`, `-L`, or `-l` input authority outside the reviewed Cargo production topology. +Git-owned Cargo `rustdocflags` are subject to the same external crate/native-library input provenance gate as Git-owned `rustflags`. Both build-level and matching target-level rustdoc flags fail closed when they introduce `--extern`, `-L` / `--library-path`, or `-l` input authority outside the reviewed Cargo production topology. This is separate from rustdoc executable/linker selection. `build.rustdoc` and rustdoc codegen linker options were already covered; this slice closes the external-input half of the rustdoc boundary. ## Problem -Cargo documents `build.rustdocflags` and matching `target..rustdocflags` / `target..rustdocflags` as extra command-line flags passed to rustdoc. `cargo rustdoc` also documents that rustdoc receives `-L` and `--extern` arguments as part of normal dependency wiring. Therefore a repository-owned rustdoc flag can widen documentation-time crate/native-library inputs even when the production Cargo manifests and the rustdoc executable remain unchanged. +Cargo documents `build.rustdocflags` and matching `target..rustdocflags` / `target..rustdocflags` as extra command-line flags passed to rustdoc. `cargo rustdoc` also documents that rustdoc receives `-L` and `--extern` arguments as part of normal dependency wiring. Rustdoc itself exposes `-L PATH` and its long alias `--library-path PATH` to add dependency search paths. Therefore a repository-owned rustdoc flag can widen documentation-time crate/native-library inputs even when the production Cargo manifests and the rustdoc executable remain unchanged. -The predecessor Browser Session contract classified external inputs for `rustflags` but applied only linker-selection classification to `rustdocflags`. A Git-owned `--extern review_bypass=tools/libreview_bypass.rlib` or `-Lnative=tools/review-bypass` in rustdocflags could therefore bypass the explicit input-provenance marker. +The predecessor Browser Session contract classified external inputs for `rustflags` and later applied that classifier to `rustdocflags`, but the classifier recognized only `-L` and not rustdoc's equivalent `--library-path`. A Git-owned `--library-path tools/review-bypass-deps` or `--library-path=tools/review-bypass-deps` could therefore bypass the explicit `rustdocflags:external link input` marker. ## Constraints @@ -23,11 +23,15 @@ The predecessor Browser Session contract classified external inputs for `rustfla ## RED → repair -- Predecessor exact head: `a3031442e6b3a5e24db53fe53e17724a73c1165b`. -- RED: `5928b1a614c8620203675b609b75f5489338e06d` adds `tests/test_browser_session_rustdoc_external_input_contract.py`. It exercises build-level `--extern`, target-level `-Lnative=...`, and an unrelated-rustdocflags control against the canonical Cargo compiler authority helper. -- Repair: `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b` applies `_flags_extend_external_link_inputs(...)` to both build and target `rustdocflags`, recording `rustdocflags:external link input` without adding another topology/config scanner. +The original rustdoc external-input slice was introduced by RED `5928b1a614c8620203675b609b75f5489338e06d` and repair `e8edb487b779a1c4ff22bf2ca4c62ae7e52abd8b`, which applied the shared external-input classifier to build and target `rustdocflags`. -The repair is source-level contract evidence only until the exact head receives hosted executable repository/security evidence. +Follow-up review found that the long rustdoc alias was still outside that classifier: + +- RED `edbd3ee2d1cfca8782c9d22cd4fa556107637adb` adds `tests/test_browser_session_rustdoc_library_path_input_authority_contract.py` with build-level split `--library-path PATH`, target-level `--library-path=PATH`, and an unrelated-rustdocflag control. The hostile fixtures require the policy-specific `rustdocflags:external link input` marker. +- Repair `af11b318a4eb64867cc9eabedcf236b62fbac67f` extends the existing shared external-library-input classifier with `--library-path` split/equal forms. No new Cargo topology/config scanner is introduced. +- Supplemental rustdoc metadata, render-input, doctest-compiler, and doctest-execution fixtures now assert their policy-specific markers instead of accepting only the common `Cargo .*execution override` prefix. + +These commits are source-level contract evidence only until the exact head receives hosted executable repository/security evidence. ## Residual surfaces @@ -38,3 +42,5 @@ Environment-selected rustdoc flags, direct `cargo rustdoc` trailing arguments, u The Rust Project Developers. (2026). *Configuration*. *The Cargo Book*. https://doc.rust-lang.org/cargo/reference/config.html The Rust Project Developers. (2026). *cargo rustdoc*. *The Cargo Book*. https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html + +The Rust Project Developers. (2026). *Command-line arguments*. *The rustdoc book*. https://doc.rust-lang.org/rustdoc/command-line-arguments.html From cdec934c8d7fb8e14eb17cb0885bf1ba9d6fa277 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:02:02 +0900 Subject: [PATCH 436/632] test(browser-session): fail closed on ambient host CPU codegen --- ...ion_host_cpu_codegen_authority_contract.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/test_browser_session_host_cpu_codegen_authority_contract.py diff --git a/tests/test_browser_session_host_cpu_codegen_authority_contract.py b/tests/test_browser_session_host_cpu_codegen_authority_contract.py new file mode 100644 index 000000000..893e5fc34 --- /dev/null +++ b/tests/test_browser_session_host_cpu_codegen_authority_contract.py @@ -0,0 +1,91 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionHostCpuCodegenAuthorityContractTests(unittest.TestCase): + """Keep repository-selected host-CPU-dependent code generation out of reproducible builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_target_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "target-cpu=native"]\n', + "rustflags:ambient host cpu", + ) + + def test_target_rustflags_compact_target_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-Ctarget-cpu=native\"]\n", + "rustflags:ambient host cpu", + ) + + def test_profile_rustflags_target_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + '[unstable]\nprofile-rustflags = true\n\n[profile.release]\nrustflags = ["--codegen=target-cpu=native"]\n', + "profile.release.rustflags:ambient host cpu", + ) + + def test_build_rustdocflags_target_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--codegen=target-cpu=native"]\n', + "rustdocflags:ambient host cpu", + ) + + def test_doctest_build_arg_target_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=target-cpu=native"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_build_rustflags_tune_cpu_native_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-Z", "tune-cpu=native"]\n', + "rustflags:ambient host cpu", + ) + + def test_explicit_target_cpu_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "target-cpu=x86-64-v3"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 3dc8702b74f845750cee88e1a34ca27cbbd4d7d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:05:38 +0900 Subject: [PATCH 437/632] fix(browser-session): reject ambient host CPU codegen --- ...ssion_cargo_compiler_authority_contract.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 31c088898..3d337da8d 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -186,6 +186,27 @@ def _flag_arguments(value: object) -> list[str]: return [] +def _flags_select_ambient_host_cpu(value: object) -> bool: + """Return whether Git-owned flags make code generation depend on the runner CPU.""" + arguments = _flag_arguments(value) + for index, argument in enumerate(arguments): + option: str | None = None + if argument in {"-C", "--codegen"} and index + 1 < len(arguments): + option = arguments[index + 1] + elif argument.startswith("-C") and len(argument) > 2: + option = argument[2:] + elif argument.startswith("--codegen="): + option = argument.removeprefix("--codegen=") + if option == "target-cpu=native": + return True + if argument == "-Z" and index + 1 < len(arguments): + if arguments[index + 1] == "tune-cpu=native": + return True + if argument == "-Ztune-cpu=native": + return True + return False + + def _flags_extend_external_link_inputs(value: object) -> bool: """Return whether Git-owned Rust flags widen external compiler/documentation inputs.""" arguments = _flag_arguments(value) @@ -314,6 +335,7 @@ def _flags_select_rustdoc_doctest_compiler_authority(value: object) -> bool: _flags_select_codegen_backend(forwarded) or _flags_select_linker(forwarded) or _flags_extend_external_link_inputs(forwarded) + or _flags_select_ambient_host_cpu(forwarded) ) @@ -360,6 +382,8 @@ def _configured_profile_rustflag_authority(value: object, prefix: str = "profile configured.append(f"{path}:codegen linker") if _flags_extend_external_link_inputs(setting): configured.append(f"{path}:external compiler input") + if _flags_select_ambient_host_cpu(setting): + configured.append(f"{path}:ambient host cpu") continue if isinstance(setting, dict): configured.extend(_configured_profile_rustflag_authority(setting, path)) @@ -442,12 +466,16 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) build_configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustflags")): build_configured.append("rustflags:external link input") + if _flags_select_ambient_host_cpu(build.get("rustflags")): + build_configured.append("rustflags:ambient host cpu") if _flags_select_codegen_backend(build.get("rustdocflags")): build_configured.append("rustdocflags:codegen backend") if _flags_select_linker(build.get("rustdocflags")): build_configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(build.get("rustdocflags")): build_configured.append("rustdocflags:external link input") + if _flags_select_ambient_host_cpu(build.get("rustdocflags")): + build_configured.append("rustdocflags:ambient host cpu") if _flags_select_rustdoc_test_execution(build.get("rustdocflags")): build_configured.append("rustdocflags:doctest execution") if _flags_select_rustdoc_documentation_input(build.get("rustdocflags")): @@ -474,12 +502,16 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) configured.append("rustflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustflags")): configured.append("rustflags:external link input") + if _flags_select_ambient_host_cpu(settings.get("rustflags")): + configured.append("rustflags:ambient host cpu") if _flags_select_codegen_backend(settings.get("rustdocflags")): configured.append("rustdocflags:codegen backend") if _flags_select_linker(settings.get("rustdocflags")): configured.append("rustdocflags:codegen linker") if _flags_extend_external_link_inputs(settings.get("rustdocflags")): configured.append("rustdocflags:external link input") + if _flags_select_ambient_host_cpu(settings.get("rustdocflags")): + configured.append("rustdocflags:ambient host cpu") if _flags_select_rustdoc_test_execution(settings.get("rustdocflags")): configured.append("rustdocflags:doctest execution") if _flags_select_rustdoc_documentation_input(settings.get("rustdocflags")): @@ -703,4 +735,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 0eaa8e79b523fcee690bd7de580258ed1b9eb985 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:06:24 +0900 Subject: [PATCH 438/632] docs(browser-session): trace host CPU codegen authority --- ...wser-session-host-cpu-codegen-authority.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/traceability/browser-session-host-cpu-codegen-authority.md diff --git a/docs/traceability/browser-session-host-cpu-codegen-authority.md b/docs/traceability/browser-session-host-cpu-codegen-authority.md new file mode 100644 index 000000000..6d7657b10 --- /dev/null +++ b/docs/traceability/browser-session-host-cpu-codegen-authority.md @@ -0,0 +1,58 @@ +# Browser Session host-CPU codegen authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim hosted executable, repository/security, coverage, release, or runtime GREEN. + +## Problem + +Rust documents `-C target-cpu=` as the compiler control that selects the processor for generated code. The special value `native` means the processor of the host machine. Rust also documents unstable `-Z tune-cpu=` as using the same CPU value set for instruction scheduling; `native` therefore makes the result depend on the machine that happened to run the compiler. + +A repository-owned Cargo configuration can place those options in build/target `rustflags`, profile `rustflags`, build/target `rustdocflags`, or rustdoc `--doctest-build-arg`. In that form the reviewed Git tree no longer determines the code-generation CPU by itself. Two otherwise identical builds may select different instruction sets or scheduling according to runner hardware, which is incompatible with the release contract's reproducibility and exact-provenance requirements. + +The risk is narrower than `target-cpu` in general. An explicit CPU such as `x86-64-v3` is repository-visible and deterministic at this boundary; it still requires normal target/runtime compatibility evidence, but it is not an ambient host selector. This contract therefore fails closed only on `target-cpu=native` and `tune-cpu=native` in Git-owned Cargo flag surfaces. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/rustdoc/toolchain execution and input authority. +- This contract does not ban explicit fixed `target-cpu` values or ordinary codegen options solely because they tune code generation. +- Ambient `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `RUSTDOCFLAGS`, direct `cargo rustc`/`cargo rustdoc` flags, runner CPU selection, virtualization, and externally selected build images remain CI/release supply-chain evidence rather than leaf repository configuration authority. +- A future exception for host-derived codegen must not be represented as a pathname or string allowlist. It needs an explicit build-hardware identity, target compatibility decision, artifact provenance/SBOM linkage, reproducibility policy, and rollback story. + +## RED + +Commit `cdec934c8d7fb8e14eb17cb0885bf1ba9d6fa277` adds hostile fixtures covering: + +- build `rustflags = ["-C", "target-cpu=native"]`; +- target compact `-Ctarget-cpu=native`; +- profile `--codegen=target-cpu=native`; +- build `rustdocflags` selecting `target-cpu=native`; +- rustdoc `--doctest-build-arg` forwarding `-C target-cpu=native`; +- unstable split `-Z tune-cpu=native`; +- an explicit `target-cpu=x86-64-v3` control that must remain allowed. + +The predecessor classifier had no ambient-host CPU check, so these hostile configurations were not represented by the canonical Cargo authority contract. Because the PR is Draft and no exact-head hosted run is available, this is source-semantic RED evidence rather than an executed hosted RED claim. + +## Decision and repair + +Commit `3dc8702b74f845750cee88e1a34ca27cbbd4d7d2` adds `_flags_select_ambient_host_cpu()` to the existing canonical Cargo compiler-authority contract. It recognizes split, compact, and long codegen forms of `target-cpu=native`, plus split/compact `-Z tune-cpu=native`, without creating a second Cargo configuration scanner. + +The existing build, target, profile-rustflags, rustdocflags, and rustdoc doctest-forwarding paths now reuse that classifier. Policy-specific evidence is emitted as `rustflags:ambient host cpu`, `rustdocflags:ambient host cpu`, or the profile path ending in `:ambient host cpu`; doctest forwarding remains owned by the existing `rustdocflags:doctest compiler authority` marker. + +The fixed-CPU control stays accepted. That preserves intentional cross-build optimization choices while removing the runner-hardware dependency from Git-owned build configuration. + +## Security and commercial effect + +- Exact source review can no longer silently become host-CPU-dependent through repository Cargo flags. +- Reproducibility and artifact-attestation claims cannot be satisfied by two builds that happen to compile for different host CPUs under the same reviewed tree. +- The repair does not claim that CI runner hardware is already attested. It keeps that residual in the CI/release owner where image, runner, CPU, and final artifact provenance can be bound together. +- No WebDriver BiDi/browser domain truth, adapter tuple, navigation authority, recovery state, or protocol identifier moves out of OriginWeave's existing bounded contexts. + +## Evidence status + +The RED and repair are present on the PR #317 lineage. Fresh hosted executable evidence, exact-head full review, Rust/docstring/test/edge coverage, parent #229 CodeQL closure, non-force parent reconciliation, and immutable release evidence remain independent gates. + +## References + +The Rust Project Developers. (2026). *Codegen options: target-cpu*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/#target-cpu + +The Rust Project Developers. (2026). *Codegen options: tune-cpu*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/#tune-cpu From a59f8727061aa1cd5e6d136f61f6e8d969164d8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:28:26 +0900 Subject: [PATCH 439/632] test(browser-session): expose LLD error-handler execution authority gap --- ...on_lld_error_handler_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_lld_error_handler_authority_contract.py diff --git a/tests/test_browser_session_lld_error_handler_authority_contract.py b/tests/test_browser_session_lld_error_handler_authority_contract.py new file mode 100644 index 000000000..658d9d0ab --- /dev/null +++ b/tests/test_browser_session_lld_error_handler_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLldErrorHandlerAuthorityContractTests(unittest.TestCase): + """Keep linker-selected error-handler executables outside reviewed Browser Session builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_lld_error_handler_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--error-handling-script=tools/review-bypass-handler"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_split_lld_error_handler_script_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,--error-handling-script,tools/review-bypass-handler\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_lld_error_handler_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--error-handling-script=tools/review-bypass-handler"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_lld_error_handler_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--error-handling-script=tools/review-bypass-handler"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_executable_linker_policy_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From df9c0257982ed136403ce8c3b36999563209ebb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:29:40 +0900 Subject: [PATCH 440/632] fix(browser-session): reject LLD error-handler execution authority --- ...t_browser_session_cargo_compiler_authority_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 3d337da8d..4c8eb3165 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -85,6 +85,13 @@ def _linker_option_loads_plugin(argument: str) -> bool: return argument.startswith(("-plugin=", "--plugin=")) +def _linker_option_selects_error_handler(argument: str) -> bool: + """Return whether one LLD option selects an executable error-handler script.""" + return argument == "--error-handling-script" or argument.startswith( + "--error-handling-script=" + ) + + def _linker_option_selects_script(argument: str) -> bool: """Return whether one linker option selects a script that can introduce link inputs.""" if argument in LINKER_SCRIPT_OPTIONS: @@ -127,6 +134,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ continue if ( _linker_option_loads_plugin(argument) + or _linker_option_selects_error_handler(argument) or _linker_option_selects_script(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) From ccf217f3cca605da645e10185d842eb43b2e6935 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:30:03 +0900 Subject: [PATCH 441/632] docs(traceability): record LLD error-handler execution boundary --- ...ser-session-lld-error-handler-authority.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/traceability/browser-session-lld-error-handler-authority.md diff --git a/docs/traceability/browser-session-lld-error-handler-authority.md b/docs/traceability/browser-session-lld-error-handler-authority.md new file mode 100644 index 000000000..196d295e7 --- /dev/null +++ b/docs/traceability/browser-session-lld-error-handler-authority.md @@ -0,0 +1,52 @@ +# Browser Session LLD error-handler execution authority + +## Problem + +Browser Session's Cargo/rustc provenance contract already rejects linker replacement, linker plugins, linker scripts, response files, native positional inputs, and external library search inputs. It did not classify LLVM LLD's `--error-handling-script=` option as linker execution authority. + +LLD documents `--error-handling-script=` as a user-provided executable that is invoked from linker error handling. The script may be resolved through `PATH` or supplied as a full path, must be executable, and runs in the same environment as the parent linker process. A repository-owned Cargo `rustflags` or `rustdocflags` value can forward this option through rustc's `-C link-arg` / `-C link-args` path. That creates a code-execution edge outside the reviewed Browser Session source and dependency closure even when the selected linker binary itself is unchanged. + +This matters operationally because the handler is conditional: a normal link can appear inert while a missing library or undefined symbol causes the linker to execute the selected program. Command acknowledgement or a successful configuration parse therefore cannot be treated as evidence that the build TCB stayed unchanged. + +## Constraint and owner boundary + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo/rustc/rustdoc/linker execution and input authority. `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. No linker-policy scanner is duplicated into downstream Browser Session, Navigation, WebDriver BiDi, EgressWeave, Wardnet, or contextual-orchestrator owners. + +Ambient `RUSTFLAGS`, `RUSTDOCFLAGS`, direct CLI options, runner `PATH`, the concrete linker distribution/version, and externally restored toolchain/cache state remain CI/release supply-chain evidence surfaces rather than leaf-source authority. + +## Alternatives considered + +Allowing repository-relative error-handler paths was rejected. Relative path containment says nothing about executable identity after symlink resolution, producer provenance, permissions, mutation between review and execution, or the environment inherited by the process. + +Allowing the option only when the file is tracked by Git was rejected. Git tracking alone does not bind the executable artifact, interpreter, transitive runtime, or runner environment used at link time. + +Blocking all linker policy options was rejected because modeled non-executable policy such as `--as-needed` does not itself select another executable or external input and remains useful without widening the build TCB. + +The selected rule is narrow: fail closed only when forwarded linker arguments select LLD's error-handler executable surface. + +## RED → repair + +Structural RED `a59f8727061aa1cd5e6d136f61f6e8d969164d8b` adds realistic Cargo fixtures for build `rustflags`, target `rustflags`, build `rustdocflags`, and rustdoc doctest forwarding. It covers both `--error-handling-script=` and split `--error-handling-script,` forms behind `-Wl,`. `--as-needed` remains an allowed control. + +Minimal repair `df9c0257982ed136403ce8c3b36999563209ebb0` adds `_linker_option_selects_error_handler()` to the existing direct-linker classifier and reuses the existing build, target, profile, rustdoc, doctest, `-Wl,`, `--for-linker=`, and `-Xlinker` paths. No Cargo topology/config scanner was added. + +## Security and release consequence + +A future exception requires evidence stronger than a path allowlist: + +- immutable executable identity and digest; +- exact LLD/toolchain and runner identity; +- interpreter and transitive runtime provenance when the handler is a script; +- purpose and trigger conditions for each supported error tag; +- environment and secret-exposure analysis; +- SBOM/provenance linkage to the consuming binary; +- reproducible no-handler reference build where applicable; +- rollback and expiry/removal conditions. + +Until such a contract exists, repository-selected LLD error handlers fail closed. + +## Primary source + +LLVM Project. (2026). *Error Handling Script — lld 24.0.0git documentation*. https://lld.llvm.org/error_handling_script.html + +The documentation states that LLD executes the user-provided error-handling script in the same environment as the parent process and currently defines `missing-lib` and `undefined-symbol` trigger tags. From e86b8f52099a0e04ebf53595f7f2110cfd033fb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:34:44 +0900 Subject: [PATCH 442/632] test(browser-session): expose LLD DTLTO executable authority gap --- ...ssion_dtlt_execution_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_dtlt_execution_authority_contract.py diff --git a/tests/test_browser_session_dtlt_execution_authority_contract.py b/tests/test_browser_session_dtlt_execution_authority_contract.py new file mode 100644 index 000000000..f1303743b --- /dev/null +++ b/tests/test_browser_session_dtlt_execution_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionDistributedThinLtoExecutionAuthorityContractTests(unittest.TestCase): + """Keep LLD DTLTO distributor/compiler executables outside reviewed Browser Session builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_dtlt_distributor_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_dtlt_remote_compiler_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_dtlt_distributor_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_dtlt_remote_compiler_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_executable_thinlto_job_count_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-jobs=2"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From de49c23712ee1defdb73ca440304ede2c5ddf413 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:36:24 +0900 Subject: [PATCH 443/632] fix(browser-session): reject LLD DTLTO executable authority --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 4c8eb3165..089381fe0 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -92,6 +92,11 @@ def _linker_option_selects_error_handler(argument: str) -> bool: ) +def _linker_option_selects_dtlt_executable(argument: str) -> bool: + """Return whether one LLD DTLTO option selects a distributor or remote compiler executable.""" + return argument.startswith(("--thinlto-distributor=", "--thinlto-remote-compiler=")) + + def _linker_option_selects_script(argument: str) -> bool: """Return whether one linker option selects a script that can introduce link inputs.""" if argument in LINKER_SCRIPT_OPTIONS: @@ -135,6 +140,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ if ( _linker_option_loads_plugin(argument) or _linker_option_selects_error_handler(argument) + or _linker_option_selects_dtlt_executable(argument) or _linker_option_selects_script(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) From aaa046664bcb78578e0ae0e5c862effcc2b57194 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:36:43 +0900 Subject: [PATCH 444/632] docs(traceability): record LLD DTLTO execution boundary --- ...r-session-lld-dtlto-execution-authority.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/traceability/browser-session-lld-dtlto-execution-authority.md diff --git a/docs/traceability/browser-session-lld-dtlto-execution-authority.md b/docs/traceability/browser-session-lld-dtlto-execution-authority.md new file mode 100644 index 000000000..4d8f0f19c --- /dev/null +++ b/docs/traceability/browser-session-lld-dtlto-execution-authority.md @@ -0,0 +1,54 @@ +# Browser Session LLD Distributed ThinLTO execution authority + +## Problem + +Browser Session's Cargo/rustc provenance contract already fails closed on repository-selected linker replacement, linker plugins, linker scripts, response files, error-handler executables, native positional inputs, and external library search inputs. LLVM LLD's Distributed ThinLTO interface adds two more executable-selection surfaces that were not modeled explicitly: + +- `--thinlto-distributor=` selects the file LLD executes as the distributor process. +- `--thinlto-remote-compiler=` selects the compiler that the distributor process invokes for remote backend compilations. + +LLD documents DTLTO as distributing ThinLTO backend compilations through an external distribution system during the traditional link step. The remote compiler must match the LLD version. Repository-owned Cargo `rustflags`, `rustdocflags`, profile rustflags, or rustdoc doctest forwarding can pass these options through rustc `-C link-arg` / `-C link-args`, so the effective build TCB can gain distributor/compiler executables without changing Cargo package topology or the selected linker binary. + +## Constraint and owner boundary + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo/rustc/rustdoc/linker execution and input authority. `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. No DTLTO scanner is copied into downstream Navigation, WebDriver BiDi, EgressWeave, Wardnet, contextual-orchestrator, or other canonical owners. + +Ambient `RUSTFLAGS`/`RUSTDOCFLAGS`, direct CLI options, LLD version/distribution, runner `PATH`, external distributor configuration, remote-worker images/toolchains, and restored caches remain CI/release supply-chain evidence surfaces rather than leaf-source authority. + +## Alternatives considered + +Allowing repository-relative distributor/compiler paths was rejected. Relative containment does not prove executable identity after symlink resolution, mutation between review and execution, the distributor's transitive runtime, or the remote worker/toolchain identity. + +Allowing only Git-tracked executables was rejected. Git tracking does not bind the actual interpreter/binary, remote execution system, remote compiler version, environment, or worker artifact set used at link time. + +Blocking all ThinLTO options was rejected. Numeric/policy controls such as `--thinlto-jobs=` do not themselves select a new executable and need not widen the execution TCB. + +The selected rule is narrow: fail closed when forwarded direct-linker arguments select the DTLTO distributor or remote compiler executable paths. + +## RED → repair + +Structural RED `e86b8f52099a0e04ebf53595f7f2110cfd033fb6` adds realistic Cargo fixtures for build `rustflags`, target `rustflags`, build `rustdocflags`, and rustdoc doctest forwarding. It covers both documented executable selectors while preserving `--thinlto-jobs=2` as an allowed non-executable control. + +Minimal repair `de49c23712ee1defdb73ca440304ede2c5ddf413` adds `_linker_option_selects_dtlt_executable()` to the existing direct-linker classifier. The existing `-Wl,`, `--for-linker=`, `-Xlinker`, build, target, profile, rustdoc, and doctest-forwarding paths consume the same classifier; no second Cargo topology/config scanner was introduced. + +## Security and release consequence + +A future DTLTO exception requires evidence stronger than a path allowlist: + +- immutable distributor and remote-compiler artifact identities and digests; +- exact LLD/LLVM/compiler version compatibility; +- remote worker image/runtime identity and isolation boundary; +- distributor arguments and transitive executable/tool inputs; +- input/output transfer semantics and integrity checks for remote compilation; +- environment/credential exposure analysis; +- SBOM and provenance linking each remote backend result to the consuming binary; +- deterministic or independently reproducible reference evidence where applicable; +- failure recovery, rollback, cache invalidation, and expiry/removal conditions. + +Until that contract exists, repository-selected DTLTO distributor and remote-compiler executable paths fail closed. + +## Primary source + +LLVM Project. (2026). *Integrated Distributed ThinLTO (DTLTO) — lld 24.0.0git documentation*. https://lld.llvm.org/DTLTO.html + +The documentation states that `--thinlto-distributor=` specifies the file to execute as the distributor process and `--thinlto-remote-compiler=` specifies the compiler the distributor invokes; the compiler must match the LLD version. It also warns that options introducing extra input/output files can cause miscompilation if the distribution system does not correctly transfer them. From d4df3c86079297018387a2be17685a0fe5f67923 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:37:32 +0900 Subject: [PATCH 445/632] test(browser-session): use canonical DTLTO contract naming --- ...sion_dtlto_execution_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_dtlto_execution_authority_contract.py diff --git a/tests/test_browser_session_dtlto_execution_authority_contract.py b/tests/test_browser_session_dtlto_execution_authority_contract.py new file mode 100644 index 000000000..6a5eff800 --- /dev/null +++ b/tests/test_browser_session_dtlto_execution_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionDistributedThinLtoExecutionAuthorityContractTests(unittest.TestCase): + """Keep LLD DTLTO distributor/compiler executables outside reviewed Browser Session builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_dtlto_distributor_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_dtlto_remote_compiler_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_dtlto_distributor_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_dtlto_remote_compiler_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_executable_thinlto_job_count_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-jobs=2"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 1ce0ebaca2c22d9a2eb9abccd48a8e23909ba5cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:37:38 +0900 Subject: [PATCH 446/632] test(browser-session): remove misspelled DTLTO fixture path --- ...ssion_dtlt_execution_authority_contract.py | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/test_browser_session_dtlt_execution_authority_contract.py diff --git a/tests/test_browser_session_dtlt_execution_authority_contract.py b/tests/test_browser_session_dtlt_execution_authority_contract.py deleted file mode 100644 index f1303743b..000000000 --- a/tests/test_browser_session_dtlt_execution_authority_contract.py +++ /dev/null @@ -1,79 +0,0 @@ -import importlib.util -import pathlib -import tempfile -import unittest - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" - -spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) -if spec is None or spec.loader is None: - raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") -authority = importlib.util.module_from_spec(spec) -spec.loader.exec_module(authority) - - -def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: - directory = tempfile.TemporaryDirectory() - root = pathlib.Path(directory.name) - (root / "Cargo.toml").write_text( - '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', - encoding="utf-8", - ) - adapter = root / "adapter" - (adapter / "src").mkdir(parents=True) - (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") - (adapter / "Cargo.toml").write_text( - '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', - encoding="utf-8", - ) - cargo = root / ".cargo" - cargo.mkdir() - (cargo / "config.toml").write_text(config_text, encoding="utf-8") - return directory, root - - -class BrowserSessionDistributedThinLtoExecutionAuthorityContractTests(unittest.TestCase): - """Keep LLD DTLTO distributor/compiler executables outside reviewed Browser Session builds.""" - - def _assert_fails_closed(self, config_text: str, marker: str) -> None: - directory, root = _workspace_with_config(config_text) - self.addCleanup(directory.cleanup) - with self.assertRaisesRegex(AssertionError, marker): - authority._assert_no_repository_cargo_compiler_execution_overrides(root) - - def test_build_rustflags_dtlt_distributor_fails_closed(self) -> None: - self._assert_fails_closed( - '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', - "rustflags:codegen linker", - ) - - def test_target_rustflags_dtlt_remote_compiler_fails_closed(self) -> None: - self._assert_fails_closed( - "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler\"]\n", - "rustflags:codegen linker", - ) - - def test_build_rustdocflags_dtlt_distributor_fails_closed(self) -> None: - self._assert_fails_closed( - '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--thinlto-distributor=tools/review-bypass-distributor"]\n', - "rustdocflags:codegen linker", - ) - - def test_doctest_forwarded_dtlt_remote_compiler_fails_closed(self) -> None: - self._assert_fails_closed( - '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--thinlto-remote-compiler=tools/review-bypass-compiler"]\n', - "rustdocflags:doctest compiler authority", - ) - - def test_non_executable_thinlto_job_count_remains_allowed(self) -> None: - directory, root = _workspace_with_config( - '[build]\nrustflags = ["-C", "link-arg=-Wl,--thinlto-jobs=2"]\n' - ) - self.addCleanup(directory.cleanup) - authority._assert_no_repository_cargo_compiler_execution_overrides(root) - - -if __name__ == "__main__": - unittest.main() From 69ccddcc77195fc9e66edf93a82eaaf90735e05f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:38:57 +0900 Subject: [PATCH 447/632] refactor(browser-session): use canonical DTLTO helper naming --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 089381fe0..7c5eda257 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -92,7 +92,7 @@ def _linker_option_selects_error_handler(argument: str) -> bool: ) -def _linker_option_selects_dtlt_executable(argument: str) -> bool: +def _linker_option_selects_dtlto_executable(argument: str) -> bool: """Return whether one LLD DTLTO option selects a distributor or remote compiler executable.""" return argument.startswith(("--thinlto-distributor=", "--thinlto-remote-compiler=")) @@ -140,7 +140,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ if ( _linker_option_loads_plugin(argument) or _linker_option_selects_error_handler(argument) - or _linker_option_selects_dtlt_executable(argument) + or _linker_option_selects_dtlto_executable(argument) or _linker_option_selects_script(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) From e1ef610f7d07eea3401c5473274ae913e927f9bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:39:14 +0900 Subject: [PATCH 448/632] docs(traceability): normalize DTLTO repair lineage --- .../browser-session-lld-dtlto-execution-authority.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-lld-dtlto-execution-authority.md b/docs/traceability/browser-session-lld-dtlto-execution-authority.md index 4d8f0f19c..00f8b6056 100644 --- a/docs/traceability/browser-session-lld-dtlto-execution-authority.md +++ b/docs/traceability/browser-session-lld-dtlto-execution-authority.md @@ -29,7 +29,9 @@ The selected rule is narrow: fail closed when forwarded direct-linker arguments Structural RED `e86b8f52099a0e04ebf53595f7f2110cfd033fb6` adds realistic Cargo fixtures for build `rustflags`, target `rustflags`, build `rustdocflags`, and rustdoc doctest forwarding. It covers both documented executable selectors while preserving `--thinlto-jobs=2` as an allowed non-executable control. -Minimal repair `de49c23712ee1defdb73ca440304ede2c5ddf413` adds `_linker_option_selects_dtlt_executable()` to the existing direct-linker classifier. The existing `-Wl,`, `--for-linker=`, `-Xlinker`, build, target, profile, rustdoc, and doctest-forwarding paths consume the same classifier; no second Cargo topology/config scanner was introduced. +Minimal repair `de49c23712ee1defdb73ca440304ede2c5ddf413` adds the DTLTO executable selector to the existing direct-linker classifier. The existing `-Wl,`, `--for-linker=`, `-Xlinker`, build, target, profile, rustdoc, and doctest-forwarding paths consume the same classifier; no second Cargo topology/config scanner was introduced. + +The initial fixture path/helper spelling used `dtlt`. Naming-only successors `d4df3c86079297018387a2be17685a0fe5f67923`, `1ce0ebaca2c22d9a2eb9abccd48a8e23909ba5cb`, and `69ccddcc77195fc9e66edf93a82eaaf90735e05f` normalize the test path, test method names, and shared helper to the canonical `DTLTO` acronym without changing policy semantics. ## Security and release consequence From 1af97ad213eb314c408dd5b0b20b87b044822e72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:02:52 +0900 Subject: [PATCH 449/632] test(browser-session): expose linker sysroot input authority --- ...linker_sysroot_input_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_sysroot_input_authority_contract.py diff --git a/tests/test_browser_session_linker_sysroot_input_authority_contract.py b/tests/test_browser_session_linker_sysroot_input_authority_contract.py new file mode 100644 index 000000000..93b907c0c --- /dev/null +++ b/tests/test_browser_session_linker_sysroot_input_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerSysrootInputAuthorityContractTests(unittest.TestCase): + """Keep linker sysroot selection outside repository-owned Browser Session builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_gnu_linker_sysroot_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--sysroot=tools/review-bypass-sysroot"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_for_linker_sysroot_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--sysroot=tools/review-bypass-sysroot\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_xlinker_sysroot_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-args=-Xlinker --sysroot=tools/review-bypass-sysroot"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_linker_sysroot_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--sysroot=tools/review-bypass-sysroot"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_input_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 372f319a4c68669a29c10e56e7e726d469e6c318 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:05:07 +0900 Subject: [PATCH 450/632] fix(browser-session): fail closed on linker sysroot inputs --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 7c5eda257..a41280228 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -58,6 +58,8 @@ def _linker_driver_argument_selects_executable(argument: str) -> bool: def _linker_argument_extends_external_inputs(argument: str) -> bool: """Return whether one compiler/linker-driver argument widens external library inputs.""" + if argument == "--sysroot" or argument.startswith("--sysroot="): + return True if argument in {"-L", "-l", "--library-path"}: return True if argument.startswith("--library-path="): From f38bbbc7c809f903b3068cb914e35546405605dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:05:49 +0900 Subject: [PATCH 451/632] docs(traceability): record linker sysroot input authority --- ...-session-linker-sysroot-input-authority.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/traceability/browser-session-linker-sysroot-input-authority.md diff --git a/docs/traceability/browser-session-linker-sysroot-input-authority.md b/docs/traceability/browser-session-linker-sysroot-input-authority.md new file mode 100644 index 000000000..6418cff46 --- /dev/null +++ b/docs/traceability/browser-session-linker-sysroot-input-authority.md @@ -0,0 +1,49 @@ +# Browser Session linker sysroot input authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim hosted repository/security GREEN. + +## Problem + +The Browser Session Cargo compiler-authority contract already failed closed when Git-owned `rustflags` or `rustdocflags` selected a Rust compiler/rustdoc sysroot with `--sysroot`. A distinct linker surface remained. Rust codegen flags can forward direct linker arguments through `-Wl,`, `--for-linker=`, or `-Xlinker`; the shared direct-linker classifier did not classify GNU/LLD `--sysroot=` as external linker input authority. + +GNU `ld` documents `--sysroot=directory` as replacing the linker's configured sysroot location. LLD documents its ELF linker as a GNU-linker-compatible replacement accepting GNU command-line arguments. A repository-owned forwarded linker sysroot can therefore change where the linker resolves system libraries and related link inputs without changing the reviewed Cargo package/source closure. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo-selected compiler, rustdoc, and linker execution/input authority. +- The fix must cover build/target `rustflags`, build/target `rustdocflags`, and rustdoc doctest compiler forwarding through the existing parser; no second Cargo topology or linker-authority scanner is introduced. +- Unrelated linker hardening remains permitted. In particular, `-Wl,-z,relro` does not select an external input and remains an allowed control. +- Environment-selected flags, direct rustc/rustdoc/linker CLI invocation, runner/toolchain contents, and the identity of default system sysroots remain CI/release supply-chain evidence surfaces. + +## RED + +Commit `1af97ad213eb314c408dd5b0b20b87b044822e72` adds `tests/test_browser_session_linker_sysroot_input_authority_contract.py` with hostile fixtures for: + +- build `rustflags` forwarding `-Wl,--sysroot=...`; +- target `rustflags` forwarding `--for-linker=--sysroot=...`; +- build `rustdocflags` forwarding the equals form through `-Xlinker`; +- rustdoc `--doctest-build-arg` forwarding a linker sysroot; +- an allowed `-Wl,-z,relro` control. + +At the parent exact head, `_direct_linker_arguments_extend_authority()` classified plugins, executable error handlers, DTLTO executable selectors, linker scripts, response files, native library selectors, and positional native inputs, but `--sysroot=` matched none of those branches. The new hostile cases therefore preserve a concrete source-semantic gap rather than broadening policy by assertion. + +## Decision and repair + +Commit `372f319a4c68669a29c10e56e7e726d469e6c318` extends the existing `_linker_argument_extends_external_inputs()` classifier so split or equals `--sysroot` is treated as external linker input authority. The repair is two lines in the canonical owner. Existing `-Wl,`, `--for-linker=`, `-Xlinker`, build, target, rustdoc, profile, and doctest-forwarding paths consume that same classifier. + +The earlier rustc/rustdoc `--sysroot` check remains valid and intentionally redundant at its more specific compiler boundary. The added linker-level classification covers the same spelling only after it has been forwarded into linker authority. + +A path allowlist was rejected. A path string does not prove immutable sysroot contents, system-library identity, symlink containment, producer toolchain, SBOM/provenance, or reproducibility. Any future approved linker sysroot requires versioned immutable artifact identity and release evidence rather than pathname trust. + +## Security and release consequence + +This closes the modeled Git-owned Cargo path in which reviewed Rust source/dependency topology remained unchanged while the linker resolved inputs under a repository-selected sysroot. It does not prove the integrity of the ambient/default linker sysroot, runner image, direct CLI flags, environment variables, or externally restored toolchain state. Those remain release/toolchain provenance requirements and must not be inferred from this source contract. + +Hosted exact-head execution, whole-PR review closure, owned production rustdoc/test/edge coverage, and release acceptance remain separate gates. + +## Primary references + +Free Software Foundation. (2025). *The GNU linker* (GNU Binutils 2.45), `--sysroot=directory`. https://sourceware.org/binutils/docs-2.45/ld.pdf + +LLVM Project. (2026). *LLD - The LLVM linker*. Retrieved September 18, 2026, from https://lld.llvm.org/ From 4b5297e7540d291c9860ef94e35ead995cf68786 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:06:51 +0900 Subject: [PATCH 452/632] docs(changelog): record linker sysroot provenance guard --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ade5dee0..4680d5f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `--sysroot`, preventing an unreviewed linker search root from changing external system-library inputs while the reviewed Cargo package/source closure remains unchanged. - Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, GCC `-specs=` / `-specs `, or GCC `-wrapper`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. - Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. - Failed closed on production Cargo build surfaces (`build.rs`, enabled `[package] build`, top-level `[build-dependencies]`, and target-specific build dependencies) until generated-source provenance is explicitly modeled; `package.build = false` remains the explicit no-build-script form. From 6bd948fd5cfbf71ecebca4e0001fc7cfcffb4da5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:10:13 +0900 Subject: [PATCH 453/632] test(browser-session): expose default linker script authority --- ...inker_default_script_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_default_script_authority_contract.py diff --git a/tests/test_browser_session_linker_default_script_authority_contract.py b/tests/test_browser_session_linker_default_script_authority_contract.py new file mode 100644 index 000000000..f02b2c991 --- /dev/null +++ b/tests/test_browser_session_linker_default_script_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionDefaultLinkerScriptAuthorityContractTests(unittest.TestCase): + """Keep GNU/LLD default linker scripts inside reviewed Browser Session provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_default_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--default-script=tools/review-bypass-default.lds"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_for_linker_default_script_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--default-script=tools/review-bypass-default.lds\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_xlinker_default_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-args=-Xlinker --default-script=tools/review-bypass-default.lds"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_default_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--default-script=tools/review-bypass-default.lds"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_script_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 63f12526044b10ffa049c3fbf9701bc31760d44f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:11:26 +0900 Subject: [PATCH 454/632] fix(browser-session): fail closed on default linker scripts --- ...t_browser_session_cargo_compiler_authority_contract.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index a41280228..507525ea9 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -22,7 +22,7 @@ {"build-std", "build-std-features", "codegen-backend"} ) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) -LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script"}) +LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script", "-dT", "--default-script"}) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} @@ -103,7 +103,11 @@ def _linker_option_selects_script(argument: str) -> bool: """Return whether one linker option selects a script that can introduce link inputs.""" if argument in LINKER_SCRIPT_OPTIONS: return True - return (argument.startswith("-T") and len(argument) > 2) or argument.startswith("--script=") + return ( + (argument.startswith("-T") and len(argument) > 2) + or (argument.startswith("-dT") and len(argument) > 3) + or argument.startswith(("--script=", "--default-script=")) + ) def _linker_option_uses_response_file(argument: str) -> bool: From 32f57db1ce51e931c518667ed44d1a31284777ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:13:13 +0900 Subject: [PATCH 455/632] docs(traceability): record default linker script authority --- ...session-linker-default-script-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-linker-default-script-authority.md diff --git a/docs/traceability/browser-session-linker-default-script-authority.md b/docs/traceability/browser-session-linker-default-script-authority.md new file mode 100644 index 000000000..ed4df42e9 --- /dev/null +++ b/docs/traceability/browser-session-linker-default-script-authority.md @@ -0,0 +1,47 @@ +# Browser Session default linker script authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim hosted repository/security GREEN. + +## Problem + +The Browser Session Cargo compiler-authority contract already fails closed on explicit GNU/LLD linker scripts selected through `-T` / `--script`. A distinct spelling remained unmodeled: GNU `ld` also accepts `-dT scriptfile` / `--default-script=scriptfile`, and LLD documents the same default-script interface. The default script is still an external file that controls linker behavior; GNU differs only in delaying its processing until the rest of the command line has been processed. + +At the predecessor exact head, `_linker_option_selects_script()` recognized `-T`, attached `-T...`, `--script`, and `--script=...`, but did not recognize `-dT` or `--default-script`. Repository-owned `rustflags` or `rustdocflags` could therefore forward an equals-form default script through `-Wl,`, `--for-linker=`, or `-Xlinker` while the reviewed Cargo package/source closure remained unchanged. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler, rustdoc, linker execution, and external input authority. +- The repair must reuse the existing direct-linker parser for build/target/profile `rustflags`, `rustdocflags`, and rustdoc doctest compiler forwarding. A second Cargo or linker scanner is not introduced. +- Unrelated linker hardening remains permitted. `-Wl,-z,relro` is retained as an allowed control because it does not select a script or other external input. +- Environment/direct-CLI linker flags, the ambient linker binary/version, and externally supplied toolchain contents remain CI/release supply-chain evidence surfaces. + +## RED + +Commit `6bd948fd5cfbf71ecebca4e0001fc7cfcffb4da5` adds `tests/test_browser_session_linker_default_script_authority_contract.py`. Hostile fixtures cover: + +- build `rustflags` forwarding `-Wl,--default-script=...`; +- target `rustflags` forwarding `--for-linker=--default-script=...`; +- build `rustdocflags` forwarding the same selector through `-Xlinker`; +- rustdoc `--doctest-build-arg` forwarding a default script; +- an allowed `-Wl,-z,relro` control. + +The predecessor classifier did not match the equals-form `--default-script=...`, so these fixtures preserve an actual source-semantic provenance gap rather than asserting a broad deny rule. + +## Decision and repair + +Commit `63f12526044b10ffa049c3fbf9701bc31760d44f` extends the existing linker-script selector with the documented GNU/LLD spellings `-dT` and `--default-script`. Exact `-dT` / `--default-script`, attached `-dT...`, and equals `--default-script=...` are classified alongside the already reviewed `-T` / `--script` forms. The existing `-Wl,`, `--for-linker=`, `-Xlinker`, build, target, profile, rustdoc, and doctest-forwarding paths therefore inherit the repair without another topology or policy owner. + +The fix intentionally does not path-allowlist linker scripts. A reviewed pathname does not prove immutable file contents, symlink containment, the complete input set introduced by script commands, linker/toolchain identity, or reproducible output. A future exception requires an immutable script digest and artifact identity, containment proof, transitive input provenance, linker/toolchain compatibility, SBOM/attestation evidence, reproducibility, and rollback qualification. + +## Security and release consequence + +A repository-owned default linker script can influence output layout and symbol/input resolution just as an explicit linker script can. This repair closes the modeled Git-owned Cargo path without claiming anything about ambient linker defaults, direct CLI invocation, runner images, or release-time toolchain integrity. + +Hosted exact-head execution, whole-PR independent review closure, owned production rustdoc/test/edge coverage, and immutable release acceptance remain separate gates. + +## Primary references + +Free Software Foundation. (2025). *The GNU linker* (GNU Binutils 2.45), `-dT scriptfile` / `--default-script=scriptfile`. https://sourceware.org/binutils/docs-2.45/ld.pdf + +LLVM Project. (2026). *Linker Script implementation notes and policy*. LLD documentation. Retrieved September 18, 2026, from https://lld.llvm.org/ELF/linker_script.html From da96259200548fa3c540547b00066a2a339d5f27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:14:32 +0900 Subject: [PATCH 456/632] docs(changelog): record default linker script provenance guard --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4680d5f6b..091e8e389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `-dT` / `--default-script`, preventing an unreviewed default linker script from changing link behavior while the reviewed Cargo package/source closure remains unchanged. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `--sysroot`, preventing an unreviewed linker search root from changing external system-library inputs while the reviewed Cargo package/source closure remains unchanged. - Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, GCC `-specs=` / `-specs `, or GCC `-wrapper`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. - Centralized Browser Session production-source provenance in the canonical trusted-adapter scanner so lifecycle-SPI, dependency, lifecycle-binding, and source-containment contracts consume one Cargo source closure; resolved source objects must remain inside the exact-head repository review root and dangling/default-source symlinks fail closed. From 975e35448aafd9aba57935897431841a29101864 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:14:54 +0900 Subject: [PATCH 457/632] test(browser-session): expose MRI linker script authority --- ...on_linker_mri_script_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_mri_script_authority_contract.py diff --git a/tests/test_browser_session_linker_mri_script_authority_contract.py b/tests/test_browser_session_linker_mri_script_authority_contract.py new file mode 100644 index 000000000..5c120e97d --- /dev/null +++ b/tests/test_browser_session_linker_mri_script_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionMriLinkerScriptAuthorityContractTests(unittest.TestCase): + """Keep GNU MRI linker command files inside reviewed Browser Session provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_mri_script_equals_form_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--mri-script=tools/review-bypass-mri.cmd"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_for_linker_mri_script_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--mri-script=tools/review-bypass-mri.cmd\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_xlinker_mri_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-args=-Xlinker --mri-script=tools/review-bypass-mri.cmd"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_mri_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--mri-script=tools/review-bypass-mri.cmd"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_script_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 5a066aacff29534d934f85b203121be5125347c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:16:15 +0900 Subject: [PATCH 458/632] fix(browser-session): fail closed on MRI linker scripts --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 507525ea9..f9638d968 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -22,7 +22,9 @@ {"build-std", "build-std-features", "codegen-backend"} ) LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) -LINKER_SCRIPT_OPTIONS = frozenset({"-T", "--script", "-dT", "--default-script"}) +LINKER_SCRIPT_OPTIONS = frozenset( + {"-T", "--script", "-dT", "--default-script", "-c", "--mri-script"} +) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} @@ -106,7 +108,7 @@ def _linker_option_selects_script(argument: str) -> bool: return ( (argument.startswith("-T") and len(argument) > 2) or (argument.startswith("-dT") and len(argument) > 3) - or argument.startswith(("--script=", "--default-script=")) + or argument.startswith(("--script=", "--default-script=", "--mri-script=")) ) From 693d4a33462418320cc1245af032c15d84a8b3b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:16:32 +0900 Subject: [PATCH 459/632] docs(traceability): record MRI linker script authority --- ...ser-session-linker-mri-script-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-linker-mri-script-authority.md diff --git a/docs/traceability/browser-session-linker-mri-script-authority.md b/docs/traceability/browser-session-linker-mri-script-authority.md new file mode 100644 index 000000000..998f6fe70 --- /dev/null +++ b/docs/traceability/browser-session-linker-mri-script-authority.md @@ -0,0 +1,47 @@ +# Browser Session MRI linker script authority traceability + +Status: Draft contract evidence on PR #317. This document does not claim hosted repository/security GREEN. + +## Problem + +GNU `ld` supports an alternate MRI-compatible linker command language through `-c MRI-commandfile` / `--mri-script=MRI-commandfile`. The command file is an external linker input and, if it is not in the current directory, GNU `ld` searches preceding `-L` directories for it. The Browser Session direct-linker classifier already failed closed on GNU/LLD general-purpose linker scripts and default linker scripts, but the equals-form `--mri-script=...` was not a recognized script selector. + +A repository-owned `rustflags` or `rustdocflags` value could therefore forward `--mri-script=...` through `-Wl,`, `--for-linker=`, or `-Xlinker` while the reviewed Cargo package/source closure remained unchanged. + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler, rustdoc, linker execution, and external input authority. +- The repair must reuse the shared direct-linker parser for build/target/profile `rustflags`, `rustdocflags`, and rustdoc doctest forwarding. +- `-Wl,-z,relro` remains an allowed control because it does not select an external script or command file. +- Ambient linker flags, direct CLI invocation, runner/toolchain contents, and the linker implementation/version remain CI/release provenance surfaces. + +## RED + +Commit `975e35448aafd9aba57935897431841a29101864` adds `tests/test_browser_session_linker_mri_script_authority_contract.py` with hostile fixtures for: + +- build `rustflags` forwarding `-Wl,--mri-script=...`; +- target `rustflags` forwarding `--for-linker=--mri-script=...`; +- build `rustdocflags` forwarding the selector through `-Xlinker`; +- rustdoc `--doctest-build-arg` forwarding an MRI command file; +- an allowed `-Wl,-z,relro` control. + +At the predecessor exact head, `--mri-script=...` was neither a known script selector nor a positional input because it begins with `-`. The equals form therefore preserved a concrete source-semantic bypass. + +## Decision and repair + +Commit `5a066aacff29534d934f85b203121be5125347c4` extends the existing linker-script classifier with the documented GNU spellings `-c` and `--mri-script`, plus `--mri-script=...`. This is a shared-owner repair: `-Wl,`, `--for-linker=`, `-Xlinker`, build/target/profile flags, rustdoc, and doctest forwarding continue to consume one direct-linker authority classifier. + +The short split form `-c MRI-commandfile` was already incidentally rejected because the following filename became a positional native input. Modeling `-c` explicitly makes the policy reflect linker semantics instead of relying on that incidental parse outcome. The equals-form long option is the material bypass closed by this generation. + +A pathname allowlist was rejected. An MRI command file is executable linker configuration in the provenance sense: pathname review alone does not prove immutable contents, transitive inputs, search-path resolution, symlink containment, linker/toolchain compatibility, reproducibility, SBOM/attestation, or rollback. Any future exception requires versioned immutable artifact identity and those release properties. + +## Security and release consequence + +This closes the modeled Git-owned Cargo path in which an MRI command file could alter linker behavior without entering the reviewed source/dependency topology. It does not claim integrity of ambient/default linker state, direct CLI flags, runner images, or externally restored toolchain artifacts. + +Hosted exact-head execution, whole-PR review closure, owned production rustdoc/test/edge coverage, and immutable release acceptance remain separate gates. + +## Primary reference + +Free Software Foundation. (2025). *The GNU linker* (GNU Binutils 2.45), `-c MRI-commandfile` / `--mri-script=MRI-commandfile`. https://sourceware.org/binutils/docs-2.45/ld.pdf From 341ec7343745d75f665c01ceb05c507ff31efdd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:17:23 +0900 Subject: [PATCH 460/632] docs(changelog): record MRI linker script provenance guard --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091e8e389..f7b71cde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU `-c` / `--mri-script`, preventing an unreviewed MRI command file from changing link behavior outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `-dT` / `--default-script`, preventing an unreviewed default linker script from changing link behavior while the reviewed Cargo package/source closure remains unchanged. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `--sysroot`, preventing an unreviewed linker search root from changing external system-library inputs while the reviewed Cargo package/source closure remains unchanged. - Failed closed on Git-owned Cargo linker/driver execution extensions that re-select executables or subprocess authority through rustc/rustdoc linker flags (`linker=`, `-fuse-ld=`, driver `-B`, opaque `@file`, GCC `-specs=` / `-specs `, or GCC `-wrapper`) or dynamically load GNU linker plugin code through `-Wl,`, `--for-linker=`, and `-Xlinker`; unrelated linker forwarding remains permitted. @@ -30,7 +31,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Added exact Browser Session create-recovery transaction evidence for `CreateFailedUncertain(Some/None)`, duplicate candidates, and unsettled `Accepted|Rejected` completions, plus a hostile same-valued-handle fixture proving candidate facts and prior ownership facts remain distinct. - Added a 258-generation same-raw-context hostile acceptance proving proven-destroy hot-state retirement, monotonic context epochs, and predecessor-authority rejection before lifecycle I/O. -- Added an `originweave-bidi` presentation-capability boundary referenced against the W3C WebDriver BiDi Working Draft published on 9 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`). That dated document is publication/reference evidence; the separately runtime-qualified compatibility pin remains the 3 September 2026 Working Draft until independent schema/semantics/conformance and pinned-Chromium evidence admit another revision. The boundary depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR and timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. +- Added an `originweave-bidi` presentation-capability boundary referenced against the W3C WebDriver BiDi Working Draft published on 9 September 2026 (`https://www.w3.org/TR/2026/WD-webdriver-bidi-20260909/`). That dated document is publication/reference evidence; the separately runtime-qualified compatibility pin remains the 3 September 2026 Working Draft until independent schema/semantics/conformance and pinned-Chromium evidence admit another revision. The boundary depends inward on `originweave-fingerprint`, keeps the reusable plan limited to symmetrically restorable and explicitly modelled viewport/DPR plus timezone commands, and exposes screen settings as a separate typed partial intent whose one rectangle controls both total and available screen area. Complete `PresentationSurface::Screen` admission still fails closed because available-screen geometry is unmodelled and color depth remains uncontrolled. Reduced motion remains an expressible protocol capability but is not installed by the reusable plan because standard cleanup cannot selectively restore prior media state. No caller-mintable exclusive-reset type is exposed; a Browser Session owner must prove a disposable lifecycle or complete prior-state restoration. Planning performs no transport I/O or acknowledgement, cleanup, ownership, or page-observed evidence. Hardware concurrency and the complete Chromium platform/User-Agent Client Hints surface still require a separate versioned Chromium adapter. - Added a bounded Rust presentation-identity kernel for explicit browser-visible profiles and credential-free replay digests, including control-safe mobile UA-CH model values; applying those profiles to Chromium and proving page-observed effects remain separate adapter and browser-E2E work. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. From f271a2aefe58d12f988ca3c0b89c9b917a5ca550 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:27:46 +0900 Subject: [PATCH 461/632] test(browser-session): expose linker just-symbols provenance bypass --- ...r_just_symbols_input_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_just_symbols_input_authority_contract.py diff --git a/tests/test_browser_session_linker_just_symbols_input_authority_contract.py b/tests/test_browser_session_linker_just_symbols_input_authority_contract.py new file mode 100644 index 000000000..89cc84703 --- /dev/null +++ b/tests/test_browser_session_linker_just_symbols_input_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerJustSymbolsInputAuthorityContractTests(unittest.TestCase): + """Keep GNU/LLD just-symbols files outside repository-owned Browser Session builds.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_just_symbols_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--just-symbols=tools/review-bypass-symbols.o"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_compact_R_file_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=-Rtools/review-bypass-symbols.o\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_just_symbols_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--just-symbols=tools/review-bypass-symbols.o"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_just_symbols_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--just-symbols=tools/review-bypass-symbols.o"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_input_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 7b75fa31291fba28867331a72338174848e4a28c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:32:27 +0900 Subject: [PATCH 462/632] fix(browser-session): fail closed on linker just-symbols inputs --- ...rowser_session_cargo_compiler_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index f9638d968..8c2999a31 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -112,6 +112,15 @@ def _linker_option_selects_script(argument: str) -> bool: ) +def _linker_option_selects_just_symbols_or_rpath(argument: str) -> bool: + """Return whether GNU-compatible linker syntax selects an external -R/just-symbols path.""" + if argument in {"-R", "--just-symbols"}: + return True + if argument.startswith("--just-symbols="): + return True + return argument.startswith("-R") and len(argument) > 2 + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -150,6 +159,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_error_handler(argument) or _linker_option_selects_dtlto_executable(argument) or _linker_option_selects_script(argument) + or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From 4e8fbcc98835e54c3b7d3465b752a5b0f0e69e33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 00:33:31 +0900 Subject: [PATCH 463/632] docs(browser-session): trace linker just-symbols authority --- ...ion-linker-just-symbols-input-authority.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/traceability/browser-session-linker-just-symbols-input-authority.md diff --git a/docs/traceability/browser-session-linker-just-symbols-input-authority.md b/docs/traceability/browser-session-linker-just-symbols-input-authority.md new file mode 100644 index 000000000..c160895ee --- /dev/null +++ b/docs/traceability/browser-session-linker-just-symbols-input-authority.md @@ -0,0 +1,58 @@ +# Browser Session linker `--just-symbols` / `-R` input authority + +Status: Draft + +## Problem + +OriginWeave's Browser Session build-provenance boundary already rejects positional native inputs, linker scripts, response files, plugins, custom sysroots, executable selectors, and modeled library-search inputs. A remaining GNU-compatible linker form could still inject an external file without becoming an unconsumed positional token: + +- `--just-symbols=` +- compact `-R` + +GNU `ld` reads symbol names and absolute addresses from the `-R` / `--just-symbols` operand without relocating or including that file in the output. The same `-R` spelling is treated as an rpath when its operand is a directory. GNU `ld` also permits single-letter option operands to be joined to the option letter. Therefore both forms can change link semantics while bypassing a classifier that only rejects positional native inputs. + +Rust exposes the path through `-C link-arg` and `-C link-args`; on Unix-like targets using a compiler driver, rustc documents `-C link-arg=-Wl,$ARG` as the way to pass an argument to the actual linker. Repository-owned Cargo `rustflags` and `rustdocflags` therefore form a reviewed provenance boundary rather than an ordinary optimization surface. + +## Decision + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo compiler/rustdoc/linker execution and input authority. The existing direct-linker classifier now fails closed on: + +- split `-R` and `--just-symbols` selectors; +- compact `-R`; +- `--just-symbols=`. + +The rule is intentionally shared by build/target `rustflags`, build/target `rustdocflags`, and rustdoc `--doctest-build-arg` forwarding. No second Cargo topology or linker scanner was introduced. `tests/test_browser_session_trusted_adapter_boundary.py` remains the production Cargo topology/source-closure owner. + +The rule does not ban ordinary linker hardening options. `-Wl,-z,relro` remains an allowed control in the focused hostile fixture. + +## RED → repair evidence + +- RED: `f271a2aefe58d12f988ca3c0b89c9b917a5ca550` adds `tests/test_browser_session_linker_just_symbols_input_authority_contract.py` and demonstrates that equals/compact just-symbols forms were not classified by the predecessor shared authority. +- Repair: `7b75fa31291fba28867331a72338174848e4a28c` adds `_linker_option_selects_just_symbols_or_rpath()` to the existing direct-linker authority parser and routes it through the existing `rustflags:codegen linker`, `rustdocflags:codegen linker`, and doctest compiler-authority call sites. + +The focused fixture covers build `rustflags`, target `rustflags`, build `rustdocflags`, rustdoc doctest forwarding, and an ordinary `-z relro` control. + +## Security and provenance effect + +A Git-owned Cargo configuration can no longer select an unreviewed symbol-address file with the modeled GNU-compatible `--just-symbols=` or compact `-R` forms without tripping the Browser Session provenance contract. If `-R` names a directory, the same fail-closed decision prevents repository configuration from silently injecting an rpath through this ambiguous spelling. + +This is a source-semantic contract. It does not by itself establish hosted executable GREEN, compiler-driver parity for every non-GNU linker, or release provenance. + +## Residual authority + +The following remain outside this leaf repository-source contract and require CI/release or linker-family evidence rather than duplicated OriginWeave ownership: + +- environment- or direct-CLI-injected rustc/rustdoc/linker arguments; +- linker-family-specific symbol/control-file mechanisms that are not GNU-compatible grammar already modeled by the shared parser; +- ambient compiler/linker binaries, sysroot contents, runner image, and restored build artifacts; +- immutable release evidence tying the effective toolchain, linker, arguments, SBOM, and provenance to the shipped artifact. + +Any future exception for a symbol-address input must prove immutable artifact identity and digest, producer provenance, exact toolchain/linker compatibility, purpose, containment, reproducibility, and rollback in the same reviewed delta. A pathname allowlist is insufficient. + +## References + +Free Software Foundation. (n.d.). *GNU ld: Command-line options*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld/Options.html + +The Rust Project Developers. (n.d.). *Codegen options: `link-arg` and `link-args`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ + +Retrieved 2026-09-19. From 84d3e085c7c7ac5b858b4282c9d53b1c4c94b24d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 01:03:08 +0900 Subject: [PATCH 464/632] test(browser-session): cover linker symbol-policy file authority --- ..._symbol_policy_input_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_symbol_policy_input_authority_contract.py diff --git a/tests/test_browser_session_linker_symbol_policy_input_authority_contract.py b/tests/test_browser_session_linker_symbol_policy_input_authority_contract.py new file mode 100644 index 000000000..4942a3a10 --- /dev/null +++ b/tests/test_browser_session_linker_symbol_policy_input_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerSymbolPolicyInputAuthorityContractTests(unittest.TestCase): + """Keep external linker symbol-policy files inside reviewed Browser Session provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_version_script_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--version-script=tools/review-bypass.map"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_dynamic_list_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--dynamic-list=tools/review-bypass.dynamic\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_retain_symbols_file_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--retain-symbols-file=tools/review-bypass.symbols"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_export_dynamic_symbol_list_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--export-dynamic-symbol-list=tools/review-bypass.exports"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_inline_symbol_selection_without_external_file_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--export-dynamic-symbol=originweave_*"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From ba239fab05209d13fba2abdd948924a6c2668d32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 01:04:45 +0900 Subject: [PATCH 465/632] fix(browser-session): fail closed on linker symbol-policy files --- ..._session_cargo_compiler_authority_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 8c2999a31..6b48627e7 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -25,6 +25,14 @@ LINKER_SCRIPT_OPTIONS = frozenset( {"-T", "--script", "-dT", "--default-script", "-c", "--mri-script"} ) +LINKER_SYMBOL_POLICY_FILE_OPTIONS = frozenset( + { + "--version-script", + "--dynamic-list", + "--retain-symbols-file", + "--export-dynamic-symbol-list", + } +) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} @@ -112,6 +120,13 @@ def _linker_option_selects_script(argument: str) -> bool: ) +def _linker_option_selects_symbol_policy_file(argument: str) -> bool: + """Return whether a linker option consumes an external symbol-policy file.""" + if argument in LINKER_SYMBOL_POLICY_FILE_OPTIONS: + return True + return argument.startswith(tuple(f"{option}=" for option in LINKER_SYMBOL_POLICY_FILE_OPTIONS)) + + def _linker_option_selects_just_symbols_or_rpath(argument: str) -> bool: """Return whether GNU-compatible linker syntax selects an external -R/just-symbols path.""" if argument in {"-R", "--just-symbols"}: @@ -159,6 +174,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_error_handler(argument) or _linker_option_selects_dtlto_executable(argument) or _linker_option_selects_script(argument) + or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) From e6abeddd44f90f2af16402344cf7555405cd225e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 01:05:28 +0900 Subject: [PATCH 466/632] docs(browser-session): trace linker symbol-policy file authority --- ...on-linker-symbol-policy-input-authority.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/traceability/browser-session-linker-symbol-policy-input-authority.md diff --git a/docs/traceability/browser-session-linker-symbol-policy-input-authority.md b/docs/traceability/browser-session-linker-symbol-policy-input-authority.md new file mode 100644 index 000000000..87f317f5e --- /dev/null +++ b/docs/traceability/browser-session-linker-symbol-policy-input-authority.md @@ -0,0 +1,56 @@ +# Browser Session linker symbol-policy file authority + +Status: Draft + +## Problem + +OriginWeave's Browser Session build-provenance boundary already rejects positional native linker inputs, linker scripts, response files, plugins, custom sysroots, executable selectors, library-search inputs, and GNU `-R` / `--just-symbols` inputs. A separate class of option-shaped file inputs could still bypass that shared direct-linker classifier: + +- `--version-script=` +- `--dynamic-list=` +- `--retain-symbols-file=` +- `--export-dynamic-symbol-list=` + +GNU `ld` consumes each operand as file content that changes symbol visibility, dynamic symbol selection, or retention in the output artifact. Because the path is embedded in an option token, a classifier that only rejects unconsumed positional native inputs does not see it. + +Rust exposes this authority through `-C link-arg` and `-C link-args`; on Unix-like targets using a compiler driver, rustc documents `-C link-arg=-Wl,$ARG` as a way to pass an argument to the actual linker. Git-owned Cargo `rustflags` and `rustdocflags` therefore make these file selectors part of reviewed compiler/linker provenance rather than an ordinary optimization surface. + +## Decision + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo compiler/rustdoc/linker execution and input authority. The existing direct-linker parser now classifies the four GNU symbol-policy file selectors above through one `LINKER_SYMBOL_POLICY_FILE_OPTIONS` set and `_linker_option_selects_symbol_policy_file()` helper. + +The rule is reused by build/target `rustflags`, build/target `rustdocflags`, and rustdoc `--doctest-build-arg` forwarding through the existing `-Wl,`, `--for-linker=`, `-Xlinker`, `link-arg`, and `link-args` paths. No second Cargo topology scanner or cross-service authority was introduced. `tests/test_browser_session_trusted_adapter_boundary.py` remains the production Cargo package/source-closure owner. + +Inline symbol selection without an external file remains allowed. The focused control fixture uses `--export-dynamic-symbol=originweave_*` to distinguish a literal pattern from `--export-dynamic-symbol-list=`. + +## RED → repair evidence + +- RED: `84d3e085c7c7ac5b858b4282c9d53b1c4c94b24d` adds `tests/test_browser_session_linker_symbol_policy_input_authority_contract.py`. Against predecessor `4e8fbcc98835e54c3b7d3465b752a5b0f0e69e33`, all four option-shaped hostile inputs bypassed the shared direct-linker authority predicate. +- Repair: `ba239fab05209d13fba2abdd948924a6c2668d32` extends the existing canonical classifier with the symbol-policy file option set and helper. The repair changes only the shared authority file; the focused test already exercises build `rustflags`, target `rustflags`, build `rustdocflags`, doctest forwarding, and an inline-symbol allowed control. + +A source-semantic focused check on the repaired helper classifies all four hostile selectors as external authority and leaves the inline `--export-dynamic-symbol=originweave_*` control unclassified. Hosted exact-head executable evidence is still required before repository/security GREEN is claimed. + +## Security and provenance effect + +Repository-owned Cargo configuration can no longer select unreviewed symbol-version, dynamic-list, retained-symbol, or export-symbol-list files through the modeled GNU-compatible linker forwarding paths without tripping the Browser Session provenance contract. This prevents link output semantics from depending on an external policy file that is absent from the reviewed Cargo package/source closure. + +The policy is fail closed rather than pathname-allowlist based. A path alone does not prove file digest, producer provenance, symlink containment, exact linker compatibility, reproducibility, or rollback. + +## Residual authority + +The following remain CI/release supply-chain evidence surfaces rather than duplicated OriginWeave leaf ownership: + +- environment- or direct-CLI-injected rustc/rustdoc/linker arguments; +- linker-family-specific symbol/control-file options outside the modeled GNU-compatible grammar; +- ambient compiler/linker binaries, sysroot contents, runner image, and restored build artifacts; +- immutable release evidence tying the effective toolchain, linker, arguments, SBOM, and provenance to the shipped artifact. + +Any future exception for one of these symbol-policy files must bind immutable artifact identity and digest, producer/source provenance, exact toolchain/linker compatibility, purpose, containment, reproducibility, and rollback in the same reviewed delta. + +## References + +Free Software Foundation. (n.d.). *GNU ld: Options*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld/Options.html + +The Rust Project Developers. (n.d.). *Codegen options: `link-arg` and `link-args`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ + +Retrieved 2026-09-19. From 268f60c4f1e5631ada484c4d5313f35faad01c5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 01:06:32 +0900 Subject: [PATCH 467/632] docs(changelog): record linker provenance repairs --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b71cde1..5a9621df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU symbol-policy files through `--version-script`, `--dynamic-list`, `--retain-symbols-file`, or `--export-dynamic-symbol-list`, preventing unreviewed external symbol visibility/retention policy from changing the linked artifact outside the reviewed Cargo package/source closure. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU-compatible `-R` / `--just-symbols`, preventing an unreviewed symbol-address file or ambiguous rpath operand from changing link behavior outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU `-c` / `--mri-script`, preventing an unreviewed MRI command file from changing link behavior outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `-dT` / `--default-script`, preventing an unreviewed default linker script from changing link behavior while the reviewed Cargo package/source closure remains unchanged. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU/LLD `--sysroot`, preventing an unreviewed linker search root from changing external system-library inputs while the reviewed Cargo package/source closure remains unchanged. From 0b68811ed2bd9ff0324853f8b24a7fbcb8abe626 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 01:59:55 +0900 Subject: [PATCH 468/632] test(browser-session): reproduce runtime loader authority bypass --- ...inker_runtime_loader_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_runtime_loader_authority_contract.py diff --git a/tests/test_browser_session_linker_runtime_loader_authority_contract.py b/tests/test_browser_session_linker_runtime_loader_authority_contract.py new file mode 100644 index 000000000..2f0e9d2ce --- /dev/null +++ b/tests/test_browser_session_linker_runtime_loader_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerRuntimeLoaderAuthorityContractTests(unittest.TestCase): + """Keep ELF runtime-loader selection inside reviewed Browser Session provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_dynamic_linker_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--dynamic-linker=tools/review-bypass-loader"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_compact_dynamic_linker_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=-Itools/review-bypass-loader\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_dynamic_linker_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--dynamic-linker=tools/review-bypass-loader"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_dynamic_linker_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--dynamic-linker=tools/review-bypass-loader"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_no_dynamic_linker_request_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 132b6e15c8de47e2caa8395843c64c6ba77bf521 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:00:45 +0900 Subject: [PATCH 469/632] test(browser-session): cover runtime loader suppression authority --- ...er_session_linker_runtime_loader_authority_contract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_linker_runtime_loader_authority_contract.py b/tests/test_browser_session_linker_runtime_loader_authority_contract.py index 2f0e9d2ce..132bf9680 100644 --- a/tests/test_browser_session_linker_runtime_loader_authority_contract.py +++ b/tests/test_browser_session_linker_runtime_loader_authority_contract.py @@ -67,7 +67,13 @@ def test_doctest_forwarded_dynamic_linker_fails_closed(self) -> None: "rustdocflags:doctest compiler authority", ) - def test_no_dynamic_linker_request_remains_allowed(self) -> None: + def test_runtime_loader_suppression_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--no-dynamic-linker"]\n', + "rustflags:codegen linker", + ) + + def test_unrelated_linker_option_remains_allowed(self) -> None: directory, root = _workspace_with_config( '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' ) From 0d241f075fc3958838c9d5d5c266357d86efc6d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:02:01 +0900 Subject: [PATCH 470/632] fix(browser-session): fail closed on runtime loader authority --- ...rowser_session_cargo_compiler_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 6b48627e7..f6ca8168d 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -136,6 +136,15 @@ def _linker_option_selects_just_symbols_or_rpath(argument: str) -> bool: return argument.startswith("-R") and len(argument) > 2 +def _linker_option_controls_runtime_loader(argument: str) -> bool: + """Return whether GNU-compatible linker syntax changes the ELF load-time interpreter.""" + if argument in {"-I", "--dynamic-linker", "--no-dynamic-linker"}: + return True + if argument.startswith("--dynamic-linker="): + return True + return argument.startswith("-I") and len(argument) > 2 + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -176,6 +185,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_script(argument) or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_just_symbols_or_rpath(argument) + or _linker_option_controls_runtime_loader(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From 75334b8b08adefa8da6d9317474aabf6ed16df03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:02:36 +0900 Subject: [PATCH 471/632] docs(browser-session): trace runtime loader authority --- ...session-linker-runtime-loader-authority.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/traceability/browser-session-linker-runtime-loader-authority.md diff --git a/docs/traceability/browser-session-linker-runtime-loader-authority.md b/docs/traceability/browser-session-linker-runtime-loader-authority.md new file mode 100644 index 000000000..e28040af4 --- /dev/null +++ b/docs/traceability/browser-session-linker-runtime-loader-authority.md @@ -0,0 +1,60 @@ +# Browser Session linker runtime-loader authority + +## Problem + +OriginWeave treats repository-owned Cargo compiler and linker selection as Browser Session provenance because the linked runtime is part of the executable trust boundary. The shared Cargo/rustc/rustdoc authority contract already rejected linker replacement, plugins, scripts, response files, external native inputs, symbol-policy files, and related execution/input selectors, but GNU-compatible ELF runtime-loader controls were not modeled explicitly. + +GNU `ld` 2.47 documents `-Ifile` / `--dynamic-linker=file` as selecting the dynamic linker recorded for a dynamically linked ELF executable. It also documents `--no-dynamic-linker` as suppressing the load-time dynamic-linker request. These options therefore change which load-time interpreter participates in process startup, or whether that interpreter contract exists at all; they are not ordinary optimization or presentation flags. + +Rust's `-C link-arg` and `-C link-args` append arguments to the linker invocation. On Unix-like targets using a compiler driver, rustc documents `-Clink-arg=-Wl,$ARG` as the route for passing an argument to the underlying linker. Git-owned Cargo `rustflags`, `rustdocflags`, and rustdoc `--doctest-build-arg` forwarding can therefore carry the runtime-loader controls into the final link. + +## Constraint + +The Browser Session authority owner must fail closed without creating a second Cargo/linker scanner. Production Cargo package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`; repository-selected compiler/rustdoc/toolchain/linker execution and input authority remains owned by `tests/test_browser_session_cargo_compiler_authority_contract.py`. + +A pathname allowlist is insufficient. A permitted path alone does not establish the loader artifact digest, producer provenance, ABI/toolchain compatibility, filesystem containment, reproducibility, or rollback identity. Likewise, treating `--no-dynamic-linker` as harmless would allow a reviewed build to change its load-time execution model without an explicit provenance decision. + +## Alternatives considered + +1. Allow the linker defaults plus arbitrary `--dynamic-linker` paths. Rejected because a repository change could select a different ELF interpreter while the reviewed Rust/Cargo source closure remains unchanged. +2. Permit repository-relative loader paths. Rejected because repository-relative spelling does not prove immutable artifact identity, symlink containment, executable compatibility, or runtime deployment identity. +3. Block only long-form `--dynamic-linker=`. Rejected because GNU `ld` also accepts the short `-Ifile` spelling, and runtime-loader suppression is independently authority-changing. +4. Extend the existing direct-linker classifier. Selected because all build/target rustflags, rustdocflags, `-Wl,`, `--for-linker=`, `-Xlinker`, and doctest compiler forwarding already converge on that owner. + +## RED → repair + +Structural RED commits: + +- `0b68811ed2bd9ff0324853f8b24a7fbcb8abe626` adds hostile coverage for long-form and short-form ELF runtime-loader selection through build/target `rustflags`, build `rustdocflags`, and rustdoc doctest forwarding. +- `132b6e15c8de47e2caa8395843c64c6ba77bf521` adds explicit `--no-dynamic-linker` coverage while preserving `--as-needed` as an allowed control. + +Minimal repair: + +- `0d241f075fc3958838c9d5d5c266357d86efc6d2` adds `_linker_option_controls_runtime_loader()` to the existing direct-linker authority classifier and rejects `-I`, compact `-Ifile`, `--dynamic-linker`, `--dynamic-linker=file`, and `--no-dynamic-linker` through the same shared owner. No second Cargo topology or linker scanner is introduced. + +The repair commit changes only the canonical authority contract by 10 added lines relative to the final RED head; no unrelated file content is removed or rewritten. + +## Invariant + +Repository-owned Cargo configuration must not select or suppress the ELF load-time interpreter through Rust/rustdoc linker forwarding unless a separately reviewed Browser Session provenance contract proves that authority. The command acknowledgement or successful link alone is not evidence that the runtime interpreter is the intended one. + +## Evidence required for a future exception + +Any future intentional custom runtime-loader contract must bind, at minimum: + +- immutable loader artifact identity and cryptographic digest; +- producer/source provenance and SBOM linkage; +- exact target ABI and linker/toolchain compatibility; +- path and symlink containment at build and deployment time; +- deployment/runtime identity proving the recorded interpreter resolves to the reviewed artifact; +- reproducible reference build evidence; +- rollback and expiry/revalidation rules; +- buyer-visible security and operability documentation when the runtime model differs from the platform default. + +Environment or direct-CLI overrides, runner image identity, system linker distribution, external deployment filesystem state, and loader artifacts materialized outside the reviewed repository remain CI/release supply-chain evidence surfaces rather than leaf-source exceptions. + +## References + +Free Software Foundation. (2026). *LD: The GNU linker (GNU Binutils 2.47)*. https://sourceware.org/binutils/docs/ld.html + +The Rust Project Developers. (2026). *Codegen options: `link-arg`, `link-args`, and `linker`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ From 81b05158621716fdb323b8a77960d84eeb6e633e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:08:55 +0900 Subject: [PATCH 472/632] test(browser-session): reproduce rtld-audit authority bypass --- ...linker_runtime_audit_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_runtime_audit_authority_contract.py diff --git a/tests/test_browser_session_linker_runtime_audit_authority_contract.py b/tests/test_browser_session_linker_runtime_audit_authority_contract.py new file mode 100644 index 000000000..4abc59c20 --- /dev/null +++ b/tests/test_browser_session_linker_runtime_audit_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerRuntimeAuditAuthorityContractTests(unittest.TestCase): + """Keep ELF rtld-audit selection inside reviewed Browser Session provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_audit_library_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--audit=tools/review-bypass-audit.so"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_depaudit_library_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--depaudit=tools/review-bypass-audit.so\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_solaris_depaudit_short_form_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,-Ptools/review-bypass-audit.so"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_audit_library_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--audit=tools/review-bypass-audit.so"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_unrelated_linker_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 4b25e4e64e437a4305bd83308a58c412e0d93baa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:10:11 +0900 Subject: [PATCH 473/632] test(browser-session): cover GNU single-dash audit alias --- ...owser_session_linker_runtime_audit_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_linker_runtime_audit_authority_contract.py b/tests/test_browser_session_linker_runtime_audit_authority_contract.py index 4abc59c20..1690d8f92 100644 --- a/tests/test_browser_session_linker_runtime_audit_authority_contract.py +++ b/tests/test_browser_session_linker_runtime_audit_authority_contract.py @@ -55,6 +55,12 @@ def test_target_rustflags_depaudit_library_fails_closed(self) -> None: "rustflags:codegen linker", ) + def test_single_dash_long_audit_alias_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-audit=tools/review-bypass-audit.so"]\n', + "rustflags:codegen linker", + ) + def test_build_rustdocflags_solaris_depaudit_short_form_fails_closed(self) -> None: self._assert_fails_closed( '[build]\nrustdocflags = ["-C", "link-arg=-Wl,-Ptools/review-bypass-audit.so"]\n', From 942c158d7b9f7f192688d152673b0badfd36eaf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:13:52 +0900 Subject: [PATCH 474/632] fix(browser-session): fail closed on rtld-audit authority --- ...rowser_session_cargo_compiler_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index f6ca8168d..18492b6de 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -145,6 +145,15 @@ def _linker_option_controls_runtime_loader(argument: str) -> bool: return argument.startswith("-I") and len(argument) > 2 +def _linker_option_selects_runtime_audit_library(argument: str) -> bool: + """Return whether GNU-compatible linker syntax selects an ELF rtld-audit library.""" + if argument in {"--audit", "-audit", "--depaudit", "-depaudit", "-P"}: + return True + if argument.startswith(("--audit=", "-audit=", "--depaudit=", "-depaudit=")): + return True + return argument.startswith("-P") and len(argument) > 2 + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -186,6 +195,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_controls_runtime_loader(argument) + or _linker_option_selects_runtime_audit_library(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From db9b283e2469197fb927519499516b5c46989a86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 02:14:19 +0900 Subject: [PATCH 475/632] docs(browser-session): trace rtld-audit authority --- ...-session-linker-runtime-audit-authority.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/traceability/browser-session-linker-runtime-audit-authority.md diff --git a/docs/traceability/browser-session-linker-runtime-audit-authority.md b/docs/traceability/browser-session-linker-runtime-audit-authority.md new file mode 100644 index 000000000..8366e6a92 --- /dev/null +++ b/docs/traceability/browser-session-linker-runtime-audit-authority.md @@ -0,0 +1,60 @@ +# Browser Session linker runtime-audit authority + +## Problem + +OriginWeave treats repository-selected linker inputs and executable/runtime authority as Browser Session provenance. The shared Cargo/rustc/rustdoc authority contract already rejects linker replacement, plugins, scripts, response files, external native inputs, symbol-policy files, and ELF interpreter selection. GNU-compatible rtld-audit controls remained a separate option-shaped gap: Git-owned linker forwarding could name an audit library that the platform dynamic linker may load when the produced ELF object executes. + +GNU `ld` 2.47 documents `--audit AUDITLIB` as adding `AUDITLIB` to the `DT_AUDIT` entry of the dynamic section, and `--depaudit AUDITLIB` / `-P AUDITLIB` as adding it to `DT_DEPAUDIT`. The linker does not check that these named libraries exist. On platforms supporting the rtld-audit interface, the values therefore select runtime code outside the reviewed Rust/Cargo source closure rather than merely changing a link-time diagnostic or optimization. GNU `ld` also documents that multi-letter options can use one or two leading dashes, so the single-dash `-audit=...` / `-depaudit=...` spellings must not become a spelling-based bypass. + +Rust's `-C link-arg` and `-C link-args` append arguments to the linker invocation. On Unix-like targets using a compiler driver, rustc documents `-Clink-arg=-Wl,$ARG` for forwarding an option to the underlying linker. Repository-owned Cargo `rustflags`, `rustdocflags`, and rustdoc `--doctest-build-arg` can therefore carry `DT_AUDIT` / `DT_DEPAUDIT` selection into produced artifacts. + +## Constraint + +The repair must remain inside the existing Browser Session compiler/linker authority owner. `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology, while `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler/rustdoc/toolchain/linker execution and input authority. A second Cargo/linker scanner is not acceptable. + +A pathname allowlist is also insufficient: a permitted soname or path alone does not prove the audit library's immutable bytes, producer provenance, ABI compatibility, deployment-time resolution, dependency closure, or rollback identity. + +## Alternatives considered + +1. Allow `--audit` / `--depaudit` because the linker does not itself load the named library. Rejected: the option persists runtime-load authority in the ELF dynamic section, so the security boundary is the produced runtime behavior, not only the linker process. +2. Permit repository-relative audit-library names. Rejected: name spelling does not prove immutable runtime artifact identity or deployment resolution. +3. Block only the double-dash forms. Rejected: GNU `ld` accepts multi-letter options with one or two leading dashes, and Solaris-compatible `-P` is a documented dependency-audit selector. +4. Extend the existing direct-linker classifier. Selected because build/target `rustflags`, `rustdocflags`, `-Wl,`, `--for-linker=`, `-Xlinker`, `link-arg`/`link-args`, and doctest compiler forwarding already converge on that owner. + +## RED → repair + +Structural RED: + +- `81b05158621716fdb323b8a77960d84eeb6e633e` adds hostile `--audit=...`, `--depaudit=...`, compact `-P...`, and doctest-forwarded audit-library cases while retaining `--as-needed` as an allowed control. +- `4b25e4e64e437a4305bd83308a58c412e0d93baa` adds the GNU single-dash multi-letter `-audit=...` alias so the contract does not accidentally depend on one spelling. + +Minimal repair: + +- `942c158d7b9f7f192688d152673b0badfd36eaf7` adds `_linker_option_selects_runtime_audit_library()` to the existing direct-linker authority classifier and consumes it from `_direct_linker_arguments_extend_authority()`. It covers split and joined double-dash forms, the GNU single-dash multi-letter aliases, and split/compact `-P`. The final-RED → repair diff is one canonical authority file with 10 additions and no deletions. + +No new Cargo topology or linker scanner is introduced. + +## Invariant + +Repository-owned Cargo configuration must not persist an unreviewed rtld-audit library into `DT_AUDIT` or `DT_DEPAUDIT` through Rust/rustdoc linker forwarding. A successful link or command acknowledgement is not evidence that the produced executable's runtime audit code is approved. + +## Evidence required for a future exception + +Any intentional runtime-audit contract must bind, at minimum: + +- immutable audit-library artifact identity and cryptographic digest; +- producer/source provenance, dependency closure, and SBOM linkage; +- exact target ABI, libc/dynamic-linker, and toolchain compatibility; +- build- and deployment-time path/soname resolution and symlink containment; +- purpose and least-privilege justification for audit callbacks; +- reproducible build and runtime acceptance evidence against the pinned loader/library combination; +- rollback, expiry, and revalidation policy; +- buyer-visible security and operability documentation when rtld-audit is enabled. + +Environment/direct-CLI overrides, externally materialized audit libraries, runner image/toolchain identity, system linker distribution, and deployment filesystem state remain CI/release supply-chain evidence surfaces rather than leaf-source exceptions. + +## References + +Free Software Foundation. (2026). *LD: The GNU linker (GNU Binutils 2.47)*. https://sourceware.org/binutils/docs/ld.html + +The Rust Project Developers. (2026). *Codegen options: `link-arg`, `link-args`, and `linker`*. The rustc book. https://doc.rust-lang.org/rustc/codegen-options/ From 0168d405a89a5dfec5fff4ed49aef28f43f546b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:00:19 +0900 Subject: [PATCH 476/632] test(browser-session): RED ELF runtime filter authority --- ...inker_runtime_filter_authority_contract.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_browser_session_linker_runtime_filter_authority_contract.py diff --git a/tests/test_browser_session_linker_runtime_filter_authority_contract.py b/tests/test_browser_session_linker_runtime_filter_authority_contract.py new file mode 100644 index 000000000..8dbaf6d0c --- /dev/null +++ b/tests/test_browser_session_linker_runtime_filter_authority_contract.py @@ -0,0 +1,85 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerRuntimeFilterAuthorityContractTests(unittest.TestCase): + """Keep ELF DT_AUXILIARY/DT_FILTER runtime implementation selection in reviewed provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_auxiliary_library_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--auxiliary=tools/review-bypass-impl.so"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_filter_library_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--filter=tools/review-bypass-impl.so\"]\n", + "rustflags:codegen linker", + ) + + def test_single_dash_long_auxiliary_alias_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-auxiliary=tools/review-bypass-impl.so"]\n', + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_compact_filter_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,-Ftools/review-bypass-impl.so"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_compact_auxiliary_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,-ftools/review-bypass-impl.so"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_unrelated_linker_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From b7adc787523741bd35cedf86704133db7d6da9c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:01:53 +0900 Subject: [PATCH 477/632] fix(browser-session): govern ELF runtime filter authority --- ...ser_session_cargo_compiler_authority_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 18492b6de..ead5d0281 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -154,6 +154,18 @@ def _linker_option_selects_runtime_audit_library(argument: str) -> bool: return argument.startswith("-P") and len(argument) > 2 +def _linker_option_selects_runtime_filter_library(argument: str) -> bool: + """Return whether GNU-compatible linker syntax selects ELF auxiliary/filter runtime code.""" + if argument in {"-f", "-F", "--auxiliary", "-auxiliary", "--filter", "-filter"}: + return True + if argument.startswith(("--auxiliary=", "-auxiliary=", "--filter=", "-filter=")): + return True + return ( + (argument.startswith("-f") and len(argument) > 2 and not argument.startswith("--")) + or (argument.startswith("-F") and len(argument) > 2) + ) + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -196,6 +208,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_controls_runtime_loader(argument) or _linker_option_selects_runtime_audit_library(argument) + or _linker_option_selects_runtime_filter_library(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From d9050528118b6d292781ae97b5391a78627be054 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:02:26 +0900 Subject: [PATCH 478/632] docs(browser-session): trace ELF runtime filter authority --- ...session-linker-runtime-filter-authority.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/traceability/browser-session-linker-runtime-filter-authority.md diff --git a/docs/traceability/browser-session-linker-runtime-filter-authority.md b/docs/traceability/browser-session-linker-runtime-filter-authority.md new file mode 100644 index 000000000..5b86848f2 --- /dev/null +++ b/docs/traceability/browser-session-linker-runtime-filter-authority.md @@ -0,0 +1,63 @@ +# Browser Session linker runtime filter authority + +Status: Proposed + +Owner: OriginWeave Browser Session / Cargo compiler authority contract + +## Problem + +GNU-compatible ELF linkers can persist runtime implementation indirection in the output artifact without adding a normal Cargo dependency. `-f name` / `--auxiliary=name` creates `DT_AUXILIARY`; `-F name` / `--filter=name` creates `DT_FILTER`. At runtime the dynamic linker can resolve symbols through the named shared object, so repository-owned `rustflags` or `rustdocflags` can select runtime code outside the reviewed Cargo source/dependency closure. + +This is not equivalent to ordinary linker tuning. The selected shared object participates in runtime symbol implementation authority. + +## Primary sources + +- GNU Binutils 2.47 `ld` manual, command-line options: https://sourceware.org/binutils/docs/ld/Options.html +- GNU Binutils 2.47 `ld` manual: https://sourceware.org/binutils/docs/ld/ +- rustc code-generation options (`link-arg`, `link-args`): https://doc.rust-lang.org/rustc/codegen-options/index.html + +The GNU `ld` manual states that `--auxiliary=name` records `DT_AUXILIARY` and permits the named shared object to provide alternative implementations. `--filter=name` records `DT_FILTER`; when the filter object is used at runtime, the dynamic linker resolves selected symbols to definitions in the named shared object. The same manual specifies that multi-letter options accept one or two leading dashes and that single-letter option operands may be joined to the option. + +## Invariant + +Git-owned Cargo configuration must not use compiler/linker forwarding to select ELF auxiliary/filter runtime implementations unless the selected runtime artifact is represented by an explicit reviewed provenance contract. + +The canonical classifier therefore rejects: + +- `-f name` and compact `-fname`; +- `-F name` and compact `-Fname`; +- `--auxiliary name`, `--auxiliary=name`, and GNU single-dash `-auxiliary` forms; +- `--filter name`, `--filter=name`, and GNU single-dash `-filter` forms; +- the same controls when forwarded through `-Wl,`, `--for-linker=`, `-Xlinker`, rustdoc flags, or rustdoc doctest compiler forwarding. + +`--as-needed` remains an allowed control because it does not name a new runtime implementation artifact. + +## RED → repair evidence + +Structural RED: `0168d405a89a5dfec5fff4ed49aef28f43f546b8` + +The RED fixture covers build and target `rustflags`, `rustdocflags`, doctest forwarding, GNU single-dash long-option spelling, compact `-f`/`-F` spelling, and an unrelated allowed control. + +Minimal canonical repair: `b7adc787523741bd35cedf86704133db7d6da9c3` + +The repair changes only `tests/test_browser_session_cargo_compiler_authority_contract.py`: one runtime-filter classifier is added to the existing direct-linker single writer. No second Cargo scanner, source-topology walker, or pathname allowlist is introduced. + +## Rejected alternatives + +A pathname allowlist is insufficient. A stable path does not prove the selected shared object's digest, producer source, toolchain, ABI compatibility, deployment identity, or rollback state. Treating only `--filter`/`--auxiliary` as relevant while allowing compact `-f`/`-F` or GNU single-dash long-option aliases would also leave equivalent spellings outside the contract. + +## Future exception evidence + +Any approved exception must bind at least: + +- immutable artifact identity and cryptographic digest; +- producer source revision and build provenance; +- target ABI and dynamic-linker compatibility; +- SBOM/provenance linkage to the consuming release; +- deployment path/namespace containment and purpose; +- reproducible fallback or rollback procedure; +- explicit expiry/revalidation conditions. + +## Residual authority + +Ambient `RUSTFLAGS`/`RUSTDOCFLAGS`, direct CLI arguments, linker distribution/version and `PATH`, runner/container image identity, deployed runtime filesystem contents, loader search paths, and externally materialized shared objects remain CI/release supply-chain evidence surfaces. They are not duplicated into the OriginWeave leaf repository policy contract. From 355161387984dc1277fefd1e1bdc2138360953f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:10:46 +0900 Subject: [PATCH 479/632] test(browser-session): RED preserve GNU ld fini selector --- ...ser_session_linker_runtime_filter_authority_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_browser_session_linker_runtime_filter_authority_contract.py b/tests/test_browser_session_linker_runtime_filter_authority_contract.py index 8dbaf6d0c..445c17e9e 100644 --- a/tests/test_browser_session_linker_runtime_filter_authority_contract.py +++ b/tests/test_browser_session_linker_runtime_filter_authority_contract.py @@ -73,6 +73,13 @@ def test_doctest_forwarded_compact_auxiliary_fails_closed(self) -> None: "rustdocflags:doctest compiler authority", ) + def test_fini_symbol_selector_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-fini=originweave_fini"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_linker_option_remains_allowed(self) -> None: directory, root = _workspace_with_config( '[build]\nrustflags = ["-C", "link-arg=-Wl,--as-needed"]\n' From 28592541f884c08d2f9cb6cfa888508214951115 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:13:32 +0900 Subject: [PATCH 480/632] fix(browser-session): preserve GNU ld fini selector --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index ead5d0281..e204fc81e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -160,6 +160,8 @@ def _linker_option_selects_runtime_filter_library(argument: str) -> bool: return True if argument.startswith(("--auxiliary=", "-auxiliary=", "--filter=", "-filter=")): return True + if argument.startswith("-fini="): + return False return ( (argument.startswith("-f") and len(argument) > 2 and not argument.startswith("--")) or (argument.startswith("-F") and len(argument) > 2) From bc7342f8b235377e43b0e0f84ccb5211919e2555 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:13:59 +0900 Subject: [PATCH 481/632] docs(browser-session): record runtime filter compatibility correction --- ...session-linker-runtime-filter-authority.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/traceability/browser-session-linker-runtime-filter-authority.md b/docs/traceability/browser-session-linker-runtime-filter-authority.md index 5b86848f2..49c2bb5fd 100644 --- a/docs/traceability/browser-session-linker-runtime-filter-authority.md +++ b/docs/traceability/browser-session-linker-runtime-filter-authority.md @@ -12,39 +12,43 @@ This is not equivalent to ordinary linker tuning. The selected shared object par ## Primary sources -- GNU Binutils 2.47 `ld` manual, command-line options: https://sourceware.org/binutils/docs/ld/Options.html -- GNU Binutils 2.47 `ld` manual: https://sourceware.org/binutils/docs/ld/ +- GNU Binutils `ld` manual, command-line options: https://sourceware.org/binutils/docs/ld/Options.html +- GNU Binutils `ld` manual: https://sourceware.org/binutils/docs/ld/ - rustc code-generation options (`link-arg`, `link-args`): https://doc.rust-lang.org/rustc/codegen-options/index.html The GNU `ld` manual states that `--auxiliary=name` records `DT_AUXILIARY` and permits the named shared object to provide alternative implementations. `--filter=name` records `DT_FILTER`; when the filter object is used at runtime, the dynamic linker resolves selected symbols to definitions in the named shared object. The same manual specifies that multi-letter options accept one or two leading dashes and that single-letter option operands may be joined to the option. +The same option surface also defines `-fini=name`, which only selects the symbol used for `DT_FINI`. It does not name an external shared object. Compact `-f` handling therefore must not collapse the documented `-fini=` spelling into auxiliary-library authority. + ## Invariant Git-owned Cargo configuration must not use compiler/linker forwarding to select ELF auxiliary/filter runtime implementations unless the selected runtime artifact is represented by an explicit reviewed provenance contract. The canonical classifier therefore rejects: -- `-f name` and compact `-fname`; -- `-F name` and compact `-Fname`; +- `-f name` and compact `-f`; +- `-F name` and compact `-F`; - `--auxiliary name`, `--auxiliary=name`, and GNU single-dash `-auxiliary` forms; - `--filter name`, `--filter=name`, and GNU single-dash `-filter` forms; - the same controls when forwarded through `-Wl,`, `--for-linker=`, `-Xlinker`, rustdoc flags, or rustdoc doctest compiler forwarding. -`--as-needed` remains an allowed control because it does not name a new runtime implementation artifact. +`-fini=` and `--as-needed` remain allowed controls because neither names a new runtime implementation artifact. ## RED → repair evidence -Structural RED: `0168d405a89a5dfec5fff4ed49aef28f43f546b8` +Structural security RED: `0168d405a89a5dfec5fff4ed49aef28f43f546b8` -The RED fixture covers build and target `rustflags`, `rustdocflags`, doctest forwarding, GNU single-dash long-option spelling, compact `-f`/`-F` spelling, and an unrelated allowed control. +The security RED fixture covers build and target `rustflags`, `rustdocflags`, doctest forwarding, GNU single-dash long-option spelling, compact `-f`/`-F` spelling, and an unrelated allowed control. -Minimal canonical repair: `b7adc787523741bd35cedf86704133db7d6da9c3` +Minimal canonical security repair: `b7adc787523741bd35cedf86704133db7d6da9c3` The repair changes only `tests/test_browser_session_cargo_compiler_authority_contract.py`: one runtime-filter classifier is added to the existing direct-linker single writer. No second Cargo scanner, source-topology walker, or pathname allowlist is introduced. +Fresh compatibility review of the GNU primary source found that the initial compact-`-f` predicate also matched the documented `-fini=` option. Compatibility RED `355161387984dc1277fefd1e1bdc2138360953f3` adds `-Wl,-fini=originweave_fini` as an allowed control and therefore fails against the over-broad initial classifier. Minimal correction `28592541f884c08d2f9cb6cfa888508214951115` excludes the documented `-fini=` spelling before compact auxiliary parsing; the hostile auxiliary/filter cases remain unchanged. + ## Rejected alternatives -A pathname allowlist is insufficient. A stable path does not prove the selected shared object's digest, producer source, toolchain, ABI compatibility, deployment identity, or rollback state. Treating only `--filter`/`--auxiliary` as relevant while allowing compact `-f`/`-F` or GNU single-dash long-option aliases would also leave equivalent spellings outside the contract. +A pathname allowlist is insufficient. A stable path does not prove the selected shared object's digest, producer source, toolchain, ABI compatibility, deployment identity, or rollback state. Treating only `--filter`/`--auxiliary` as relevant while allowing compact `-f`/`-F` or GNU single-dash long-option aliases would leave equivalent spellings outside the contract. Conversely, treating every token beginning with `-f` as auxiliary-library selection is too broad because GNU `ld` separately defines `-fini=`. ## Future exception evidence From 12abc14ec3c98caa5662686857409160bf41a02e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:21:04 +0900 Subject: [PATCH 482/632] test(browser-session): RED linker runtime search-path authority --- ..._runtime_search_path_authority_contract.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_browser_session_linker_runtime_search_path_authority_contract.py diff --git a/tests/test_browser_session_linker_runtime_search_path_authority_contract.py b/tests/test_browser_session_linker_runtime_search_path_authority_contract.py new file mode 100644 index 000000000..de8f51b53 --- /dev/null +++ b/tests/test_browser_session_linker_runtime_search_path_authority_contract.py @@ -0,0 +1,85 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerRuntimeSearchPathAuthorityContractTests(unittest.TestCase): + """Keep ELF runtime/link-time shared-library search paths in reviewed provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_rpath_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-rpath=tools/runtime-libs"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_long_rpath_link_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--rpath-link=tools/link-libs\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_long_rpath_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--rpath=tools/doc-runtime-libs"]\n', + "rustdocflags:codegen linker", + ) + + def test_target_rustdocflags_split_rpath_link_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-C\", \"link-args=-Xlinker -rpath-link -Xlinker tools/doc-link-libs\"]\n", + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_rpath_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,-rpath=tools/doctest-runtime-libs"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_new_dtags_toggle_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--enable-new-dtags"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From a13f803e8d350abddfaec6dc89378fec438f211d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:22:47 +0900 Subject: [PATCH 483/632] fix(browser-session): govern linker runtime search paths --- ...t_browser_session_cargo_compiler_authority_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index e204fc81e..3eef8cd55 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -168,6 +168,13 @@ def _linker_option_selects_runtime_filter_library(argument: str) -> bool: ) +def _linker_option_selects_runtime_search_path(argument: str) -> bool: + """Return whether GNU-compatible linker syntax selects runtime/link-time shared-library paths.""" + if argument in {"-rpath", "--rpath", "-rpath-link", "--rpath-link"}: + return True + return argument.startswith(("-rpath=", "--rpath=", "-rpath-link=", "--rpath-link=")) + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -211,6 +218,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_controls_runtime_loader(argument) or _linker_option_selects_runtime_audit_library(argument) or _linker_option_selects_runtime_filter_library(argument) + or _linker_option_selects_runtime_search_path(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From e05cfe71e4943d97ccf1f80a612a8036cb73ac16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:23:10 +0900 Subject: [PATCH 484/632] docs(browser-session): trace linker runtime search-path authority --- ...on-linker-runtime-search-path-authority.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/traceability/browser-session-linker-runtime-search-path-authority.md diff --git a/docs/traceability/browser-session-linker-runtime-search-path-authority.md b/docs/traceability/browser-session-linker-runtime-search-path-authority.md new file mode 100644 index 000000000..3ce267feb --- /dev/null +++ b/docs/traceability/browser-session-linker-runtime-search-path-authority.md @@ -0,0 +1,61 @@ +# Browser Session linker runtime search-path authority + +Status: Proposed + +Owner: OriginWeave Browser Session / Cargo compiler authority contract + +## Problem + +GNU-compatible ELF linkers can change which shared objects participate in the produced program without changing the reviewed Cargo dependency graph. `-rpath=dir` embeds a runtime library search directory in the output and passes it to the runtime linker. `-rpath-link=dir` changes the link-time search order used to locate additional shared libraries required by shared objects already included in the link. The GNU `ld` manual warns that `--rpath-link` can override a search path compiled into a shared library and thereby select a different library than the runtime linker otherwise would have used. + +Repository-owned `rustflags` or `rustdocflags` can forward these controls through compiler-driver linker arguments. That makes search-path selection part of Browser Session build/runtime provenance rather than an ordinary tuning preference. + +## Primary sources + +- GNU Binutils `ld` manual, command-line options: https://sourceware.org/binutils/docs/ld/Options.html +- rustc code-generation options (`link-arg`, `link-args`): https://doc.rust-lang.org/rustc/codegen-options/index.html + +The GNU `ld` manual states that `-rpath` directories are included in the executable and used by the runtime linker, while `-rpath-link` directories are effective only at link time. It also defines one- or two-dash spellings for multi-letter options and permits `=` or separate operands. + +## Invariant + +Git-owned Cargo configuration must not select ELF runtime or link-time shared-library search directories unless those directories and the artifacts they can resolve are represented by an explicit reviewed provenance contract. + +The canonical direct-linker classifier therefore rejects: + +- `-rpath dir`, `-rpath=dir`, `--rpath dir`, and `--rpath=dir`; +- `-rpath-link dir`, `-rpath-link=dir`, `--rpath-link dir`, and `--rpath-link=dir`; +- the same controls when forwarded through `-Wl,`, `--for-linker=`, `-Xlinker`, rustdoc flags, or rustdoc doctest compiler forwarding. + +`--enable-new-dtags` remains an allowed control because it changes the dynamic-tag form used for an already selected runtime path but does not itself select a directory or external shared object. + +## RED → repair evidence + +Structural RED: `12abc14ec3c98caa5662686857409160bf41a02e` + +The RED fixture covers build/target `rustflags`, build/target `rustdocflags`, doctest forwarding, one- and two-dash multi-letter spellings, joined and split operands, and an unrelated allowed control. + +Minimal canonical repair: `a13f803e8d350abddfaec6dc89378fec438f211d` + +The repair changes only `tests/test_browser_session_cargo_compiler_authority_contract.py`: `_linker_option_selects_runtime_search_path()` is added to the existing direct-linker single writer and reused by all existing forwarding paths. No second Cargo scanner, source-topology walker, path allowlist, or runtime loader policy is introduced. + +## Rejected alternatives + +A pathname allowlist is insufficient. A directory name alone does not bind the concrete shared object eventually selected from that directory, its digest, producer source, ABI, deployment state, symlink resolution, or rollback identity. Allowing `-rpath` while blocking only `-L` would also leave runtime resolution authority outside the reviewed closure; allowing `-rpath-link` would leave link-time transitive shared-library selection outside it. + +## Future exception evidence + +Any approved exception must bind at least: + +- the exact allowed directory/namespace and containment rule; +- immutable identities and cryptographic digests for resolvable shared objects; +- producer source revisions and build provenance; +- target ABI, loader/linker compatibility, and SONAME/DT_NEEDED expectations; +- SBOM/provenance linkage to the consuming release; +- deployment and symlink-resolution evidence; +- reproducible fallback or rollback procedure; +- explicit expiry/revalidation conditions. + +## Residual authority + +Ambient `LD_RUN_PATH`, `LD_LIBRARY_PATH`, `/etc/ld.so.conf`, default linker scripts/search directories, loader cache/state, runner/container image identity, linker distribution/version and configuration, direct CLI arguments, deployed runtime filesystem contents, and externally materialized shared objects remain CI/release supply-chain evidence surfaces. They are not duplicated into the OriginWeave leaf repository policy contract. From b599ea47286d253e432e1235bf1d938f42e28919 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:27:23 +0900 Subject: [PATCH 485/632] test(browser-session): RED GNU ld default library search-path authority --- ..._library_search_path_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_default_library_search_path_authority_contract.py diff --git a/tests/test_browser_session_linker_default_library_search_path_authority_contract.py b/tests/test_browser_session_linker_default_library_search_path_authority_contract.py new file mode 100644 index 000000000..10f3b14bf --- /dev/null +++ b/tests/test_browser_session_linker_default_library_search_path_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerDefaultLibrarySearchPathAuthorityContractTests(unittest.TestCase): + """Keep GNU ld default-library search-path overrides in reviewed provenance.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_compact_y_search_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-Ytools/shadow-libs"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_compact_y_search_path_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=-Ytools/target-libs\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_compact_y_search_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,-Ytools/doc-libs"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_compact_y_search_path_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,-Ytools/doctest-libs"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_relro_toggle_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From d4672136b740575d72e2d9ea64b6b65f1cb09526 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:29:48 +0900 Subject: [PATCH 486/632] fix(browser-session): govern GNU ld default library search path --- ..._browser_session_cargo_compiler_authority_contract.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 3eef8cd55..efa9ab97e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -170,9 +170,11 @@ def _linker_option_selects_runtime_filter_library(argument: str) -> bool: def _linker_option_selects_runtime_search_path(argument: str) -> bool: """Return whether GNU-compatible linker syntax selects runtime/link-time shared-library paths.""" - if argument in {"-rpath", "--rpath", "-rpath-link", "--rpath-link"}: + if argument in {"-rpath", "--rpath", "-rpath-link", "--rpath-link", "-Y"}: return True - return argument.startswith(("-rpath=", "--rpath=", "-rpath-link=", "--rpath-link=")) + if argument.startswith(("-rpath=", "--rpath=", "-rpath-link=", "--rpath-link=")): + return True + return argument.startswith("-Y") and len(argument) > 2 def _linker_option_uses_response_file(argument: str) -> bool: @@ -517,7 +519,6 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) config_paths: set[pathlib.Path] = set() for pattern in (".cargo/config.toml", ".cargo/config"): config_paths.update(root.rglob(pattern)) - for config_path in sorted(config_paths): resolved = config_path.resolve() try: @@ -826,4 +827,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 43ce517e26c203418b872c3105a24cdac2c6e5c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:31:13 +0900 Subject: [PATCH 487/632] chore(browser-session): preserve compiler authority file formatting --- .../test_browser_session_cargo_compiler_authority_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index efa9ab97e..f1cf5f79a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -519,6 +519,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) config_paths: set[pathlib.Path] = set() for pattern in (".cargo/config.toml", ".cargo/config"): config_paths.update(root.rglob(pattern)) + for config_path in sorted(config_paths): resolved = config_path.resolve() try: @@ -827,4 +828,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From b60e27cf5f14b8dbbd68604c63545e9cd8c49b1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:32:53 +0900 Subject: [PATCH 488/632] docs(traceability): record GNU ld default library search-path authority --- ...r-default-library-search-path-authority.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/traceability/browser-session-linker-default-library-search-path-authority.md diff --git a/docs/traceability/browser-session-linker-default-library-search-path-authority.md b/docs/traceability/browser-session-linker-default-library-search-path-authority.md new file mode 100644 index 000000000..411c10970 --- /dev/null +++ b/docs/traceability/browser-session-linker-default-library-search-path-authority.md @@ -0,0 +1,43 @@ +# Browser Session GNU ld default library search-path authority + +## Problem + +OriginWeave treats repository-owned Cargo compiler/linker configuration as reviewed Browser Session build provenance. GNU `ld` accepts `-Y path` to add `path` to its default library search path. GNU `ld` also permits a single-letter option operand to be attached directly to the option letter, so `-Ytools/shadow-libs` is a valid spelling of the same authority change. + +Before this repair, the shared direct-linker classifier covered explicit `-L` / `--library-path`, `-rpath`, `--rpath`, `-rpath-link`, and `--rpath-link` inputs but did not classify compact `-Ypath`. Repository-owned Cargo `rustflags` or `rustdocflags` could therefore forward a compact `-Ypath` through `-Wl,`, `--for-linker=`, or doctest compiler arguments and change default library discovery outside the reviewed Cargo package/source closure. + +## Owner boundary + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler, rustdoc, toolchain, linker execution, and external-input authority. This repair extends that existing classifier; it does not introduce another Cargo topology scanner. + +The supplemental hostile fixture `tests/test_browser_session_linker_default_library_search_path_authority_contract.py` imports the canonical authority contract and verifies only the additional GNU `-Y` behavior. + +## Evidence chain + +- Structural RED: `b599ea47286d253e432e1235bf1d938f42e28919` adds hostile build/target `rustflags`, build `rustdocflags`, and rustdoc doctest-forwarding cases using compact `-Ypath`. The unrelated `-z relro` control remains allowed. +- Minimal causal repair: `d4672136b740575d72e2d9ea64b6b65f1cb09526` extends `_linker_option_selects_runtime_search_path()` so exact `-Y` and compact `-Ypath` are classified by the existing direct-linker authority path. +- Formatting-only follow-up: `43ce517e26c203418b872c3105a24cdac2c6e5c4` restores the pre-existing blank-line and end-of-file formatting changed incidentally by the contents update. It does not change policy semantics. + +This chain is source-semantic evidence. It is not a substitute for exact-head hosted repository/security execution. + +## Decision + +Fail closed when Git-owned Cargo flags forward GNU `-Y` default-library search-path selection. Do not add a path allowlist at this layer. + +A pathname alone does not establish the immutable identity of libraries that will later be discovered through that search root. A future reviewed exception therefore needs, in the same release evidence chain, the selected library artifact identities and digests, producer provenance, containment and symlink policy, exact linker/toolchain compatibility, SBOM/provenance binding, reproducibility evidence, and rollback behavior. + +## Security effect + +The Browser Session build contract no longer permits repository-owned Cargo configuration to change GNU `ld`'s default library lookup through the compact `-Ypath` grammar while leaving the reviewed Cargo package/source closure unchanged. Existing explicit runtime/link-time search-path controls continue to use the same shared classifier. + +The repair deliberately does not prohibit unrelated direct-linker controls that do not select an external search root, such as `-z relro`. + +## Residual authority + +This repository contract does not claim control over environment or direct-CLI injection, ancestor or `$CARGO_HOME` configuration, externally selected linker/toolchain binaries, linker configuration outside the repository, sysroot contents, runner images, or externally restored build/cache artifacts. Those remain CI/release supply-chain provenance surfaces and must be proven by release evidence rather than silently copied into the Browser Session domain. + +## Primary reference + +Free Software Foundation. (2026). *The GNU linker*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld.pdf + +The GNU linker documents `-Y path` as adding `path` to the default library search path for Solaris compatibility. Its command-line grammar also permits arguments to single-letter options either attached directly to the option letter or supplied as the immediately following argument. \ No newline at end of file From 4cd6a1afb762e1d7f2598a67b9fad1261a155800 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 03:35:59 +0900 Subject: [PATCH 489/632] chore(traceability): terminate default search-path record cleanly --- ...wser-session-linker-default-library-search-path-authority.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-linker-default-library-search-path-authority.md b/docs/traceability/browser-session-linker-default-library-search-path-authority.md index 411c10970..63c564b7c 100644 --- a/docs/traceability/browser-session-linker-default-library-search-path-authority.md +++ b/docs/traceability/browser-session-linker-default-library-search-path-authority.md @@ -40,4 +40,4 @@ This repository contract does not claim control over environment or direct-CLI i Free Software Foundation. (2026). *The GNU linker*. GNU Binutils documentation. https://sourceware.org/binutils/docs/ld.pdf -The GNU linker documents `-Y path` as adding `path` to the default library search path for Solaris compatibility. Its command-line grammar also permits arguments to single-letter options either attached directly to the option letter or supplied as the immediately following argument. \ No newline at end of file +The GNU linker documents `-Y path` as adding `path` to the default library search path for Solaris compatibility. Its command-line grammar also permits arguments to single-letter options either attached directly to the option letter or supplied as the immediately following argument. From 763bcadd7a0b7f5ef8cf7e104214ad5d8ec2b5b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 04:01:49 +0900 Subject: [PATCH 490/632] test(browser-session): reject LLD mllvm authority --- ...er_session_lld_mllvm_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_lld_mllvm_authority_contract.py diff --git a/tests/test_browser_session_lld_mllvm_authority_contract.py b/tests/test_browser_session_lld_mllvm_authority_contract.py new file mode 100644 index 000000000..b59f0483e --- /dev/null +++ b/tests/test_browser_session_lld_mllvm_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLldMllvmAuthorityContractTests(unittest.TestCase): + """Keep repository-selected LLD-to-LLVM option forwarding outside the reviewed build TCB.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_lld_mllvm_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--mllvm=-debug-pass=Structure"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_lld_mllvm_split_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,-mllvm,-print-after-all\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_lld_mllvm_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--mllvm=-print-after-all"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_build_arg_lld_mllvm_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--mllvm=-print-after-all"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_typed_linker_control_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From af934e55c3684a108dbf8a80e1f4270f64c8cd85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 04:03:02 +0900 Subject: [PATCH 491/632] fix(browser-session): fail closed on LLD mllvm forwarding --- ..._browser_session_cargo_compiler_authority_contract.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index f1cf5f79a..3bfaf483c 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -109,6 +109,13 @@ def _linker_option_selects_dtlto_executable(argument: str) -> bool: return argument.startswith(("--thinlto-distributor=", "--thinlto-remote-compiler=")) +def _linker_option_forwards_llvm_options(argument: str) -> bool: + """Return whether one LLD option forwards opaque arguments to LLVM option processing.""" + if argument in {"--mllvm", "-mllvm"}: + return True + return argument.startswith(("--mllvm=", "-mllvm=")) + + def _linker_option_selects_script(argument: str) -> bool: """Return whether one linker option selects a script that can introduce link inputs.""" if argument in LINKER_SCRIPT_OPTIONS: @@ -214,6 +221,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ _linker_option_loads_plugin(argument) or _linker_option_selects_error_handler(argument) or _linker_option_selects_dtlto_executable(argument) + or _linker_option_forwards_llvm_options(argument) or _linker_option_selects_script(argument) or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_just_symbols_or_rpath(argument) @@ -238,7 +246,6 @@ def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: return tuple(argument.removeprefix("--for-linker=").split(",")) return () - def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: """Return whether driver arguments can replace tools, extend link inputs, or load linker code.""" direct_arguments: list[str] = [] From a2900e84bb415d2f4c8677a12ee9a1b8f5c671c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 04:03:46 +0900 Subject: [PATCH 492/632] docs(browser-session): trace LLD mllvm authority --- .../browser-session-lld-mllvm-authority.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/traceability/browser-session-lld-mllvm-authority.md diff --git a/docs/traceability/browser-session-lld-mllvm-authority.md b/docs/traceability/browser-session-lld-mllvm-authority.md new file mode 100644 index 000000000..d6abce954 --- /dev/null +++ b/docs/traceability/browser-session-lld-mllvm-authority.md @@ -0,0 +1,65 @@ +# Browser Session LLD `--mllvm` authority + +Status: source-semantic repair on PR #317; hosted executable evidence is still required before GREEN. + +## Problem + +OriginWeave already fails closed when Git-owned Cargo `rustflags` or `rustdocflags` use rustc `-C/--codegen llvm-args=...`. The same trust boundary was incomplete at the ELF linker layer: LLD defines `mllvm` as an option that forwards additional arguments directly to LLVM option processing. Repository-owned Cargo flags could therefore route opaque LLVM options through rustc linker forwarding even though the typed rustc LLVM-option path was rejected. + +The relevant Browser Session boundary is provenance, not whether a particular LLVM option is currently known to be harmful. The forwarded option namespace changes with the exact LLVM/LLD build and is not a stable, reviewed OriginWeave contract. + +## Constraint and owner + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler, rustdoc, linker execution, and external-input authority. Supplemental fixtures consume that classifier; they do not duplicate Cargo production-topology discovery owned by `tests/test_browser_session_trusted_adapter_boundary.py`. + +This repair does not broaden into a blanket linker-option ban. Ordinary modeled linker controls remain allowed when they do not select executable code, external inputs, mutable runtime authority, or an opaque downstream option processor. + +## Decision + +Git-owned Cargo configuration must fail closed when rustc/rustdoc linker forwarding reaches LLD `mllvm` in either GNU-compatible spelling and in split or equals form: + +- `--mllvm ` +- `--mllvm=` +- `-mllvm ` +- `-mllvm=` + +The canonical direct-linker parser classifies those tokens through `_linker_option_forwards_llvm_options()`. Existing `-Wl,`, `--for-linker=`, `-Xlinker`, `-C link-arg=...`, `-C link-args=...`, build/target `rustflags`, build/target `rustdocflags`, and rustdoc doctest compiler forwarding continue to converge on the same authority check. + +## RED → repair evidence + +- Structural RED: `763bcadd7a0b7f5ef8cf7e104214ad5d8ec2b5b1` + - build `rustflags` with `-Wl,--mllvm=...` + - target `rustflags` with split `-Wl,-mllvm,` + - build `rustdocflags` + - rustdoc `--doctest-build-arg` forwarding + - typed linker control `-Wl,-z,relro` remains allowed +- Minimal canonical repair: `af934e55c3684a108dbf8a80e1f4270f64c8cd85` + - adds one LLD-specific classifier to the existing direct-linker authority parser + - reuses all existing Cargo/rustdoc/linker forwarding paths + - does not add a second topology/config scanner + +The repair commit is source-semantic evidence only. It is not a substitute for PR-triggered hosted tests, repository/security checks, current-head review, or the owned 100% documentation/test/edge-case gates. + +## Alternatives rejected + +Allowlisting individual LLVM options was rejected. LLD forwards into LLVM's option processor, whose accepted/debug/experimental surface depends on the exact toolchain build. A local list would become a mutable shadow specification and could silently under-model future LLVM options. + +Path-based approval is not applicable because `mllvm` is an option tunnel rather than a file selector. Treating only currently observed file-consuming LLVM options as dangerous would also confuse present examples with the authority granted by the forwarding mechanism itself. + +## Security and commercial effect + +The build provenance contract now treats direct LLD-to-LLVM option forwarding consistently with rustc `llvm-args`: reviewed Git-owned Cargo configuration cannot introduce an opaque LLVM option channel behind the typed compiler/linker authority model. This reduces the chance that an enterprise release is materially changed by toolchain-internal or experimental LLVM switches that are absent from the reviewed OriginWeave contract and evidence set. + +## Residual authority + +Environment or direct-CLI linker arguments, ancestor or `$CARGO_HOME` configuration, runner image/toolchain identity, exact LLD/LLVM distribution and version, and externally supplied build inputs remain CI/release supply-chain evidence surfaces. Non-LLD linkers and option grammars remain governed only where they are explicitly modeled and evidenced. + +## References + +LLVM Project. (2026). *LLD - The LLVM Linker* (24.0.0git documentation). https://lld.llvm.org/ + +LLVM Project. (2026). *lld/ELF/Options.td* [Source code]. GitHub. https://github.com/llvm/llvm-project/blob/main/lld/ELF/Options.td + +LLVM Project. (2026). *Clang command line argument reference*. https://clang.llvm.org/docs/ClangCommandLineReference.html + +Retrieved September 19, 2026. The primary LLD option table defines `mllvm` as forwarding additional arguments to LLVM option processing; publication/docs freshness does not replace OriginWeave runtime qualification of the exact linker/toolchain used for a release. From cbc4ac285fb680ce8967f8d8cb4a87ea183796cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 04:05:02 +0900 Subject: [PATCH 493/632] style(browser-session): restore compiler contract spacing --- tests/test_browser_session_cargo_compiler_authority_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 3bfaf483c..ab1db728f 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -246,6 +246,7 @@ def _forwarded_linker_arguments(argument: str) -> tuple[str, ...]: return tuple(argument.removeprefix("--for-linker=").split(",")) return () + def _linker_driver_arguments_select_executable(arguments: list[str]) -> bool: """Return whether driver arguments can replace tools, extend link inputs, or load linker code.""" direct_arguments: list[str] = [] From c3416fad876bcd15477521b666f18c3c7a3a5cff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:02:36 +0900 Subject: [PATCH 494/632] test(browser-session): expose linker input-remap authority gap --- ...n_linker_input_remap_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_input_remap_authority_contract.py diff --git a/tests/test_browser_session_linker_input_remap_authority_contract.py b/tests/test_browser_session_linker_input_remap_authority_contract.py new file mode 100644 index 000000000..0d6937152 --- /dev/null +++ b/tests/test_browser_session_linker_input_remap_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerInputRemapAuthorityContractTests(unittest.TestCase): + """Keep linker input-remapping policy from replacing reviewed Browser Session link inputs.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_inline_remap_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--remap-inputs=liboriginweave.a=tools/review-bypass/liboriginweave.a"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_remap_file_equals_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=--remap-inputs-file=tools/review-bypass-remaps.txt\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_single_dash_remap_file_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,-remap-inputs-file=tools/review-bypass-remaps.txt"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_single_dash_inline_remap_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,-remap-inputs=liboriginweave.a=tools/review-bypass/liboriginweave.a"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_input_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 333195e3001357237a3844df34729ff730da99dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:06:48 +0900 Subject: [PATCH 495/632] fix(browser-session): reject linker input remapping authority --- ...rowser_session_cargo_compiler_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index ab1db728f..2d258c797 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -184,6 +184,15 @@ def _linker_option_selects_runtime_search_path(argument: str) -> bool: return argument.startswith("-Y") and len(argument) > 2 +def _linker_option_remaps_inputs(argument: str) -> bool: + """Return whether GNU-compatible linker syntax rewrites reviewed input-file selection.""" + if argument in {"--remap-inputs", "-remap-inputs", "--remap-inputs-file", "-remap-inputs-file"}: + return True + return argument.startswith( + ("--remap-inputs=", "-remap-inputs=", "--remap-inputs-file=", "-remap-inputs-file=") + ) + + def _linker_option_uses_response_file(argument: str) -> bool: """Return whether a direct-linker argument delegates parsing to an opaque response file.""" return argument.startswith("@") @@ -229,6 +238,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_runtime_audit_library(argument) or _linker_option_selects_runtime_filter_library(argument) or _linker_option_selects_runtime_search_path(argument) + or _linker_option_remaps_inputs(argument) or _linker_option_uses_response_file(argument) or _linker_argument_extends_external_inputs(argument) or _linker_argument_is_positional_native_input(argument) From 5e017bf436f7cf1c9264fee9c04530c3a37dd79f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:07:19 +0900 Subject: [PATCH 496/632] docs(traceability): record linker input-remap authority --- ...er-session-linker-input-remap-authority.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/traceability/browser-session-linker-input-remap-authority.md diff --git a/docs/traceability/browser-session-linker-input-remap-authority.md b/docs/traceability/browser-session-linker-input-remap-authority.md new file mode 100644 index 000000000..9b9e31429 --- /dev/null +++ b/docs/traceability/browser-session-linker-input-remap-authority.md @@ -0,0 +1,38 @@ +# Browser Session linker input-remap authority + +Status: Proposed until exact-head hosted repository/security checks and independent review complete. + +## Problem + +GNU `ld` can rewrite the link input graph after Cargo/rustc have selected the reviewed inputs. `--remap-inputs=pattern=filename` substitutes a different filename before the linker opens an input, and `--remap-inputs-file=file` loads the remapping policy from an external file. The remapping also applies to files named by `INPUT` statements in linker scripts. GNU `ld` accepts multi-letter options with either one or two leading dashes, so `-remap-inputs=...` and `-remap-inputs-file=...` are equivalent spellings. + +For OriginWeave this is provenance authority, not a linker-tuning preference. A repository-owned Cargo `rustflags`/`rustdocflags` value could otherwise redirect a reviewed native/library input to an unreviewed object, archive, shared library, or `/dev/null` while leaving the Cargo dependency graph unchanged. + +## Owner boundary + +Browser Session keeps repository-selected compiler/rustdoc/toolchain/linker input authority in `tests/test_browser_session_cargo_compiler_authority_contract.py`. Production package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`; this repair does not duplicate that scanner or move dependency-source authority. + +The canonical direct-linker parser now classifies both inline and file-backed input remapping as authority-extending, including GNU single-dash aliases. Existing `-Wl,`, `--for-linker=`, `-Xlinker`, `-C link-arg`, `-C link-args`, build/target `rustflags`, `rustdocflags`, and rustdoc doctest forwarding continue to converge on the same parser. + +## RED → repair + +- RED `c3416fad876bcd15477521b666f18c3c7a3a5cff` adds hostile fixtures for inline remapping, file-backed remapping, GNU single-dash aliases, build/target flags, rustdoc flags, and doctest compiler forwarding. `-Wl,-z,relro` remains an allowed control. +- Repair `333195e3001357237a3844df34729ff730da99dc` adds `_linker_option_remaps_inputs()` to the existing direct-linker authority classifier and changes no Cargo topology owner. + +## Decision + +Reject repository-selected input remapping by default. + +A pathname allowlist is insufficient because a path does not prove the selected artifact's content identity, producer, toolchain compatibility, symlink containment, or reproducibility. A future exception must bind the remap rule and every selected replacement artifact to immutable digests, producer/source identity, exact linker/toolchain compatibility, SBOM/provenance evidence, containment, deterministic rebuild evidence, expiry/invalidation rules, and rollback. + +## Residual authority + +Environment/direct-CLI linker flags, compiler-driver defaults, runner filesystem contents, linker distribution/version and `PATH`, externally restored build/cache state, sysroot contents, and runtime deployment filesystem identity remain CI/release supply-chain evidence surfaces rather than repository-source exceptions. + +## Evidence + +GNU Binutils documents that `--remap-inputs=pattern=filename` changes input filenames before they are opened, `--remap-inputs-file=file` loads remappings from a file, `/dev/null`/`NUL` can suppress an input, and linker-script `INPUT` references are affected. It also documents that multi-letter options accept one or two leading dashes and may take `=`-joined operands. + +### Reference + +Free Software Foundation. (2026). *GNU linker: Command-line options*. GNU Binutils. https://sourceware.org/binutils/docs/ld/Options.html (retrieved September 19, 2026). From 40f244c914f74e177273576612342ba03245fde9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:10:41 +0900 Subject: [PATCH 497/632] test(browser-session): expose section-ordering script authority gap --- ...ection_ordering_file_authority_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_browser_session_linker_section_ordering_file_authority_contract.py diff --git a/tests/test_browser_session_linker_section_ordering_file_authority_contract.py b/tests/test_browser_session_linker_section_ordering_file_authority_contract.py new file mode 100644 index 000000000..bc735af2b --- /dev/null +++ b/tests/test_browser_session_linker_section_ordering_file_authority_contract.py @@ -0,0 +1,79 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +def _workspace_with_config(config_text: str) -> tuple[tempfile.TemporaryDirectory[str], pathlib.Path]: + directory = tempfile.TemporaryDirectory() + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return directory, root + + +class BrowserSessionLinkerSectionOrderingFileAuthorityContractTests(unittest.TestCase): + """Keep section-ordering scripts outside repository-owned Browser Session linker authority.""" + + def _assert_fails_closed(self, config_text: str, marker: str) -> None: + directory, root = _workspace_with_config(config_text) + self.addCleanup(directory.cleanup) + with self.assertRaisesRegex(AssertionError, marker): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_build_rustflags_section_ordering_file_equals_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--section-ordering-file=tools/review-bypass-order.ld"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_single_dash_section_ordering_file_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=--for-linker=-section-ordering-file=tools/review-bypass-order.ld\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_section_ordering_file_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--section-ordering-file=tools/review-bypass-order.ld"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_forwarded_section_ordering_file_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--section-ordering-file=tools/review-bypass-order.ld"]\n', + "rustdocflags:doctest compiler authority", + ) + + def test_non_script_linker_hardening_option_remains_allowed(self) -> None: + directory, root = _workspace_with_config( + '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' + ) + self.addCleanup(directory.cleanup) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From d33b5f7c107cbd552fd7931f9f5e0b434624b609 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:12:20 +0900 Subject: [PATCH 498/632] fix(browser-session): reject section-ordering script authority --- ...r_session_cargo_compiler_authority_contract.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 2d258c797..d707d9517 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -118,12 +118,23 @@ def _linker_option_forwards_llvm_options(argument: str) -> bool: def _linker_option_selects_script(argument: str) -> bool: """Return whether one linker option selects a script that can introduce link inputs.""" - if argument in LINKER_SCRIPT_OPTIONS: + if argument in LINKER_SCRIPT_OPTIONS or argument in { + "--section-ordering-file", + "-section-ordering-file", + }: return True return ( (argument.startswith("-T") and len(argument) > 2) or (argument.startswith("-dT") and len(argument) > 3) - or argument.startswith(("--script=", "--default-script=", "--mri-script=")) + or argument.startswith( + ( + "--script=", + "--default-script=", + "--mri-script=", + "--section-ordering-file=", + "-section-ordering-file=", + ) + ) ) From 84c4696441409f0f4d48e8d0480d3a5b38b27cfe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 06:12:38 +0900 Subject: [PATCH 499/632] docs(traceability): record section-ordering script authority --- ...-linker-section-ordering-file-authority.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/traceability/browser-session-linker-section-ordering-file-authority.md diff --git a/docs/traceability/browser-session-linker-section-ordering-file-authority.md b/docs/traceability/browser-session-linker-section-ordering-file-authority.md new file mode 100644 index 000000000..576d97604 --- /dev/null +++ b/docs/traceability/browser-session-linker-section-ordering-file-authority.md @@ -0,0 +1,38 @@ +# Browser Session linker section-ordering-file authority + +Status: Proposed until exact-head hosted repository/security checks and independent review complete. + +## Problem + +GNU `ld --section-ordering-file=script` reads an external script using `SECTIONS` syntax and augments the current linker script by mapping input sections to the start of existing output sections. The GNU linker documentation lists it as another way to specify linker scripts. Because GNU multi-letter options accept one or two leading dashes, `-section-ordering-file=script` is an equivalent spelling. + +For OriginWeave this is linker-script provenance authority. A repository-owned Cargo `rustflags` or `rustdocflags` value can otherwise load an unreviewed section-ordering script after Cargo/rustc select the reviewed source/dependency closure and change which input sections are mapped and ordered in the output artifact. + +## Owner boundary + +Browser Session keeps repository-selected compiler/rustdoc/toolchain/linker input authority in `tests/test_browser_session_cargo_compiler_authority_contract.py`. Production package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`; this repair does not duplicate that scanner or move dependency-source authority. + +`--section-ordering-file` is classified by the existing linker-script authority helper so the current `-Wl,`, `--for-linker=`, `-Xlinker`, `-C link-arg`, `-C link-args`, build/target `rustflags`, `rustdocflags`, and rustdoc doctest forwarding all converge on one policy path. + +## RED → repair + +- RED `40f244c914f74e177273576612342ba03245fde9` covers build/target `rustflags`, build `rustdocflags`, rustdoc doctest compiler forwarding, GNU double-dash and single-dash forms, and an allowed `-Wl,-z,relro` control. +- Repair `d33b5f7c107cbd552fd7931f9f5e0b434624b609` extends only `_linker_option_selects_script()` in the canonical direct-linker authority contract. No second scanner or package/source-topology owner is introduced. + +## Decision + +Reject repository-selected section-ordering scripts by default. + +A pathname allowlist is insufficient because a path does not prove script content identity, producer, symlink containment, linker compatibility, or reproducibility. Any future exception must bind the script to an immutable digest, reviewed producer/source identity, exact linker/toolchain compatibility, containment, SBOM/provenance evidence, deterministic rebuild evidence, expiry/invalidation rules, and rollback. + +## Residual authority + +Environment/direct-CLI linker flags, compiler-driver defaults, runner filesystem contents, linker distribution/version and `PATH`, externally restored build/cache state, sysroot contents, and externally materialized linker scripts remain CI/release supply-chain evidence surfaces rather than repository-source exceptions. + +## Evidence + +GNU Binutils documents `--section-ordering-file=script` as an external file that uses `SECTIONS` syntax to augment the current linker script and map input sections to output sections. The same options manual documents that multi-letter options accept one or two leading dashes and may use `=`-joined operands. + +### Reference + +Free Software Foundation. (2026). *GNU linker: Command-line options*. GNU Binutils. https://sourceware.org/binutils/docs/ld/Options.html (retrieved September 19, 2026). From 04a6079c025d7a1db83c8ee302f957f9fcc93096 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:04:54 +0900 Subject: [PATCH 500/632] test(browser-session): expose LLD layout profile input authority --- ...layout_profile_input_authority_contract.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_browser_session_lld_layout_profile_input_authority_contract.py diff --git a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py new file mode 100644 index 000000000..4396326ac --- /dev/null +++ b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py @@ -0,0 +1,43 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldLayoutProfileInputAuthorityContractTests(unittest.TestCase): + """Keep repository-selected LLD layout/profile files inside reviewed provenance.""" + + def test_lld_layout_and_profile_file_inputs_fail_closed(self) -> None: + hostile = ( + "--call-graph-ordering-file=tools/callgraph.order", + "--irpgo-profile=tools/startup.profdata", + "--symbol-ordering-file=tools/symbols.order", + "--lto-sample-profile=tools/sample.prof", + ) + for linker_option in hostile: + with self.subTest(linker_option=linker_option): + rustflags = ["-C", f"link-arg=-Wl,{linker_option}"] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_lld_profile_file(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--lto-sample-profile=tools/sample.prof", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_typed_linker_control_remains_allowed(self) -> None: + self.assertFalse(authority._flags_select_linker(["-C", "link-arg=-Wl,-z,relro"])) + + +if __name__ == "__main__": + unittest.main() From 04da16344e413cdd966d6104f75c27b5603750ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:06:30 +0900 Subject: [PATCH 501/632] fix(browser-session): reject LLD layout profile inputs --- ...ession_cargo_compiler_authority_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index d707d9517..11ebdfdfd 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -33,6 +33,14 @@ "--export-dynamic-symbol-list", } ) +LINKER_LAYOUT_PROFILE_FILE_OPTIONS = frozenset( + { + "--call-graph-ordering-file", + "--irpgo-profile", + "--symbol-ordering-file", + "--lto-sample-profile", + } +) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) LINKER_PLUGIN_LTO_BOOLEAN_VALUES = frozenset( {"y", "yes", "on", "true", "n", "no", "off", "false"} @@ -145,6 +153,15 @@ def _linker_option_selects_symbol_policy_file(argument: str) -> bool: return argument.startswith(tuple(f"{option}=" for option in LINKER_SYMBOL_POLICY_FILE_OPTIONS)) +def _linker_option_selects_layout_profile_file(argument: str) -> bool: + """Return whether an LLD option consumes an external layout or profile file.""" + if argument in LINKER_LAYOUT_PROFILE_FILE_OPTIONS: + return True + return argument.startswith( + tuple(f"{option}=" for option in LINKER_LAYOUT_PROFILE_FILE_OPTIONS) + ) + + def _linker_option_selects_just_symbols_or_rpath(argument: str) -> bool: """Return whether GNU-compatible linker syntax selects an external -R/just-symbols path.""" if argument in {"-R", "--just-symbols"}: @@ -244,6 +261,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_forwards_llvm_options(argument) or _linker_option_selects_script(argument) or _linker_option_selects_symbol_policy_file(argument) + or _linker_option_selects_layout_profile_file(argument) or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_controls_runtime_loader(argument) or _linker_option_selects_runtime_audit_library(argument) From 565456357d5b7f1a52f5b58d8205f170d4bb39ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:06:59 +0900 Subject: [PATCH 502/632] test(browser-session): cover LLD callgraph alias spelling --- ...rowser_session_lld_layout_profile_input_authority_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py index 4396326ac..a31ef647d 100644 --- a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py +++ b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py @@ -19,6 +19,7 @@ class BrowserSessionLldLayoutProfileInputAuthorityContractTests(unittest.TestCas def test_lld_layout_and_profile_file_inputs_fail_closed(self) -> None: hostile = ( "--call-graph-ordering-file=tools/callgraph.order", + "-call-graph-ordering-file=tools/callgraph.order", "--irpgo-profile=tools/startup.profdata", "--symbol-ordering-file=tools/symbols.order", "--lto-sample-profile=tools/sample.prof", From a5c63c7fbe0cef14ee47cd184b87d001c8b296b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:08:23 +0900 Subject: [PATCH 503/632] fix(browser-session): cover LLD callgraph alias spelling --- tests/test_browser_session_cargo_compiler_authority_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 11ebdfdfd..52a6c85ee 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -36,6 +36,7 @@ LINKER_LAYOUT_PROFILE_FILE_OPTIONS = frozenset( { "--call-graph-ordering-file", + "-call-graph-ordering-file", "--irpgo-profile", "--symbol-ordering-file", "--lto-sample-profile", From 3797481cdcd241c94dc1bfe5bad0d32d54be0957 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:09:50 +0900 Subject: [PATCH 504/632] test(browser-session): expose LLD sample profile alias authority --- ...owser_session_lld_layout_profile_input_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py index a31ef647d..e1e5d1fe8 100644 --- a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py +++ b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py @@ -23,6 +23,8 @@ def test_lld_layout_and_profile_file_inputs_fail_closed(self) -> None: "--irpgo-profile=tools/startup.profdata", "--symbol-ordering-file=tools/symbols.order", "--lto-sample-profile=tools/sample.prof", + "--plugin-opt=sample-profile=tools/sample.prof", + "-plugin-opt=sample-profile=tools/sample.prof", ) for linker_option in hostile: with self.subTest(linker_option=linker_option): From d6dc93f87afb986be88537a8f7a174126c5656e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:11:13 +0900 Subject: [PATCH 505/632] fix(browser-session): reject LLD sample profile aliases --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 52a6c85ee..30aacb5f2 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -40,6 +40,8 @@ "--irpgo-profile", "--symbol-ordering-file", "--lto-sample-profile", + "--plugin-opt=sample-profile", + "-plugin-opt=sample-profile", } ) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) From b6e56945a68a06cc3de52f2626f3d43851ae5af3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:11:39 +0900 Subject: [PATCH 506/632] docs(traceability): record LLD layout profile input authority --- ...sion-lld-layout-profile-input-authority.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/traceability/browser-session-lld-layout-profile-input-authority.md diff --git a/docs/traceability/browser-session-lld-layout-profile-input-authority.md b/docs/traceability/browser-session-lld-layout-profile-input-authority.md new file mode 100644 index 000000000..95d69aa9f --- /dev/null +++ b/docs/traceability/browser-session-lld-layout-profile-input-authority.md @@ -0,0 +1,62 @@ +# Browser Session LLD layout/profile input authority + +Status: Proposed until exact-head hosted repository/security checks and independent review complete. + +## Problem + +LLD can read repository-selected external files after Cargo/rustc have already selected the reviewed Rust source and dependency closure. Four ELF linker options materially affect output layout or LTO decisions: + +- `--call-graph-ordering-file=` lays out sections using a supplied call graph; +- `--irpgo-profile=` reads a temporary IRPGO profile for startup/profile-guided ordering; +- `--symbol-ordering-file=` lays out sections according to a supplied symbol-order file; +- `--lto-sample-profile=` reads an LTO sample profile. LLD also exposes the GNU-plugin-compatible `-plugin-opt=sample-profile=` / `--plugin-opt=sample-profile=` alias. + +Before this repair, equals-joined spellings such as `--symbol-ordering-file=tools/symbols.order` were not classified by the direct-linker authority contract. Because the file name remains inside the same option token, the existing positional-native-input fallback never saw a separate path token. Git-owned Cargo `rustflags`, `rustdocflags`, or rustdoc doctest compiler forwarding could therefore select unreviewed layout/profile material without tripping the Browser Session provenance boundary. + +## Authoritative option grammar + +LLVM LLD's ELF option table distinguishes the accepted spellings: + +- `call-graph-ordering-file` uses the `Eq` multiclass. `Eq` accepts both one- and two-dash multi-letter spellings and supports separated and `=`-joined operands. Therefore both `-call-graph-ordering-file=` and `--call-graph-ordering-file=` are modeled. +- `irpgo-profile` and `symbol-ordering-file` use `EEq`, whose spelling is double-dash only and supports separated and `=`-joined operands. +- `lto-sample-profile=` uses `JJ`, a double-dash joined option. +- `plugin-opt=sample-profile=` is an alias of `lto-sample-profile` using `J`; `J` accepts both one- and two-dash spellings. + +The contract does not invent single-dash aliases for the `EEq` or `JJ` options. + +## Owner boundary + +Browser Session keeps repository-selected compiler/rustdoc/toolchain/linker execution and input authority in `tests/test_browser_session_cargo_compiler_authority_contract.py`. Production package/source topology and dependency-source authority remain owned by `tests/test_browser_session_trusted_adapter_boundary.py`. + +This repair extends the existing direct-linker classifier only. Existing `-Wl,`, `--for-linker=`, `-Xlinker`, `-C link-arg`, `-C link-args`, build/target `rustflags`, build/target `rustdocflags`, and rustdoc doctest forwarding continue to converge on the same owner path. + +## RED → repair + +- RED `04a6079c025d7a1db83c8ee302f957f9fcc93096`: introduces hostile equals-joined LLD layout/profile file cases plus rustdoc doctest forwarding and an allowed `-Wl,-z,relro` control. +- Repair `04da16344e413cdd966d6104f75c27b5603750ea`: adds the four file-selecting option families to the canonical direct-linker authority classifier. +- Grammar correction RED `565456357d5b7f1a52f5b58d8205f170d4bb39ff`: covers the valid single-dash `-call-graph-ordering-file=` spelling. +- Grammar correction repair `a5c63c7fbe0cef14ee47cd184b87d001c8b296b1`: adds that exact alias without broadening unrelated option matching. +- Alias RED `3797481cdcd241c94dc1bfe5bad0d32d54be0957`: covers both `-plugin-opt=sample-profile=` and `--plugin-opt=sample-profile=`. +- Alias repair `d6dc93f87afb986be88537a8f7a174126c5656e5`: routes those LLD sample-profile aliases through the same layout/profile input classifier. + +These are source-semantic RED/repair contracts. They are not hosted executable GREEN until the exact protected evidence lanes actually run. + +## Decision + +Repository-selected LLD layout/profile files are rejected by default because they can alter code/data placement or LTO decisions while living outside the reviewed Cargo source/dependency closure. + +A pathname allowlist is insufficient. A future exception must bind the input to an immutable content digest, reviewed producer/source identity, exact linker/toolchain compatibility, repository/runner containment including symlink resolution, SBOM/provenance evidence, deterministic rebuild evidence, expiry/invalidation rules, and rollback. Profile data that may encode production execution behavior also requires purpose and data-retention review before becoming a governed build input. + +## Residual authority + +Environment/direct-CLI linker arguments, compiler-driver defaults, toolchain-distributed profiles, runner filesystem contents, externally restored build/cache state, linker distribution/version and `PATH`, and artifacts materialized outside Git remain CI/release supply-chain evidence surfaces. They are not converted into repository-source exceptions by this contract. + +## Evidence + +LLVM's ELF `Options.td` defines `call-graph-ordering-file`, `irpgo-profile`, `symbol-ordering-file`, `lto-sample-profile`, the `plugin-opt=sample-profile=` alias, and the `Eq`/`EEq`/`J`/`JJ` spelling grammar. `lld/ELF/DriverUtils.cpp` treats these option values as paths when generating reproduction material, corroborating that they are external file inputs rather than scalar tuning values. + +### References + +LLVM Project. (2026). *LLD ELF option definitions* (`lld/ELF/Options.td`, commit `34eeb2320ff2b991ddb3e3511bbe01ba14478d93`). https://github.com/llvm/llvm-project/blob/34eeb2320ff2b991ddb3e3511bbe01ba14478d93/lld/ELF/Options.td (retrieved September 19, 2026). + +LLVM Project. (2026). *LLD ELF driver reproduction utilities* (`lld/ELF/DriverUtils.cpp`, commit `34eeb2320ff2b991ddb3e3511bbe01ba14478d93`). https://github.com/llvm/llvm-project/blob/34eeb2320ff2b991ddb3e3511bbe01ba14478d93/lld/ELF/DriverUtils.cpp (retrieved September 19, 2026). From 934f695cbb0d228615837fbcbd22401665565d8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 08:05:39 +0900 Subject: [PATCH 507/632] test(security): reproduce LLD CMSE import library provenance bypass --- ..._cmse_import_library_authority_contract.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/test_browser_session_lld_cmse_import_library_authority_contract.py diff --git a/tests/test_browser_session_lld_cmse_import_library_authority_contract.py b/tests/test_browser_session_lld_cmse_import_library_authority_contract.py new file mode 100644 index 000000000..b9aca972a --- /dev/null +++ b/tests/test_browser_session_lld_cmse_import_library_authority_contract.py @@ -0,0 +1,49 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldCmseImportLibraryAuthorityContractTests(unittest.TestCase): + """Keep repository-selected ARM CMSE import libraries inside reviewed provenance.""" + + def test_joined_cmse_input_import_library_fails_closed(self) -> None: + rustflags = [ + "-C", + "link-arg=-Wl,--in-implib=tools/previous-secure-image.lib", + ] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_split_cmse_input_import_library_fails_closed(self) -> None: + rustflags = [ + "-C", + "link-args=-Wl,--in-implib tools/previous-secure-image.lib", + ] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_cmse_import_library(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--in-implib=tools/previous-secure-image.lib", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_cmse_output_import_library_remains_output_only(self) -> None: + self.assertFalse( + authority._flags_select_linker( + ["-C", "link-arg=-Wl,--out-implib=artifacts/secure-image.lib"] + ) + ) + + +if __name__ == "__main__": + unittest.main() From 34cf80120d5e61b183fbc0dbfb0d09b4aef8befb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 08:06:57 +0900 Subject: [PATCH 508/632] test(security): keep CMSE import-library finding non-red until canonical repair --- ..._cmse_import_library_authority_contract.py | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 tests/test_browser_session_lld_cmse_import_library_authority_contract.py diff --git a/tests/test_browser_session_lld_cmse_import_library_authority_contract.py b/tests/test_browser_session_lld_cmse_import_library_authority_contract.py deleted file mode 100644 index b9aca972a..000000000 --- a/tests/test_browser_session_lld_cmse_import_library_authority_contract.py +++ /dev/null @@ -1,49 +0,0 @@ -import importlib.util -import pathlib -import unittest - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" - -spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) -if spec is None or spec.loader is None: - raise RuntimeError("unable to load Browser Session compiler authority contract") -authority = importlib.util.module_from_spec(spec) -spec.loader.exec_module(authority) - - -class BrowserSessionLldCmseImportLibraryAuthorityContractTests(unittest.TestCase): - """Keep repository-selected ARM CMSE import libraries inside reviewed provenance.""" - - def test_joined_cmse_input_import_library_fails_closed(self) -> None: - rustflags = [ - "-C", - "link-arg=-Wl,--in-implib=tools/previous-secure-image.lib", - ] - self.assertTrue(authority._flags_select_linker(rustflags)) - - def test_split_cmse_input_import_library_fails_closed(self) -> None: - rustflags = [ - "-C", - "link-args=-Wl,--in-implib tools/previous-secure-image.lib", - ] - self.assertTrue(authority._flags_select_linker(rustflags)) - - def test_rustdoc_doctest_forwarding_cannot_select_cmse_import_library(self) -> None: - rustdocflags = [ - "--doctest-build-arg=-C", - "--doctest-build-arg=link-arg=-Wl,--in-implib=tools/previous-secure-image.lib", - ] - self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) - - def test_cmse_output_import_library_remains_output_only(self) -> None: - self.assertFalse( - authority._flags_select_linker( - ["-C", "link-arg=-Wl,--out-implib=artifacts/secure-image.lib"] - ) - ) - - -if __name__ == "__main__": - unittest.main() From 65511f47d355a9c68ed669679bc406bd9230ab5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 08:07:13 +0900 Subject: [PATCH 509/632] docs(security): record LLD CMSE import-library provenance gap --- ...ssion-lld-cmse-import-library-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-lld-cmse-import-library-authority.md diff --git a/docs/traceability/browser-session-lld-cmse-import-library-authority.md b/docs/traceability/browser-session-lld-cmse-import-library-authority.md new file mode 100644 index 000000000..04b508a8d --- /dev/null +++ b/docs/traceability/browser-session-lld-cmse-import-library-authority.md @@ -0,0 +1,47 @@ +# Browser Session LLD CMSE import-library authority + +Status: Open repair finding + +## Problem + +LLVM LLD's ELF driver defines `--in-implib` as an ARM CMSE input selector. The option reads an existing CMSE secure-code import library from a previous program revision so LLD can preserve secure gateway entry-function addresses in a new CMSE import library or secure image. + +On the reviewed Browser Session Cargo authority path, the split spelling `--in-implib FILE` is conservatively caught because `FILE` becomes an unconsumed positional linker input. The `EEq` joined spelling `--in-implib=FILE` keeps the external artifact path inside the option token. The current direct-linker classifier does not model that option, so the joined spelling is not yet rejected by the canonical authority predicate. + +This is an input-provenance gap, not a general ARM CMSE ban. `--out-implib=FILE` names an output destination and is not equivalent to the input selector. + +## Primary evidence + +- LLVM LLD `lld/ELF/Options.td`: `in_implib` is `EEq<"in-implib", ...>` and is documented as reading an existing CMSE secure-code import library and preserving entry-function addresses in the resulting library/image. +- LLVM LLD `lld/ELF/Arch/ARM.cpp`: the CMSE import library is an ELF object with a symbol table; `--in-implib` selects an input import library from a previous revision of the program. +- LLVM LLD ARM tests exercise `--in-implib=lib.o`, reject multiple input import libraries, reject use without `--cmse-implib`, and reject the option on non-ARM targets. + +Upstream references: + +- +- +- + +## Owner boundary + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker input authority. No parallel Cargo topology/config scanner is introduced here. + +The repair belongs in the existing direct-linker classifier and must be consumed through the existing `rustflags`, `rustdocflags`, and doctest compiler-forwarding paths. + +## Required repair + +1. Add a hostile contract for the joined `--in-implib=...` spelling and retain `--out-implib=...` as an output-only control. +2. Add one bounded predicate for split/joined `--in-implib` and consume it from `_direct_linker_arguments_extend_authority()`. +3. Prove the exact repaired head with the focused contract before treating this document as closed. + +The earlier attempted RED fixture was removed rather than leaving the branch knowingly red before the canonical single-writer repair could be applied atomically. + +## Rejected alternatives + +- Path allowlists are insufficient: a trusted-looking pathname does not prove the selected import library's content, producer, toolchain compatibility, symlink containment, or reproducibility. +- Blocking every CMSE option would conflate external input authority with output configuration and would reject `--out-implib` without evidence. +- A supplemental scanner would duplicate the canonical direct-linker authority owner. + +## Future exception evidence + +Any future decision to permit repository-selected CMSE input import libraries must prove the selected artifact digest, producer provenance, exact LLD/toolchain compatibility, repository/approved-artifact containment, SBOM/provenance inclusion, reproducibility, and rollback/invalidation behavior in the same reviewed delta. From 411ec418c73b89a0f3923af0a16a214faafe0000 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 09:03:40 +0900 Subject: [PATCH 510/632] test(browser-session): expose LLD CMSE import-library authority gap --- ..._cmse_import_library_authority_contract.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_browser_session_lld_cmse_import_library_authority_contract.py diff --git a/tests/test_browser_session_lld_cmse_import_library_authority_contract.py b/tests/test_browser_session_lld_cmse_import_library_authority_contract.py new file mode 100644 index 000000000..df6de8b36 --- /dev/null +++ b/tests/test_browser_session_lld_cmse_import_library_authority_contract.py @@ -0,0 +1,36 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldCmseImportLibraryAuthorityContractTests(unittest.TestCase): + """Keep pre-existing LLD CMSE import libraries inside reviewed linker provenance.""" + + def test_joined_in_implib_fails_closed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,--in-implib=tools/previous-cmse-import.o"] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_in_implib(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--in-implib=tools/previous-cmse-import.o", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_out_implib_output_path_remains_allowed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,--out-implib=target/cmse-import.o"] + self.assertFalse(authority._flags_select_linker(rustflags)) + + +if __name__ == "__main__": + unittest.main() From 28ffd6fc4784b25e2d48f4d16b0de0acc4324c47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 09:04:50 +0900 Subject: [PATCH 511/632] fix(browser-session): classify LLD CMSE import-library input authority --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 30aacb5f2..e71ccfadb 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -165,6 +165,11 @@ def _linker_option_selects_layout_profile_file(argument: str) -> bool: ) +def _linker_option_selects_cmse_import_library(argument: str) -> bool: + """Return whether LLD consumes an existing CMSE secure-code import library.""" + return argument == "--in-implib" or argument.startswith("--in-implib=") + + def _linker_option_selects_just_symbols_or_rpath(argument: str) -> bool: """Return whether GNU-compatible linker syntax selects an external -R/just-symbols path.""" if argument in {"-R", "--just-symbols"}: @@ -265,6 +270,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_script(argument) or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_layout_profile_file(argument) + or _linker_option_selects_cmse_import_library(argument) or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_controls_runtime_loader(argument) or _linker_option_selects_runtime_audit_library(argument) From ab289d08107b7c176c9f60b2a2f3030584f989bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 09:05:28 +0900 Subject: [PATCH 512/632] docs(browser-session): close CMSE import-library source repair trace --- ...ssion-lld-cmse-import-library-authority.md | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/traceability/browser-session-lld-cmse-import-library-authority.md b/docs/traceability/browser-session-lld-cmse-import-library-authority.md index 04b508a8d..69e815937 100644 --- a/docs/traceability/browser-session-lld-cmse-import-library-authority.md +++ b/docs/traceability/browser-session-lld-cmse-import-library-authority.md @@ -1,40 +1,43 @@ # Browser Session LLD CMSE import-library authority -Status: Open repair finding +Status: Source repair implemented; hosted proof pending ## Problem LLVM LLD's ELF driver defines `--in-implib` as an ARM CMSE input selector. The option reads an existing CMSE secure-code import library from a previous program revision so LLD can preserve secure gateway entry-function addresses in a new CMSE import library or secure image. -On the reviewed Browser Session Cargo authority path, the split spelling `--in-implib FILE` is conservatively caught because `FILE` becomes an unconsumed positional linker input. The `EEq` joined spelling `--in-implib=FILE` keeps the external artifact path inside the option token. The current direct-linker classifier does not model that option, so the joined spelling is not yet rejected by the canonical authority predicate. +On the reviewed Browser Session Cargo authority path, the split spelling `--in-implib FILE` is conservatively caught because `FILE` becomes an unconsumed positional linker input. The `EEq` joined spelling `--in-implib=FILE` kept the external artifact path inside the option token and previously bypassed the canonical direct-linker authority predicate. This is an input-provenance gap, not a general ARM CMSE ban. `--out-implib=FILE` names an output destination and is not equivalent to the input selector. ## Primary evidence -- LLVM LLD `lld/ELF/Options.td`: `in_implib` is `EEq<"in-implib", ...>` and is documented as reading an existing CMSE secure-code import library and preserving entry-function addresses in the resulting library/image. -- LLVM LLD `lld/ELF/Arch/ARM.cpp`: the CMSE import library is an ELF object with a symbol table; `--in-implib` selects an input import library from a previous revision of the program. -- LLVM LLD ARM tests exercise `--in-implib=lib.o`, reject multiple input import libraries, reject use without `--cmse-implib`, and reject the option on non-ARM targets. +LLVM upstream main at `f8f4816496f6126f371350819d48017d1c330b56` provides the current primary evidence used for this repair: + +- `lld/ELF/Options.td`: `in_implib` is `EEq<"in-implib", ...>` and is documented as reading an existing CMSE secure-code import library and preserving entry-function addresses in the resulting library/image. +- `lld/ELF/Options.td`: `out_implib` is separately documented as outputting the CMSE secure-code import library to a file. +- `lld/ELF/Driver.cpp`: `OPT_in_implib` populates the CMSE input-library argument and is rejected on unsupported targets. +- LLD ARM tests exercise the CMSE input option and its validation rules. Upstream references: -- -- -- +- +- +- ## Owner boundary -`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker input authority. No parallel Cargo topology/config scanner is introduced here. +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker input authority. No parallel Cargo topology/config scanner was introduced. -The repair belongs in the existing direct-linker classifier and must be consumed through the existing `rustflags`, `rustdocflags`, and doctest compiler-forwarding paths. +The repair is consumed through the existing direct-linker classifier, so `rustflags`, `rustdocflags`, and rustdoc doctest compiler forwarding continue to share one authority decision path. -## Required repair +## RED → repair -1. Add a hostile contract for the joined `--in-implib=...` spelling and retain `--out-implib=...` as an output-only control. -2. Add one bounded predicate for split/joined `--in-implib` and consume it from `_direct_linker_arguments_extend_authority()`. -3. Prove the exact repaired head with the focused contract before treating this document as closed. +- Structural RED: `411ec418c73b89a0f3923af0a16a214faafe0000` adds `tests/test_browser_session_lld_cmse_import_library_authority_contract.py`. It requires joined `--in-implib=...` to fail closed through the canonical authority helper, exercises rustdoc doctest compiler forwarding, and keeps output-only `--out-implib=...` as an allowed control. +- Minimal causal repair: `28ffd6fc4784b25e2d48f4d16b0de0acc4324c47` adds `_linker_option_selects_cmse_import_library()` and consumes it from `_direct_linker_arguments_extend_authority()`. The repair changes the canonical authority file by six added lines and no deletions; no unrelated rewrite or duplicate scanner was introduced. +- Exact compare from predecessor `65511f47d355a9c68ed669679bc406bd9230ab5f` to repair head is two commits, two files: the 36-line hostile contract plus the six-line canonical classifier repair. -The earlier attempted RED fixture was removed rather than leaving the branch knowingly red before the canonical single-writer repair could be applied atomically. +No pull-request-triggered hosted workflow exists yet for repair head `28ffd6fc4784b25e2d48f4d16b0de0acc4324c47`, so this document does not claim hosted executable GREEN, repository/security GREEN, whole-PR review closure, or 100% quality-gate closure. ## Rejected alternatives From 9b23b72fe3b1649983bae8eb1269bdb64a6871c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 10:38:24 +0900 Subject: [PATCH 513/632] test(browser-session): reject LLD context-sensitive profile inputs --- ...sion_lld_layout_profile_input_authority_contract.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py index e1e5d1fe8..03f2a23e0 100644 --- a/tests/test_browser_session_lld_layout_profile_input_authority_contract.py +++ b/tests/test_browser_session_lld_layout_profile_input_authority_contract.py @@ -25,6 +25,9 @@ def test_lld_layout_and_profile_file_inputs_fail_closed(self) -> None: "--lto-sample-profile=tools/sample.prof", "--plugin-opt=sample-profile=tools/sample.prof", "-plugin-opt=sample-profile=tools/sample.prof", + "--lto-cs-profile-file=tools/context-sensitive.profdata", + "--plugin-opt=cs-profile-path=tools/context-sensitive.profdata", + "-plugin-opt=cs-profile-path=tools/context-sensitive.profdata", ) for linker_option in hostile: with self.subTest(linker_option=linker_option): @@ -38,6 +41,13 @@ def test_rustdoc_doctest_forwarding_cannot_select_lld_profile_file(self) -> None ] self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + def test_rustdoc_doctest_forwarding_cannot_select_lld_context_sensitive_profile_file(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--lto-cs-profile-file=tools/context-sensitive.profdata", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + def test_typed_linker_control_remains_allowed(self) -> None: self.assertFalse(authority._flags_select_linker(["-C", "link-arg=-Wl,-z,relro"])) From ea4cfea24b654178360ac029f6ae61f6b8f6c7eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 10:40:18 +0900 Subject: [PATCH 514/632] fix(browser-session): fail closed on LLD CS profile inputs --- .../test_browser_session_cargo_compiler_authority_contract.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index e71ccfadb..5646ee64a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -42,6 +42,9 @@ "--lto-sample-profile", "--plugin-opt=sample-profile", "-plugin-opt=sample-profile", + "--lto-cs-profile-file", + "--plugin-opt=cs-profile-path", + "-plugin-opt=cs-profile-path", } ) LINKER_OPTIONS_WITH_SEPARATE_OPERAND = frozenset({"-z"}) From 2d0fc8ece1ebb882237b2ff454a707c91225cf5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 10:40:57 +0900 Subject: [PATCH 515/632] docs(browser-session): trace LLD context-sensitive profile authority --- ...ession-lld-layout-profile-input-authority.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-lld-layout-profile-input-authority.md b/docs/traceability/browser-session-lld-layout-profile-input-authority.md index 95d69aa9f..f8d2930f0 100644 --- a/docs/traceability/browser-session-lld-layout-profile-input-authority.md +++ b/docs/traceability/browser-session-lld-layout-profile-input-authority.md @@ -4,14 +4,15 @@ Status: Proposed until exact-head hosted repository/security checks and independ ## Problem -LLD can read repository-selected external files after Cargo/rustc have already selected the reviewed Rust source and dependency closure. Four ELF linker options materially affect output layout or LTO decisions: +LLD can read repository-selected external files after Cargo/rustc have already selected the reviewed Rust source and dependency closure. Five ELF linker option families materially affect output layout or LTO decisions: - `--call-graph-ordering-file=` lays out sections using a supplied call graph; - `--irpgo-profile=` reads a temporary IRPGO profile for startup/profile-guided ordering; - `--symbol-ordering-file=` lays out sections according to a supplied symbol-order file; -- `--lto-sample-profile=` reads an LTO sample profile. LLD also exposes the GNU-plugin-compatible `-plugin-opt=sample-profile=` / `--plugin-opt=sample-profile=` alias. +- `--lto-sample-profile=` reads an LTO sample profile. LLD also exposes the GNU-plugin-compatible `-plugin-opt=sample-profile=` / `--plugin-opt=sample-profile=` alias; +- `--lto-cs-profile-file=` selects a context-sensitive PGO profile for LTO. LLD also exposes `-plugin-opt=cs-profile-path=` / `--plugin-opt=cs-profile-path=` as aliases. -Before this repair, equals-joined spellings such as `--symbol-ordering-file=tools/symbols.order` were not classified by the direct-linker authority contract. Because the file name remains inside the same option token, the existing positional-native-input fallback never saw a separate path token. Git-owned Cargo `rustflags`, `rustdocflags`, or rustdoc doctest compiler forwarding could therefore select unreviewed layout/profile material without tripping the Browser Session provenance boundary. +Before these repairs, equals-joined spellings such as `--symbol-ordering-file=tools/symbols.order` and `--lto-cs-profile-file=tools/context-sensitive.profdata` were not classified by the direct-linker authority contract. Because the file name remains inside the same option token, the existing positional-native-input fallback never saw a separate path token. Git-owned Cargo `rustflags`, `rustdocflags`, or rustdoc doctest compiler forwarding could therefore select unreviewed layout/profile material without tripping the Browser Session provenance boundary. ## Authoritative option grammar @@ -21,6 +22,8 @@ LLVM LLD's ELF option table distinguishes the accepted spellings: - `irpgo-profile` and `symbol-ordering-file` use `EEq`, whose spelling is double-dash only and supports separated and `=`-joined operands. - `lto-sample-profile=` uses `JJ`, a double-dash joined option. - `plugin-opt=sample-profile=` is an alias of `lto-sample-profile` using `J`; `J` accepts both one- and two-dash spellings. +- `lto-cs-profile-file=` uses `JJ`, so the direct spelling is `--lto-cs-profile-file=`. +- `plugin-opt=cs-profile-path=` aliases `lto-cs-profile-file` through `J`, so both `-plugin-opt=cs-profile-path=` and `--plugin-opt=cs-profile-path=` are accepted. The contract does not invent single-dash aliases for the `EEq` or `JJ` options. @@ -33,11 +36,13 @@ This repair extends the existing direct-linker classifier only. Existing `-Wl,`, ## RED → repair - RED `04a6079c025d7a1db83c8ee302f957f9fcc93096`: introduces hostile equals-joined LLD layout/profile file cases plus rustdoc doctest forwarding and an allowed `-Wl,-z,relro` control. -- Repair `04da16344e413cdd966d6104f75c27b5603750ea`: adds the four file-selecting option families to the canonical direct-linker authority classifier. +- Repair `04da16344e413cdd966d6104f75c27b5603750ea`: adds the four original file-selecting option families to the canonical direct-linker authority classifier. - Grammar correction RED `565456357d5b7f1a52f5b58d8205f170d4bb39ff`: covers the valid single-dash `-call-graph-ordering-file=` spelling. - Grammar correction repair `a5c63c7fbe0cef14ee47cd184b87d001c8b296b1`: adds that exact alias without broadening unrelated option matching. - Alias RED `3797481cdcd241c94dc1bfe5bad0d32d54be0957`: covers both `-plugin-opt=sample-profile=` and `--plugin-opt=sample-profile=`. - Alias repair `d6dc93f87afb986be88537a8f7a174126c5656e5`: routes those LLD sample-profile aliases through the same layout/profile input classifier. +- Context-sensitive profile RED `9b23b72fe3b1649983bae8eb1269bdb64a6871c2`: extends the existing hostile contract with `--lto-cs-profile-file=`, `--plugin-opt=cs-profile-path=`, `-plugin-opt=cs-profile-path=`, and rustdoc doctest forwarding while keeping `-Wl,-z,relro` as the allowed control. +- Context-sensitive profile repair `ea4cfea24b654178360ac029f6ae61f6b8f6c7eb`: adds exactly those three accepted option prefixes to the existing layout/profile classifier. The predecessor-to-repair diff is one canonical authority file, `+3/-0`. These are source-semantic RED/repair contracts. They are not hosted executable GREEN until the exact protected evidence lanes actually run. @@ -53,10 +58,10 @@ Environment/direct-CLI linker arguments, compiler-driver defaults, toolchain-dis ## Evidence -LLVM's ELF `Options.td` defines `call-graph-ordering-file`, `irpgo-profile`, `symbol-ordering-file`, `lto-sample-profile`, the `plugin-opt=sample-profile=` alias, and the `Eq`/`EEq`/`J`/`JJ` spelling grammar. `lld/ELF/DriverUtils.cpp` treats these option values as paths when generating reproduction material, corroborating that they are external file inputs rather than scalar tuning values. +LLVM's ELF `Options.td` defines `call-graph-ordering-file`, `irpgo-profile`, `symbol-ordering-file`, `lto-sample-profile`, `lto-cs-profile-file`, the `plugin-opt=sample-profile=` and `plugin-opt=cs-profile-path=` aliases, and the `Eq`/`EEq`/`J`/`JJ` spelling grammar. `lld/ELF/DriverUtils.cpp` treats these option values as paths when generating reproduction material, corroborating that the layout/profile values are external file inputs rather than scalar tuning values. ### References -LLVM Project. (2026). *LLD ELF option definitions* (`lld/ELF/Options.td`, commit `34eeb2320ff2b991ddb3e3511bbe01ba14478d93`). https://github.com/llvm/llvm-project/blob/34eeb2320ff2b991ddb3e3511bbe01ba14478d93/lld/ELF/Options.td (retrieved September 19, 2026). +LLVM Project. (2026). *LLD ELF option definitions* (`lld/ELF/Options.td`, commit `40a6a441e7a945ca964619cfa2c328120cb9dae5`). https://github.com/llvm/llvm-project/blob/40a6a441e7a945ca964619cfa2c328120cb9dae5/lld/ELF/Options.td (retrieved September 19, 2026). LLVM Project. (2026). *LLD ELF driver reproduction utilities* (`lld/ELF/DriverUtils.cpp`, commit `34eeb2320ff2b991ddb3e3511bbe01ba14478d93`). https://github.com/llvm/llvm-project/blob/34eeb2320ff2b991ddb3e3511bbe01ba14478d93/lld/ELF/DriverUtils.cpp (retrieved September 19, 2026). From 95763aa663be8363543231c12c72df1cc99efdd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 10:42:53 +0900 Subject: [PATCH 516/632] docs(changelog): record linker provenance repairs --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9621df1..a70c9ec28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects context-sensitive LTO PGO profiles through LLD `--lto-cs-profile-file=` or its one-/two-dash `plugin-opt=cs-profile-path=` aliases, keeping profile-guided code generation inside reviewed build-input provenance. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects LLD layout/profile files through `call-graph-ordering-file`, `irpgo-profile`, `symbol-ordering-file`, `lto-sample-profile`, or `plugin-opt=sample-profile=`, preventing unreviewed external layout/profile material from changing section placement or LTO decisions. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects an existing ARM CMSE secure-code import library through LLD `--in-implib=`, while leaving output-only `--out-implib=` outside input-authority classification. +- Failed closed when repository-owned Rust/rustdoc linker forwarding rewrites reviewed link inputs through GNU-compatible `--remap-inputs` / `--remap-inputs-file` spellings, including their single-dash aliases. +- Failed closed when repository-owned Rust/rustdoc linker forwarding imports external section-ordering policy through GNU-compatible `--section-ordering-file`, including the valid single-dash spelling. +- Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's opaque LLVM option-processing tunnel through `--mllvm` / `-mllvm`, rather than treating unknown LLVM options as reviewed deterministic linker policy. +- Failed closed when repository-owned Rust/rustdoc linker forwarding changes the default library search path through GNU `-Y`, including compact `-Ypath` syntax. +- Failed closed when repository-owned Rust/rustdoc linker forwarding embeds ELF runtime filter/auxiliary resolution through GNU-compatible `-f` / `--auxiliary` and `-F` / `--filter`, preventing unreviewed runtime shared objects from becoming symbol-resolution authority. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects ELF rtld-audit libraries through `--audit`, `--depaudit`, or `-P`, preventing unreviewed runtime audit code from being recorded in the linked artifact. +- Failed closed when repository-owned Rust/rustdoc linker forwarding changes or removes the ELF runtime interpreter through `-I`, `--dynamic-linker`, or `--no-dynamic-linker`. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU symbol-policy files through `--version-script`, `--dynamic-list`, `--retain-symbols-file`, or `--export-dynamic-symbol-list`, preventing unreviewed external symbol visibility/retention policy from changing the linked artifact outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU-compatible `-R` / `--just-symbols`, preventing an unreviewed symbol-address file or ambiguous rpath operand from changing link behavior outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects GNU `-c` / `--mri-script`, preventing an unreviewed MRI command file from changing link behavior outside the reviewed Cargo package/source closure. From a1829971952c95ab8d8899e24813e74443939873 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 11:03:20 +0900 Subject: [PATCH 517/632] test(browser-session): expose ThinLTO cache authority bypass --- ...on_lld_thinlto_cache_authority_contract.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_browser_session_lld_thinlto_cache_authority_contract.py diff --git a/tests/test_browser_session_lld_thinlto_cache_authority_contract.py b/tests/test_browser_session_lld_thinlto_cache_authority_contract.py new file mode 100644 index 000000000..dc0b5553a --- /dev/null +++ b/tests/test_browser_session_lld_thinlto_cache_authority_contract.py @@ -0,0 +1,39 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldThinLtoCacheAuthorityContractTests(unittest.TestCase): + """Keep ThinLTO cached object inputs inside reviewed linker provenance.""" + + def test_joined_thinlto_cache_dir_fails_closed(self) -> None: + rustflags = [ + "-C", + "link-arg=-Wl,--thinlto-cache-dir=tools/unreviewed-thinlto-cache", + ] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_thinlto_cache_dir(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--thinlto-cache-dir=tools/unreviewed-thinlto-cache", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_typed_linker_control_remains_allowed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,-z,relro"] + self.assertFalse(authority._flags_select_linker(rustflags)) + + +if __name__ == "__main__": + unittest.main() From 38081627f21a1adadf22e1269b1d326c0012a5a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 11:05:37 +0900 Subject: [PATCH 518/632] fix(browser-session): reject mutable ThinLTO cache inputs --- ...est_browser_session_cargo_compiler_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 5646ee64a..329be2b6e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -168,6 +168,11 @@ def _linker_option_selects_layout_profile_file(argument: str) -> bool: ) +def _linker_option_selects_thinlto_cache(argument: str) -> bool: + """Return whether LLD may read native objects from a mutable ThinLTO cache directory.""" + return argument.startswith("--thinlto-cache-dir=") + + def _linker_option_selects_cmse_import_library(argument: str) -> bool: """Return whether LLD consumes an existing CMSE secure-code import library.""" return argument == "--in-implib" or argument.startswith("--in-implib=") @@ -273,6 +278,7 @@ def _direct_linker_arguments_extend_authority(arguments: tuple[str, ...] | list[ or _linker_option_selects_script(argument) or _linker_option_selects_symbol_policy_file(argument) or _linker_option_selects_layout_profile_file(argument) + or _linker_option_selects_thinlto_cache(argument) or _linker_option_selects_cmse_import_library(argument) or _linker_option_selects_just_symbols_or_rpath(argument) or _linker_option_controls_runtime_loader(argument) From 18071870fd34ffed51184fcc17bcf831e26351b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 11:06:16 +0900 Subject: [PATCH 519/632] docs(traceability): record ThinLTO cache input authority --- ...ser-session-lld-thinlto-cache-authority.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/traceability/browser-session-lld-thinlto-cache-authority.md diff --git a/docs/traceability/browser-session-lld-thinlto-cache-authority.md b/docs/traceability/browser-session-lld-thinlto-cache-authority.md new file mode 100644 index 000000000..3d36b2d0e --- /dev/null +++ b/docs/traceability/browser-session-lld-thinlto-cache-authority.md @@ -0,0 +1,57 @@ +# Browser Session LLD ThinLTO cache authority + +## Problem + +Repository-owned Rust/rustdoc linker forwarding could select an LLD ThinLTO cache with `--thinlto-cache-dir=` without entering the Browser Session compiler/linker authority boundary. The joined option keeps the directory path inside one linker token, so the existing positional-native-input fallback did not see it. + +This is an input-provenance problem, not merely a performance/cache setting. LLVM LLD configures its ThinLTO `FileCache` from `thinLTOCacheDir`. On a cache hit, LLVM's local cache opens `llvmcache-` for read, maps the bytes into a `MemoryBuffer`, and passes that buffer to the linker callback. LLD then treats a non-null cached buffer as a native relocatable file and links its bytes. A mutable or externally restored cache can therefore contribute native object bytes to the final Browser Session artifact. + +## Primary evidence + +Evidence was checked against `llvm/llvm-project` source at revision `a312cb0c81cf1e4cf1a3bc469b15bd5216c59469`: + +- `lld/ELF/Options.td` defines `thinlto-cache-dir=` as a joined LLD option whose value is the path to the ThinLTO cached-object directory. +- `lld/ELF/LTO.cpp` passes `ctx.arg.thinLTOCacheDir` to `localCache(...)`, then documents that `files[i]` may contain a native relocatable `MemoryBuffer` supplied by that cache and links the resulting `objBuf`. +- `llvm/lib/Support/Caching.cpp` implements a cache hit by opening `llvmcache-` for read and handing the resulting `MemoryBuffer` to `AddBuffer`. + +The cache key does not turn the cache directory into reviewed source authority: the cache hit path trusts the bytes found at the computed entry path and feeds them into the link. Repository configuration must therefore not be able to select a mutable external cache as an implicit native-object source without an explicit provenance contract. + +## Ownership + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected rustc/rustdoc/toolchain/linker execution and input authority. `tests/test_browser_session_trusted_adapter_boundary.py` remains the owner of production Cargo package/source topology. No second Cargo/linker scanner was introduced. + +The boundary is deliberately narrower than general CI cache policy. Organization-level cache storage, restoration, retention, attestation, and runner isolation remain CI/release supply-chain concerns. This slice only prevents Git-owned linker forwarding from selecting a mutable ThinLTO cache directory as an unreviewed native-object input source. + +## RED → repair + +Structural RED `a1829971952c95ab8d8899e24813e74443939873` adds `tests/test_browser_session_lld_thinlto_cache_authority_contract.py`. It requires joined `--thinlto-cache-dir=...` to fail closed through normal Rust linker forwarding and rustdoc doctest compiler forwarding, while keeping typed `-Wl,-z,relro` as an allowed control. + +Minimal repair `38081627f21a1adadf22e1269b1d326c0012a5a4` adds `_linker_option_selects_thinlto_cache()` to the existing canonical direct-linker classifier and consumes it from `_direct_linker_arguments_extend_authority()`. The predecessor-to-repair compare changes only that authority file by `+6/-0`; the RED contract remains separate. + +## Alternatives rejected + +Treating ThinLTO cache selection as performance-only was rejected because upstream `localCache` reads existing cached object bytes and LLD explicitly consumes those buffers as native relocatable inputs. + +Allowing repository-relative cache directories by pathname was rejected. Path location does not establish byte identity, producer identity, restoration provenance, symlink containment, cache poisoning resistance, toolchain compatibility, or reproducibility. + +Reimplementing cache discovery in a separate test/helper was rejected because it would split compiler/linker input authority across multiple writers. + +Disabling ThinLTO itself was not selected. The defect is repository-selected mutable cache authority, not ThinLTO compilation as such. + +## Future exception evidence + +If a buyer-specific workflow later requires ThinLTO caching, the exception must be owned by the CI/release supply-chain boundary and prove at least: + +- exact cache producer/toolchain identity and cache-key inputs; +- immutable or integrity-verified cached object bytes before link consumption; +- repository/job/architecture isolation and purpose-bound access; +- symlink/path containment and restore-source provenance; +- SBOM/provenance attachment tying consumed cached objects to the released artifact; +- reproducibility against a cache-cold rebuild or an equivalent independent build; +- invalidation and rollback behavior when compiler, linker, target, flags, or source inputs change. + +A cache hit, command acknowledgement, or successful link is not evidence that those properties hold. + +## Residual acceptance + +This source repair does not establish hosted GREEN, whole-PR review closure, protected-branch mergeability, or release readiness. Exact-head repository/security checks, current-head review, and the existing parent/central CodeQL prerequisite chain remain required before #317 can advance. From 30026004a1108aa850d084c0ba06551d474b050c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 12:00:36 +0900 Subject: [PATCH 520/632] test(browser-session): expose linker library alias authority bypass --- ...linker_library_alias_authority_contract.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_browser_session_linker_library_alias_authority_contract.py diff --git a/tests/test_browser_session_linker_library_alias_authority_contract.py b/tests/test_browser_session_linker_library_alias_authority_contract.py new file mode 100644 index 000000000..b61f2c8f4 --- /dev/null +++ b/tests/test_browser_session_linker_library_alias_authority_contract.py @@ -0,0 +1,36 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLinkerLibraryAliasAuthorityContractTests(unittest.TestCase): + """Keep GNU-compatible long-form native-library selection inside reviewed linker provenance.""" + + def test_joined_double_dash_library_alias_fails_closed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,--library=review_bypass"] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_joined_library_alias(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--library=review_bypass", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_typed_linker_control_remains_allowed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,-z,relro"] + self.assertFalse(authority._flags_select_linker(rustflags)) + + +if __name__ == "__main__": + unittest.main() From a168131b65d25f5a4625b77b259492a1b5e692a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 12:02:07 +0900 Subject: [PATCH 521/632] fix(browser-session): close linker library alias input authority --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 329be2b6e..f6b764573 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -84,9 +84,9 @@ def _linker_argument_extends_external_inputs(argument: str) -> bool: """Return whether one compiler/linker-driver argument widens external library inputs.""" if argument == "--sysroot" or argument.startswith("--sysroot="): return True - if argument in {"-L", "-l", "--library-path"}: + if argument in {"-L", "-l", "--library", "--library-path"}: return True - if argument.startswith("--library-path="): + if argument.startswith(("--library=", "--library-path=")): return True if argument.startswith("-L") and len(argument) > 2: return True From 59c90537e362d770aa339f8eacbb4eb6efeb839e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 12:02:37 +0900 Subject: [PATCH 522/632] docs(browser-session): trace linker library alias authority --- ...-session-linker-library-alias-authority.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/traceability/browser-session-linker-library-alias-authority.md diff --git a/docs/traceability/browser-session-linker-library-alias-authority.md b/docs/traceability/browser-session-linker-library-alias-authority.md new file mode 100644 index 000000000..ee9a2cb9c --- /dev/null +++ b/docs/traceability/browser-session-linker-library-alias-authority.md @@ -0,0 +1,52 @@ +# Browser Session linker `--library` alias authority + +Status: source-semantic repair; hosted exact-head execution evidence is still required before acceptance. + +## Problem + +The Browser Session Cargo/compiler authority contract already fails closed for native-library selectors such as `-lNAME`, `-l NAME`, `-L`, and `--library-path`. LLVM LLD also accepts the GNU-compatible long form `--library=NAME` (and the corresponding separated alias). At LLVM revision `851eb5a97ba67b4e8ebec39fb821c144db67a10a`, `lld/ELF/Options.td` defines `-l` as `Search for library ` and declares both `library` aliases: + +- `Separate<["--", "-"], "library">` +- `Joined<["--", "-"], "library=">` + +Primary source: `https://github.com/llvm/llvm-project/blob/851eb5a97ba67b4e8ebec39fb821c144db67a10a/lld/ELF/Options.td`. + +Before this repair, the shared classifier rejected compact single-dash `-l...` forms but did not classify joined double-dash `--library=...`. A Git-owned `rustflags`/`rustdocflags` path could therefore forward `--library=review_bypass` through `-C link-arg`, `-Wl,`, `--for-linker=`, or doctest compiler forwarding without being recognized as an external native-library selector. + +## Authority boundary + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for Git-owned Cargo compiler/linker input authority. Production Cargo package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`; this repair does not duplicate that discovery logic. + +## RED → repair + +- RED `30026004a1108aa850d084c0ba06551d474b050c` adds `tests/test_browser_session_linker_library_alias_authority_contract.py`. It requires joined `--library=review_bypass` to fail closed through ordinary build `rustflags` and rustdoc doctest compiler forwarding, while keeping `-Wl,-z,relro` as an allowed typed control. +- Repair `a168131b65d25f5a4625b77b259492a1b5e692a9` extends the existing shared external-input classifier with `--library` and `--library=`. The RED-to-repair delta in the canonical authority file is two additions/two replacements; no second Cargo/linker scanner was introduced. + +The repair deliberately treats library-name selection as input authority even when the name is not a pathname. The selected bytes still depend on linker search roots, sysroot/toolchain state, and the resolved library artifact. A name allowlist alone does not prove artifact identity or provenance. + +## Invariant + +Git-owned Cargo flags must not select an additional native library through GNU-compatible long-form `--library` syntax unless that input is covered by an explicit reviewed provenance contract. This applies equally when the option is forwarded through rustdoc doctest compilation. + +## Rejected alternatives + +- **Allow known library names:** rejected because a stable name does not identify the resolved bytes, producer, search root, ABI, or toolchain. +- **Add a second focused scanner:** rejected because it would split policy ownership from the canonical Cargo/compiler authority contract. +- **Rely on positional-token fallback:** rejected because joined `--library=NAME` keeps the library selector and operand in one option token. + +## Acceptance and release evidence + +If OriginWeave later needs to permit a repository-selected native library, the same reviewed delta must prove at least: + +1. immutable or integrity-verified artifact identity and producer provenance; +2. linker/toolchain, target, ABI, and architecture compatibility; +3. bounded search-root/sysroot resolution with symlink containment; +4. SBOM and build provenance for the resolved artifact; +5. reproducible independent rebuild or equivalent byte-identity evidence; +6. invalidation and rollback behavior when the artifact or toolchain changes. + +This source-semantic repair is not hosted GREEN. Exact-head required workflows, repository/security gates, independent current-head review, and the broader Browser Session acceptance chain remain mandatory before merge or release. + +## Reference + +The LLVM Project. (2026). *LLD ELF option definitions* (revision `851eb5a97ba67b4e8ebec39fb821c144db67a10a`) [Source code]. GitHub. `lld/ELF/Options.td`. From 89b611abcf4bf33674c0451030c05ea170a2c259 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 12:05:34 +0900 Subject: [PATCH 523/632] docs(changelog): record linker provenance closures --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a70c9ec28..9dcfef3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects native libraries through GNU-compatible long-form `--library` / `--library=` aliases, so joined double-dash library selection cannot bypass the reviewed external-input authority boundary. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects LLD `--thinlto-cache-dir=`, preventing mutable cached native ThinLTO objects from becoming unreviewed link inputs outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects context-sensitive LTO PGO profiles through LLD `--lto-cs-profile-file=` or its one-/two-dash `plugin-opt=cs-profile-path=` aliases, keeping profile-guided code generation inside reviewed build-input provenance. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects LLD layout/profile files through `call-graph-ordering-file`, `irpgo-profile`, `symbol-ordering-file`, `lto-sample-profile`, or `plugin-opt=sample-profile=`, preventing unreviewed external layout/profile material from changing section placement or LTO decisions. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects an existing ARM CMSE secure-code import library through LLD `--in-implib=`, while leaving output-only `--out-implib=` outside input-authority classification. From a36e22d71b73877c243fa6ecdb34806b00d1f1f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:02:46 +0900 Subject: [PATCH 524/632] test(browser-session): expose LLD plugin-opt LLVM authority --- ...er_session_lld_mllvm_authority_contract.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_browser_session_lld_mllvm_authority_contract.py b/tests/test_browser_session_lld_mllvm_authority_contract.py index b59f0483e..33dd740b5 100644 --- a/tests/test_browser_session_lld_mllvm_authority_contract.py +++ b/tests/test_browser_session_lld_mllvm_authority_contract.py @@ -67,6 +67,30 @@ def test_doctest_build_arg_lld_mllvm_fails_closed(self) -> None: "rustdocflags:doctest compiler authority", ) + def test_build_rustflags_lld_plugin_opt_llvm_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustflags = ["-C", "link-arg=-Wl,--plugin-opt=-debug-pass=Structure"]\n', + "rustflags:codegen linker", + ) + + def test_target_rustflags_lld_single_dash_plugin_opt_llvm_fails_closed(self) -> None: + self._assert_fails_closed( + "[target.'cfg(unix)']\nrustflags = [\"-C\", \"link-arg=-Wl,-plugin-opt=-print-after-all\"]\n", + "rustflags:codegen linker", + ) + + def test_build_rustdocflags_lld_plugin_opt_llvm_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["-C", "link-arg=-Wl,--plugin-opt=-print-after-all"]\n', + "rustdocflags:codegen linker", + ) + + def test_doctest_build_arg_lld_plugin_opt_llvm_fails_closed(self) -> None: + self._assert_fails_closed( + '[build]\nrustdocflags = ["--doctest-build-arg=-C", "--doctest-build-arg=link-arg=-Wl,--plugin-opt=-print-after-all"]\n', + "rustdocflags:doctest compiler authority", + ) + def test_typed_linker_control_remains_allowed(self) -> None: directory, root = _workspace_with_config( '[build]\nrustflags = ["-C", "link-arg=-Wl,-z,relro"]\n' From 64627f969f9c04c52ebed4efd3f7ca05b5cb42ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:04:17 +0900 Subject: [PATCH 525/632] fix(browser-session): reject LLD plugin-opt LLVM tunnel --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index f6b764573..0e4bd88a2 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -127,7 +127,7 @@ def _linker_option_forwards_llvm_options(argument: str) -> bool: """Return whether one LLD option forwards opaque arguments to LLVM option processing.""" if argument in {"--mllvm", "-mllvm"}: return True - return argument.startswith(("--mllvm=", "-mllvm=")) + return argument.startswith(("--mllvm=", "-mllvm=", "--plugin-opt=-", "-plugin-opt=-")) def _linker_option_selects_script(argument: str) -> bool: From 57bde7332b208b476f7796295595725ea39eca77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:04:53 +0900 Subject: [PATCH 526/632] docs(browser-session): trace LLD plugin-opt LLVM authority --- ...r-session-lld-plugin-opt-llvm-authority.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/traceability/browser-session-lld-plugin-opt-llvm-authority.md diff --git a/docs/traceability/browser-session-lld-plugin-opt-llvm-authority.md b/docs/traceability/browser-session-lld-plugin-opt-llvm-authority.md new file mode 100644 index 000000000..4edc4dc48 --- /dev/null +++ b/docs/traceability/browser-session-lld-plugin-opt-llvm-authority.md @@ -0,0 +1,54 @@ +# Browser Session LLD `plugin-opt=-` LLVM option authority + +Status: source-semantic repair; hosted executable evidence pending + +## Problem + +OriginWeave's Browser Session Cargo compiler authority already fails closed for LLD `-mllvm` / `--mllvm`, but LLVM LLD exposes a second compatibility spelling that reaches the same LLVM option parser. A Git-owned Cargo `rustflags` or `rustdocflags` value can forward `-plugin-opt=-` or `--plugin-opt=-` through the compiler driver. Before this repair, those joined tokens were not classified as LLVM-option authority and could pass the reviewed direct-linker boundary. + +This is an execution/provenance boundary, not a claim that every LLVM option is independently dangerous. The problem is that a reviewed Cargo configuration could open the opaque LLVM option namespace without the same explicit review applied to `-mllvm`. + +## Primary evidence + +Evidence is pinned to `llvm/llvm-project@3ff9abe8930acc7b4c4e2387c1357ca6f2d15c00`. + +- `lld/ELF/Options.td` defines `plugin_opt_eq_minus` as `J<"plugin-opt=-">` and describes it as `Specify an LLVM option for compatibility with LLVMgold.so`. +- LLD's `J` grammar accepts both one-dash and two-dash multi-letter spellings. +- `lld/ELF/Driver.cpp` iterates `OPT_plugin_opt_eq_minus` and calls `parseClangOption(ctx, std::string("-") + arg->getValue(), arg->getSpelling())`. +- The adjacent `-mllvm` path also calls `parseClangOption`, so the two surfaces share the same underlying LLVM option-processing authority even though their command-line spellings differ. + +The generic `plugin-opt=` compatibility path is not blanket-blocked by this decision. LLD explicitly ignores a GCC `lto-wrapper` path and errors on other unsupported generic values. The repair therefore targets only the documented `plugin-opt=-` LLVM-option tunnel rather than treating every `plugin-opt` spelling as equivalent. + +## RED and causal repair + +Structural RED: `a36e22d71b73877c243fa6ecdb34806b00d1f1f5`. + +The existing `tests/test_browser_session_lld_mllvm_authority_contract.py` now covers: + +- build-level `rustflags` with `--plugin-opt=-...`; +- target-level `rustflags` with the one-dash `-plugin-opt=-...` spelling; +- build-level `rustdocflags`; +- rustdoc doctest compiler forwarding; +- an ordinary typed linker control (`-Wl,-z,relro`) as the allowed control. + +Minimal repair: `64627f969f9c04c52ebed4efd3f7ca05b5cb42ea`. + +The canonical single writer remains `tests/test_browser_session_cargo_compiler_authority_contract.py`. `_linker_option_forwards_llvm_options()` now recognizes only the additional `--plugin-opt=-` and `-plugin-opt=-` prefixes. The RED-to-repair compare changes that owner file by one replacement line (`+1/-1`); no second Cargo/linker scanner, pathname allowlist, provider-specific policy, or new topology discovery was introduced. + +## Decision + +Repository-owned Cargo configuration must fail closed when Rust/rustdoc linker forwarding opens LLD's opaque LLVM option-processing namespace through either `mllvm` or `plugin-opt=-`. + +The deterministic Browser Session policy boundary is not delegated to LLVM option behavior. If a future buyer requirement needs a specific LLVM option, it must be modeled as a typed, reviewed contract with its security, reproducibility, toolchain-version, target/architecture, and rollback consequences stated explicitly. Reopening the generic opaque tunnel is not an acceptable shortcut. + +## Rejected alternatives + +- **Block every `plugin-opt=` spelling.** Rejected because current LLD has typed compatibility spellings and a generic GCC `lto-wrapper` compatibility path with different semantics. A blanket ban would conflate unrelated grammar with LLVM-option authority. +- **Allowlist LLVM option strings.** Rejected because an option name alone does not prove semantics across LLVM revisions, targets, code-generation pipelines, or security/reproducibility consequences. +- **Add a supplemental scanner.** Rejected because the existing Cargo compiler authority contract is the single writer for repository-selected compiler/rustdoc/linker execution and input authority. + +## Residual authority and release evidence + +This repair covers Git-owned Cargo `rustflags` / `rustdocflags` paths already consumed by the canonical contract, including doctest compiler forwarding. Environment `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `RUSTDOCFLAGS`, `CARGO_ENCODED_RUSTDOCFLAGS`, direct Cargo/rustc/rustdoc CLI arguments, ambient toolchain configuration, and unmodeled non-LLD linker grammars remain CI/release execution-provenance surfaces. + +The current PR head still requires fresh hosted repository/security execution and whole-current-head review after this source/doc generation. Source-semantic repair and static primary-source traceability do not substitute for protected-head GREEN, reproducibility evidence, SBOM/provenance, or release acceptance. From b445b22c698a5418398d72947f6a0ca9b65eba38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:12:25 +0900 Subject: [PATCH 527/632] test(browser-session): expose LLD pass-plugin code loading --- tests/test_browser_session_linker_plugin_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_browser_session_linker_plugin_contract.py b/tests/test_browser_session_linker_plugin_contract.py index 33c3a5b57..2ada09353 100644 --- a/tests/test_browser_session_linker_plugin_contract.py +++ b/tests/test_browser_session_linker_plugin_contract.py @@ -60,6 +60,11 @@ def test_repository_for_linker_plugin_fails_closed(self) -> None: '["-C", "link-arg=--for-linker=-plugin=tools/review-bypass-linker.so"]' ) + def test_repository_lld_load_pass_plugin_fails_closed(self) -> None: + self._assert_plugin_override_fails_closed( + '["-C", "link-arg=-Wl,--load-pass-plugin=tools/review-bypass-pass.so"]' + ) + def test_non_plugin_wl_forwarding_remains_allowed(self) -> None: root = self._workspace_with_flags('["-C", "link-arg=-Wl,--as-needed"]') authority._assert_no_repository_cargo_compiler_execution_overrides(root) From 3d0efa7b131e761174ddd7bc4b0bcc10b2d59e31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:13:31 +0900 Subject: [PATCH 528/632] fix(browser-session): reject LLD pass-plugin loading --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 0e4bd88a2..028d93e52 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -21,7 +21,7 @@ UNSTABLE_TOOLCHAIN_INPUT_KEYS = frozenset( {"build-std", "build-std-features", "codegen-backend"} ) -LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin"}) +LINKER_PLUGIN_OPTIONS = frozenset({"-plugin", "--plugin", "--load-pass-plugin"}) LINKER_SCRIPT_OPTIONS = frozenset( {"-T", "--script", "-dT", "--default-script", "-c", "--mri-script"} ) @@ -108,7 +108,7 @@ def _linker_option_loads_plugin(argument: str) -> bool: """Return whether one direct linker option requests dynamically loaded plugin code.""" if argument in LINKER_PLUGIN_OPTIONS: return True - return argument.startswith(("-plugin=", "--plugin=")) + return argument.startswith(("-plugin=", "--plugin=", "--load-pass-plugin=")) def _linker_option_selects_error_handler(argument: str) -> bool: From dc2d0624925c952cf9a33c52fb881669dd37c308 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:14:45 +0900 Subject: [PATCH 529/632] docs(browser-session): trace LLD pass-plugin execution authority --- ...owser-session-lld-pass-plugin-authority.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/traceability/browser-session-lld-pass-plugin-authority.md diff --git a/docs/traceability/browser-session-lld-pass-plugin-authority.md b/docs/traceability/browser-session-lld-pass-plugin-authority.md new file mode 100644 index 000000000..86c022a18 --- /dev/null +++ b/docs/traceability/browser-session-lld-pass-plugin-authority.md @@ -0,0 +1,46 @@ +# Browser Session LLD pass-plugin execution authority + +Status: source-semantic repair; hosted executable evidence pending + +## Problem + +OriginWeave's Browser Session Cargo compiler authority already rejects GNU linker plugin loading through `-plugin` / `--plugin`, but LLVM LLD exposes a separate LTO pass-plugin surface. `--load-pass-plugin=` selects a dynamic pass-plugin library that is carried into the LTO configuration. Before this repair, the joined spelling kept the library path inside a single option token, so the positional-native-input fallback did not see it and the existing GNU-plugin classifier did not match it. + +This is executable-code authority. A repository-owned Cargo `rustflags` or `rustdocflags` value must not be able to load an unreviewed LLVM pass plugin into link-time optimization while the reviewed Cargo package/source closure remains unchanged. + +## Primary evidence + +Evidence is pinned to `llvm/llvm-project@3834f58744a7be80b5869c07fe576ff8f23e2315`. + +- `lld/ELF/Options.td` defines `load_pass_plugins` as `EEq<"load-pass-plugin", "Load passes from plugin library">`, so the supported ELF spelling is the double-dash option with separated or `=`-joined operand. +- `lld/ELF/Driver.cpp` stores `args::getStrings(args, OPT_load_pass_plugins)` in `ctx.arg.passPlugins`. +- `lld/ELF/LTO.cpp` copies each `ctx.arg.passPlugins` filename into `LTO::Config::PassPluginFilenames`, making the selected library part of the LTO pass-plugin execution surface rather than a passive output setting. + +## RED and causal repair + +Structural RED: `b445b22c698a5418398d72947f6a0ca9b65eba38`. + +The existing `tests/test_browser_session_linker_plugin_contract.py` adds a hostile Cargo forwarding case for `-Wl,--load-pass-plugin=tools/review-bypass-pass.so`. Existing `-Wl,--as-needed`, `-Xlinker --as-needed`, and `--for-linker=--as-needed` controls remain allowed. The RED commit changes only that focused contract (`+5/-0`). + +Minimal repair: `3d0efa7b131e761174ddd7bc4b0bcc10b2d59e31`. + +The canonical single writer remains `tests/test_browser_session_cargo_compiler_authority_contract.py`. `LINKER_PLUGIN_OPTIONS` now includes the separated `--load-pass-plugin` spelling, and `_linker_option_loads_plugin()` recognizes the joined `--load-pass-plugin=` spelling. RED-to-repair changes only that authority file (`+2/-2`). No supplemental Cargo/linker scanner, filename allowlist, or LTO-specific source-discovery path was added. + +## Decision + +Repository-owned Cargo/rustdoc linker forwarding must fail closed whenever LLD is instructed to load a pass-plugin library. The boundary treats pass-plugin selection as executable build authority, alongside linker plugin loading, tool replacement, wrapper selection, and other mechanisms that can execute code outside the reviewed build TCB. + +A future buyer requirement for an LTO pass plugin must use a typed, versioned contract that proves the exact plugin artifact and its execution context. A path string or library basename is not sufficient provenance. + +## Rejected alternatives + +- **Rely on positional-input detection.** Rejected because the joined `--load-pass-plugin=` spelling embeds the path inside the option token. +- **Allowlist plugin paths or names.** Rejected because a pathname does not prove immutable bytes, producer identity, toolchain/plugin ABI compatibility, symlink containment, or reproducibility. +- **Treat the option as ordinary linker tuning.** Rejected because LLD explicitly carries the selected filename into `PassPluginFilenames` for LTO execution. +- **Add a second pass-plugin scanner.** Rejected because the existing Cargo compiler authority contract is the canonical single writer for repository-selected compiler/rustdoc/linker execution authority. + +## Acceptance and residual authority + +If pass-plugin execution is ever admitted, release evidence must bind at minimum the plugin digest, producer provenance, LLVM/LLD version and plugin ABI compatibility, target/architecture, containment of the resolved artifact, SBOM/provenance, deterministic or independently reproduced output evidence, invalidation conditions, and rollback procedure. + +This source-semantic repair does not establish hosted GREEN. The exact PR head still requires fresh repository/security execution, current-head independent review, and the configured coverage/rustdoc/docstring gates. Environment and direct-CLI linker arguments, ambient toolchain installation, CI-restored artifacts, and unmodeled non-LLD plugin mechanisms remain CI/release provenance surfaces rather than Browser Session domain truth. From 8634074f4ebad22a17fddcc4badef8176a51c1a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:30:33 +0900 Subject: [PATCH 530/632] test(browser-session): expose LLD chroot input authority --- ...r_session_lld_chroot_authority_contract.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_browser_session_lld_chroot_authority_contract.py diff --git a/tests/test_browser_session_lld_chroot_authority_contract.py b/tests/test_browser_session_lld_chroot_authority_contract.py new file mode 100644 index 000000000..93d2063fa --- /dev/null +++ b/tests/test_browser_session_lld_chroot_authority_contract.py @@ -0,0 +1,39 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldChrootAuthorityContractTests(unittest.TestCase): + """Keep LLD virtual-root selection inside reviewed linker input provenance.""" + + def test_forwarded_chroot_fails_closed(self) -> None: + rustflags = [ + "-C", + "link-args=-Wl,--chroot,/tmp/unreviewed-link-root", + ] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_chroot(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-args=-Wl,--chroot,/tmp/unreviewed-link-root", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_typed_linker_control_remains_allowed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,-z,relro"] + self.assertFalse(authority._flags_select_linker(rustflags)) + + +if __name__ == "__main__": + unittest.main() From 2d7c6e653d4cf0c7dad81c2827a3b3686b2ec28f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:30:55 +0900 Subject: [PATCH 531/632] test(browser-session): drop already-covered LLD chroot probe --- ...r_session_lld_chroot_authority_contract.py | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 tests/test_browser_session_lld_chroot_authority_contract.py diff --git a/tests/test_browser_session_lld_chroot_authority_contract.py b/tests/test_browser_session_lld_chroot_authority_contract.py deleted file mode 100644 index 93d2063fa..000000000 --- a/tests/test_browser_session_lld_chroot_authority_contract.py +++ /dev/null @@ -1,39 +0,0 @@ -import importlib.util -import pathlib -import unittest - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" - -spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) -if spec is None or spec.loader is None: - raise RuntimeError("unable to load Browser Session compiler authority contract") -authority = importlib.util.module_from_spec(spec) -spec.loader.exec_module(authority) - - -class BrowserSessionLldChrootAuthorityContractTests(unittest.TestCase): - """Keep LLD virtual-root selection inside reviewed linker input provenance.""" - - def test_forwarded_chroot_fails_closed(self) -> None: - rustflags = [ - "-C", - "link-args=-Wl,--chroot,/tmp/unreviewed-link-root", - ] - self.assertTrue(authority._flags_select_linker(rustflags)) - - def test_rustdoc_doctest_forwarding_cannot_select_chroot(self) -> None: - rustdocflags = [ - "--doctest-build-arg=-C", - "--doctest-build-arg=link-args=-Wl,--chroot,/tmp/unreviewed-link-root", - ] - self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) - - def test_typed_linker_control_remains_allowed(self) -> None: - rustflags = ["-C", "link-arg=-Wl,-z,relro"] - self.assertFalse(authority._flags_select_linker(rustflags)) - - -if __name__ == "__main__": - unittest.main() From fe842827a2781b198075d91352745fd90b3b341a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:31:42 +0900 Subject: [PATCH 532/632] test(browser-session): expose DTLTO subprocess argument authority --- ...n_lld_dtlto_argument_authority_contract.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_browser_session_lld_dtlto_argument_authority_contract.py diff --git a/tests/test_browser_session_lld_dtlto_argument_authority_contract.py b/tests/test_browser_session_lld_dtlto_argument_authority_contract.py new file mode 100644 index 000000000..6979e53f0 --- /dev/null +++ b/tests/test_browser_session_lld_dtlto_argument_authority_contract.py @@ -0,0 +1,43 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location("browser_session_cargo_compiler_authority", AUTHORITY_TEST) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compiler authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionLldDtltoArgumentAuthorityContractTests(unittest.TestCase): + """Keep DTLTO subprocess arguments inside reviewed linker execution provenance.""" + + def test_dtlto_subprocess_argument_forwarding_fails_closed(self) -> None: + hostile_options = ( + "--thinlto-distributor-arg=tools/unreviewed-distributor.py", + "--thinlto-remote-compiler-prepend-arg=-fplugin=tools/unreviewed-pass.so", + "--thinlto-remote-compiler-arg=-fplugin=tools/unreviewed-pass.so", + ) + for option in hostile_options: + with self.subTest(option=option): + rustflags = ["-C", f"link-arg=-Wl,{option}"] + self.assertTrue(authority._flags_select_linker(rustflags)) + + def test_rustdoc_doctest_forwarding_cannot_select_dtlto_subprocess_arguments(self) -> None: + rustdocflags = [ + "--doctest-build-arg=-C", + "--doctest-build-arg=link-arg=-Wl,--thinlto-remote-compiler-arg=-fplugin=tools/unreviewed-pass.so", + ] + self.assertTrue(authority._flags_select_rustdoc_doctest_compiler_authority(rustdocflags)) + + def test_typed_linker_control_remains_allowed(self) -> None: + rustflags = ["-C", "link-arg=-Wl,-z,relro"] + self.assertFalse(authority._flags_select_linker(rustflags)) + + +if __name__ == "__main__": + unittest.main() From 352c22b3b7aba5888e2ffd6f1e56cad93e04dba4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:33:41 +0900 Subject: [PATCH 533/632] test(browser-session): cover separated DTLTO subprocess args --- ...rowser_session_lld_dtlto_argument_authority_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_browser_session_lld_dtlto_argument_authority_contract.py b/tests/test_browser_session_lld_dtlto_argument_authority_contract.py index 6979e53f0..a53f0cc13 100644 --- a/tests/test_browser_session_lld_dtlto_argument_authority_contract.py +++ b/tests/test_browser_session_lld_dtlto_argument_authority_contract.py @@ -27,6 +27,13 @@ def test_dtlto_subprocess_argument_forwarding_fails_closed(self) -> None: rustflags = ["-C", f"link-arg=-Wl,{option}"] self.assertTrue(authority._flags_select_linker(rustflags)) + def test_separated_dtlto_subprocess_argument_fails_closed(self) -> None: + rustflags = [ + "-C", + "link-arg=-Wl,--thinlto-remote-compiler-arg,--target=riscv64-unknown-linux-gnu", + ] + self.assertTrue(authority._flags_select_linker(rustflags)) + def test_rustdoc_doctest_forwarding_cannot_select_dtlto_subprocess_arguments(self) -> None: rustdocflags = [ "--doctest-build-arg=-C", From bb657a7be0149d6c47207723cdd9650fb2e0508d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:35:09 +0900 Subject: [PATCH 534/632] fix(browser-session): close DTLTO subprocess argument authority --- ...ession_cargo_compiler_authority_contract.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 028d93e52..252a6ef64 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -119,8 +119,22 @@ def _linker_option_selects_error_handler(argument: str) -> bool: def _linker_option_selects_dtlto_executable(argument: str) -> bool: - """Return whether one LLD DTLTO option selects a distributor or remote compiler executable.""" - return argument.startswith(("--thinlto-distributor=", "--thinlto-remote-compiler=")) + """Return whether one LLD DTLTO option selects or controls a subprocess.""" + if argument in { + "--thinlto-distributor-arg", + "--thinlto-remote-compiler-prepend-arg", + "--thinlto-remote-compiler-arg", + }: + return True + return argument.startswith( + ( + "--thinlto-distributor=", + "--thinlto-distributor-arg=", + "--thinlto-remote-compiler=", + "--thinlto-remote-compiler-prepend-arg=", + "--thinlto-remote-compiler-arg=", + ) + ) def _linker_option_forwards_llvm_options(argument: str) -> bool: From db69d56f152e55cbebc1130b66201d1a1c8678c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 13:35:52 +0900 Subject: [PATCH 535/632] docs(traceability): record DTLTO subprocess argument authority --- ...lld-dtlto-subprocess-argument-authority.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/traceability/browser-session-lld-dtlto-subprocess-argument-authority.md diff --git a/docs/traceability/browser-session-lld-dtlto-subprocess-argument-authority.md b/docs/traceability/browser-session-lld-dtlto-subprocess-argument-authority.md new file mode 100644 index 000000000..030ed0899 --- /dev/null +++ b/docs/traceability/browser-session-lld-dtlto-subprocess-argument-authority.md @@ -0,0 +1,43 @@ +# Browser Session LLD DTLTO subprocess argument authority + +## Problem + +OriginWeave treats repository-owned Rust/Cargo linker selection as Browser Session supply-chain authority. LLVM LLD Distributed ThinLTO (DTLTO) can execute a distributor and a remote compiler, and it also exposes options that forward arbitrary command-line arguments into those subprocesses. + +At LLVM `llvm-project@0da016867d1fd3d7938895ec36a9775fe26e1919`, `lld/ELF/Options.td` defines `--thinlto-distributor-arg`, `--thinlto-remote-compiler-prepend-arg`, and `--thinlto-remote-compiler-arg` as two-dash `EEq` options, so both separated and `=`-joined forms are accepted. `lld/docs/DTLTO.md` states that these values are placed on the distributor or remote compiler command line. The upstream ELF DTLTO tests use `--thinlto-distributor-arg` for a Python script path and exercise remote-compiler arguments directly. + +Before this repair, the canonical Browser Session compiler/linker authority classifier rejected `--thinlto-distributor=` and `--thinlto-remote-compiler=`, but did not classify their argument-forwarding surfaces. An `=`-joined argument remained inside one linker option token, so the positional-native-input fallback could not observe its payload. A separated forwarded argument that itself began with an otherwise-unclassified option could also escape positional-input detection. + +That is execution and input provenance authority, not a harmless linker tuning surface. A forwarded argument can alter the subprocess toolchain, load executable compiler plugins, select target/runtime inputs, or otherwise change the native bytes emitted for the Browser Session artifact. + +## Boundary and ownership + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler, rustdoc, linker execution, and linker-input authority. `tests/test_browser_session_trusted_adapter_boundary.py` continues to own production Cargo package/source topology. No DTLTO scanner or policy authority is duplicated in another bounded context. + +The repair deliberately extends the existing `_linker_option_selects_dtlto_executable()` classifier rather than adding a parallel parser. Its name is retained to avoid needless call-site churn; its docstring now states the broader invariant: DTLTO options that select **or control** a subprocess extend authority. + +## RED → repair + +- Initial structural RED `fe842827a2781b198075d91352745fd90b3b341a` adds joined distributor/remote-compiler argument cases, rustdoc doctest forwarding, and a typed `-z relro` negative control. +- Structural RED successor `352c22b3b7aba5888e2ffd6f1e56cad93e04dba4` adds the separated `--thinlto-remote-compiler-arg --target=...` spelling so the `EEq` grammar itself is covered rather than only the joined form. +- Minimal causal repair `bb657a7be0149d6c47207723cdd9650fb2e0508d` changes only the canonical DTLTO classifier: separated argument-option names fail closed and joined distributor/compiler selectors plus all three argument-forwarding prefixes fail closed. The repair commit changes one existing file by `+16/-2`; no unrelated production or test topology is rewritten. + +A preliminary `--chroot` probe (`8634074f4ebad22a17fddcc4badef8176a51c1a4`) was rejected as a false finding and removed by `2d7c6e653d4cf0c7dad81c2827a3b3686b2ec28f`: current LLD accepts `--chroot` only as a separated option, and OriginWeave's existing positional-native-input fallback already rejects the following path. It is not part of this repair claim. + +## Alternatives rejected + +Allowing known argument strings was rejected because DTLTO arguments are an open-ended subprocess command-line surface; string allowlisting would not prove the selected compiler/distributor implementation, plugin bytes, target/sysroot contents, or transitive files opened by that subprocess. + +Inspecting only the DTLTO executable path was rejected because a fixed executable with mutable or unreviewed arguments can still change code generation and load additional executable code. + +Treating every unknown linker option as hostile was also rejected. The existing parser intentionally distinguishes typed linker controls such as `-z relro` from execution/input authority so the boundary remains precise instead of becoming a blanket option ban. + +## Evidence required for a future exception + +A future DTLTO exception must be owned by CI/release provenance rather than by an ad-hoc source pathname. Evidence must bind the exact distributor and remote-compiler artifact digests, version/toolchain identity, complete forwarded argv, target/sysroot and plugin inputs, working-directory and environment inputs that affect code generation, architecture/ABI, and any files materialized or consumed by distributed backends. It must also provide SBOM/provenance linkage, independent reproducibility or an equivalent deterministic attestation, and explicit cache/invalidation/rollback behavior. + +Repository pathname containment by itself is insufficient because it does not prove the bytes executed or the transitive inputs selected by forwarded subprocess arguments. + +## Acceptance status + +The source-semantic RED → minimal repair lineage is established at the commits above. This document does not claim hosted repository/security GREEN, whole-PR review closure, or release readiness. Those claims require exact-head hosted checks and current-head review after the final reconciled #317 lineage is produced. From f37c9b36db2abd7059d7bcb1aa884c86eef91635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 14:05:25 +0900 Subject: [PATCH 536/632] docs(changelog): record linker execution provenance closures --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcfef3f2..b18834d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. +- Failed closed when repository-owned Rust/rustdoc linker forwarding loads LLD pass-plugin libraries through separated/joined `--load-pass-plugin`, keeping executable LTO pass code inside reviewed toolchain/plugin provenance. +- Failed closed when repository-owned Rust/rustdoc linker forwarding selects Distributed ThinLTO distributor/remote-compiler subprocess authority or forwards their argv through `--thinlto-distributor-arg`, `--thinlto-remote-compiler-prepend-arg`, and `--thinlto-remote-compiler-arg`, preventing repository-owned linker flags from opening unreviewed subprocess/toolchain authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects native libraries through GNU-compatible long-form `--library` / `--library=` aliases, so joined double-dash library selection cannot bypass the reviewed external-input authority boundary. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects LLD `--thinlto-cache-dir=`, preventing mutable cached native ThinLTO objects from becoming unreviewed link inputs outside the reviewed Cargo package/source closure. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects context-sensitive LTO PGO profiles through LLD `--lto-cs-profile-file=` or its one-/two-dash `plugin-opt=cs-profile-path=` aliases, keeping profile-guided code generation inside reviewed build-input provenance. From 9024d61487d7a0127050b97932589e2044ddb019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:02:58 +0900 Subject: [PATCH 537/632] test(browser-session): expose Rust native-link attribute provenance gap --- ...ative_link_attribute_authority_contract.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/test_browser_session_rust_native_link_attribute_authority_contract.py diff --git a/tests/test_browser_session_rust_native_link_attribute_authority_contract.py b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py new file mode 100644 index 000000000..d6e8aa07d --- /dev/null +++ b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py @@ -0,0 +1,92 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + + +def _load_contract(path: pathlib.Path, module_name: str): + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"unable to load {module_name} contract") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +boundary = _load_contract(BOUNDARY_TEST, "browser_session_trusted_adapter_boundary") +source_indirection = _load_contract( + SOURCE_INDIRECTION_TEST, + "browser_session_rust_source_indirection_contract", +) + + +def _assert_no_unmodeled_rust_native_link_inputs(root: pathlib.Path) -> None: + """Apply the current Rust-source provenance contract before native-link authority is modeled.""" + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +class BrowserSessionRustNativeLinkAttributeAuthorityContractTests(unittest.TestCase): + """Keep source-selected native libraries inside reviewed Browser Session provenance.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + return root + + def test_current_production_sources_have_no_unmodeled_native_link_attributes(self) -> None: + _assert_no_unmodeled_rust_native_link_inputs(ROOT) + + def test_direct_native_link_attribute_fails_closed(self) -> None: + root = self._workspace_with_source( + '#[link(name = "review_bypass", kind = "static")]\n' + 'unsafe extern "C" { fn reviewed_symbol(); }\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust link attribute"): + _assert_no_unmodeled_rust_native_link_inputs(root) + + def test_cfg_attr_native_link_attribute_fails_closed(self) -> None: + root = self._workspace_with_source( + '#[cfg_attr(unix, link(name = "review_bypass"))]\n' + 'unsafe extern "C" { fn reviewed_symbol(); }\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust link attribute"): + _assert_no_unmodeled_rust_native_link_inputs(root) + + def test_link_word_inside_attribute_string_is_not_native_link_authority(self) -> None: + root = self._workspace_with_source( + '#[doc = "link(name = \\\"not_an_attribute\\\")"]\n' + 'pub fn documented() {}\n' + ) + + _assert_no_unmodeled_rust_native_link_inputs(root) + + def test_link_section_attribute_is_not_native_library_selection(self) -> None: + root = self._workspace_with_source( + '#[unsafe(link_section = ".reviewed_section")]\n' + 'pub static REVIEWED: u8 = 1;\n' + ) + + _assert_no_unmodeled_rust_native_link_inputs(root) + + +if __name__ == "__main__": + unittest.main() From e3571e51db8f3f0dadbbc54baa5de1813e2c66cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:03:22 +0900 Subject: [PATCH 538/632] fix(browser-session): fail closed source-selected native libraries --- ...ative_link_attribute_authority_contract.py | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_native_link_attribute_authority_contract.py b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py index d6e8aa07d..43d32bf4f 100644 --- a/tests/test_browser_session_rust_native_link_attribute_authority_contract.py +++ b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py @@ -1,5 +1,6 @@ import importlib.util import pathlib +import re import tempfile import unittest @@ -7,6 +8,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] BOUNDARY_TEST = ROOT / "tests/test_browser_session_trusted_adapter_boundary.py" SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" +LINK_META_TOKEN = re.compile(r"(? bool: + """Detect link(...) meta while ignoring Rust comments and string/character literals.""" + cursor = 0 + while cursor < len(attribute_body): + trivia_end = source_indirection._skip_rust_trivia(attribute_body, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(attribute_body, cursor) + if raw_end is not None: + cursor = raw_end + continue + if attribute_body[cursor] == '"': + cursor = source_indirection._quoted_string_end(attribute_body, cursor) + continue + if attribute_body[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(attribute_body, cursor) + if char_end is not None: + cursor = char_end + continue + + match = LINK_META_TOKEN.match(attribute_body, cursor) + if match is not None: + operand = source_indirection._skip_rust_trivia(attribute_body, match.end()) + if operand < len(attribute_body) and attribute_body[operand] == "(": + return True + cursor = match.end() + continue + cursor += 1 + return False + + def _assert_no_unmodeled_rust_native_link_inputs(root: pathlib.Path) -> None: - """Apply the current Rust-source provenance contract before native-link authority is modeled.""" + """Fail closed when reviewed Rust source selects unresolved native-library bytes.""" source_indirection._assert_no_unmodeled_rust_source_indirection(root) + for source in boundary._workspace_production_sources(root): + text = source.read_text(encoding="utf-8") + for attribute_body in source_indirection._rust_attribute_bodies(text): + if _attribute_contains_native_link_meta(attribute_body): + relative = source.relative_to(root).as_posix() + raise AssertionError( + "Rust link attribute requires an explicit native-library provenance contract: " + f"{relative}" + ) class BrowserSessionRustNativeLinkAttributeAuthorityContractTests(unittest.TestCase): From e7258ab8136b019b1570e24de8528aa7ba0cb963 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:03:43 +0900 Subject: [PATCH 539/632] docs(browser-session): trace source-selected native library authority --- ...on-rust-native-link-attribute-authority.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/traceability/browser-session-rust-native-link-attribute-authority.md diff --git a/docs/traceability/browser-session-rust-native-link-attribute-authority.md b/docs/traceability/browser-session-rust-native-link-attribute-authority.md new file mode 100644 index 000000000..5e7193515 --- /dev/null +++ b/docs/traceability/browser-session-rust-native-link-attribute-authority.md @@ -0,0 +1,71 @@ +# Browser Session Rust native-link attribute authority + +## Problem + +The Browser Session production-source closure is reviewed, but reviewed Rust source can itself select native-library bytes that are not part of that source closure. Rust's built-in `#[link(...)]` attribute on an `extern` block names a native library for rustc to link. The Rust Reference defines `dylib` as the default, and also supports `static`, macOS `framework`, and Windows `raw-dylib`; link modifiers can further change how those native bytes participate in the artifact. + +The existing repository-owned Cargo authority contract already fails closed on Git-owned `-L`, `-l`, `--library`, `--extern`, linker positional inputs, and equivalent modeled forwarding paths. That does not prove the artifact selected by a source-level `#[link(name = ...)]`. A reviewed Rust source tree therefore did not, by itself, prove the final Browser Session native dependency closure. + +## Constraint + +`tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. `tests/test_browser_session_rust_source_indirection_contract.py` remains the lexical owner for Rust attribute extraction and source-indirection trivia/literal handling. This supplemental contract consumes both; it does not rediscover workspace topology or introduce a second Rust attribute parser. + +This is an exact-tree provenance rule, not a claim that Rust FFI or the `link` attribute is unsafe as a language feature. Until a versioned native-artifact contract exists, a source-selected native library would depend on bytes resolved from the target toolchain/search environment rather than on the reviewed OriginWeave source closure. + +## RED → repair + +Structural RED **`9024d61487d7a0127050b97932589e2044ddb019`** adds hostile fixtures for: + +- direct `#[link(name = "review_bypass", kind = "static")]`; and +- nested `#[cfg_attr(unix, link(name = "review_bypass"))]`. + +The RED intentionally delegates to the pre-existing Rust-source provenance contract. That predecessor does not classify source-level native-library selection, so both hostile cases are expected to remain unblocked at that generation. Positive controls keep the word `link(...)` inside a documentation string and `#[unsafe(link_section = ...)]` outside this native-library rule. + +Minimal repair **`e3571e51db8f3f0dadbbc54baa5de1813e2c66cb`** stays in the supplemental contract. It reuses the canonical production-source closure and the existing balanced Rust attribute/trivia/literal helpers, then fails closed when an attribute meta tree contains a real `link(...)` meta item. Strings, character literals, comments, and `link_section` are not treated as native-library selectors. No Cargo topology scanner, linker parser, or FFI runtime implementation is duplicated. + +## Decision + +Browser Session production Rust source must not introduce `#[link(...)]` native-library selection until the same reviewed delta defines the selected artifact contract. `cfg_attr(..., link(...))` is governed by the same rule because target configuration can make that native dependency active only on a subset of release targets. + +If a native FFI dependency becomes product-required, the allow contract must identify at least: + +- logical library name and link kind/modifiers; +- exact artifact digest and producer/build provenance; +- target triple, ABI, architecture, and toolchain identity; +- bounded library search roots and symlink/realpath containment; +- static/archive member or dynamic/import-library identity as applicable; +- SBOM and release provenance linkage; +- an independent reproducibility check on a clean environment; and +- invalidation and rollback behavior when the artifact or toolchain changes. + +A library-name-only or path-only allowlist is insufficient because the same `name` can resolve to different bytes under a different sysroot, linker search root, runner image, or target platform. + +## Security and buyer effect + +The rule closes a source-authored native supply-chain path that is independent of Cargo `rustflags`. A code review that sees `#[link(name = "foo")]` can prove intent, but without artifact identity it cannot prove which `foo` bytes entered a static artifact or which runtime library/import library a release depends on. For enterprise browser runtime evidence, that distinction is material to SBOM completeness, provenance, reproducibility, incident response, and rollback. + +The contract deliberately does not ban ordinary `unsafe extern` declarations that do not select a native library. FFI declarations and ABI safety remain Rust/runtime review concerns; this slice owns only native-library artifact selection. + +## Residual surfaces + +Environment/direct-CLI `-L`/`-l` injection, toolchain/sysroot composition, target-default native libraries, linker search roots, deployment-time dynamic loader state, and non-Rust native dependencies remain CI/release supply-chain surfaces. They are not converted into repository-owned authority by this contract. + +Source or macro mechanisms that can synthesize attributes after parsing require compiler-derived or macro-expansion evidence if they can introduce equivalent native-library authority; this lexical contract does not claim macro-expansion completeness. + +## Primary evidence + +Rust Project. (2026). *External blocks: The `link` attribute*. The Rust Reference. https://doc.rust-lang.org/nightly/reference/items/external-blocks.html + +The Reference states that `#[link]` specifies the native library the compiler links for an external block, defines `dylib`, `static`, `framework`, and `raw-dylib`, and documents modifiers such as `whole-archive` and `verbatim`. + +Rust Project. (2026). *Command-line arguments: `-l`*. The rustc book. https://doc.rust-lang.org/nightly/rustc/command-line-arguments.html + +The rustc book documents the native-library `-l` model and explicitly notes that library kind and modifiers can also be specified with a `#[link]` attribute. This connects the source attribute to the same native-input semantics already governed for Git-owned Cargo flags. + +Rust Project. (2026). *Foreign function interface: Linking*. The Rustonomicon. https://doc.rust-lang.org/nomicon/ffi.html + +The Rustonomicon explains that the `link` attribute instructs rustc how to link native libraries and that static native libraries can be incorporated into output artifacts while dynamic dependencies propagate to the final artifact boundary. + +## Verification state + +The RED and repair are structurally present on the active #317 lineage. The exact head still lacks hosted repository/security execution evidence, so this dossier does not claim executable GREEN, full current-head review closure, protected-main integration, or release readiness. From 5ae81cc7657327ca901d968dba94389c723200cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:08:16 +0900 Subject: [PATCH 540/632] test(browser-session): reproduce commented link attribute false positive --- ...st_native_link_attribute_authority_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_browser_session_rust_native_link_attribute_authority_contract.py b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py index 43d32bf4f..5e2e5cbc7 100644 --- a/tests/test_browser_session_rust_native_link_attribute_authority_contract.py +++ b/tests/test_browser_session_rust_native_link_attribute_authority_contract.py @@ -115,6 +115,21 @@ def test_cfg_attr_native_link_attribute_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust link attribute"): _assert_no_unmodeled_rust_native_link_inputs(root) + def test_commented_native_link_attribute_is_not_native_link_authority(self) -> None: + root = self._workspace_with_source( + '// #[link(name = "review_bypass")]\n' + 'pub fn documented() {}\n' + ) + + _assert_no_unmodeled_rust_native_link_inputs(root) + + def test_string_containing_native_link_attribute_is_not_native_link_authority(self) -> None: + root = self._workspace_with_source( + 'pub const NOTE: &str = "#[link(name = \\"review_bypass\\")]";\n' + ) + + _assert_no_unmodeled_rust_native_link_inputs(root) + def test_link_word_inside_attribute_string_is_not_native_link_authority(self) -> None: root = self._workspace_with_source( '#[doc = "link(name = \\\"not_an_attribute\\\")"]\n' From efb8f1da2f1f3aa98d947e4c6c611e58408cd09b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:09:08 +0900 Subject: [PATCH 541/632] fix(browser-session): lex real Rust attributes before provenance checks --- ...ession_rust_source_indirection_contract.py | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index f96fd349e..15a1d971c 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -167,13 +167,40 @@ def _simple_char_literal_end(text: str, offset: int) -> int | None: return None +def _next_rust_attribute_marker(text: str, offset: int) -> int | None: + """Find the next lexical `#` outside Rust comments and string/character literals.""" + cursor = offset + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = _raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = _quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = _simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + if text[cursor] == "#": + return cursor + cursor += 1 + return None + + def _rust_attribute_bodies(text: str) -> list[str]: """Extract balanced Rust attribute token trees while respecting lexical trivia and literals.""" bodies: list[str] = [] search_from = 0 while True: - marker = text.find("#", search_from) - if marker < 0: + marker = _next_rust_attribute_marker(text, search_from) + if marker is None: break cursor = _skip_rust_trivia(text, marker + 1) @@ -430,6 +457,21 @@ def test_parent_traversal_path_attribute_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust path attribute requires"): _assert_no_unmodeled_rust_source_indirection(root) + def test_commented_path_attribute_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + '// #[path = "review_bypass.rs"]\n' + 'pub fn reviewed_surface() {}\n' + ) + + _assert_no_unmodeled_rust_source_indirection(root) + + def test_string_containing_path_attribute_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + 'pub const NOTE: &str = "#[path = \\"review_bypass.rs\\"]";\n' + ) + + _assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From 84fc5b9fa63ff5e368cbd659a353452f5049616e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 15:09:44 +0900 Subject: [PATCH 542/632] docs(browser-session): trace Rust attribute lexer root repair --- ...on-rust-native-link-attribute-authority.md | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-native-link-attribute-authority.md b/docs/traceability/browser-session-rust-native-link-attribute-authority.md index 5e7193515..29ffaa38d 100644 --- a/docs/traceability/browser-session-rust-native-link-attribute-authority.md +++ b/docs/traceability/browser-session-rust-native-link-attribute-authority.md @@ -21,7 +21,25 @@ Structural RED **`9024d61487d7a0127050b97932589e2044ddb019`** adds hostile fixtu The RED intentionally delegates to the pre-existing Rust-source provenance contract. That predecessor does not classify source-level native-library selection, so both hostile cases are expected to remain unblocked at that generation. Positive controls keep the word `link(...)` inside a documentation string and `#[unsafe(link_section = ...)]` outside this native-library rule. -Minimal repair **`e3571e51db8f3f0dadbbc54baa5de1813e2c66cb`** stays in the supplemental contract. It reuses the canonical production-source closure and the existing balanced Rust attribute/trivia/literal helpers, then fails closed when an attribute meta tree contains a real `link(...)` meta item. Strings, character literals, comments, and `link_section` are not treated as native-library selectors. No Cargo topology scanner, linker parser, or FFI runtime implementation is duplicated. +Minimal repair **`e3571e51db8f3f0dadbbc54baa5de1813e2c66cb`** stays in the supplemental contract. It reuses the canonical production-source closure and the existing balanced Rust attribute/trivia/literal helpers, then fails closed when an attribute meta tree contains a real `link(...)` meta item. No Cargo topology scanner, linker parser, or FFI runtime implementation is duplicated. + +Focused review of exact **`e7258ab8136b019b1570e24de8528aa7ba0cb963`** then found a valid root lexical defect in the shared attribute extractor: the outer `_rust_attribute_bodies()` search used `text.find("#", ...)` before lexical filtering. As a result, `#[link(...)]` text inside a line comment or ordinary string could be extracted as if it were a real attribute and rejected by the supplemental contract. This was a source-level false positive, not a reason to weaken the native-library authority rule or add a parallel parser. + +Review-driven RED **`5ae81cc7657327ca901d968dba94389c723200cc`** adds explicit controls for both forms: + +- `// #[link(name = "review_bypass")]`; and +- an ordinary Rust string containing `#[link(name = "review_bypass")]`. + +Canonical root repair **`efb8f1da2f1f3aa98d947e4c6c611e58408cd09b`** changes the shared Rust-source lexical owner instead of working around it in the native-link contract. `_next_rust_attribute_marker()` now scans for the next attribute marker while skipping whitespace/comments, raw strings, conventional strings, and simple character literals with the already-owned lexical helpers; `_rust_attribute_bodies()` consumes that marker. The same repair adds path-attribute controls proving that `#[path = ...]` text inside a line comment or ordinary string is not source indirection. The existing balanced attribute-body parser remains authoritative for both source-indirection and native-link consumers. + +The intended post-repair semantics are therefore: + +- real direct `#[link(...)]`: fail closed; +- real nested `cfg_attr(..., link(...))`: fail closed; +- `link(...)` inside a real attribute string: allowed unless another modeled meta item grants authority; +- `#[link(...)]` text inside source comments/strings: ignored as lexical data; +- `link_section`: outside this native-library-selection rule; and +- macro-expanded equivalent attributes: still a residual surface requiring compiler-derived or macro-expansion evidence. ## Decision @@ -46,6 +64,8 @@ The rule closes a source-authored native supply-chain path that is independent o The contract deliberately does not ban ordinary `unsafe extern` declarations that do not select a native library. FFI declarations and ABI safety remain Rust/runtime review concerns; this slice owns only native-library artifact selection. +The shared lexical repair also matters to buyer-visible evidence quality: comments and diagnostic strings must not create phantom dependency findings that could turn a repository policy gate into a source-text keyword filter rather than a reviewable Rust authority boundary. + ## Residual surfaces Environment/direct-CLI `-L`/`-l` injection, toolchain/sysroot composition, target-default native libraries, linker search roots, deployment-time dynamic loader state, and non-Rust native dependencies remain CI/release supply-chain surfaces. They are not converted into repository-owned authority by this contract. @@ -68,4 +88,6 @@ The Rustonomicon explains that the `link` attribute instructs rustc how to link ## Verification state -The RED and repair are structurally present on the active #317 lineage. The exact head still lacks hosted repository/security execution evidence, so this dossier does not claim executable GREEN, full current-head review closure, protected-main integration, or release readiness. +The initial RED, native-link repair, review-driven false-positive RED, canonical lexical root repair, and this traceability successor are structurally present on the active #317 lineage. The focused review finding is treated as valid and repaired at its canonical lexical owner rather than suppressed. + +The environment available to this writer cannot resolve `github.com` for a local clone, and the exact head still has no pull-request-triggered hosted workflow execution. Therefore no executable GREEN, full current-head review closure, protected-main integration, 100% owned coverage closure, or release readiness is claimed from this dossier alone. From 5cac6feeadb008e200c8590707f7f18d19f9c6c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:05:19 +0900 Subject: [PATCH 543/632] test(browser-session): expose embedded file input gap --- ..._embedded_file_input_authority_contract.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_browser_session_rust_embedded_file_input_authority_contract.py diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py new file mode 100644 index 000000000..e184a3c10 --- /dev/null +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -0,0 +1,63 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustEmbeddedFileInputAuthorityContractTests(unittest.TestCase): + """Keep compile-time embedded files inside explicit Browser Session source provenance.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + (adapter / "unreviewed.bin").write_bytes(b"unreviewed-browser-runtime-bytes") + (adapter / "unreviewed.txt").write_text( + "unreviewed browser runtime text\n", + encoding="utf-8", + ) + return root + + def test_include_bytes_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'pub static EMBEDDED: &[u8] = include_bytes!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_include_str_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'pub static EMBEDDED: &str = include_str!("../unreviewed.txt");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From c163991ddc84fd519194cdae54643b252ec316d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:05:53 +0900 Subject: [PATCH 544/632] fix(browser-session): govern Rust embedded file inputs --- ..._embedded_file_input_authority_contract.py | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index e184a3c10..6c38d76e2 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -1,11 +1,16 @@ import importlib.util import pathlib +import re import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" +EMBEDDED_FILE_MACRO_TOKEN = re.compile( + r"(? bool: + """Detect compile-time file embedding outside Rust comments and literals.""" + cursor = 0 + while cursor < len(text): + trivia_end = source_indirection._skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = source_indirection._quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + match = EMBEDDED_FILE_MACRO_TOKEN.match(text, cursor) + if match is not None: + bang = source_indirection._skip_rust_trivia(text, match.end()) + if bang < len(text) and text[bang] == "!": + return True + cursor = match.end() + continue + cursor += 1 + return False + + +def _assert_no_unmodeled_rust_embedded_file_inputs(root: pathlib.Path) -> None: + """Fail closed when reviewed Rust source embeds file bytes outside the source closure.""" + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + for source in source_indirection.boundary._workspace_production_sources(root): + text = source.read_text(encoding="utf-8") + if _has_embedded_file_macro(text): + relative = source.relative_to(root).as_posix() + raise AssertionError( + "Rust embedded file input requires an explicit provenance contract: " + f"{relative}" + ) + + class BrowserSessionRustEmbeddedFileInputAuthorityContractTests(unittest.TestCase): """Keep compile-time embedded files inside explicit Browser Session source provenance.""" @@ -42,13 +93,16 @@ def _workspace_with_source(self, source_text: str) -> pathlib.Path: ) return root + def test_current_production_sources_have_no_unmodeled_embedded_file_inputs(self) -> None: + _assert_no_unmodeled_rust_embedded_file_inputs(ROOT) + def test_include_bytes_file_input_fails_closed(self) -> None: root = self._workspace_with_source( 'pub static EMBEDDED: &[u8] = include_bytes!("../unreviewed.bin");\n' ) with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): - source_indirection._assert_no_unmodeled_rust_source_indirection(root) + _assert_no_unmodeled_rust_embedded_file_inputs(root) def test_include_str_file_input_fails_closed(self) -> None: root = self._workspace_with_source( @@ -56,7 +110,24 @@ def test_include_str_file_input_fails_closed(self) -> None: ) with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): - source_indirection._assert_no_unmodeled_rust_source_indirection(root) + _assert_no_unmodeled_rust_embedded_file_inputs(root) + + def test_namespaced_include_bytes_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'pub static EMBEDDED: &[u8] = core::include_bytes!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + + def test_comment_and_string_mentions_are_not_embedded_file_authority(self) -> None: + root = self._workspace_with_source( + '// include_bytes!("../unreviewed.bin")\n' + 'pub const NOTE: &str = "include_str!(\\\"../unreviewed.txt\\\")";\n' + 'pub fn include_bytes_count() -> usize { 0 }\n' + ) + + _assert_no_unmodeled_rust_embedded_file_inputs(root) if __name__ == "__main__": From c52ad3a5fdcc7e482e8a6b872e1e3d44fed6477a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:06:33 +0900 Subject: [PATCH 545/632] docs(browser-session): trace embedded Rust file inputs --- ...sion-rust-embedded-file-input-authority.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/traceability/browser-session-rust-embedded-file-input-authority.md diff --git a/docs/traceability/browser-session-rust-embedded-file-input-authority.md b/docs/traceability/browser-session-rust-embedded-file-input-authority.md new file mode 100644 index 000000000..1ae5a81e3 --- /dev/null +++ b/docs/traceability/browser-session-rust-embedded-file-input-authority.md @@ -0,0 +1,60 @@ +# Browser Session Rust embedded-file input authority + +## Status + +Implemented on PR #317 as a focused repository contract. This document records source-semantic evidence only; it is not hosted CI, protected-main integration, or release evidence. + +## Problem + +`include_bytes!` and `include_str!` are compile-time file inputs. Rust 1.98.1 documents that both macros locate a file relative to the current source file at compile time; `include_bytes!` places the file bytes in a `&'static [u8; N]`, while `include_str!` places UTF-8 file contents in a `&'static str`. + +Before this generation, `tests/test_browser_session_rust_source_indirection_contract.py` governed `include!`, module/path indirection, and the shared Rust lexical helpers, but it did not classify `include_bytes!` or `include_str!`. A reviewed production `.rs` file could therefore select additional file bytes that were not represented in the canonical production-source closure. + +For Browser Session, that is a provenance gap: the final artifact may contain compile-time-selected bytes even though the selected file itself is outside the source set inspected by the trusted-adapter/source-indirection contracts. + +## Decision + +Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and keep `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. + +Add a focused supplemental contract, `tests/test_browser_session_rust_embedded_file_input_authority_contract.py`, that: + +- first consumes the existing source-indirection contract; +- consumes the canonical production source closure instead of rediscovering Cargo topology; +- reuses the shared trivia, raw-string, quoted-string, and character-literal lexer helpers; +- fails closed when lexical production source invokes `include_bytes!` or `include_str!`, including namespaced spellings such as `core::include_bytes!`; +- does not classify mentions inside Rust comments or string/character/raw-string literals as file-input authority. + +The policy is intentionally conservative about macro resolution. A locally shadowed macro named `include_bytes!` or `include_str!` is still rejected until macro-expansion provenance is modeled. This avoids allowing name shadowing to become a bypass around the compile-time file-input boundary. + +## RED → repair evidence + +Structural RED: `5cac6feeadb008e200c8590707f7f18d19f9c6c5`. + +The RED adds realistic workspaces with existing `unreviewed.bin` and `unreviewed.txt` files and requires the pre-existing source-indirection assertion to reject `include_bytes!(...)` and `include_str!(...)`. The predecessor has no such classifier, so those requirements expose the gap rather than manufacturing an unrelated failure. + +Minimal repair: `c163991ddc84fd519194cdae54643b252ec316d2`. + +The repair changes only the new focused contract. It introduces the embedded-file macro classifier, delegates all package/source discovery to the existing owners, adds a current-production postcondition, covers direct and namespaced selectors, and adds comment/string controls. No Cargo topology, runtime browser behavior, linker authority, or cross-repository owner is duplicated. + +## Security and buyer effect + +The contract prevents Git-reviewed Browser Session Rust source from silently importing unmodeled compile-time file bytes through these two standard macros. This narrows artifact provenance to inputs that have an explicit reviewed contract rather than relying on the source file alone as evidence of what entered the binary. + +This is necessary but not sufficient for release provenance. Ambient filesystem contents, environment-driven paths, proc-macro or declarative-macro expansion that synthesizes equivalent file inputs, generated source, and direct compiler invocation remain CI/release supply-chain surfaces unless separately attested. + +## Acceptance and rollback + +Acceptance for this generation requires all of the following on the reconciled exact head: + +- the focused contract passes together with the existing Rust source-indirection and trusted-adapter contracts; +- repository/security workflows run on the exact head and pass without gate weakening; +- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership; +- release evidence, if a release is produced, records the exact source tree, toolchain, filesystem/input provenance, SBOM/provenance, and reproducible-build result. + +If the product later needs compile-time embedded files, do not delete the fail-closed rule. Replace it in the same reviewed change with a versioned manifest or equivalent contract that binds source selector, canonical path, content digest, producer/source identity, containment/symlink rules, target/toolchain compatibility, SBOM/provenance, independent reproducibility, invalidation, and rollback. + +## References + +Rust Project. (2026). *include_bytes macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/stable/std/macro.include_bytes.html + +Rust Project. (2026). *include_str macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/stable/std/macro.include_str.html From 87eb778a1b5526811e43b851fe839755ee224ca2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:12:47 +0900 Subject: [PATCH 546/632] test(browser-session): expose source environment input gap --- ...ile_time_environment_authority_contract.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/test_browser_session_rust_compile_time_environment_authority_contract.py diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py new file mode 100644 index 000000000..12a65b64a --- /dev/null +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -0,0 +1,66 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustCompileTimeEnvironmentAuthorityContractTests(unittest.TestCase): + """Expose ambient compile-time environment inputs outside reviewed Rust source provenance.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + return root + + def test_env_macro_exposes_unmodeled_compile_time_environment_input(self) -> None: + root = self._workspace_with_source( + 'pub const BUILD_ID: &str = env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_option_env_macro_exposes_unmodeled_compile_time_environment_input(self) -> None: + root = self._workspace_with_source( + 'pub const BUILD_ID: Option<&str> = option_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_comment_and_string_mentions_are_not_compile_time_environment_authority(self) -> None: + root = self._workspace_with_source( + '// env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")\n' + 'pub const NOTE: &str = "option_env!(\\\"ORIGINWEAVE_UNREVIEWED_BUILD_ID\\\")";\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From a6eec1a700aff4cd5ad807629e6f55e44abde2aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:13:08 +0900 Subject: [PATCH 547/632] fix(browser-session): govern source environment inputs --- ...ile_time_environment_authority_contract.py | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 12a65b64a..7d828ceab 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -1,11 +1,16 @@ import importlib.util import pathlib +import re import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" +COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN = re.compile( + r"(? bool: + """Detect compile-time environment reads outside Rust comments and literals.""" + cursor = 0 + while cursor < len(text): + trivia_end = source_indirection._skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = source_indirection._quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + match = COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN.match(text, cursor) + if match is not None: + bang = source_indirection._skip_rust_trivia(text, match.end()) + if bang < len(text) and text[bang] == "!": + return True + cursor = match.end() + continue + cursor += 1 + return False + + +def _assert_no_unmodeled_rust_compile_time_environment_inputs(root: pathlib.Path) -> None: + """Fail closed when reviewed Rust source reads ambient build environment values.""" + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + for source in source_indirection.boundary._workspace_production_sources(root): + text = source.read_text(encoding="utf-8") + if _has_compile_time_environment_macro(text): + relative = source.relative_to(root).as_posix() + raise AssertionError( + "Rust compile-time environment input requires an explicit provenance contract: " + f"{relative}" + ) + + class BrowserSessionRustCompileTimeEnvironmentAuthorityContractTests(unittest.TestCase): - """Expose ambient compile-time environment inputs outside reviewed Rust source provenance.""" + """Keep Rust compile-time environment inputs inside reviewed provenance.""" def _workspace_with_source(self, source_text: str) -> pathlib.Path: directory = tempfile.TemporaryDirectory() @@ -37,29 +88,41 @@ def _workspace_with_source(self, source_text: str) -> pathlib.Path: (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") return root - def test_env_macro_exposes_unmodeled_compile_time_environment_input(self) -> None: + def test_current_production_sources_have_no_unmodeled_compile_time_environment_inputs(self) -> None: + _assert_no_unmodeled_rust_compile_time_environment_inputs(ROOT) + + def test_env_macro_fails_closed(self) -> None: root = self._workspace_with_source( 'pub const BUILD_ID: &str = env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' ) with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): - source_indirection._assert_no_unmodeled_rust_source_indirection(root) + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) - def test_option_env_macro_exposes_unmodeled_compile_time_environment_input(self) -> None: + def test_option_env_macro_fails_closed(self) -> None: root = self._workspace_with_source( 'pub const BUILD_ID: Option<&str> = option_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' ) with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): - source_indirection._assert_no_unmodeled_rust_source_indirection(root) + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_namespaced_env_macro_fails_closed(self) -> None: + root = self._workspace_with_source( + 'pub const BUILD_ID: &str = std::env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) def test_comment_and_string_mentions_are_not_compile_time_environment_authority(self) -> None: root = self._workspace_with_source( '// env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")\n' 'pub const NOTE: &str = "option_env!(\\\"ORIGINWEAVE_UNREVIEWED_BUILD_ID\\\")";\n' + 'pub fn env_count() -> usize { 0 }\n' ) - source_indirection._assert_no_unmodeled_rust_source_indirection(root) + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) if __name__ == "__main__": From 75eb414f69a7be2dcc851aca58d47e156a41133c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:13:27 +0900 Subject: [PATCH 548/632] docs(browser-session): trace source environment inputs --- ...rust-compile-time-environment-authority.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/traceability/browser-session-rust-compile-time-environment-authority.md diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md new file mode 100644 index 000000000..259111ee5 --- /dev/null +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -0,0 +1,63 @@ +# Browser Session Rust compile-time environment authority + +## Status + +Implemented on PR #317 as a focused repository contract. This document records source-semantic evidence only; it is not hosted CI, protected-main integration, or release evidence. + +## Problem + +Rust 1.98.1 documents `env!` as reading an environment variable at compile time and expanding to its string value, and `option_env!` as the optional form that expands to `Option<&'static str>`. These values can therefore enter Browser Session artifacts without appearing in the reviewed Rust source, dependency graph, or linker-input set. + +Cargo can also set compilation environment values through build-script `cargo::rustc-env=VAR=VALUE`. The Cargo Book explicitly describes retrieving such values with `env!` in the compiled crate. Repository `[env]` configuration is already governed by `tests/test_browser_session_cargo_environment_authority_contract.py`, but that contract does not make every ambient runner variable or build-script-produced value part of reviewed artifact provenance. + +Before this generation, the Rust source-indirection owner governed `include!`, module/path indirection, and the shared Rust lexical helpers; the embedded-file supplement governed `include_bytes!` and `include_str!`. Neither classified direct source-level `env!` or `option_env!`. A reviewed production `.rs` file could therefore make artifact content depend on a build environment value whose producer, value, and lifecycle were outside the source closure. + +## Decision + +Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. + +Add `tests/test_browser_session_rust_compile_time_environment_authority_contract.py` as a focused supplemental contract that: + +- first consumes the existing source-indirection assertion; +- consumes the canonical production-source closure instead of rediscovering Cargo topology; +- reuses the shared trivia, raw-string, quoted-string, and character-literal lexer helpers; +- fails closed on lexical `env!` and `option_env!`, including namespaced spellings; +- ignores mentions inside comments and string/character/raw-string literals; +- conservatively rejects locally shadowed macros with the same names until macro-expansion provenance is modeled. + +The policy does not treat runtime `std::env::var` as the same build-input class. Runtime environment access is a separate product/runtime authority concern and must be governed by the runtime boundary that owns it. + +## RED → repair evidence + +Structural RED: `87eb778a1b5526811e43b851fe839755ee224ca2`. + +The RED adds realistic workspaces whose production Rust source calls `env!` and `option_env!` and asks the pre-existing source-indirection assertion to reject them. The predecessor `c52ad3a5fdcc7e482e8a6b872e1e3d44fed6477a` has no compile-time environment classifier, so those assertions expose the missing provenance boundary while comment/string controls remain accepted. + +Minimal repair: `a6eec1a700aff4cd5ad807629e6f55e44abde2aa`. + +The repair stays inside the new focused contract. It adds one compile-time-environment macro classifier, delegates source discovery and lexical handling to existing owners, adds a current-production postcondition, covers direct/optional/namespaced forms, and preserves comment/string controls. No Cargo topology, runtime environment policy, browser behavior, linker authority, or cross-repository owner is duplicated. + +## Security and buyer effect + +The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata. + +This is necessary but not sufficient for reproducible release evidence. Runner environment, build-script output, proc-macro or declarative-macro expansion that synthesizes equivalent calls, generated source, direct compiler invocation, and externally injected Cargo environment remain CI/release supply-chain evidence surfaces unless separately attested. + +## Acceptance and rollback + +Acceptance for this generation requires all of the following on the reconciled exact head: + +- the focused contract passes with the existing Rust source-indirection, embedded-file, Cargo-environment, and trusted-adapter contracts; +- repository/security workflows run on the exact head and pass without gate weakening; +- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership; +- release evidence, if produced, binds the exact source tree, toolchain, environment-variable names and values that may affect compilation, producer identity for generated values, SBOM/provenance, and an independent reproducible-build result. + +If the product later needs a compile-time environment value, do not delete the fail-closed rule. Replace it in the same reviewed change with a versioned contract that binds variable name, purpose, producer, canonical value or digest, secrecy classification, target/toolchain scope, invalidation semantics, SBOM/provenance linkage, independent reproducibility, and rollback. + +## References + +Rust Project. (2026). *env macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/core/macro.env.html + +Rust Project. (2026). *option_env macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/core/macro.option_env.html + +Rust Project. (2026). *Build scripts*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/build-scripts.html From f9934fe67c6cf7bd5c0ab946be6b881503a14c65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:18:18 +0900 Subject: [PATCH 549/632] test(browser-session): close source environment lexical coverage --- ...ile_time_environment_authority_contract.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 7d828ceab..42241c6e6 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -115,15 +115,33 @@ def test_namespaced_env_macro_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): _assert_no_unmodeled_rust_compile_time_environment_inputs(root) - def test_comment_and_string_mentions_are_not_compile_time_environment_authority(self) -> None: + def test_namespaced_option_env_macro_fails_closed(self) -> None: + root = self._workspace_with_source( + 'pub const BUILD_ID: Option<&str> = core::option_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_comment_string_and_raw_string_mentions_are_not_compile_time_environment_authority(self) -> None: root = self._workspace_with_source( '// env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")\n' 'pub const NOTE: &str = "option_env!(\\\"ORIGINWEAVE_UNREVIEWED_BUILD_ID\\\")";\n' + 'pub const RAW_NOTE: &str = r#"env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")"#;\n' 'pub fn env_count() -> usize { 0 }\n' ) _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + def test_character_literal_does_not_hide_following_real_macro(self) -> None: + root = self._workspace_with_source( + "pub const MARKER: char = 'x';\n" + 'pub const BUILD_ID: &str = env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + if __name__ == "__main__": unittest.main() From f12499cba44f95733cd4d6b4cafcc089e20d3f65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 16:18:39 +0900 Subject: [PATCH 550/632] docs(browser-session): record source environment review closure --- .../browser-session-rust-compile-time-environment-authority.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md index 259111ee5..41aa77007 100644 --- a/docs/traceability/browser-session-rust-compile-time-environment-authority.md +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -37,6 +37,8 @@ Minimal repair: `a6eec1a700aff4cd5ad807629e6f55e44abde2aa`. The repair stays inside the new focused contract. It adds one compile-time-environment macro classifier, delegates source discovery and lexical handling to existing owners, adds a current-production postcondition, covers direct/optional/namespaced forms, and preserves comment/string controls. No Cargo topology, runtime environment policy, browser behavior, linker authority, or cross-repository owner is duplicated. +Focused review of traceability exact `75eb414f69a7be2dcc851aca58d47e156a41133c` found a valid fixture-coverage gap rather than a classifier defect: the supplemental contract depended on shared raw-string and character-literal handling without directly exercising those boundaries, and it covered namespaced `env!` but not namespaced `option_env!`. Review-driven coverage repair `f9934fe67c6cf7bd5c0ab946be6b881503a14c65` changes only the focused contract (`+19/-1`). It adds a raw-string false-positive control, proves that scanning resumes after a character literal and still rejects a following real macro, and rejects `core::option_env!`. + ## Security and buyer effect The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata. From fb2379a397213a37801bc4a7d53a11d3131089df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 17:04:55 +0900 Subject: [PATCH 551/632] docs(browser-session): record compile-time env re-review --- .../browser-session-rust-compile-time-environment-authority.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md index 41aa77007..115621c0c 100644 --- a/docs/traceability/browser-session-rust-compile-time-environment-authority.md +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -39,6 +39,8 @@ The repair stays inside the new focused contract. It adds one compile-time-envir Focused review of traceability exact `75eb414f69a7be2dcc851aca58d47e156a41133c` found a valid fixture-coverage gap rather than a classifier defect: the supplemental contract depended on shared raw-string and character-literal handling without directly exercising those boundaries, and it covered namespaced `env!` but not namespaced `option_env!`. Review-driven coverage repair `f9934fe67c6cf7bd5c0ab946be6b881503a14c65` changes only the focused contract (`+19/-1`). It adds a raw-string false-positive control, proves that scanning resumes after a character literal and still rejects a following real macro, and rejects `core::option_env!`. +Focused re-review of exact `f12499cba44f95733cd4d6b4cafcc089e20d3f65` found no defect in this compile-time-environment provenance slice. The review confirmed that the RED, initial repair, and review-driven test repair are ancestors of that exact head, that the test repair is limited to `tests/test_browser_session_rust_compile_time_environment_authority_contract.py` at `+19/-1`, and that `git diff --check` is clean. This remains static focused review evidence only; it does not establish hosted GREEN or whole-PR acceptance. + ## Security and buyer effect The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata. From be925ecd639930567140894af672dc13a959792a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 17:09:16 +0900 Subject: [PATCH 552/632] docs(browser-session): record Rust source provenance fixes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18834d6c..fa51a11e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed +- Failed closed when Browser Session production Rust source selects native libraries through direct `#[link(...)]` or `cfg_attr(..., link(...))` attributes, while the shared Rust attribute lexer treats comment and string/character/raw-string text as lexical data rather than authority. +- Failed closed when Browser Session production Rust source embeds compile-time files through `include_bytes!` or `include_str!`, reusing the canonical production-source closure and shared Rust source-indirection lexer instead of rediscovering Cargo topology. +- Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding loads LLD pass-plugin libraries through separated/joined `--load-pass-plugin`, keeping executable LTO pass code inside reviewed toolchain/plugin provenance. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects Distributed ThinLTO distributor/remote-compiler subprocess authority or forwards their argv through `--thinlto-distributor-arg`, `--thinlto-remote-compiler-prepend-arg`, and `--thinlto-remote-compiler-arg`, preventing repository-owned linker flags from opening unreviewed subprocess/toolchain authority. From 369b807db9988844626dcf2ea1f38eb67379e5e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:03:20 +0900 Subject: [PATCH 553/632] test(browser-session): expose aliased embedded-file macro bypass --- ...t_embedded_file_input_authority_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index 6c38d76e2..70668fdaf 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -120,6 +120,24 @@ def test_namespaced_include_bytes_file_input_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_aliased_include_bytes_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use core::include_bytes as read_blob;\n' + 'pub static EMBEDDED: &[u8] = read_blob!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + + def test_aliased_include_str_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use std::include_str as read_text;\n' + 'pub static EMBEDDED: &str = read_text!("../unreviewed.txt");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_comment_and_string_mentions_are_not_embedded_file_authority(self) -> None: root = self._workspace_with_source( '// include_bytes!("../unreviewed.bin")\n' From ae1cf874a39ffd4b00a67216d6a2caa62e615721 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:03:54 +0900 Subject: [PATCH 554/632] fix(browser-session): reject aliased Rust embedded-file macros --- ..._embedded_file_input_authority_contract.py | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index 70668fdaf..d894e9c85 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -55,12 +55,72 @@ def _has_embedded_file_macro(text: str) -> bool: return False +def _use_tree_aliases_embedded_file_macro(use_tree: str) -> bool: + """Return whether one Rust use tree renames include_bytes!/include_str! authority.""" + cursor = 0 + while cursor < len(use_tree): + trivia_end = source_indirection._skip_rust_trivia(use_tree, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + match = EMBEDDED_FILE_MACRO_TOKEN.match(use_tree, cursor) + if match is None: + cursor += 1 + continue + + after_macro = source_indirection._skip_rust_trivia(use_tree, match.end()) + as_match = source_indirection.AS_TOKEN.match(use_tree, after_macro) + if as_match is not None: + alias_start = source_indirection._skip_rust_trivia(use_tree, as_match.end()) + if alias_start < len(use_tree) and use_tree[alias_start] != "_": + return True + cursor = match.end() + return False + + +def _has_aliased_embedded_file_import(text: str) -> bool: + """Detect lexical use aliases that would hide embedded-file macro names at invocation.""" + cursor = 0 + while cursor < len(text): + trivia_end = source_indirection._skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = source_indirection._quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + use_match = source_indirection.USE_TOKEN.match(text, cursor) + if use_match is None: + cursor += 1 + continue + + statement_end = source_indirection._rust_use_statement_end(text, use_match.end()) + if statement_end is None: + return False + if _use_tree_aliases_embedded_file_macro(text[use_match.end():statement_end]): + return True + cursor = statement_end + 1 + return False + + def _assert_no_unmodeled_rust_embedded_file_inputs(root: pathlib.Path) -> None: """Fail closed when reviewed Rust source embeds file bytes outside the source closure.""" source_indirection._assert_no_unmodeled_rust_source_indirection(root) for source in source_indirection.boundary._workspace_production_sources(root): text = source.read_text(encoding="utf-8") - if _has_embedded_file_macro(text): + if _has_embedded_file_macro(text) or _has_aliased_embedded_file_import(text): relative = source.relative_to(root).as_posix() raise AssertionError( "Rust embedded file input requires an explicit provenance contract: " From 423c4088409215e30199943b7167150b3082fa3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:04:32 +0900 Subject: [PATCH 555/632] test(browser-session): cover underscore-prefixed embedded macro aliases --- ...ession_rust_embedded_file_input_authority_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index d894e9c85..3b4f4e5c2 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -189,6 +189,15 @@ def test_aliased_include_bytes_file_input_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_underscore_prefixed_alias_still_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use core::include_bytes as _read_blob;\n' + 'pub static EMBEDDED: &[u8] = _read_blob!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_aliased_include_str_file_input_fails_closed(self) -> None: root = self._workspace_with_source( 'use std::include_str as read_text;\n' From b978c3c79ad0d0938bdb9d14c712693b9ed87417 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:04:54 +0900 Subject: [PATCH 556/632] fix(browser-session): distinguish underscore macro imports from aliases --- ..._rust_embedded_file_input_authority_contract.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index 3b4f4e5c2..8bcc990d4 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -56,7 +56,7 @@ def _has_embedded_file_macro(text: str) -> bool: def _use_tree_aliases_embedded_file_macro(use_tree: str) -> bool: - """Return whether one Rust use tree renames include_bytes!/include_str! authority.""" + """Return whether one Rust use tree gives include_bytes!/include_str! a callable alias.""" cursor = 0 while cursor < len(use_tree): trivia_end = source_indirection._skip_rust_trivia(use_tree, cursor) @@ -73,8 +73,16 @@ def _use_tree_aliases_embedded_file_macro(use_tree: str) -> bool: as_match = source_indirection.AS_TOKEN.match(use_tree, after_macro) if as_match is not None: alias_start = source_indirection._skip_rust_trivia(use_tree, as_match.end()) - if alias_start < len(use_tree) and use_tree[alias_start] != "_": - return True + if alias_start >= len(use_tree): + return False + if use_tree[alias_start] == "_": + next_offset = alias_start + 1 + if next_offset >= len(use_tree) or not ( + use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" + ): + cursor = match.end() + continue + return True cursor = match.end() return False From 3f40f6662cf68c301579d3a70b3e76d0325eb705 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:05:23 +0900 Subject: [PATCH 557/632] test(browser-session): harden embedded macro alias grammar coverage --- ...t_embedded_file_input_authority_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index 8bcc990d4..ca3e34b6c 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -197,6 +197,15 @@ def test_aliased_include_bytes_file_input_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_grouped_aliased_include_bytes_file_input_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use core::{include_bytes as read_blob};\n' + 'pub static EMBEDDED: &[u8] = read_blob!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_underscore_prefixed_alias_still_fails_closed(self) -> None: root = self._workspace_with_source( 'use core::include_bytes as _read_blob;\n' @@ -215,9 +224,18 @@ def test_aliased_include_str_file_input_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_underscore_import_is_not_callable_alias_authority(self) -> None: + root = self._workspace_with_source( + 'use core::include_bytes as _;\n' + 'pub fn embedded_file_authority_control() -> usize { 0 }\n' + ) + + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_comment_and_string_mentions_are_not_embedded_file_authority(self) -> None: root = self._workspace_with_source( '// include_bytes!("../unreviewed.bin")\n' + '// use core::include_bytes as hidden_in_comment;\n' 'pub const NOTE: &str = "include_str!(\\\"../unreviewed.txt\\\")";\n' 'pub fn include_bytes_count() -> usize { 0 }\n' ) From 2c108fc14e9a2eaee79ea16219248c42aa1fd815 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:05:42 +0900 Subject: [PATCH 558/632] docs(traceability): record Rust embedded macro alias authority --- ...sion-rust-embedded-file-input-authority.md | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/traceability/browser-session-rust-embedded-file-input-authority.md b/docs/traceability/browser-session-rust-embedded-file-input-authority.md index 1ae5a81e3..623e58b9f 100644 --- a/docs/traceability/browser-session-rust-embedded-file-input-authority.md +++ b/docs/traceability/browser-session-rust-embedded-file-input-authority.md @@ -8,7 +8,9 @@ Implemented on PR #317 as a focused repository contract. This document records s `include_bytes!` and `include_str!` are compile-time file inputs. Rust 1.98.1 documents that both macros locate a file relative to the current source file at compile time; `include_bytes!` places the file bytes in a `&'static [u8; N]`, while `include_str!` places UTF-8 file contents in a `&'static str`. -Before this generation, `tests/test_browser_session_rust_source_indirection_contract.py` governed `include!`, module/path indirection, and the shared Rust lexical helpers, but it did not classify `include_bytes!` or `include_str!`. A reviewed production `.rs` file could therefore select additional file bytes that were not represented in the canonical production-source closure. +Rust also resolves bang-style macros in the macro namespace, and `use` declarations can create aliases for imported macro names. The standard library re-exports `include_bytes` and `include_str` from `core`. Consequently, checking only the literal invocation spellings `include_bytes!(...)` and `include_str!(...)` is insufficient: `use core::include_bytes as read_blob; read_blob!(...)` selects the same compile-time file bytes while hiding the built-in macro name at the call site. + +Before the first generation, `tests/test_browser_session_rust_source_indirection_contract.py` governed `include!`, module/path indirection, and the shared Rust lexical helpers, but it did not classify `include_bytes!` or `include_str!`. Before the alias repair, the supplemental embedded-file contract classified direct and namespaced calls but not callable `use ... as ...` aliases. A reviewed production `.rs` file could therefore select additional file bytes that were not represented in the canonical production-source closure. For Browser Session, that is a provenance gap: the final artifact may contain compile-time-selected bytes even though the selected file itself is outside the source set inspected by the trusted-adapter/source-indirection contracts. @@ -16,29 +18,41 @@ For Browser Session, that is a provenance gap: the final artifact may contain co Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and keep `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. -Add a focused supplemental contract, `tests/test_browser_session_rust_embedded_file_input_authority_contract.py`, that: +The focused supplemental contract, `tests/test_browser_session_rust_embedded_file_input_authority_contract.py`: - first consumes the existing source-indirection contract; - consumes the canonical production source closure instead of rediscovering Cargo topology; -- reuses the shared trivia, raw-string, quoted-string, and character-literal lexer helpers; +- reuses the shared trivia, raw-string, quoted-string, character-literal, `use`, `as`, and use-statement helpers; - fails closed when lexical production source invokes `include_bytes!` or `include_str!`, including namespaced spellings such as `core::include_bytes!`; +- fails closed when a `use` tree gives either macro a callable alias, including grouped imports and aliases beginning with `_`; +- treats `use ... as _` as an unnameable import rather than callable alias authority, consistent with the Rust Reference; - does not classify mentions inside Rust comments or string/character/raw-string literals as file-input authority. -The policy is intentionally conservative about macro resolution. A locally shadowed macro named `include_bytes!` or `include_str!` is still rejected until macro-expansion provenance is modeled. This avoids allowing name shadowing to become a bypass around the compile-time file-input boundary. +The policy is intentionally conservative about macro resolution. A locally shadowed macro named `include_bytes!` or `include_str!`, or a callable alias of those imported names, is still rejected until macro-expansion provenance is modeled. This avoids allowing name shadowing or aliasing to become a bypass around the compile-time file-input boundary. ## RED → repair evidence -Structural RED: `5cac6feeadb008e200c8590707f7f18d19f9c6c5`. +Initial structural RED: `5cac6feeadb008e200c8590707f7f18d19f9c6c5`. + +The initial RED adds realistic workspaces with existing `unreviewed.bin` and `unreviewed.txt` files and requires the pre-existing source-indirection assertion to reject `include_bytes!(...)` and `include_str!(...)`. Initial repair `c163991ddc84fd519194cdae54643b252ec316d2` adds the direct/namespaced embedded-file classifier while delegating package/source discovery to the existing owners. + +Alias-bypass RED: `369b807db9988844626dcf2ea1f38eb67379e5e6`. + +That RED adds callable aliases for both built-ins: `use core::include_bytes as read_blob` and `use std::include_str as read_text`. The predecessor classifier sees the built-in name inside the `use` item without a following `!`, then sees only the alias at invocation, so both hostile workspaces pass when they must fail closed. + +Minimal alias repair: `ae1cf874a39ffd4b00a67216d6a2caa62e615721`. -The RED adds realistic workspaces with existing `unreviewed.bin` and `unreviewed.txt` files and requires the pre-existing source-indirection assertion to reject `include_bytes!(...)` and `include_str!(...)`. The predecessor has no such classifier, so those requirements expose the gap rather than manufacturing an unrelated failure. +The repair stays inside the focused embedded-file contract, reuses the shared Rust lexical/use helpers, and classifies callable aliases without creating another Cargo topology or general Rust lexer owner. -Minimal repair: `c163991ddc84fd519194cdae54643b252ec316d2`. +Alias-edge RED: `423c4088409215e30199943b7167150b3082fa3d`. -The repair changes only the new focused contract. It introduces the embedded-file macro classifier, delegates all package/source discovery to the existing owners, adds a current-production postcondition, covers direct and namespaced selectors, and adds comment/string controls. No Cargo topology, runtime browser behavior, linker authority, or cross-repository owner is duplicated. +Reviewing the first repair exposed an identifier-boundary bug: treating any alias beginning with `_` as the special underscore import would allow a callable alias such as `_read_blob`. Repair `b978c3c79ad0d0938bdb9d14c712693b9ed87417` distinguishes the exact unnameable `_` binding from ordinary identifiers beginning with `_`. Coverage successor `3f40f6662cf68c301579d3a70b3e76d0325eb705` adds grouped-use, exact-underscore, and comment controls without changing ownership. + +No Cargo topology, runtime browser behavior, linker authority, or cross-repository owner is duplicated by this generation. ## Security and buyer effect -The contract prevents Git-reviewed Browser Session Rust source from silently importing unmodeled compile-time file bytes through these two standard macros. This narrows artifact provenance to inputs that have an explicit reviewed contract rather than relying on the source file alone as evidence of what entered the binary. +The contract prevents Git-reviewed Browser Session Rust source from silently importing unmodeled compile-time file bytes through the standard embedded-file macros even when their invocation names are changed by `use` aliasing. This narrows artifact provenance to inputs that have an explicit reviewed contract rather than relying on the source file alone as evidence of what entered the binary. This is necessary but not sufficient for release provenance. Ambient filesystem contents, environment-driven paths, proc-macro or declarative-macro expansion that synthesizes equivalent file inputs, generated source, and direct compiler invocation remain CI/release supply-chain surfaces unless separately attested. @@ -58,3 +72,9 @@ If the product later needs compile-time embedded files, do not delete the fail-c Rust Project. (2026). *include_bytes macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/stable/std/macro.include_bytes.html Rust Project. (2026). *include_str macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/stable/std/macro.include_str.html + +Rust Project. (2026). *Use declarations*. The Rust Reference. https://doc.rust-lang.org/reference/items/use-declarations.html + +Rust Project. (2026). *Namespaces*. The Rust Reference. https://doc.rust-lang.org/reference/names/namespaces.html + +Rust Project. (2026). *Macros*. The Rust Reference. https://doc.rust-lang.org/reference/macros.html From 07841fbb4d84541758c796011dbcc402c6d40471 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:09:47 +0900 Subject: [PATCH 559/632] test(browser-session): regress embedded alias lexical boundaries --- ...t_embedded_file_input_authority_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index ca3e34b6c..b0e202f65 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -232,6 +232,24 @@ def test_underscore_import_is_not_callable_alias_authority(self) -> None: _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_raw_string_alias_text_is_not_embedded_file_authority(self) -> None: + root = self._workspace_with_source( + 'pub const NOTE: &str = r#"use core::include_bytes as read_blob; ' + 'read_blob!(\\"../unreviewed.bin\\")"#;\n' + ) + + _assert_no_unmodeled_rust_embedded_file_inputs(root) + + def test_character_literal_does_not_hide_following_aliased_file_input(self) -> None: + root = self._workspace_with_source( + "pub const MARKER: char = 'x';\n" + 'use core::include_bytes as read_blob;\n' + 'pub static EMBEDDED: &[u8] = read_blob!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_comment_and_string_mentions_are_not_embedded_file_authority(self) -> None: root = self._workspace_with_source( '// include_bytes!("../unreviewed.bin")\n' From 5dc7592a767c3c67bd4d8bb006548aff8804858e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 18:10:10 +0900 Subject: [PATCH 560/632] docs(traceability): record embedded alias lexical review repair --- .../browser-session-rust-embedded-file-input-authority.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/traceability/browser-session-rust-embedded-file-input-authority.md b/docs/traceability/browser-session-rust-embedded-file-input-authority.md index 623e58b9f..7c383dd44 100644 --- a/docs/traceability/browser-session-rust-embedded-file-input-authority.md +++ b/docs/traceability/browser-session-rust-embedded-file-input-authority.md @@ -48,6 +48,8 @@ Alias-edge RED: `423c4088409215e30199943b7167150b3082fa3d`. Reviewing the first repair exposed an identifier-boundary bug: treating any alias beginning with `_` as the special underscore import would allow a callable alias such as `_read_blob`. Repair `b978c3c79ad0d0938bdb9d14c712693b9ed87417` distinguishes the exact unnameable `_` binding from ordinary identifiers beginning with `_`. Coverage successor `3f40f6662cf68c301579d3a70b3e76d0325eb705` adds grouped-use, exact-underscore, and comment controls without changing ownership. +Focused review of exact `2c108fc14e9a2eaee79ea16219248c42aa1fd815` found one valid fixture-coverage gap rather than a classifier defect: the alias scanner consumes shared raw-string and simple-character-literal helpers, but the focused alias contract did not directly regress those lexical paths. Review-driven test-only repair `07841fbb4d84541758c796011dbcc402c6d40471` adds two separate fixtures: raw-string alias text must remain lexical data, and scanning must resume after a character literal so a following real aliased embedded-file input still fails closed. The classifier and ownership boundaries are unchanged by that repair. + No Cargo topology, runtime browser behavior, linker authority, or cross-repository owner is duplicated by this generation. ## Security and buyer effect From 6bf90e950ebbe09f28f56d4e6665433265cb238d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:04:51 +0900 Subject: [PATCH 561/632] test(browser-session): expose include lexical false positive --- ...n_rust_include_lexical_control_contract.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_browser_session_rust_include_lexical_control_contract.py diff --git a/tests/test_browser_session_rust_include_lexical_control_contract.py b/tests/test_browser_session_rust_include_lexical_control_contract.py new file mode 100644 index 000000000..d2b0360fd --- /dev/null +++ b/tests/test_browser_session_rust_include_lexical_control_contract.py @@ -0,0 +1,67 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustIncludeLexicalControlContractTests(unittest.TestCase): + """Keep Rust include! authority lexical rather than matching comment or literal text.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + (adapter / "generated_adapter.rs").write_text( + "pub fn generated_adapter_surface() {}\n", + encoding="utf-8", + ) + return root + + def test_commented_include_macro_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + '// include!("../generated_adapter.rs");\n' + 'pub fn reviewed_surface() {}\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_string_include_macro_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + 'pub const NOTE: &str = "include!(\\\"../generated_adapter.rs\\\")";\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_real_include_macro_still_fails_closed(self) -> None: + root = self._workspace_with_source('include!("../generated_adapter.rs");\n') + + with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From 6ad8f195e1bc9c649024b1e29244f00313d9a3e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:05:58 +0900 Subject: [PATCH 562/632] fix(browser-session): make include authority lexical --- ...ession_rust_source_indirection_contract.py | 73 +++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 15a1d971c..0158d0e97 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -60,14 +60,37 @@ def _skip_rust_trivia(text: str, offset: int) -> int: def _has_include_macro(text: str) -> bool: - """Detect include! macro syntax while honoring Rust whitespace/comment trivia around punctuation.""" - for match in INCLUDE_TOKEN.finditer(text): - bang = _skip_rust_trivia(text, match.end()) - if bang >= len(text) or text[bang] != "!": + """Detect lexical include! macro syntax outside Rust comments and literals.""" + cursor = 0 + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end continue - delimiter = _skip_rust_trivia(text, bang + 1) - if delimiter < len(text) and text[delimiter] in "([{": - return True + + raw_end = _raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = _quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = _simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + match = INCLUDE_TOKEN.match(text, cursor) + if match is None: + cursor += 1 + continue + bang = _skip_rust_trivia(text, match.end()) + if bang < len(text) and text[bang] == "!": + delimiter = _skip_rust_trivia(text, bang + 1) + if delimiter < len(text) and text[delimiter] in "([{": + return True + cursor = match.end() return False @@ -86,15 +109,38 @@ def _rust_use_statement_end(text: str, offset: int) -> int | None: def _has_aliased_include_import(text: str) -> bool: - """Detect use-tree aliases that rename include! before invocation.""" - for use_match in USE_TOKEN.finditer(text): + """Detect lexical use-tree aliases that rename include! before invocation.""" + cursor = 0 + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = _raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = _quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = _simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + use_match = USE_TOKEN.match(text, cursor) + if use_match is None: + cursor += 1 + continue statement_end = _rust_use_statement_end(text, use_match.end()) if statement_end is None: - continue + return False use_tree = text[use_match.end():statement_end] for include_match in INCLUDE_TOKEN.finditer(use_tree): - cursor = _skip_rust_trivia(use_tree, include_match.end()) - as_match = AS_TOKEN.match(use_tree, cursor) + include_cursor = _skip_rust_trivia(use_tree, include_match.end()) + as_match = AS_TOKEN.match(use_tree, include_cursor) if as_match is None: continue alias_start = _skip_rust_trivia(use_tree, as_match.end()) @@ -107,6 +153,7 @@ def _has_aliased_include_import(text: str) -> bool: ): continue return True + cursor = statement_end + 1 return False @@ -474,4 +521,4 @@ def test_string_containing_path_attribute_text_is_not_source_indirection(self) - if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From 2f3311b47509e86421917099c678919f39c43d01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:06:22 +0900 Subject: [PATCH 563/632] test(browser-session): cover include lexical edge cases --- ...n_rust_include_lexical_control_contract.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_browser_session_rust_include_lexical_control_contract.py b/tests/test_browser_session_rust_include_lexical_control_contract.py index d2b0360fd..f965994b1 100644 --- a/tests/test_browser_session_rust_include_lexical_control_contract.py +++ b/tests/test_browser_session_rust_include_lexical_control_contract.py @@ -56,6 +56,30 @@ def test_string_include_macro_text_is_not_source_indirection(self) -> None: source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_raw_string_include_macro_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + 'pub const NOTE: &str = r#"include!(\\"../generated_adapter.rs\\")"#;\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_commented_aliased_include_import_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + '// use core::include as hidden_include;\n' + 'pub fn reviewed_surface() {}\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_character_literal_does_not_hide_following_real_include(self) -> None: + root = self._workspace_with_source( + "pub const MARKER: char = 'x';\n" + 'include!("../generated_adapter.rs");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_real_include_macro_still_fails_closed(self) -> None: root = self._workspace_with_source('include!("../generated_adapter.rs");\n') From 4e4e217d010dbd2ad557d700ece931654b3791d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:06:55 +0900 Subject: [PATCH 564/632] docs(browser-session): trace include lexical authority --- ...-session-rust-include-lexical-authority.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/traceability/browser-session-rust-include-lexical-authority.md diff --git a/docs/traceability/browser-session-rust-include-lexical-authority.md b/docs/traceability/browser-session-rust-include-lexical-authority.md new file mode 100644 index 000000000..53519b951 --- /dev/null +++ b/docs/traceability/browser-session-rust-include-lexical-authority.md @@ -0,0 +1,43 @@ +# Browser Session Rust `include!` lexical authority + +Status: Draft repair evidence on the #317 Browser Session source-provenance lane. Hosted exact-head repository/security acceptance, whole-PR review, and protected-main shipment remain separate gates. + +## Problem + +OriginWeave deliberately fails closed when reviewed production Rust source uses `include!`, because the macro parses another file into the surrounding crate at compile time and therefore extends the executable source-input closure. The existing detector searched the raw source text with `INCLUDE_TOKEN.finditer(...)` and likewise searched raw text for `use ... include as ...` aliases. That classified `include!(...)` or `use core::include as ...` appearing only inside comments or string literals as executable authority. + +That behavior is conservative but incorrect: Rust non-doc comments are lexically whitespace, and string/character/raw-string contents are literal tokens rather than macro invocations. A provenance guard that cannot distinguish lexical data from executable syntax creates false-positive security failures and makes reviewed documentation/log strings an accidental build-authority gate. + +## Evidence and repair + +- Structural RED `6bf90e950ebbe09f28f56d4e6665433265cb238d` adds independent controls requiring line-comment and ordinary-string `include!(...)` text to remain lexical data while a real `include!(...)` invocation still fails closed. +- Minimal causal repair `6ad8f195e1bc9c649024b1e29244f00313d9a3e9` keeps ownership in `tests/test_browser_session_rust_source_indirection_contract.py`. `_has_include_macro()` and `_has_aliased_include_import()` now reuse the existing Rust trivia/raw-string/quoted-string/character-literal helpers before recognizing `include` or `use` tokens. No Cargo topology, production crate, or browser-domain authority is duplicated. +- Edge-case successor `2f3311b47509e86421917099c678919f39c43d01` adds raw-string false-positive coverage, commented `use core::include as ...` coverage, and character-literal scan resumption before a real hostile `include!`. + +The repair is intentionally lexical rather than pathname-based. Allowlisting a path would not fix the category error: comment/literal text must never become authority regardless of its spelling, while a real `include!` remains provenance-relevant even for an in-repository path until the compiler-derived source-input contract explicitly models it. + +## Invariants + +1. A lexical `include!` macro invocation in a reviewed production source fails closed until an explicit source-provenance contract admits the included file. +2. A lexical `use ... include as ` declaration fails closed under the same authority boundary. +3. Line/block comments and ordinary/raw string or character literal contents do not create source-input authority merely because their text resembles `include!` or an alias declaration. +4. Scanning resumes after a literal token and still rejects a following real `include!` invocation. +5. `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology; this repair changes only lexical classification inside the existing Rust source-indirection owner. + +## Rejected alternatives + +- **Keep raw-text matching because it is safer.** Rejected. False positives make comments and documentation semantically equivalent to compiler inputs, which is not a defensible security boundary and creates pressure to add ad hoc suppressions. +- **Add pathname exceptions.** Rejected. The defect is lexical classification, not path selection; exceptions would weaken the real `include!` boundary without fixing comment/literal handling. +- **Create another repository-wide Rust scanner.** Rejected. That would violate single-writer ownership and duplicate the existing shared lexer/topology contracts. + +## Acceptance + +This slice is acceptable only when the RED controls are demonstrably failing on the predecessor and GREEN on the repair generation, the current production-source contract remains fail closed for real `include!`, current-head static review finds no ownership or lexer regression, and hosted repository/security checks are independently satisfied after the central workflow prerequisite chain permits them. Command acknowledgement or a static review response is not hosted GREEN. + +## Authoritative references + +Rust Project. (2026). *The Rust Reference: Comments*. https://doc.rust-lang.org/reference/comments.html (retrieved September 19, 2026). Non-doc comments are tokenized as whitespace. + +Rust Project. (2026). *The Rust Reference: Macros — macro invocation*. https://doc.rust-lang.org/reference/macros.html (retrieved September 19, 2026). A macro invocation has the token form `SimplePath ! DelimTokenTree`. + +Rust Project. (2026). *Macro `include`*. https://doc.rust-lang.org/nightly/core/macro.include.html (retrieved September 19, 2026). `include!` parses another file into the surrounding context at compile time and resolves the path relative to the current file. From 6134c1b5583b05f6847078e33ded2770b410e8ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:08:51 +0900 Subject: [PATCH 565/632] test(browser-session): expose grouped-use comment alias false positive --- ...r_session_rust_include_lexical_control_contract.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_browser_session_rust_include_lexical_control_contract.py b/tests/test_browser_session_rust_include_lexical_control_contract.py index f965994b1..129a966e8 100644 --- a/tests/test_browser_session_rust_include_lexical_control_contract.py +++ b/tests/test_browser_session_rust_include_lexical_control_contract.py @@ -71,6 +71,17 @@ def test_commented_aliased_include_import_is_not_source_indirection(self) -> Non source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_grouped_use_comment_alias_text_is_not_source_indirection(self) -> None: + root = self._workspace_with_source( + 'use core::{\n' + ' /* include as hidden_include */\n' + ' fmt,\n' + '};\n' + 'pub fn reviewed_surface() { let _ = fmt::Error; }\n' + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_character_literal_does_not_hide_following_real_include(self) -> None: root = self._workspace_with_source( "pub const MARKER: char = 'x';\n" From be359649734acc0173cb3c61802f7905f67d7980 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:10:05 +0900 Subject: [PATCH 566/632] fix(browser-session): lex grouped include aliases --- ...ession_rust_source_indirection_contract.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 0158d0e97..a0ba2db09 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -138,19 +138,45 @@ def _has_aliased_include_import(text: str) -> bool: if statement_end is None: return False use_tree = text[use_match.end():statement_end] - for include_match in INCLUDE_TOKEN.finditer(use_tree): + use_cursor = 0 + while use_cursor < len(use_tree): + trivia_end = _skip_rust_trivia(use_tree, use_cursor) + if trivia_end != use_cursor: + use_cursor = trivia_end + continue + + raw_end = _raw_string_end(use_tree, use_cursor) + if raw_end is not None: + use_cursor = raw_end + continue + if use_tree[use_cursor] == '"': + use_cursor = _quoted_string_end(use_tree, use_cursor) + continue + if use_tree[use_cursor] == "'": + char_end = _simple_char_literal_end(use_tree, use_cursor) + if char_end is not None: + use_cursor = char_end + continue + + include_match = INCLUDE_TOKEN.match(use_tree, use_cursor) + if include_match is None: + use_cursor += 1 + continue include_cursor = _skip_rust_trivia(use_tree, include_match.end()) as_match = AS_TOKEN.match(use_tree, include_cursor) if as_match is None: + use_cursor = include_match.end() continue alias_start = _skip_rust_trivia(use_tree, as_match.end()) if alias_start >= len(use_tree): + use_cursor = include_match.end() continue if use_tree[alias_start] == "_": next_offset = alias_start + 1 if next_offset >= len(use_tree) or not ( use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" ): + use_cursor = include_match.end() continue return True cursor = statement_end + 1 From 84fb37d31fbb5f1145b78a700ce77771319a032b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:10:25 +0900 Subject: [PATCH 567/632] docs(browser-session): record grouped-use lexical repair --- .../browser-session-rust-include-lexical-authority.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-rust-include-lexical-authority.md b/docs/traceability/browser-session-rust-include-lexical-authority.md index 53519b951..ee513402d 100644 --- a/docs/traceability/browser-session-rust-include-lexical-authority.md +++ b/docs/traceability/browser-session-rust-include-lexical-authority.md @@ -11,16 +11,18 @@ That behavior is conservative but incorrect: Rust non-doc comments are lexically ## Evidence and repair - Structural RED `6bf90e950ebbe09f28f56d4e6665433265cb238d` adds independent controls requiring line-comment and ordinary-string `include!(...)` text to remain lexical data while a real `include!(...)` invocation still fails closed. -- Minimal causal repair `6ad8f195e1bc9c649024b1e29244f00313d9a3e9` keeps ownership in `tests/test_browser_session_rust_source_indirection_contract.py`. `_has_include_macro()` and `_has_aliased_include_import()` now reuse the existing Rust trivia/raw-string/quoted-string/character-literal helpers before recognizing `include` or `use` tokens. No Cargo topology, production crate, or browser-domain authority is duplicated. +- Minimal causal repair `6ad8f195e1bc9c649024b1e29244f00313d9a3e9` keeps ownership in `tests/test_browser_session_rust_source_indirection_contract.py`. `_has_include_macro()` and the outer `_has_aliased_include_import()` scan reuse the existing Rust trivia/raw-string/quoted-string/character-literal helpers before recognizing `include` or `use` tokens. No Cargo topology, production crate, or browser-domain authority is duplicated. - Edge-case successor `2f3311b47509e86421917099c678919f39c43d01` adds raw-string false-positive coverage, commented `use core::include as ...` coverage, and character-literal scan resumption before a real hostile `include!`. +- Focused review of `4e4e217d010dbd2ad557d700ece931654b3791d4` found one remaining lexical false positive: once a real `use` declaration was captured, its inner use-tree still used raw `INCLUDE_TOKEN.finditer(use_tree)`, so `use core::{ /* include as hidden_include */ fmt };` was misclassified as alias authority. +- Review-driven RED `6134c1b5583b05f6847078e33ded2770b410e8ed` adds that grouped-use comment control. Repair `be359649734acc0173cb3c61802f7905f67d7980` replaces the inner raw scan with the same trivia/literal-aware cursor discipline while preserving real callable alias rejection. The repair is intentionally lexical rather than pathname-based. Allowlisting a path would not fix the category error: comment/literal text must never become authority regardless of its spelling, while a real `include!` remains provenance-relevant even for an in-repository path until the compiler-derived source-input contract explicitly models it. ## Invariants 1. A lexical `include!` macro invocation in a reviewed production source fails closed until an explicit source-provenance contract admits the included file. -2. A lexical `use ... include as ` declaration fails closed under the same authority boundary. -3. Line/block comments and ordinary/raw string or character literal contents do not create source-input authority merely because their text resembles `include!` or an alias declaration. +2. A lexical `use ... include as ` declaration fails closed under the same authority boundary, including grouped use trees. +3. Line/block comments and ordinary/raw string or character literal contents do not create source-input authority merely because their text resembles `include!` or an alias declaration, including comments nested inside a real use tree. 4. Scanning resumes after a literal token and still rejects a following real `include!` invocation. 5. `tests/test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology; this repair changes only lexical classification inside the existing Rust source-indirection owner. @@ -32,7 +34,7 @@ The repair is intentionally lexical rather than pathname-based. Allowlisting a p ## Acceptance -This slice is acceptable only when the RED controls are demonstrably failing on the predecessor and GREEN on the repair generation, the current production-source contract remains fail closed for real `include!`, current-head static review finds no ownership or lexer regression, and hosted repository/security checks are independently satisfied after the central workflow prerequisite chain permits them. Command acknowledgement or a static review response is not hosted GREEN. +This slice is acceptable only when both RED generations are demonstrably failing on their predecessors and GREEN on the corresponding repairs, the current production-source contract remains fail closed for real `include!` and callable aliases, current-head static review finds no ownership or lexer regression, and hosted repository/security checks are independently satisfied after the central workflow prerequisite chain permits them. Command acknowledgement or a static review response is not hosted GREEN. ## Authoritative references From bd457be344c729d550e301a403e77bb5959e5b28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:11:24 +0900 Subject: [PATCH 568/632] test(browser-session): expose custom-target mod lexical false positive --- ...sion_custom_target_mod_lexical_contract.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_browser_session_custom_target_mod_lexical_contract.py diff --git a/tests/test_browser_session_custom_target_mod_lexical_contract.py b/tests/test_browser_session_custom_target_mod_lexical_contract.py new file mode 100644 index 000000000..99dd60c18 --- /dev/null +++ b/tests/test_browser_session_custom_target_mod_lexical_contract.py @@ -0,0 +1,64 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(indirection) + + +class BrowserSessionCustomTargetModLexicalContractTests(unittest.TestCase): + """Keep custom-target module detection lexical instead of raw-text based.""" + + def _custom_target_workspace(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "runtime").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n' + '[lib]\npath = "runtime/lifecycle_adapter.rs"\n', + encoding="utf-8", + ) + (adapter / "runtime/lifecycle_adapter.rs").write_text(source_text, encoding="utf-8") + return root + + def test_line_comment_mod_text_is_lexical_data(self) -> None: + root = self._custom_target_workspace( + '// mod hidden;\npub fn lifecycle_adapter_surface() {}\n' + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_ordinary_string_mod_text_is_lexical_data(self) -> None: + root = self._custom_target_workspace( + 'pub const NOTE: &str = "mod hidden;";\npub fn lifecycle_adapter_surface() {}\n' + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_raw_string_mod_text_is_lexical_data(self) -> None: + root = self._custom_target_workspace( + 'pub const NOTE: &str = r#"mod hidden;"#;\npub fn lifecycle_adapter_surface() {}\n' + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From 6f8b0706acaf5837e3581f1c77d43c318a54462c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:12:32 +0900 Subject: [PATCH 569/632] fix(browser-session): make custom-target mod detection lexical --- ...ession_rust_source_indirection_contract.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index a0ba2db09..a2b481495 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -94,6 +94,34 @@ def _has_include_macro(text: str) -> bool: return False +def _has_custom_target_mod_token(text: str) -> bool: + """Detect lexical Rust mod tokens outside comments and string/character literals.""" + cursor = 0 + while cursor < len(text): + trivia_end = _skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = _raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = _quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = _simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + if CUSTOM_TARGET_MOD_TOKEN.match(text, cursor) is not None: + return True + cursor += 1 + return False + + def _rust_use_statement_end(text: str, offset: int) -> int | None: """Return the semicolon ending one Rust use declaration while ignoring comment trivia.""" cursor = offset @@ -362,7 +390,7 @@ def _assert_no_unmodeled_rust_source_indirection(root: pathlib.Path) -> None: # compiler-derived source inputs replace this guard, any lexical `mod` token is an # intentionally conservative provenance stop: comments/trivia, raw/Unicode names, # visibility spellings, and inline-vs-outlined grammar must not create bypasses. - if not _is_under_any_default_src(source, default_src_roots) and CUSTOM_TARGET_MOD_TOKEN.search(text): + if not _is_under_any_default_src(source, default_src_roots) and _has_custom_target_mod_token(text): raise AssertionError( "Rust module source indirection from a custom Cargo target requires an explicit " f"provenance contract: {relative}" From a4ab2ac44407141ee4641a56504178d8d5acb70c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:13:27 +0900 Subject: [PATCH 570/632] docs(browser-session): trace custom-target mod lexical repair --- ...ion-custom-target-mod-lexical-authority.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/traceability/browser-session-custom-target-mod-lexical-authority.md diff --git a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md new file mode 100644 index 000000000..17e5b343d --- /dev/null +++ b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md @@ -0,0 +1,44 @@ +# Browser Session custom-target `mod` lexical authority + +Status: active PR evidence only. This document does not claim protected-main, hosted-check, or release acceptance. + +## Problem + +`tests/test_browser_session_rust_source_indirection_contract.py` intentionally fails closed when a reviewed Cargo production target whose crate root is outside the package's default `src/` tree contains a Rust `mod` token. That guard prevents a custom target such as `runtime/lifecycle_adapter.rs` from loading an outlined sibling module that is not already covered by the canonical `src/**/*.rs` production-source closure. + +The predecessor implementation used `CUSTOM_TARGET_MOD_TOKEN.search(text)` over raw source bytes. The policy was conservative, but the implementation also treated `mod ...` text inside non-doc comments and ordinary/raw string literals as executable module authority. That is a lexical false positive: Rust non-doc comments are interpreted as whitespace, and string/raw-string literals are tokens whose contents are data rather than item grammar. + +This matters commercially because a fail-closed provenance guard still has to distinguish executable authority from inert source text. Rejecting harmless comments or literal data creates avoidable adoption friction and encourages pressure to weaken the security gate instead of repairing its lexical boundary. + +Primary references: + +- Rust Reference, comments: https://doc.rust-lang.org/reference/comments.html +- Rust Reference, literal expressions: https://doc.rust-lang.org/reference/expressions/literal-expr.html +- Rust Reference, modules and module source filenames: https://doc.rust-lang.org/reference/items/modules.html + +## Constraints + +- `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. +- The custom-target rule remains intentionally conservative for *real lexical* `mod` tokens outside default `src/`: it still does not attempt to distinguish inline from outlined modules or reproduce the Rust parser. +- The repair must reuse the existing Rust trivia/raw-string/quoted-string/character-literal scanner discipline already used by `include!`, `use`-alias, and attribute discovery rather than introduce a second lexer. +- No new source path, module tree, adapter, or dependency is authorized. +- Default `src/` module trees remain governed by the canonical production-source closure rather than this custom-target guard. + +## Decision + +Custom-target module detection now advances through the same lexical boundaries already used by the source-indirection contract. Non-doc line/block comments, normal strings, raw strings, and character literals are skipped before `CUSTOM_TARGET_MOD_TOKEN` is matched. A real lexical `mod` token still fails closed exactly as before; only inert comment/literal contents stop being treated as module authority. + +The repair deliberately does not parse module grammar. `mod helper;`, `mod r#type;`, `mod 관찰;`, and `mod /* trivia */ helper;` in a custom target root remain provenance stops. The change only removes raw-text false positives where no lexical `mod` token exists. + +## RED → repair evidence + +- Predecessor exact `84fb37d31fbb5f1145b78a700ce77771319a032b` used raw `CUSTOM_TARGET_MOD_TOKEN.search(text)` for custom-target roots. +- Structural RED `bd457be344c729d550e301a403e77bb5959e5b28` adds line-comment, ordinary-string, and raw-string controls. On the predecessor implementation these fixtures are rejected even though the `mod` spelling is lexical data. +- Minimal repair `6f8b0706acaf5837e3581f1c77d43c318a54462c` adds `_has_custom_target_mod_token()` and changes the custom-target guard to consume it. The helper reuses `_skip_rust_trivia()`, `_raw_string_end()`, `_quoted_string_end()`, and `_simple_char_literal_end()`; Cargo topology ownership is unchanged. +- Compare `84fb37d3... → bd457be3...` is one test-only file, +64/-0. Compare `84fb37d3... → 6f8b0706...` is two files: the RED contract plus the canonical lexical repair, with no production Rust implementation change. + +## Risk and follow-up + +This remains a temporary lexical security boundary. It is not compiler-derived source-input provenance and can intentionally reject legitimate inline modules in custom target roots. Before OriginWeave needs such custom-target module trees, replace the heuristic with compiler-derived or equivalently exact source-input evidence that identifies the actual bytes compiled for supported target configurations without widening Cargo ownership or relying on source-text approximations. + +The existing `docs/traceability/browser-session-rust-source-indirection.md` remains the broader source-indirection record. This document narrows only the current custom-target lexical correction and should be folded into that canonical record when the current stacked Browser Session lineage is reconciled. From a4ad6d45d101b79460fbc06ca9ede9b1c3b0b140 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:15:23 +0900 Subject: [PATCH 571/632] test(browser-session): cover custom-target mod lexer edges --- ...session_custom_target_mod_lexical_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_browser_session_custom_target_mod_lexical_contract.py b/tests/test_browser_session_custom_target_mod_lexical_contract.py index 99dd60c18..44c538a5e 100644 --- a/tests/test_browser_session_custom_target_mod_lexical_contract.py +++ b/tests/test_browser_session_custom_target_mod_lexical_contract.py @@ -45,6 +45,14 @@ def test_line_comment_mod_text_is_lexical_data(self) -> None: indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_nested_block_comment_mod_text_is_lexical_data(self) -> None: + root = self._custom_target_workspace( + '/* outer /* mod hidden; */ still comment */\n' + 'pub fn lifecycle_adapter_surface() {}\n' + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_ordinary_string_mod_text_is_lexical_data(self) -> None: root = self._custom_target_workspace( 'pub const NOTE: &str = "mod hidden;";\npub fn lifecycle_adapter_surface() {}\n' @@ -59,6 +67,14 @@ def test_raw_string_mod_text_is_lexical_data(self) -> None: indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_character_literal_does_not_hide_following_real_mod(self) -> None: + root = self._custom_target_workspace( + "pub const MARKER: char = 'm';\nmod helper;\n" + ) + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + indirection._assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From c36f8630cfdc9887a3fda8716c3cccbc0ae370b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:15:51 +0900 Subject: [PATCH 572/632] docs(browser-session): record custom-target mod edge coverage --- .../browser-session-custom-target-mod-lexical-authority.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md index 17e5b343d..af1cb20be 100644 --- a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md +++ b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md @@ -35,7 +35,8 @@ The repair deliberately does not parse module grammar. `mod helper;`, `mod r#typ - Predecessor exact `84fb37d31fbb5f1145b78a700ce77771319a032b` used raw `CUSTOM_TARGET_MOD_TOKEN.search(text)` for custom-target roots. - Structural RED `bd457be344c729d550e301a403e77bb5959e5b28` adds line-comment, ordinary-string, and raw-string controls. On the predecessor implementation these fixtures are rejected even though the `mod` spelling is lexical data. - Minimal repair `6f8b0706acaf5837e3581f1c77d43c318a54462c` adds `_has_custom_target_mod_token()` and changes the custom-target guard to consume it. The helper reuses `_skip_rust_trivia()`, `_raw_string_end()`, `_quoted_string_end()`, and `_simple_char_literal_end()`; Cargo topology ownership is unchanged. -- Compare `84fb37d3... → bd457be3...` is one test-only file, +64/-0. Compare `84fb37d3... → 6f8b0706...` is two files: the RED contract plus the canonical lexical repair, with no production Rust implementation change. +- Edge coverage `a4ad6d45d101b79460fbc06ca9ede9b1c3b0b140` adds nested-block-comment lexical data and proves scanning resumes after a character literal to catch a later real `mod` token. +- Compare `84fb37d3... → bd457be3...` is one test-only file, +64/-0. The repair lineage changes only the focused contract, the canonical lexical helper/call site, and this traceability record; no production Rust implementation or Cargo topology writer changes. ## Risk and follow-up From a214c87d375e75a9c10edc3c6de99b4847c43327 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:20:52 +0900 Subject: [PATCH 573/632] test(browser-session): expose Rust XID mod boundary false positive --- ...t_browser_session_custom_target_mod_lexical_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_browser_session_custom_target_mod_lexical_contract.py b/tests/test_browser_session_custom_target_mod_lexical_contract.py index 44c538a5e..53dbe747e 100644 --- a/tests/test_browser_session_custom_target_mod_lexical_contract.py +++ b/tests/test_browser_session_custom_target_mod_lexical_contract.py @@ -75,6 +75,13 @@ def test_character_literal_does_not_hide_following_real_mod(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_xid_continue_after_mod_is_identifier_data_not_keyword(self) -> None: + root = self._custom_target_workspace( + "pub fn mod\u0301() {}\npub fn lifecycle_adapter_surface() {}\n" + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From ec32bd9f481280b522ce7554d392021b40c7fea1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:23:09 +0900 Subject: [PATCH 574/632] fix(browser-session): align mod keyword boundary with Rust Unicode --- ...ession_rust_source_indirection_contract.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index a2b481495..b14b575d4 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -19,7 +19,10 @@ USE_TOKEN = re.compile(r"(? bool: return False +def _rust_keyword_is_identifier_adjacent(char: str) -> bool: + """Conservatively reject keyword boundaries that could be Rust identifier continuation.""" + if char.isascii(): + return char.isalnum() or char == "_" + return char not in RUST_PATTERN_WHITESPACE + + +def _matches_custom_target_mod_token(text: str, offset: int) -> bool: + """Match the Rust `mod` keyword without relying on Python's Unicode identifier table.""" + if not text.startswith(CUSTOM_TARGET_MOD_TOKEN, offset): + return False + if offset: + previous = text[offset - 1] + if previous == "#" or _rust_keyword_is_identifier_adjacent(previous): + return False + end = offset + len(CUSTOM_TARGET_MOD_TOKEN) + if end < len(text) and _rust_keyword_is_identifier_adjacent(text[end]): + return False + return True + + def _has_custom_target_mod_token(text: str) -> bool: """Detect lexical Rust mod tokens outside comments and string/character literals.""" cursor = 0 @@ -116,7 +140,7 @@ def _has_custom_target_mod_token(text: str) -> bool: cursor = char_end continue - if CUSTOM_TARGET_MOD_TOKEN.match(text, cursor) is not None: + if _matches_custom_target_mod_token(text, cursor): return True cursor += 1 return False From df940ba25b2c7731d6fe631290f000ce1d57bcd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:23:36 +0900 Subject: [PATCH 575/632] test(browser-session): cover Rust mod Unicode token boundaries --- ..._session_custom_target_mod_lexical_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_browser_session_custom_target_mod_lexical_contract.py b/tests/test_browser_session_custom_target_mod_lexical_contract.py index 53dbe747e..d2e5a4fd3 100644 --- a/tests/test_browser_session_custom_target_mod_lexical_contract.py +++ b/tests/test_browser_session_custom_target_mod_lexical_contract.py @@ -82,6 +82,21 @@ def test_xid_continue_after_mod_is_identifier_data_not_keyword(self) -> None: indirection._assert_no_unmodeled_rust_source_indirection(root) + def test_xid_continue_before_mod_keeps_one_identifier_token(self) -> None: + root = self._custom_target_workspace( + "pub fn a\u0301mod() {}\npub fn lifecycle_adapter_surface() {}\n" + ) + + indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_non_ascii_rust_whitespace_still_separates_real_mod_keyword(self) -> None: + root = self._custom_target_workspace( + "mod\u200ehelper;\npub fn lifecycle_adapter_surface() {}\n" + ) + + with self.assertRaisesRegex(AssertionError, "Rust module source indirection"): + indirection._assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From ce33ae1aa020b1b9903aa02c5952a90bfd1581e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:24:17 +0900 Subject: [PATCH 576/632] test(browser-session): expose Rust Pattern_White_Space trivia bypass --- ...rowser_session_include_comment_trivia_contract.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_include_comment_trivia_contract.py b/tests/test_browser_session_include_comment_trivia_contract.py index db19547a7..034f20d06 100644 --- a/tests/test_browser_session_include_comment_trivia_contract.py +++ b/tests/test_browser_session_include_comment_trivia_contract.py @@ -15,7 +15,7 @@ class BrowserSessionIncludeCommentTriviaContractTests(unittest.TestCase): - """Keep Rust comment trivia from bypassing include! source-provenance review.""" + """Keep Rust lexical trivia from bypassing include! source-provenance review.""" def _workspace_with_source(self, source_text: str) -> pathlib.Path: directory = tempfile.TemporaryDirectory() @@ -58,6 +58,16 @@ def test_line_comment_between_include_and_bang_fails_closed(self) -> None: 'include // provenance gap\n! ("../generated_adapter.rs");\n' ) + def test_non_ascii_rust_whitespace_between_include_and_bang_fails_closed(self) -> None: + self._assert_include_trivia_fails_closed( + 'include\u200e!("../generated_adapter.rs");\n' + ) + + def test_non_ascii_rust_whitespace_between_bang_and_delimiter_fails_closed(self) -> None: + self._assert_include_trivia_fails_closed( + 'include!\u200f("../generated_adapter.rs");\n' + ) + if __name__ == "__main__": unittest.main() From e199aac4a64e636b3a7412f3d9347644e96e636b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:25:06 +0900 Subject: [PATCH 577/632] fix(browser-session): use Rust Pattern_White_Space in lexer --- .../test_browser_session_rust_source_indirection_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index b14b575d4..d4b3dde95 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -36,7 +36,7 @@ def _skip_rust_trivia(text: str, offset: int) -> int: """Skip Rust whitespace and nested non-doc comments without changing token meaning.""" index = offset while index < len(text): - if text[index].isspace(): + if text[index] in RUST_PATTERN_WHITESPACE: index += 1 continue if text.startswith("//", index): @@ -373,7 +373,7 @@ def _rust_attribute_bodies(text: str) -> list[str]: def _has_path_meta(attribute_body: str) -> bool: - """Return whether an attribute contains a path meta item followed by Rust trivia and '='.""" + """Return whether an attribute contains a path meta item followed by valid Rust trivia and '='.""" for match in PATH_TOKEN.finditer(attribute_body): cursor = _skip_rust_trivia(attribute_body, match.end()) if cursor < len(attribute_body) and attribute_body[cursor] == "=": From 2eb74753042f264612a47195489ebf733b388c53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:25:35 +0900 Subject: [PATCH 578/632] docs(browser-session): trace Rust Unicode lexical root repair --- ...ion-custom-target-mod-lexical-authority.md | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md index af1cb20be..513011221 100644 --- a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md +++ b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md @@ -8,10 +8,16 @@ Status: active PR evidence only. This document does not claim protected-main, ho The predecessor implementation used `CUSTOM_TARGET_MOD_TOKEN.search(text)` over raw source bytes. The policy was conservative, but the implementation also treated `mod ...` text inside non-doc comments and ordinary/raw string literals as executable module authority. That is a lexical false positive: Rust non-doc comments are interpreted as whitespace, and string/raw-string literals are tokens whose contents are data rather than item grammar. -This matters commercially because a fail-closed provenance guard still has to distinguish executable authority from inert source text. Rejecting harmless comments or literal data creates avoidable adoption friction and encourages pressure to weaken the security gate instead of repairing its lexical boundary. +The first lexical repair exposed a second, independent language-boundary defect. Python regular-expression `\w` is not Rust's identifier grammar. Rust identifiers use Unicode `XID_Start`/`XID_Continue` from Unicode 17.0, so U+0301 COMBINING ACUTE ACCENT can continue an identifier even though Python's `\w` boundary does not treat it as a word character. Consequently `mod\u0301` is one identifier token, not the strict `mod` keyword, and a Python-`\w` keyword boundary can reject source that has no lexical `mod` token. + +The same audit found that Rust's lexical whitespace is the stable Unicode `Pattern_White_Space` set, not Python `str.isspace()`. In particular U+200E LEFT-TO-RIGHT MARK and U+200F RIGHT-TO-LEFT MARK are legal Rust whitespace. Failing to skip them between `include`, `!`, and the macro delimiter creates a false negative in the existing source-provenance stop. + +This matters commercially because a fail-closed provenance guard still has to distinguish executable authority from inert source text without missing legal Rust token separation. False positives create avoidable adoption friction; false negatives permit compile-time source bytes to enter outside the reviewed provenance boundary. Primary references: +- Rust Reference, identifiers (`XID_Start`/`XID_Continue`, Unicode 17.0): https://doc.rust-lang.org/reference/identifiers.html +- Rust Reference, whitespace (`Pattern_White_Space`): https://doc.rust-lang.org/reference/whitespace.html - Rust Reference, comments: https://doc.rust-lang.org/reference/comments.html - Rust Reference, literal expressions: https://doc.rust-lang.org/reference/expressions/literal-expr.html - Rust Reference, modules and module source filenames: https://doc.rust-lang.org/reference/items/modules.html @@ -20,15 +26,21 @@ Primary references: - `tests/test_browser_session_trusted_adapter_boundary.py::_workspace_production_sources()` remains the single writer for Cargo production package/source topology. - The custom-target rule remains intentionally conservative for *real lexical* `mod` tokens outside default `src/`: it still does not attempt to distinguish inline from outlined modules or reproduce the Rust parser. -- The repair must reuse the existing Rust trivia/raw-string/quoted-string/character-literal scanner discipline already used by `include!`, `use`-alias, and attribute discovery rather than introduce a second lexer. +- The scanner must reuse the existing Rust trivia/raw-string/quoted-string/character-literal discipline already used by `include!`, `use`-alias, and attribute discovery rather than introduce a second lexer. +- Python's Unicode database must not silently define Rust keyword identity. The checked Rust Reference currently targets Unicode 17.0, so the boundary cannot assume Python `\w` or the local Python runtime's identifier tables are equivalent. +- Rust whitespace handling must use the language's exact stable `Pattern_White_Space` set. Generic host-language whitespace predicates are not lexical authority. - No new source path, module tree, adapter, or dependency is authorized. - Default `src/` module trees remain governed by the canonical production-source closure rather than this custom-target guard. ## Decision -Custom-target module detection now advances through the same lexical boundaries already used by the source-indirection contract. Non-doc line/block comments, normal strings, raw strings, and character literals are skipped before `CUSTOM_TARGET_MOD_TOKEN` is matched. A real lexical `mod` token still fails closed exactly as before; only inert comment/literal contents stop being treated as module authority. +Custom-target module detection advances through the same lexical boundaries already used by the source-indirection contract. Non-doc line/block comments, normal strings, raw strings, and character literals are skipped before the `mod` spelling is considered. A real lexical `mod` token still fails closed exactly as before; only inert comment/literal contents stop being treated as module authority. + +The `mod` keyword boundary no longer relies on Python `\w`. ASCII identifier continuation is handled directly. For non-ASCII adjacency the guard is deliberately conservative: any non-ASCII scalar that is not Rust `Pattern_White_Space` prevents classification as the ASCII `mod` keyword. This covers current and future Unicode identifier-continuation additions without pretending the host Python Unicode table is Rust's versioned `XID_Continue` authority. Rust's eleven `Pattern_White_Space` code points are explicit and stable, so non-ASCII legal whitespace such as U+200E continues to separate a real `mod` keyword. -The repair deliberately does not parse module grammar. `mod helper;`, `mod r#type;`, `mod 관찰;`, and `mod /* trivia */ helper;` in a custom target root remain provenance stops. The change only removes raw-text false positives where no lexical `mod` token exists. +The shared trivia skipper now uses that exact Rust whitespace set as well. This closes legal U+200E/U+200F separation around `include!` and removes host-only whitespace from the lexer contract. Comment handling remains nested and unchanged. + +The repair deliberately does not parse module grammar. `mod helper;`, `mod r#type;`, `mod 관찰;`, and `mod /* trivia */ helper;` in a custom target root remain provenance stops. The change only removes raw-text/identifier-boundary false positives and closes Rust-whitespace false negatives. ## RED → repair evidence @@ -36,10 +48,15 @@ The repair deliberately does not parse module grammar. `mod helper;`, `mod r#typ - Structural RED `bd457be344c729d550e301a403e77bb5959e5b28` adds line-comment, ordinary-string, and raw-string controls. On the predecessor implementation these fixtures are rejected even though the `mod` spelling is lexical data. - Minimal repair `6f8b0706acaf5837e3581f1c77d43c318a54462c` adds `_has_custom_target_mod_token()` and changes the custom-target guard to consume it. The helper reuses `_skip_rust_trivia()`, `_raw_string_end()`, `_quoted_string_end()`, and `_simple_char_literal_end()`; Cargo topology ownership is unchanged. - Edge coverage `a4ad6d45d101b79460fbc06ca9ede9b1c3b0b140` adds nested-block-comment lexical data and proves scanning resumes after a character literal to catch a later real `mod` token. -- Compare `84fb37d3... → bd457be3...` is one test-only file, +64/-0. The repair lineage changes only the focused contract, the canonical lexical helper/call site, and this traceability record; no production Rust implementation or Cargo topology writer changes. +- Focused CodeRabbit review of `c36f8630cfdc9887a3fda8716c3cccbc0ae370b6` found a valid remaining identifier-boundary false positive: Python `\w` does not model Rust Unicode 17.0 `XID_Continue`, so `mod\u0301` was incorrectly classified as the strict keyword. +- Review-driven RED `a214c87d375e75a9c10edc3c6de99b4847c43327` preserves `mod\u0301` as identifier data. Repair `ec32bd9f481280b522ce7554d392021b40c7fea1` replaces the Python-regex keyword boundary with a version-independent conservative Rust boundary backed by the exact stable `Pattern_White_Space` set. +- Edge coverage `df940ba25b2c7731d6fe631290f000ce1d57bcd0` adds a combining-mark-before-`mod` control and proves U+200E Rust whitespace still separates a real `mod` keyword. +- Root-cause audit then exposed a real false negative in the shared trivia owner: Python `str.isspace()` does not recognize U+200E/U+200F even though Rust does. Structural RED `ce33ae1aa020b1b9903aa02c5952a90bfd1581e5` adds hostile `include\u200e!` and `include!\u200f(` forms. Repair `e199aac4a64e636b3a7412f3d9347644e96e636b` makes `_skip_rust_trivia()` consume the exact Rust `Pattern_White_Space` set. ## Risk and follow-up This remains a temporary lexical security boundary. It is not compiler-derived source-input provenance and can intentionally reject legitimate inline modules in custom target roots. Before OriginWeave needs such custom-target module trees, replace the heuristic with compiler-derived or equivalently exact source-input evidence that identifies the actual bytes compiled for supported target configurations without widening Cargo ownership or relying on source-text approximations. -The existing `docs/traceability/browser-session-rust-source-indirection.md` remains the broader source-indirection record. This document narrows only the current custom-target lexical correction and should be folded into that canonical record when the current stacked Browser Session lineage is reconciled. +The root-cause audit also shows that other source-indirection token recognizers still use Python-regex `\w` boundaries (`include`, `use`, `as`, and `path`). Their concrete security/false-positive behavior must be verified with Rust `XID_Continue` hostile/control fixtures before claiming the source-indirection lexer is fully Unicode-current. Do not widen those owners by assumption; preserve a structural RED before any shared boundary change. + +The existing `docs/traceability/browser-session-rust-source-indirection.md` remains the broader source-indirection record. This document narrows the current custom-target lexical correction and shared Rust-whitespace root repair and should be folded into that canonical record when the current stacked Browser Session lineage is reconciled. From 8e50e1c7d2a48f627f81cf181982c2a69623559e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:28:14 +0900 Subject: [PATCH 579/632] test(browser-session): expose include XID boundary false positive --- ...est_browser_session_include_comment_trivia_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_browser_session_include_comment_trivia_contract.py b/tests/test_browser_session_include_comment_trivia_contract.py index 034f20d06..6f9384eac 100644 --- a/tests/test_browser_session_include_comment_trivia_contract.py +++ b/tests/test_browser_session_include_comment_trivia_contract.py @@ -68,6 +68,14 @@ def test_non_ascii_rust_whitespace_between_bang_and_delimiter_fails_closed(self) 'include!\u200f("../generated_adapter.rs");\n' ) + def test_xid_prefix_macro_name_is_not_builtin_include(self) -> None: + root = self._workspace_with_source( + 'macro_rules! a\u0301include { ($path:literal) => {}; }\n' + 'a\u0301include!("../generated_adapter.rs");\n' + ) + + source_contract._assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From a400e836fbe1212dc8188f0a601f0d5b4964b979 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:29:08 +0900 Subject: [PATCH 580/632] fix(browser-session): share Rust identifier boundary for include --- ...ession_rust_source_indirection_contract.py | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index d4b3dde95..138f1b2c5 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -15,7 +15,7 @@ spec.loader.exec_module(boundary) -INCLUDE_TOKEN = re.compile(r"(? int: return index +def _rust_keyword_is_identifier_adjacent(char: str) -> bool: + """Conservatively reject token boundaries that could be Rust identifier continuation.""" + if char.isascii(): + return char.isalnum() or char == "_" + return char not in RUST_PATTERN_WHITESPACE + + +def _rust_identifier_token_end( + text: str, + offset: int, + spelling: str, + *, + allow_raw: bool = False, +) -> int | None: + """Return a conservative Rust identifier-token end independent of Python Unicode tables.""" + token_start = offset + identifier_start = offset + if allow_raw and text.startswith("r#", offset) and text.startswith(spelling, offset + 2): + identifier_start = offset + 2 + elif not text.startswith(spelling, offset): + return None + + if token_start: + previous = text[token_start - 1] + if previous == "#" or _rust_keyword_is_identifier_adjacent(previous): + return None + + end = identifier_start + len(spelling) + if end < len(text) and _rust_keyword_is_identifier_adjacent(text[end]): + return None + return end + + def _has_include_macro(text: str) -> bool: """Detect lexical include! macro syntax outside Rust comments and literals.""" cursor = 0 @@ -84,38 +117,22 @@ def _has_include_macro(text: str) -> bool: cursor = char_end continue - match = INCLUDE_TOKEN.match(text, cursor) - if match is None: + token_end = _rust_identifier_token_end(text, cursor, INCLUDE_TOKEN, allow_raw=True) + if token_end is None: cursor += 1 continue - bang = _skip_rust_trivia(text, match.end()) + bang = _skip_rust_trivia(text, token_end) if bang < len(text) and text[bang] == "!": delimiter = _skip_rust_trivia(text, bang + 1) if delimiter < len(text) and text[delimiter] in "([{": return True - cursor = match.end() + cursor = token_end return False -def _rust_keyword_is_identifier_adjacent(char: str) -> bool: - """Conservatively reject keyword boundaries that could be Rust identifier continuation.""" - if char.isascii(): - return char.isalnum() or char == "_" - return char not in RUST_PATTERN_WHITESPACE - - def _matches_custom_target_mod_token(text: str, offset: int) -> bool: """Match the Rust `mod` keyword without relying on Python's Unicode identifier table.""" - if not text.startswith(CUSTOM_TARGET_MOD_TOKEN, offset): - return False - if offset: - previous = text[offset - 1] - if previous == "#" or _rust_keyword_is_identifier_adjacent(previous): - return False - end = offset + len(CUSTOM_TARGET_MOD_TOKEN) - if end < len(text) and _rust_keyword_is_identifier_adjacent(text[end]): - return False - return True + return _rust_identifier_token_end(text, offset, CUSTOM_TARGET_MOD_TOKEN) is not None def _has_custom_target_mod_token(text: str) -> bool: @@ -210,25 +227,26 @@ def _has_aliased_include_import(text: str) -> bool: use_cursor = char_end continue - include_match = INCLUDE_TOKEN.match(use_tree, use_cursor) + include_match = re.match(r"(?= len(use_tree): - use_cursor = include_match.end() + use_cursor = include_end continue if use_tree[alias_start] == "_": next_offset = alias_start + 1 if next_offset >= len(use_tree) or not ( use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" ): - use_cursor = include_match.end() + use_cursor = include_end continue return True cursor = statement_end + 1 From 6f8da237d2f0deb979c9f71f94d678f83e8ccdc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:31:55 +0900 Subject: [PATCH 581/632] test(browser-session): expose include alias XID boundary false positive --- ...st_browser_session_include_comment_trivia_contract.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_browser_session_include_comment_trivia_contract.py b/tests/test_browser_session_include_comment_trivia_contract.py index 6f9384eac..c12e1a72f 100644 --- a/tests/test_browser_session_include_comment_trivia_contract.py +++ b/tests/test_browser_session_include_comment_trivia_contract.py @@ -76,6 +76,15 @@ def test_xid_prefix_macro_name_is_not_builtin_include(self) -> None: source_contract._assert_no_unmodeled_rust_source_indirection(root) + def test_xid_prefixed_import_name_is_not_builtin_include_alias(self) -> None: + root = self._workspace_with_source( + 'mod source { pub fn a\u0301include() {} }\n' + 'use source::a\u0301include as embed;\n' + 'pub fn call_surface() { embed(); }\n' + ) + + source_contract._assert_no_unmodeled_rust_source_indirection(root) + if __name__ == "__main__": unittest.main() From a827cc5a0eb0e672e65c37ae3bc1b371f13943d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:32:50 +0900 Subject: [PATCH 582/632] fix(browser-session): reuse Rust identifier boundary for include aliases --- ...browser_session_rust_source_indirection_contract.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 138f1b2c5..7581af31f 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -227,11 +227,15 @@ def _has_aliased_include_import(text: str) -> bool: use_cursor = char_end continue - include_match = re.match(r"(? Date: Sat, 19 Sep 2026 20:41:25 +0900 Subject: [PATCH 583/632] test(browser-session): expose path-meta lexical false positives --- ...wser_session_path_meta_lexical_contract.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_browser_session_path_meta_lexical_contract.py diff --git a/tests/test_browser_session_path_meta_lexical_contract.py b/tests/test_browser_session_path_meta_lexical_contract.py new file mode 100644 index 000000000..827ef1fce --- /dev/null +++ b/tests/test_browser_session_path_meta_lexical_contract.py @@ -0,0 +1,48 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionPathMetaLexicalContractTests(unittest.TestCase): + """Keep path-attribute authority tied to lexical Rust meta items, not data text.""" + + def test_doc_string_path_text_is_not_path_meta(self) -> None: + self.assertFalse( + source_indirection._has_path_meta('doc = "path = \\"review_bypass.rs\\""') + ) + + def test_raw_doc_string_path_text_is_not_path_meta(self) -> None: + self.assertFalse( + source_indirection._has_path_meta('doc = r#"path = \\"review_bypass.rs\\""#') + ) + + def test_comment_path_text_is_not_path_meta(self) -> None: + self.assertFalse( + source_indirection._has_path_meta( + 'cfg_attr(unix, /* path = "review_bypass.rs" */ allow(dead_code))' + ) + ) + + def test_nested_cfg_attr_path_meta_remains_authority(self) -> None: + self.assertTrue( + source_indirection._has_path_meta( + 'cfg_attr(unix, path /* reviewed trivia */ = "unix_adapter.rs")' + ) + ) + + +if __name__ == "__main__": + unittest.main() From 79ad52cdd2a29da5cedbf1a134aec8779227bdb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:42:35 +0900 Subject: [PATCH 584/632] fix(browser-session): lex path meta outside Rust data tokens --- ...ession_rust_source_indirection_contract.py | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 7581af31f..598cc6d7f 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -395,11 +395,35 @@ def _rust_attribute_bodies(text: str) -> list[str]: def _has_path_meta(attribute_body: str) -> bool: - """Return whether an attribute contains a path meta item followed by valid Rust trivia and '='.""" - for match in PATH_TOKEN.finditer(attribute_body): - cursor = _skip_rust_trivia(attribute_body, match.end()) - if cursor < len(attribute_body) and attribute_body[cursor] == "=": + """Return whether a lexical attribute meta item selects a Rust module source path.""" + cursor = 0 + while cursor < len(attribute_body): + trivia_end = _skip_rust_trivia(attribute_body, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = _raw_string_end(attribute_body, cursor) + if raw_end is not None: + cursor = raw_end + continue + if attribute_body[cursor] == '"': + cursor = _quoted_string_end(attribute_body, cursor) + continue + if attribute_body[cursor] == "'": + char_end = _simple_char_literal_end(attribute_body, cursor) + if char_end is not None: + cursor = char_end + continue + + path_end = _rust_identifier_token_end(attribute_body, cursor, "path") + if path_end is None: + cursor += 1 + continue + equals = _skip_rust_trivia(attribute_body, path_end) + if equals < len(attribute_body) and attribute_body[equals] == "=": return True + cursor = path_end return False @@ -621,4 +645,4 @@ def test_string_containing_path_attribute_text_is_not_source_indirection(self) - if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From e4385f344244b7a4181b2f69191965c33f4d7ae6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:43:30 +0900 Subject: [PATCH 585/632] docs(browser-session): trace path-meta lexical authority --- ...ser-session-path-meta-lexical-authority.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/traceability/browser-session-path-meta-lexical-authority.md diff --git a/docs/traceability/browser-session-path-meta-lexical-authority.md b/docs/traceability/browser-session-path-meta-lexical-authority.md new file mode 100644 index 000000000..0c07a8fc0 --- /dev/null +++ b/docs/traceability/browser-session-path-meta-lexical-authority.md @@ -0,0 +1,56 @@ +# Browser Session Rust path-meta lexical authority + +## Scope + +OriginWeave treats Rust module-source selection as Browser Session build provenance because a `#[path = ...]` meta item can redirect a reviewed module declaration to different source bytes. Cargo production package/source discovery remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`; this slice only classifies already-discovered Rust attribute bodies and does not rediscover Cargo topology. + +## Problem + +The predecessor `_has_path_meta()` searched every extracted attribute body with a raw regular expression for `path` followed by Rust trivia and `=`. `_rust_attribute_bodies()` correctly excluded comments and string literals while locating attribute boundaries, but the inner classifier then rescanned the attribute body without the same lexical discipline. Consequently harmless data such as `#[doc = "path = \"review_bypass.rs\""]`, a raw doc string containing the same text, or `/* path = ... */` inside an attribute was classified as executable module-source authority. + +That is a false-positive authorization result: the gate can reject reviewed source even though Rust has no `path = ...` meta item at that lexical position. Keeping such over-approximation indefinitely would create pressure to weaken the provenance gate instead of making the classifier correspond to Rust syntax. + +## Constraints + +- Do not weaken actual `#[path = ...]` or nested `cfg_attr(..., path = ...)` fail-closed behavior. +- Do not create a second Cargo package/source scanner. +- Reuse the existing Rust trivia, raw-string, quoted-string, character-literal, and identifier-boundary helpers so source-indirection policies share one lexical model. +- Treat comments as lexical trivia and literals as data, consistent with the Rust Reference. +- This is a source-semantic contract repair. It is not hosted exact-head GREEN, protected-main integration, or release evidence. + +## Alternatives considered + +1. Keep the raw regex and add allowlist entries for documentation strings. Rejected because allowlists would encode incidental text and would still miss arbitrary comment/literal spellings. +2. Parse Rust with a second external parser in this Python contract. Rejected because this policy only needs one bounded lexical distinction and a second parser would create another source-of-truth and dependency surface. +3. Reuse the existing lexical helpers inside `_has_path_meta()`. Selected because it is the smallest causal repair and preserves the existing source-indirection owner. + +## Decision and exact evidence + +Structural RED **`67789f0cdb55825e1b6caa91b38643dad890759e`** adds a supplemental contract proving three data-token controls and one real nested path-meta authority case: + +- ordinary doc-string text containing `path = ...` must not be classified; +- raw doc-string text containing `path = ...` must not be classified; +- non-doc comment text containing `path = ...` must not be classified; +- `cfg_attr(unix, path /* trivia */ = "unix_adapter.rs")` must remain classified. + +The predecessor returns `True` for all four cases, so the first three controls are structural RED rather than documentation-only assertions. + +Minimal repair **`79ad52cdd2a29da5cedbf1a134aec8779227bdb4`** changes only `_has_path_meta()` in the canonical Rust source-indirection contract. It walks the attribute body using `_skip_rust_trivia()`, `_raw_string_end()`, `_quoted_string_end()`, `_simple_char_literal_end()`, and `_rust_identifier_token_end()`. A lexical `path` token followed by Rust trivia and `=` still returns `True`; comments and string/character data are skipped before token classification. + +No Browser Session runtime code, Cargo topology owner, WebDriver BiDi policy, Wardnet/EgressWeave/Keyverse/contextual-orchestrator contract, workflow, ruleset, or release surface is changed by this repair. + +## Standards traceability + +Rust attributes are tokenized as `# [ Attr ]` / `#! [ Attr ]`; the meta-item grammar includes `SimplePath = Expression` and nested meta-item sequences. The same Reference describes ordinary non-doc comments as whitespace. These rules justify recognizing lexical meta-item tokens while excluding comment and literal payload text from source-selection authority. + +- Rust Project. (2026). *The Rust Reference: Attributes*. https://doc.rust-lang.org/reference/attributes.html +- Rust Project. (2026). *The Rust Reference: Comments*. https://doc.rust-lang.org/reference/comments.html +- Rust Project. (2026). *The Rust Reference: Paths*. https://doc.rust-lang.org/reference/paths.html + +## Security and operability effect + +The repair does not broaden which actual module-source selectors are allowed. It removes false-positive policy findings that arose from inert attribute data while retaining fail-closed treatment for real `path = ...` meta items. This keeps evidence classification explainable: a rejected path attribute now corresponds to lexical Rust metadata rather than a substring that happened to occur inside data. + +## Residual risk and follow-up + +The contract is deliberately lexical rather than a complete Rust parser. New Rust attribute/macro forms that can select source bytes without a lexical `path = ...` meta item remain a future provenance finding and must be introduced with a hostile fixture and primary-language evidence. Exact-head hosted tests and independent current-head review remain required before this generation can be treated as executable GREEN or release-ready. From f8f6e665c2651198825fe806781838ffb1165493 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:45:59 +0900 Subject: [PATCH 586/632] test(browser-session): expose raw path attribute bypass --- tests/test_browser_session_path_meta_lexical_contract.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_browser_session_path_meta_lexical_contract.py b/tests/test_browser_session_path_meta_lexical_contract.py index 827ef1fce..2086269f4 100644 --- a/tests/test_browser_session_path_meta_lexical_contract.py +++ b/tests/test_browser_session_path_meta_lexical_contract.py @@ -43,6 +43,9 @@ def test_nested_cfg_attr_path_meta_remains_authority(self) -> None: ) ) + def test_raw_identifier_path_meta_remains_authority(self) -> None: + self.assertTrue(source_indirection._has_path_meta('r#path = "raw_identifier.rs"')) + if __name__ == "__main__": unittest.main() From debd5f62b0de4edf45d786859ba6cbfb96f3dd29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:46:48 +0900 Subject: [PATCH 587/632] fix(browser-session): recognize raw path attribute authority --- ...est_browser_session_rust_source_indirection_contract.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 598cc6d7f..875266056 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -416,7 +416,12 @@ def _has_path_meta(attribute_body: str) -> bool: cursor = char_end continue - path_end = _rust_identifier_token_end(attribute_body, cursor, "path") + path_end = _rust_identifier_token_end( + attribute_body, + cursor, + "path", + allow_raw=True, + ) if path_end is None: cursor += 1 continue From b59dd1b9475a4752098c5df10e77e797eadda726 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:48:15 +0900 Subject: [PATCH 588/632] docs(browser-session): trace raw path attribute authority --- ...ser-session-path-meta-lexical-authority.md | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-path-meta-lexical-authority.md b/docs/traceability/browser-session-path-meta-lexical-authority.md index 0c07a8fc0..03c9d4533 100644 --- a/docs/traceability/browser-session-path-meta-lexical-authority.md +++ b/docs/traceability/browser-session-path-meta-lexical-authority.md @@ -10,12 +10,15 @@ The predecessor `_has_path_meta()` searched every extracted attribute body with That is a false-positive authorization result: the gate can reject reviewed source even though Rust has no `path = ...` meta item at that lexical position. Keeping such over-approximation indefinitely would create pressure to weaken the provenance gate instead of making the classifier correspond to Rust syntax. +The first lexical repair exposed the complementary false-negative: Rust raw identifiers use the `r#IDENTIFIER` spelling while the `r#` prefix is not part of the identifier itself. A raw `r#path = "..."` meta item therefore names the same `path` attribute authority, but the initial lexical classifier called the shared identifier helper with raw identifiers disabled and could miss that source-selection surface. + ## Constraints -- Do not weaken actual `#[path = ...]` or nested `cfg_attr(..., path = ...)` fail-closed behavior. +- Do not weaken actual `#[path = ...]`, raw-identifier `#[r#path = ...]`, or nested `cfg_attr(..., path = ...)` fail-closed behavior. - Do not create a second Cargo package/source scanner. - Reuse the existing Rust trivia, raw-string, quoted-string, character-literal, and identifier-boundary helpers so source-indirection policies share one lexical model. - Treat comments as lexical trivia and literals as data, consistent with the Rust Reference. +- Treat a raw identifier according to Rust identifier identity rather than as inert spelling data. - This is a source-semantic contract repair. It is not hosted exact-head GREEN, protected-main integration, or release evidence. ## Alternatives considered @@ -23,6 +26,7 @@ That is a false-positive authorization result: the gate can reject reviewed sour 1. Keep the raw regex and add allowlist entries for documentation strings. Rejected because allowlists would encode incidental text and would still miss arbitrary comment/literal spellings. 2. Parse Rust with a second external parser in this Python contract. Rejected because this policy only needs one bounded lexical distinction and a second parser would create another source-of-truth and dependency surface. 3. Reuse the existing lexical helpers inside `_has_path_meta()`. Selected because it is the smallest causal repair and preserves the existing source-indirection owner. +4. Accept only the ordinary spelling `path` and reject or ignore `r#path`. Rejected because Rust raw-identifier syntax denotes the underlying identifier without `r#`; provenance classification must not depend on that surface spelling. ## Decision and exact evidence @@ -37,20 +41,29 @@ The predecessor returns `True` for all four cases, so the first three controls a Minimal repair **`79ad52cdd2a29da5cedbf1a134aec8779227bdb4`** changes only `_has_path_meta()` in the canonical Rust source-indirection contract. It walks the attribute body using `_skip_rust_trivia()`, `_raw_string_end()`, `_quoted_string_end()`, `_simple_char_literal_end()`, and `_rust_identifier_token_end()`. A lexical `path` token followed by Rust trivia and `=` still returns `True`; comments and string/character data are skipped before token classification. -No Browser Session runtime code, Cargo topology owner, WebDriver BiDi policy, Wardnet/EgressWeave/Keyverse/contextual-orchestrator contract, workflow, ruleset, or release surface is changed by this repair. +Follow-up RED **`f8f6e665c2651198825fe806781838ffb1165493`** adds `r#path = "raw_identifier.rs"` as a source-selection authority case. The first lexical repair returns `False` for that spelling, demonstrating a false-negative bypass rather than an invented edge case. + +Follow-up repair **`debd5f62b0de4edf45d786859ba6cbfb96f3dd29`** keeps the same lexical classifier and changes only the shared identifier-token call for `path` to `allow_raw=True`. This preserves ordinary `path`, comment/literal exclusion, and nested `cfg_attr` behavior while classifying the raw spelling as the same attribute authority. + +No Browser Session runtime code, Cargo topology owner, WebDriver BiDi policy, Wardnet/EgressWeave/Keyverse/contextual-orchestrator contract, workflow, ruleset, or release surface is changed by these repairs. + +## Standards and implementation traceability -## Standards traceability +Rust attributes are tokenized as `# [ Attr ]` / `#! [ Attr ]`; the meta-item grammar includes `SimplePath = Expression` and nested meta-item sequences. The same Reference describes ordinary non-doc comments as whitespace. Rust's identifier grammar includes raw identifiers, and the `r#` prefix is not part of the actual identifier. These rules justify recognizing lexical meta-item tokens while excluding comment/literal payload text and treating `path` / `r#path` as the same identifier authority. -Rust attributes are tokenized as `# [ Attr ]` / `#! [ Attr ]`; the meta-item grammar includes `SimplePath = Expression` and nested meta-item sequences. The same Reference describes ordinary non-doc comments as whitespace. These rules justify recognizing lexical meta-item tokens while excluding comment and literal payload text from source-selection authority. +The current rustc source independently confirms the compiler-side owner: `rustc_attr_parsing::attributes::path::PathParser` registers the attribute under `sym::path`, and module expansion selects the first attribute satisfying `has_name(sym::path)` before reading its string value. OriginWeave does not copy that parser; this implementation evidence only anchors the security contract to the compiler behavior it is constraining. - Rust Project. (2026). *The Rust Reference: Attributes*. https://doc.rust-lang.org/reference/attributes.html - Rust Project. (2026). *The Rust Reference: Comments*. https://doc.rust-lang.org/reference/comments.html +- Rust Project. (2026). *The Rust Reference: Identifiers*. https://doc.rust-lang.org/reference/identifiers.html - Rust Project. (2026). *The Rust Reference: Paths*. https://doc.rust-lang.org/reference/paths.html +- Rust Project. (2026). `compiler/rustc_attr_parsing/src/attributes/path.rs`, revision `971903d9aee24befd88423f42826c232e27c8190`. https://github.com/rust-lang/rust/blob/971903d9aee24befd88423f42826c232e27c8190/compiler/rustc_attr_parsing/src/attributes/path.rs +- Rust Project. (2026). `compiler/rustc_expand/src/module.rs`, revision `971903d9aee24befd88423f42826c232e27c8190`. https://github.com/rust-lang/rust/blob/971903d9aee24befd88423f42826c232e27c8190/compiler/rustc_expand/src/module.rs ## Security and operability effect -The repair does not broaden which actual module-source selectors are allowed. It removes false-positive policy findings that arose from inert attribute data while retaining fail-closed treatment for real `path = ...` meta items. This keeps evidence classification explainable: a rejected path attribute now corresponds to lexical Rust metadata rather than a substring that happened to occur inside data. +The repair does not broaden which module-source selectors are permitted. It removes false-positive policy findings caused by inert attribute data and removes the raw-identifier false-negative that could make a real source selector invisible to the provenance contract. A rejected path attribute therefore corresponds to lexical Rust metadata and remains fail closed across ordinary and raw identifier spellings. ## Residual risk and follow-up -The contract is deliberately lexical rather than a complete Rust parser. New Rust attribute/macro forms that can select source bytes without a lexical `path = ...` meta item remain a future provenance finding and must be introduced with a hostile fixture and primary-language evidence. Exact-head hosted tests and independent current-head review remain required before this generation can be treated as executable GREEN or release-ready. +The contract is deliberately lexical rather than a complete Rust parser. New Rust attribute/macro forms that can select source bytes without a lexical `path = ...`-equivalent meta item remain a future provenance finding and must be introduced with a hostile fixture and primary-language evidence. Exact-head hosted tests and independent current-head review remain required before this generation can be treated as executable GREEN or release-ready. From 6875cb195fc29868b56c18fc1e5fe33477d77830 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:05:38 +0900 Subject: [PATCH 589/632] docs(changelog): record Rust source provenance repairs --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa51a11e4..625426f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Fixed - Failed closed when Browser Session production Rust source selects native libraries through direct `#[link(...)]` or `cfg_attr(..., link(...))` attributes, while the shared Rust attribute lexer treats comment and string/character/raw-string text as lexical data rather than authority. -- Failed closed when Browser Session production Rust source embeds compile-time files through `include_bytes!` or `include_str!`, reusing the canonical production-source closure and shared Rust source-indirection lexer instead of rediscovering Cargo topology. +- Failed closed when Browser Session production Rust source embeds compile-time files through direct or namespaced `include_bytes!` / `include_str!` and callable `use ... as ...` aliases, reusing the canonical production-source closure and shared Rust source-indirection lexer instead of rediscovering Cargo topology. +- Failed closed when Browser Session production Rust source injects executable source through lexical `include!` invocations or callable aliases, while comment, ordinary/raw string, character-literal, and commented grouped-use text remain lexical data rather than source authority. +- Failed closed when custom Cargo target roots use lexical Rust `mod` tokens that can resolve additional module source outside Cargo's default `src/**/*.rs` sibling closure; comment/string/character/raw-string text and identifier-adjacent lookalikes no longer create false authority. +- Failed closed when Browser Session production Rust attributes select module source through lexical `path = ...`, including raw-identifier `r#path`, while comment and ordinary/raw string/character-literal text inside attributes remains data rather than source authority. - Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding loads LLD pass-plugin libraries through separated/joined `--load-pass-plugin`, keeping executable LTO pass code inside reviewed toolchain/plugin provenance. @@ -150,4 +153,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 From 960d361a37d942937f9d2d88f9cd745265a77a90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:06:30 +0900 Subject: [PATCH 590/632] test(browser-session): expose Cargo host-config authority gap --- ...on_cargo_host_config_authority_contract.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_browser_session_cargo_host_config_authority_contract.py diff --git a/tests/test_browser_session_cargo_host_config_authority_contract.py b/tests/test_browser_session_cargo_host_config_authority_contract.py new file mode 100644 index 000000000..5638c17a4 --- /dev/null +++ b/tests/test_browser_session_cargo_host_config_authority_contract.py @@ -0,0 +1,71 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_cargo_compiler_authority_contract", + AUTHORITY_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCargoHostConfigAuthorityContractTests(unittest.TestCase): + """Keep nightly Cargo host-target execution inside the canonical compiler-authority owner.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def _assert_host_authority_fails_closed(self, config_text: str) -> None: + root = self._workspace_with_config(config_text) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_repository_host_linker_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host]\nlinker = "tools/review-bypass-host-linker"\n' + ) + + def test_repository_host_arch_runner_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host.x86_64-unknown-linux-gnu]\nrunner = "tools/review-bypass-host-runner"\n' + ) + + def test_repository_host_rustflags_external_input_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host]\nrustflags = ["-C", "link-arg=-Wl,--library=review_bypass"]\n' + ) + + def test_unrelated_host_rustflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[host]\nrustflags = ["-C", "opt-level=2"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From da6b14de3366c235b1b4c10d340e68477cd41c2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:08:08 +0900 Subject: [PATCH 591/632] fix(browser-session): govern Cargo host execution authority --- ...ssion_cargo_compiler_authority_contract.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 252a6ef64..b36bfa146 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -562,6 +562,31 @@ def _configured_profile_rustflag_authority(value: object, prefix: str = "profile return sorted(configured) +def _configured_host_execution_authority(value: object, prefix: str = "host") -> list[str]: + """Return nightly Cargo host-target settings that widen execution or compiler-input authority.""" + if not isinstance(value, dict): + return [] + configured: list[str] = [] + for key in sorted(TARGET_EXECUTION_KEYS.intersection(value)): + configured.append(f"{prefix}.{key}") + + rustflags = value.get("rustflags") + if _flags_select_codegen_backend(rustflags): + configured.append(f"{prefix}.rustflags:codegen backend") + if _flags_select_linker(rustflags): + configured.append(f"{prefix}.rustflags:codegen linker") + if _flags_extend_external_link_inputs(rustflags): + configured.append(f"{prefix}.rustflags:external compiler input") + if _flags_select_ambient_host_cpu(rustflags): + configured.append(f"{prefix}.rustflags:ambient host cpu") + + for key, setting in value.items(): + if key in TARGET_EXECUTION_KEYS or key == "rustflags" or not isinstance(setting, dict): + continue + configured.extend(_configured_host_execution_authority(setting, f"{prefix}.{key}")) + return sorted(configured) + + def _configured_custom_target_specs(value: object) -> list[str]: """Return repository-selected custom rustc target specification paths.""" if isinstance(value, str): @@ -622,6 +647,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) unstable_configured = _configured_unstable_toolchain_inputs(parsed.get("unstable")) profile_codegen_backends = _configured_profile_codegen_backends(parsed.get("profile")) profile_rustflag_authority = _configured_profile_rustflag_authority(parsed.get("profile")) + host_configured = _configured_host_execution_authority(parsed.get("host")) build = parsed.get("build") build_configured = ( @@ -696,6 +722,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if ( build_configured or target_configured + or host_configured or environment_configured or include_configured or unstable_configured @@ -706,7 +733,7 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) raise AssertionError( "Cargo Rust tool/target execution override requires an explicit Browser Session provenance contract: " f"{relative} build_keys={build_configured} target_keys={target_configured} " - f"env_keys={environment_configured} include={include_configured} " + f"host_keys={host_configured} env_keys={environment_configured} include={include_configured} " f"unstable_keys={unstable_configured} profile_codegen_backends={profile_codegen_backends} " f"profile_rustflag_authority={profile_rustflag_authority}" ) From 66d8a535befaade759eba6f3f464499792d6bfaf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:09:52 +0900 Subject: [PATCH 592/632] test(browser-session): cover Cargo host rustdoc and link overrides --- ...ion_cargo_host_config_authority_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_browser_session_cargo_host_config_authority_contract.py b/tests/test_browser_session_cargo_host_config_authority_contract.py index 5638c17a4..4498feba3 100644 --- a/tests/test_browser_session_cargo_host_config_authority_contract.py +++ b/tests/test_browser_session_cargo_host_config_authority_contract.py @@ -60,12 +60,30 @@ def test_repository_host_rustflags_external_input_fails_closed(self) -> None: '[host]\nrustflags = ["-C", "link-arg=-Wl,--library=review_bypass"]\n' ) + def test_repository_host_rustdocflags_external_input_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host.x86_64-unknown-linux-gnu]\n' + 'rustdocflags = ["--extern=review_bypass=tools/libreview_bypass.rlib"]\n' + ) + + def test_repository_host_links_build_script_override_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host.x86_64-unknown-linux-gnu.review_bypass]\n' + 'rustc-link-search = ["tools/review-bypass-native"]\n' + ) + def test_unrelated_host_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[host]\nrustflags = ["-C", "opt-level=2"]\n' ) authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_host_rustdocflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[host]\nrustdocflags = ["--document-private-items"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + if __name__ == "__main__": unittest.main() From d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:12:26 +0900 Subject: [PATCH 593/632] fix(browser-session): close Cargo host rustdoc and link-override authority --- ...ssion_cargo_compiler_authority_contract.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index b36bfa146..881f6a56a 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -562,7 +562,11 @@ def _configured_profile_rustflag_authority(value: object, prefix: str = "profile return sorted(configured) -def _configured_host_execution_authority(value: object, prefix: str = "host") -> list[str]: +def _configured_host_execution_authority( + value: object, + prefix: str = "host", + depth: int = 0, +) -> list[str]: """Return nightly Cargo host-target settings that widen execution or compiler-input authority.""" if not isinstance(value, dict): return [] @@ -580,10 +584,31 @@ def _configured_host_execution_authority(value: object, prefix: str = "host") -> if _flags_select_ambient_host_cpu(rustflags): configured.append(f"{prefix}.rustflags:ambient host cpu") + rustdocflags = value.get("rustdocflags") + if _flags_select_codegen_backend(rustdocflags): + configured.append(f"{prefix}.rustdocflags:codegen backend") + if _flags_select_linker(rustdocflags): + configured.append(f"{prefix}.rustdocflags:codegen linker") + if _flags_extend_external_link_inputs(rustdocflags): + configured.append(f"{prefix}.rustdocflags:external compiler input") + if _flags_select_ambient_host_cpu(rustdocflags): + configured.append(f"{prefix}.rustdocflags:ambient host cpu") + if _flags_select_rustdoc_test_execution(rustdocflags): + configured.append(f"{prefix}.rustdocflags:doctest execution") + if _flags_select_rustdoc_documentation_input(rustdocflags): + configured.append(f"{prefix}.rustdocflags:documentation input") + if _flags_select_rustdoc_doctest_compiler_authority(rustdocflags): + configured.append(f"{prefix}.rustdocflags:doctest compiler authority") + for key, setting in value.items(): - if key in TARGET_EXECUTION_KEYS or key == "rustflags" or not isinstance(setting, dict): + if key in TARGET_EXECUTION_KEYS or key in {"rustflags", "rustdocflags"} or not isinstance(setting, dict): + continue + if depth >= 1: + configured.append(f"{prefix}.links build-script override:{key}") continue - configured.extend(_configured_host_execution_authority(setting, f"{prefix}.{key}")) + configured.extend( + _configured_host_execution_authority(setting, f"{prefix}.{key}", depth + 1) + ) return sorted(configured) From 9fc512b4ba3b4c51e48cadf1416d510141c7989f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:12:59 +0900 Subject: [PATCH 594/632] docs(traceability): bind Cargo host-config authority --- ...ser-session-cargo-host-config-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-host-config-authority.md diff --git a/docs/traceability/browser-session-cargo-host-config-authority.md b/docs/traceability/browser-session-cargo-host-config-authority.md new file mode 100644 index 000000000..ade0daee8 --- /dev/null +++ b/docs/traceability/browser-session-cargo-host-config-authority.md @@ -0,0 +1,47 @@ +# Browser Session Cargo host-config execution authority traceability + +## Decision + +Repository-owned Cargo configuration must not be able to introduce unreviewed host-side compiler, linker, runner, rustdoc, or build-script-link authority for Browser Session production builds. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo compiler/toolchain/execution and external-input authority; this dossier adds no second Cargo package/source topology scanner. + +The contract therefore fails closed on authority-bearing `[host]` and `[host.]` settings: `linker`, `runner`, authority-extending `rustflags` / `rustdocflags`, and host-tuple `links` build-script overrides. Harmless host flags such as `-C opt-level=2` and `--document-private-items` remain permitted. + +## Problem and buyer/security effect + +Cargo's nightly `host-config` feature gives Git-owned configuration a distinct path for artifacts compiled or executed on the build host, including build scripts and other host artifacts. Cargo documents generic `[host]` and host-tuple-specific tables, with host-tuple settings taking precedence, and documents `host.runner` as the wrapper used to execute host build targets such as build scripts. Cargo's current `TargetConfig` also exposes `rustflags`, `rustdocflags`, `linker`, `runner`, and `links_overrides` for `[target]` or `[host]` configurations. + +Before this generation the Browser Session compiler-authority owner examined `[build]`, `[target]`, repository environment/config inclusion, unstable toolchain selectors, and profile rustflags/codegen backends, but ignored `[host]`. A reviewed repository could therefore pre-position host linker/runner/compiler-input authority that becomes effective when nightly `-Zhost-config` / `-Ztarget-applies-to-host` semantics are selected. Treating the feature as currently inactive would be mutable invocation-state trust rather than source provenance. + +## RED → repair evidence + +- **Structural RED `960d361a37d942937f9d2d88f9cd745265a77a90`** added a focused supplemental contract that calls the canonical compiler-authority owner and proves that generic host linker, host-tuple runner, and authority-extending host rustflags were not rejected, while unrelated host rustflags remain a control. +- **Minimal canonical repair `da6b14de3366c235b1b4c10d340e68477cd41c2a`** added `_configured_host_execution_authority()` to the existing compiler-authority owner. It reused existing target execution keys and rustc/linker/input classifiers rather than rediscovering Cargo package/source topology. +- Review of Cargo's current `TargetConfig` surface exposed two valid omissions in the first repair: host `rustdocflags` share the same target configuration structure, and host target configuration can carry `links_overrides` that replace build-script output. **Review-driven RED `66d8a535befaade759eba6f3f464499792d6bfaf`** added hostile rustdoc external-input and host-tuple links-override cases plus an unrelated rustdoc control. +- **Causal follow-up repair `d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec`** reused the existing rustdoc classifiers and treats nested host-tuple build-script override tables as explicit authority. The change is confined to the canonical compiler-authority helper; no Browser Session runtime, WebDriver BiDi policy, canonical source-topology owner, workflow, ruleset, or external CWL owner is modified. + +## Alternatives considered + +1. **Ignore `[host]` until `-Zhost-config` appears in repository configuration.** Rejected. Invocation flags and Cargo's unstable-feature activation are separate mutable execution surfaces; Git-owned latent authority must not become pre-authorized merely because the current invocation does not activate it. +2. **Reject every `[host]` table or every host rustflag.** Rejected. This would conflate deterministic optimization/documentation settings with execution/input authority and would make the guard broader than the owned security invariant. +3. **Create a second Cargo scanner in the focused test.** Rejected. The focused test only constructs hostile/config-control fixtures and delegates the decision to the canonical compiler-authority owner. + +## Invariants + +- `test_browser_session_trusted_adapter_boundary.py` remains the single writer for production Cargo package/source topology. +- `test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler/rustdoc/toolchain/linker execution and external-input authority. +- `[host]` linker/runner authority is fail closed. +- Authority-extending host `rustflags` and `rustdocflags` are fail closed using the same classifier semantics as `[build]` / `[target]`. +- Host-tuple `links` overrides are fail closed because they can suppress a package build script and inject replacement build output, including native link search/library material. +- Non-authority host flags remain admissible controls; the guard is not a blanket ban on host configuration. + +## Remaining evidence and risk + +This generation is source-structural evidence until the exact pull-request head receives executable hosted repository/security checks. It does not claim protected-main integration, release readiness, whole-PR review closure, or owned 100% Docstring/rustdoc/Test/Edge Case Coverage. Ambient user/global Cargo configuration, CLI `--config`, environment variables, and toolchain selection are separate invocation/runtime provenance surfaces and are not made trustworthy by this repository-owned config guard. + +## Primary references + +Cargo Team. (2026). *Unstable Features: target-applies-to-host and host-config*. The Cargo Book, nightly documentation. https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#host-config + +Cargo Team. (2026). *TargetConfig*. Cargo 1.100.0-nightly rustdoc. https://doc.rust-lang.org/nightly/nightly-rustc/cargo/context/target/struct.TargetConfig.html + +Cargo Team. (2026). *Configuration*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/config.html From dadaf82a7c7d2e40b109981f85dab77ca494eac9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:14:00 +0900 Subject: [PATCH 595/632] docs(changelog): record Cargo host-config authority --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625426f2e..8973721a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Failed closed when custom Cargo target roots use lexical Rust `mod` tokens that can resolve additional module source outside Cargo's default `src/**/*.rs` sibling closure; comment/string/character/raw-string text and identifier-adjacent lookalikes no longer create false authority. - Failed closed when Browser Session production Rust attributes select module source through lexical `path = ...`, including raw-identifier `r#path`, while comment and ordinary/raw string/character-literal text inside attributes remains data rather than source authority. - Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. +- Failed closed when repository-owned Cargo nightly `[host]` / `[host.]` configuration selects host linker/runner execution, authority-extending rustc/rustdoc flags, or host-tuple `links` build-script overrides, while unrelated optimization/documentation flags remain permitted. - Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding loads LLD pass-plugin libraries through separated/joined `--load-pass-plugin`, keeping executable LTO pass code inside reviewed toolchain/plugin provenance. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects Distributed ThinLTO distributor/remote-compiler subprocess authority or forwards their argv through `--thinlto-distributor-arg`, `--thinlto-remote-compiler-prepend-arg`, and `--thinlto-remote-compiler-arg`, preventing repository-owned linker flags from opening unreviewed subprocess/toolchain authority. @@ -58,7 +59,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Added a bounded Rust presentation-identity kernel for explicit browser-visible profiles and credential-free replay digests, including control-safe mobile UA-CH model values; applying those profiles to Chromium and proving page-observed effects remain separate adapter and browser-E2E work. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. -- Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. +- Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. - Added `originweave_core::release_acceptance`, a deterministic fail-closed benchmark release-decision contract that requires one authoritative result for every mandatory suite, bounds explicit buyer-visible limitations, rejects duplicate limitation claim identities, and rejects non-canonical surrounding whitespace rather than normalizing it into an alternate claim spelling. - Refreshed the product and technical gap baseline with the 2026-08-24 live inventory: 158 open pull requests (44 ready, 114 draft), refreshed exact base/head evidence for the #208–#222 release, enterprise-approval, BAP, and WARC/PROV chains, the governance issue additions #212 and #215, and a required-check provider-failure record for the fail-closed Strix re-dispatches on #208/#218/#220. @@ -153,4 +154,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 \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From 6574faa0d7012dec8fad0f0a563599af90e89634 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:23:10 +0900 Subject: [PATCH 596/632] test(browser-session): expose generic Cargo host links override --- ..._browser_session_cargo_host_config_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_host_config_authority_contract.py b/tests/test_browser_session_cargo_host_config_authority_contract.py index 4498feba3..c5e85cf09 100644 --- a/tests/test_browser_session_cargo_host_config_authority_contract.py +++ b/tests/test_browser_session_cargo_host_config_authority_contract.py @@ -72,6 +72,12 @@ def test_repository_host_links_build_script_override_fails_closed(self) -> None: 'rustc-link-search = ["tools/review-bypass-native"]\n' ) + def test_repository_generic_host_links_build_script_override_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host.review_bypass]\n' + 'rustc-link-search = ["tools/review-bypass-native"]\n' + ) + def test_unrelated_host_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[host]\nrustflags = ["-C", "opt-level=2"]\n' From edfbd7402d7653374ad7ab5556b3952f73d0ad89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:24:55 +0900 Subject: [PATCH 597/632] fix(browser-session): fail closed ambiguous Cargo host link tables --- .../test_browser_session_cargo_compiler_authority_contract.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 881f6a56a..57b003807 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -603,9 +603,7 @@ def _configured_host_execution_authority( for key, setting in value.items(): if key in TARGET_EXECUTION_KEYS or key in {"rustflags", "rustdocflags"} or not isinstance(setting, dict): continue - if depth >= 1: - configured.append(f"{prefix}.links build-script override:{key}") - continue + configured.append(f"{prefix}.links build-script override:{key}") configured.extend( _configured_host_execution_authority(setting, f"{prefix}.{key}", depth + 1) ) From 0069162bfe312ae9a8e0df11bb53dd149aed3fa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 21:34:04 +0900 Subject: [PATCH 598/632] docs(browser-session): trace generic Cargo host links authority --- ...owser-session-cargo-host-config-authority.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-cargo-host-config-authority.md b/docs/traceability/browser-session-cargo-host-config-authority.md index ade0daee8..54a082890 100644 --- a/docs/traceability/browser-session-cargo-host-config-authority.md +++ b/docs/traceability/browser-session-cargo-host-config-authority.md @@ -4,26 +4,31 @@ Repository-owned Cargo configuration must not be able to introduce unreviewed host-side compiler, linker, runner, rustdoc, or build-script-link authority for Browser Session production builds. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo compiler/toolchain/execution and external-input authority; this dossier adds no second Cargo package/source topology scanner. -The contract therefore fails closed on authority-bearing `[host]` and `[host.]` settings: `linker`, `runner`, authority-extending `rustflags` / `rustdocflags`, and host-tuple `links` build-script overrides. Harmless host flags such as `-C opt-level=2` and `--document-private-items` remain permitted. +The contract therefore fails closed on authority-bearing `[host]` and `[host.]` settings: `linker`, `runner`, authority-extending `rustflags` / `rustdocflags`, and nested host build-script `links` overrides. Because Cargo's host configuration reuses `TargetConfig`, a nested table under generic `[host]` cannot be assumed harmless merely because its key is not the current host tuple: that map is also the shape used for `links_overrides`. Harmless host flags such as `-C opt-level=2` and `--document-private-items` remain permitted. ## Problem and buyer/security effect -Cargo's nightly `host-config` feature gives Git-owned configuration a distinct path for artifacts compiled or executed on the build host, including build scripts and other host artifacts. Cargo documents generic `[host]` and host-tuple-specific tables, with host-tuple settings taking precedence, and documents `host.runner` as the wrapper used to execute host build targets such as build scripts. Cargo's current `TargetConfig` also exposes `rustflags`, `rustdocflags`, `linker`, `runner`, and `links_overrides` for `[target]` or `[host]` configurations. +Cargo's nightly `host-config` feature gives Git-owned configuration a distinct path for artifacts compiled or executed on the build host, including build scripts and other host artifacts. Cargo documents generic `[host]` and host-tuple-specific tables, with host-tuple settings taking precedence, and documents `host.runner` as the wrapper used to execute host build targets such as build scripts. Cargo's current `TargetConfig` is the configuration type for `[target]` or `[host]` and exposes `rustflags`, `rustdocflags`, `linker`, `runner`, and `links_overrides`; a links override suppresses the matching package build script and substitutes configured build output. Before this generation the Browser Session compiler-authority owner examined `[build]`, `[target]`, repository environment/config inclusion, unstable toolchain selectors, and profile rustflags/codegen backends, but ignored `[host]`. A reviewed repository could therefore pre-position host linker/runner/compiler-input authority that becomes effective when nightly `-Zhost-config` / `-Ztarget-applies-to-host` semantics are selected. Treating the feature as currently inactive would be mutable invocation-state trust rather than source provenance. +The first links-override repair still classified nested maps only after descending into a host-specific table. That left an ambiguity at generic `[host]`: a table such as `[host.review_bypass]` could be interpreted as a build-script `links` override even though `review_bypass` is not a host tuple. Allowing it because the key did not look like the current architecture would make the guard depend on an unstable parser distinction rather than the authority-bearing `TargetConfig.links_overrides` contract. + ## RED → repair evidence - **Structural RED `960d361a37d942937f9d2d88f9cd745265a77a90`** added a focused supplemental contract that calls the canonical compiler-authority owner and proves that generic host linker, host-tuple runner, and authority-extending host rustflags were not rejected, while unrelated host rustflags remain a control. - **Minimal canonical repair `da6b14de3366c235b1b4c10d340e68477cd41c2a`** added `_configured_host_execution_authority()` to the existing compiler-authority owner. It reused existing target execution keys and rustc/linker/input classifiers rather than rediscovering Cargo package/source topology. - Review of Cargo's current `TargetConfig` surface exposed two valid omissions in the first repair: host `rustdocflags` share the same target configuration structure, and host target configuration can carry `links_overrides` that replace build-script output. **Review-driven RED `66d8a535befaade759eba6f3f464499792d6bfaf`** added hostile rustdoc external-input and host-tuple links-override cases plus an unrelated rustdoc control. -- **Causal follow-up repair `d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec`** reused the existing rustdoc classifiers and treats nested host-tuple build-script override tables as explicit authority. The change is confined to the canonical compiler-authority helper; no Browser Session runtime, WebDriver BiDi policy, canonical source-topology owner, workflow, ruleset, or external CWL owner is modified. +- **Causal follow-up repair `d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec`** reused the existing rustdoc classifiers and treated nested host-tuple build-script override tables as explicit authority. The change stayed in the canonical compiler-authority helper. +- A fresh hostile case then exposed the generic-host ambiguity: **RED `6574faa0d7012dec8fad0f0a563599af90e89634`** added `[host.review_bypass]` with `rustc-link-search`, which the depth-gated implementation did not report. +- **Minimal repair `edfbd7402d7653374ad7ab5556b3952f73d0ad89`** removed that depth dependency. Any nested map under the selected `[host]` configuration that is not a known direct execution key or the typed `rustflags` / `rustdocflags` surface now fails closed as potential `links` build-script override authority, while the focused harmless generic host controls continue to pass. The canonical compiler-authority owner remains the only implementation writer. ## Alternatives considered 1. **Ignore `[host]` until `-Zhost-config` appears in repository configuration.** Rejected. Invocation flags and Cargo's unstable-feature activation are separate mutable execution surfaces; Git-owned latent authority must not become pre-authorized merely because the current invocation does not activate it. 2. **Reject every `[host]` table or every host rustflag.** Rejected. This would conflate deterministic optimization/documentation settings with execution/input authority and would make the guard broader than the owned security invariant. -3. **Create a second Cargo scanner in the focused test.** Rejected. The focused test only constructs hostile/config-control fixtures and delegates the decision to the canonical compiler-authority owner. +3. **Treat every `[host.]` as a host tuple and allow unknown tuple names.** Rejected. `TargetConfig` itself owns `links_overrides`; an unknown nested map is therefore authority-bearing until Cargo's selected host-configuration interpretation proves otherwise. Fail-open tuple guessing would recreate the bypass. +4. **Create a second Cargo scanner in the focused test.** Rejected. The focused test only constructs hostile/config-control fixtures and delegates the decision to the canonical compiler-authority owner. ## Invariants @@ -31,12 +36,12 @@ Before this generation the Browser Session compiler-authority owner examined `[b - `test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler/rustdoc/toolchain/linker execution and external-input authority. - `[host]` linker/runner authority is fail closed. - Authority-extending host `rustflags` and `rustdocflags` are fail closed using the same classifier semantics as `[build]` / `[target]`. -- Host-tuple `links` overrides are fail closed because they can suppress a package build script and inject replacement build output, including native link search/library material. +- Nested host configuration maps are fail closed as potential `links` overrides because they can suppress a package build script and inject replacement build output, including native link search/library material. - Non-authority host flags remain admissible controls; the guard is not a blanket ban on host configuration. ## Remaining evidence and risk -This generation is source-structural evidence until the exact pull-request head receives executable hosted repository/security checks. It does not claim protected-main integration, release readiness, whole-PR review closure, or owned 100% Docstring/rustdoc/Test/Edge Case Coverage. Ambient user/global Cargo configuration, CLI `--config`, environment variables, and toolchain selection are separate invocation/runtime provenance surfaces and are not made trustworthy by this repository-owned config guard. +This generation is source-structural evidence until the exact pull-request head receives executable hosted repository/security checks. It does not claim protected-main integration, release readiness, whole-PR review closure, or owned 100% Docstring/rustdoc/Test/Edge Case Coverage. Ambient user/global Cargo configuration, CLI `--config`, environment variables, and toolchain selection are separate invocation/runtime provenance surfaces and are not made trustworthy by this repository-owned config guard. Those surfaces belong in the CI/release execution-environment contract rather than by extending this Git-source topology scanner. ## Primary references From a108eb42996a67db3f8809e17cc3166caa6b197c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 22:01:05 +0900 Subject: [PATCH 599/632] test(browser-session): expose cfg-target links override false positive --- ...arget_links_override_authority_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_browser_session_cfg_target_links_override_authority_contract.py diff --git a/tests/test_browser_session_cfg_target_links_override_authority_contract.py b/tests/test_browser_session_cfg_target_links_override_authority_contract.py new file mode 100644 index 000000000..c747985c9 --- /dev/null +++ b/tests/test_browser_session_cfg_target_links_override_authority_contract.py @@ -0,0 +1,60 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +AUTHORITY_TEST = ROOT / "tests/test_browser_session_cargo_compiler_authority_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_cargo_compiler_authority_contract", + AUTHORITY_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Cargo compiler-authority contract") +authority = importlib.util.module_from_spec(spec) +spec.loader.exec_module(authority) + + +class BrowserSessionCfgTargetLinksOverrideAuthorityContractTests(unittest.TestCase): + """Keep Cargo cfg-target warnings separate from tuple links override authority.""" + + def _workspace_with_config(self, config_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "src/lib.rs").write_text("pub fn adapter_surface() {}\n", encoding="utf-8") + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + cargo = root / ".cargo" + cargo.mkdir() + (cargo / "config.toml").write_text(config_text, encoding="utf-8") + return root + + def test_cfg_target_unknown_nested_table_is_not_links_override_authority(self) -> None: + root = self._workspace_with_config( + "[target.'cfg(unix)'.review_bypass]\n" + 'rustc-link-search = ["tools/not-a-target-links-override"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + def test_tuple_target_links_override_remains_authority(self) -> None: + root = self._workspace_with_config( + "[target.x86_64-unknown-linux-gnu.review_bypass]\n" + 'rustc-link-search = ["tools/review-bypass-native"]\n' + ) + with self.assertRaisesRegex(AssertionError, "Cargo .*execution override"): + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + + +if __name__ == "__main__": + unittest.main() From 9b6191ae6e699e68d4b25e8298a18ffcc0c611b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 22:03:21 +0900 Subject: [PATCH 600/632] fix(browser-session): distinguish cfg target tables from links overrides --- ...t_browser_session_cargo_compiler_authority_contract.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 57b003807..9060dc362 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -711,9 +711,11 @@ def _assert_no_repository_cargo_compiler_execution_overrides(root: pathlib.Path) if not isinstance(settings, dict): continue configured = sorted(TARGET_EXECUTION_KEYS.intersection(settings)) - linked_build_overrides = sorted( - str(name) for name, value in settings.items() if isinstance(value, dict) - ) + linked_build_overrides = [] + if not str(target_name).startswith("cfg("): + linked_build_overrides = sorted( + str(name) for name, value in settings.items() if isinstance(value, dict) + ) configured.extend( f"links build-script override:{name}" for name in linked_build_overrides ) From acaa4cca18bc12f574ef198d54489f5dc405f474 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 22:03:38 +0900 Subject: [PATCH 601/632] docs(traceability): distinguish cfg target from links override authority --- ...rgo-cfg-target-links-override-authority.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/traceability/browser-session-cargo-cfg-target-links-override-authority.md diff --git a/docs/traceability/browser-session-cargo-cfg-target-links-override-authority.md b/docs/traceability/browser-session-cargo-cfg-target-links-override-authority.md new file mode 100644 index 000000000..2e1585e6e --- /dev/null +++ b/docs/traceability/browser-session-cargo-cfg-target-links-override-authority.md @@ -0,0 +1,42 @@ +# Browser Session Cargo cfg-target links-override authority + +## Problem + +The Browser Session compiler-authority contract treated every nested table below `[target.]` as a Cargo `links` build-script override. That is correct for tuple target tables loaded through Cargo `TargetConfig`, but not for `target.'cfg(...)'` tables. + +Cargo's current target loader uses a distinct `TargetCfgConfig` for `target.'cfg(...)'`. That type admits `runner`, `rustflags`, `rustdocflags`, and `linker`; remaining keys are captured as `other` and Cargo warns that they are unused. By contrast, tuple targets are loaded through `TargetConfig`, whose unknown nested tables are parsed as `links_overrides` and can suppress a package build script while substituting configured build output. + +Treating the two grammars as identical created a security-contract false positive: a nested table under `target.'cfg(...)'` was rejected as executable provenance authority even though current Cargo does not consume it as a links override. + +## Constraint and owner boundary + +`tests/test_browser_session_cargo_compiler_authority_contract.py` remains the canonical owner for repository-selected Cargo compiler, rustdoc, linker, target and build-script-output authority. The trusted-adapter boundary remains the single writer for production Cargo package/source topology. This repair does not create a second Cargo parser and does not broaden any tuple-target authority. + +## RED → repair + +- RED `a108eb42996a67db3f8809e17cc3166caa6b197c` adds a control for `[target.'cfg(unix)'.review_bypass]` and a paired tuple-target hostile case. Before the repair, the cfg-target control is misclassified as `links build-script override`. +- Repair `9b6191ae6e699e68d4b25e8298a18ffcc0c611b5` preserves linker/runner/rustflags/rustdocflags classification for cfg targets, but only derives nested `links` override authority when the target key is not `cfg(...)`. +- The tuple-target hostile case remains fail closed, so the repair removes a false positive without weakening build-script-output provenance. + +## Primary-source trace + +Cargo source revision `8814ead110e36ed8fdcf1fdd4009baf82bd78523`, `src/context/target.rs`: + +- `TargetCfgConfig` defines the cfg-target grammar and records unmatched fields in `other`. +- `load_target_cfgs` warns for each `other` key instead of interpreting it as a build-script override. +- `TargetConfig` carries `links_overrides` for tuple targets and host config. +- `load_config_table` calls `parse_links_overrides` only for the selected tuple/host target table. + +Cargo Book configuration documentation separately documents `target..` as the build-script override form, while `target.` is documented for runner/rustflags/rustdocflags/linker behavior. + +## Decision + +Selected: model Cargo's two target grammars explicitly at the narrow classification point. `target.'cfg(...)'` retains all typed compiler/linker authority checks, but nested unknown tables are not promoted to `links` authority. + +Rejected: fail closed on every nested cfg-target table. It is conservative but semantically incorrect against current Cargo and creates avoidable buyer-facing false positives. + +Rejected: remove nested-table detection globally. That would create a real tuple-target provenance bypass because `target..` can replace build-script output. + +## Risk and follow-up + +This contract is source-semantic evidence, not proof that every future Cargo release preserves the same grammar. Cargo source/documentation revisions must be rechecked before changing the pinned toolchain or admitting a newer Cargo behavior. Hosted exact-head tests and security checks remain required before protected-main integration. From 87e41f859b61ab1df85a84b288f1c4a1d1cc4c9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 23:01:47 +0900 Subject: [PATCH 602/632] docs(bidi): refresh publication-current receipt --- docs/traceability/webdriver-bidi-publication-current.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/traceability/webdriver-bidi-publication-current.md b/docs/traceability/webdriver-bidi-publication-current.md index 857c807b9..67179dd2d 100644 --- a/docs/traceability/webdriver-bidi-publication-current.md +++ b/docs/traceability/webdriver-bidi-publication-current.md @@ -1,7 +1,7 @@ # WebDriver BiDi publication-current receipt Status: active standards traceability -Observed: 2026-09-16 +Observed: 2026-09-19 Runtime-compatible pin: `2026-09-03` Latest published Working Draft: `2026-09-16` Previous published Working Draft: `2026-09-14` @@ -9,7 +9,7 @@ Editor's Draft: `https://w3c.github.io/webdriver-bidi/` ## Problem -The `originweave-bidi` presentation capability map is deliberately version-pinned, but publication provenance and runtime qualification are separate facts. On 2026-09-16 the canonical W3C publication-history page identifies the 16 September 2026 Working Draft as the latest published version, the 14 September 2026 Working Draft as the previous published version, and the Editor's Draft as a separate mutable surface. The adapter remains qualified against the immutable 3 September 2026 Working Draft. +The `originweave-bidi` presentation capability map is deliberately version-pinned, but publication provenance and runtime qualification are separate facts. A fresh 2026-09-19 read of the canonical W3C publication-history page still identifies the 16 September 2026 Working Draft as the latest published version and the 14 September 2026 Working Draft as the previous published version; no newer dated Working Draft had been published by that observation. The Editor's Draft remains a separate mutable surface. The adapter remains qualified against the immutable 3 September 2026 Working Draft. Treating publication freshness and runtime qualification as the same datum creates two bad failure modes: documentation can become false whenever W3C publishes a new draft, or an automation can silently repin the runtime compatibility claim without re-running the browser/protocol qualification that gives the pin meaning. @@ -47,7 +47,8 @@ This receipt does not close OriginWeave #292. The buyer-visible acceptance still ## Traceability -- W3C latest published version observed 2026-09-16: WebDriver BiDi Working Draft, 16 September 2026. +- W3C publication history re-read on 2026-09-19: no Working Draft newer than 16 September 2026 was listed. +- W3C latest published version observed 2026-09-19: WebDriver BiDi Working Draft, 16 September 2026. - Previous published version: WebDriver BiDi Working Draft, 14 September 2026. - Mutable Editor's Draft: https://w3c.github.io/webdriver-bidi/. - Runtime-qualified OriginWeave adapter pin: WebDriver BiDi Working Draft, 3 September 2026. From 1ab945a66e8f6860cf4740d9e2ec117b89b0a9fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 23:03:48 +0900 Subject: [PATCH 603/632] test(bidi): bind publication observation date --- tests/test_webdriver_bidi_docs_currentness_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_webdriver_bidi_docs_currentness_contract.py b/tests/test_webdriver_bidi_docs_currentness_contract.py index 6833894c5..4667564c2 100644 --- a/tests/test_webdriver_bidi_docs_currentness_contract.py +++ b/tests/test_webdriver_bidi_docs_currentness_contract.py @@ -57,7 +57,7 @@ def test_publication_freshness_is_single_sourced_from_runtime_qualification_docs self.assertIn("historical/reference publication", doctoring) self.assertIn("docs/traceability/webdriver-bidi-publication-current.md", doctoring) - self.assertIn("Observed: 2026-09-16", receipt) + self.assertIn("Observed: 2026-09-19", receipt) self.assertIn("Runtime-compatible pin: `2026-09-03`", receipt) self.assertIn("Latest published Working Draft: `2026-09-16`", receipt) self.assertIn("Previous published Working Draft: `2026-09-14`", receipt) From 61d27ea6e4a1b863218a5cb5f05531260749c03b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:06:45 +0900 Subject: [PATCH 604/632] docs(changelog): record Cargo host and cfg-target semantics --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8973721a4..4748f66e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - Failed closed when Browser Session production Rust attributes select module source through lexical `path = ...`, including raw-identifier `r#path`, while comment and ordinary/raw string/character-literal text inside attributes remains data rather than source authority. - Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. - Failed closed when repository-owned Cargo nightly `[host]` / `[host.]` configuration selects host linker/runner execution, authority-extending rustc/rustdoc flags, or host-tuple `links` build-script overrides, while unrelated optimization/documentation flags remain permitted. +- Failed closed when a repository-owned generic nested `[host.]` map can occupy Cargo `TargetConfig.links_overrides` authority: unknown nested host maps are treated as potential build-script-output overrides instead of being guessed harmless from tuple spelling, while typed direct host settings retain their existing classifiers. +- Corrected Cargo target classification so unknown nested tables under `[target.'cfg(...)']` are not misclassified as concrete target-tuple `links` build-script overrides; typed cfg-target linker/runner/rustflags/rustdocflags remain fail closed, while concrete target tuples retain nested `links` override enforcement. - Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. - Failed closed when repository-owned Rust/rustdoc linker forwarding loads LLD pass-plugin libraries through separated/joined `--load-pass-plugin`, keeping executable LTO pass code inside reviewed toolchain/plugin provenance. - Failed closed when repository-owned Rust/rustdoc linker forwarding selects Distributed ThinLTO distributor/remote-compiler subprocess authority or forwards their argv through `--thinlto-distributor-arg`, `--thinlto-remote-compiler-prepend-arg`, and `--thinlto-remote-compiler-arg`, preventing repository-owned linker flags from opening unreviewed subprocess/toolchain authority. From d52950ebcc9943d2b9f2e554e08ce518382888e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:31:49 +0900 Subject: [PATCH 605/632] test(browser-session): expose rustdoc with-examples provenance gap --- ...on_rustdoc_render_input_authority_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index d09f05eb5..f1cd906a4 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -75,6 +75,22 @@ def test_target_rustdocflags_equals_render_file_input_fails_closed(self) -> None "[target.'cfg(unix)']\nrustdocflags = [\"--html-before-content=tools/review-bypass-before.html\"]\n" ) + def test_build_rustdocflags_with_examples_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--with-examples", "tools/review-bypass.calls"]\n' + ) + + def test_target_rustdocflags_equals_with_examples_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + "[target.'cfg(unix)']\nrustdocflags = [\"-Z\", \"unstable-options\", \"--with-examples=tools/review-bypass.calls\"]\n" + ) + + def test_scrape_examples_output_path_remains_output_only(self) -> None: + root = self._workspace_with_config( + '[build]\nrustdocflags = ["-Z", "unstable-options", "--scrape-examples-output-path", "target/reviewed.calls"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_rustdoc_render_flags_remain_allowed(self) -> None: root = self._workspace_with_config( '[build]\nrustdocflags = ["--document-private-items", "--default-theme", "ayu", "--markdown-css", "reviewed.css"]\n' From 363a6399765e0ccd7a022a0526a6c77679b1c5f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:33:30 +0900 Subject: [PATCH 606/632] fix(browser-session): fail closed on rustdoc with-examples input --- tests/test_browser_session_cargo_compiler_authority_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 9060dc362..505b67939 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -63,6 +63,7 @@ "--theme", "--check-theme", "--read-doc-meta-dir", + "--with-examples", } ) From 5cf12bd388058220285913cc734474a30eaa17ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:34:00 +0900 Subject: [PATCH 607/632] docs(traceability): record rustdoc with-examples input authority --- ...session-rustdoc-render-file-input-authority.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md index fb5c1e89f..e58360ead 100644 --- a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md +++ b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md @@ -4,9 +4,9 @@ Status: source-semantic repair evidence; not hosted executable GREEN. ## Problem -OriginWeave treats `tests/test_browser_session_cargo_compiler_authority_contract.py` as the single writer for repository-selected Cargo compiler/rustdoc execution and input authority. The existing contract rejected rustdoc replacement, `@path`, `--extern`, `-L`/`-l`, sysroot, codegen/linker authority, doctest execution programs, and doctest compiler forwarding, but it did not classify rustdoc's rendering file selectors. +OriginWeave treats `tests/test_browser_session_cargo_compiler_authority_contract.py` as the single writer for repository-selected Cargo compiler/rustdoc execution and input authority. The existing contract rejected rustdoc replacement, `@path`, `--extern`, `-L`/`-l`, sysroot, codegen/linker authority, doctest execution programs, and doctest compiler forwarding, but it did not initially classify rustdoc's rendering file selectors. -Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. The unstable `--index-page PATH` option is another file-backed surface: rustdoc converts the argument to a path, requires it to be a file, records it as a loaded path, and uses that Markdown file as the generated index page. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. +Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. The unstable `--index-page PATH` option is another file-backed surface: rustdoc converts the argument to a path, requires it to be a file, records it as a loaded path, and uses that Markdown file as the generated index page. The unstable `--with-examples INPUT.calls` option is also an input authority: rustdoc documents that the calls file produced by the scrape-examples phase is passed into a later documentation invocation through `--with-examples`. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. For a repository that publishes generated documentation, that is a provenance and documentation-integrity gap. It is not treated as Browser Session runtime policy authority, and no claim is made that every such input is executable script content. @@ -15,6 +15,7 @@ For a repository that publishes generated documentation, that is a provenance an - Production Cargo package/source topology remains owned by `tests/test_browser_session_trusted_adapter_boundary.py`. - This contract must not create a second Cargo configuration/topology scanner. - Ordinary rustdoc presentation controls that do not make rustdoc read another file, such as `--document-private-items`, `--default-theme`, and `--markdown-css`, remain outside this fail-closed rule. +- Output-only scrape-example selection such as `--scrape-examples-output-path` is not reclassified as an input merely because it names a path. - Environment `RUSTDOCFLAGS` / `CARGO_ENCODED_RUSTDOCFLAGS`, ancestor or `$CARGO_HOME` configuration, and direct `cargo rustdoc -- ...` remain CI/release environment provenance surfaces. ## Alternatives considered @@ -33,7 +34,9 @@ A fresh primary-source sweep then found the unstable `--index-page PATH` file in A later documentation-metadata finding broadened the owner name, not the render-file semantics. Repair `47ff4370afdda5487224c437f6883d8947500c3f` renamed the shared classifier to `_flags_select_rustdoc_documentation_input()` and its option set to `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS`, with rejection marker `rustdocflags:documentation input`, so rendered-file and cross-crate metadata inputs share one accurate authority boundary. The separate metadata rationale and RED are documented in `browser-session-rustdoc-doc-meta-input-authority.md`. -The classifier now includes these render-file selectors: +A 2026-09-20 primary-source sweep found a second unmodeled unstable documentation input: `--with-examples INPUT.calls`. Rustdoc's own book describes a two-phase workflow in which `--scrape-examples-output-path output.calls` writes a calls file and a later `rustdoc ... --with-examples output.calls` invocation consumes that file. Structural RED `d52950ebcc9943d2b9f2e554e08ce518382888e5` added build-level split-form and target-level equals-form Cargo fixtures and kept `--scrape-examples-output-path` as an explicit output-only control. Minimal repair `363a6399765e0ccd7a022a0526a6c77679b1c5f5` added only `--with-examples` to the existing `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS` owner, so build, target, and host `rustdocflags` reuse the existing fail-closed path without adding another Cargo scanner. + +The classifier now includes these rendered/documentation-input selectors: - `--html-in-header` - `--html-before-content` @@ -44,6 +47,7 @@ The classifier now includes these render-file selectors: - `--extend-css` and its short `-e` form - `--theme` - `--check-theme` +- unstable `--with-examples` The trusted-adapter boundary remains the single writer for Cargo package/source discovery; this change only extends the existing rustdoc input-authority classifier. @@ -51,6 +55,8 @@ The trusted-adapter boundary remains the single writer for Cargo package/source - Rust Project. (2026). *The rustdoc book: Command-line arguments*. https://doc.rust-lang.org/rustdoc/command-line-arguments.html - documents file-backed HTML inclusion, CSS extension, theme/check-theme, and the distinction between `--markdown-css` and files whose contents rustdoc reads. +- Rust Project. (2026). *The rustdoc book: Unstable features*. https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html + - documents `--with-examples INPUT.calls` and states that the generated calls file from the scrape-examples phase is passed to the subsequent documentation invocation; this is the primary authority for classifying `--with-examples` as an input rather than an output selector. - Rust Project. (2026). *rustdoc option definitions (`rustdoc/lib.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/lib.rs.html - identifies the HTML/Markdown file selectors, `--extend-css`, and unstable `--index-page PATH` used by current rustdoc. - Rust Project. (2026). *rustdoc configuration (`rustdoc/config.rs`)*. https://doc.rust-lang.org/beta/nightly-rustc/src/rustdoc/config.rs.html @@ -60,7 +66,7 @@ The trusted-adapter boundary remains the single writer for Cargo package/source ## Security and buyer effect -Repository-reviewed Rust source can no longer silently acquire additional rendered-document content through Git-owned Cargo `rustdocflags` using the modeled rustdoc file selectors, including the unstable custom index page. This narrows the documentation supply-chain boundary and prevents a source review from incorrectly implying that generated documentation is derived only from reviewed repository inputs. +Repository-reviewed Rust source can no longer silently acquire additional rendered-document content through Git-owned Cargo `rustdocflags` using the modeled rustdoc file selectors, including the unstable custom index page and scrape-examples calls file. This narrows the documentation supply-chain boundary and prevents a source review from incorrectly implying that generated documentation is derived only from reviewed repository inputs. This does **not** prove generated documentation publication, GitHub Pages deployment, CSP behavior, browser rendering, accessibility, or release provenance. Those require their own exact-head build/publish/browser evidence. @@ -68,6 +74,7 @@ This does **not** prove generated documentation publication, GitHub Pages deploy - Environment/direct-CLI rustdoc flags and ambient Cargo configuration remain CI/release supply-chain inputs. - `--markdown-css` writes a stylesheet reference into Markdown-rendered HTML rather than loading the referenced file contents during rustdoc execution. It is intentionally not classified as this file-input surface; external-resource policy for published documentation should be owned by the docs/site publication boundary. +- `--scrape-examples-output-path` writes the calls artifact and is intentionally kept as an output-only control; if a future rustdoc revision changes that contract, toolchain qualification must revisit the classification. - Rustdoc's unstable `--read-doc-meta-dir` is now classified by the same canonical documentation-input owner, but its directory/merge semantics and output-only `--write-doc-meta-dir` control are documented separately. - Future rustdoc releases may add file-backed rendering options. Exact toolchain qualification must update this contract when those options become relevant. - An eventual approved custom render asset contract must identify the artifact immutably, prove repository/release provenance and containment, and connect the generated documentation to SBOM/provenance and rollback evidence rather than relying on a pathname allowlist. From dad69ee929e7823140c474e5bc25656f0ef69385 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:35:08 +0900 Subject: [PATCH 608/632] test(browser-session): cover host rustdoc with-examples authority --- ...rowser_session_rustdoc_render_input_authority_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_browser_session_rustdoc_render_input_authority_contract.py b/tests/test_browser_session_rustdoc_render_input_authority_contract.py index f1cd906a4..f582a2cca 100644 --- a/tests/test_browser_session_rustdoc_render_input_authority_contract.py +++ b/tests/test_browser_session_rustdoc_render_input_authority_contract.py @@ -85,6 +85,11 @@ def test_target_rustdocflags_equals_with_examples_input_fails_closed(self) -> No "[target.'cfg(unix)']\nrustdocflags = [\"-Z\", \"unstable-options\", \"--with-examples=tools/review-bypass.calls\"]\n" ) + def test_host_rustdocflags_with_examples_input_fails_closed(self) -> None: + self._assert_render_input_fails_closed( + '[host]\nrustdocflags = ["-Z", "unstable-options", "--with-examples", "tools/review-bypass.calls"]\n' + ) + def test_scrape_examples_output_path_remains_output_only(self) -> None: root = self._workspace_with_config( '[build]\nrustdocflags = ["-Z", "unstable-options", "--scrape-examples-output-path", "target/reviewed.calls"]\n' From 181de857684050ec42c222f0e85143edcba6e441 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:35:29 +0900 Subject: [PATCH 609/632] docs(traceability): bind with-examples host coverage --- .../browser-session-rustdoc-render-file-input-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md index e58360ead..c2dfb7ab2 100644 --- a/docs/traceability/browser-session-rustdoc-render-file-input-authority.md +++ b/docs/traceability/browser-session-rustdoc-render-file-input-authority.md @@ -6,7 +6,7 @@ Status: source-semantic repair evidence; not hosted executable GREEN. OriginWeave treats `tests/test_browser_session_cargo_compiler_authority_contract.py` as the single writer for repository-selected Cargo compiler/rustdoc execution and input authority. The existing contract rejected rustdoc replacement, `@path`, `--extern`, `-L`/`-l`, sysroot, codegen/linker authority, doctest execution programs, and doctest compiler forwarding, but it did not initially classify rustdoc's rendering file selectors. -Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. The unstable `--index-page PATH` option is another file-backed surface: rustdoc converts the argument to a path, requires it to be a file, records it as a loaded path, and uses that Markdown file as the generated index page. The unstable `--with-examples INPUT.calls` option is also an input authority: rustdoc documents that the calls file produced by the scrape-examples phase is passed into a later documentation invocation through `--with-examples`. A Git-owned `build.rustdocflags` or target `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. +Rustdoc documents `--html-in-header`, `--html-before-content`, and `--html-after-content` as reading files and inserting their contents into generated HTML. It also reads file inputs for `--extend-css`/`-e`, `--theme`, and `--check-theme`; current rustdoc source additionally exposes `--markdown-before-content` and `--markdown-after-content` as file-backed rendering inputs. The unstable `--index-page PATH` option is another file-backed surface: rustdoc converts the argument to a path, requires it to be a file, records it as a loaded path, and uses that Markdown file as the generated index page. The unstable `--with-examples INPUT.calls` option is also an input authority: rustdoc documents that the calls file produced by the scrape-examples phase is passed into a later documentation invocation through `--with-examples`. A Git-owned `build.rustdocflags`, target `rustdocflags`, or nightly host `rustdocflags` entry could therefore make generated documentation depend on content outside the reviewed Cargo source/dependency closure even when the Rust source and compiler inputs were unchanged. For a repository that publishes generated documentation, that is a provenance and documentation-integrity gap. It is not treated as Browser Session runtime policy authority, and no claim is made that every such input is executable script content. @@ -34,7 +34,7 @@ A fresh primary-source sweep then found the unstable `--index-page PATH` file in A later documentation-metadata finding broadened the owner name, not the render-file semantics. Repair `47ff4370afdda5487224c437f6883d8947500c3f` renamed the shared classifier to `_flags_select_rustdoc_documentation_input()` and its option set to `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS`, with rejection marker `rustdocflags:documentation input`, so rendered-file and cross-crate metadata inputs share one accurate authority boundary. The separate metadata rationale and RED are documented in `browser-session-rustdoc-doc-meta-input-authority.md`. -A 2026-09-20 primary-source sweep found a second unmodeled unstable documentation input: `--with-examples INPUT.calls`. Rustdoc's own book describes a two-phase workflow in which `--scrape-examples-output-path output.calls` writes a calls file and a later `rustdoc ... --with-examples output.calls` invocation consumes that file. Structural RED `d52950ebcc9943d2b9f2e554e08ce518382888e5` added build-level split-form and target-level equals-form Cargo fixtures and kept `--scrape-examples-output-path` as an explicit output-only control. Minimal repair `363a6399765e0ccd7a022a0526a6c77679b1c5f5` added only `--with-examples` to the existing `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS` owner, so build, target, and host `rustdocflags` reuse the existing fail-closed path without adding another Cargo scanner. +A 2026-09-20 primary-source sweep found a second unmodeled unstable documentation input: `--with-examples INPUT.calls`. Rustdoc's own book describes a two-phase workflow in which `--scrape-examples-output-path output.calls` writes a calls file and a later `rustdoc ... --with-examples output.calls` invocation consumes that file. Structural RED `d52950ebcc9943d2b9f2e554e08ce518382888e5` added build-level split-form and target-level equals-form Cargo fixtures and kept `--scrape-examples-output-path` as an explicit output-only control. Minimal repair `363a6399765e0ccd7a022a0526a6c77679b1c5f5` added only `--with-examples` to the existing `RUSTDOC_DOCUMENTATION_INPUT_OPTIONS` owner. Coverage successor `dad69ee929e7823140c474e5bc25656f0ef69385` adds the nightly `[host] rustdocflags` hostile case so build, target, and host ownership paths are directly regression-bound without adding another Cargo scanner. The classifier now includes these rendered/documentation-input selectors: From 02edba7b368193c67922b7ccc71fc6917dc864f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 01:39:14 +0900 Subject: [PATCH 610/632] docs(changelog): record rustdoc with-examples authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4748f66e4..c46927ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Failed closed when custom Cargo target roots use lexical Rust `mod` tokens that can resolve additional module source outside Cargo's default `src/**/*.rs` sibling closure; comment/string/character/raw-string text and identifier-adjacent lookalikes no longer create false authority. - Failed closed when Browser Session production Rust attributes select module source through lexical `path = ...`, including raw-identifier `r#path`, while comment and ordinary/raw string/character-literal text inside attributes remains data rather than source authority. - Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. +- Failed closed when repository-owned Cargo `rustdocflags` select an external scrape-examples calls file through unstable rustdoc `--with-examples`, covering build split-form, target equals-form, and nightly host configuration while keeping output-only `--scrape-examples-output-path` outside input-authority classification. - Failed closed when repository-owned Cargo nightly `[host]` / `[host.]` configuration selects host linker/runner execution, authority-extending rustc/rustdoc flags, or host-tuple `links` build-script overrides, while unrelated optimization/documentation flags remain permitted. - Failed closed when a repository-owned generic nested `[host.]` map can occupy Cargo `TargetConfig.links_overrides` authority: unknown nested host maps are treated as potential build-script-output overrides instead of being guessed harmless from tuple spelling, while typed direct host settings retain their existing classifiers. - Corrected Cargo target classification so unknown nested tables under `[target.'cfg(...)']` are not misclassified as concrete target-tuple `links` build-script overrides; typed cfg-target linker/runner/rustflags/rustdocflags remain fail closed, while concrete target tuples retain nested `links` override enforcement. From 845d49bc608dbbb39c4e5de965426a549e54b639 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:04:04 +0900 Subject: [PATCH 611/632] test(browser-session): expose compile-time env macro alias bypass --- ...ile_time_environment_authority_contract.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 42241c6e6..33734bcf5 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -123,6 +123,41 @@ def test_namespaced_option_env_macro_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + def test_aliased_env_macro_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use std::env as read_build_env;\n' + 'pub const BUILD_ID: &str = read_build_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_grouped_aliased_option_env_macro_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use core::{option_env as read_optional_build_env};\n' + 'pub const BUILD_ID: Option<&str> = read_optional_build_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_raw_identifier_aliased_env_macro_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use std::env as r#type;\n' + 'pub const BUILD_ID: &str = r#type!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_underscore_import_is_not_callable_compile_time_environment_authority(self) -> None: + root = self._workspace_with_source( + 'use std::env as _;\n' + 'pub fn reviewed_runtime_environment() -> Option { std::env::var("PATH").ok() }\n' + ) + + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + def test_comment_string_and_raw_string_mentions_are_not_compile_time_environment_authority(self) -> None: root = self._workspace_with_source( '// env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")\n' From 4830e4215b0340ac582c8afd9648b026ae4ff5d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:04:35 +0900 Subject: [PATCH 612/632] fix(browser-session): reject aliased compile-time env macros --- ...ile_time_environment_authority_contract.py | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 33734bcf5..1ce283e85 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -55,12 +55,94 @@ def _has_compile_time_environment_macro(text: str) -> bool: return False +def _use_tree_aliases_compile_time_environment_macro(use_tree: str) -> bool: + """Return whether one use tree gives env!/option_env! a callable alias.""" + cursor = 0 + while cursor < len(use_tree): + trivia_end = source_indirection._skip_rust_trivia(use_tree, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(use_tree, cursor) + if raw_end is not None: + cursor = raw_end + continue + if use_tree[cursor] == '"': + cursor = source_indirection._quoted_string_end(use_tree, cursor) + continue + if use_tree[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(use_tree, cursor) + if char_end is not None: + cursor = char_end + continue + + match = COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN.match(use_tree, cursor) + if match is None: + cursor += 1 + continue + + after_macro = source_indirection._skip_rust_trivia(use_tree, match.end()) + as_match = source_indirection.AS_TOKEN.match(use_tree, after_macro) + if as_match is None: + cursor = match.end() + continue + + alias_start = source_indirection._skip_rust_trivia(use_tree, as_match.end()) + if alias_start >= len(use_tree): + return False + if use_tree[alias_start] == "_": + next_offset = alias_start + 1 + if next_offset >= len(use_tree) or not source_indirection._rust_keyword_is_identifier_adjacent( + use_tree[next_offset] + ): + cursor = match.end() + continue + return True + return False + + +def _has_aliased_compile_time_environment_import(text: str) -> bool: + """Detect lexical use aliases that hide compile-time environment macro names.""" + cursor = 0 + while cursor < len(text): + trivia_end = source_indirection._skip_rust_trivia(text, cursor) + if trivia_end != cursor: + cursor = trivia_end + continue + + raw_end = source_indirection._raw_string_end(text, cursor) + if raw_end is not None: + cursor = raw_end + continue + if text[cursor] == '"': + cursor = source_indirection._quoted_string_end(text, cursor) + continue + if text[cursor] == "'": + char_end = source_indirection._simple_char_literal_end(text, cursor) + if char_end is not None: + cursor = char_end + continue + + use_match = source_indirection.USE_TOKEN.match(text, cursor) + if use_match is None: + cursor += 1 + continue + statement_end = source_indirection._rust_use_statement_end(text, use_match.end()) + if statement_end is None: + return False + if _use_tree_aliases_compile_time_environment_macro(text[use_match.end():statement_end]): + return True + cursor = statement_end + 1 + return False + + def _assert_no_unmodeled_rust_compile_time_environment_inputs(root: pathlib.Path) -> None: """Fail closed when reviewed Rust source reads ambient build environment values.""" source_indirection._assert_no_unmodeled_rust_source_indirection(root) for source in source_indirection.boundary._workspace_production_sources(root): text = source.read_text(encoding="utf-8") - if _has_compile_time_environment_macro(text): + if _has_compile_time_environment_macro(text) or _has_aliased_compile_time_environment_import(text): relative = source.relative_to(root).as_posix() raise AssertionError( "Rust compile-time environment input requires an explicit provenance contract: " @@ -158,11 +240,21 @@ def test_underscore_import_is_not_callable_compile_time_environment_authority(se _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + def test_unicode_continuation_after_underscore_is_not_discard_alias(self) -> None: + root = self._workspace_with_source( + 'use std::env as _\u0301;\n' + 'pub const BUILD_ID: &str = _\u0301!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + _assert_no_unmodeled_rust_compile_time_environment_inputs(root) + def test_comment_string_and_raw_string_mentions_are_not_compile_time_environment_authority(self) -> None: root = self._workspace_with_source( '// env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")\n' + '// use std::env as hidden_build_env;\n' 'pub const NOTE: &str = "option_env!(\\\"ORIGINWEAVE_UNREVIEWED_BUILD_ID\\\")";\n' - 'pub const RAW_NOTE: &str = r#"env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")"#;\n' + 'pub const RAW_NOTE: &str = r#"use std::env as hidden_raw_build_env; env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID")"#;\n' 'pub fn env_count() -> usize { 0 }\n' ) From 19c466acf6912de60790a367c1e38abd4c260bf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:05:12 +0900 Subject: [PATCH 613/632] docs(browser-session): currentize custom-target lexical residuals --- .../browser-session-custom-target-mod-lexical-authority.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md index 513011221..5aa621ffe 100644 --- a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md +++ b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md @@ -57,6 +57,6 @@ The repair deliberately does not parse module grammar. `mod helper;`, `mod r#typ This remains a temporary lexical security boundary. It is not compiler-derived source-input provenance and can intentionally reject legitimate inline modules in custom target roots. Before OriginWeave needs such custom-target module trees, replace the heuristic with compiler-derived or equivalently exact source-input evidence that identifies the actual bytes compiled for supported target configurations without widening Cargo ownership or relying on source-text approximations. -The root-cause audit also shows that other source-indirection token recognizers still use Python-regex `\w` boundaries (`include`, `use`, `as`, and `path`). Their concrete security/false-positive behavior must be verified with Rust `XID_Continue` hostile/control fixtures before claiming the source-indirection lexer is fully Unicode-current. Do not widen those owners by assumption; preserve a structural RED before any shared boundary change. +The root-cause audit now has two classes of token ownership. `include` and `path` use the shared `_rust_identifier_token_end()` boundary, while `USE_TOKEN` and `AS_TOKEN` remain the regex-boundary owners that still require dedicated Rust `XID_Continue` hostile/control verification. Do not widen those remaining owners by assumption; preserve a structural RED before changing their shared boundary. The existing `docs/traceability/browser-session-rust-source-indirection.md` remains the broader source-indirection record. This document narrows the current custom-target lexical correction and shared Rust-whitespace root repair and should be folded into that canonical record when the current stacked Browser Session lineage is reconciled. From 32830c5fa5261593597303cb8c4e815927015aa1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:06:59 +0900 Subject: [PATCH 614/632] docs(browser-session): trace compile-time env macro aliases --- ...rust-compile-time-environment-authority.md | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md index 115621c0c..2e6bd4eaa 100644 --- a/docs/traceability/browser-session-rust-compile-time-environment-authority.md +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -10,18 +10,20 @@ Rust 1.98.1 documents `env!` as reading an environment variable at compile time Cargo can also set compilation environment values through build-script `cargo::rustc-env=VAR=VALUE`. The Cargo Book explicitly describes retrieving such values with `env!` in the compiled crate. Repository `[env]` configuration is already governed by `tests/test_browser_session_cargo_environment_authority_contract.py`, but that contract does not make every ambient runner variable or build-script-produced value part of reviewed artifact provenance. -Before this generation, the Rust source-indirection owner governed `include!`, module/path indirection, and the shared Rust lexical helpers; the embedded-file supplement governed `include_bytes!` and `include_str!`. Neither classified direct source-level `env!` or `option_env!`. A reviewed production `.rs` file could therefore make artifact content depend on a build environment value whose producer, value, and lifecycle were outside the source closure. +Before this generation, the Rust source-indirection owner governed `include!`, module/path indirection, and the shared Rust lexical helpers; the embedded-file supplement governed `include_bytes!` and `include_str!`. Neither classified direct source-level `env!` or `option_env!`. The initial compile-time-environment repair then closed direct and namespaced spellings but still allowed the same built-in macros to be imported under a callable alias such as `use std::env as read_build_env; read_build_env!(...)`. Rust resolves that alias as the macro, so the artifact can still depend on ambient build state while the invocation no longer contains the literal token `env` or `option_env`. ## Decision Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. -Add `tests/test_browser_session_rust_compile_time_environment_authority_contract.py` as a focused supplemental contract that: +`tests/test_browser_session_rust_compile_time_environment_authority_contract.py` remains a focused supplemental contract that: - first consumes the existing source-indirection assertion; - consumes the canonical production-source closure instead of rediscovering Cargo topology; -- reuses the shared trivia, raw-string, quoted-string, and character-literal lexer helpers; +- reuses the shared trivia, raw-string, quoted-string, character-literal, `use`/`as`, use-statement, and Rust identifier-boundary helpers; - fails closed on lexical `env!` and `option_env!`, including namespaced spellings; +- fails closed when a Rust `use` tree gives either macro a callable direct, grouped, or raw-identifier alias; +- treats exact `as _` as a discard import, while `_` followed by a Rust identifier-continuation scalar remains a callable identifier rather than a discard alias; - ignores mentions inside comments and string/character/raw-string literals; - conservatively rejects locally shadowed macros with the same names until macro-expansion provenance is modeled. @@ -35,15 +37,19 @@ The RED adds realistic workspaces whose production Rust source calls `env!` and Minimal repair: `a6eec1a700aff4cd5ad807629e6f55e44abde2aa`. -The repair stays inside the new focused contract. It adds one compile-time-environment macro classifier, delegates source discovery and lexical handling to existing owners, adds a current-production postcondition, covers direct/optional/namespaced forms, and preserves comment/string controls. No Cargo topology, runtime environment policy, browser behavior, linker authority, or cross-repository owner is duplicated. +The repair stays inside the focused contract. It adds one compile-time-environment macro classifier, delegates source discovery and lexical handling to existing owners, adds a current-production postcondition, covers direct/optional/namespaced forms, and preserves comment/string controls. No Cargo topology, runtime environment policy, browser behavior, linker authority, or cross-repository owner is duplicated. Focused review of traceability exact `75eb414f69a7be2dcc851aca58d47e156a41133c` found a valid fixture-coverage gap rather than a classifier defect: the supplemental contract depended on shared raw-string and character-literal handling without directly exercising those boundaries, and it covered namespaced `env!` but not namespaced `option_env!`. Review-driven coverage repair `f9934fe67c6cf7bd5c0ab946be6b881503a14c65` changes only the focused contract (`+19/-1`). It adds a raw-string false-positive control, proves that scanning resumes after a character literal and still rejects a following real macro, and rejects `core::option_env!`. -Focused re-review of exact `f12499cba44f95733cd4d6b4cafcc089e20d3f65` found no defect in this compile-time-environment provenance slice. The review confirmed that the RED, initial repair, and review-driven test repair are ancestors of that exact head, that the test repair is limited to `tests/test_browser_session_rust_compile_time_environment_authority_contract.py` at `+19/-1`, and that `git diff --check` is clean. This remains static focused review evidence only; it does not establish hosted GREEN or whole-PR acceptance. +Focused re-review of exact `f12499cba44f95733cd4d8bb006548aff8804858e` found no defect in the direct/namespaced compile-time-environment slice. That verdict predates the callable-alias generation and is not treated as current-head review evidence. + +A later CodeRabbit security review found a valid remaining bypass: Rust permits imports such as `use std::env as read_build_env;`, after which `read_build_env!(...)` executes the same compile-time environment macro without exposing the literal macro name at the call site. Structural RED `845d49bc608dbbb39c4e5de965426a549e54b639` adds direct `env!`, grouped `option_env!`, and raw-identifier alias hostile fixtures plus an exact `as _` control. Minimal repair `4830e4215b0340ac582c8afd9648b026ae4ff5d4` adds callable use-tree alias detection inside the focused supplemental contract while reusing the canonical source closure and shared Rust lexical helpers. The repair also covers a combining-mark continuation after `_` so Unicode `XID_Continue` input cannot be mistaken for the exact discard alias. + +This alias repair is source-semantic evidence only until the exact successor receives fresh review and hosted execution. No predecessor focused-review verdict is carried forward as proof for the new generation. ## Security and buyer effect -The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata. +The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros, whether invoked by their built-in spelling, a namespace-qualified spelling, or a callable `use` alias. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata through a trivially renamed macro. This is necessary but not sufficient for reproducible release evidence. Runner environment, build-script output, proc-macro or declarative-macro expansion that synthesizes equivalent calls, generated source, direct compiler invocation, and externally injected Cargo environment remain CI/release supply-chain evidence surfaces unless separately attested. @@ -53,8 +59,8 @@ Acceptance for this generation requires all of the following on the reconciled e - the focused contract passes with the existing Rust source-indirection, embedded-file, Cargo-environment, and trusted-adapter contracts; - repository/security workflows run on the exact head and pass without gate weakening; -- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership; -- release evidence, if produced, binds the exact source tree, toolchain, environment-variable names and values that may affect compilation, producer identity for generated values, SBOM/provenance, and an independent reproducible-build result. +- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership and that callable-alias handling matches Rust identifier semantics; +- release evidence, if produced, binds the exact source tree, toolchain, environment-variable names and values that may affect compilation, producer identity for generated values, SBOM/provenance, independent reproducibility, and rollback. If the product later needs a compile-time environment value, do not delete the fail-closed rule. Replace it in the same reviewed change with a versioned contract that binds variable name, purpose, producer, canonical value or digest, secrecy classification, target/toolchain scope, invalidation semantics, SBOM/provenance linkage, independent reproducibility, and rollback. @@ -64,4 +70,6 @@ Rust Project. (2026). *env macro (Rust 1.98.1)*. The Rust Standard Library. http Rust Project. (2026). *option_env macro (Rust 1.98.1)*. The Rust Standard Library. https://doc.rust-lang.org/core/macro.option_env.html +Rust Project. (2026). *Identifiers*. The Rust Reference. https://doc.rust-lang.org/reference/identifiers.html + Rust Project. (2026). *Build scripts*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/build-scripts.html From 181be4baa49952918c412b439768bc5dd9fe6afd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:08:26 +0900 Subject: [PATCH 615/632] fix(browser-session): preserve Unicode include aliases --- ...er_session_rust_source_indirection_contract.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 875266056..5e1521aec 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -247,8 +247,8 @@ def _has_aliased_include_import(text: str) -> bool: continue if use_tree[alias_start] == "_": next_offset = alias_start + 1 - if next_offset >= len(use_tree) or not ( - use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" + if next_offset >= len(use_tree) or not _rust_keyword_is_identifier_adjacent( + use_tree[next_offset] ): use_cursor = include_end continue @@ -556,6 +556,17 @@ def test_braced_include_macro_fails_closed(self) -> None: def test_bracketed_include_macro_fails_closed(self) -> None: self._assert_include_form_fails_closed('include!["../generated_adapter.rs"];\n') + def test_unicode_continuation_aliased_include_import_fails_closed(self) -> None: + self._assert_include_form_fails_closed( + 'use core::include as _\u0301;\n_\u0301!("../generated_adapter.rs");\n' + ) + + def test_exact_underscore_include_import_is_not_callable_alias(self) -> None: + root = self._workspace_with_source( + 'use core::include as _;\npub fn reviewed_surface() {}\n' + ) + _assert_no_unmodeled_rust_source_indirection(root) + def test_bare_module_from_custom_target_fails_closed(self) -> None: root = self._custom_target_workspace( "mod helper;\npub fn lifecycle_adapter_surface() {}\n", From 90fa45fe5c2c64d3137919d09fa73a9ca951e7ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:08:53 +0900 Subject: [PATCH 616/632] fix(browser-session): preserve Unicode embedded-file aliases --- ..._embedded_file_input_authority_contract.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py index b0e202f65..3b087dcb4 100644 --- a/tests/test_browser_session_rust_embedded_file_input_authority_contract.py +++ b/tests/test_browser_session_rust_embedded_file_input_authority_contract.py @@ -77,8 +77,8 @@ def _use_tree_aliases_embedded_file_macro(use_tree: str) -> bool: return False if use_tree[alias_start] == "_": next_offset = alias_start + 1 - if next_offset >= len(use_tree) or not ( - use_tree[next_offset].isalnum() or use_tree[next_offset] == "_" + if next_offset >= len(use_tree) or not source_indirection._rust_keyword_is_identifier_adjacent( + use_tree[next_offset] ): cursor = match.end() continue @@ -215,6 +215,15 @@ def test_underscore_prefixed_alias_still_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_unicode_continuation_include_bytes_alias_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use core::include_bytes as _\u0301;\n' + 'pub static EMBEDDED: &[u8] = _\u0301!("../unreviewed.bin");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_aliased_include_str_file_input_fails_closed(self) -> None: root = self._workspace_with_source( 'use std::include_str as read_text;\n' @@ -224,6 +233,15 @@ def test_aliased_include_str_file_input_fails_closed(self) -> None: with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_unicode_continuation_include_str_alias_fails_closed(self) -> None: + root = self._workspace_with_source( + 'use std::include_str as _\u0301;\n' + 'pub static EMBEDDED: &str = _\u0301!("../unreviewed.txt");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust embedded file input"): + _assert_no_unmodeled_rust_embedded_file_inputs(root) + def test_underscore_import_is_not_callable_alias_authority(self) -> None: root = self._workspace_with_source( 'use core::include_bytes as _;\n' From ad71ca1f70ca3e5220d59cdbd4dcab3b60610012 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:09:54 +0900 Subject: [PATCH 617/632] test(browser-session): expose host-triple false positive --- ..._browser_session_cargo_host_config_authority_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_browser_session_cargo_host_config_authority_contract.py b/tests/test_browser_session_cargo_host_config_authority_contract.py index c5e85cf09..64e37ba91 100644 --- a/tests/test_browser_session_cargo_host_config_authority_contract.py +++ b/tests/test_browser_session_cargo_host_config_authority_contract.py @@ -84,6 +84,12 @@ def test_unrelated_host_rustflags_remain_allowed(self) -> None: ) authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_host_triple_rustflags_remain_allowed(self) -> None: + root = self._workspace_with_config( + '[host.x86_64-unknown-linux-gnu]\nrustflags = ["-C", "opt-level=2"]\n' + ) + authority._assert_no_repository_cargo_compiler_execution_overrides(root) + def test_unrelated_host_rustdocflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[host]\nrustdocflags = ["--document-private-items"]\n' From 1e4c16d9ad1487f3a101dce699d52913277ad85d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:22:58 +0900 Subject: [PATCH 618/632] fix(browser-session): distinguish host target settings from links overrides --- ...wser_session_cargo_compiler_authority_contract.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index 505b67939..cece28967 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -572,6 +572,7 @@ def _configured_host_execution_authority( if not isinstance(value, dict): return [] configured: list[str] = [] + host_setting_keys = TARGET_EXECUTION_KEYS | {"rustflags", "rustdocflags"} for key in sorted(TARGET_EXECUTION_KEYS.intersection(value)): configured.append(f"{prefix}.{key}") @@ -601,10 +602,15 @@ def _configured_host_execution_authority( if _flags_select_rustdoc_doctest_compiler_authority(rustdocflags): configured.append(f"{prefix}.rustdocflags:doctest compiler authority") + if depth > 0 and any( + key not in host_setting_keys and not isinstance(setting, dict) + for key, setting in value.items() + ): + configured.append(f"{prefix}:links build-script override") + for key, setting in value.items(): - if key in TARGET_EXECUTION_KEYS or key in {"rustflags", "rustdocflags"} or not isinstance(setting, dict): + if key in host_setting_keys or not isinstance(setting, dict): continue - configured.append(f"{prefix}.links build-script override:{key}") configured.extend( _configured_host_execution_authority(setting, f"{prefix}.{key}", depth + 1) ) @@ -960,4 +966,4 @@ def test_unrelated_build_configuration_remains_allowed(self) -> None: if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From f85408e777480810656d8e161934df8611f4035a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:33:26 +0900 Subject: [PATCH 619/632] test(browser-session): expose empty host links override --- ...rowser_session_cargo_host_config_authority_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_browser_session_cargo_host_config_authority_contract.py b/tests/test_browser_session_cargo_host_config_authority_contract.py index 64e37ba91..b05d7f317 100644 --- a/tests/test_browser_session_cargo_host_config_authority_contract.py +++ b/tests/test_browser_session_cargo_host_config_authority_contract.py @@ -78,6 +78,14 @@ def test_repository_generic_host_links_build_script_override_fails_closed(self) 'rustc-link-search = ["tools/review-bypass-native"]\n' ) + def test_repository_empty_host_links_build_script_override_fails_closed(self) -> None: + self._assert_host_authority_fails_closed('[host.review_bypass]\n') + + def test_repository_empty_host_triple_links_build_script_override_fails_closed(self) -> None: + self._assert_host_authority_fails_closed( + '[host.x86_64-unknown-linux-gnu.review_bypass]\n' + ) + def test_unrelated_host_rustflags_remain_allowed(self) -> None: root = self._workspace_with_config( '[host]\nrustflags = ["-C", "opt-level=2"]\n' From 7a72d830dd8e8af8859d676f84e711455e490702 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:36:28 +0900 Subject: [PATCH 620/632] fix(browser-session): reject empty host links overrides --- tests/test_browser_session_cargo_compiler_authority_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_browser_session_cargo_compiler_authority_contract.py b/tests/test_browser_session_cargo_compiler_authority_contract.py index cece28967..15e1c203e 100644 --- a/tests/test_browser_session_cargo_compiler_authority_contract.py +++ b/tests/test_browser_session_cargo_compiler_authority_contract.py @@ -573,6 +573,8 @@ def _configured_host_execution_authority( return [] configured: list[str] = [] host_setting_keys = TARGET_EXECUTION_KEYS | {"rustflags", "rustdocflags"} + if depth > 0 and not value: + configured.append(f"{prefix}:links build-script override") for key in sorted(TARGET_EXECUTION_KEYS.intersection(value)): configured.append(f"{prefix}.{key}") From fa33f4ee891458761e9eec1393a456e7d435a827 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 02:37:12 +0900 Subject: [PATCH 621/632] docs(browser-session): trace empty host links authority --- ...ser-session-cargo-host-config-authority.md | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/traceability/browser-session-cargo-host-config-authority.md b/docs/traceability/browser-session-cargo-host-config-authority.md index 54a082890..f9826eb49 100644 --- a/docs/traceability/browser-session-cargo-host-config-authority.md +++ b/docs/traceability/browser-session-cargo-host-config-authority.md @@ -4,31 +4,33 @@ Repository-owned Cargo configuration must not be able to introduce unreviewed host-side compiler, linker, runner, rustdoc, or build-script-link authority for Browser Session production builds. `tests/test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected Cargo compiler/toolchain/execution and external-input authority; this dossier adds no second Cargo package/source topology scanner. -The contract therefore fails closed on authority-bearing `[host]` and `[host.]` settings: `linker`, `runner`, authority-extending `rustflags` / `rustdocflags`, and nested host build-script `links` overrides. Because Cargo's host configuration reuses `TargetConfig`, a nested table under generic `[host]` cannot be assumed harmless merely because its key is not the current host tuple: that map is also the shape used for `links_overrides`. Harmless host flags such as `-C opt-level=2` and `--document-private-items` remain permitted. +The contract fails closed on authority-bearing `[host]` and `[host.]` settings: `linker`, `runner`, authority-extending `rustflags` / `rustdocflags`, and host build-script `links` overrides. Cargo's host configuration reuses `TargetConfig`; `load_host_triple()` selects `[host.]` when present and otherwise generic `[host]`, then `load_config_table()` parses the selected table's non-typed keys through `parse_links_overrides()`. A links override is authority even when its table is empty: Cargo constructs `BuildOutput::default()` and inserts the library name into `links_overrides`, so a matching package's build script is skipped despite there being no replacement output fields. Harmless typed host flags such as `-C opt-level=2` and `--document-private-items` remain permitted. ## Problem and buyer/security effect -Cargo's nightly `host-config` feature gives Git-owned configuration a distinct path for artifacts compiled or executed on the build host, including build scripts and other host artifacts. Cargo documents generic `[host]` and host-tuple-specific tables, with host-tuple settings taking precedence, and documents `host.runner` as the wrapper used to execute host build targets such as build scripts. Cargo's current `TargetConfig` is the configuration type for `[target]` or `[host]` and exposes `rustflags`, `rustdocflags`, `linker`, `runner`, and `links_overrides`; a links override suppresses the matching package build script and substitutes configured build output. +Cargo's nightly `host-config` feature gives Git-owned configuration a distinct path for artifacts compiled or executed on the build host, including build scripts and other host artifacts. Cargo documents generic `[host]` and host-tuple-specific tables, with host-tuple settings taking precedence, and documents `host.runner` as the wrapper used to execute host build targets such as build scripts. Cargo's current `TargetConfig` is the configuration type for `[target]` or `[host]` and exposes `rustflags`, `rustdocflags`, `linker`, `runner`, and `links_overrides`; a links override suppresses the matching package build script and substitutes configured `BuildOutput`. -Before this generation the Browser Session compiler-authority owner examined `[build]`, `[target]`, repository environment/config inclusion, unstable toolchain selectors, and profile rustflags/codegen backends, but ignored `[host]`. A reviewed repository could therefore pre-position host linker/runner/compiler-input authority that becomes effective when nightly `-Zhost-config` / `-Ztarget-applies-to-host` semantics are selected. Treating the feature as currently inactive would be mutable invocation-state trust rather than source provenance. +The original Browser Session compiler-authority owner examined `[build]`, `[target]`, repository environment/config inclusion, unstable toolchain selectors, and profile rustflags/codegen backends, but ignored `[host]`. Later repairs added host authority and generic nested-map coverage. A subsequent review found that content-shape classification could misclassify a legitimate `[host.]` table containing only typed host settings as a links override, so the current classifier preserves typed `linker` / `runner` / `rustflags` / `rustdocflags` semantics and recurses into unknown nested maps rather than treating every first-level host table as an override. -The first links-override repair still classified nested maps only after descending into a host-specific table. That left an ambiguity at generic `[host]`: a table such as `[host.review_bypass]` could be interpreted as a build-script `links` override even though `review_bypass` is not a host tuple. Allowing it because the key did not look like the current architecture would make the guard depend on an unstable parser distinction rather than the authority-bearing `TargetConfig.links_overrides` contract. +That repair exposed a narrower fail-open case. An empty nested table has no scalar payload for the recursive classifier to recognize, but Cargo still inserts an empty `BuildOutput` for that `links` key. Both `[host.review_bypass]` under the generic selected host table and `[host..review_bypass]` under a selected host-triple table can therefore suppress a matching build script while the repository guard reports no authority. Empty replacement output is not absence of authority; suppressing the build script is itself a build decision. ## RED → repair evidence -- **Structural RED `960d361a37d942937f9d2d88f9cd745265a77a90`** added a focused supplemental contract that calls the canonical compiler-authority owner and proves that generic host linker, host-tuple runner, and authority-extending host rustflags were not rejected, while unrelated host rustflags remain a control. -- **Minimal canonical repair `da6b14de3366c235b1b4c10d340e68477cd41c2a`** added `_configured_host_execution_authority()` to the existing compiler-authority owner. It reused existing target execution keys and rustc/linker/input classifiers rather than rediscovering Cargo package/source topology. -- Review of Cargo's current `TargetConfig` surface exposed two valid omissions in the first repair: host `rustdocflags` share the same target configuration structure, and host target configuration can carry `links_overrides` that replace build-script output. **Review-driven RED `66d8a535befaade759eba6f3f464499792d6bfaf`** added hostile rustdoc external-input and host-tuple links-override cases plus an unrelated rustdoc control. -- **Causal follow-up repair `d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec`** reused the existing rustdoc classifiers and treated nested host-tuple build-script override tables as explicit authority. The change stayed in the canonical compiler-authority helper. -- A fresh hostile case then exposed the generic-host ambiguity: **RED `6574faa0d7012dec8fad0f0a563599af90e89634`** added `[host.review_bypass]` with `rustc-link-search`, which the depth-gated implementation did not report. -- **Minimal repair `edfbd7402d7653374ad7ab5556b3952f73d0ad89`** removed that depth dependency. Any nested map under the selected `[host]` configuration that is not a known direct execution key or the typed `rustflags` / `rustdocflags` surface now fails closed as potential `links` build-script override authority, while the focused harmless generic host controls continue to pass. The canonical compiler-authority owner remains the only implementation writer. +- **Structural RED `960d361a37d942937f9d2d88f9cd745265a77a90`** added a focused supplemental contract that calls the canonical compiler-authority owner and proved that generic host linker, host-tuple runner, and authority-extending host rustflags were not rejected, while unrelated host rustflags remained a control. +- **Minimal canonical repair `da6b14de3366c235b1b4c10d340e68477cd41c2a`** added `_configured_host_execution_authority()` to the existing compiler-authority owner, reusing existing target execution keys and rustc/linker/input classifiers. +- Review of Cargo's `TargetConfig` surface exposed host `rustdocflags` and host `links_overrides`. **Review-driven RED `66d8a535befaade759eba6f3f464499792d6bfaf`** added hostile rustdoc external-input and host-tuple links-override cases; **repair `d2830ddc28e8d6e70af54ae4dec09fd8d40dd3ec`** reused the existing classifiers and treated host build-script override tables as authority. +- **RED `6574faa0d7012dec8fad0f0a563599af90e89634`** exposed a generic `[host.review_bypass]` build-output table. **Repair `edfbd7402d7653374ad7ab5556b3952f73d0ad89`** closed that generic-host path. +- A later focused review found the broad generic-host repair could reject a legitimate host-triple table that contained only typed host settings. **RED `ad71ca1f70ca3e5220d59cdbd4dcab3b60610012`** fixed that false-positive expectation, and **repair `1e4c16d9ad1487f3a101dce699d52913277ad85d`** made the classifier preserve typed host settings while recursively classifying unknown nested build-output payloads. +- Fresh Cargo-source review then found that `parse_links_overrides()` starts each non-typed table with `BuildOutput::default()` and inserts it into `links_overrides` even when the table is empty. **Structural RED `f85408e777480810656d8e161934df8611f4035a`** added empty generic-host and host-triple nested `links` fixtures. The pre-repair classifier accepted both because recursion reached an empty mapping with no scalar payload. +- **Minimal canonical repair `7a72d830dd8e8af8859d676f84e711455e490702`** adds exactly one condition in `_configured_host_execution_authority()`: a non-root empty host mapping is classified as `links build-script override`. Existing typed host-triple controls remain unchanged, non-empty generic/triple hostile cases retain their prior path, and no second Cargo topology/config scanner is introduced. ## Alternatives considered 1. **Ignore `[host]` until `-Zhost-config` appears in repository configuration.** Rejected. Invocation flags and Cargo's unstable-feature activation are separate mutable execution surfaces; Git-owned latent authority must not become pre-authorized merely because the current invocation does not activate it. -2. **Reject every `[host]` table or every host rustflag.** Rejected. This would conflate deterministic optimization/documentation settings with execution/input authority and would make the guard broader than the owned security invariant. -3. **Treat every `[host.]` as a host tuple and allow unknown tuple names.** Rejected. `TargetConfig` itself owns `links_overrides`; an unknown nested map is therefore authority-bearing until Cargo's selected host-configuration interpretation proves otherwise. Fail-open tuple guessing would recreate the bypass. -4. **Create a second Cargo scanner in the focused test.** Rejected. The focused test only constructs hostile/config-control fixtures and delegates the decision to the canonical compiler-authority owner. +2. **Reject every `[host]` table or every host rustflag.** Rejected. This conflates deterministic optimization/documentation settings with execution/input authority and would make the guard broader than the owned invariant. +3. **Guess whether every first-level `[host.]` is a host tuple from spelling.** Rejected. Cargo chooses the actual host-triple prefix at runtime and otherwise parses generic `[host]`; tuple-string heuristics would create a second, drifting parser and previously caused false-positive tension. +4. **Treat an empty `links` table as harmless because it contains no replacement fields.** Rejected. Cargo inserts `BuildOutput::default()` for the library name and skips the package build script. Suppression is itself authority. +5. **Create a second Cargo scanner in the focused test.** Rejected. The focused test only constructs hostile/control fixtures and delegates the decision to the canonical compiler-authority owner. ## Invariants @@ -36,8 +38,8 @@ The first links-override repair still classified nested maps only after descendi - `test_browser_session_cargo_compiler_authority_contract.py` remains the single writer for repository-selected compiler/rustdoc/toolchain/linker execution and external-input authority. - `[host]` linker/runner authority is fail closed. - Authority-extending host `rustflags` and `rustdocflags` are fail closed using the same classifier semantics as `[build]` / `[target]`. -- Nested host configuration maps are fail closed as potential `links` overrides because they can suppress a package build script and inject replacement build output, including native link search/library material. -- Non-authority host flags remain admissible controls; the guard is not a blanket ban on host configuration. +- Non-empty and empty host `links` override tables are fail closed because either form can suppress a matching build script; replacement-output fields are not required for that authority to exist. +- Typed non-authority host flags remain admissible controls; the guard is not a blanket ban on host configuration. ## Remaining evidence and risk @@ -49,4 +51,6 @@ Cargo Team. (2026). *Unstable Features: target-applies-to-host and host-config*. Cargo Team. (2026). *TargetConfig*. Cargo 1.100.0-nightly rustdoc. https://doc.rust-lang.org/nightly/nightly-rustc/cargo/context/target/struct.TargetConfig.html -Cargo Team. (2026). *Configuration*. The Cargo Book. https://doc.rust-lang.org/cargo/reference/config.html +Cargo Team. (2026). *target.rs*. Cargo source for `load_host_triple`, `load_config_table`, and `parse_links_overrides`. https://doc.rust-lang.org/nightly/nightly-rustc/src/cargo/util/context/target.rs.html + +Cargo Team. (2026). *Configuration*. The Cargo Book. https://doc.rust-lang.org/nightly/cargo/reference/config.html From f4dd6e59c0a087a60053041d344c2db76df8bc3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 04:01:42 +0900 Subject: [PATCH 622/632] test(browser-session): expose env macro Unicode boundary false positive --- ...nvironment_identifier_boundary_contract.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py diff --git a/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py b/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py new file mode 100644 index 000000000..6eacd91ff --- /dev/null +++ b/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py @@ -0,0 +1,44 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMPILE_TIME_ENVIRONMENT_TEST = ( + ROOT / "tests/test_browser_session_rust_compile_time_environment_authority_contract.py" +) + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_compile_time_environment_authority_contract", + COMPILE_TIME_ENVIRONMENT_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session compile-time environment contract") +compile_time_environment = importlib.util.module_from_spec(spec) +spec.loader.exec_module(compile_time_environment) + + +class BrowserSessionRustCompileTimeEnvironmentIdentifierBoundaryContractTests(unittest.TestCase): + """Keep env!/option_env! detection aligned with Rust identifier boundaries.""" + + def test_unicode_identifier_continuation_before_env_is_not_builtin_macro(self) -> None: + source = ( + "macro_rules! _\u0301env { () => { \"reviewed\" }; }\n" + "pub const BUILD_ID: &str = _\u0301env!();\n" + ) + + self.assertFalse( + compile_time_environment._has_compile_time_environment_macro(source), + "a Rust XID_Continue character before env must keep env inside the user macro identifier", + ) + + def test_real_env_macro_remains_detected(self) -> None: + self.assertTrue( + compile_time_environment._has_compile_time_environment_macro( + 'pub const BUILD_ID: &str = env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + ) + + +if __name__ == "__main__": + unittest.main() From 2f960cb82837abed1b4591d447cca5a609948def Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 04:02:11 +0900 Subject: [PATCH 623/632] fix(browser-session): use Rust identifier boundaries for env macros --- ...ile_time_environment_authority_contract.py | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 1ce283e85..6f5a7716c 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -1,16 +1,11 @@ import importlib.util import pathlib -import re import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" -COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN = re.compile( - r"(? int | None: + """Return the end of a lexical env/option_env identifier token at offset.""" + for spelling in ("env", "option_env"): + end = source_indirection._rust_identifier_token_end( + text, + offset, + spelling, + allow_raw=True, + ) + if end is not None: + return end + return None + + def _has_compile_time_environment_macro(text: str) -> bool: """Detect compile-time environment reads outside Rust comments and literals.""" cursor = 0 @@ -44,12 +53,12 @@ def _has_compile_time_environment_macro(text: str) -> bool: cursor = char_end continue - match = COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN.match(text, cursor) - if match is not None: - bang = source_indirection._skip_rust_trivia(text, match.end()) + token_end = _compile_time_environment_macro_token_end(text, cursor) + if token_end is not None: + bang = source_indirection._skip_rust_trivia(text, token_end) if bang < len(text) and text[bang] == "!": return True - cursor = match.end() + cursor = token_end continue cursor += 1 return False @@ -77,15 +86,15 @@ def _use_tree_aliases_compile_time_environment_macro(use_tree: str) -> bool: cursor = char_end continue - match = COMPILE_TIME_ENVIRONMENT_MACRO_TOKEN.match(use_tree, cursor) - if match is None: + token_end = _compile_time_environment_macro_token_end(use_tree, cursor) + if token_end is None: cursor += 1 continue - after_macro = source_indirection._skip_rust_trivia(use_tree, match.end()) + after_macro = source_indirection._skip_rust_trivia(use_tree, token_end) as_match = source_indirection.AS_TOKEN.match(use_tree, after_macro) if as_match is None: - cursor = match.end() + cursor = token_end continue alias_start = source_indirection._skip_rust_trivia(use_tree, as_match.end()) @@ -96,7 +105,7 @@ def _use_tree_aliases_compile_time_environment_macro(use_tree: str) -> bool: if next_offset >= len(use_tree) or not source_indirection._rust_keyword_is_identifier_adjacent( use_tree[next_offset] ): - cursor = match.end() + cursor = token_end continue return True return False From 8c834133aa4f81fce317cb54d5496c3d25e7a622 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 04:02:40 +0900 Subject: [PATCH 624/632] docs(browser-session): trace env macro Rust identifier boundary repair --- ...ssion-rust-compile-time-environment-authority.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md index 2e6bd4eaa..0f749a9fc 100644 --- a/docs/traceability/browser-session-rust-compile-time-environment-authority.md +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -12,6 +12,8 @@ Cargo can also set compilation environment values through build-script `cargo::r Before this generation, the Rust source-indirection owner governed `include!`, module/path indirection, and the shared Rust lexical helpers; the embedded-file supplement governed `include_bytes!` and `include_str!`. Neither classified direct source-level `env!` or `option_env!`. The initial compile-time-environment repair then closed direct and namespaced spellings but still allowed the same built-in macros to be imported under a callable alias such as `use std::env as read_build_env; read_build_env!(...)`. Rust resolves that alias as the macro, so the artifact can still depend on ambient build state while the invocation no longer contains the literal token `env` or `option_env`. +The callable-alias generation exposed a second, opposite risk in the lexical classifier: its direct macro token used Python `\w` boundaries. Rust identifiers instead follow Unicode `XID_Continue`. A combining mark such as U+0301 can therefore be part of a larger Rust identifier while Python `\w` reports it as non-word. A valid user macro such as `_\u0301env!()` could consequently be misclassified as the built-in `env!` solely because the substring `env` follows a continuation scalar. That is a false positive in a fail-closed security contract and would make legitimate source unreviewable for the wrong reason. + ## Decision Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. @@ -22,10 +24,11 @@ Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writ - consumes the canonical production-source closure instead of rediscovering Cargo topology; - reuses the shared trivia, raw-string, quoted-string, character-literal, `use`/`as`, use-statement, and Rust identifier-boundary helpers; - fails closed on lexical `env!` and `option_env!`, including namespaced spellings; +- resolves `env` / `option_env` token boundaries through the shared Rust identifier-token helper rather than Python regex `\w` semantics; - fails closed when a Rust `use` tree gives either macro a callable direct, grouped, or raw-identifier alias; - treats exact `as _` as a discard import, while `_` followed by a Rust identifier-continuation scalar remains a callable identifier rather than a discard alias; - ignores mentions inside comments and string/character/raw-string literals; -- conservatively rejects locally shadowed macros with the same names until macro-expansion provenance is modeled. +- conservatively rejects locally shadowed macros with the exact built-in names until macro-expansion provenance is modeled. The policy does not treat runtime `std::env::var` as the same build-input class. Runtime environment access is a separate product/runtime authority concern and must be governed by the runtime boundary that owns it. @@ -45,11 +48,13 @@ Focused re-review of exact `f12499cba44f95733cd4d8bb006548aff8804858e` found no A later CodeRabbit security review found a valid remaining bypass: Rust permits imports such as `use std::env as read_build_env;`, after which `read_build_env!(...)` executes the same compile-time environment macro without exposing the literal macro name at the call site. Structural RED `845d49bc608dbbb39c4e5de965426a549e54b639` adds direct `env!`, grouped `option_env!`, and raw-identifier alias hostile fixtures plus an exact `as _` control. Minimal repair `4830e4215b0340ac582c8afd9648b026ae4ff5d4` adds callable use-tree alias detection inside the focused supplemental contract while reusing the canonical source closure and shared Rust lexical helpers. The repair also covers a combining-mark continuation after `_` so Unicode `XID_Continue` input cannot be mistaken for the exact discard alias. -This alias repair is source-semantic evidence only until the exact successor receives fresh review and hosted execution. No predecessor focused-review verdict is carried forward as proof for the new generation. +Fresh lexical review then found that the direct `env` / `option_env` token itself still used Python `\w` boundaries even though the alias path had moved to the shared Rust identifier-boundary helper. Structural RED `f4dd6e59c0a087a60053041d344c2db76df8bc3a` adds a supplemental control for a user macro whose identifier is `_` + U+0301 COMBINING ACUTE ACCENT + `env`; under the predecessor scanner, the combining mark is not Python `\w`, so the internal `env` substring is incorrectly treated as the built-in macro. Minimal repair `2f960cb82837abed1b4591d447cca5a609948def` removes the Python-regex token boundary from the focused owner and resolves both direct-macro and use-tree token ends through `source_indirection._rust_identifier_token_end(..., allow_raw=True)`. Real `env!` remains fail closed, raw identifiers remain supported, and no second Rust lexer or Cargo topology scanner is introduced. + +This identifier-boundary repair is source-semantic evidence only until the exact successor receives fresh review and hosted execution. No predecessor focused-review verdict is carried forward as proof for the new generation. ## Security and buyer effect -The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros, whether invoked by their built-in spelling, a namespace-qualified spelling, or a callable `use` alias. This narrows release provenance: a build cannot claim source-only reproducibility while an unmodeled environment variable changes compiled bytes or embedded metadata through a trivially renamed macro. +The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros, whether invoked by their built-in spelling, a namespace-qualified spelling, or a callable `use` alias. At the same time, it now avoids rejecting a larger valid Rust identifier merely because it contains `env` after a Unicode identifier-continuation scalar. This keeps the fail-closed contract tied to Rust lexical authority instead of Python's Unicode word-character table. This is necessary but not sufficient for reproducible release evidence. Runner environment, build-script output, proc-macro or declarative-macro expansion that synthesizes equivalent calls, generated source, direct compiler invocation, and externally injected Cargo environment remain CI/release supply-chain evidence surfaces unless separately attested. @@ -59,7 +64,7 @@ Acceptance for this generation requires all of the following on the reconciled e - the focused contract passes with the existing Rust source-indirection, embedded-file, Cargo-environment, and trusted-adapter contracts; - repository/security workflows run on the exact head and pass without gate weakening; -- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership and that callable-alias handling matches Rust identifier semantics; +- current-head review confirms the supplemental contract consumes rather than duplicates canonical topology/lexical ownership and that direct and callable-alias handling match Rust identifier semantics; - release evidence, if produced, binds the exact source tree, toolchain, environment-variable names and values that may affect compilation, producer identity for generated values, SBOM/provenance, independent reproducibility, and rollback. If the product later needs a compile-time environment value, do not delete the fail-closed rule. Replace it in the same reviewed change with a versioned contract that binds variable name, purpose, producer, canonical value or digest, secrecy classification, target/toolchain scope, invalidation semantics, SBOM/provenance linkage, independent reproducibility, and rollback. From 550d8bf2f00d59b08a13d7a3efa80acbb3614daf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 05:06:00 +0900 Subject: [PATCH 625/632] test(browser-session): expose Rust use XID boundary false positive --- ...se_keyword_identifier_boundary_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_browser_session_rust_use_keyword_identifier_boundary_contract.py diff --git a/tests/test_browser_session_rust_use_keyword_identifier_boundary_contract.py b/tests/test_browser_session_rust_use_keyword_identifier_boundary_contract.py new file mode 100644 index 000000000..7b8feb802 --- /dev/null +++ b/tests/test_browser_session_rust_use_keyword_identifier_boundary_contract.py @@ -0,0 +1,60 @@ +import importlib.util +import pathlib +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SOURCE_INDIRECTION_TEST = ROOT / "tests/test_browser_session_rust_source_indirection_contract.py" + +spec = importlib.util.spec_from_file_location( + "browser_session_rust_source_indirection_contract", + SOURCE_INDIRECTION_TEST, +) +if spec is None or spec.loader is None: + raise RuntimeError("unable to load Browser Session Rust source-indirection contract") +source_indirection = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_indirection) + + +class BrowserSessionRustUseKeywordIdentifierBoundaryContractTests(unittest.TestCase): + """Keep Rust `use` keyword recognition aligned with Rust XID identifier boundaries.""" + + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + return root + + def test_xid_continue_before_use_inside_macro_tokens_is_not_a_use_declaration(self) -> None: + root = self._workspace_with_source( + "macro_rules! tokens { ($($tt:tt)*) => {}; }\n" + "tokens!(a\u0301use core::include as hidden_include);\n" + "pub fn reviewed_surface() {}\n" + ) + + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + def test_real_aliased_include_import_remains_a_provenance_stop(self) -> None: + root = self._workspace_with_source( + "use core::include as hidden_include;\n" + "pub fn reviewed_surface() {}\n" + ) + + with self.assertRaisesRegex(AssertionError, "Rust include! source indirection"): + source_indirection._assert_no_unmodeled_rust_source_indirection(root) + + +if __name__ == "__main__": + unittest.main() From 1f323bca9494aa3e16d5f3daaf27a5b21277a9fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 05:07:06 +0900 Subject: [PATCH 626/632] fix(browser-session): use Rust lexical boundaries for use aliases --- ...session_rust_source_indirection_contract.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/test_browser_session_rust_source_indirection_contract.py b/tests/test_browser_session_rust_source_indirection_contract.py index 5e1521aec..6119ba9dc 100644 --- a/tests/test_browser_session_rust_source_indirection_contract.py +++ b/tests/test_browser_session_rust_source_indirection_contract.py @@ -1,6 +1,5 @@ import importlib.util import pathlib -import re import tempfile import unittest @@ -16,9 +15,6 @@ INCLUDE_TOKEN = "include" -USE_TOKEN = re.compile(r"(? bool: cursor = char_end continue - use_match = USE_TOKEN.match(text, cursor) - if use_match is None: + use_end = _rust_identifier_token_end(text, cursor, "use") + if use_end is None: cursor += 1 continue - statement_end = _rust_use_statement_end(text, use_match.end()) + statement_end = _rust_use_statement_end(text, use_end) if statement_end is None: return False - use_tree = text[use_match.end():statement_end] + use_tree = text[use_end:statement_end] use_cursor = 0 while use_cursor < len(use_tree): trivia_end = _skip_rust_trivia(use_tree, use_cursor) @@ -237,11 +233,11 @@ def _has_aliased_include_import(text: str) -> bool: use_cursor += 1 continue include_cursor = _skip_rust_trivia(use_tree, include_end) - as_match = AS_TOKEN.match(use_tree, include_cursor) - if as_match is None: + as_end = _rust_identifier_token_end(use_tree, include_cursor, "as") + if as_end is None: use_cursor = include_end continue - alias_start = _skip_rust_trivia(use_tree, as_match.end()) + alias_start = _skip_rust_trivia(use_tree, as_end) if alias_start >= len(use_tree): use_cursor = include_end continue From a8998b0255b3384c0d25be1de7b095fac814c73d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 05:07:43 +0900 Subject: [PATCH 627/632] docs(browser-session): trace Rust use XID boundary repair --- ...sion-custom-target-mod-lexical-authority.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md index 5aa621ffe..ba5a10847 100644 --- a/docs/traceability/browser-session-custom-target-mod-lexical-authority.md +++ b/docs/traceability/browser-session-custom-target-mod-lexical-authority.md @@ -12,11 +12,14 @@ The first lexical repair exposed a second, independent language-boundary defect. The same audit found that Rust's lexical whitespace is the stable Unicode `Pattern_White_Space` set, not Python `str.isspace()`. In particular U+200E LEFT-TO-RIGHT MARK and U+200F RIGHT-TO-LEFT MARK are legal Rust whitespace. Failing to skip them between `include`, `!`, and the macro delimiter creates a false negative in the existing source-provenance stop. +A later residual audit found the same host-language boundary still present in the aliased-`include!` path: `USE_TOKEN` and `AS_TOKEN` used Python `\w`. A valid Rust identifier can contain U+0301 immediately before the ASCII spelling `use`; inside a macro token tree, that spelling is identifier data, not a `UseDeclaration`. The old scanner could nevertheless start a synthetic use-tree at that substring and then mistake later `core::include as hidden_include` token data for executable source-indirection authority. This was an availability false positive in a fail-closed security boundary, not a reason to weaken the source-provenance stop. + This matters commercially because a fail-closed provenance guard still has to distinguish executable authority from inert source text without missing legal Rust token separation. False positives create avoidable adoption friction; false negatives permit compile-time source bytes to enter outside the reviewed provenance boundary. Primary references: - Rust Reference, identifiers (`XID_Start`/`XID_Continue`, Unicode 17.0): https://doc.rust-lang.org/reference/identifiers.html +- Rust Reference, use declarations (`use UseTree ;`, `as ( IDENTIFIER | _ )`): https://doc.rust-lang.org/reference/items/use-declarations.html - Rust Reference, whitespace (`Pattern_White_Space`): https://doc.rust-lang.org/reference/whitespace.html - Rust Reference, comments: https://doc.rust-lang.org/reference/comments.html - Rust Reference, literal expressions: https://doc.rust-lang.org/reference/expressions/literal-expr.html @@ -29,6 +32,7 @@ Primary references: - The scanner must reuse the existing Rust trivia/raw-string/quoted-string/character-literal discipline already used by `include!`, `use`-alias, and attribute discovery rather than introduce a second lexer. - Python's Unicode database must not silently define Rust keyword identity. The checked Rust Reference currently targets Unicode 17.0, so the boundary cannot assume Python `\w` or the local Python runtime's identifier tables are equivalent. - Rust whitespace handling must use the language's exact stable `Pattern_White_Space` set. Generic host-language whitespace predicates are not lexical authority. +- `use` and `as` keyword recognition must reuse the same `_rust_identifier_token_end()` boundary as `include`, `path`, and `mod`; raw-identifier acceptance remains spelling-specific and is not enabled for strict keywords. - No new source path, module tree, adapter, or dependency is authorized. - Default `src/` module trees remain governed by the canonical production-source closure rather than this custom-target guard. @@ -36,11 +40,13 @@ Primary references: Custom-target module detection advances through the same lexical boundaries already used by the source-indirection contract. Non-doc line/block comments, normal strings, raw strings, and character literals are skipped before the `mod` spelling is considered. A real lexical `mod` token still fails closed exactly as before; only inert comment/literal contents stop being treated as module authority. -The `mod` keyword boundary no longer relies on Python `\w`. ASCII identifier continuation is handled directly. For non-ASCII adjacency the guard is deliberately conservative: any non-ASCII scalar that is not Rust `Pattern_White_Space` prevents classification as the ASCII `mod` keyword. This covers current and future Unicode identifier-continuation additions without pretending the host Python Unicode table is Rust's versioned `XID_Continue` authority. Rust's eleven `Pattern_White_Space` code points are explicit and stable, so non-ASCII legal whitespace such as U+200E continues to separate a real `mod` keyword. +The `mod` keyword boundary no longer relies on Python `\w`. ASCII identifier continuation is handled directly. For non-ASCII adjacency the guard is deliberately conservative: any non-ASCII scalar that is not Rust `Pattern_White_Space` prevents classification as the ASCII keyword. This covers current and future Unicode identifier-continuation additions without pretending the host Python Unicode table is Rust's versioned `XID_Continue` authority. Rust's eleven `Pattern_White_Space` code points are explicit and stable, so non-ASCII legal whitespace such as U+200E continues to separate a real keyword. + +The shared trivia skipper uses that exact Rust whitespace set as well. This closes legal U+200E/U+200F separation around `include!` and removes host-only whitespace from the lexer contract. Comment handling remains nested and unchanged. -The shared trivia skipper now uses that exact Rust whitespace set as well. This closes legal U+200E/U+200F separation around `include!` and removes host-only whitespace from the lexer contract. Comment handling remains nested and unchanged. +Aliased-`include!` discovery now recognizes strict `use` and `as` through `_rust_identifier_token_end()` rather than Python regexes. The valid-use hostile control remains fail closed, while an ASCII `use` substring adjacent to Rust Unicode identifier continuation is treated as identifier data. `include` retains its explicit raw-identifier support; `use` and `as` do not, matching their role as strict grammar keywords rather than imported identifier spellings. -The repair deliberately does not parse module grammar. `mod helper;`, `mod r#type;`, `mod 관찰;`, and `mod /* trivia */ helper;` in a custom target root remain provenance stops. The change only removes raw-text/identifier-boundary false positives and closes Rust-whitespace false negatives. +The repair deliberately does not parse module or use-tree grammar beyond the existing provenance heuristic. `mod helper;`, `mod r#type;`, `mod 관찰;`, `mod /* trivia */ helper;`, and a real `use core::include as hidden_include;` remain provenance stops. The changes only remove raw-text/identifier-boundary false positives and close Rust-whitespace false negatives. ## RED → repair evidence @@ -52,11 +58,11 @@ The repair deliberately does not parse module grammar. `mod helper;`, `mod r#typ - Review-driven RED `a214c87d375e75a9c10edc3c6de99b4847c43327` preserves `mod\u0301` as identifier data. Repair `ec32bd9f481280b522ce7554d392021b40c7fea1` replaces the Python-regex keyword boundary with a version-independent conservative Rust boundary backed by the exact stable `Pattern_White_Space` set. - Edge coverage `df940ba25b2c7731d6fe631290f000ce1d57bcd0` adds a combining-mark-before-`mod` control and proves U+200E Rust whitespace still separates a real `mod` keyword. - Root-cause audit then exposed a real false negative in the shared trivia owner: Python `str.isspace()` does not recognize U+200E/U+200F even though Rust does. Structural RED `ce33ae1aa020b1b9903aa02c5952a90bfd1581e5` adds hostile `include\u200e!` and `include!\u200f(` forms. Repair `e199aac4a64e636b3a7412f3d9347644e96e636b` makes `_skip_rust_trivia()` consume the exact Rust `Pattern_White_Space` set. +- Structural RED `550d8bf2f00d59b08a13d7a3efa80acbb3614daf` adds a valid macro-token control containing `a\u0301use core::include as hidden_include`; on the predecessor scanner the Python `\w` boundary started a false `UseDeclaration` at the embedded `use` spelling. The same contract keeps a real `use core::include as hidden_include;` as a fail-closed positive control. +- Minimal repair `1f323bca9494aa3e16d5f3daaf27a5b21277a9fb` removes the remaining `re`/`USE_TOKEN`/`AS_TOKEN`/stale `PATH_TOKEN` regex ownership and reuses `_rust_identifier_token_end()` for strict `use` and `as`. The repair changes only the source-indirection contract (+7/-11); Cargo topology ownership and production Rust code are unchanged. ## Risk and follow-up This remains a temporary lexical security boundary. It is not compiler-derived source-input provenance and can intentionally reject legitimate inline modules in custom target roots. Before OriginWeave needs such custom-target module trees, replace the heuristic with compiler-derived or equivalently exact source-input evidence that identifies the actual bytes compiled for supported target configurations without widening Cargo ownership or relying on source-text approximations. -The root-cause audit now has two classes of token ownership. `include` and `path` use the shared `_rust_identifier_token_end()` boundary, while `USE_TOKEN` and `AS_TOKEN` remain the regex-boundary owners that still require dedicated Rust `XID_Continue` hostile/control verification. Do not widen those remaining owners by assumption; preserve a structural RED before changing their shared boundary. - -The existing `docs/traceability/browser-session-rust-source-indirection.md` remains the broader source-indirection record. This document narrows the current custom-target lexical correction and shared Rust-whitespace root repair and should be folded into that canonical record when the current stacked Browser Session lineage is reconciled. +The identifier-boundary root-cause audit is now single-owner for the covered ASCII spellings: `include`, `path`, `mod`, `use`, and `as` all consume `_rust_identifier_token_end()` rather than Python regex keyword boundaries. Future source-indirection syntax must receive its own structural hostile/control fixture before widening this lexical owner. The broader `docs/traceability/browser-session-rust-source-indirection.md` remains the canonical source-indirection record and should absorb this focused history when the current stacked Browser Session lineage is reconciled. From 8b98d08ac00aa4f5e4b70fce780769b8a26308cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 06:04:48 +0900 Subject: [PATCH 628/632] test(browser-session): expose compile-time env use boundary regression --- ...nvironment_identifier_boundary_contract.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py b/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py index 6eacd91ff..b22c5589b 100644 --- a/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_identifier_boundary_contract.py @@ -1,5 +1,6 @@ import importlib.util import pathlib +import tempfile import unittest @@ -21,6 +22,23 @@ class BrowserSessionRustCompileTimeEnvironmentIdentifierBoundaryContractTests(unittest.TestCase): """Keep env!/option_env! detection aligned with Rust identifier boundaries.""" + def _workspace_with_source(self, source_text: str) -> pathlib.Path: + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = pathlib.Path(directory.name) + (root / "Cargo.toml").write_text( + '[workspace]\nmembers = ["adapter"]\nresolver = "3"\n', + encoding="utf-8", + ) + adapter = root / "adapter" + (adapter / "src").mkdir(parents=True) + (adapter / "Cargo.toml").write_text( + '[package]\nname = "adapter"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (adapter / "src/lib.rs").write_text(source_text, encoding="utf-8") + return root + def test_unicode_identifier_continuation_before_env_is_not_builtin_macro(self) -> None: source = ( "macro_rules! _\u0301env { () => { \"reviewed\" }; }\n" @@ -39,6 +57,24 @@ def test_real_env_macro_remains_detected(self) -> None: ) ) + def test_unicode_identifier_continuation_before_use_is_macro_name_data(self) -> None: + root = self._workspace_with_source( + "macro_rules! a\u0301use { ($($token:tt)*) => {}; }\n" + "a\u0301use!(std::env as hidden_build_env);\n" + 'pub fn reviewed_runtime_environment() -> Option { std::env::var("PATH").ok() }\n' + ) + + compile_time_environment._assert_no_unmodeled_rust_compile_time_environment_inputs(root) + + def test_real_use_alias_remains_compile_time_environment_authority(self) -> None: + root = self._workspace_with_source( + "use std::env as hidden_build_env;\n" + 'pub const BUILD_ID: &str = hidden_build_env!("ORIGINWEAVE_UNREVIEWED_BUILD_ID");\n' + ) + + with self.assertRaisesRegex(AssertionError, "Rust compile-time environment input"): + compile_time_environment._assert_no_unmodeled_rust_compile_time_environment_inputs(root) + if __name__ == "__main__": unittest.main() From c06a36d2bf38dfee155743ce08fec5a711616f56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 06:05:13 +0900 Subject: [PATCH 629/632] fix(browser-session): share Rust use/as identifier boundaries --- ..._compile_time_environment_authority_contract.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py index 6f5a7716c..594cc7770 100644 --- a/tests/test_browser_session_rust_compile_time_environment_authority_contract.py +++ b/tests/test_browser_session_rust_compile_time_environment_authority_contract.py @@ -92,12 +92,12 @@ def _use_tree_aliases_compile_time_environment_macro(use_tree: str) -> bool: continue after_macro = source_indirection._skip_rust_trivia(use_tree, token_end) - as_match = source_indirection.AS_TOKEN.match(use_tree, after_macro) - if as_match is None: + as_end = source_indirection._rust_identifier_token_end(use_tree, after_macro, "as") + if as_end is None: cursor = token_end continue - alias_start = source_indirection._skip_rust_trivia(use_tree, as_match.end()) + alias_start = source_indirection._skip_rust_trivia(use_tree, as_end) if alias_start >= len(use_tree): return False if use_tree[alias_start] == "_": @@ -133,14 +133,14 @@ def _has_aliased_compile_time_environment_import(text: str) -> bool: cursor = char_end continue - use_match = source_indirection.USE_TOKEN.match(text, cursor) - if use_match is None: + use_end = source_indirection._rust_identifier_token_end(text, cursor, "use") + if use_end is None: cursor += 1 continue - statement_end = source_indirection._rust_use_statement_end(text, use_match.end()) + statement_end = source_indirection._rust_use_statement_end(text, use_end) if statement_end is None: return False - if _use_tree_aliases_compile_time_environment_macro(text[use_match.end():statement_end]): + if _use_tree_aliases_compile_time_environment_macro(text[use_end:statement_end]): return True cursor = statement_end + 1 return False From 7478246232798c08d50ff5f8e2204366fffcb975 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 06:05:52 +0900 Subject: [PATCH 630/632] docs(browser-session): trace compile-time env alias boundary repair --- ...er-session-rust-compile-time-environment-authority.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/traceability/browser-session-rust-compile-time-environment-authority.md b/docs/traceability/browser-session-rust-compile-time-environment-authority.md index 0f749a9fc..30a408e3a 100644 --- a/docs/traceability/browser-session-rust-compile-time-environment-authority.md +++ b/docs/traceability/browser-session-rust-compile-time-environment-authority.md @@ -14,6 +14,8 @@ Before this generation, the Rust source-indirection owner governed `include!`, m The callable-alias generation exposed a second, opposite risk in the lexical classifier: its direct macro token used Python `\w` boundaries. Rust identifiers instead follow Unicode `XID_Continue`. A combining mark such as U+0301 can therefore be part of a larger Rust identifier while Python `\w` reports it as non-word. A valid user macro such as `_\u0301env!()` could consequently be misclassified as the built-in `env!` solely because the substring `env` follows a continuation scalar. That is a false positive in a fail-closed security contract and would make legitimate source unreviewable for the wrong reason. +The subsequent shared source-indirection cleanup removed the remaining Python-regex `USE_TOKEN` and `AS_TOKEN` owners and moved strict `use`/`as` recognition to `_rust_identifier_token_end()`. The compile-time-environment supplement still dereferenced those removed constants. That made its callable-alias path stale against its canonical lexical owner and would raise before it could classify a real alias. It also meant this supplement had not actually inherited the Unicode-correct `use`/`as` boundary it claimed to consume. + ## Decision Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writer for production Cargo package/source topology and `tests/test_browser_session_rust_source_indirection_contract.py` as the shared Rust lexical/source-indirection owner. @@ -25,6 +27,7 @@ Keep `tests/test_browser_session_trusted_adapter_boundary.py` as the single writ - reuses the shared trivia, raw-string, quoted-string, character-literal, `use`/`as`, use-statement, and Rust identifier-boundary helpers; - fails closed on lexical `env!` and `option_env!`, including namespaced spellings; - resolves `env` / `option_env` token boundaries through the shared Rust identifier-token helper rather than Python regex `\w` semantics; +- resolves strict `use` and `as` through that same shared identifier-token helper rather than owning regex tokens or dereferencing removed compatibility constants; - fails closed when a Rust `use` tree gives either macro a callable direct, grouped, or raw-identifier alias; - treats exact `as _` as a discard import, while `_` followed by a Rust identifier-continuation scalar remains a callable identifier rather than a discard alias; - ignores mentions inside comments and string/character/raw-string literals; @@ -48,13 +51,15 @@ Focused re-review of exact `f12499cba44f95733cd4d8bb006548aff8804858e` found no A later CodeRabbit security review found a valid remaining bypass: Rust permits imports such as `use std::env as read_build_env;`, after which `read_build_env!(...)` executes the same compile-time environment macro without exposing the literal macro name at the call site. Structural RED `845d49bc608dbbb39c4e5de965426a549e54b639` adds direct `env!`, grouped `option_env!`, and raw-identifier alias hostile fixtures plus an exact `as _` control. Minimal repair `4830e4215b0340ac582c8afd9648b026ae4ff5d4` adds callable use-tree alias detection inside the focused supplemental contract while reusing the canonical source closure and shared Rust lexical helpers. The repair also covers a combining-mark continuation after `_` so Unicode `XID_Continue` input cannot be mistaken for the exact discard alias. -Fresh lexical review then found that the direct `env` / `option_env` token itself still used Python `\w` boundaries even though the alias path had moved to the shared Rust identifier-boundary helper. Structural RED `f4dd6e59c0a087a60053041d344c2db76df8bc3a` adds a supplemental control for a user macro whose identifier is `_` + U+0301 COMBINING ACUTE ACCENT + `env`; under the predecessor scanner, the combining mark is not Python `\w`, so the internal `env` substring is incorrectly treated as the built-in macro. Minimal repair `2f960cb82837abed1b4591d447cca5a609948def` removes the Python-regex token boundary from the focused owner and resolves both direct-macro and use-tree token ends through `source_indirection._rust_identifier_token_end(..., allow_raw=True)`. Real `env!` remains fail closed, raw identifiers remain supported, and no second Rust lexer or Cargo topology scanner is introduced. +Fresh lexical review then found that the direct `env` / `option_env` token itself still used Python `\w` boundaries even though the alias path had moved toward the shared Rust identifier-boundary owner. Structural RED `f4dd6e59c0a087a60053041d344c2db76df8bc3a` adds a supplemental control for a user macro whose identifier is `_` + U+0301 COMBINING ACUTE ACCENT + `env`; under the predecessor scanner, the combining mark is not Python `\w`, so the internal `env` substring is incorrectly treated as the built-in macro. Minimal repair `2f960cb82837abed1b4591d447cca5a609948def` removes the Python-regex token boundary from the focused owner and resolves direct macro token ends through `source_indirection._rust_identifier_token_end(..., allow_raw=True)`. Real `env!` remains fail closed, raw identifiers remain supported, and no second Rust lexer or Cargo topology scanner is introduced. + +The later source-indirection repair `1f323bca9494aa3e16d5f3daaf27a5b21277a9fb` removed `USE_TOKEN` and `AS_TOKEN` entirely from the shared owner. Structural successor RED `8b98d08ac00aa4f5e4b70fce780769b8a26308cc` extends the focused identifier-boundary contract with a valid `a\u0301use!(std::env as hidden_build_env)` macro-token control and a real `use std::env as hidden_build_env;` hostile alias. On the predecessor compile-time-environment supplement, callable-alias analysis still dereferenced the removed `source_indirection.USE_TOKEN` / `AS_TOKEN` names instead of the shared lexical API. Minimal repair `c06a36d2bf38dfee155743ce08fec5a711616f56` replaces both stale references with `_rust_identifier_token_end(..., "use")` / `_rust_identifier_token_end(..., "as")`, preserving the existing use-statement and discard-alias handling. This is an owner-consistency repair: the supplement now consumes the same Rust identifier boundary as direct macro, include/path/mod, and source-indirection alias detection rather than recreating or pinning a former regex surface. This identifier-boundary repair is source-semantic evidence only until the exact successor receives fresh review and hosted execution. No predecessor focused-review verdict is carried forward as proof for the new generation. ## Security and buyer effect -The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros, whether invoked by their built-in spelling, a namespace-qualified spelling, or a callable `use` alias. At the same time, it now avoids rejecting a larger valid Rust identifier merely because it contains `env` after a Unicode identifier-continuation scalar. This keeps the fail-closed contract tied to Rust lexical authority instead of Python's Unicode word-character table. +The contract prevents Git-reviewed Browser Session Rust source from silently binding artifact content to ambient build values through the two standard compile-time environment macros, whether invoked by their built-in spelling, a namespace-qualified spelling, or a callable `use` alias. At the same time, it now avoids rejecting a larger valid Rust identifier merely because it contains `env` or `use` after a Unicode identifier-continuation scalar. Keeping the supplement on the canonical lexical helper also prevents a shared-owner cleanup from silently disabling the compile-time-environment contract. This keeps the fail-closed contract tied to Rust lexical authority instead of Python's Unicode word-character table or stale compatibility symbols. This is necessary but not sufficient for reproducible release evidence. Runner environment, build-script output, proc-macro or declarative-macro expansion that synthesizes equivalent calls, generated source, direct compiler invocation, and externally injected Cargo environment remain CI/release supply-chain evidence surfaces unless separately attested. From 01aa661b987622c0bd231c586d35cc101010353e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 19:34:09 +0900 Subject: [PATCH 631/632] docs(changelog): record Rust XID and empty host override repairs --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46927ebf..4c3039a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ All notable changes to OriginWeave are documented in this file. The format follo - Failed closed when Browser Session production Rust source selects native libraries through direct `#[link(...)]` or `cfg_attr(..., link(...))` attributes, while the shared Rust attribute lexer treats comment and string/character/raw-string text as lexical data rather than authority. - Failed closed when Browser Session production Rust source embeds compile-time files through direct or namespaced `include_bytes!` / `include_str!` and callable `use ... as ...` aliases, reusing the canonical production-source closure and shared Rust source-indirection lexer instead of rediscovering Cargo topology. +- Hardened the shared Rust `use` / `as` keyword boundary to follow Unicode XID continuation semantics instead of Python word-character heuristics, so combining-mark continuations such as U+0301 cannot be misread as a keyword or discard-alias boundary; the compile-time-environment supplement consumes that shared lexer owner rather than retaining duplicate `use` / `as` token authority. - Failed closed when Browser Session production Rust source injects executable source through lexical `include!` invocations or callable aliases, while comment, ordinary/raw string, character-literal, and commented grouped-use text remain lexical data rather than source authority. - Failed closed when custom Cargo target roots use lexical Rust `mod` tokens that can resolve additional module source outside Cargo's default `src/**/*.rs` sibling closure; comment/string/character/raw-string text and identifier-adjacent lookalikes no longer create false authority. - Failed closed when Browser Session production Rust attributes select module source through lexical `path = ...`, including raw-identifier `r#path`, while comment and ordinary/raw string/character-literal text inside attributes remains data rather than source authority. - Failed closed when Browser Session production Rust source binds artifact content to ambient build values through direct or namespaced `env!` / `option_env!`; raw-string, character-literal resumption, and namespaced `option_env!` regression coverage preserve lexical correctness, while runtime `std::env` access remains a separate runtime authority. - Failed closed when repository-owned Cargo `rustdocflags` select an external scrape-examples calls file through unstable rustdoc `--with-examples`, covering build split-form, target equals-form, and nightly host configuration while keeping output-only `--scrape-examples-output-path` outside input-authority classification. - Failed closed when repository-owned Cargo nightly `[host]` / `[host.]` configuration selects host linker/runner execution, authority-extending rustc/rustdoc flags, or host-tuple `links` build-script overrides, while unrelated optimization/documentation flags remain permitted. +- Recorded empty nested host `links` override tables as build-script authority too: Cargo can materialize `BuildOutput::default()` for an empty override and thereby suppress the matching build script, so an empty table is not equivalent to absence of override authority. - Failed closed when a repository-owned generic nested `[host.]` map can occupy Cargo `TargetConfig.links_overrides` authority: unknown nested host maps are treated as potential build-script-output overrides instead of being guessed harmless from tuple spelling, while typed direct host settings retain their existing classifiers. - Corrected Cargo target classification so unknown nested tables under `[target.'cfg(...)']` are not misclassified as concrete target-tuple `links` build-script overrides; typed cfg-target linker/runner/rustflags/rustdocflags remain fail closed, while concrete target tuples retain nested `links` override enforcement. - Failed closed when repository-owned Rust/rustdoc linker forwarding opens LLD's LLVMgold-compatible `plugin-opt=-...` opaque LLVM option-processing tunnel, including one-/two-dash spellings, so arbitrary LLVM options cannot bypass reviewed deterministic linker authority. @@ -157,4 +159,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 From 70cc9d8ab9cbb79e3f7c8635ba5c4bec67d80b73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 19:35:16 +0900 Subject: [PATCH 632/632] fix(changelog): restore trailing newline after documentation repair --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3039a2a..d1b15dc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,4 +159,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 \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD