@kontextmind/kxm 0.7.134 → 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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.134",
14
+ "version": "0.7.135",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
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-14"
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 `STEEL_API_KEY` in the operators' password manager.
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), [ADR-0005](../adr/ADR-0005-obscura-default-playwright.md), and [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.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 its API key. [ADR-0002](../adr/ADR-0002-browser-automation-steel-doks.md) describes the reference deployment on Kubernetes.
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 API key. The bundled skills use `pass-cli`.
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 Steel API key and site credentials live |
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
- | `STEEL_API_KEY` | A `pass-cli` lookup | The API key. When unset, the library runs a `pass-cli` lookup of a fixed KontextMind vault item |
44
- | `USE_PASS_CLI` | enabled | Set to `false` to disable that `pass-cli` fallback |
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 `STEEL_API_KEY` explicitly. Without them the library falls back to KontextMind's own deployment and vault item, which are not yours to use.
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 key from your secret manager; <vault> and <item> are yours.
52
- export STEEL_API_KEY="$(pass-cli item view --vault-name '<vault>' --item-title '<item>' --field STEEL_API_KEY)"
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>&apiKey=<key>` | CDP endpoint for `agent-browser` and Playwright |
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 the key as a header read from standard input, so it never appears in a process list:
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 'x-steel-api-key: %s\n' "$STEEL_API_KEY" | curl "${args[@]}"
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. For exploration, attach `agent-browser --cdp "<cdp-url>"`. Playwright tests use Obscura. Set `KXM_BROWSER=steel` and `chromium.connectOverCDP(<cdp-url>)` only when the test must drive this takeover session. Check the session's state with `steel GET "/v1/sessions/$SESSION_ID"`.
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
- $headers = @{ "x-steel-api-key" = $env:STEEL_API_KEY }
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
- | `401` or `403` from Steel | Missing or wrong API key | Re-export `STEEL_API_KEY` from your secret manager |
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-23"
10
+ updated: "2026-09-27"
11
11
  authority: "instruction"
12
12
  confidence: "verified"
13
- summary: "How the Steel client resolves its API key and how browser work keeps secrets out of model context."
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 API key
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
- | API key | An explicit override, then `STEEL_API_KEY`, then a `pass-cli` lookup |
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. Set `STEEL_API_URL` and `STEEL_API_KEY` for
37
- yours, and set `USE_PASS_CLI=false` to turn the lookup off.
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
- `steel_…` keys, and any field named like a key, secret, token, auth or
54
- password before it logs or returns output.
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). `resolveBrowserCdpEndpoint(session)` returns the same URL as `formatCDPEndpoint()` when `KXM_BROWSER=steel`.
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. The configuration comes
28
- from `STEEL_API_URL` and `STEEL_API_KEY`:
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 { formatCDPEndpoint, resolveSteelConfig } from "@kontextmind/kxm/runtime";
38
+ import { formatCDPConnect, resolveSteelConfig } from "@kontextmind/kxm/runtime";
32
39
 
33
- const config = resolveSteelConfig();
34
- const cdpUrl = formatCDPEndpoint({ id: sessionId, websocketUrl: "" }, config);
40
+ const { url, headers } = formatCDPConnect(
41
+ { id: sessionId, websocketUrl: "" },
42
+ resolveSteelConfig(),
43
+ );
35
44
  ```
36
45
 
37
- The result has this shape. It carries the API key, so never log it:
46
+ The URL has this shape:
38
47
 
39
48
  ```text
40
- wss://<steel-host>/v1/devtools?sessionId=<session-id>&apiKey=<steel-api-key>
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 browser = await chromium.connectOverCDP(process.env.STEEL_CDP_URL!);
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-23"
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 `STEEL_API_KEY` from your secret manager first. With
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
- STEEL_API_KEY=$(pass-cli item view --vault-name "<vault>" --item-title "<item>" --field STEEL_API_KEY)
31
- export STEEL_API_KEY
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 -s "$STEEL_API_URL/v1/sessions" \
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
- curl -s -X POST "$STEEL_API_URL/v1/sessions/<session-id>/release" \
41
- -H "x-steel-api-key: $STEEL_API_KEY"
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 request fails, so an empty result
64
- does not prove there are no orphans. Check with the `curl` call above.
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 and make Playwright and agent-browser attach to remote Steel."
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:** in Playwright, connect with `chromium.connectOverCDP(cdpUrl)`.
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:** always pass the session's CDP URL:
34
- `--cdp "wss://<steel-host>/v1/devtools?sessionId=<session-id>&apiKey=<steel-api-key>"`.
35
- Build the Obscura URL with `resolveObscuraCdpEndpoint()`, or the Steel URL with `formatCDPEndpoint()` when `KXM_BROWSER=steel`. See
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` and `STEEL_API_KEY`.
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-23"
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, verify `x-steel-api-key` header or `?apiKey=` query parameter.
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-23"
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 `STEEL_API_KEY` from the secret manager, for example with `pass-cli`, without logging raw values.
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` or `Playwright` via CDP.
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`, `STEEL_API_KEY`, `STEEL_UI_URL`, `USE_PASS_CLI` | [Browser settings](#browser-automation) |
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` chooses the CDP URL. `node scripts/obscura.mjs` downloads pinned Obscura v0.2.3 and serves it. See [Browser automation](../guides/browser-automation.md).
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 Steel session URL from `formatCDPEndpoint()` and requires a session id. Any other value throws |
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
- | `STEEL_API_KEY` | A `pass-cli` lookup | Steel API key. When unset, the library runs a `pass-cli` lookup of a fixed KontextMind vault item |
213
- | `USE_PASS_CLI` | enabled | Set to `false` to disable that `pass-cli` fallback |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.134",
3
+ "version": "0.7.135",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -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.134",
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.134";
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();