@kontextmind/kxm 0.7.145 → 0.7.147

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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/README.md +20 -2
  3. package/.kxm/agents/{coordinator.yaml → planner.yaml} +2 -0
  4. package/.kxm/agents/{critic-arch.yaml → reviewer-arch.yaml} +2 -0
  5. package/.kxm/agents/{critic-cli.yaml → reviewer-cli.yaml} +2 -0
  6. package/.kxm/agents/{implementer.yaml → writer.yaml} +2 -0
  7. package/.kxm/roles/planner.yaml +4 -2
  8. package/.kxm/roles/reviewer-arch.yaml +4 -2
  9. package/.kxm/roles/reviewer-cli.yaml +4 -2
  10. package/.kxm/roles/writer.yaml +6 -3
  11. package/.kxm/workflows/default.yaml +20 -12
  12. package/.kxm/workflows/land.yaml +1 -1
  13. package/.kxm/workflows/{review-arch-only.yaml → reviewer-arch-only.yaml} +7 -3
  14. package/.kxm/workflows/{review-cli-only.yaml → reviewer-cli-only.yaml} +7 -3
  15. package/.kxm/workflows/{implement-only.yaml → writer-only.yaml} +8 -4
  16. package/CHANGELOG.md +48 -8
  17. package/docs/README.md +1 -1
  18. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +8 -5
  19. package/docs/adr/ADR-0005-obscura-default-playwright.md +4 -3
  20. package/docs/adr/ADR-0006-machine-account-names.md +90 -0
  21. package/docs/adr/ADR-0007-steel-caddy-authentik.md +97 -0
  22. package/docs/adr/README.md +3 -1
  23. package/docs/contributing/harness-routing-internals.md +17 -11
  24. package/docs/contributing/operating-rules.md +13 -1
  25. package/docs/guides/agent-skills.md +1 -1
  26. package/docs/guides/browser-automation.md +44 -28
  27. package/docs/kb/how-credentials-retrieved-safely.md +22 -23
  28. package/docs/kb/how-to-connect-playwright-to-steel.md +8 -5
  29. package/docs/kb/how-to-recover-expired-session-or-orphan.md +11 -10
  30. package/docs/kb/why-automation-opened-different-browser.md +4 -3
  31. package/docs/operations/deploy.md +31 -0
  32. package/docs/operations/troubleshooting.md +23 -1
  33. package/docs/prompts/browser-diagnose-recover.md +2 -2
  34. package/docs/prompts/browser-start.md +1 -1
  35. package/docs/reference/cli-reference.md +23 -24
  36. package/docs/reference/config-reference.md +69 -62
  37. package/docs/reference/configuration.md +9 -7
  38. package/docs/reference/harness-routing.md +6 -5
  39. package/docs/reference/workflow-catalog.md +3 -3
  40. package/package.json +3 -3
  41. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  42. package/plugins/kxm/dist/cli.js +1145 -805
  43. package/plugins/kxm/dist/mcp-server.js +1 -1
  44. package/plugins/kxm/dist/runtime-supervisor.js +636 -308
  45. package/plugins/kxm/dist/runtime.js +710 -382
  46. package/plugins/kxm/dist/server.js +49 -5
  47. package/plugins/kxm/package.json +1 -1
  48. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +11 -12
  49. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +5 -4
  50. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +4 -3
  51. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +5 -5
  52. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +1 -1
  53. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -3
  54. package/plugins/kxm/src/browser.ts +12 -9
  55. package/plugins/kxm/src/cli/project.ts +2 -1
  56. package/plugins/kxm/src/cli/roles.ts +3 -4
  57. package/plugins/kxm/src/engine.ts +9 -6
  58. package/plugins/kxm/src/mcp-server.ts +1 -1
  59. package/plugins/kxm/src/modes.ts +1 -1
  60. package/plugins/kxm/src/policy-draft.mjs +11 -5
  61. package/plugins/kxm/src/project-config.ts +49 -11
  62. package/plugins/kxm/src/runtime-service.ts +2 -1
  63. package/plugins/kxm/src/template.ts +39 -10
  64. package/plugins/kxm/src/workflow-manager.ts +34 -28
  65. package/plugins/kxm/src/workforce-names.d.mts +38 -0
  66. package/plugins/kxm/src/workforce-names.mjs +317 -0
  67. package/schemas/agent.schema.json +7 -0
  68. package/schemas/model.schema.json +12 -0
  69. package/schemas/workflow.schema.json +14 -0
  70. package/scripts/harness-run.mjs +1 -1
  71. package/scripts/native-critic.mjs +5 -5
  72. package/scripts/roster-policy.mjs +9 -3
  73. package/scripts/workforce-lint.mjs +18 -0
