diff --git a/.github/workflows/browser-runtime-boundary.yml b/.github/workflows/browser-runtime-boundary.yml new file mode 100644 index 0000000..5a813fb --- /dev/null +++ b/.github/workflows/browser-runtime-boundary.yml @@ -0,0 +1,34 @@ +name: Browser Runtime Boundary + +on: + pull_request: + paths: + - "schemas/browser-runtime-boundary-decision.schema.json" + - "examples/browser-runtime-boundary.*.json" + - "scripts/verify-browser-runtime-boundary.py" + - "docs/browser-runtime-boundary.md" + - ".github/workflows/browser-runtime-boundary.yml" + push: + branches: + - main + paths: + - "schemas/browser-runtime-boundary-decision.schema.json" + - "examples/browser-runtime-boundary.*.json" + - "scripts/verify-browser-runtime-boundary.py" + - "docs/browser-runtime-boundary.md" + - ".github/workflows/browser-runtime-boundary.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-browser-runtime-boundary: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Validate Browser Runtime Boundary fixtures + run: python3 scripts/verify-browser-runtime-boundary.py diff --git a/docs/browser-runtime-boundary.md b/docs/browser-runtime-boundary.md new file mode 100644 index 0000000..ba1e095 --- /dev/null +++ b/docs/browser-runtime-boundary.md @@ -0,0 +1,54 @@ +# BearBrowser Runtime Boundary Decision + +## Purpose + +`BrowserRuntimeBoundaryDecision` is a decision-only record for BearBrowser runtime, automation, credential, and workspace-bridge surfaces. + +It exists to prevent BearBrowser from collapsing policy text, credential mediation, browser automation, authenticated-session state, downloads, workspace bridges, and provenance events into a single implicit allow. + +## Boundary chain + +```text +browser / operator / agent request = evidence input +Policy Fabric decision = policy admission +Agent Registry ref = agent identity / authority evidence +BrowserRuntimeBoundaryDecision = local decision-only boundary +BearBrowser automation / credential broker / workspace bridge = later runtime surface +Provenance event = redacted evidence record only +``` + +## Hard rules + +A valid boundary decision must keep: + +- `performedAction = false` +- `credentialExportAllowed = false` +- `inheritsHumanCredentials = false` +- `nonLoopbackControlAllowed = false` +- `nativeExecutionAllowed = false` +- `declaredWorkspaceScopeOnly = true` +- `secretValuesLogged = false` +- `sessionMaterialLogged = false` +- `paymentMaterialLogged = false` + +Agent actors must carry an Agent Registry ref. Policy decisions must be explicit refs. Evidence must be refs, not raw secrets or session material. + +## Validation + +```bash +python3 scripts/verify-browser-runtime-boundary.py +``` + +The verifier validates the good agent automation fixture and rejects fixtures that attempt credential export or raw secret logging. + +## Related surfaces + +- `TRUST_SURFACE.yaml` +- `policy/credential-broker-contract.yaml` +- `scripts/policy-surface` +- `scripts/credential-surface` +- `docs/provenance-events.md` + +## Non-goals + +This tranche does not execute browser automation, grant credential access, submit forms, bridge downloads to workspaces, send native messages, open non-loopback control, or mutate profiles. It only adds the boundary record and validation path. diff --git a/examples/browser-runtime-boundary.agent-automation.valid.json b/examples/browser-runtime-boundary.agent-automation.valid.json new file mode 100644 index 0000000..94a224f --- /dev/null +++ b/examples/browser-runtime-boundary.agent-automation.valid.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": "bearbrowser.runtime-boundary.v1", + "kind": "BrowserRuntimeBoundaryDecision", + "decisionId": "browser-runtime-boundary:agent-automation-001", + "surface": "agent-browser-runtime", + "actor": { + "type": "agent", + "id": "agent://bearbrowser/test-agent", + "agentRegistryRef": "agent-registry://bearbrowser/test-agent" + }, + "requestedAction": "agent_navigation", + "policyDecisionId": "policy-decision://bearbrowser/agent-navigation/allow-001", + "credentialBoundary": { + "credentialAccessRequested": false, + "credentialExportAllowed": false, + "inheritsHumanCredentials": false, + "osMediatedOnly": true + }, + "automationBoundary": { + "automationRequested": true, + "nonLoopbackControlAllowed": false, + "nativeExecutionAllowed": false + }, + "workspaceBoundary": { + "downloadOrWorkspaceBridgeRequested": false, + "declaredWorkspaceScopeOnly": true + }, + "redactionBoundary": { + "secretValuesLogged": false, + "sessionMaterialLogged": false, + "paymentMaterialLogged": false + }, + "performedAction": false, + "evidenceRefs": [ + "evidence://bearbrowser/runtime-boundary/agent-automation-001" + ], + "notes": "Decision-only record: policy admitted agent navigation, but this record performs no browser action and grants no credential authority." +} diff --git a/examples/browser-runtime-boundary.credential-export.invalid.json b/examples/browser-runtime-boundary.credential-export.invalid.json new file mode 100644 index 0000000..f29e90f --- /dev/null +++ b/examples/browser-runtime-boundary.credential-export.invalid.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "bearbrowser.runtime-boundary.v1", + "kind": "BrowserRuntimeBoundaryDecision", + "decisionId": "browser-runtime-boundary:credential-export-invalid", + "surface": "credential-broker", + "actor": { + "type": "agent", + "id": "agent://bearbrowser/test-agent", + "agentRegistryRef": "agent-registry://bearbrowser/test-agent" + }, + "requestedAction": "credential_export", + "policyDecisionId": "policy-decision://bearbrowser/credential-export/deny-001", + "credentialBoundary": { + "credentialAccessRequested": true, + "credentialExportAllowed": true, + "inheritsHumanCredentials": false, + "osMediatedOnly": true + }, + "automationBoundary": { + "automationRequested": false, + "nonLoopbackControlAllowed": false, + "nativeExecutionAllowed": false + }, + "workspaceBoundary": { + "downloadOrWorkspaceBridgeRequested": false, + "declaredWorkspaceScopeOnly": true + }, + "redactionBoundary": { + "secretValuesLogged": false, + "sessionMaterialLogged": false, + "paymentMaterialLogged": false + }, + "performedAction": false, + "evidenceRefs": ["evidence://bearbrowser/runtime-boundary/credential-export-invalid"], + "notes": "Invalid fixture: credential export must remain denied." +} diff --git a/examples/browser-runtime-boundary.raw-secret.invalid.json b/examples/browser-runtime-boundary.raw-secret.invalid.json new file mode 100644 index 0000000..8600dd3 --- /dev/null +++ b/examples/browser-runtime-boundary.raw-secret.invalid.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "bearbrowser.runtime-boundary.v1", + "kind": "BrowserRuntimeBoundaryDecision", + "decisionId": "browser-runtime-boundary:raw-secret-invalid", + "surface": "credential-broker", + "actor": { + "type": "agent", + "id": "agent://bearbrowser/test-agent", + "agentRegistryRef": "agent-registry://bearbrowser/test-agent" + }, + "requestedAction": "credential_request", + "policyDecisionId": "policy-decision://bearbrowser/credential-request/deny-001", + "credentialBoundary": { + "credentialAccessRequested": true, + "credentialExportAllowed": false, + "inheritsHumanCredentials": false, + "osMediatedOnly": true + }, + "automationBoundary": { + "automationRequested": false, + "nonLoopbackControlAllowed": false, + "nativeExecutionAllowed": false + }, + "workspaceBoundary": { + "downloadOrWorkspaceBridgeRequested": false, + "declaredWorkspaceScopeOnly": true + }, + "redactionBoundary": { + "secretValuesLogged": true, + "sessionMaterialLogged": false, + "paymentMaterialLogged": false + }, + "performedAction": false, + "evidenceRefs": ["evidence://bearbrowser/runtime-boundary/raw-secret-invalid"], + "notes": "Invalid fixture: runtime boundary records must not log raw secret values." +} diff --git a/schemas/browser-runtime-boundary-decision.schema.json b/schemas/browser-runtime-boundary-decision.schema.json new file mode 100644 index 0000000..3507fcb --- /dev/null +++ b/schemas/browser-runtime-boundary-decision.schema.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.dev/bearbrowser/browser-runtime-boundary-decision.schema.json", + "title": "BearBrowser Runtime Boundary Decision", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "kind", + "decisionId", + "surface", + "actor", + "requestedAction", + "policyDecisionId", + "credentialBoundary", + "automationBoundary", + "workspaceBoundary", + "redactionBoundary", + "performedAction", + "evidenceRefs" + ], + "properties": { + "schemaVersion": { "const": "bearbrowser.runtime-boundary.v1" }, + "kind": { "const": "BrowserRuntimeBoundaryDecision" }, + "decisionId": { "type": "string", "minLength": 1 }, + "surface": { "type": "string", "enum": ["human-secure-browser", "agent-browser-runtime", "automation-surface", "credential-broker", "workspace-bridge"] }, + "actor": { + "type": "object", + "additionalProperties": false, + "required": ["type", "id"], + "properties": { + "type": { "type": "string", "enum": ["human", "agent", "service"] }, + "id": { "type": "string", "minLength": 1 }, + "agentRegistryRef": { "type": ["string", "null"] } + } + }, + "requestedAction": { "type": "string", "minLength": 1 }, + "policyDecisionId": { "type": "string", "minLength": 1 }, + "credentialBoundary": { + "type": "object", + "additionalProperties": false, + "required": ["credentialAccessRequested", "credentialExportAllowed", "inheritsHumanCredentials", "osMediatedOnly"], + "properties": { + "credentialAccessRequested": { "type": "boolean" }, + "credentialExportAllowed": { "const": false }, + "inheritsHumanCredentials": { "const": false }, + "osMediatedOnly": { "type": "boolean" } + } + }, + "automationBoundary": { + "type": "object", + "additionalProperties": false, + "required": ["automationRequested", "nonLoopbackControlAllowed", "nativeExecutionAllowed"], + "properties": { + "automationRequested": { "type": "boolean" }, + "nonLoopbackControlAllowed": { "const": false }, + "nativeExecutionAllowed": { "const": false } + } + }, + "workspaceBoundary": { + "type": "object", + "additionalProperties": false, + "required": ["downloadOrWorkspaceBridgeRequested", "declaredWorkspaceScopeOnly"], + "properties": { + "downloadOrWorkspaceBridgeRequested": { "type": "boolean" }, + "declaredWorkspaceScopeOnly": { "const": true } + } + }, + "redactionBoundary": { + "type": "object", + "additionalProperties": false, + "required": ["secretValuesLogged", "sessionMaterialLogged", "paymentMaterialLogged"], + "properties": { + "secretValuesLogged": { "const": false }, + "sessionMaterialLogged": { "const": false }, + "paymentMaterialLogged": { "const": false } + } + }, + "performedAction": { "const": false }, + "evidenceRefs": { "type": "array", "minItems": 1, "items": { "type": "string" }, "uniqueItems": true }, + "notes": { "type": "string" } + } +} diff --git a/scripts/verify-browser-runtime-boundary.py b/scripts/verify-browser-runtime-boundary.py new file mode 100644 index 0000000..104caa9 --- /dev/null +++ b/scripts/verify-browser-runtime-boundary.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Verify BearBrowser runtime boundary decision fixtures.""" +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = ROOT / "schemas" / "browser-runtime-boundary-decision.schema.json" +VALID = ROOT / "examples" / "browser-runtime-boundary.agent-automation.valid.json" +INVALID_CREDENTIAL_EXPORT = ROOT / "examples" / "browser-runtime-boundary.credential-export.invalid.json" +INVALID_RAW_SECRET = ROOT / "examples" / "browser-runtime-boundary.raw-secret.invalid.json" + +REQUIRED = { + "schemaVersion", + "kind", + "decisionId", + "surface", + "actor", + "requestedAction", + "policyDecisionId", + "credentialBoundary", + "automationBoundary", + "workspaceBoundary", + "redactionBoundary", + "performedAction", + "evidenceRefs", +} + + +class BoundaryError(Exception): + pass + + +def load_json(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise BoundaryError(f"{path.relative_to(ROOT)}: expected object") + return payload + + +def require(condition: bool, message: str) -> None: + if not condition: + raise BoundaryError(message) + + +def validate_schema(schema: dict[str, Any]) -> None: + require(schema.get("$schema") == "https://json-schema.org/draft/2020-12/schema", "schema draft mismatch") + require(schema.get("additionalProperties") is False, "schema must be closed") + + +def validate_boundary(path: Path, record: dict[str, Any]) -> None: + missing = sorted(REQUIRED - set(record)) + require(not missing, f"{path}: missing fields: {missing}") + require(record.get("schemaVersion") == "bearbrowser.runtime-boundary.v1", f"{path}: schemaVersion mismatch") + require(record.get("kind") == "BrowserRuntimeBoundaryDecision", f"{path}: kind mismatch") + require(record.get("performedAction") is False, f"{path}: boundary record must not perform browser action") + require(str(record.get("policyDecisionId", "")).startswith("policy-decision://"), f"{path}: policyDecisionId required") + + actor = record.get("actor", {}) + require(isinstance(actor, dict), f"{path}: actor must be object") + if actor.get("type") == "agent": + require(actor.get("agentRegistryRef"), f"{path}: agent actor requires Agent Registry ref") + + credential = record.get("credentialBoundary", {}) + require(credential.get("credentialExportAllowed") is False, f"{path}: credential export must remain denied") + require(credential.get("inheritsHumanCredentials") is False, f"{path}: agent runtime must not inherit human credentials") + + automation = record.get("automationBoundary", {}) + require(automation.get("nonLoopbackControlAllowed") is False, f"{path}: non-loopback control must remain denied") + require(automation.get("nativeExecutionAllowed") is False, f"{path}: native execution must remain denied") + + workspace = record.get("workspaceBoundary", {}) + require(workspace.get("declaredWorkspaceScopeOnly") is True, f"{path}: workspace bridge must stay declared-scope only") + + redaction = record.get("redactionBoundary", {}) + require(redaction.get("secretValuesLogged") is False, f"{path}: secret values must not be logged") + require(redaction.get("sessionMaterialLogged") is False, f"{path}: session material must not be logged") + require(redaction.get("paymentMaterialLogged") is False, f"{path}: payment material must not be logged") + + evidence_refs = record.get("evidenceRefs", []) + require(isinstance(evidence_refs, list) and evidence_refs, f"{path}: evidenceRefs required") + for ref in evidence_refs: + require(isinstance(ref, str) and ref.startswith("evidence://"), f"{path}: evidence refs must be evidence://") + + +def expect_invalid(path: Path) -> None: + try: + validate_boundary(path.relative_to(ROOT), load_json(path)) + except BoundaryError: + return + raise BoundaryError(f"invalid fixture unexpectedly validated: {path.relative_to(ROOT)}") + + +def main() -> int: + try: + validate_schema(load_json(SCHEMA)) + validate_boundary(VALID.relative_to(ROOT), load_json(VALID)) + expect_invalid(INVALID_CREDENTIAL_EXPORT) + expect_invalid(INVALID_RAW_SECRET) + except (OSError, json.JSONDecodeError, BoundaryError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + print("BearBrowser runtime boundary fixtures verified") + return 0 + + +if __name__ == "__main__": + sys.exit(main())