Skip to content

security: add persistent command audit log to browse server#617

Open
halbert04 wants to merge 1 commit intogarrytan:mainfrom
halbert04:security/browse-audit-log
Open

security: add persistent command audit log to browse server#617
halbert04 wants to merge 1 commit intogarrytan:mainfrom
halbert04:security/browse-audit-log

Conversation

@halbert04
Copy link
Copy Markdown

Summary

  • Every browse server command is now logged to .gstack/browse-audit.jsonl as append-only JSONL
  • Each entry records: timestamp, command, args, page origin, duration, status, error, whether cookies are imported, and connection mode
  • The audit log persists across server restarts (unlike the in-memory ring buffers)
  • All writes are best-effort — audit failures never block command execution

Why this matters

The browse server executes commands on behalf of AI agents — navigating pages, clicking elements, running JavaScript, importing cookies. When something goes wrong (unexpected navigation, data exfiltration, prompt injection), there's currently no persistent forensic trail. The in-memory ring buffers (console, network, dialog) are capped at 50K entries and lost on server restart.

The audit log creates a persistent record of every command the server processed. Example use cases:

# What commands ran in the last session?
cat .gstack/browse-audit.jsonl | jq .

# Which commands ran with imported cookies? (elevated risk)
cat .gstack/browse-audit.jsonl | jq 'select(.hasCookies == true)'

# What JS was executed and on which origins?
cat .gstack/browse-audit.jsonl | jq 'select(.cmd == "js" or .cmd == "eval")'

# What failed?
cat .gstack/browse-audit.jsonl | jq 'select(.status == "error")'

Changes

File What changed
browse/src/audit.ts New file. initAuditLog() and writeAuditEntry() — append-only JSONL writer with truncation
browse/src/config.ts Added auditLog path to BrowseConfig (.gstack/browse-audit.jsonl)
browse/src/server.ts Initialize audit log on startup, write entries on command success and error
browse/src/browser-manager.ts Added markCookiesImported() and hasCookieImports() for the hasCookies audit field
browse/src/write-commands.ts Call markCookiesImported() on cookie-import and cookie-import-browser

Design decisions

  • Append-only, never truncated by server. The console/network/dialog logs are cleared on each server start. The audit log is not — it accumulates across sessions. Users can rotate or truncate it manually if it grows large.
  • Args truncated to 200 chars, errors to 300 chars. Prevents the log from growing unboundedly if large payloads are passed (e.g., js with a long expression).
  • hasCookies boolean flag. Makes it trivial to filter for elevated-risk commands — any command that ran while browser cookies were imported from the user's real browser.
  • Independent of other security PRs. The hasCookieImports() tracking added here is minimal (just a boolean). It's compatible with but doesn't depend on security: track cookie-imported domains and scope cookie imports #615 (domain-level tracking).

Test plan

  • Start browse server, run a few commands (goto, snapshot, js), verify .gstack/browse-audit.jsonl contains entries
  • Import cookies, run commands, verify hasCookies: true in subsequent entries
  • Trigger an error (e.g., js with invalid expression), verify error entry is logged
  • Restart server, verify audit log is preserved (not cleared)
  • Verify jq queries above work against the log format

Made with Cursor

Every command dispatched through the browse server is now logged to
.gstack/browse-audit.jsonl as an append-only JSONL file. Each entry
records: timestamp, command, args (truncated to 200 chars), page origin
URL, duration, status (ok/error), error message, whether cookies have
been imported, and connection mode (headless/headed).

Unlike the in-memory ring buffers (console, network, dialog) which are
capped at 50K entries and lost on restart, the audit log persists across
server restarts and is never truncated by the server. This creates a
forensic trail for post-incident analysis — if an agent performed
unexpected actions (e.g., navigated to a sensitive domain, ran JS with
imported cookies), the audit log shows exactly what happened and when.

The audit log is best-effort: write failures are silently ignored and
never cause command failures. The hasCookies flag makes it easy to
filter for elevated-risk commands (sessions with imported browser
cookies).

Changes:
- browse/src/audit.ts: new module with initAuditLog() and writeAuditEntry()
- browse/src/config.ts: added auditLog path to BrowseConfig
- browse/src/server.ts: initialize audit log, write entries on command
  success and error
- browse/src/browser-manager.ts: added hasCookieImports() tracking
- browse/src/write-commands.ts: mark cookies imported on cookie-import
  and cookie-import-browser

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant