Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- 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.
- Pinned Chrome-for-Testing Agent Task evidence now captures a bounded sampled Chromium root-plus-descendant process count and RSS total from one `/proc` status sweep, with bounded failure-type diagnostics while preserving the root-only metric and making no trusted per-task attribution claim.
- Pinned Chrome-for-Testing Agent Task evidence now locates the controlled result by exact browser-computed `status`/`Task result` semantics and records only a bounded canonical SHA-256 digest plus stable field identity for the extracted synthetic value, without emitting the raw value.
- 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.
- Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation.
- Authoritative product documentation graph spanning PRD, TRD, ADR lifecycle/index, product-wide UML, conceptual ERD, requirement/decision traceability, threat modeling, product-wide test strategy, operability, API/protocol, release/rollback, and current primary-source standards doctoring, with machine-checkable repository contracts that keep conversation-derived future work distinct from protected-main implementation claims.
Expand All @@ -47,6 +48,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Security

- The loopback Manifest V3 fixture server rejects resolved request targets outside its configured fixture root, including escapes through fixture-tree symlinks.
- Raw page content cannot become a trusted instruction.
- Raw secrets are rejected and secret-capable actions require an opaque broker handle.
- Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision.
Expand Down
66 changes: 64 additions & 2 deletions scripts/ci/run_mv3_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

import contextlib
import hashlib
import http.client
import http.server
import json
Expand Down Expand Up @@ -46,6 +47,7 @@
MAX_BROWSER_PROCESS_TREE_SIZE = 256
MAX_PROC_PROCESS_SCAN_SIZE = 32_768
MAX_SEMANTIC_LOCATOR_CANDIDATES = 128
MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES = 4_096
MAX_U64 = (1 << 64) - 1
W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
PATH_TOKEN_CHARACTERS = frozenset(string.ascii_letters + string.digits + "-_.")
Expand All @@ -54,6 +56,15 @@
class QuietFixtureHandler(http.server.SimpleHTTPRequestHandler):
"""Serve only the controlled local fixture without noisy access logging."""

def translate_path(self, path: str) -> str:
"""Keep resolved request targets inside the configured fixture root."""

fixture_root = pathlib.Path(self.directory).resolve()
candidate = pathlib.Path(super().translate_path(path)).resolve()
if not candidate.is_relative_to(fixture_root):
return str(fixture_root / ".originweave-denied")
return str(candidate)

def log_message(self, _format: str, *args: object) -> None:
"""Suppress request logs because the fixture contains no diagnostic value."""

