Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions automation/automation-session-ui.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: sourceos.dev/v1alpha1
kind: AutomationSessionUI
metadata:
name: bearbrowser-automation-session-surface
labels:
sourceos.dev/product: BearBrowser
sourceos.dev/surface: automation-session-ui
spec:
description: >-
Visible, user-controlled surface that displays every active browser automation
session backed by a BrowserAutomationReceipt. The surface must be rendered
whenever any automation transport is active and must never be suppressed.

visibility:
always: true
suppressible: false
location: browser-toolbar-badge-and-panel

sessionCard:
requiredFields:
- receiptId
- ownerRef
- displayName
- transport
- tabScope
- permissionScope
- origin
- status
- capturedAt

displayMapping:
ownerRef: "Owner / Agent"
displayName: "Session label"
transport: "Active transport"
tabScope: "Controlled tab(s) / window(s)"
permissionScope: "Granted permissions"
origin: "Origin (local / remote / workspace)"
receiptId: "Evidence receipt ID"
status: "Session status"
capturedAt: "Session started"

actions:
revoke:
label: "Revoke / Kill session"
icon: stop-circle
requiresConfirmation: true
confirmationPrompt: "Revoking this session will immediately terminate the automation transport and invalidate all session tokens. Proceed?"
effect:
- terminateTransport: true
- invalidateSessionToken: true
- updateReceiptStatus: "revoked"
- emitProvenanceEvent: "automation.action_denied"
- setRevokedAt: now

sessionList:
emptyState: "No active automation sessions."
sortOrder: capturedAt-desc
maxVisible: 10
overflowBehavior: paginate

enforcement:
noOwnerPolicy: reject
noPolicyDecisionPolicy: reject
orphanPolicy: quarantine
logMode: compactReceiptRef
debugLogMode: fullTopologyOnlyWhenDebugEnabled
63 changes: 63 additions & 0 deletions docs/runtime-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,76 @@ Backend preference order:
5. Links
6. Lynx

## Automation receipts

Every automation transport start produces a `BrowserAutomationReceipt` before the transport is permitted to operate.

### Receipt structure

| Field | Description |
|---|---|
| `receiptId` | Stable URN: `urn:srcos:receipt:browser-automation:<local-id>` |
| `sessionRef` | Active BearBrowser session reference |
| `ownerRef` | Agent, plugin, or workspace that owns the session |
| `transport` | `native_pipe`, `cdp`, `webdriver`, `extension`, or `accessibility` |
| `permissionScope` | Explicit permissions: `read_dom`, `click`, `type`, `download`, `upload`, `inspect_network`, `inspect_cookies`, `use_credentials` |
| `origin` | `local`, `remote`, or `workspace` |
| `userVisible` | Always `true` |
| `revocable` | Always `true` |
| `policyDecisionRef` | PolicyFabric decision that admitted the session |
| `evidenceRefs` | Provenance artifact references |
| `capturedAt` | Session start timestamp |
| `status` | `active`, `revoked`, `ended`, `denied`, or `orphaned` |

Schema: `schemas/browser-automation-receipt.schema.json`
Example: `examples/browser-automation-receipt.example.json`

### Receipt lifecycle

1. **Transport starts** → receipt created with `status: active` and persisted to the provenance directory.
2. **Policy denied** → receipt created with `status: denied`; transport is not started.
3. **User revokes** → receipt updated to `status: revoked`, `revokedAt` set; transport terminated and session token invalidated.
4. **Session ends normally** → receipt updated to `status: ended`.
5. **Orphaned event** (no matching receipt) → event quarantined, never silently accepted.

### Verifying receipts

```bash
python3 scripts/bearbrowser-verify-automation-receipt.py examples/browser-automation-receipt.example.json
```

Run the built-in acceptance test suite:

```bash
python3 scripts/bearbrowser-verify-automation-receipt.py --self-test
```

## Visible session surface

The automation session UI (`automation/automation-session-ui.yaml`) shows:

1. Which agent/plugin/workspace owns the session
2. Active transport
3. Controlled tab/window/page scope
4. Granted permissions
5. Local/remote/workspace origin
6. Evidence receipt ID
7. One-click revoke/kill control