@@ -14958,6 +14958,7 @@ var MODES = Object.freeze(["headless", "interactive", "either"]);
14958
14958
  var MODEL_KEYS = Object.freeze([
14959
14959
  "schema",
14960
14960
  "id",
14961
+ "aliases",
14961
14962
  "harness",
14962
14963
  "model",
14963
14964
  "vendor",
@@ -14996,8 +14997,51 @@ var FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
14996
14997
  var ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
14997
14998
  var CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
14998
14999
 
14999
- // plugins/kxm/src/template.ts
15000
+ // plugins/kxm/src/workforce-names.mjs
15000
15001
  var import_yaml2 = __toESM(require_dist(), 1);
15002
+ var ROLE_IDS = Object.freeze([
15003
+ "writer",
15004
+ "planner",
15005
+ "reviewer-arch",
15006
+ "reviewer-cli",
15007
+ "experiment"
15008
+ ]);
15009
+ var ROUTE_RENAMES = Object.freeze([
15010
+ ["grok-native", "grok-grok-4-7"],
15011
+ ["qwen-openrouter-pi", "pi-qwen3-coder-plus-openrouter"],
15012
+ ["gemini-agy", "agy-gemini-3-8-flash-high"],
15013
+ ["fable-claude", "claude-fable"],
15014
+ ["sol-codex", "codex-gpt-5-6-sol"],
15015
+ ["grok-default", "grok-grok-4-6"],
15016
+ ["fable-default", "claude-fable"]
15017
+ ]);
15018
+ var AGENT_RENAMES = Object.freeze([
15019
+ ["coordinator", "planner"],
15020
+ ["implementer", "writer"],
15021
+ ["critic-arch", "reviewer-arch"],
15022
+ ["critic-cli", "reviewer-cli"]
15023
+ ]);
15024
+ var WORKFLOW_RENAMES = Object.freeze([
15025
+ ["implement-only", "writer-only"],
15026
+ ["review-arch-only", "reviewer-arch-only"],
15027
+ ["review-cli-only", "reviewer-cli-only"]
15028
+ ]);
15029
+ var STEP_RENAMES = Object.freeze([
15030
+ ["implement", "writer"],
15031
+ ["critic-arch", "reviewer-arch"],
15032
+ ["review-arch", "reviewer-arch"],
15033
+ ["critic-cli", "reviewer-cli"],
15034
+ ["review-cli", "reviewer-cli"]
15035
+ ]);
15036
+ var RENAMES = Object.freeze({
15037
+ route: ROUTE_RENAMES,
15038
+ agent: AGENT_RENAMES,
15039
+ workflow: WORKFLOW_RENAMES,
15040
+ step: STEP_RENAMES
15041
+ });
15042
+
15043
+ // plugins/kxm/src/template.ts
15044
+ var import_yaml3 = __toESM(require_dist(), 1);
15001
15045
 
15002
15046
  // plugins/kxm/src/repo-root.ts
15003
15047
  import { existsSync } from "node:fs";
@@ -18253,7 +18297,7 @@ function explainContextItem(id, pool) {
18253
18297
  }
18254
18298
 
18255
18299
  // plugins/kxm/src/memory.ts
18256
- var import_yaml3 = __toESM(require_dist(), 1);
18300
+ var import_yaml4 = __toESM(require_dist(), 1);
18257
18301
  import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
18258
18302
  import { extname as extname2, join as join3, resolve as resolve2 } from "node:path";
18259
18303
  var MEMORY_SCHEMA = "kxm.memory.v1";
@@ -18285,7 +18329,7 @@ function parseFrontmatter(content) {
18285
18329
  }
18286
18330
  function parseMemoryRecord(raw, filename = "memory.md") {
18287
18331
  const { frontmatter, body } = parseFrontmatter(raw);
18288
- const data = (0, import_yaml3.parse)(frontmatter);
18332
+ const data = (0, import_yaml4.parse)(frontmatter);
18289
18333
  if (!data || typeof data !== "object" || Array.isArray(data)) {
18290
18334
  throw new Error(`invalid YAML frontmatter in ${filename}`);
18291
18335
  }
@@ -18622,7 +18666,7 @@ function boundedRefs(value, field) {
18622
18666
  }
18623
18667
 
18624
18668
  // plugins/kxm/src/skills.ts
18625
- var import_yaml4 = __toESM(require_dist(), 1);
18669
+ var import_yaml5 = __toESM(require_dist(), 1);
18626
18670
  import { createHash as createHash4 } from "node:crypto";
18627
18671
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
18628
18672
  import { dirname as dirname4, join as join4 } from "node:path";
@@ -18666,7 +18710,7 @@ function parseSkillFrontmatter(content) {
18666
18710
  return { frontmatter: null, body: content };
18667
18711
  }
18668
18712
  try {
18669
- const parsed = (0, import_yaml4.parse)(rawFm);
18713
+ const parsed = (0, import_yaml5.parse)(rawFm);
18670
18714
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
18671
18715
  return { frontmatter: parsed, body: rawBody };
18672
18716
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.145",
3
+ "version": "0.7.147",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,38 +1,37 @@
1
1
  ---
2
2
  name: kxm-browser-auth
3
- description: Retrieve application credentials and manage authenticated browser profiles safely via pass-cli without secret exposure.
3
+ description: Retrieve application credentials and manage authenticated browser profiles without secret exposure. Steel uses the svc-steel credential from 1Password via op read.
4
4
  ---
5
5
 
6
6
  # KXM Browser Credentials & Authenticated Profiles
7
7
 
8
- Use this skill to retrieve target application credentials and manage browser session state securely using `pass-cli` as the sole authoritative store.
8
+ Use this skill to retrieve target application credentials and manage browser session state. The Steel `svc-steel` credential comes from 1Password and is read with `op read`. It is never written to disk.
9
9
 
10
10
  ## Purpose & Scope
11
11
 
12
- - Enforce `pass-cli` as the single source of truth for credentials and API keys.
12
+ - Read the Steel credential at runtime from 1Password.
13
13
  - Prevent secrets from leaking into git repositories, logs, prompts, or model-visible tool outputs.
14
14
  - Support safe storage and retrieval of session storage state and authenticated profiles.
15
15
 
16
16
  ## Credential Retrieval Guidelines
17
17
 
18
- ### 1. Authoritative Tool: pass-cli
18
+ ### 1. Steel credential: 1Password
19
19
 
20
- Always retrieve credentials and API keys directly from `pass-cli`:
20
+ `STEEL_API_KEY` is deprecated. Steel and Caddy do not enforce it. Authenticate as `svc-steel`. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`. Those override `STEEL_API_KEY`. `STEEL_API_URL` defaults to `https://steel.kontextmind.com`. `kxm` 0.7.135 or newer is required.
21
21
 
22
22
  ```bash
23
- # Retrieve target login password into an environment variable or piping mechanism
24
- pass-cli item view --vault-name "<vault>" --item-title "<title>" --field password
25
-
26
- # Retrieve the Authentik app password for Steel. The proxy accepts Authorization: Basic.
27
- # STEEL_AUTH_USER is the Authentik username (for example svc-steel).
28
- pass-cli item view --vault-name "<vault>" --item-title "<steel-item>" --field password
23
+ # Vault kontextmind, item "Steel (svc-steel)", field basic_auth.
24
+ # Do not redirect this into a file.
25
+ export STEEL_AUTH_BASIC="$(op read 'op://kontextmind/Steel (svc-steel)/basic_auth')"
29
26
  ```
30
27
 
28
+ `STEEL_AUTH_USER` is `svc-steel` when you use the user and token pair. Send the value as `Authorization` on `/v1/devtools`. Never put it in the URL.
29
+
31
30
  ### 2. Secret Redaction Invariants
32
31
 
33
32
  - **Never** write plain passwords, session tokens, or API keys into markdown docs, commit messages, or prompts.
34
33
  - **Never** pass plain credentials as unredacted command line arguments in shared logs.
35
- - Use environment variable injection (`pass-cli run`) or direct in-memory pipes.
34
+ - Keep the `op read` result in the environment of the process that calls Steel.
36
35
 
37
36
  ### 3. Profile & Storage State Management
38
37
 
@@ -15,16 +15,17 @@ Use this skill to investigate and resolve connectivity failures, CDP attachment
15
15
  - **Diagnosis**:
16
16
  - KontextMind Steel is behind Authentik forward auth. Unauthenticated requests redirect to `id.kxmd.dev`. Authentik accepts an app password only as `Authorization: Basic`. A Bearer token is refused.
17
17
  - Check that `STEEL_AUTH_BASIC` is set, or that both `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN` are set (`test -n "$STEEL_AUTH_TOKEN" && echo set`); never print the value.
18
- - A legacy `STEEL_API_KEY` still uses `x-steel-api-key` and `?apiKey=` through the temporary proxy shim. Prefer the Authentik variables so the credential stays out of URLs.
19
- - **Remedy**: Re-export the Authentik app password into the session environment. Do not log it.
18
+ - `STEEL_API_KEY` is deprecated. Steel and Caddy do not enforce it. Do not put a credential in the URL. Direct LAN, tailnet, and host-forward connections are blocked.
19
+ - `websocketUrl` `ws://steel-browser/` means the client is older than `kxm` 0.7.135. The server returns `wss://steel.kontextmind.com/`.
20
+ - **Remedy**: Re-read the `svc-steel` field `basic_auth` with `op read 'op://kontextmind/Steel (svc-steel)/basic_auth'` into `STEEL_AUTH_BASIC`. Do not log it or write it to disk. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`.
20
21
 
21
22
  ### 2. CDP WebSocket Attachment Failure
22
23
 
23
24
  - **Symptom**: `WebSocket connection to wss://... failed: 404/500`.
24
25
  - **Diagnosis**:
25
26
  - Check if the target session ID has already been released or timed out.
26
- - Verify ingress WebSocket headers: ensure `nginx.ingress.kubernetes.io/websocket-services` is enabled.
27
- - **Remedy**: Query `GET /v1/sessions/<id>`. If status is `released`, launch a fresh session.
27
+ - The CDP path is `/v1/devtools` on `wss://steel.kontextmind.com/` with an `Authorization` header. Caddy must pass the WebSocket upgrade. A credential in the URL is not accepted.
28
+ - **Remedy**: Query `GET /v1/sessions/<id>` with the same `Authorization` header. If status is `released`, launch a fresh session.
28
29
 
29
30
  ### 3. Session Timeout & Expiration
30
31
 
@@ -21,11 +21,12 @@ Use this skill for exploratory navigation, DOM inspection, scraping, and interac
21
21
  Ensure an active Steel session exists and obtain its CDP endpoint:
22
22
 
23
23
  ```bash
24
- # CDP URL only. The Authentik credential is an Authorization header, not a query parameter.
25
- CDP_URL="wss://<steel-host>/v1/devtools?sessionId=<sessionId>"
24
+ # CDP path only. The session websocketUrl is wss://steel.kontextmind.com/.
25
+ # The Authentik credential is an Authorization header, not a query parameter.
26
+ CDP_URL="wss://steel.kontextmind.com/v1/devtools?sessionId=<sessionId>"
26
27
  ```
27
28
 
28
- Build that URL with `formatCDPConnect()` so the `Authorization: Basic` header is available for the handshake. `STEEL_AUTH_BASIC`, or `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`, supplies it. A Bearer token is not accepted.
29
+ Build that URL with `formatCDPConnect()` so the `Authorization` header is available for the handshake. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`. Those override `STEEL_API_KEY`, which Steel and Caddy do not enforce. A Bearer token is not accepted. `kxm` 0.7.135 or newer is required. Read `basic_auth` with `op read` and do not write it to disk.
29
30
 
30
31
  ### 2. Connect a client that can send the header
31
32
 
@@ -5,7 +5,7 @@ description: Start, attach to, inspect, and release self-hosted Steel browser se
5
5
 
6
6
  # KXM Browser Session Management
7
7
 
8
- Use this skill to create, inspect, attach automation tools to, and release isolated browser sessions running on your self-hosted Steel deployment. Set `STEEL_API_URL` (and optionally `STEEL_UI_URL`) to your deployment; KXM does not provide one.
8
+ Use this skill to create, inspect, attach automation tools to, and release isolated browser sessions on the Steel server. `STEEL_API_URL` defaults to `https://steel.kontextmind.com`. `steel.theneuro.me` is an alias of that server. `kxm` 0.7.135 or newer is required.
9
9
 
10
10
  Playwright testing and verification use Obscura by default (`resolveBrowserCdpEndpoint()`, or `npm run e2e`). Use this skill's Steel session for human takeover, MFA, and the live session viewer. Attach Playwright to that session only when `KXM_BROWSER=steel`.
11
11
 
@@ -18,9 +18,9 @@ Playwright testing and verification use Obscura by default (`resolveBrowserCdpEn
18
18
 
19
19
  ## Prerequisites
20
20
 
21
- 1. Your own Steel deployment, with `STEEL_API_URL` set to its base URL (for example `https://steel.example.com`) and `STEEL_UI_URL` set if the viewer lives elsewhere (default `$STEEL_API_URL/ui`).
22
- 2. Authentik app-password auth in the environment. The KontextMind Steel hosts are behind Authentik forward auth, which accepts `Authorization: Basic` and refuses a Bearer token. Export `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN` (or the pre-encoded `STEEL_AUTH_BASIC`) from your password manager, for example `export STEEL_AUTH_TOKEN="$(pass-cli item view --vault-name '<vault>' --item-title '<item>' --field password)"`. Never paste the token into a prompt. `STEEL_API_KEY` is a deprecated shim (`x-steel-api-key` and `?apiKey=`); do not put credentials in URLs.
23
- 3. Network access to remote CDP endpoints on port 443 / 9223.
21
+ 1. `kxm` 0.7.135 or newer. The server is `https://steel.kontextmind.com`. Caddy and Authentik forward auth are the only path. Direct LAN, tailnet, and host-forward access is blocked. Sessions return `websocketUrl` `wss://steel.kontextmind.com/` (previously `ws://steel-browser/`).
22
+ 2. The `svc-steel` Authentik credential in the environment. Allowed groups are `steel-users`, `kxmd-users`, `kxmd-admins`, and `kxmd-owners`. Read it at runtime and do not write it to disk: `export STEEL_AUTH_BASIC="$(op read 'op://kontextmind/Steel (svc-steel)/basic_auth')"`. Precedence is `STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`. Those override `STEEL_API_KEY`. `STEEL_API_KEY` is deprecated. Steel and Caddy do not enforce it. A Bearer token is refused. Never put the credential in a URL.
23
+ 3. HTTPS to `steel.kontextmind.com` for the CDP path `/v1/devtools`. Send `Authorization` on the handshake.
24
24
 
25
25
  ## Session Lifecycle States
26
26
 
@@ -48,7 +48,7 @@ Playwright testing and verification use Obscura by default (`resolveBrowserCdpEn
48
48
  - **Inputs**: Task ID, target URL, session timeout (default 300s, max 1800s), optional proxy or viewport dimensions.
49
49
  - **Outputs**:
50
50
  - `sessionId`: Unique session UUID.
51
- - `cdpUrl`: Remote CDP WebSocket URL (`wss://<steel-host>/v1/devtools?sessionId=<id>`). Send `Authorization: Basic` on the handshake. The URL has no credential when Authentik auth is configured.
51
+ - `cdpUrl`: `wss://steel.kontextmind.com/v1/devtools?sessionId=<id>`. Send `Authorization` on the handshake. The URL has no credential. The session `websocketUrl` is `wss://steel.kontextmind.com/`.
52
52
  - `sessionViewerUrl`: Interactive web session viewer URL (`$STEEL_UI_URL?sessionId=<id>`).
53
53
  - `status`: `live` | `idle` | `released`.
54
54
 
@@ -77,7 +77,7 @@ Local pages require the launcher flag `--allow-private-network` (the launcher al
77
77
 
78
78
  ## Steel, only for takeover
79
79
 
80
- When the case is human takeover, MFA, or the live session viewer, set `KXM_BROWSER=steel` and a session id. `connectBrowserOverCdp()` passes `formatCDPConnect()` headers into `chromium.connectOverCDP`. Closing the Playwright browser disconnects the client and does not release the Steel session.
80
+ When the case is human takeover, MFA, or the live session viewer, set `KXM_BROWSER=steel` and a session id. That path needs `kxm` 0.7.135 or newer. `connectBrowserOverCdp()` passes `formatCDPConnect()` headers into `chromium.connectOverCDP` for `/v1/devtools` on `wss://steel.kontextmind.com/`. The `Authorization` header is the `svc-steel` credential (`STEEL_AUTH_HEADER`, then `STEEL_AUTH_BASIC`, then `STEEL_AUTH_USER` and `STEEL_AUTH_TOKEN`). `STEEL_API_KEY` is deprecated and is not enforced. Closing the Playwright browser disconnects the client and does not release the Steel session.
81
81
 
82
82
  ```typescript
83
83
  import { chromium } from "playwright";
@@ -22,7 +22,7 @@ to run in their own terminal or in Claude Code.
22
22
  `ready`, `repair`, or `legacy`, and `issues` lists anything to fix first.
23
23
  2. `kxm init --name "<display name>"` prints
24
24
  `initialized KXM project at <root>`. It writes `.kxm/project.yaml`,
25
- `.kxm/agents/coordinator.yaml`, `.kxm/agents/implementer.yaml`,
25
+ `.kxm/agents/planner.yaml`, `.kxm/agents/writer.yaml`,
26
26
  `.kxm/gates.yaml`, `.kxm/repo/repo.yaml`, `.kxm/template-provenance.yaml`,
27
27
  and `.kxm/workflows/default.yaml`. Outside Git it fails with
28
28
  `git_root_required`. In an existing project, plain `kxm init` validates,
@@ -44,7 +44,7 @@ After the user's commit:
44
44
  1. `kxm workflow add first --template spec-and-plan` prints
45
45
  `Added workflow 'first' to local (<root>/.kxm/workflows/first.yaml)`. Add
46
46
  `--dry-run` first to see the path without writing. The template has two
47
- steps, `plan` then `review-arch`, both run by the `coordinator` agent with
47
+ steps, `plan` then `reviewer-arch`, both run by the `planner` agent with
48
48
  `repositories: control: read`. It writes nothing and runs no test command,
49
49
  and a failed review goes back to `plan` at most twice.
50
50
  2. `kxm init` validates it and prints `validated KXM project at <root>`.
@@ -71,7 +71,7 @@ After the user's commit:
71
71
  4. `kxm runs status <runId>` prints `created`.
72
72
  5. `kxm runs drive <runId> --simulated --wait --timeout-ms 60000` prints a
73
73
  `kxm.drive-receipt.v1` whose settlement is terminal `completed`, and exits 0.
74
- The run moves from `plan` to `review-arch` to `completed`. Always pass
74
+ The run moves from `plan` to `reviewer-arch` to `completed`. Always pass
75
75
  `--simulated`; without it, drive calls live harnesses.
76
76
  6. `kxm runs status <runId>` prints `completed … (receipt verified)`.
77
77
  7. `kxm runs receipt <runId>` and `kxm runs list`. Cancel a stuck run with
@@ -5,11 +5,12 @@
5
5
  * (`resolveBrowserCdpEndpoint()`). Steel remains the client for human
6
6
  * takeover, MFA, and the live session viewer (`KXM_BROWSER=steel`).
7
7
  * Manages remote Steel sessions, CDP endpoints, human takeover handoffs,
8
- * pass-cli credential references, and automated cleanup without leaking secrets.
8
+ * and automated cleanup without leaking secrets.
9
9
  *
10
- * Hosts behind Authentik forward auth (the KontextMind Steel proxies) accept
11
- * an app password only as `Authorization: Basic`. A Bearer token is refused.
12
- * `STEEL_API_KEY` remains a legacy shim: `x-steel-api-key` and `?apiKey=`.
10
+ * steel.kontextmind.com is reached only through Caddy and Authentik forward
11
+ * auth. Clients send `Authorization: Basic` for the svc-steel credential.
12
+ * A Bearer token is refused. `STEEL_API_KEY` is deprecated and is not
13
+ * enforced by Steel or Caddy. Do not put a credential in the URL.
13
14
  */
14
15
 
15
16
  import { execSync } from "node:child_process";
@@ -188,9 +189,9 @@ export class SteelAuthRedirectError extends Error {
188
189
  }
189
190
 
190
191
  const LEGACY_STEEL_AUTH_WARNING =
191
- "kxm: STEEL_API_KEY is deprecated for Steel. Authentik forward auth accepts app passwords only as Authorization: Basic. " +
192
- "Set STEEL_AUTH_BASIC, or STEEL_AUTH_USER and STEEL_AUTH_TOKEN. " +
193
- "The legacy x-steel-api-key header and apiKey query parameter remain for the temporary proxy shim.\n";
192
+ "kxm: STEEL_API_KEY is deprecated for Steel. Steel and Caddy do not enforce it. " +
193
+ "Set STEEL_AUTH_HEADER, or STEEL_AUTH_BASIC, or STEEL_AUTH_USER and STEEL_AUTH_TOKEN. " +
194
+ "Those override STEEL_API_KEY. Send Authorization on the request, not in the URL.\n";
194
195
 
195
196
  let legacySteelAuthWarned = false;
196
197
 
@@ -310,6 +311,7 @@ export function resolvePassCliApiKey(
310
311
  }
311
312
  }
312
313
  try {
314
+ // Deprecated lookup. The hosted server does not enforce STEEL_API_KEY.
313
315
  const output = execFn(
314
316
  'pass-cli item view --vault-name "AI Provider Keys" --item-title "Steel Browser (KontextMind DOKS)" --output json',
315
317
  );
@@ -325,8 +327,9 @@ export function resolvePassCliApiKey(
325
327
  }
326
328
 
327
329
  /**
328
- * Resolve Steel configuration from environment or pass-cli.
330
+ * Resolve Steel configuration from the environment.
329
331
  * Does not write secrets to disk or logs.
332
+ * Authentik Basic auth overrides `STEEL_API_KEY`. Steel and Caddy do not enforce that key.
330
333
  *
331
334
  * Authentik Basic auth (`STEEL_AUTH_HEADER`, `STEEL_AUTH_BASIC`, or
332
335
  * `STEEL_AUTH_USER` + `STEEL_AUTH_TOKEN`) wins over `STEEL_API_KEY`.
@@ -610,7 +613,7 @@ export class SteelClient {
610
613
  }
611
614
 
612
615
  /**
613
- * Launch a new Steel browser session on DOKS.
616
+ * Launch a new Steel browser session.
614
617
  */
615
618
  async createSession(options?: CreateSessionOptions): Promise<SteelSession> {
616
619
  const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs ?? 300000;
@@ -11,6 +11,7 @@ import { runVisionGate } from "../vision-gate.ts";
11
11
  import {
12
12
  KxmConfigError,
13
13
  discoverKxmProjectRoot,
14
+ lookupKxmResource,
14
15
  type KxmInitializationPlan,
15
16
  } from "../project-config.ts";
16
17
  import { initializeKxmProject } from "../init.ts";
@@ -439,7 +440,7 @@ export function resolveKxmRunTarget(
439
440
  }
440
441
  const bundle = loadKxmProject(projectRoot, {});
441
442
  const workflowId = workflow ?? String(bundle.project.value.defaultWorkflow ?? "default");
442
- if (!bundle.workflows.has(workflowId)) {
443
+ if (!lookupKxmResource(bundle.workflows.values(), workflowId, "workflow")) {
443
444
  print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow: workflowId, ...envelope }, `workflow ${workflowId} does not exist in this project`);
444
445
  return 1;
445
446
  }
@@ -15,6 +15,7 @@ import {
15
15
  type KxmRoleDefinition,
16
16
  } from "../role.ts";
17
17
  import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
18
+ import { findYamlBasename } from "../workforce-names.mjs";
18
19
  import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
19
20
  import { projectRuntimeOwnsRun } from "../runtime-store.ts";
20
21
  import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
@@ -185,8 +186,7 @@ export async function cmdRoleAdd(
185
186
  // after its roster is parsed, for both local and global scope.
186
187
  const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
187
188
  const refuseMissingRoute = (routeId: string): boolean => {
188
- const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
189
- if (existsSync(modelFile)) return false;
189
+ if (findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) return false;
190
190
  runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
191
191
  return true;
192
192
  };
@@ -398,8 +398,7 @@ export async function cmdRoleModify(
398
398
  if (options.addRoute) {
399
399
  const routeId = options.addRoute;
400
400
  const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
401
- const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
402
- if (!existsSync(modelFile)) {
401
+ if (!findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) {
403
402
  runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
404
403
  return 1;
405
404
  }
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parse } from "yaml";
5
5
  import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
6
+ import { findYamlBasename } from "./workforce-names.mjs";
6
7
  import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
7
8
  import {
8
9
  buildFormalContextPacket,
@@ -101,7 +102,7 @@ import {
101
102
  type KxmRunRecord,
102
103
  type KxmRunStatus,
103
104
  } from "./runtime-store.ts";
104
- import { KxmConfigError, kxmCanonicalJson, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
105
+ import { KxmConfigError, kxmCanonicalJson, lookupKxmResource, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
105
106
  import { evaluateArtifactsGate } from "./engine-artifacts.ts";
106
107
  import { createCommandObserver } from "./engine-command.ts";
107
108
  import {
@@ -277,7 +278,7 @@ export function pinKxmCompiledPlan(
277
278
  if (String(bundle.project.value.id) !== run.projectId) {
278
279
  throw runtimeError("run_owner_mismatch", runId, "bundle project does not match the run");
279
280
  }
280
- const workflow = bundle.workflows.get(run.workflowId);
281
+ const workflow = lookupKxmResource(bundle.workflows.values(), run.workflowId, "workflow");
281
282
  if (!workflow) throw runtimeError("run_workflow_unknown", run.workflowId, `workflow ${run.workflowId} does not exist in this project`);
282
283
  const revisions = kxmPolicyRevisions(bundle, options);
283
284
  if (
@@ -1397,7 +1398,8 @@ function resolveProducerRoute(
1397
1398
  },
1398
1399
  };
1399
1400
  }
1400
- const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
1401
+ const agentFile = findYamlBasename(join(projectRoot, ".kxm", "agents"), agentId, "agent") ?? agentId;
1402
+ const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentFile}.yaml`));
1401
1403
  const role = typeof agent?.role === "string" ? agent.role : "";
1402
1404
  if (!role) {
1403
1405
  return {
@@ -1424,7 +1426,8 @@ function resolveProducerRoute(
1424
1426
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1425
1427
  const record = entry as { route?: unknown; effort?: unknown };
1426
1428
  if (typeof record.route !== "string" || record.route.length === 0) continue;
1427
- const modelPath = join(projectRoot, ".kxm", "models", `${record.route}.yaml`);
1429
+ const routeFile = findYamlBasename(join(projectRoot, ".kxm", "models"), record.route, "route") ?? record.route;
1430
+ const modelPath = join(projectRoot, ".kxm", "models", `${routeFile}.yaml`);
1428
1431
  const doc = readModelDocument(modelPath);
1429
1432
  if (!doc) {
1430
1433
  return {
@@ -1487,7 +1490,7 @@ function resolveProducerRoute(
1487
1490
  model: selector.slice(slash + 1),
1488
1491
  selector,
1489
1492
  harness,
1490
- routeId: record.route,
1493
+ routeId: routeFile,
1491
1494
  role,
1492
1495
  permissions,
1493
1496
  ...(typeof record.effort === "string" ? { effort: record.effort } : {}),
@@ -2871,7 +2874,7 @@ export function kxmLiveRunPrerequisites(
2871
2874
  workflowId: string,
2872
2875
  projectRoot: string,
2873
2876
  ): KxmRunHandoff[] {
2874
- const workflow = bundle.workflows.get(workflowId);
2877
+ const workflow = lookupKxmResource(bundle.workflows.values(), workflowId, "workflow");
2875
2878
  if (!workflow) throw runtimeError("run_workflow_unknown", workflowId, `workflow ${workflowId} does not exist in this project`);
2876
2879
  const plan = compileKxmWorkflow({ id: workflowId, value: workflow.value, logicalPath: workflow.logicalPath });
2877
2880
  const envelope = { plan, projectLimits: kxmProjectAdmissionLimits(bundle), gates: pinnedGatesForCompiledPlan(plan, bundle, projectRoot) };
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
11
11
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
12
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
13
 
14
- const VERSION = "0.7.145";
14
+ const VERSION = "0.7.147";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -123,7 +123,7 @@ export const DEFAULT_MODES_CONFIG: ModesConfig = Object.freeze({
123
123
  browser: {
124
124
  description: "Remote Steel browser sessions and visual testing",
125
125
  tools: ["steel_session", "steel_scrape", "steel_screenshot"],
126
- promptSnippet: "Use Steel on DOKS for browser automation; invoke takeover on MFA.",
126
+ promptSnippet: "Playwright tests use Obscura. Steel is remote browsing and takeover through Caddy and Authentik; send Authorization, never a credential in the URL.",
127
127
  },
128
128
  },
129
129
  });
@@ -20,7 +20,7 @@ const TOKEN = /[\s\x00-\x1f]/u;
20
20
  const EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21
21
  const MODES = Object.freeze(["headless", "interactive", "either"]);
22
22
  const MODEL_KEYS = Object.freeze([
23
- "schema", "id", "harness", "model", "vendor", "status", "permissions", "origin",
23
+ "schema", "id", "aliases", "harness", "model", "vendor", "status", "permissions", "origin",
24
24
  "thinking", "tags", "capabilities", "priority", "fallbacks", "limits",
25
25
  ]);
26
26
  const ROLE_KEYS = Object.freeze([
@@ -226,6 +226,9 @@ function validateModelShape(document, id, label, issues) {
226
226
  if (document.id !== undefined && document.id !== id) {
227
227
  issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
228
228
  }
229
+ if (document.aliases !== undefined && !identifierList(document.aliases)) {
230
+ issues.push(issue("schema", "schema_pattern", label, "aliases must be unique identifiers"));
231
+ }
229
232
  if (document.harness !== undefined && !identifier(document.harness)) {
230
233
  issues.push(issue("schema", "schema_pattern", label, "harness must be an identifier"));
231
234
  }
@@ -511,9 +514,12 @@ function validateRoleSemantics(role, id, label, models, options, issues) {
511
514
  const admitted = role.roster
512
515
  .map((entry) => models.get(entry.route))
513
516
  .filter((model) => model && admittedRoute(model));
514
- if (admitted.length !== 1) {
515
- issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list exactly one admitted route"));
516
- } else if (admitted[0].permissions.length !== 1 || admitted[0].permissions[0] !== "read-only" || role.permission !== "read-only") {
517
+ // The first roster entry is the required critic. Later entries are
518
+ // fallbacks so one provider outage does not stop the role. Every entry
519
+ // stays read-only; vendor independence uses the first entry only.
520
+ if (admitted.length < 1) {
521
+ issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list at least one admitted route"));
522
+ } else if (role.permission !== "read-only" || admitted.some((model) => model.permissions.length !== 1 || model.permissions[0] !== "read-only")) {
517
523
  issues.push(issue("semantic", "permission_escalation", label, "critic purpose files must be read-only"));
518
524
  }
519
525
  }
@@ -549,7 +555,7 @@ function validateCriticVendors(roles, models, options, issues) {
549
555
  const admitted = (role.roster ?? [])
550
556
  .map((entry) => models.get(entry.route))
551
557
  .filter((model) => model && admittedRoute(model));
552
- if (admitted.length === 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
558
+ if (admitted.length >= 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
553
559
  }
554
560
  if (critics.length === 2 && critics[0].vendor === critics[1].vendor) {
555
561
  issues.push(issue("semantic", "critic_vendor_collision", "roles/reviewer-arch", "required critics must have independent vendors"));