Expand Down Expand Up @@ -249,6 +260,19 @@ def _find_element_by_accessible_role_name(
return matches[0]


def _hash_agent_task_structured_value(value: str) -> str:
"""Hash one bounded extracted text value without retaining the raw value in evidence."""

if not isinstance(value, str):
raise TypeError("Agent Task structured value must be text")
encoded = value.encode("utf-8")
if not encoded:
raise ValueError("Agent Task structured value must not be empty")
if len(encoded) > MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES:
raise ValueError("Agent Task structured value exceeded the bounded text contract")
return "sha256:" + hashlib.sha256(encoded).hexdigest()


def _parse_linux_proc_status_rss_bytes(status_text: str) -> int:
"""Parse exactly one positive Linux ``VmRSS`` kB field into bounded bytes."""

Expand Down Expand Up @@ -437,6 +461,17 @@ def _sample_linux_process_set_rss_bytes(
return total_rss_bytes


def _sample_linux_process_snapshot_rss_bytes(
process_id: int,
process_evidence: dict[int, tuple[int, int | None]],
) -> int:
"""Sample one process RSS from the same bounded snapshot as its process set."""

if process_id not in process_evidence:
raise RuntimeError("Linux process snapshot did not contain the browser root PID")
return _sample_linux_process_set_rss_bytes((process_id,), process_evidence)


def _wait_for_extension_evidence(
driver_port: int,
session_id: str,
Expand Down Expand Up @@ -863,7 +898,19 @@ def _run_agent_task_browser_pass(
if not url_unchanged:
raise RuntimeError("Agent Task URL changed during submission")

result_element = _find_element(driver_port, session_id, "#task-result")
result_element = _find_element_by_accessible_role_name(
driver_port,
session_id,
"status",
"Task result",
)
result_role, result_name = _get_element_semantics(
driver_port,
session_id,
result_element,
)
if result_role != "status" or result_name != "Task result":
raise RuntimeError("Agent Task result semantic evidence mismatch")
state = _json_request(
driver_port,
"GET",
Expand All @@ -877,13 +924,17 @@ def _run_agent_task_browser_pass(
_validate_agent_task_submitted_state(state)
if text != AGENT_TASK_INPUT_VALUE:
raise RuntimeError("Agent Task result did not match the synthetic typed value")
structured_value_sha256 = _hash_agent_task_structured_value(text)

process_evidence = _snapshot_linux_process_evidence()
chromium_process_ids = _discover_linux_process_tree_ids(
browser_process_id,
process_evidence,
)
browser_process_rss_bytes = _sample_linux_process_rss_bytes(browser_process_id)
browser_process_rss_bytes = _sample_linux_process_snapshot_rss_bytes(
browser_process_id,
process_evidence,
)
Comment thread
seonghobae marked this conversation as resolved.
chromium_process_set_rss_bytes = _sample_linux_process_set_rss_bytes(
chromium_process_ids,
process_evidence,
Expand All @@ -899,6 +950,9 @@ def _run_agent_task_browser_pass(
"url_unchanged": url_unchanged,
"input_semantics_verified": True,
"submit_semantics_verified": True,
"result_semantics_verified": True,
"structured_value_field": "task_result",
"structured_value_sha256": structured_value_sha256,
"extensions_disabled": True,
"browser_process_rss_bytes": browser_process_rss_bytes,
"chromium_process_count": chromium_process_count,
Expand Down Expand Up @@ -958,6 +1012,9 @@ def _run_agent_task_trial(
"url_unchanged": result["url_unchanged"],
"input_semantics_verified": result["input_semantics_verified"],
"submit_semantics_verified": result["submit_semantics_verified"],
"result_semantics_verified": result["result_semantics_verified"],
"structured_value_field": result["structured_value_field"],
"structured_value_sha256": result["structured_value_sha256"],
"extensions_disabled": result["extensions_disabled"],
"browser_process_rss_bytes": result["browser_process_rss_bytes"],
"chromium_process_count": result["chromium_process_count"],
Expand Down Expand Up @@ -1094,6 +1151,11 @@ def main() -> int:
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("result_semantics_verified") is True
and trial.get("structured_value_field") == "task_result"
and isinstance(trial.get("structured_value_sha256"), str)
and len(trial["structured_value_sha256"]) == len("sha256:") + 64
and trial["structured_value_sha256"].startswith("sha256:")
and trial.get("extensions_disabled") is True
and trial.get("profile_cleaned") is True
and isinstance(trial.get("browser_process_rss_bytes"), int)
Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/agent_task_basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ <h1>Controlled Agent Task</h1>
<button type="submit">Submit task</button>
</form>

<output id="task-result" data-state="idle" aria-live="polite">idle</output>
<output
id="task-result"
data-state="idle"
aria-label="Task result"
aria-live="polite"
>idle</output>
Comment thread
seonghobae marked this conversation as resolved.

<p
data-originweave-untrusted="prompt-injection"
Expand Down
66 changes: 66 additions & 0 deletions tests/test_agent_task_structured_value_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Contract for credential-safe structured extraction in the controlled Agent Task."""

from __future__ import annotations

import hashlib
import pathlib
import runpy
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"
FIXTURE = ROOT / "tests" / "fixtures" / "agent_task_basic" / "index.html"


class AgentTaskStructuredValueContractTests(unittest.TestCase):
"""Require bounded semantic extraction without retaining the extracted value."""

def test_result_is_discovered_by_exact_browser_semantics(self) -> None:
"""The result node must be located by browser-computed role/name, not fixture CSS."""

runner = RUNNER.read_text(encoding="utf-8")
fixture = FIXTURE.read_text(encoding="utf-8")
self.assertIn('aria-label="Task result"', fixture)
self.assertIn('"status"', runner)
self.assertIn('"Task result"', runner)
self.assertIn('"result_semantics_verified"', runner)
self.assertNotIn('_find_element(driver_port, session_id, "#task-result")', runner)

def test_structured_value_hash_is_bounded_and_canonical(self) -> None:
"""Only a canonical SHA-256 digest may leave the controlled extraction boundary."""

namespace = runpy.run_path(str(RUNNER), run_name="agent_task_structured_value_contract")
self.assertIn("MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES", namespace)
self.assertIn("_hash_agent_task_structured_value", namespace)
helper = namespace["_hash_agent_task_structured_value"]
maximum = namespace["MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES"]

value = "synthetic structured value"
expected = "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
digest = helper(value)
self.assertEqual(digest, expected)
self.assertNotIn(value, digest)
self.assertEqual(len(digest), len("sha256:") + 64)

with self.assertRaises(ValueError):
helper("")
with self.assertRaises(ValueError):
helper("x" * (maximum + 1))
with self.assertRaises(TypeError):
helper(42)

def test_agent_task_evidence_reports_field_and_digest_not_raw_result(self) -> None:
"""Trial evidence must expose a field identifier and digest, not extracted text."""

runner = RUNNER.read_text(encoding="utf-8")
for expected in (
'"structured_value_field"',
'"structured_value_sha256"',
'"task_result"',
):
with self.subTest(expected=expected):
self.assertIn(expected, runner)


if __name__ == "__main__":
unittest.main()
28 changes: 28 additions & 0 deletions tests/test_mv3_compatibility_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import http.client
import json
import pathlib
import runpy
import tempfile
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -120,6 +122,32 @@ def test_runner_transport_cannot_follow_dynamic_url_schemes(self) -> None:
self.assertNotIn("urllib.request", runner)
self.assertNotIn("urllib.error", runner)

def test_fixture_handler_cannot_follow_symlinks_outside_its_root(self) -> None:
"""The loopback fixture server must not expose files outside its configured root."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_contract")
start_server = namespace["_start_fixture_server"]
stop_server = namespace["_stop_fixture_server"]
with tempfile.TemporaryDirectory() as temporary_directory:
temporary_root = pathlib.Path(temporary_directory)
fixture_root = temporary_root / "fixture"
outside_root = temporary_root / "outside"
fixture_root.mkdir()
outside_root.mkdir()
(outside_root / "secret.txt").write_text("outside", encoding="utf-8")
(fixture_root / "escape").symlink_to(outside_root, target_is_directory=True)

server, thread = start_server(fixture_root)
connection = http.client.HTTPConnection(*server.server_address, timeout=1)
try:
connection.request("GET", "/escape/secret.txt")
response = connection.getresponse()
self.assertEqual(response.status, 404)
self.assertNotIn(b"outside", response.read())
finally:
connection.close()
stop_server(server, thread)

def test_runner_accepts_real_chromedriver_element_ids_without_path_injection(self) -> None:
"""ChromeDriver dotted element IDs must work while path syntax stays fail-closed."""

Expand Down
Loading