From 977d2682dc191ca6b26b9de631a3642680abdbc0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 12:02:44 +0900 Subject: [PATCH 1/9] test(browser): require semantic role-name evidence --- .../test_agent_task_pinned_chrome_contract.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_agent_task_pinned_chrome_contract.py b/tests/test_agent_task_pinned_chrome_contract.py index 63830f3eb..9f8816f1c 100644 --- a/tests/test_agent_task_pinned_chrome_contract.py +++ b/tests/test_agent_task_pinned_chrome_contract.py @@ -46,6 +46,25 @@ def test_agent_task_pass_uses_real_webdriver_input_and_post_condition(self) -> N with self.subTest(expected=expected): self.assertIn(expected, runner) + def test_agent_task_observes_computed_role_and_name_before_action(self) -> None: + """Real-browser evidence must bind the controlled targets to semantic role/name.""" + + namespace = runpy.run_path(str(RUNNER), run_name="agent_task_semantics_contract") + runner = RUNNER.read_text(encoding="utf-8") + self.assertIn("_get_element_semantics", namespace) + for expected in ( + '"/computedrole"', + '"/computedlabel"', + '"textbox"', + '"Task text"', + '"button"', + '"Submit task"', + '"input_semantics_verified"', + '"submit_semantics_verified"', + ): + with self.subTest(expected=expected): + self.assertIn(expected, runner) + def test_agent_task_fixture_runs_under_the_existing_pinned_chrome_job(self) -> None: """No floating browser or second workflow may be introduced for this slice.""" From 5f1f972f3e9888faa44af184fd54a466d20b6ddb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 12:07:16 +0900 Subject: [PATCH 2/9] test(browser): verify computed role-name evidence --- scripts/ci/run_mv3_compatibility.py | 53 ++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index bb077ad94..f52c15d6e 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -7,9 +7,10 @@ content-script, storage, declarative-net-request, tabs, windows, scripting, commands, side-panel, bookmarks, history, real browser-click, and restart-persistence behavior. It also executes the controlled Agent Task fixture -with extensions disabled in a fresh profile, performs real WebDriver input and -click operations, verifies the observable post-condition, and proves profile -cleanup without treating page content as instruction or authority. +with extensions disabled in a fresh profile, verifies browser-computed role/name +for the controlled action targets, performs real WebDriver input and click +operations, verifies the observable post-condition, and proves profile cleanup +without treating page content as instruction or authority. """ from __future__ import annotations @@ -179,6 +180,28 @@ def _element_command_path(session_id: str, element_id: str, suffix: str) -> str: return _webdriver_path(session_id, f"/element/{safe_element}{suffix}") +def _get_element_semantics( + driver_port: int, + session_id: str, + element_id: str, +) -> tuple[str, str]: + """Read one controlled element's browser-computed role and accessible name.""" + + role = _json_request( + driver_port, + "GET", + _element_command_path(session_id, element_id, "/computedrole"), + ).get("value") + label = _json_request( + driver_port, + "GET", + _element_command_path(session_id, element_id, "/computedlabel"), + ).get("value") + if not isinstance(role, str) or not isinstance(label, str): + raise RuntimeError("WebDriver returned malformed element semantics") + return role, label + + def _wait_for_extension_evidence( driver_port: int, session_id: str, @@ -511,6 +534,13 @@ def _run_agent_task_browser_pass( {"url": fixture_url}, ) input_element = _find_element(driver_port, session_id, "#task-text") + input_role, input_name = _get_element_semantics( + driver_port, + session_id, + input_element, + ) + if input_role != "textbox" or input_name != "Task text": + raise RuntimeError("Agent Task input semantic evidence mismatch") _json_request( driver_port, "POST", @@ -528,6 +558,13 @@ def _run_agent_task_browser_pass( session_id, "#agent-task-form button[type=submit]", ) + submit_role, submit_name = _get_element_semantics( + driver_port, + session_id, + submit_element, + ) + if submit_role != "button" or submit_name != "Submit task": + raise RuntimeError("Agent Task submit semantic evidence mismatch") _json_request( driver_port, "POST", @@ -553,6 +590,8 @@ def _run_agent_task_browser_pass( "browser_version": browser_version, "post_condition": True, "input_echo_verified": True, + "input_semantics_verified": True, + "submit_semantics_verified": True, "extensions_disabled": True, "duration_ms": round((time.monotonic() - started) * 1000), } @@ -603,13 +642,17 @@ def _run_agent_task_trial( "browser_version": result["browser_version"], "post_condition": result["post_condition"], "input_echo_verified": result["input_echo_verified"], + "input_semantics_verified": result["input_semantics_verified"], + "submit_semantics_verified": result["submit_semantics_verified"], "extensions_disabled": result["extensions_disabled"], "profile_cleaned": profile_cleaned, "duration_ms": round((time.monotonic() - trial_started) * 1000), } -def _start_fixture_server(directory: pathlib.Path) -> tuple[http.server.ThreadingHTTPServer, threading.Thread]: +def _start_fixture_server( + directory: pathlib.Path, +) -> tuple[http.server.ThreadingHTTPServer, threading.Thread]: """Start one loopback-only static fixture server for a bounded browser lane.""" server = http.server.ThreadingHTTPServer( @@ -726,6 +769,8 @@ def main() -> int: agent_task_surfaces_complete = all( trial.get("post_condition") is True and trial.get("input_echo_verified") is True + and trial.get("input_semantics_verified") is True + and trial.get("submit_semantics_verified") is True and trial.get("extensions_disabled") is True and trial.get("profile_cleaned") is True for trial in agent_task_trials From adc263b542aa17639832ffdab14b25b398c7bfb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:48:54 +0900 Subject: [PATCH 3/9] chore(browser): sync hardened fixture contract --- tests/test_agent_task_fixture_contract.py | 66 +++++++++++++++++++---- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/tests/test_agent_task_fixture_contract.py b/tests/test_agent_task_fixture_contract.py index 2a1465b2e..2565a35c7 100644 --- a/tests/test_agent_task_fixture_contract.py +++ b/tests/test_agent_task_fixture_contract.py @@ -10,6 +10,21 @@ FIXTURE = ROOT / "tests" / "fixtures" / "agent_task_basic" / "index.html" +def _is_credential_input(attributes: dict[str, str | None]) -> bool: + """Return whether parsed input attributes describe a credential surface.""" + + input_type = (attributes.get("type") or "").strip().lower() + if input_type == "password": + return True + + autocomplete = (attributes.get("autocomplete") or "").strip().lower() + autocomplete_tokens = autocomplete.split() + return any( + token == "one-time-code" or "password" in token + for token in autocomplete_tokens + ) + + class _FixtureParser(HTMLParser): """Collect the small semantic surface required by the deterministic fixture.""" @@ -18,6 +33,7 @@ def __init__(self) -> None: self.ids: set[str] = set() self.labels_for: set[str] = set() self.input_names: set[str] = set() + self.input_attributes: list[dict[str, str | None]] = [] self.button_types: set[str] = set() self.hidden_injection_markers = 0 @@ -30,12 +46,15 @@ def handle_starttag( self.ids.add(element_id) if tag == "label" and attributes.get("for"): self.labels_for.add(attributes["for"]) - if tag == "input" and attributes.get("name"): - self.input_names.add(attributes["name"]) + if tag == "input": + self.input_attributes.append(attributes) + if attributes.get("name"): + self.input_names.add(attributes["name"]) if tag == "button" and attributes.get("type"): self.button_types.add(attributes["type"]) if ( attributes.get("data-originweave-untrusted") == "prompt-injection" + and "hidden" in attributes and attributes.get("aria-hidden") == "true" ): self.hidden_injection_markers += 1 @@ -70,20 +89,49 @@ def test_fixture_contains_explicit_untrusted_hidden_prompt_injection(self) -> No self.assertIn("UNTRUSTED_PAGE_INSTRUCTION", self.html) self.assertIn("request new browser capabilities", self.html) + def test_hidden_injection_requires_the_actual_hidden_attribute(self) -> None: + """ARIA metadata alone must not satisfy the hidden-injection fixture contract.""" + + parser = _FixtureParser() + parser.feed( + "" + "" + ) + self.assertEqual(parser.hidden_injection_markers, 1) + def test_fixture_is_synthetic_and_has_no_credential_fields(self) -> None: """The controlled workflow must not require or imitate real secret collection.""" + for attributes in self.parser.input_attributes: + with self.subTest(attributes=attributes): + self.assertFalse(_is_credential_input(attributes)) + lowered = self.html.lower() - for forbidden in ( - 'type="password"', - 'autocomplete="current-password"', - 'autocomplete="one-time-code"', - "api_key", - "secret_key", - ): + for forbidden in ("api_key", "secret_key"): with self.subTest(forbidden=forbidden): self.assertNotIn(forbidden, lowered) + def test_credential_detection_is_quote_independent(self) -> None: + """Parsed credential semantics must reject single-quoted and tokenized forms.""" + + for html in ( + "", + "", + "", + "", + "", + ): + with self.subTest(html=html): + parser = _FixtureParser() + parser.feed(html) + self.assertEqual(len(parser.input_attributes), 1) + self.assertTrue(_is_credential_input(parser.input_attributes[0])) + + parser = _FixtureParser() + parser.feed("") + self.assertEqual(len(parser.input_attributes), 1) + self.assertFalse(_is_credential_input(parser.input_attributes[0])) + if __name__ == "__main__": unittest.main() From fa11155e93843047892b0ad7f8dec7ad02cfac36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 01:20:42 +0900 Subject: [PATCH 4/9] test(browser): inherit Agent Task URL invariant --- tests/test_agent_task_pinned_chrome_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_agent_task_pinned_chrome_contract.py b/tests/test_agent_task_pinned_chrome_contract.py index 9f8816f1c..90ca61ff5 100644 --- a/tests/test_agent_task_pinned_chrome_contract.py +++ b/tests/test_agent_task_pinned_chrome_contract.py @@ -46,6 +46,19 @@ def test_agent_task_pass_uses_real_webdriver_input_and_post_condition(self) -> N with self.subTest(expected=expected): self.assertIn(expected, runner) + def test_agent_task_submission_preserves_the_loaded_url(self) -> None: + """Submission must prove that the controlled action did not navigate away.""" + + runner = RUNNER.read_text(encoding="utf-8") + for expected in ( + "initial_url", + "post_submit_url", + "url_unchanged", + ): + with self.subTest(expected=expected): + self.assertIn(expected, runner) + self.assertIn("Agent Task URL changed during submission", runner) + def test_agent_task_observes_computed_role_and_name_before_action(self) -> None: """Real-browser evidence must bind the controlled targets to semantic role/name.""" From 7794a45f6c71bba94445d29b28a058cf6b3d63fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 05:14:21 +0900 Subject: [PATCH 5/9] fix(browser): inherit current Agent Task URL invariant --- scripts/ci/run_mv3_compatibility.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index f52c15d6e..a198cf670 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -533,6 +533,15 @@ def _run_agent_task_browser_pass( _webdriver_path(session_id, "/url"), {"url": fixture_url}, ) + initial_url = _json_request( + driver_port, + "GET", + _webdriver_path(session_id, "/url"), + ).get("value") + if initial_url != fixture_url: + raise RuntimeError( + f"Agent Task initial URL mismatch: expected {fixture_url!r}, got {initial_url!r}" + ) input_element = _find_element(driver_port, session_id, "#task-text") input_role, input_name = _get_element_semantics( driver_port, @@ -571,6 +580,14 @@ def _run_agent_task_browser_pass( _element_command_path(session_id, submit_element, "/click"), {}, ) + post_submit_url = _json_request( + driver_port, + "GET", + _webdriver_path(session_id, "/url"), + ).get("value") + url_unchanged = post_submit_url == initial_url + if not url_unchanged: + raise RuntimeError("Agent Task URL changed during submission") result_element = _find_element(driver_port, session_id, "#task-result") state = _json_request( driver_port, @@ -590,6 +607,7 @@ def _run_agent_task_browser_pass( "browser_version": browser_version, "post_condition": True, "input_echo_verified": True, + "url_unchanged": url_unchanged, "input_semantics_verified": True, "submit_semantics_verified": True, "extensions_disabled": True, @@ -642,6 +660,7 @@ def _run_agent_task_trial( "browser_version": result["browser_version"], "post_condition": result["post_condition"], "input_echo_verified": result["input_echo_verified"], + "url_unchanged": result["url_unchanged"], "input_semantics_verified": result["input_semantics_verified"], "submit_semantics_verified": result["submit_semantics_verified"], "extensions_disabled": result["extensions_disabled"], @@ -769,6 +788,7 @@ def main() -> int: agent_task_surfaces_complete = all( trial.get("post_condition") is True and trial.get("input_echo_verified") is True + and trial.get("url_unchanged") is True and trial.get("input_semantics_verified") is True and trial.get("submit_semantics_verified") is True and trial.get("extensions_disabled") is True @@ -821,4 +841,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From 4708135516ba262fe31ed58adcbec27a00bce0e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 05:16:23 +0900 Subject: [PATCH 6/9] style(browser): preserve canonical final newline --- scripts/ci/run_mv3_compatibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index a198cf670..e0f16c51a 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -841,4 +841,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 3671bbcef2c6d2c7114b165af6f730cc9116883f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:49:40 +0900 Subject: [PATCH 7/9] fix(stack): preserve current prerequisite tree in semantic role-name lane --- CHANGELOG.md | 2 + crates/originweave-core/src/lib.rs | 34 ++++- .../tests/extension_authority.rs | 119 +++++++++++++++++- docs/TRD.md | 2 +- .../0013-manifest-v3-extension-authority.md | 2 +- docs/doctoring.md | 12 ++ .../extension-authority-security.md | 6 + scripts/ci/run_mv3_compatibility.py | 0 8 files changed, 168 insertions(+), 9 deletions(-) mode change 100644 => 100755 scripts/ci/run_mv3_compatibility.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39ca..d17419927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 88dd2e586..b6ed55ff2 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -967,16 +967,20 @@ pub struct ExtensionAgentGrant { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + expires_at_epoch_seconds: u64, capabilities: BTreeSet, } impl ExtensionAgentGrant { - /// Build an exact extension-to-Agent grant for one browser session and context. + /// Build an exact extension-to-Agent grant for one session, context, origin, and exclusive expiry. #[must_use] pub fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + expires_at_epoch_seconds: u64, capabilities: I, ) -> Self where @@ -986,6 +990,8 @@ impl ExtensionAgentGrant { extension_id, browser_session, browsing_context, + origin, + expires_at_epoch_seconds, capabilities: capabilities.into_iter().collect(), } } @@ -997,22 +1003,31 @@ pub struct ExtensionAccessRequest { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, } impl ExtensionAccessRequest { /// Build one exact extension capability request without granting authority. + /// + /// `now_epoch_seconds` must be trusted evaluation time supplied by the host, + /// not a page, extension, or model clock. #[must_use] pub const fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, ) -> Self { Self { extension_id, browser_session, browsing_context, + origin, + now_epoch_seconds, capability, } } @@ -1021,7 +1036,7 @@ impl ExtensionAccessRequest { /// Result of evaluating an extension request against one explicit Agent grant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExtensionAccessDecision { - /// The exact extension, session, context, and capability are explicitly granted. + /// The exact extension, session, context, origin, unexpired grant, and capability are explicitly granted. Allow, /// No explicit extension-to-Agent grant was supplied. DenyMissingGrant, @@ -1031,6 +1046,10 @@ pub enum ExtensionAccessDecision { DenyBrowserSessionMismatch, /// The request belongs to a different independently navigable browser context. DenyBrowsingContextMismatch, + /// The request belongs to a different canonical origin than the grant. + DenyOriginMismatch, + /// Trusted evaluation time is at or after the grant's exclusive expiry. + DenyExpired, /// The extension grant does not contain the requested OriginWeave capability. DenyCapabilityNotGranted, } @@ -1039,8 +1058,9 @@ pub enum ExtensionAccessDecision { /// /// A Chrome extension permission, installation state, or page capability is never /// consulted here. A future Chromium adapter must construct a host-originated -/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session/context -/// request at the boundary where Agent authority would otherwise cross. +/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session, context, +/// canonical origin, and exclusive expiry at the boundary where Agent authority +/// would otherwise cross. #[must_use] pub fn evaluate_extension_access( request: &ExtensionAccessRequest, @@ -1058,6 +1078,12 @@ pub fn evaluate_extension_access( if request.browsing_context != grant.browsing_context { return ExtensionAccessDecision::DenyBrowsingContextMismatch; } + if request.origin != grant.origin { + return ExtensionAccessDecision::DenyOriginMismatch; + } + if request.now_epoch_seconds >= grant.expires_at_epoch_seconds { + return ExtensionAccessDecision::DenyExpired; + } if !grant.capabilities.contains(&request.capability) { return ExtensionAccessDecision::DenyCapabilityNotGranted; } diff --git a/crates/originweave-core/tests/extension_authority.rs b/crates/originweave-core/tests/extension_authority.rs index 82507a244..f34c30e9b 100644 --- a/crates/originweave-core/tests/extension_authority.rs +++ b/crates/originweave-core/tests/extension_authority.rs @@ -2,7 +2,7 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, - ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, evaluate_extension_access, + ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, Origin, evaluate_extension_access, }; fn extension_id(value: &str) -> ExtensionId { @@ -17,6 +17,13 @@ fn context(value: u64) -> BrowsingContextId { BrowsingContextId::new(value).expect("nonzero browsing context") } +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("canonical origin") +} + +const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; +const UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS: u64 = 1_700_000_600; + #[test] fn extension_id_accepts_only_canonical_chromium_extension_ids() { let canonical = "abcdefghijklmnopabcdefghijklmnop"; @@ -43,10 +50,13 @@ fn extension_id_accepts_only_canonical_chromium_extension_ids() { fn extension_agent_access_requires_an_explicit_exact_grant() { let allowed_extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); let other_extension = extension_id("bcdefghijklmnopabcdefghijklmnopa"); + let granted_origin = origin("https://app.example"); let grant = ExtensionAgentGrant::new( allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -54,6 +64,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -68,6 +80,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { other_extension, session(7), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -79,6 +93,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(8), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -87,24 +103,55 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { ); let wrong_context = ExtensionAccessRequest::new( - allowed_extension, + allowed_extension.clone(), session(7), context(12), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( evaluate_extension_access(&wrong_context, Some(&grant)), ExtensionAccessDecision::DenyBrowsingContextMismatch ); + + let wrong_origin = ExtensionAccessRequest::new( + allowed_extension.clone(), + session(7), + context(11), + origin("https://other.example"), + UNEXPIRED_NOW_EPOCH_SECONDS, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_origin, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); + + let wrong_port = ExtensionAccessRequest::new( + allowed_extension, + session(7), + context(11), + origin("https://app.example:8443"), + UNEXPIRED_NOW_EPOCH_SECONDS, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_port, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); } #[test] fn chrome_permissions_never_imply_originweave_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://mail.example"); let grant = ExtensionAgentGrant::new( id.clone(), session(3), context(5), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -112,6 +159,8 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { id, session(3), context(5), + granted_origin, + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!( @@ -123,10 +172,13 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { #[test] fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("http://127.0.0.1:8080"); let grant = ExtensionAgentGrant::new( id.clone(), session(13), context(17), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, @@ -137,10 +189,71 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, ] { - let request = ExtensionAccessRequest::new(id.clone(), session(13), context(17), capability); + let request = ExtensionAccessRequest::new( + id.clone(), + session(13), + context(17), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, + capability, + ); assert_eq!( evaluate_extension_access(&request, Some(&grant)), ExtensionAccessDecision::Allow ); } } + +#[test] +fn expired_origin_bound_grant_cannot_be_reused_after_exclusive_deadline() { + let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://billing.example"); + let expires_at_epoch_seconds = 1_700_000_100; + let grant = ExtensionAgentGrant::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + [ExtensionAgentCapability::ObserveCurrentContext], + ); + + let before_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds - 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&before_deadline, Some(&grant)), + ExtensionAccessDecision::Allow + ); + + let at_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&at_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); + + let after_deadline = ExtensionAccessRequest::new( + id, + session(19), + context(23), + granted_origin, + expires_at_epoch_seconds + 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&after_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); +} diff --git a/docs/TRD.md b/docs/TRD.md index 3e8030012..0e60e5ca5 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -25,7 +25,7 @@ The current reusable Rust control plane is intentionally smaller than the final | Module / boundary | Current responsibility | Protected-main status | Active/non-shipped evidence | |---|---|---|---| -| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | PR #40 builds a protocol-ID registry on top of these values; it is not protected-main truth | +| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | Active origin-bound `ExtensionAgentGrant` evaluation adds canonical-origin matching and exclusive trusted-time expiry; it is not protected-main truth until merge | | `originweave-policy` | Pure fail-closed action policy including purpose-bound sensitive-data authority. | **Implemented** | Trusted broker/runtime lifecycle remains separate planned work under issue #10 | | `originweave-destination` | Resolved-address classification, origin-bound snapshots, route authority, connection pinning, rebinding and redirect authority. | **Implemented** | PAC evaluation/proxy transport/CONNECT are still Planned | | `originweave-network` | Direct single-address TCP connection plan and exact operating-system peer verification. | **Implemented** | — | diff --git a/docs/adr/0013-manifest-v3-extension-authority.md b/docs/adr/0013-manifest-v3-extension-authority.md index e620edf9d..8feacbf27 100644 --- a/docs/adr/0013-manifest-v3-extension-authority.md +++ b/docs/adr/0013-manifest-v3-extension-authority.md @@ -92,7 +92,7 @@ No persistent database migration is introduced. A release can roll back the Chro ## Open follow-ups -- Complete issue #27's compatibility matrix and production isolation acceptance. +- Complete issue #27's compatibility matrix and production isolation acceptance. Exclusive trusted-time expiry on origin-bound `ExtensionAgentGrant` evaluation is the next protected-main candidate; task identity binding remains open. - Define managed-extension identity/update semantics. - Implement the native-messaging allow-list/process boundary before claiming support. - Integrate the complete Agent Task browser vertical slice under issue #28. diff --git a/docs/doctoring.md b/docs/doctoring.md index 75c107ef0..f0133bb5d 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -14,6 +14,14 @@ The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal The exact Chromium regression evidence is pinned to revision `446d05d21720f0b3505ec21057b3e9f909784262`. A mutable `HEAD` reference is not sufficient for a reproducible security contract. +### Extension-to-Agent grant origin binding + +RFC 6454 defines a web origin as the scheme, host, and port tuple that browsers use to isolate authority. An OriginWeave `extension_grant` that is bound only to extension identity, session, and browsing context would remain valid after the same context navigates to another origin. OriginWeave therefore requires the grant and the request to carry the same canonical origin. A host change or a non-default port change is a different origin and cannot reuse the grant. This is grant-scope isolation only; it does not install an extension, parse Chrome messages, or mint Agent capabilities from Manifest V3 permissions. + +### Extension-to-Agent grant exclusive expiry + +RFC 9700 is the current Best Current Practice for OAuth 2.0 security. It requires access tokens to be restricted in lifetime and treats long-lived bearer credentials as a standing authorization risk. An OriginWeave `extension_grant` that matches extension identity, session, browsing context, and canonical origin but has no exclusive expiry remains usable after the Agent Task window ends. OriginWeave therefore requires the grant to carry an exclusive `expires_at_epoch_seconds` deadline and the request to carry trusted `now_epoch_seconds`. Evaluation fails closed when `now >= expires_at`, matching the existing sensitive-handle exclusive-expiry rule. Page, extension, and model clocks are not trusted time. This slice does not bind task identity, install an extension, or mint Agent capabilities from Manifest V3 permissions. + ### Resolved destination and redirect safety Canonical origin identity is not a network-destination authorization. The IANA IPv4 and IPv6 Special-Purpose Address Space registries enumerate blocks whose source, destination, forwardability, globally reachable, and protocol-reserved properties differ. Both registries were last updated on 9 October 2025 and explicitly warn that registry presence does not guarantee routability in a particular local or global context. RFC 6890 established the common special-purpose registry fields, and RFC 8190 replaced the ambiguous `global` field with `globally reachable`. @@ -96,6 +104,8 @@ Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retriev Autio, C., Schwartz, R., Dunietz, J., Jain, S., Stanley, M., Tabassi, E., Hall, P., & Roberts, K. (2024). *Artificial intelligence risk management framework: Generative artificial intelligence profile* (NIST AI 600-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.600-1 +Barth, A. (2011). *The web origin concept* (RFC 6454). Internet Engineering Task Force. https://doi.org/10.17487/RFC6454 + Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the special-purpose IP address registries* (RFC 8190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8190 Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md @@ -128,6 +138,8 @@ International Organization for Standardization. (2017). *Information and documen Koster, M., Illyes, G., Zeller, H., & Sassman, L. (2022). *Robots Exclusion Protocol* (RFC 9309). Internet Engineering Task Force. https://doi.org/10.17487/RFC9309 +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 security best current practice* (RFC 9700). Internet Engineering Task Force. https://doi.org/10.17487/RFC9700 + Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index a36380a31..1c211f83d 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -50,6 +50,12 @@ Exact head `e83749acd1cf5a0b778ba38eb9d6ed5a9bd1e68f` deliberately keeps only th The exact head has successful CI, exact owned production coverage, Security Scan, SAST and CodeRabbit status and is Ready for review. It has no raw secret bytes and does not create approval evidence, a broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or release claim. +### Origin-bound extension grant evaluation + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAccessRequest` to carry the same canonical origin. A same-session, same-context request for `https://other.example` or `https://app.example:8443` against a grant for `https://app.example` is `DenyOriginMismatch`. Exclusive trusted-time expiry is evaluated after that origin match: `now >= expires_at` is `DenyExpired`. This does not install an extension, parse Chrome messages, bind task identity, or mint Agent capabilities from Manifest V3 permissions. + ## 4. Security interpretation The executable authority chain is intentionally non-transitive: diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py old mode 100644 new mode 100755 From b1160275e2a73c98db754c342d6f9b690d07e9b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 10:52:18 +0900 Subject: [PATCH 8/9] docs(browser): record semantic role evidence --- CHANGELOG.md | 1 + docs/DOCUMENTATION_FITNESS.md | 4 ++-- docs/TEST_STRATEGY.md | 5 +++++ .../action-postcondition-evidence.md | 17 +++++++++++++---- tests/test_agent_task_pinned_chrome_contract.py | 4 ++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd858e22..e4af90fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. - Real pinned-Chrome WebDriver evidence for the controlled Agent Task fixture: the CI lane uses an isolated profile, disables extensions, types and submits synthetic text, observes the same-document post-condition, and proves profile cleanup; this does not claim a shipped OriginWeave browser adapter. +- Active pinned-Chrome Agent Task evidence verifies browser-computed role/name for controlled input and submit targets before action; this remains test-harness semantic evidence and does not claim a product semantic observer or authority. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index cd637ae65..f82bd4fe8 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -158,9 +158,9 @@ Active #64 makes a successful action-outcome value require existing verified pro ### 3.19 Controlled Agent Task fixture -Active #65 supplies a deterministic synthetic local web fixture with a labelled semantic input, submit control, same-document post-condition and explicitly hidden/untrusted prompt-injection text. Active #70 executes that fixture through real WebDriver on pinned Chrome with an isolated profile, disabled extensions, synthetic input, same-document post-condition verification and profile cleanup. The fixture contains no credential collection surface and requires no live third-party site. +Active #65 supplies a deterministic synthetic local web fixture with a labelled semantic input, submit control, same-document post-condition and explicitly hidden/untrusted prompt-injection text. Active #70 executes that fixture through real WebDriver on pinned Chrome with an isolated profile, disabled extensions, synthetic input, same-document post-condition verification and profile cleanup. Active #71 verifies browser-computed role/name for the controlled input and submit button before action. The fixture contains no credential collection surface and requires no live third-party site. -**Resolution:** the #65/#70 lane makes controlled browser-level evidence reproducible without turning a third-party site into a test dependency. It is not a browser adapter, semantic extractor, OriginWeave input-dispatch authority, policy engine, trusted clock, process-attribution source or proof of the shipped product runtime. +**Resolution:** the #65/#70/#71 lane makes controlled browser-level and browser-computed role/name evidence reproducible without turning a third-party site into a test dependency. It is not a browser adapter, product semantic extractor, OriginWeave input-dispatch authority, policy engine, trusted clock, process-attribution source or proof of the shipped product runtime. ### 3.20 Bounded browser process-set resource evidence diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 56dd6f18f..faf7f54b3 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -85,6 +85,11 @@ browser-level fixture execution only; it does not replace the OriginWeave BiDi/CDP authority adapter, semantic node contract, policy dispatch or protected-main runtime acceptance required by issue #28. +Active PR #71 additionally verifies browser-computed role/name for the +controlled input and submit target before the real WebDriver action. CSS remains +a fixture-harness locator; this does not establish OriginWeave node authority, +semantic provenance or policy dispatch. + ### 3.5 Buyer acceptance Versioned task packs measure repeatable product outcomes rather than one lucky agent run. The benchmark artifact records browser build, OriginWeave version, model/provider/reasoning configuration, seed where supported, policy profile, hardware profile and source fixtures. diff --git a/docs/traceability/action-postcondition-evidence.md b/docs/traceability/action-postcondition-evidence.md index 3dc12137a..482538394 100644 --- a/docs/traceability/action-postcondition-evidence.md +++ b/docs/traceability/action-postcondition-evidence.md @@ -63,6 +63,14 @@ PR #70 reuses the existing pinned Chrome for Testing workflow and executes the # This is real WebDriver evidence for a controlled local fixture, not a product browser adapter. It does not establish WebDriver BiDi/CDP authority translation, OriginWeave semantic observation or node handles, policy-authorized typed action dispatch, trusted browser-process attribution, or protected-main product runtime completion. +### PR #71 — browser-computed semantic role/name evidence before action + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +PR #71 extends the pinned-Chrome fixture lane by reading WebDriver's browser-computed role and accessible name for the controlled input and submit button before sending input or clicking. The exact expected values are `textbox` / `Task text` and `button` / `Submit task`; the repeatability gate requires both semantic checks in every successful trial. + +This is bounded browser-computed evidence for a synthetic test target, not the OriginWeave semantic observation adapter. CSS locators remain test-harness selectors, and the lane does not create OriginWeave node handles, source-channel provenance, policy authority, or permission to execute page-advertised actions. + ## 4. Non-transitive success semantics The intended first-slice chain is: @@ -88,7 +96,7 @@ VerifiedActionOutcomeEvidence type existence -/> proof of real Chromium executio controlled fixture success -/> proof of an OriginWeave product browser runtime ``` -PR #64 now rejects a caller-supplied observation timestamp that predates caller-supplied dispatch time, but the type cannot independently prove the clock source, that a real browser actually dispatched the action, that the supplied provenance belongs to the claimed browser target/node, or that the observed state was caused by that action. PR #70 proves real Chromium execution against the controlled fixture, but its test-harness CSS locators and direct WebDriver calls are not the OriginWeave adapter/runtime composition required under issue #28. +PR #64 now rejects a caller-supplied observation timestamp that predates caller-supplied dispatch time, but the type cannot independently prove the clock source, that a real browser actually dispatched the action, that the supplied provenance belongs to the claimed browser target/node, or that the observed state was caused by that action. PR #70 proves real Chromium execution against the controlled fixture and PR #71 adds browser-computed role/name evidence, but their test-harness CSS locators and direct WebDriver calls are not the OriginWeave adapter/runtime composition required under issue #28. ## 5. Active prerequisite graph for issue #28 @@ -102,9 +110,10 @@ The first real Chromium vertical slice remains distributed across bounded active - PR #51 — bounded browser-task telemetry plus one explicitly supplied Linux PID `VmRSS` sampler; Chromium process discovery/process-set attribution remains outside that slice; - PR #64 — verified and caller-timestamp-ordered post-condition action-outcome evidence; and - PR #65 — controlled hostile local Agent Task workflow fixture; and -- PR #70 — real WebDriver execution of that fixture on pinned Chrome, without claiming a product browser adapter. +- PR #70 — real WebDriver execution of that fixture on pinned Chrome, without claiming a product browser adapter; and +- PR #71 — browser-computed role/name evidence before controlled action, without claiming a product semantic observer. -These active PRs are non-shipped evidence. PR #70 proves a bounded browser-level fixture flow, but the active set does not itself compose WebDriver BiDi/CDP transport, OriginWeave authority translation, trusted Chromium process attribution, policy-authorized real input dispatch, causal post-condition observation, or deterministic end-to-end teardown/recovery into one protected-main runtime. +These active PRs are non-shipped evidence. PR #70/#71 prove bounded browser-level and semantic evidence, but the active set does not itself compose WebDriver BiDi/CDP transport, OriginWeave authority translation, trusted Chromium process attribution, policy-authorized real input dispatch, causal post-condition observation, or deterministic end-to-end teardown/recovery into one protected-main runtime. ## 6. Remaining issue #28 boundary @@ -122,4 +131,4 @@ This dossier does **not** close issue #28. Material remaining work includes: ## 7. Documentation fitness consequence -The ADR/PRD/TRD/Architecture/UML/ERD graph remains **DESIGN-SUFFICIENT / PROTECTED-MAIN-PARTIAL**. PR #64 narrows a typed evidence gap, PR #65 supplies the controlled fixture, and PR #70 supplies real WebDriver evidence for that fixture. Neither introduces a new trust domain, deployed component, persistence owner, database schema, or independent architecture decision, so a new ADR or physical ERD entity would overstate the implementation. Detailed real-Chromium dispatch/post-condition sequence diagrams should be reconciled when the executable adapter chain stabilizes rather than manufacturing as-built detail before that runtime exists. +The ADR/PRD/TRD/Architecture/UML/ERD graph remains **DESIGN-SUFFICIENT / PROTECTED-MAIN-PARTIAL**. PR #64 narrows a typed evidence gap, PR #65 supplies the controlled fixture, and PR #70/#71 supply real WebDriver and browser-computed semantic evidence for that fixture. Neither introduces a new trust domain, deployed component, persistence owner, database schema, or independent architecture decision, so a new ADR or physical ERD entity would overstate the implementation. Detailed real-Chromium dispatch/post-condition sequence diagrams should be reconciled when the executable adapter chain stabilizes rather than manufacturing as-built detail before that runtime exists. diff --git a/tests/test_agent_task_pinned_chrome_contract.py b/tests/test_agent_task_pinned_chrome_contract.py index 7bc75872e..2571cabd4 100644 --- a/tests/test_agent_task_pinned_chrome_contract.py +++ b/tests/test_agent_task_pinned_chrome_contract.py @@ -142,6 +142,10 @@ def test_documentation_separates_active_browser_evidence_from_product_runtime(se self.assertIn("not a product browser adapter", traceability) self.assertIn("pinned Chrome", fitness) self.assertIn("not a browser adapter", fitness) + self.assertIn("browser-computed role/name", changelog) + self.assertIn("PR #71", traceability) + self.assertIn("computed role/name", traceability) + self.assertIn("computed role/name", fitness) if __name__ == "__main__": From 94c962463c2adebf3e8510057cf9c948ee3fba18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:18:33 -0700 Subject: [PATCH 9/9] docs(browser): preserve semantic evidence changelog after stack refresh --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c4d8240..a126ace14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. - Real pinned-Chrome WebDriver evidence for the controlled Agent Task fixture: the CI lane uses an isolated profile, disables extensions, types and submits synthetic text, observes the same-document post-condition, and proves profile cleanup; this does not claim a shipped OriginWeave browser adapter. +- Active pinned-Chrome Agent Task evidence verifies browser-computed role/name for controlled input and submit targets before action; this remains test-harness semantic evidence and does not claim a product semantic observer or authority. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge.