From 86a464030207993d577a18f6702c154da2c80bc1 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 6 May 2026 20:16:55 -0400 Subject: [PATCH 1/3] Add BearHistory dry-run CLI --- scripts/bearbrowser-history.py | 188 +++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 scripts/bearbrowser-history.py diff --git a/scripts/bearbrowser-history.py b/scripts/bearbrowser-history.py new file mode 100644 index 0000000..c9c8217 --- /dev/null +++ b/scripts/bearbrowser-history.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""BearHistory dry-run contract surface for BearBrowser. + +This command intentionally does not read browser profiles, cookies, credentials, +history databases, downloads, or captures. It emits deterministic JSON plans for +policy review and downstream contract integration. +""" + +from __future__ import annotations + +import argparse +import json +from datetime import UTC, datetime +from typing import Any + + +DEFAULT_POLICY_REF = "urn:srcos:bearhistory-sync-policy:default-local-first-v1" +DEFAULT_SERVICE_REF = "urn:srcos:local-first-service:bearhistoryd" +DEFAULT_WORKROOM = "urn:srcos:workroom:professional-intelligence-demo" +DEFAULT_AGENT_PROFILE = "urn:srcos:profile:agent-runtime" + + +def now() -> str: + return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def print_json(value: dict[str, Any]) -> int: + print(json.dumps(value, indent=2, sort_keys=True)) + return 0 + + +def policy_explain(profile: str) -> dict[str, Any]: + if profile not in {"human-secure", "agent-runtime"}: + raise SystemExit("profile must be human-secure or agent-runtime") + + human_secure = profile == "human-secure" + export_to_ops_history = "deny" if human_secure else "policy-gated" + lanes = [ + { + "lane": "save", + "priority": "maintenance" if human_secure else "normal", + "idleDelaySeconds": 90 if human_secure else 30, + "cadence": [{"count": 1, "intervalSeconds": 86400}] + if human_secure + else [{"count": 50, "intervalSeconds": 60}, {"count": 10, "intervalSeconds": 300}], + "requiresPolicyDecision": True, + }, + { + "lane": "redaction", + "priority": "critical", + "idleDelaySeconds": 6, + "cadence": [{"count": 20, "intervalSeconds": 5}], + "requiresPolicyDecision": True, + }, + ] + if not human_secure: + lanes.insert( + 1, + { + "lane": "export", + "priority": "normal", + "idleDelaySeconds": 30, + "cadence": [{"count": 20, "intervalSeconds": 180}], + "requiresPolicyDecision": True, + }, + ) + + return { + "planKind": "bearhistory-policy-explain", + "dryRun": True, + "generatedAt": now(), + "profileClass": profile, + "policyRef": DEFAULT_POLICY_REF, + "serviceManifestRef": DEFAULT_SERVICE_REF, + "exportToOpsHistory": export_to_ops_history, + "syncWindowSeconds": 1209600 if human_secure else 604800, + "payloadCapChars": 100000 if human_secure else 50000, + "profileBoundary": { + "credentialsExportAllowed": False, + "cookiesExportAllowed": False, + "ambientSessionExportAllowed": False, + "humanProfileExportAllowed": False, + "agentRuntimeExportAllowed": not human_secure, + }, + "lanes": lanes, + "network": { + "offlineBehavior": "queue-local", + "meteredAllowed": False, + "batteryAllowed": True, + "transferDirection": "bidirectional", + }, + "nonGoals": [ + "no browser database reads", + "no credential access", + "no cookie access", + "no live export", + ], + } + + +def export_explain(session: str, profile: str) -> dict[str, Any]: + if profile not in {"human-secure", "agent-runtime"}: + raise SystemExit("profile must be human-secure or agent-runtime") + + allowed = profile == "agent-runtime" + return { + "planKind": "bearhistory-export-explain", + "dryRun": True, + "generatedAt": now(), + "sessionRef": session if session.startswith("urn:") else f"urn:srcos:browser-session:{session}", + "profileClass": profile, + "outcome": "metadata-only" if allowed else "deny", + "opsHistoryEventRef": f"urn:srcos:ops-history-event:bearbrowser-{session}-metadata-dry-run", + "policyDecisionRefs": [ + "urn:srcos:policy-decision:ops-history-browser-event-export-demo-0001" + ], + "agentRegistryRefs": ["urn:srcos:agent-grant:ops-history-browser-demo"] if allowed else [], + "payloadMode": "metadata-only" if allowed else "redacted", + "profileBoundary": { + "humanProfileExportAllowed": False, + "credentialMaterialIncluded": False, + "cookieMaterialIncluded": False, + "ambientSessionMaterialIncluded": False, + }, + "targetScope": { + "workroomRef": DEFAULT_WORKROOM, + "profileRef": DEFAULT_AGENT_PROFILE if allowed else "urn:srcos:profile:human-secure", + }, + } + + +def redactions_pending() -> dict[str, Any]: + return { + "planKind": "bearhistory-redactions-pending", + "dryRun": True, + "generatedAt": now(), + "pending": [], + "policy": { + "redactionPriority": "critical", + "targetPropagationSeconds": 30, + "invalidateOpsHistoryExports": True, + "invalidateContextPacks": True, + "invalidateMemoryWritebacks": True, + }, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="bearbrowser-history", + description="BearHistory dry-run policy and export-plan surface.", + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + policy = subparsers.add_parser("policy", help="Explain BearHistory profile policy.") + policy_sub = policy.add_subparsers(dest="policy_command", required=True) + explain_policy = policy_sub.add_parser("explain") + explain_policy.add_argument("--profile", default="agent-runtime") + explain_policy.add_argument("--dry-run", action="store_true", default=True) + + export = subparsers.add_parser("export", help="Explain BearHistory export posture.") + export_sub = export.add_subparsers(dest="export_command", required=True) + explain_export = export_sub.add_parser("explain") + explain_export.add_argument("--session", default="demo") + explain_export.add_argument("--profile", default="agent-runtime") + explain_export.add_argument("--dry-run", action="store_true", default=True) + + redactions = subparsers.add_parser("redactions", help="Show pending BearHistory redaction posture.") + redactions.add_argument("--dry-run", action="store_true", default=True) + + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + if not getattr(args, "dry_run", True): + raise SystemExit("BearHistory commands are dry-run only in this implementation slice") + if args.command == "policy" and args.policy_command == "explain": + return print_json(policy_explain(args.profile)) + if args.command == "export" and args.export_command == "explain": + return print_json(export_explain(args.session, args.profile)) + if args.command == "redactions": + return print_json(redactions_pending()) + raise SystemExit("unknown BearHistory command") + + +if __name__ == "__main__": + raise SystemExit(main()) From 2af470d0b22645ccd7bd59fbb6e3e4759f8f7a2f Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 6 May 2026 20:17:41 -0400 Subject: [PATCH 2/3] Expose BearHistory dry-run CLI in Homebrew formula --- packaging/homebrew/Formula/bearbrowser.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packaging/homebrew/Formula/bearbrowser.rb b/packaging/homebrew/Formula/bearbrowser.rb index bbfc7c9..f5b0558 100644 --- a/packaging/homebrew/Formula/bearbrowser.rb +++ b/packaging/homebrew/Formula/bearbrowser.rb @@ -46,6 +46,7 @@ def install (bin/"bearbrowser-playwright").write wrapper_for("bearbrowser-playwright.sh") (bin/"bearbrowser-stagehand").write wrapper_for("bearbrowser-stagehand.sh") (bin/"bearbrowser-terminal").write wrapper_for("bearbrowser-terminal.sh") + (bin/"bearbrowser-history").write wrapper_for("bearbrowser-history.py") end def wrapper_for(script) @@ -97,6 +98,9 @@ def caveats bearbrowser-playwright --dry-run bearbrowser-stagehand --dry-run bearbrowser-terminal --dry-run + bearbrowser-history policy explain --profile agent-runtime --dry-run + bearbrowser-history export explain --session demo --profile agent-runtime --dry-run + bearbrowser-history redactions --dry-run Full signed app distribution will use: brew install --cask SourceOS-Linux/tap/bearbrowser @@ -119,5 +123,7 @@ def caveats assert_match "BearBrowser policy action", shell_output("#{bin}/bearbrowser-propose-action --action-type summarize_page --target-kind page --target-label test") assert_match "BearBrowser agent sidecar contract verified", shell_output("#{bin}/bearbrowser-verify-agent-sidecar") assert_match "BearBrowser sidecar status verified", shell_output("#{bin}/bearbrowser-verify-sidecar-status") + assert_match "bearhistory-policy-explain", shell_output("#{bin}/bearbrowser-history policy explain --profile agent-runtime --dry-run") + assert_match "bearhistory-export-explain", shell_output("#{bin}/bearbrowser-history export explain --session demo --profile agent-runtime --dry-run") end end From 730d44a5e458501c4857a8cdc4caa7a59a04d19f Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 6 May 2026 20:20:44 -0400 Subject: [PATCH 3/3] Document BearHistory dry-run command surface --- docs/local-first-bearhistory.md | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/local-first-bearhistory.md b/docs/local-first-bearhistory.md index 70f36df..cf00476 100644 --- a/docs/local-first-bearhistory.md +++ b/docs/local-first-bearhistory.md @@ -88,6 +88,37 @@ BearHistory may export agent-runtime events into OpsHistory when: Human-secure events are not exported to OpsHistory by default. +## Dry-run command surface + +This implementation slice adds `bearbrowser-history` as a dry-run contract surface. The command does not read browser profiles, cookies, credentials, history databases, downloads, captures, or automation traces. + +Commands: + +```bash +bearbrowser-history policy explain --profile human-secure --dry-run +bearbrowser-history policy explain --profile agent-runtime --dry-run +bearbrowser-history export explain --session demo --profile agent-runtime --dry-run +bearbrowser-history redactions --dry-run +``` + +Expected posture: + +- human-secure export outcome is `deny`; +- agent-runtime export outcome is `metadata-only` until PolicyFabric and Agent Registry permit stronger behavior; +- credentials, cookies, and ambient session material are always absent in this slice; +- redaction posture is critical priority and invalidates downstream OpsHistory exports, context packs, and memory writebacks. + +## Future service model + +The future service boundary is `bearhistoryd`, represented by `urn:srcos:local-first-service:bearhistoryd`. + +Expected local endpoint names: + +- `org.sourceos.BearHistory`; +- `org.sourceos.BearHistory.Push`; +- `org.sourceos.BearHistory.Redaction`; +- `org.sourceos.BearHistory.Export`. + ## Non-goals BearHistory does not provide: @@ -110,3 +141,12 @@ BearBrowser should expose a doctor/explain path for BearHistory policy: - policy decision IDs; - blocked export reasons; - redaction status. + +## Validation + +The Homebrew Formula exposes `bearbrowser-history` and checks: + +```bash +bearbrowser-history policy explain --profile agent-runtime --dry-run +bearbrowser-history export explain --session demo --profile agent-runtime --dry-run +```