@kontextmind/kxm 0.7.133 → 0.7.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +11 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +3 -2
- package/docs/guides/agent-skills.md +1 -1
- package/docs/guides/browser-automation.md +44 -17
- package/docs/kb/how-credentials-retrieved-safely.md +19 -8
- package/docs/kb/how-to-connect-playwright-to-steel.md +28 -9
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +12 -11
- package/docs/kb/why-automation-opened-different-browser.md +10 -7
- package/docs/prompts/browser-diagnose-recover.md +2 -2
- package/docs/prompts/browser-start.md +3 -3
- package/docs/reference/configuration.md +9 -5
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +164 -30
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +3 -3
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +7 -6
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +19 -8
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +11 -8
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +19 -4
- package/plugins/kxm/src/browser.ts +257 -31
- package/plugins/kxm/src/mcp-server.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -140,6 +140,17 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
140
140
|
|
|
141
141
|
### Changed
|
|
142
142
|
|
|
143
|
+
- **Steel clients authenticate to Authentik with `Authorization: Basic`.**
|
|
144
|
+
`STEEL_AUTH_BASIC`, or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, set that
|
|
145
|
+
header on Steel HTTP requests and on the CDP options from `formatCDPConnect()`.
|
|
146
|
+
`KXM_BROWSER=steel` passes those headers through `connectBrowserOverCdp()`
|
|
147
|
+
into `chromium.connectOverCDP`. Obscura stays the default and sends no Steel
|
|
148
|
+
headers. `STEEL_AUTH_HEADER` overrides the value. The CDP URL omits the credential when
|
|
149
|
+
those variables are set. A 302 to the identity provider fails closed and does
|
|
150
|
+
not follow the login redirect. `STEEL_API_KEY` still sends the legacy
|
|
151
|
+
`x-steel-api-key` header and `apiKey` query parameter for the temporary proxy
|
|
152
|
+
shim, and warns once. See
|
|
153
|
+
[Browser automation](docs/guides/browser-automation.md).
|
|
143
154
|
- **Dispatch reads role and model files, and agents bind a role.**
|
|
144
155
|
`scripts/roster-policy.mjs` builds the developer policy from
|
|
145
156
|
`.kxm/models/*.yaml` and `.kxm/roles/*.yaml` at `refs/remotes/origin/main`.
|
|
@@ -7,7 +7,7 @@ project: "kxm"
|
|
|
7
7
|
status: "accepted"
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-14"
|
|
10
|
-
updated: "2026-09-
|
|
10
|
+
updated: "2026-09-27"
|
|
11
11
|
authority: "decision"
|
|
12
12
|
confidence: "verified"
|
|
13
13
|
summary: "Adopt self-hosted Steel on DigitalOcean Kubernetes (DOKS) with agent-browser and Playwright as KXM's primary browser automation infrastructure."
|
|
@@ -100,7 +100,8 @@ AI coding agents and orchestration workflows in KXM require browser interaction
|
|
|
100
100
|
|
|
101
101
|
- **Verification**: Health endpoint `$STEEL_API_URL/v1/health` verified with HTTP 200 and Let's Encrypt TLS.
|
|
102
102
|
- **Integration Test**: `test/core/browser.test.ts` validates session lifecycle, CDP endpoint formatting, takeover transitions, and secret redaction.
|
|
103
|
-
- **Security Check**: `pass-cli` verified as the authoritative store for
|
|
103
|
+
- **Security Check**: `pass-cli` verified as the authoritative store for Steel credentials in the operators' password manager.
|
|
104
|
+
- **Edge auth (2026-09-27)**: `steel.kontextmind.com` and `steel.theneuro.me`, including the CDP WebSocket, are behind Authentik forward auth. Steel does not check `STEEL_API_KEY`. Clients send `Authorization: Basic`. Unauthenticated requests are redirected to `id.kxmd.dev`. The legacy `x-steel-api-key` header and `apiKey` query parameter remain a temporary proxy shim.
|
|
104
105
|
|
|
105
106
|
## Related
|
|
106
107
|
|
|
@@ -68,7 +68,7 @@ Tools map the same way: peer tools to `kxm-peer`, workflow tools to `kxm-workflo
|
|
|
68
68
|
|
|
69
69
|
## Browser automation skills
|
|
70
70
|
|
|
71
|
-
Playwright testing and verification use Obscura. The Steel skills cover human takeover, MFA, and the live session viewer. They own no `kxm` command. See [Browser automation](browser-automation.md)
|
|
71
|
+
Playwright testing and verification use Obscura ([ADR-0005](../adr/ADR-0005-obscura-default-playwright.md)). The Steel skills cover human takeover, MFA, and the live session viewer. Those hosts sit behind Authentik forward auth: send `Authorization: Basic` (`STEEL_AUTH_BASIC`, or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`) and keep the credential out of URLs. They own no `kxm` command. See [Browser automation](browser-automation.md) and [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.md).
|
|
72
72
|
|
|
73
73
|
| Skill | Use it to |
|
|
74
74
|
|---|---|
|
|
@@ -5,9 +5,9 @@ Give agents a real browser without giving them your desktop. Playwright testing
|
|
|
5
5
|
## Before you begin
|
|
6
6
|
|
|
7
7
|
- 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.
|
|
8
|
-
- For takeover: a Steel deployment you operate, reachable over HTTPS, and
|
|
8
|
+
- For takeover: a Steel deployment you operate, reachable over HTTPS, and an Authentik app password when that host is behind forward auth. [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.md) describes the reference deployment on Kubernetes.
|
|
9
9
|
- `curl` and `jq`. Optionally `agent-browser` for exploration. Playwright tests use Obscura; do not run `playwright install`.
|
|
10
|
-
- A secret manager for the
|
|
10
|
+
- A secret manager for the Authentik app password and site credentials. The bundled skills use `pass-cli`.
|
|
11
11
|
- The `kxm-browser-*` skills from the plugin or Pi package. See [Agent skills](agent-skills.md#browser-automation-skills).
|
|
12
12
|
|
|
13
13
|
## Components
|
|
@@ -18,7 +18,7 @@ Give agents a real browser without giving them your desktop. Playwright testing
|
|
|
18
18
|
| Steel | Isolated Chromium sessions, a REST API, a CDP WebSocket, and a session viewer for takeover |
|
|
19
19
|
| `agent-browser` | Fast, token-efficient exploration: accessibility snapshots, navigation, DOM inspection |
|
|
20
20
|
| Playwright | Assertions, bug reproductions, visual proof and permanent regression tests |
|
|
21
|
-
| Secret manager | The only place the
|
|
21
|
+
| Secret manager | The only place the Authentik app password and site credentials live |
|
|
22
22
|
| Human operator | Completes MFA, CAPTCHA, SSO or consent in the Steel session viewer |
|
|
23
23
|
|
|
24
24
|
## Run Playwright on Obscura
|
|
@@ -30,49 +30,60 @@ node scripts/obscura.mjs --ensure
|
|
|
30
30
|
npm run e2e
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Set `video: "off"` in Playwright. Obscura does not record video. Connect with the worker-scoped `browser` fixture and `chromium.connectOverCDP()`. `chromium.connect` and `use.connectOptions` are not supported.
|
|
33
|
+
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.
|
|
34
34
|
|
|
35
35
|
## Configure the Steel endpoint
|
|
36
36
|
|
|
37
37
|
The KXM browser library reads these variables, and the shell procedure below uses the same names so both agree.
|
|
38
38
|
|
|
39
|
+
KontextMind's Steel hosts (`steel.kontextmind.com` and `steel.theneuro.me`, including the `wss://` CDP endpoint) sit behind Authentik forward auth at the reverse proxy. Steel itself does not check an API key. Unauthenticated requests receive a 302 redirect to the Authentik login at `id.kxmd.dev`. Authentik accepts an app password only as `Authorization: Basic`. A Bearer token is refused.
|
|
40
|
+
|
|
39
41
|
| Variable | Default | Effect |
|
|
40
42
|
|---|---|---|
|
|
41
43
|
| `STEEL_API_URL` | A KontextMind-operated deployment | Base URL of your Steel API. Always set it |
|
|
42
44
|
| `STEEL_UI_URL` | `$STEEL_API_URL/ui` | Base URL of the session viewer |
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
+
| `STEEL_AUTH_HEADER` | unset | Full `Authorization` value. Wins over the other auth variables |
|
|
46
|
+
| `STEEL_AUTH_BASIC` | unset | `base64(user:token)`, with or without a leading `Basic` prefix. Sent as `Authorization: Basic` |
|
|
47
|
+
| `STEEL_AUTH_USER` | unset | Authentik username, for example `svc-steel`. Used with `STEEL_AUTH_TOKEN` |
|
|
48
|
+
| `STEEL_AUTH_TOKEN` | unset | Authentik app password. Used with `STEEL_AUTH_USER` |
|
|
49
|
+
| `STEEL_API_KEY` | A `pass-cli` lookup | Deprecated. Sent as `x-steel-api-key` and as `?apiKey=` on the CDP URL, which the proxy still accepts as a temporary shim. The library warns once on stderr |
|
|
50
|
+
| `USE_PASS_CLI` | enabled | Set to `false` to disable the legacy `STEEL_API_KEY` `pass-cli` fallback |
|
|
51
|
+
|
|
52
|
+
Set one Authentik credential. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` together with `STEEL_AUTH_TOKEN`. If only one of the user or token pair is set, configuration fails instead of falling back to the legacy key. When any of those are set, the legacy key is not sent and is not placed in a URL. The `pass-cli` fallback looks up `STEEL_API_KEY` only.
|
|
45
53
|
|
|
46
54
|
> [!WARNING]
|
|
47
|
-
> Set `STEEL_API_URL` and
|
|
55
|
+
> Set `STEEL_API_URL` and an Authentik credential explicitly. Without them the library falls back to KontextMind's own deployment and vault item, which are not yours to use. Never put the credential in a URL, a prompt, or a log.
|
|
48
56
|
|
|
49
57
|
```bash
|
|
50
58
|
export STEEL_API_URL="https://steel.example.com"
|
|
51
|
-
# Read the
|
|
52
|
-
export
|
|
59
|
+
# Read the app password from your secret manager; <vault> and <item> are yours.
|
|
60
|
+
export STEEL_AUTH_USER="svc-steel"
|
|
61
|
+
export STEEL_AUTH_TOKEN="$(pass-cli item view --vault-name '<vault>' --item-title '<item>' --field password)"
|
|
53
62
|
export USE_PASS_CLI=false
|
|
54
63
|
```
|
|
55
64
|
|
|
65
|
+
`STEEL_AUTH_BASIC` is the same pair already encoded: `printf '%s:%s' "$STEEL_AUTH_USER" "$STEEL_AUTH_TOKEN" | base64 | tr -d '\n'`. Prefer the user and token pair, or the pre-encoded value, and keep them in the environment of the process that calls Steel.
|
|
66
|
+
|
|
56
67
|
| Endpoint | Purpose |
|
|
57
68
|
|---|---|
|
|
58
69
|
| `POST /v1/sessions` | Create a session; body `{"timeout": <ms>}` |
|
|
59
70
|
| `GET /v1/sessions/<id>` | Inspect one session; `GET /v1/sessions` lists them all |
|
|
60
71
|
| `POST /v1/sessions/<id>/release` | Release a session |
|
|
61
72
|
| `POST /v1/scrape`, `POST /v1/screenshot` | One-shot page fetch or screenshot without a session |
|
|
62
|
-
| `wss://<steel-host>/v1/devtools?sessionId=<id
|
|
73
|
+
| `wss://<steel-host>/v1/devtools?sessionId=<id>` | CDP endpoint. Send `Authorization` on the WebSocket handshake; do not add the credential to this URL |
|
|
63
74
|
| `$STEEL_UI_URL?sessionId=<id>` | Session viewer for human takeover |
|
|
64
75
|
|
|
65
|
-
The CDP URL carries the API key in its query string. Treat it as a secret: never paste it into chat, a prompt, an issue or a log.
|
|
66
|
-
|
|
67
76
|
## Run a browser task
|
|
68
77
|
|
|
69
|
-
1. Define a helper that sends
|
|
78
|
+
1. Define a helper that sends `Authorization` on stdin, so the secret never appears in a process list:
|
|
70
79
|
|
|
71
80
|
```bash
|
|
72
81
|
steel() { # usage: steel METHOD PATH [JSON-BODY]
|
|
82
|
+
local basic
|
|
73
83
|
local args=(-sS -X "$1" "$STEEL_API_URL$2" -H @- -H 'Content-Type: application/json')
|
|
74
84
|
if [ -n "${3:-}" ]; then args+=(-d "$3"); fi
|
|
75
|
-
printf '
|
|
85
|
+
basic="$(printf '%s:%s' "$STEEL_AUTH_USER" "$STEEL_AUTH_TOKEN" | base64 | tr -d '\n')"
|
|
86
|
+
printf 'Authorization: Basic %s\n' "$basic" | curl "${args[@]}"
|
|
76
87
|
}
|
|
77
88
|
```
|
|
78
89
|
|
|
@@ -83,7 +94,19 @@ The CDP URL carries the API key in its query string. Treat it as a secret: never
|
|
|
83
94
|
echo "Session: $SESSION_ID"
|
|
84
95
|
```
|
|
85
96
|
|
|
86
|
-
3.
|
|
97
|
+
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"`.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { chromium } from "playwright";
|
|
101
|
+
import { formatCDPConnect, resolveSteelConfig } from "@kontextmind/kxm/runtime";
|
|
102
|
+
|
|
103
|
+
const { url, headers } = formatCDPConnect(
|
|
104
|
+
{ id: sessionId, websocketUrl: "" },
|
|
105
|
+
resolveSteelConfig(),
|
|
106
|
+
);
|
|
107
|
+
const browser = await chromium.connectOverCDP(url, { headers });
|
|
108
|
+
```
|
|
109
|
+
|
|
87
110
|
4. For a quick fetch that needs no session, scrape instead:
|
|
88
111
|
|
|
89
112
|
```bash
|
|
@@ -99,7 +122,9 @@ The CDP URL carries the API key in its query string. Treat it as a secret: never
|
|
|
99
122
|
<details><summary>PowerShell</summary>
|
|
100
123
|
|
|
101
124
|
```powershell
|
|
102
|
-
$
|
|
125
|
+
$pair = "{0}:{1}" -f $env:STEEL_AUTH_USER, $env:STEEL_AUTH_TOKEN
|
|
126
|
+
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
|
|
127
|
+
$headers = @{ Authorization = "Basic $basic" }
|
|
103
128
|
$session = Invoke-RestMethod -Method Post -Uri "$env:STEEL_API_URL/v1/sessions" -Headers $headers -ContentType "application/json" -Body '{"timeout": 300000}'
|
|
104
129
|
Invoke-RestMethod -Method Post -Uri "$env:STEEL_API_URL/v1/sessions/$($session.id)/release" -Headers $headers
|
|
105
130
|
```
|
|
@@ -139,9 +164,11 @@ The KXM browser library tracks these states in the process that owns the session
|
|
|
139
164
|
|
|
140
165
|
| Symptom | Cause | Fix |
|
|
141
166
|
|---|---|---|
|
|
142
|
-
| `
|
|
167
|
+
| `302` to `id.kxmd.dev` | The request had no Authentik app password | Export `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, or `STEEL_AUTH_BASIC`. A Bearer token is not accepted |
|
|
168
|
+
| `401` or `403` | Wrong app password, or a legacy key the shim rejected | Re-export the Authentik credential from your secret manager. Re-export `STEEL_API_KEY` only when that is the credential you still use |
|
|
143
169
|
| Requests go to an unexpected host | `STEEL_API_URL` is unset | Export it before starting the agent |
|
|
144
170
|
| Playwright opens a local browser | The client called `chromium.launch()` or `chromium.connect()` | Use the worker-scoped fixture and `connectOverCDP` against Obscura |
|
|
171
|
+
| Steel CDP connects without auth | `connectOverCDP` was called with only the URL | Call `connectOverCDP(url, { headers })` with the headers from `formatCDPConnect()` or `connectBrowserOverCdp()` |
|
|
145
172
|
| `Access to private/internal IP address` | Obscura was started without `--allow-private-network` | Run `node scripts/obscura.mjs`, which passes that flag |
|
|
146
173
|
| Signed-in state is gone | The session expired or was released | Create a new session and repeat the takeover |
|
|
147
174
|
| 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 |
|
|
@@ -7,10 +7,10 @@ project: "kxm"
|
|
|
7
7
|
status: "accepted"
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-14"
|
|
10
|
-
updated: "2026-09-
|
|
10
|
+
updated: "2026-09-27"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "verified"
|
|
13
|
-
summary: "How the Steel client resolves
|
|
13
|
+
summary: "How the Steel client resolves Authentik Basic auth and how browser work keeps secrets out of model context."
|
|
14
14
|
tags: ["browser", "credentials", "pass-cli", "security"]
|
|
15
15
|
related: ["docs/guides/browser-automation.md", "docs/guides/agent-skills.md"]
|
|
16
16
|
---
|
|
@@ -21,7 +21,7 @@ Browser automation keeps passwords, MFA secrets and API tokens out of model
|
|
|
21
21
|
prompts and reasoning traces. The model sees references to credentials, never
|
|
22
22
|
their values.
|
|
23
23
|
|
|
24
|
-
## How the Steel client finds its
|
|
24
|
+
## How the Steel client finds its credential
|
|
25
25
|
|
|
26
26
|
`resolveSteelConfig()` resolves each setting in this order, and never writes a
|
|
27
27
|
secret to disk or to a log:
|
|
@@ -29,12 +29,22 @@ secret to disk or to a log:
|
|
|
29
29
|
| Setting | Resolved from |
|
|
30
30
|
|---|---|
|
|
31
31
|
| API URL | An explicit override, then `STEEL_API_URL`, then a built-in default |
|
|
32
|
-
|
|
|
32
|
+
| Authentik `Authorization` | `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN` |
|
|
33
|
+
| Legacy API key | An explicit override, then `STEEL_API_KEY`, then a `pass-cli` lookup, and only when no Authentik credential is set |
|
|
33
34
|
| Session viewer URL | An explicit override, then `STEEL_UI_URL`, then `<api-url>/ui` |
|
|
34
35
|
|
|
36
|
+
KontextMind's Steel hosts are behind Authentik forward auth. The client sends
|
|
37
|
+
`Authorization: Basic` on every HTTP request and on the CDP WebSocket. A Bearer
|
|
38
|
+
token is not accepted. The legacy key is sent as `x-steel-api-key` and as an
|
|
39
|
+
`apiKey` query parameter, which the proxy still accepts as a temporary shim.
|
|
40
|
+
The library writes one deprecation warning to stderr and does not print the
|
|
41
|
+
key. When an Authentik variable is set, the key is not sent and is not placed
|
|
42
|
+
in a URL.
|
|
43
|
+
|
|
35
44
|
The built-in default URL and the `pass-cli` lookup point at the maintainers'
|
|
36
|
-
own Steel deployment and vault.
|
|
37
|
-
|
|
45
|
+
own Steel deployment and vault. The lookup reads `STEEL_API_KEY` only. Set
|
|
46
|
+
`STEEL_API_URL` and an Authentik credential for yours, and set
|
|
47
|
+
`USE_PASS_CLI=false` to turn the lookup off.
|
|
38
48
|
|
|
39
49
|
## Mechanisms
|
|
40
50
|
|
|
@@ -50,8 +60,9 @@ yours, and set `USE_PASS_CLI=false` to turn the lookup off.
|
|
|
50
60
|
2. **References, not values.** A prompt receives a credential reference such
|
|
51
61
|
as `vault: "<vault>", item: "<item>"`, never the secret itself.
|
|
52
62
|
3. **Log sanitization.** The KXM browser client redacts `apiKey=` query values,
|
|
53
|
-
`
|
|
54
|
-
password before it logs or
|
|
63
|
+
`Authorization` header values, `Basic` credentials, `steel_…` keys, and any
|
|
64
|
+
field named like a key, secret, token, auth or password before it logs or
|
|
65
|
+
returns output.
|
|
55
66
|
4. **Human handoff for high-privilege sign-in.** For production accounts or
|
|
56
67
|
MFA, the agent does not touch the credential at all. It follows the
|
|
57
68
|
`kxm-browser-takeover` skill and lets you sign in directly.
|
|
@@ -20,33 +20,52 @@ related: ["docs/guides/browser-automation.md", "docs/kb/how-to-connect-playwrigh
|
|
|
20
20
|
Obscura is the default for Playwright testing and verification. Use this page when `KXM_BROWSER=steel` and you are attaching to a live Steel session for human takeover, MFA, or the session viewer. The default path is [How do I connect Playwright to Obscura?](how-to-connect-playwright-to-obscura.md).
|
|
21
21
|
|
|
22
22
|
Run Playwright against a remote Steel session instead of a local browser by
|
|
23
|
-
connecting over the Chrome DevTools Protocol (CDP).
|
|
23
|
+
connecting over the Chrome DevTools Protocol (CDP). When `KXM_BROWSER=steel`,
|
|
24
|
+
`resolveBrowserCdpEndpoint(session)` returns the same URL as `formatCDPEndpoint()`.
|
|
25
|
+
The handshake needs the headers from `resolveBrowserCdpConnect(session)` or
|
|
26
|
+
`formatCDPConnect()`. `connectBrowserOverCdp()` passes those headers into
|
|
27
|
+
`chromium.connectOverCDP`.
|
|
24
28
|
|
|
25
29
|
## 1. Build the CDP endpoint
|
|
26
30
|
|
|
27
|
-
Build the WebSocket CDP URL from the active session ID
|
|
28
|
-
|
|
31
|
+
Build the WebSocket CDP URL from the active session ID, and take the headers
|
|
32
|
+
with it. Authentik forward auth accepts the app password only as
|
|
33
|
+
`Authorization: Basic` on the WebSocket handshake. The URL does not carry the
|
|
34
|
+
credential when `STEEL_AUTH_BASIC` or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`
|
|
35
|
+
are set:
|
|
29
36
|
|
|
30
37
|
```typescript
|
|
31
|
-
import {
|
|
38
|
+
import { formatCDPConnect, resolveSteelConfig } from "@kontextmind/kxm/runtime";
|
|
32
39
|
|
|
33
|
-
const
|
|
34
|
-
|
|
40
|
+
const { url, headers } = formatCDPConnect(
|
|
41
|
+
{ id: sessionId, websocketUrl: "" },
|
|
42
|
+
resolveSteelConfig(),
|
|
43
|
+
);
|
|
35
44
|
```
|
|
36
45
|
|
|
37
|
-
The
|
|
46
|
+
The URL has this shape:
|
|
38
47
|
|
|
39
48
|
```text
|
|
40
|
-
wss://<steel-host>/v1/devtools?sessionId=<session-id
|
|
49
|
+
wss://<steel-host>/v1/devtools?sessionId=<session-id>
|
|
41
50
|
```
|
|
42
51
|
|
|
52
|
+
`headers` is `{ Authorization: "Basic <base64>" }`. Pass that object to
|
|
53
|
+
Playwright. Do not log it. A legacy `STEEL_API_KEY` still appends `apiKey` to
|
|
54
|
+
the URL for the temporary proxy shim; prefer the Authentik variables so the
|
|
55
|
+
credential stays out of the URL.
|
|
56
|
+
|
|
43
57
|
## 2. Connect in Playwright
|
|
44
58
|
|
|
45
59
|
```typescript
|
|
46
60
|
import { test, expect, chromium } from "@playwright/test";
|
|
61
|
+
import { formatCDPConnect, resolveSteelConfig } from "@kontextmind/kxm/runtime";
|
|
47
62
|
|
|
48
63
|
test("execute test on remote steel session", async () => {
|
|
49
|
-
const
|
|
64
|
+
const { url, headers } = formatCDPConnect(
|
|
65
|
+
{ id: process.env.STEEL_SESSION_ID!, websocketUrl: "" },
|
|
66
|
+
resolveSteelConfig(),
|
|
67
|
+
);
|
|
68
|
+
const browser = await chromium.connectOverCDP(url, { headers });
|
|
50
69
|
|
|
51
70
|
// Use the existing context and page, or create them.
|
|
52
71
|
const context = browser.contexts()[0] || await browser.newContext();
|
|
@@ -7,7 +7,7 @@ project: "kxm"
|
|
|
7
7
|
status: "accepted"
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-14"
|
|
10
|
-
updated: "2026-09-
|
|
10
|
+
updated: "2026-09-27"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "verified"
|
|
13
13
|
summary: "Find and release stale or orphaned browser sessions on Steel."
|
|
@@ -22,23 +22,23 @@ browser can keep running on your Steel deployment until its timeout.
|
|
|
22
22
|
|
|
23
23
|
## 1. List active sessions
|
|
24
24
|
|
|
25
|
-
Load `STEEL_API_URL` and `
|
|
26
|
-
`pass-cli`, for example:
|
|
25
|
+
Load `STEEL_API_URL`, `STEEL_AUTH_USER`, and `STEEL_AUTH_TOKEN` from your secret
|
|
26
|
+
manager first. With `pass-cli`, for example:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
export STEEL_API_URL="https://<steel-host>"
|
|
30
|
-
|
|
31
|
-
export
|
|
30
|
+
export STEEL_AUTH_USER="svc-steel"
|
|
31
|
+
export STEEL_AUTH_TOKEN="$(pass-cli item view --vault-name "<vault>" --item-title "<item>" --field password)"
|
|
32
|
+
basic="$(printf '%s:%s' "$STEEL_AUTH_USER" "$STEEL_AUTH_TOKEN" | base64 | tr -d '\n')"
|
|
32
33
|
|
|
33
|
-
curl -
|
|
34
|
-
-H "x-steel-api-key: $STEEL_API_KEY" | jq .
|
|
34
|
+
printf 'Authorization: Basic %s\n' "$basic" | curl -sS -H @- "$STEEL_API_URL/v1/sessions" | jq .
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## 2. Release an orphaned session
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
|
|
41
|
-
-
|
|
40
|
+
printf 'Authorization: Basic %s\n' "$basic" \
|
|
41
|
+
| curl -sS -X POST -H @- "$STEEL_API_URL/v1/sessions/<session-id>/release"
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
## 3. Sweep orphans with the KXM client
|
|
@@ -60,5 +60,6 @@ for (const sessionId of orphans) {
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
It returns an empty list when the Steel API
|
|
64
|
-
does not prove there are no orphans.
|
|
63
|
+
It returns an empty list when the Steel API responds with an error status, so
|
|
64
|
+
an empty result does not prove there are no orphans. A 302 from Authentik
|
|
65
|
+
throws instead of looking like an empty list. Check with the `curl` call above.
|
|
@@ -10,7 +10,7 @@ created: "2026-09-14"
|
|
|
10
10
|
updated: "2026-09-27"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "verified"
|
|
13
|
-
summary: "Prevent accidental local browser launches
|
|
13
|
+
summary: "Prevent accidental local browser launches. Playwright attaches to Obscura, or to Steel with Authentik Basic auth when KXM_BROWSER=steel."
|
|
14
14
|
tags: ["browser", "cdp", "playwright", "agent-browser", "troubleshooting"]
|
|
15
15
|
related: ["docs/guides/browser-automation.md", "docs/kb/how-to-connect-playwright-to-obscura.md", "docs/kb/how-to-connect-playwright-to-steel.md"]
|
|
16
16
|
---
|
|
@@ -26,19 +26,22 @@ You expected automation to run on Obscura, or on a Steel takeover session, but a
|
|
|
26
26
|
1. **The script called `chromium.launch()` instead of
|
|
27
27
|
`chromium.connectOverCDP()`.**
|
|
28
28
|
- `chromium.launch()` starts a browser on the local machine.
|
|
29
|
-
- **Fix:**
|
|
29
|
+
- **Fix:** Obscura has no auth headers. Call `connectBrowserOverCdp()`, or `chromium.connectOverCDP()` with `resolveObscuraCdpEndpoint()`. For `KXM_BROWSER=steel`, pass `{ headers }` from `formatCDPConnect()`.
|
|
30
30
|
2. **`agent-browser` ran without `--cdp`.**
|
|
31
31
|
- `agent-browser open <url>` without `--cdp` starts a local headless
|
|
32
32
|
browser.
|
|
33
|
-
- **Fix:**
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
- **Fix:** pass `--cdp` with the Obscura URL from `resolveObscuraCdpEndpoint()`.
|
|
34
|
+
For a Steel takeover session, `agent-browser --cdp` takes a URL only and
|
|
35
|
+
cannot send the Authentik `Authorization` header, so use Playwright and
|
|
36
|
+
`connectBrowserOverCdp()`. Do not put the credential in the URL. See
|
|
36
37
|
[How do I connect Playwright to Obscura?](how-to-connect-playwright-to-obscura.md)
|
|
37
38
|
and
|
|
38
39
|
[How do I connect Playwright to the existing Steel session?](how-to-connect-playwright-to-steel.md).
|
|
39
|
-
3. **The Playwright client had no CDP endpoint.**
|
|
40
|
+
3. **The Playwright client had no CDP endpoint, or Steel auth was missing.**
|
|
40
41
|
- A script that falls back to `chromium.launch()` when no CDP URL is set
|
|
41
42
|
opens a local browser.
|
|
42
43
|
- **Fix:** use `resolveObscuraCdpEndpoint()` (default
|
|
43
44
|
`http://127.0.0.1:9222`). For a Steel takeover session, set
|
|
44
|
-
`KXM_BROWSER=steel` and load `STEEL_API_URL`
|
|
45
|
+
`KXM_BROWSER=steel` and load `STEEL_API_URL` plus `STEEL_AUTH_USER` and
|
|
46
|
+
`STEEL_AUTH_TOKEN` (or `STEEL_AUTH_BASIC`). A legacy `STEEL_API_KEY` still
|
|
47
|
+
works through the proxy shim and warns once.
|
|
@@ -7,7 +7,7 @@ project: "kxm"
|
|
|
7
7
|
status: "accepted"
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-14"
|
|
10
|
-
updated: "2026-09-
|
|
10
|
+
updated: "2026-09-27"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "verified"
|
|
13
13
|
summary: "Troubleshoot unresponsive Steel sessions, CDP attachment errors, auth loops, and orphaned browser containers."
|
|
@@ -53,4 +53,4 @@ Use this prompt to troubleshoot unresponsive sessions, CDP attachment errors, au
|
|
|
53
53
|
|
|
54
54
|
4. **Verify WebSocket / CDP Ingress**:
|
|
55
55
|
- Ensure the reverse proxy or ingress in front of Steel passes WebSocket upgrades through. On Kubernetes with ingress-nginx, that is the `nginx.ingress.kubernetes.io/websocket-services` annotation.
|
|
56
|
-
- If CDP fails with 401,
|
|
56
|
+
- If the response is a 302 to `id.kxmd.dev`, or CDP fails with 401, send `Authorization: Basic` from `STEEL_AUTH_BASIC` or from `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`. A Bearer token is not accepted. Do not put the credential in the URL. The legacy `x-steel-api-key` header and `?apiKey=` query parameter remain only for the temporary proxy shim.
|
|
@@ -7,7 +7,7 @@ project: "kxm"
|
|
|
7
7
|
status: "accepted"
|
|
8
8
|
owner: "@operator"
|
|
9
9
|
created: "2026-09-14"
|
|
10
|
-
updated: "2026-09-
|
|
10
|
+
updated: "2026-09-27"
|
|
11
11
|
authority: "instruction"
|
|
12
12
|
confidence: "verified"
|
|
13
13
|
summary: "Initialize a remote Steel browser session for a project task, verifying credentials, connectivity, and attachment endpoints before automation."
|
|
@@ -42,7 +42,7 @@ Use this prompt to initialize a remote browser session on self-hosted Steel for
|
|
|
42
42
|
## Instructions for the agent
|
|
43
43
|
|
|
44
44
|
1. **Verify Credential Reference**:
|
|
45
|
-
- Resolve the target credentials and `
|
|
45
|
+
- Resolve the target credentials and the Steel Authentik app password (`STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, or `STEEL_AUTH_BASIC`) from the secret manager, for example with `pass-cli`, without logging raw values. A Bearer token is not accepted. Keep the credential in a header, not a URL.
|
|
46
46
|
- Do not print credentials to the chat or save them to tracked files.
|
|
47
47
|
|
|
48
48
|
2. **Launch Remote Steel Session**:
|
|
@@ -50,7 +50,7 @@ Use this prompt to initialize a remote browser session on self-hosted Steel for
|
|
|
50
50
|
- Capture `sessionId`, `websocketUrl`, and `sessionViewerUrl`.
|
|
51
51
|
|
|
52
52
|
3. **Verify Target Endpoint Connectivity**:
|
|
53
|
-
- Connect `agent-browser`
|
|
53
|
+
- Connect Playwright via CDP with the headers from `formatCDPConnect()` (`Authorization: Basic`). Do not put the credential in the URL. `agent-browser --cdp` cannot send that header.
|
|
54
54
|
- Navigate to `{{TARGET_BASE_URL}}` within `{{PERMISSION_LEVEL}}` constraints.
|
|
55
55
|
- If `PERMISSION_LEVEL` is `INSPECT_ONLY`, do not click submit buttons or mutate forms.
|
|
56
56
|
|
|
@@ -32,7 +32,7 @@ Names that start with `KXM_` are not all operator settings. This map covers ever
|
|
|
32
32
|
| Runtime supervisor | `KXM_STATE_HOME`, `KXM_RUNTIME_SYNC_INTERVAL_MS`, `KXM_RUNTIME_STOP_GRACE_MS` | [Runtime supervisor settings](#runtime-supervisor-settings) |
|
|
33
33
|
| Operator CLI and sessions | `KXM_USER_CONFIG_DIR`, `KXM_USER_TELEMETRY_DIR`, `KXM_SESSION_TOKEN`, `KXM_SESSION_BRIEF`, `KXM_WORKFLOW_*`, `GITHUB_TOKEN`, and others | [CLI and session settings](#cli-and-session-settings) |
|
|
34
34
|
| Nous model providers | `KXM_NOUS_PROVIDERS`, `KXM_NOUS_PROXY_URL`, `KXM_NOUS_DISCOVERY_TIMEOUT_MS`, `KXM_NOUS_CATALOG_FILE`, `NOUS_API_KEY` | [Nous providers](../guides/nous-providers.md) |
|
|
35
|
-
| Browser automation | `KXM_BROWSER`, `OBSCURA_CDP_URL`, `OBSCURA_PORT`, `STEEL_API_URL`, `
|
|
35
|
+
| Browser automation | `KXM_BROWSER`, `OBSCURA_CDP_URL`, `OBSCURA_PORT`, `STEEL_API_URL`, `STEEL_UI_URL`, `STEEL_AUTH_HEADER`, `STEEL_AUTH_BASIC`, `STEEL_AUTH_USER`, `STEEL_AUTH_TOKEN`, `STEEL_API_KEY` (legacy), `USE_PASS_CLI` | [Browser settings](#browser-automation) |
|
|
36
36
|
| Set by a harness, not by you | `KXM_PROJECT_DIR` (Claude Code plugin), `KXM_ATTEMPT_TOKEN` (Runtime attempts), `KXM_WORKER_IDENTITY_KEY`, `KXM_WORKER_GENERATION`, `KXM_WORKER_CHILD_INCARCATION`, `KXM_WORKER_SESSION_SCOPE` (worker supervisor to its Pi child) | [Internal variables](#internal-variables) |
|
|
37
37
|
| Maintainer and test only | `KXM_SMOKE*`, `KXM_ASSET*`, `KXM_RELEASE_TAG`, `KXM_PUBLISH_WAIT_MS`, `KXM_DETERMINISTIC_TEST_CLOCK`, `KXM_WORKER_STOP_AFTER_MS`, `KXM_STUDIO_ONCE` | [Development](../contributing/development.md) |
|
|
38
38
|
| Maintainer critic script | `KXM_CRITIC_DIR`, `KXM_REVIEW_TARGET` | [Maintainer critic script](#maintainer-critic-script) |
|
|
@@ -200,17 +200,21 @@ The Runtime supervisor runs `kxm run` workflows and syncs their summaries to the
|
|
|
200
200
|
|
|
201
201
|
## Browser automation
|
|
202
202
|
|
|
203
|
-
Playwright testing and verification use Obscura. Steel is for human takeover, MFA, and the live session viewer. `resolveBrowserCdpEndpoint()` in `plugins/kxm/src/browser.ts`
|
|
203
|
+
Playwright testing and verification use Obscura. Steel is for human takeover, MFA, and the live session viewer. `resolveBrowserCdpEndpoint()` in `plugins/kxm/src/browser.ts` returns the CDP URL. `resolveBrowserCdpConnect()` returns that URL and the handshake headers, and `connectBrowserOverCdp()` passes them to `chromium.connectOverCDP`. `node scripts/obscura.mjs` downloads pinned Obscura v0.2.3 and serves it. See [Browser automation](../guides/browser-automation.md).
|
|
204
204
|
|
|
205
205
|
| Variable | Default | Effect |
|
|
206
206
|
|---|---|---|
|
|
207
|
-
| `KXM_BROWSER` | `obscura` | `obscura` selects the Obscura CDP URL. `steel` selects the
|
|
207
|
+
| `KXM_BROWSER` | `obscura` | `obscura` selects the Obscura CDP URL and sends no Steel headers. `steel` selects `formatCDPConnect()` (the session URL plus `Authorization`, or the legacy API-key header) and requires a session id. Any other value throws |
|
|
208
208
|
| `OBSCURA_CDP_URL` | `http://127.0.0.1:${OBSCURA_PORT:-9222}` | CDP URL passed to `chromium.connectOverCDP()`. When set, it wins over `OBSCURA_PORT` for that URL |
|
|
209
209
|
| `OBSCURA_PORT` | `9222` | TCP port for `obscura serve`. Used in the default CDP URL when `OBSCURA_CDP_URL` is unset. The launcher refuses to start when this port disagrees with the port in `OBSCURA_CDP_URL` |
|
|
210
210
|
| `STEEL_API_URL` | A KontextMind-operated deployment | Base URL of your Steel API. Set it before using `KXM_BROWSER=steel` |
|
|
211
211
|
| `STEEL_UI_URL` | `$STEEL_API_URL/ui` | Base URL of the Steel session viewer |
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
212
|
+
| `STEEL_AUTH_HEADER` | unset | Full `Authorization` value. Wins over the other Steel auth variables |
|
|
213
|
+
| `STEEL_AUTH_BASIC` | unset | `base64(user:token)`, with or without a leading `Basic` prefix. Sent as `Authorization: Basic` |
|
|
214
|
+
| `STEEL_AUTH_USER` | unset | Authentik username. Used with `STEEL_AUTH_TOKEN`. An incomplete pair fails closed |
|
|
215
|
+
| `STEEL_AUTH_TOKEN` | unset | Authentik app password. Used with `STEEL_AUTH_USER` |
|
|
216
|
+
| `STEEL_API_KEY` | A `pass-cli` lookup | Deprecated. Sent as `x-steel-api-key` and as `?apiKey=` on the CDP URL for the temporary proxy shim. The library warns once. Not sent when an Authentik variable is set |
|
|
217
|
+
| `USE_PASS_CLI` | enabled | Set to `false` to disable the legacy `STEEL_API_KEY` `pass-cli` fallback |
|
|
214
218
|
|
|
215
219
|
Obscura's own `OBSCURA_TIMEZONE` (default `Europe/Berlin`) and `OBSCURA_CDP_TOKEN` (required only for a non-loopback bind) are read by the Obscura process, not by KXM.
|
|
216
220
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.135",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
|
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17313
17313
|
}
|
|
17314
17314
|
|
|
17315
17315
|
// plugins/kxm/src/mcp-server.ts
|
|
17316
|
-
var VERSION = "0.7.
|
|
17316
|
+
var VERSION = "0.7.135";
|
|
17317
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17318
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17319
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|