# Spec 079: Browser Sessions ## Status Implemented for local commands. ## Scope This spec covers the `dagu browser session` commands: opening a session, running operations in it, describing its page, exporting it as a `browser.run` step, and its lifetime. The operations themselves behave as in Spec 072 and are not restated here. Sessions are local to the host that runs the commands; there is no REST or MCP surface for them. ## Goal A person or an agent can build a browser step by working the real site one operation at a time, with the operations the step will run, seeing the page after each, and keep exactly what worked as a step whose first run replays it. ## Behavior ### Commands Every command prints one JSON object to stdout. A command that fails prints `{"error": {"code", "message"}}` and exits 1. | Command | Purpose | |---|---| | `open [URL]` | Launch a browser, go to URL, and leave it waiting for commands | | `do ` | Run one operation, read from stdin, in the session | | `describe ` | Report what the session's page shows | | `export ` | Build a `browser.run` step from the session's history | | `close ` | Close the browser and remove the session, or end it with `--keep` | | `list` | List the host's sessions | `open` accepts `--profile`, `--headed`, `--viewport WxH`, `--executable`, `--proxy`, `--allowed-domain` (repeatable), `--idle-timeout` (default `30m`, at most `24h`), and the model as `--llm` (a step's `llm` field, in YAML or JSON) or `--provider`, `--model`, `--base-url`, and `--api-key-name`. Its result carries the session ID, the page's URL, title, and outline, and the browser's DevTools address (`cdp_url`), where an application can show the page and let a person act on it. `open` also returns `tls_verified`: true confirms that the certificate proxy verified the final HTTPS page's host. False means no such evidence was available; it does not by itself mean the certificate failed. A host the guard carried counts only while the guard runs, since the browser reaches it directly once the guard stops. This confirms transport trust, not login or whether a configured authority was needed. `list` includes `capabilities: ["trusted_cas"]` so applications can detect support without assuming an engine version. A session's browser presents the client certificates of the host configuration's `browser.client_certificates`, and guards its other HTTPS requests as the host configuration says, as a browser step does, for as long as the browser runs, including between commands. What the certificate proxy reports while a command runs, such as a site rejecting the certificate, appears in the command's warnings or error. Between commands the browser and its proxy keep running, so local processes can use the certificates as long as the session lasts, as they can during a browser step. The host's `browser.trusted_cas` also applies, with the same scoped trust and browser-lifetime snapshot as a workflow browser. ### Lifetime A session waits between commands until its idle deadline, which every command renews. Opening a session starts a watchdog process that closes the browser once the session is idle past its deadline. A sweep by a later session command, a browser step, or a server also closes it, as it does when the process running a command died. A session whose browser closed this way ends: `do` and `describe` fail with `session_ended`, and its history stays readable, so `export` and `list` still work, for 24 hours. `close --keep` ends a session the same way at once, freeing its profile for a step. `close` removes the session at once, with its history. Commands on one session run one at a time; a command that finds the session in use fails at once with `session_busy`. ### Operations `do` reads one operation from stdin in the form of an item of a browser step's `with.do`: `goto`, `act`, `upload`, `extract`, `expect`, `wait`, or `screenshot`, with `when` and `timeout`. `ask` is refused. Each operation behaves as the same operation in a step, except that an `act` or an `upload` into words never replays a recording; it asks the model and records what it did. An upload's relative paths are under the command's working directory. `do` also acts on an element the outline shows with its ID, without the model: `{"click": ID}`, `{"type": {"into": ID, "text": TEXT}}`, `{"select": {"in": ID, "option": OPTION}}`, or `{"upload": {"into": ID, "files": FILES}}`, each with `when` and `timeout` as an act takes them. The element must still be on the page, have a name, and be one the operation fits: a field to type into, a select to pick from. The session keeps a click, typing, or pick as the act a step writes, such as `Click the "Sign in" button`, which the result reports as `act`, and an upload as the upload into the element in words, such as `the "Receipt" file input`, which the result reports as `into`, with the action it took as its recording, so an exported step replays exactly that action and heals by the operation's words when the page changes. The result reports the operation's index in the session's history, its status (`done`, `failed`, or `skipped` when its `when` does not hold), the actions an act or upload performed and whether an exported step will replay them, what an extract read, the files a screenshot or download saved, the dialogs the browser accepted, the requests `allowed_domains` blocked, the tokens used, and the page the browser is on next with its outline. A failed operation is kept in the history and the session stays open. An extract warns about the same link fields loading a step does. `open`, `do`, and `describe` report a page its site refused, as a browser step counts one, as `refused`: the `host`, the HTTP `status`, `bot_check` naming the check, such as `cloudflare`, when the site answered with one, and a `message` saying so in a sentence. The error of an operation that failed on it ends with that sentence. A browser reattached by a later command learns a bot check only from pages it loads, so a command that finds the browser on the same page, with the same host and status, as the command before keeps the bot check that command reported, as after a person reloaded the page between commands. ### Variables Variable values come with the operation on stdin, under `variables`, never on the command line. A value given as `{"env": NAME}` is read from the environment on that command and every later one, which then require it, and is masked like a secret in everything the session reports. A literal value serves its own command only and is not masked. No value is ever kept in the session's state, only the salted digest of one an act's actions never used. An instruction that holds a secret value is refused, and an act whose recorded actions hold a variable's value is not exported for replay. ### Model A session asks the model given to `open` for acts, uploads into words, extracts, and judged conditions. The model's settings read the environment variables they name, its API key's and those its base URL references, on each command; no other variable is visible to them. A session opened without a model refuses those operations with `model_required` and runs the rest. ### Outline The outline describes the page without a model request, from the same accessibility tree an act shows the model: headings, fields with their labels, selects with their choice and options, file inputs, buttons, checkboxes, links with their addresses, messages, and tables and lists by their columns and rows. Each element an operation can act on shows its ID in brackets, and a link to the page's own site shows its path; a list item that is one link shows only the link. The page's own content comes first, and the site's header, menus, sidebars, and footer after it, so a limit on the outline leaves out menus rather than content. A run of more than five alike rows shows the first three and counts the rest. Text typed into fields is never shown. `describe --find` shows only the entries containing the text, with the entries they sit in; `--tree` reports the raw tree instead; `--screenshot` also saves a screenshot. ### Export `export --dag --step ` builds a `browser.run` step from the operations that succeeded and those skipped because their `when` did not hold, in the order they ran. Failed operations and screenshots are left out, and `--skip ` leaves out others. The page `open` went to becomes `with.url`, and the session's browser options other than `headless` become `with.browser`. A variable read from the environment becomes `${NAME}` of that variable, which the DAG declares under its secrets; a literal one becomes a parameter of its own name. Unless `--dry-run` is given, export writes the recordings of the acts and uploads to the replay cache of the named DAG and step on this host, keyed by their places in the step's `with.do`, so the step's first run replays them without a model request when it meets the pages the session met, and heals as usual when it does not. An act whose actions never used a variable its instruction names keeps a salted digest of the value the session gave it, so the step replays it only while that variable has that value (Spec 072); one the session recorded without the digest is exported with a warning and no recording. Export runs the checks the step gets when its DAG loads and warns about an act or upload with no recording, about a page reached outside the exported operations, and about each upload, whose files are those the session was given until the step names what each run uploads. `--format yaml` prints the step alone. ### Profiles A session holds its profile from `open` until it ends or is closed. A browser step naming that profile fails at once, naming the session and how to close it, and a session cannot open on a profile a running step uses. ### Platforms Sessions work on Linux, macOS, and Windows. On Windows, a session opened from a job object that ends its processes when it closes warns that its browser closes with that job. ## Errors | Code | Meaning | |---|---| | `invalid_input` | A flag, the operation, or a variable is invalid | | `session_not_found` | No session with the ID exists on this host | | `session_busy` | Another command holds the session | | `session_ended` | The session's browser has closed | | `model_required` | The operation needs a model the session lacks | | `profile_in_use` | A step or another session holds the profile | | `launch_failed` | The browser could not start | | `operation_failed` | The operation failed; the session stays open | | `export_invalid` | The exported step would not pass its DAG's checks | | `failed` | Any other failure | ## Examples ```sh dagu browser session open https://portal.example.com/login --provider openai --model gpt-5-mini echo '{"act": "Type %user% into the Username field", "variables": {"user": "alice"}}' | dagu browser session do ab2cd3ef4g echo '{"act": "Type %password% into the Password field", "variables": {"password": {"env": "PORTAL_PASSWORD"}}}' | dagu browser session do ab2cd3ef4g echo '{"act": "Click the Sign in button"}' | dagu browser session do ab2cd3ef4g dagu browser session describe ab2cd3ef4g --find Orders dagu browser session export ab2cd3ef4g --dag orders --step fetch --format yaml dagu browser session close ab2cd3ef4g ``` ## Conformance `conformance/spec079_browser_session/` runs each session command as its own process against a real Chrome, a local portal, and a scripted model. It works the portal through a failed operation, exports the step, and checks it against a static fixture, whose first run then makes no act model request; the password never reaches the model or the output. It uploads a file into a file input by its ID without the model and checks that the exported step's first run replays it. It checks the profile a session holds, a session ending once idle with its history still exported, a session without a model, and input refused before a browser starts.