The surface is always visible when any automation transport is active and cannot be suppressed.

## Governance rules

- Automation frameworks provide control mechanisms, not authority.
- PolicyFabric grants authority.
- BearBrowser emits provenance events.
- Remote debugging remains denied unless explicitly granted.
- Agent-runtime live browser operations require policy decision IDs.
- No automation session may run without an owner.
- No automation session may run without a policy decision.
- Orphaned automation events are quarantined, never silently accepted.

See `policy/automation-receipt-policy.yaml` for the full runtime policy.

## Current caveat

Expand Down
20 changes: 20 additions & 0 deletions examples/browser-automation-receipt.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"schemaVersion": "bearbrowser.browser_automation_receipt.v1",
"receiptId": "urn:srcos:receipt:browser-automation:local-smoke-001",
"sessionRef": "bb-session-example",
"ownerRef": "agent-example",
"transport": "cdp",
"permissionScope": ["read_dom", "click", "type"],
"origin": "local",
"userVisible": true,
"revocable": true,
"policyDecisionRef": "policy-decision-example",
"evidenceRefs": [
"artifact://bearbrowser/capture/example",
"evt-provenance-example-001"
],
"capturedAt": "2026-05-06T18:00:00Z",
"status": "active",
"tabScope": ["tab-1"],
"displayName": "Example Agent (local smoke)"
}
62 changes: 62 additions & 0 deletions policy/automation-receipt-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
apiVersion: sourceos.dev/v1alpha1
kind: AutomationReceiptPolicy
metadata:
name: bearbrowser-automation-receipt-policy
labels:
sourceos.dev/product: BearBrowser
sourceos.dev/surface: automation-receipt
spec:
description: >-
Runtime policy rules governing BrowserAutomationReceipt creation, validation,
revocation, and orphan handling. Every automation transport start must produce
a receipt before the transport is permitted to operate.

sessionStart:
requireReceipt: true
requireOwnerRef: true
requirePolicyDecisionRef: true
defaultDecision: deny
receiptMustBeUserVisible: true
receiptMustBeRevocable: true

receiptLifecycle:
onTransportStart:
action: createReceipt
emitEvent: automation.action_requested
statusOnCreation: active
onPolicyDenied:
action: createReceiptWithTerminalState
status: denied
emitEvent: automation.action_denied
onRevoke:
action: updateReceiptStatus
status: revoked
setRevokedAt: now
terminateTransport: true
invalidateSessionToken: true
emitEvent: automation.action_denied
onSessionEnd:
action: updateReceiptStatus
status: ended
emitEvent: automation.observed

orphanHandling:
definition: "An automation event that cannot be matched to an active, owner-bearing receipt."
defaultAction: quarantine
allowedActions:
- quarantine
- reject
logMode: compactReceiptRef
neverSilentlyAccept: true

missingOwner:
action: reject
reason: "No automation session may run without an owner."

logging:
normalMode: compactReceiptRef
debugMode: fullTopologyOnlyWhenExplicitlyEnabled
neverLogHighLeakageTopologyInNormalMode: true

receiptSchema: schemas/browser-automation-receipt.schema.json
uiContract: automation/automation-session-ui.yaml
70 changes: 62 additions & 8 deletions runtime/playwright-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';

const url = process.env.BEARBROWSER_URL || 'about:blank';
const mode = process.env.BEARBROWSER_MODE || 'agent-runtime';
const live = process.env.BEARBROWSER_ENABLE_LIVE_PLAYWRIGHT === '1';
const policyDecisionId = process.env.BEARBROWSER_POLICY_DECISION_ID || '';
const provenanceDir = process.env.BEARBROWSER_PROVENANCE_PATH || path.join(os.tmpdir(), 'bearbrowser-provenance');

const sessionId = process.env.BEARBROWSER_SESSION_ID || `bb-${Date.now()}`;
const agentId = process.env.BEARBROWSER_AGENT_ID || 'local-smoke';
const workspaceId = process.env.BEARBROWSER_WORKSPACE_ID || 'local';

// Generate the hex suffix for the receipt URN.
const receiptHexId = crypto.randomBytes(8).toString('hex');
const receiptId = `urn:srcos:receipt:browser-automation:${receiptHexId}`;

function buildReceipt(status, extra = {}) {
const receipt = {
schemaVersion: 'bearbrowser.browser_automation_receipt.v1',
receiptId,
sessionRef: sessionId,
ownerRef: agentId,
transport: 'cdp',
permissionScope: ['read_dom', 'click', 'type'],
origin: 'local',
userVisible: true,
revocable: true,
policyDecisionRef: policyDecisionId || 'not-yet-assigned',
evidenceRefs: [],
capturedAt: new Date().toISOString(),
status,
displayName: `${agentId} (playwright smoke)`,
...extra,
};
return receipt;
}

function emitReceipt(status, extra = {}) {
const receipt = buildReceipt(status, extra);
console.log(JSON.stringify({ eventType: 'browser.automation.receipt', receipt }, null, 2));
return receipt;
}

const event = {
eventType: 'browser.session.started',
eventVersion: 'v1alpha1',
timestamp: new Date().toISOString(),
sessionId: process.env.BEARBROWSER_SESSION_ID || `bb-${Date.now()}`,
agentId: process.env.BEARBROWSER_AGENT_ID || 'local-smoke',
workspaceId: process.env.BEARBROWSER_WORKSPACE_ID || 'local',
sessionId,
agentId,
workspaceId,
profileMode: mode,
policyDecisionId: policyDecisionId || null,
automationReceiptId: receiptId,
url,
liveExecution: live,
controlPlane: 'playwright'
Expand All @@ -27,6 +64,8 @@ console.log(JSON.stringify(event, null, 2));

if (!policyDecisionId && mode === 'agent-runtime') {
console.error('ERROR: BEARBROWSER_POLICY_DECISION_ID is required for live agent-runtime Playwright execution.');
// Emit a denied receipt so the session is not silently orphaned.
emitReceipt('denied');
if (!live) {
console.log('Dry-run mode accepted without live execution.');
process.exit(0);
Expand All @@ -36,12 +75,20 @@ if (!policyDecisionId && mode === 'agent-runtime') {

if (!live) {
console.log('Dry run complete. Set BEARBROWSER_ENABLE_LIVE_PLAYWRIGHT=1 to run a guarded live smoke test.');
// Emit an ended receipt for observability in dry-run mode.
emitReceipt('ended');
process.exit(0);
}

const { chromium } = await import('playwright');
fs.mkdirSync(provenanceDir, { recursive: true });
fs.writeFileSync(path.join(provenanceDir, `${event.sessionId}.started.json`), JSON.stringify(event, null, 2));

// Emit and persist the active automation receipt before the transport starts.
const activeReceipt = emitReceipt('active');
const receiptPath = path.join(provenanceDir, `${sessionId}.receipt.json`);
fs.writeFileSync(receiptPath, JSON.stringify(activeReceipt, null, 2));

fs.writeFileSync(path.join(provenanceDir, `${sessionId}.started.json`), JSON.stringify(event, null, 2));

const browser = await chromium.launch({ headless: true });
try {
Expand All @@ -50,24 +97,31 @@ try {
console.log(JSON.stringify({
eventType: 'browser.navigation.requested',
timestamp: new Date().toISOString(),
sessionId: event.sessionId,
sessionId,
automationReceiptId: receiptId,
url,
policyDecisionId
}, null, 2));
await page.goto(url, { waitUntil: 'domcontentloaded' });
console.log(JSON.stringify({
eventType: 'browser.navigation.completed',
timestamp: new Date().toISOString(),
sessionId: event.sessionId,
sessionId,
automationReceiptId: receiptId,
url: page.url(),
title: await page.title()
}, null, 2));
} finally {
await browser.close();
// Update the receipt to ended state.
const endedReceipt = emitReceipt('ended');
fs.writeFileSync(receiptPath, JSON.stringify(endedReceipt, null, 2));
console.log(JSON.stringify({
eventType: 'browser.session.ended',
timestamp: new Date().toISOString(),
sessionId: event.sessionId,
cleanupStatus: 'browserClosed'
sessionId,
automationReceiptId: receiptId,
cleanupStatus: 'browserClosed',
receiptStatus: 'ended'
}, null, 2));
}
Loading