UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

220 lines (169 loc) • 14.4 kB
# Browser automation Give agents a real browser without giving them your desktop. Playwright testing and verification use [Obscura](https://github.com/h4ckf0r0day/obscura) by default. Steel remains for human takeover, MFA, and the live session viewer. Explore pages with `agent-browser`, and hand a Steel session to a person for login, MFA, or consent. This page is for developers and operators. The `browser` mode names this page as its context file (`kxm explain --mode browser` counts it), so it stays short and procedure-first. ## Before you begin - For Playwright: Node, and `node scripts/obscura.mjs` (it downloads pinned Obscura v0.2.3). [ADR-0005](../adr/ADR-0005-obscura-default-playwright.md) records that default. - For Steel: `kxm` 0.7.135 or newer. [ADR-0007](../adr/ADR-0007-steel-caddy-authentik.md) is the current server. [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.md) is the superseded Kubernetes deployment. - `curl` and `jq`. Optionally `agent-browser` for exploration. Playwright tests use Obscura; do not run `playwright install`. - The 1Password CLI (`op`) for the `svc-steel` credential. Read it at runtime. Do not write it to disk. - The `kxm-browser-*` skills from the plugin or Pi package. See [Agent skills](agent-skills.md#browser-automation-skills). ## Components | Component | Role | |---|---| | Obscura | Default headless browser for Playwright (`chromium.connectOverCDP`) | | Steel | Isolated Chromium sessions, a REST API, a CDP WebSocket, and a session viewer for takeover | | `agent-browser` | Fast, token-efficient exploration: accessibility snapshots, navigation, DOM inspection | | Playwright | Assertions, bug reproductions, visual proof and permanent regression tests | | Secret manager | The only place the Authentik app password and site credentials live | | Human operator | Completes MFA, CAPTCHA, SSO or consent in the Steel session viewer | ## Run Playwright on Obscura `resolveBrowserCdpEndpoint()` returns the Obscura URL unless `KXM_BROWSER=steel`. The launcher and the settings are in [How do I connect Playwright to Obscura?](../kb/how-to-connect-playwright-to-obscura.md) and [Browser settings](../reference/configuration.md#browser-automation). ```bash node scripts/obscura.mjs --ensure npm run e2e ``` Set `video: "off"` in Playwright. Obscura does not record video. Connect with the worker-scoped `browser` fixture and `connectBrowserOverCdp()`. That helper calls `chromium.connectOverCDP()` with no Steel headers unless `KXM_BROWSER=steel`. `chromium.connect` and `use.connectOptions` are not supported. ## Configure the Steel endpoint The KXM browser library reads these variables, and the shell procedure below uses the same names so both agree. The Steel server is `https://steel.kontextmind.com`. `steel.theneuro.me` is an alias of that same server. The only path is Caddy on `kxmd-proxy` (VM 230) with Authentik forward auth. Direct LAN, tailnet, and host-forward connections are blocked. Unauthenticated requests receive a 302 redirect to `id.kxmd.dev`. Sessions return `websocketUrl` `wss://steel.kontextmind.com/`. The previous value was `ws://steel-browser/`. Connect Chrome DevTools Protocol at `/v1/devtools` and send `Authorization` on the handshake. The URL never carries a credential. Authentik accepts the `svc-steel` credential as `Authorization: Basic`. A Bearer token is refused. These groups may connect: `steel-users`, `kxmd-users`, `kxmd-admins`, and `kxmd-owners`. Steel needs `kxm` 0.7.135 or newer. | Variable | Default | Effect | |---|---|---| | `STEEL_API_URL` | `https://steel.kontextmind.com` | Base URL of the Steel API | | `STEEL_UI_URL` | `$STEEL_API_URL/ui` | Base URL of the session viewer | | `STEEL_AUTH_HEADER` | unset | Full `Authorization` value. First in the precedence below | | `STEEL_AUTH_BASIC` | unset | `base64(user:token)`, with or without a leading `Basic` prefix | | `STEEL_AUTH_USER` | unset | Authentik username `svc-steel`. Used with `STEEL_AUTH_TOKEN` | | `STEEL_AUTH_TOKEN` | unset | Authentik app password. Used with `STEEL_AUTH_USER` | | `STEEL_API_KEY` | unset | Deprecated. Steel and Caddy do not enforce it. See the migration note | | `USE_PASS_CLI` | enabled | Turns the legacy `STEEL_API_KEY` lookup off when set to `false` | Set one Authentik credential. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` together with `STEEL_AUTH_TOKEN`. Those three override `STEEL_API_KEY`. If only one of the user or token pair is set, configuration fails closed. When any Authentik variable is set, the legacy key is not sent. > [!WARNING] > `STEEL_API_KEY` is deprecated. Steel and Caddy do not enforce it. A client that still sends `x-steel-api-key` or `?apiKey=` is not authenticated. Migrate by setting one Authentik variable above and removing `STEEL_API_KEY`. Read the credential with `op read`. Never write it to disk, a URL, a prompt, or a log. ```bash # Optional. This is already the default. export STEEL_API_URL="https://steel.kontextmind.com" # Vault kontextmind, item "Steel (svc-steel)", field basic_auth. # The value stays in this process. Do not redirect it to a file. export STEEL_AUTH_BASIC="$(op read 'op://kontextmind/Steel (svc-steel)/basic_auth')" ``` `STEEL_AUTH_USER` is `svc-steel` when you set the user and token pair instead of `STEEL_AUTH_BASIC`. Keep the value in the environment of the process that calls Steel. | Endpoint | Purpose | |---|---| | `POST /v1/sessions` | Create a session; body `{"timeout": <ms>}` | | `GET /v1/sessions/<id>` | Inspect one session; `GET /v1/sessions` lists them all | | `POST /v1/sessions/<id>/release` | Release a session | | `POST /v1/scrape`, `POST /v1/screenshot` | One-shot page fetch or screenshot without a session | | `wss://steel.kontextmind.com/v1/devtools?sessionId=<id>` | CDP path. Send `Authorization` on the handshake. The session `websocketUrl` is `wss://steel.kontextmind.com/` | | `$STEEL_UI_URL?sessionId=<id>` | Session viewer for human takeover | ## Run a browser task 1. Define a helper that sends `Authorization` on stdin, so the secret never appears in a process list. `STEEL_API_URL` defaults to `https://steel.kontextmind.com` when unset: ```bash steel() { # usage: steel METHOD PATH [JSON-BODY] local header args args=(-sS -X "$1" "${STEEL_API_URL:-https://steel.kontextmind.com}$2" -H @- -H 'Content-Type: application/json') if [ -n "${3:-}" ]; then args+=(-d "$3"); fi if [ -n "${STEEL_AUTH_HEADER:-}" ]; then header="$STEEL_AUTH_HEADER" elif [ -n "${STEEL_AUTH_BASIC:-}" ]; then case "$STEEL_AUTH_BASIC" in Basic\ *) header="$STEEL_AUTH_BASIC" ;; *) header="Basic $STEEL_AUTH_BASIC" ;; esac else header="Basic $(printf '%s:%s' "$STEEL_AUTH_USER" "$STEEL_AUTH_TOKEN" | base64 | tr -d '\n')" fi printf 'Authorization: %s\n' "$header" | curl "${args[@]}" } ``` 2. Create a session. Use one session per task; 300,000 ms (5 minutes) is the default. Your Steel deployment sets the maximum; the skills assume 30 minutes. ```bash SESSION_ID=$(steel POST /v1/sessions '{"timeout": 300000}' | jq -r .id) echo "Session: $SESSION_ID" ``` 3. Playwright tests use Obscura. To drive this takeover session, set `KXM_BROWSER=steel` and pass the headers from `formatCDPConnect()` into `chromium.connectOverCDP`. `connectBrowserOverCdp()` does that when `KXM_BROWSER=steel`. `agent-browser --cdp` accepts a URL only and cannot send that header. Do not put the credential in the URL. Check the session's state with `steel GET "/v1/sessions/$SESSION_ID"`. ```typescript import { chromium } from "playwright"; import { formatCDPConnect, resolveSteelConfig } from "@kontextmind/kxm/runtime"; const { url, headers } = formatCDPConnect( { id: sessionId, websocketUrl: "" }, resolveSteelConfig(), ); const browser = await chromium.connectOverCDP(url, { headers }); ``` 4. For a quick fetch that needs no session, scrape instead: ```bash steel POST /v1/scrape '{"url": "https://example.com"}' ``` 5. Release the session when the task ends, even when it failed. Disconnecting a client does not release the session; it stays open for a human until it is released or times out. ```bash steel POST "/v1/sessions/$SESSION_ID/release" ``` <details><summary>PowerShell</summary> ```powershell $pair = "{0}:{1}" -f $env:STEEL_AUTH_USER, $env:STEEL_AUTH_TOKEN $basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair)) $headers = @{ Authorization = "Basic $basic" } $session = Invoke-RestMethod -Method Post -Uri "$env:STEEL_API_URL/v1/sessions" -Headers $headers -ContentType "application/json" -Body '{"timeout": 300000}' Invoke-RestMethod -Method Post -Uri "$env:STEEL_API_URL/v1/sessions/$($session.id)/release" -Headers $headers ``` </details> ## Hand control to a human When a site asks for MFA, a CAPTCHA, SSO or sensitive consent, the agent stops and a person takes over. The diagram shows who controls the session in each state. ```mermaid stateDiagram-v2 [*] --> AGENT_CONTROL: session created AGENT_CONTROL --> HUMAN_CONTROL: login, MFA, CAPTCHA or consent needed HUMAN_CONTROL --> VERIFY_AUTHENTICATION: operator confirms completion VERIFY_AUTHENTICATION --> AGENT_CONTROL: agent confirms signed-in state AGENT_CONTROL --> RELEASED: task done HUMAN_CONTROL --> RELEASED: operator abandons RELEASED --> [*] ``` 1. **Pause.** The agent stops all automated input and says why it needs a person. 2. **Share the viewer link.** It gives the operator `$STEEL_UI_URL?sessionId=<id>`, never the CDP URL. 3. **Operator acts.** The operator opens the viewer, completes the step, and confirms in the terminal, for example `auth complete`. 4. **Verify, then resume.** The agent inspects the page, confirms the signed-in state, refreshes its DOM snapshot, and only then continues. The KXM browser library tracks these states in the process that owns the session. Steel itself does not know them; in a shell-driven task they are a protocol the agent announces. Only one controller, agent or human, acts at a time. ## Control cost and resources - **Timeouts.** Sessions end on their own when the timeout expires. Set a longer timeout at creation when a person will need time for a login step. - **Orphan sweeps.** List sessions with `steel GET /v1/sessions` and release any you do not track. The library flags untracked sessions older than 10 minutes, and tracked ones idle that long unless a human holds them. - **Shared memory.** Give Chromium a large `/dev/shm` on Steel LXC 240, or tabs crash. - **No shared profiles.** Concurrent sessions must not write to the same browser profile. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `302` to `id.kxmd.dev` | The request had no Authentik credential | Export `STEEL_AUTH_BASIC` from `op read`, or the user and token pair. A Bearer token is not accepted | | `401` or `403` | Wrong `svc-steel` credential, or the account is outside the allowed groups | Re-read `basic_auth` with `op read`. `STEEL_API_KEY` does not authenticate | | Connection refused on a LAN, tailnet, or host-forward address | Those paths are blocked | Use `https://steel.kontextmind.com` through Caddy | | `websocketUrl` is `ws://steel-browser/` | The client is older than `kxm` 0.7.135 | Upgrade `kxm`. The server returns `wss://steel.kontextmind.com/` | | Requests go to an unexpected host | `STEEL_API_URL` points somewhere else | Unset it to use `https://steel.kontextmind.com`, or set that URL | | Playwright opens a local browser | The client called `chromium.launch()` or `chromium.connect()` | Use the worker-scoped fixture and `connectOverCDP` against Obscura | | Steel CDP connects without auth | `connectOverCDP` was called with only the URL | Call `connectOverCDP(url, { headers })` with the headers from `formatCDPConnect()` or `connectBrowserOverCdp()` | | `Access to private/internal IP address` | Obscura was started without `--allow-private-network` | Run `node scripts/obscura.mjs`, which passes that flag | | Signed-in state is gone | The session expired or was released | Create a new session and repeat the takeover | | The viewer shows the page but clicks do nothing | The viewer is a screencast, and some capture modes do not forward clicks | Use the DevTools inspector at `$STEEL_API_URL/v1/devtools/inspector.html`, with the agent paused | ## Knowledge base - [How are credentials retrieved without exposing them to the model?](../kb/how-credentials-retrieved-safely.md) - [How do I capture a UI section and annotate changes for an agent?](../kb/how-to-capture-and-annotate-section.md) - [How do I connect Playwright to Obscura?](../kb/how-to-connect-playwright-to-obscura.md) - [How do I connect Playwright to the existing Steel session?](../kb/how-to-connect-playwright-to-steel.md) - [How do I recover an expired session or remove an orphaned browser?](../kb/how-to-recover-expired-session-or-orphan.md) - [How does an agent resume after MFA?](../kb/how-to-resume-after-mfa.md) - [How do I take over a browser session to log in?](../kb/how-to-take-over-session.md) - [Why did authentication disappear?](../kb/why-authentication-disappeared.md) - [Why did automation open a different browser?](../kb/why-automation-opened-different-browser.md) - [Why can I view a session but not control it?](../kb/why-session-viewer-cannot-control.md) ## Prompt templates - [Starting browser work](../prompts/browser-start.md) - [Exploring an application](../prompts/browser-explore.md) - [Requesting human takeover](../prompts/browser-takeover.md) - [Diagnosing and recovering a failed session](../prompts/browser-diagnose-recover.md) - [Reproducing a UI bug and writing a Playwright test](../prompts/browser-repro-fix.md) - [Capturing UI section annotations](../prompts/browser-annotate-feedback.md) ## Next steps - The seven browser skills: [Agent skills](agent-skills.md#browser-automation-skills) - Why Obscura is the Playwright default: [ADR-0005](../adr/ADR-0005-obscura-default-playwright.md) - Where Steel runs now: [ADR-0007](../adr/ADR-0007-steel-caddy-authentik.md) - The superseded Kubernetes deployment: [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.md) - Estimate the `browser` mode's prompt footprint: [`kxm explain`](../reference/cli-reference.md#kxm-explain)