@kontextmind/kxm 0.7.181 → 0.7.183

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.181",
14
+ "version": "0.7.183",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ All notable user-facing changes are documented here. The project follows [Semant
6
6
 
7
7
  ### Added
8
8
 
9
+ - **The witnessed MG1 Python control-plane foundation is now reproducible from this repository.**
10
+ `python/kxm-control` provides the pinned Python 3.13 uv workspace, fixed
11
+ `kxmd` Postgres/Temporal caller, Authentik user and runner verification
12
+ that requires token expiry, tenant/project/repository registration,
13
+ owner/admin tenant rename, revision and artifact APIs, and a probe entry
14
+ point. OpenAPI and the Studio path inventory are derived from the FastAPI
15
+ app. The checked generator emits the revision helper. Studio exposes the
16
+ bounded `/api/control/account` relay with a three-second bound and explicit
17
+ failure states. Removing registration leaves source state untouched and
18
+ later registry calls return `registration_unavailable` until the schema is
19
+ installed again. The package does not change host or guest installs. The
20
+ required CI aggregate runs the Python contracts against PostgreSQL 17. This
21
+ does not move the hub or Runtime from their existing Node and SQLite owners.
22
+
9
23
  - **KXM configuration now has one owner-validated terminal editor.**
10
24
  `kxm config edit --scope project|user` and OMP/Pi `/kxm config` publish the
11
25
  same `kxm.config.v1` surface. Accepted writes are saved and re-read; rejected
@@ -1,7 +1,7 @@
1
1
  # Authentication
2
2
 
3
- Read from `plugins/kxm/src/harness.ts` and `plugins/kxm/src/cli.ts`.
4
-
3
+ Read from `plugins/kxm/src/harness.ts`, `plugins/kxm/src/cli.ts`, and
4
+ `python/kxm-control/src/kxm_control/auth.py`.
5
5
  `kxm harness list` runs `probeHarnesses` (`cmdHarnessList` in
6
6
  `plugins/kxm/src/cli/project.ts`). For each detected harness that has
7
7
  `authArgs`, `probeEntry` runs that command and `interpretAuth` reads the
@@ -23,6 +23,15 @@ the harness command's own stdout.
23
23
  issues, or clears the local session token file. That token is a KXM session
24
24
  token, not a provider key. Provider logins stay in each harness.
25
25
 
26
+ The MG1 Python control API uses a separate Authentik boundary. It accepts only
27
+ RS256 tokens whose issuer and audience match its configured values, whose
28
+ `exp` is present and unexpired, and whose `kxm_actor` is `user` or `runner`.
29
+ A token without `exp` is rejected.
30
+ The signed subject and actor kind must match
31
+ an explicit tenant membership; a user membership never authorizes a runner
32
+ token with the same subject. Only a user can create a tenant. The API does not
33
+ store the bearer token.
34
+
26
35
  ## Related
27
36
 
28
37
  - [Access](access.md)
@@ -52,11 +52,13 @@ roadmap question. The tracker wins.
52
52
  proposal is not in this repository. Its delivery position is unchanged:
53
53
  none of the delivery is scheduled there.
54
54
 
55
- The tracker names that file. The sentences that can be repeated without a
56
- hostname, an account name, or a secret are: a first move is recorded as
57
- installed, Postgres and Temporal are described as loopback only, KXM does not
58
- write those stores, Redis Streams, NATS, raw WebSockets, and the A2A Python
59
- SDK stay backlog, there is no S3, and the SQLite write path is unchanged.
55
+ Today the Node hub and Runtime still write local SQLite and local files. The
56
+ checked-in MG1 Python control API is the narrow exception: it owns only central
57
+ registration and bounded tenant artifacts in the fixed `kxmd` PostgreSQL
58
+ account. Temporal is provisioned for `kxmd`, but no production workflow cutover
59
+ is selected. Redis Streams, NATS, raw WebSockets, the A2A Python SDK, and S3
60
+ remain backlog. The rest of this page remains a target, not a live deployment
61
+ description.
60
62
 
61
63
  ## Related
62
64
 
@@ -6,7 +6,8 @@ Read from `plugins/kxm/src/cli.ts`, `plugins/kxm/src/config-editor.ts`,
6
6
  `plugins/kxm/src/runtime-paths.ts`, `plugins/kxm/src/mcp-server.ts`,
7
7
  `plugins/kxm/src/oneshot-producer.ts`, `plugins/kxm/src/studio-layout.ts`,
8
8
  `plugins/kxm/src/cli/types.ts` (`workspaceDirs`),
9
- `plugins/kxm/src/local-snapshot.ts`, and `scripts/kxm-worker.mjs`.
9
+ `plugins/kxm/src/local-snapshot.ts`, `scripts/kxm-worker.mjs`, and
10
+ `python/kxm-control/src/kxm_control/{api,registry}.py`.
10
11
 
11
12
  | Surface | Started by | Listener or path | Store it owns | Defining file |
12
13
  | --- | --- | --- | --- | --- |
@@ -21,7 +22,8 @@ Read from `plugins/kxm/src/cli.ts`, `plugins/kxm/src/config-editor.ts`,
21
22
  | MCP server | Claude plugin or stdio launch | stdio | none | `plugins/kxm/src/mcp-server.ts` |
22
23
  | One-shot harness | supervisor producer | child process, no listener | none | `plugins/kxm/src/oneshot-producer.ts` |
23
24
  | Pi worker | `kxm agent worker` | no HTTP listener in `scripts/kxm-worker.mjs` | none of its own | `scripts/kxm-worker.mjs` |
24
- | Studio | `kxm studio serve` | `127.0.0.1:4242` | none | `plugins/kxm/src/studio-layout.ts` |
25
+ | Studio | `kxm studio serve` | `127.0.0.1:4242` | none; `/api/control/account` is a bounded relay to the configured control API | `plugins/kxm/src/studio-layout.ts` |
26
+ | Python control API | ASGI server loading `kxm_control.api:app` | deployment-selected; the package declares no network default | `kxm_registry` in the fixed `kxmd` PostgreSQL database and the configured tenant artifact root | `python/kxm-control/src/kxm_control/api.py` |
25
27
  | Workspace config | `workspaceDirs` | `.kxm/config`, or `KXM_CONFIG_DIR` | files in that directory | `plugins/kxm/src/cli/types.ts` |
26
28
  | Workspace logs | `workspaceDirs` | `.kxm/logs`, or `KXM_LOGS_DIR` | log files | `plugins/kxm/src/cli/types.ts` |
27
29
  | Workspace assets | `workspaceDirs` | `.kxm/assets`, or `KXM_ASSETS_DIR` | asset files | `plugins/kxm/src/cli/types.ts` |
@@ -2,7 +2,8 @@
2
2
 
3
3
  Read from `plugins/kxm/src/cli.ts`, `plugins/kxm/src/runtime-supervisor.ts`,
4
4
  `scripts/kxm-hub.mjs`, `plugins/kxm/src/hub.ts`, `plugins/kxm/src/mcp-server.ts`,
5
- and `plugins/kxm/src/oneshot-producer.ts`.
5
+ `plugins/kxm/src/oneshot-producer.ts`, and
6
+ `python/kxm-control/src/kxm_control/api.py`.
6
7
 
7
8
  KXM on this checkout is local-first. The `kxm` CLI is the operator entry
8
9
  (`plugins/kxm/src/cli.ts`, launched through `scripts/kxm.mjs`). The hub is an
@@ -18,17 +19,29 @@ requested. The MCP server (`plugins/kxm/src/mcp-server.ts`) uses
18
19
  spawned by the producer registered in `plugins/kxm/src/oneshot-producer.ts`.
19
20
  That child process is not a listener.
20
21
 
22
+ The checked-in MG1 Python control API is additive. It owns central registration
23
+ and bounded tenant artifacts through the fixed `kxmd` account. Only an owner
24
+ or admin membership can rename a tenant. Removing the registration schema
25
+ leaves source state in place, and later registry routes return
26
+ `registration_unavailable` until the schema is installed again. The API does
27
+ not install or change host or guest software. It does not replace the Node
28
+ hub, Runtime, CLI, or their SQLite stores. Studio can relay
29
+ `GET /api/control/account` to this service when `KXM_CONTROL_URL` is configured.
30
+
21
31
  ```mermaid
22
32
  flowchart LR
23
33
  cli["kxm CLI"]
24
- hub["hub"]
25
- sup["Runtime supervisor"]
34
+ hub["Node hub"]
35
+ sup["Node Runtime supervisor"]
26
36
  mcp["MCP server"]
27
37
  shot["one-shot harness"]
38
+ studio["Studio"]
39
+ control["MG1 Python control API"]
28
40
  cli -->|"kxm hub start"| hub
29
41
  mcp -->|"HTTP to hub"| hub
30
42
  cli -->|"kxm run"| sup
31
43
  sup -->|"spawn producer"| shot
44
+ studio -->|"bounded account read"| control
32
45
  ```
33
46
 
34
47
  ## Listeners
@@ -39,7 +52,8 @@ flowchart LR
39
52
  | Supervisor HTTP | `127.0.0.1` and an ephemeral port when the requested port is `0` |
40
53
  | MCP | stdio, no TCP port |
41
54
  | CLI and one-shot harness | no listener |
42
- | Studio, when served | `127.0.0.1:4242` from `createStudioServer` in `plugins/kxm/src/studio-layout.ts` |
55
+ | Studio, when served | `127.0.0.1:4242` from `createStudioServer` in `plugins/kxm/src/studio-layout.ts`; its optional control relay targets `KXM_CONTROL_URL` |
56
+ | Python control API | ASGI listener selected by deployment; the package declares no host or port default |
43
57
  | Antigravity OAuth callback | loopback port `51121` only during login (`plugins/kxm/src/providers/antigravity/auth/oauth.ts`) |
44
58
 
45
59
  ## Related
@@ -1,7 +1,8 @@
1
1
  # CI and release
2
2
 
3
- Pull requests run lint, typecheck, and the unit suite on Linux Node 24.
4
- Pushes to `main` also run that suite and the full OS × Node matrix.
3
+ Pull requests run lint, typecheck, the Node unit suite on Linux Node 24, and
4
+ the Python control-plane suite against PostgreSQL 17. Pushes to `main` also run
5
+ the Node suite and the full OS × Node matrix.
5
6
  The complete suite with coverage floors runs nightly, every merge to `main`
6
7
  cuts a patch release, and every release is verified before it reaches npm.
7
8
  This page explains which checks run where, how a merge becomes a published
@@ -15,7 +16,7 @@ release and an npm publish; the nightly job adds the slower suites.
15
16
 
16
17
  ```mermaid
17
18
  flowchart LR
18
- PR[Pull request] -->|"unit shards, docs lint, plugin validation"| REQ["CI / required"]
19
+ PR[Pull request] -->|"Node shards, Python/Postgres, docs lint, plugin validation"| REQ["CI / required"]
19
20
  REQ --> MERGE{Merged?}
20
21
  MERGE -->|yes| MAIN[main]
21
22
  MAIN -->|"unit shards plus validate:pr matrix"| PUSH[Push CI]
@@ -40,6 +41,7 @@ together.
40
41
  | Pull request, push to `main`, or manual | `ci.yml` (`required`) | Aggregates the lanes below into one pass/fail check named `CI / required` |
41
42
  | Pull request and push | `ci.yml` (Docs lint) | `lint:docs` and `check:versions`, always |
42
43
  | Code pull request and code push | `ci.yml` (Unit engine and Unit, Linux Node 24) | `engine.test.ts` split by test name; `permission.test.ts`, `runtime.test.ts`, and `package-install.test.ts` one file at a time; every other unit file in two light shards. Typecheck and `check-generated` run on `light-1` |
44
+ | Code pull request and code push | `ci.yml` (Python control plane) | Python 3.13 `unittest` contracts from the frozen uv workspace against PostgreSQL 17 started by `scripts/ci-postgres-17.sh` |
43
45
  | Platform-sensitive pull request | `ci.yml` (Unit, Windows Node 24) | The same unit lanes on `windows-latest` |
44
46
  | Push to `main`, or manual | `ci.yml` (Validate matrix) | `validate:pr` on Linux and Windows for Node 22.19.0 and Node 24; not on a pull request |
45
47
  | Code pull request and code push | `ci.yml` (Plugin validation) | `claude plugin validate --strict` on the marketplace and the plugin |
@@ -73,7 +75,8 @@ What moved off the pull-request lane, and what did not:
73
75
  runs `validate:pr` on Windows.
74
76
  - Coverage, `test/simulations`, and `npm pack --dry-run` stay in the nightly
75
77
  complete suite. They were not part of pull-request CI before this split.
76
- - Plugin validation still runs on code pull requests and code pushes.
78
+ - Python control-plane validation and Plugin validation still run on code pull
79
+ requests and code pushes.
77
80
 
78
81
  Run `npm run verify` locally before every push. A nightly failure is a release
79
82
  blocker: it is the only place the simulation suite and coverage floors run.
@@ -82,8 +85,13 @@ blocker: it is the only place the simulation suite and coverage floors run.
82
85
 
83
86
  Code pull requests run Docs lint, two Linux Node 24 engine shards, a serial
84
87
  lane (`permission.test.ts`, then `runtime.test.ts`, then
85
- `package-install.test.ts`), two light shards for every other unit file, and
86
- Plugin validation. `light-1` typechecks and checks generated bundles.
88
+ `package-install.test.ts`), two light shards for every other Node unit file,
89
+ the Python 3.13 control-plane suite against PostgreSQL 17, and Plugin
90
+ validation. The Python lane stays on `kontextmind-doks`. That scale set has
91
+ no container-engine socket, so the job starts PostgreSQL 17 with
92
+ `scripts/ci-postgres-17.sh` on `127.0.0.1:5432` and uses the same admin and
93
+ test DSNs as the contributor procedure in [Develop KXM](development.md).
94
+ `light-1` typechecks and checks generated bundles.
87
95
  `engine.test.ts` is split by test name because that file alone was 174
88
96
  seconds; the serial lane keeps the next longest files off the light pool,
89
97
  which was 268 seconds when every non-engine file shared one job.
@@ -165,9 +173,10 @@ workaround.
165
173
  The first job, Classify changes, lists the changed paths and sets `code=false`
166
174
  when every path matches `*.md` (including `plans/**/*.md`), `docs/**`,
167
175
  `.kxm/assets/**`, `LICENSE`, the issue and PR templates, or `dependabot.yml`.
168
- A non-markdown file under `plans/` is code. Unit lanes, Plugin validation,
169
- and the Validate matrix are skipped. Docs lint runs, and `CI / required`
170
- passes. `scripts/ci-classify.mjs` and `ci-contract.test.ts` pin this behavior.
176
+ A non-markdown file under `plans/` is code. Unit lanes, the Python control-plane
177
+ lane, Plugin validation, and the Validate matrix are skipped. Docs lint runs,
178
+ and `CI / required` passes. `scripts/ci-classify.mjs` and
179
+ `ci-contract.test.ts` pin this behavior.
171
180
 
172
181
  `platform=true` when a path is a package manifest, the lockfile, a file under
173
182
  `scripts/` or `.github/workflows/`, or a filename that names path, process,
@@ -198,3 +198,34 @@ Evidence: Evidence: `git diff HEAD origin/main` on the stale branch showed upstr
198
198
  - Interim recommendation: use the opt-in installer until OMP exposes a
199
199
  first-class transactional status-preset API.
200
200
  Evidence: `plugins/kxm/src/cli/plugins.ts`, `plugins/kxm/src/session-work.ts`, and `test/core/cli.test.ts`.
201
+
202
+ ## 2026-10-11 — Bring the witnessed MG1 Python foundation onto main (migration)
203
+
204
+ - Context: the tracker records MG1 as passed off-repo while the operator's
205
+ primary checkout holds the matching untracked Python workspace, MG0 evidence,
206
+ and Studio control-account proxy. Leaving the implementation outside Git
207
+ makes the recorded gate irreproducible and blocks MG2 from a reviewed base.
208
+ - Options:
209
+ - Keep MG1 off-repo and start the roadmap's role-authority work — weight 0.2.
210
+ Avoids a CI change, but preserves the tracker/source contradiction and risks
211
+ losing the only implementation matching the deployed witness.
212
+ - Import MG1 at `python/kxm-control`, keep Node as the current product owner,
213
+ and add a required Python/PostgreSQL contract lane — weight 0.9. Makes the
214
+ accepted foundation reproducible without claiming a Python cutover.
215
+ - Replace the Node CLI and Runtime with the Python package now — weight 0.1.
216
+ MG2 through MG8 are still open, so this would skip import, bridge, runner,
217
+ parity, and retirement gates.
218
+ - Selected: import the bounded MG1 foundation and its evidence, harden the
219
+ Studio proxy, and make the Python unit/contract suite part of `CI / required`.
220
+ SQLite and the Node Runtime remain authoritative. OpenAPI and the Studio
221
+ path inventory are derived from the FastAPI app; the checked generator emits
222
+ the revision helper. The package does not change host or guest installs.
223
+ After registration removal, registry routes return `registration_unavailable`
224
+ until the schema is installed again.
225
+ - Solid recommendation: keep every accepted migration wave in Git with its
226
+ generated contracts and a required service-backed test lane.
227
+ - Interim recommendation: if the service lane is unavailable, do not merge the
228
+ Python source; retain the off-repo tracker wording rather than claiming an
229
+ unverified gate.
230
+ Evidence: `plans/implementation-plan.md`, `plans/plan-python-migration.md`,
231
+ `python/kxm-control/tests/test_mg1.py`, and `.github/workflows/ci.yml`.
@@ -23,7 +23,10 @@ extension, the Claude Code plugin, or the bundled skills.
23
23
  `kxm lane run <unit> --brief <file> --workflow implement-only`.
24
24
  The read-only critic workflows on the same command are
25
25
  `review-arch-only` and `review-cli-only`.
26
- - Docker for the clean-container install smoke.
26
+ - Docker for the clean-container install smoke and the Python control-plane
27
+ contract suite.
28
+ - Python 3.13 and [`uv`](https://docs.astral.sh/uv/) for the Python
29
+ control-plane contract suite.
27
30
 
28
31
  ## Set up a checkout
29
32
 
@@ -255,6 +258,41 @@ It runs three scripts in order:
255
258
  | 2 | `npm run check` | `tsc --noEmit`, `markdownlint-cli2`, and version surfaces |
256
259
  | 3 | `npm run check:generated` | Generated artifacts are tracked and match the staged copy |
257
260
 
261
+ Changes to the Python control plane also run its frozen-workspace contract
262
+ suite before the npm gate. The suite drops and recreates `kxm_mg1_test`, so
263
+ the admin DSN account needs database create and drop privilege. The `postgres`
264
+ superuser in the container below is that account. Run PostgreSQL 17 in a
265
+ disposable container published only on loopback `127.0.0.1:5432`, with
266
+ `POSTGRES_PASSWORD=postgres`. Leave port `5432` on loopback free. Wait until
267
+ `pg_isready` succeeds, export the two DSNs CI uses, run the frozen suite, and
268
+ remove the container afterward:
269
+
270
+ ```bash
271
+ docker rm -f kxm-mg1-pg >/dev/null 2>&1 || true
272
+ docker run --detach --name kxm-mg1-pg \
273
+ --publish 127.0.0.1:5432:5432 \
274
+ --env POSTGRES_PASSWORD=postgres \
275
+ postgres:17
276
+ for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
277
+ docker exec kxm-mg1-pg pg_isready -U postgres && break
278
+ sleep 1
279
+ done
280
+ docker exec kxm-mg1-pg pg_isready -U postgres
281
+ export KXM_MG1_ADMIN_DSN='host=127.0.0.1 port=5432 dbname=postgres user=postgres password=postgres'
282
+ export KXM_MG1_TEST_DSN='host=127.0.0.1 port=5432 dbname=kxm_mg1_test user=postgres password=postgres'
283
+ uv run --project python --frozen python -m unittest discover -s python/kxm-control/tests
284
+ suite_status=$?
285
+ docker rm -f kxm-mg1-pg
286
+ (exit "$suite_status")
287
+ ```
288
+
289
+ The required CI aggregate repeats that suite against PostgreSQL 17 with the
290
+ same two DSNs and the same `postgres` superuser. On the `kontextmind-doks`
291
+ runners, `scripts/ci-postgres-17.sh` starts that server on `127.0.0.1:5432`
292
+ from a pinned PostgreSQL 17 binary. Contributors use the `postgres:17`
293
+ container procedure above, which publishes the same port and DSNs. The suite
294
+ remains a CI lane rather than a third root gate script.
295
+
258
296
  Every suite script passes `--test-force-exit`, and `scripts/run-bounded.mjs` stops the run if the suite is still going after its wall-clock limit (20 minutes, or 40 for the complete and coverage suites). The bound is that wall clock per script. A per-test timeout is not used because under `node --test` each file is itself a test, so a per-test timeout also bounds each file.
259
297
 
260
298
  Other scripts you will use:
@@ -30,6 +30,7 @@ Names that start with `KXM_` are not all operator settings. This map covers ever
30
30
  | Agents | `KXM_SERVER_URL`, `KXM_AUTH_TOKEN`, `KXM_PROJECT`, `KXM_AGENT_NAME`, `KXM_AGENT_PURPOSE` | [Agent settings](#agent-settings) |
31
31
  | Long-lived Pi workers | `KXM_PI_COMMAND`, `KXM_WORKER_*` (operator settings), `KXM_AGENT_LOG_PATH` | [Long-lived worker settings](#long-lived-worker-settings) |
32
32
  | Runtime supervisor | `KXM_STATE_HOME`, `KXM_RUNTIME_SYNC_INTERVAL_MS`, `KXM_RUNTIME_STOP_GRACE_MS` | [Runtime supervisor settings](#runtime-supervisor-settings) |
33
+ | Studio and Python control plane | `KXM_CONTROL_URL`, `KXM_KXMD_*`, `KXM_AUTHENTIK_*`, `KXM_POSTGRES_*`, `KXM_DATABASE`, `KXM_DB_USER`, `KXM_TEMPORAL_*` | [Studio and Python control-plane settings](#studio-and-python-control-plane-settings) |
33
34
  | 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
35
  | 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
36
  | 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) |
@@ -187,6 +188,27 @@ The Runtime supervisor runs `kxm run` workflows and syncs their summaries to the
187
188
  | `KXM_RUNTIME_SYNC_INTERVAL_MS` | `10000` | How often the supervisor heartbeats and pushes its outbox, clamped to `250` through `60000` |
188
189
  | `KXM_RUNTIME_STOP_GRACE_MS` | `30000` | How long a stopping supervisor waits for open drives, `0` to `600000`; an invalid value warns and uses the default |
189
190
 
191
+ ## Studio and Python control-plane settings
192
+
193
+ The MG1 Python service is additive: these values do not redirect the Node hub
194
+ or Runtime from SQLite. Requests cannot override its database host, database,
195
+ role, Temporal target, or namespace.
196
+
197
+ | Variable | Default | Effect |
198
+ |---|---|---|
199
+ | `KXM_CONTROL_URL` | Unset | Python control API base URL used by Studio's `GET /api/control/account` relay. The relay returns `control_unconfigured` when absent, and aborts a nonresponding upstream after three seconds with `control_unreachable` |
200
+ | `KXM_KXMD_PASSWORD` | None; required | PostgreSQL password for the fixed `kxmd` role. Keep it in the deployment secret manager |
201
+ | `KXM_POSTGRES_HOST` | `127.0.0.1` | PostgreSQL host selected by the service deployment, including an operator-owned tunnel |
202
+ | `KXM_POSTGRES_PORT` | `5432` | PostgreSQL port, from 1 through 65535 |
203
+ | `KXM_DATABASE` | `kxmd` | Fixed database assertion. Any other value refuses startup-time account resolution |
204
+ | `KXM_DB_USER` | `kxmd` | Fixed PostgreSQL role assertion. Any other value is refused |
205
+ | `KXM_TEMPORAL_TARGET` | `127.0.0.1:7233` | Temporal endpoint selected by the service deployment |
206
+ | `KXM_TEMPORAL_NAMESPACE` | `kxmd` | Fixed Temporal namespace assertion. Any other value is refused |
207
+ | `KXM_KXMD_ARTIFACTS` | `/var/lib/kxm/artifacts/kxmd` | Root for bounded per-tenant artifacts |
208
+ | `KXM_AUTHENTIK_ISSUER` | None; required for authenticated routes | Exact token issuer |
209
+ | `KXM_AUTHENTIK_AUDIENCE` | None; required for authenticated routes | Exact token audience |
210
+ | `KXM_AUTHENTIK_JWKS_URL` | None; required for authenticated routes | JWKS endpoint used to verify RS256 user and runner tokens |
211
+
190
212
  ## CLI and session settings
191
213
 
192
214
  | Variable | Default | Effect |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.181",
3
+ "version": "0.7.183",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -77,9 +77,9 @@
77
77
  ]
78
78
  },
79
79
  "dependencies": {
80
- "@anthropic-ai/claude-agent-sdk": "0.3.289",
81
- "@earendil-works/pi-tui": "1.0.2",
82
- "@modelcontextprotocol/sdk": "1.32.0",
80
+ "@anthropic-ai/claude-agent-sdk": "0.3.296",
81
+ "@earendil-works/pi-tui": "1.1.0",
82
+ "@modelcontextprotocol/sdk": "1.32.1",
83
83
  "yaml": "^2.9.0"
84
84
  },
85
85
  "peerDependencies": {
@@ -95,17 +95,17 @@
95
95
  }
96
96
  },
97
97
  "devDependencies": {
98
- "@earendil-works/pi-coding-agent": "1.0.2",
99
- "@modelcontextprotocol/sdk": "1.32.0",
100
- "@playwright/test": "1.63.0",
101
- "@types/node": "26.6.4",
98
+ "@earendil-works/pi-coding-agent": "1.1.0",
99
+ "@modelcontextprotocol/sdk": "1.32.1",
100
+ "@playwright/test": "1.64.0",
101
+ "@types/node": "26.6.5",
102
102
  "ajv": "8.20.0",
103
103
  "commander": "15.0.0",
104
104
  "esbuild": "0.28.2",
105
105
  "markdownlint-cli2": "0.23.3",
106
- "nx": "23.2.1",
106
+ "nx": "23.3.0",
107
107
  "tsx": "^4.23.13",
108
- "typebox": "1.3.34",
108
+ "typebox": "1.3.36",
109
109
  "typescript": "7.0.2"
110
110
  },
111
111
  "files": [
@@ -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.181",
5
+ "version": "0.7.183",
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",
@@ -38995,6 +38995,11 @@ function generateStudioLayout(plan, state) {
38995
38995
  };
38996
38996
  }
38997
38997
  var DEFAULT_STUDIO_PORT = 4242;
38998
+ function isKxmdAccountView(value) {
38999
+ if (typeof value !== "object" || value === null) return false;
39000
+ const account = value;
39001
+ return account.account === "kxmd" && account.database === "kxmd" && account.namespace === "kxmd" && account.role === "kxmd";
39002
+ }
38998
39003
  function createStudioServer(options = {}) {
38999
39004
  const targetPort = options.port ?? DEFAULT_STUDIO_PORT;
39000
39005
  const host = options.host ?? "127.0.0.1";
@@ -39013,6 +39018,46 @@ function createStudioServer(options = {}) {
39013
39018
  res.end(JSON.stringify({ ok: true, studio: true, port: targetPort }));
39014
39019
  return;
39015
39020
  }
39021
+ if (url.pathname === "/api/control/account" && req.method === "GET") {
39022
+ const base = options.controlUrl ?? process.env.KXM_CONTROL_URL;
39023
+ if (!base) {
39024
+ res.writeHead(503, { "Content-Type": "application/json" });
39025
+ res.end(JSON.stringify({ ok: false, error: "control_unconfigured" }));
39026
+ return;
39027
+ }
39028
+ let upstream;
39029
+ try {
39030
+ upstream = await fetch(new URL("/v1/account", base), {
39031
+ signal: AbortSignal.timeout(3e3)
39032
+ });
39033
+ } catch {
39034
+ res.writeHead(502, { "Content-Type": "application/json" });
39035
+ res.end(JSON.stringify({ ok: false, error: "control_unreachable" }));
39036
+ return;
39037
+ }
39038
+ const contentType = upstream.headers.get("content-type");
39039
+ if (!contentType?.toLowerCase().startsWith("application/json")) {
39040
+ res.writeHead(502, { "Content-Type": "application/json" });
39041
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
39042
+ return;
39043
+ }
39044
+ let body;
39045
+ try {
39046
+ body = await upstream.json();
39047
+ } catch {
39048
+ res.writeHead(502, { "Content-Type": "application/json" });
39049
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
39050
+ return;
39051
+ }
39052
+ if (upstream.ok && !isKxmdAccountView(body)) {
39053
+ res.writeHead(502, { "Content-Type": "application/json" });
39054
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
39055
+ return;
39056
+ }
39057
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
39058
+ res.end(JSON.stringify(body));
39059
+ return;
39060
+ }
39016
39061
  if (url.pathname === "/api/layout" && req.method === "GET") {
39017
39062
  const plan = options.planProvider?.();
39018
39063
  if (!plan) {
@@ -45197,6 +45242,9 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
45197
45242
  if (term.startsWith("screen")) {
45198
45243
  return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
45199
45244
  }
45245
+ if (termProgram === "herdr") {
45246
+ return { images: null, trueColor: hasTrueColorHint, hyperlinks: true };
45247
+ }
45200
45248
  if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
45201
45249
  return { images: "kitty", trueColor: true, hyperlinks: true };
45202
45250
  }
@@ -46352,6 +46400,10 @@ var Box = class {
46352
46400
  setBgFn(bgFn) {
46353
46401
  this.bgFn = bgFn;
46354
46402
  }
46403
+ setPaddingX(paddingX) {
46404
+ this.paddingX = paddingX;
46405
+ this.invalidateCache();
46406
+ }
46355
46407
  invalidateCache() {
46356
46408
  this.cache = void 0;
46357
46409
  }
@@ -46461,11 +46513,11 @@ var TUI_KEYBINDINGS = {
46461
46513
  description: "Move cursor word right"
46462
46514
  },
46463
46515
  "tui.editor.cursorLineStart": {
46464
- defaultKeys: ["home", "ctrl+home", "ctrl+a"],
46516
+ defaultKeys: ["home", "ctrl+a"],
46465
46517
  description: "Move to line start"
46466
46518
  },
46467
46519
  "tui.editor.cursorLineEnd": {
46468
- defaultKeys: ["end", "ctrl+end", "ctrl+e"],
46520
+ defaultKeys: ["end", "ctrl+e"],
46469
46521
  description: "Move to line end"
46470
46522
  },
46471
46523
  "tui.editor.jumpForward": {
@@ -46570,8 +46622,8 @@ var TUI_KEYBINDINGS = {
46570
46622
  defaultKeys: "escape",
46571
46623
  description: "Close transcript search"
46572
46624
  },
46573
- "tui.altScreen.top": { defaultKeys: "home", description: "Scroll viewport to top" },
46574
- "tui.altScreen.bottom": { defaultKeys: "end", description: "Scroll viewport to bottom" }
46625
+ "tui.altScreen.top": { defaultKeys: "ctrl+home", description: "Scroll viewport to top" },
46626
+ "tui.altScreen.bottom": { defaultKeys: "ctrl+end", description: "Scroll viewport to bottom" }
46575
46627
  };
46576
46628
  function normalizeKeys(keys) {
46577
46629
  if (keys === void 0)
@@ -46692,6 +46744,10 @@ var Text = class {
46692
46744
  this.cachedWidth = void 0;
46693
46745
  this.cachedLines = void 0;
46694
46746
  }
46747
+ setPaddingX(paddingX) {
46748
+ this.paddingX = paddingX;
46749
+ this.invalidate();
46750
+ }
46695
46751
  invalidate() {
46696
46752
  this.cachedText = void 0;
46697
46753
  this.cachedWidth = void 0;
@@ -47805,6 +47861,40 @@ function getNativePlatformHelper() {
47805
47861
  return loadNativePlatformHelper(process.platform);
47806
47862
  }
47807
47863
 
47864
+ // node_modules/@earendil-works/pi-tui/dist/program-status.js
47865
+ var PROGRAM_STATUS_QUERY = "\x1B]7501;?\x1B\\";
47866
+ function isProgramStatusReply(sequence) {
47867
+ return /^\x1b\]7501;\?[^\x07\x1b]*(?:\x07|\x1b\\)$/.test(sequence);
47868
+ }
47869
+ var APP_PATTERN = /^[A-Za-z0-9_.+-]{1,32}$/;
47870
+ var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f-\u009f]+/g;
47871
+ var MAX_MESSAGE_BYTES = 2048;
47872
+ function truncateUtf8(text, maxBytes) {
47873
+ if (Buffer.byteLength(text, "utf8") <= maxBytes)
47874
+ return text;
47875
+ let bytes = 0;
47876
+ let end = 0;
47877
+ for (const char of text) {
47878
+ const size = Buffer.byteLength(char, "utf8");
47879
+ if (bytes + size > maxBytes)
47880
+ break;
47881
+ bytes += size;
47882
+ end += char.length;
47883
+ }
47884
+ return text.slice(0, end);
47885
+ }
47886
+ function formatProgramStatus(status) {
47887
+ const pairs = [`state=${status.state}`];
47888
+ if (status.app !== void 0 && APP_PATTERN.test(status.app))
47889
+ pairs.push(`app=${status.app}`);
47890
+ if (status.state === "blocked" && status.kind)
47891
+ pairs.push(`kind=${status.kind}`);
47892
+ const message = truncateUtf8((status.message ?? "").replace(CONTROL_CHARACTERS2, " ").trim(), MAX_MESSAGE_BYTES);
47893
+ if (message)
47894
+ pairs.push(`msg=${Buffer.from(message, "utf8").toString("base64")}`);
47895
+ return `\x1B]7501;${pairs.join(":")}\x1B\\`;
47896
+ }
47897
+
47808
47898
  // node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js
47809
47899
  import { EventEmitter as EventEmitter2 } from "events";
47810
47900
  var ESC = "\x1B";
@@ -48106,7 +48196,8 @@ var TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1B]9;4;0\x07";
48106
48196
  var NATIVE_SHIFT_ENTER_SEQUENCE = "\x1B[13;2u";
48107
48197
  var DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS = 7;
48108
48198
  var KEYBOARD_PROTOCOL_RESPONSE_FRAGMENT_TIMEOUT_MS = 150;
48109
- var KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1B[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1B[?u\x1B[c`;
48199
+ var KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1B[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1B[?u`;
48200
+ var DEVICE_ATTRIBUTES_QUERY = "\x1B[c";
48110
48201
  function parseKeyboardProtocolNegotiationSequence(sequence) {
48111
48202
  const kittyFlags = sequence.match(/^\x1b\[\?(\d+)u$/);
48112
48203
  if (kittyFlags) {
@@ -48162,6 +48253,12 @@ var ProcessTerminal = class {
48162
48253
  stdinBuffer;
48163
48254
  stdinDataHandler;
48164
48255
  progressInterval;
48256
+ /** Latest program status, kept across stop() so start() can report it again. */
48257
+ programStatus;
48258
+ /** Whether the terminal confirmed OSC 7501 support since the last start(), or `PI_PROGRAM_STATUS=1`. */
48259
+ programStatusSupported = false;
48260
+ /** The support query was sent and its DA1 sentinel has not arrived yet. */
48261
+ programStatusQueryPending = false;
48165
48262
  writeLogPath = (() => {
48166
48263
  const env = process.env.PI_TUI_WRITE_LOG || "";
48167
48264
  if (!env)
@@ -48208,6 +48305,14 @@ var ProcessTerminal = class {
48208
48305
  setupStdinBuffer() {
48209
48306
  this.stdinBuffer = new StdinBuffer({ escapeTimeout: resolveEscapeTimeoutMs() });
48210
48307
  this.stdinBuffer.on("data", (sequence) => {
48308
+ if (isProgramStatusReply(sequence)) {
48309
+ if (this.programStatusQueryPending) {
48310
+ this.programStatusQueryPending = false;
48311
+ this.programStatusSupported = true;
48312
+ this.writeProgramStatus();
48313
+ }
48314
+ return;
48315
+ }
48211
48316
  const negotiation = this.readKeyboardProtocolNegotiationSequence(sequence);
48212
48317
  if (negotiation === "pending") {
48213
48318
  this.scheduleKeyboardProtocolNegotiationBufferFlush();
@@ -48239,6 +48344,9 @@ var ProcessTerminal = class {
48239
48344
  * - 1 = disambiguate escape codes
48240
48345
  * - 2 = report event types (press/repeat/release)
48241
48346
  * - 4 = report alternate keys (shifted key, base layout key)
48347
+ *
48348
+ * The OSC 7501 program status query shares the DA sentinel: a terminal that supports it replies
48349
+ * before DA. `PI_PROGRAM_STATUS=1` or `0` skips the query.
48242
48350
  */
48243
48351
  queryAndEnableKittyProtocol() {
48244
48352
  this.setupStdinBuffer();
@@ -48246,7 +48354,12 @@ var ProcessTerminal = class {
48246
48354
  this.keyboardProtocolPushed = true;
48247
48355
  this.pendingKeyboardProtocolDeviceAttributes += 1;
48248
48356
  this.clearKeyboardProtocolNegotiationBuffer();
48249
- process.stdout.write(KITTY_KEYBOARD_PROTOCOL_QUERY);
48357
+ const programStatusOverride = process.env.PI_PROGRAM_STATUS;
48358
+ this.programStatusSupported = programStatusOverride === "1";
48359
+ this.programStatusQueryPending = programStatusOverride !== "1" && programStatusOverride !== "0";
48360
+ const programStatusQuery = this.programStatusQueryPending ? PROGRAM_STATUS_QUERY : "";
48361
+ process.stdout.write(`${KITTY_KEYBOARD_PROTOCOL_QUERY}${programStatusQuery}${DEVICE_ATTRIBUTES_QUERY}`);
48362
+ this.writeProgramStatus();
48250
48363
  }
48251
48364
  handleKeyboardProtocolNegotiationSequence(negotiationSequence) {
48252
48365
  this.clearKeyboardProtocolNegotiationBuffer();
@@ -48254,6 +48367,8 @@ var ProcessTerminal = class {
48254
48367
  if (this.pendingKeyboardProtocolDeviceAttributes === 0)
48255
48368
  return false;
48256
48369
  this.pendingKeyboardProtocolDeviceAttributes -= 1;
48370
+ if (this.pendingKeyboardProtocolDeviceAttributes === 0)
48371
+ this.programStatusQueryPending = false;
48257
48372
  }
48258
48373
  if (negotiationSequence.type === "kitty-flags") {
48259
48374
  if (negotiationSequence.flags !== 0) {
@@ -48395,6 +48510,11 @@ var ProcessTerminal = class {
48395
48510
  if (this.clearProgressInterval()) {
48396
48511
  process.stdout.write(TERMINAL_PROGRESS_CLEAR_SEQUENCE);
48397
48512
  }
48513
+ if (this.programStatusSupported && this.programStatus) {
48514
+ process.stdout.write(formatProgramStatus({ state: "clear" }));
48515
+ }
48516
+ this.programStatusSupported = false;
48517
+ this.programStatusQueryPending = false;
48398
48518
  process.stdout.write("\x1B[?2004l");
48399
48519
  const shouldDisableKittyProtocol = this.keyboardProtocolPushed || this._kittyProtocolActive;
48400
48520
  this.clearKeyboardProtocolNegotiationBuffer();
@@ -48463,6 +48583,16 @@ var ProcessTerminal = class {
48463
48583
  setTitle(title) {
48464
48584
  process.stdout.write(`\x1B]0;${title}\x07`);
48465
48585
  }
48586
+ setProgramStatus(status) {
48587
+ this.programStatus = status.state === "clear" ? void 0 : status;
48588
+ if (this.programStatusSupported)
48589
+ process.stdout.write(formatProgramStatus(status));
48590
+ }
48591
+ writeProgramStatus() {
48592
+ if (this.programStatusSupported && this.programStatus) {
48593
+ process.stdout.write(formatProgramStatus(this.programStatus));
48594
+ }
48595
+ }
48466
48596
  setProgress(active) {
48467
48597
  if (active) {
48468
48598
  process.stdout.write(TERMINAL_PROGRESS_ACTIVE_SEQUENCE);
@@ -49280,6 +49410,11 @@ var TuiAltScreen = class extends TuiBase {
49280
49410
  return false;
49281
49411
  return this.copyTextToClipboard(text);
49282
49412
  }
49413
+ /** Drop the text selection and multi-click history, e.g. before the host replaces the transcript. */
49414
+ resetTextSelection() {
49415
+ this.clearTextSelection();
49416
+ this.lastClick = void 0;
49417
+ }
49283
49418
  /** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
49284
49419
  getScreenLines() {
49285
49420
  return [...this.previousScreen];
@@ -41853,6 +41853,9 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
41853
41853
  if (term.startsWith("screen")) {
41854
41854
  return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
41855
41855
  }
41856
+ if (termProgram === "herdr") {
41857
+ return { images: null, trueColor: hasTrueColorHint, hyperlinks: true };
41858
+ }
41856
41859
  if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
41857
41860
  return { images: "kitty", trueColor: true, hyperlinks: true };
41858
41861
  }
@@ -43008,6 +43011,10 @@ var Box = class {
43008
43011
  setBgFn(bgFn) {
43009
43012
  this.bgFn = bgFn;
43010
43013
  }
43014
+ setPaddingX(paddingX) {
43015
+ this.paddingX = paddingX;
43016
+ this.invalidateCache();
43017
+ }
43011
43018
  invalidateCache() {
43012
43019
  this.cache = void 0;
43013
43020
  }
@@ -43117,11 +43124,11 @@ var TUI_KEYBINDINGS = {
43117
43124
  description: "Move cursor word right"
43118
43125
  },
43119
43126
  "tui.editor.cursorLineStart": {
43120
- defaultKeys: ["home", "ctrl+home", "ctrl+a"],
43127
+ defaultKeys: ["home", "ctrl+a"],
43121
43128
  description: "Move to line start"
43122
43129
  },
43123
43130
  "tui.editor.cursorLineEnd": {
43124
- defaultKeys: ["end", "ctrl+end", "ctrl+e"],
43131
+ defaultKeys: ["end", "ctrl+e"],
43125
43132
  description: "Move to line end"
43126
43133
  },
43127
43134
  "tui.editor.jumpForward": {
@@ -43226,8 +43233,8 @@ var TUI_KEYBINDINGS = {
43226
43233
  defaultKeys: "escape",
43227
43234
  description: "Close transcript search"
43228
43235
  },
43229
- "tui.altScreen.top": { defaultKeys: "home", description: "Scroll viewport to top" },
43230
- "tui.altScreen.bottom": { defaultKeys: "end", description: "Scroll viewport to bottom" }
43236
+ "tui.altScreen.top": { defaultKeys: "ctrl+home", description: "Scroll viewport to top" },
43237
+ "tui.altScreen.bottom": { defaultKeys: "ctrl+end", description: "Scroll viewport to bottom" }
43231
43238
  };
43232
43239
  function normalizeKeys(keys) {
43233
43240
  if (keys === void 0)
@@ -43348,6 +43355,10 @@ var Text = class {
43348
43355
  this.cachedWidth = void 0;
43349
43356
  this.cachedLines = void 0;
43350
43357
  }
43358
+ setPaddingX(paddingX) {
43359
+ this.paddingX = paddingX;
43360
+ this.invalidate();
43361
+ }
43351
43362
  invalidate() {
43352
43363
  this.cachedText = void 0;
43353
43364
  this.cachedWidth = void 0;
@@ -44422,7 +44433,7 @@ var cjsRequire = createRequire4(import.meta.url);
44422
44433
 
44423
44434
  // node_modules/@earendil-works/pi-tui/dist/terminal.js
44424
44435
  var DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS = 7;
44425
- var KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1B[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1B[?u\x1B[c`;
44436
+ var KITTY_KEYBOARD_PROTOCOL_QUERY = `\x1B[>${DESIRED_KITTY_KEYBOARD_PROTOCOL_FLAGS}u\x1B[?u`;
44426
44437
 
44427
44438
  // node_modules/@earendil-works/pi-tui/dist/alt-screen-search.js
44428
44439
  var segmenter2 = getGraphemeSegmenter();
@@ -45219,6 +45230,11 @@ var TuiAltScreen = class extends TuiBase {
45219
45230
  return false;
45220
45231
  return this.copyTextToClipboard(text);
45221
45232
  }
45233
+ /** Drop the text selection and multi-click history, e.g. before the host replaces the transcript. */
45234
+ resetTextSelection() {
45235
+ this.clearTextSelection();
45236
+ this.lastClick = void 0;
45237
+ }
45222
45238
  /** The lines of the last rendered frame, one per terminal row, as written to the terminal. */
45223
45239
  getScreenLines() {
45224
45240
  return [...this.previousScreen];
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
25462
25462
  }
25463
25463
 
25464
25464
  // plugins/kxm/src/mcp-server.ts
25465
- var VERSION = "0.7.181";
25465
+ var VERSION = "0.7.183";
25466
25466
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25467
25467
  var inbox = /* @__PURE__ */ new Map();
25468
25468
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.181",
3
+ "version": "0.7.183",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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.181";
14
+ const VERSION = "0.7.183";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -290,6 +290,7 @@ export interface StudioServerOptions {
290
290
  planProvider?: (() => KxmCompiledPlan | undefined) | undefined;
291
291
  stateProvider?: (() => KxmRunState | undefined) | undefined;
292
292
  onMutation?: ((command: string, args: Record<string, unknown>) => Promise<{ ok: boolean; result?: unknown; error?: string }> | { ok: boolean; result?: unknown; error?: string }) | undefined;
293
+ controlUrl?: string | undefined;
293
294
  }
294
295
 
295
296
  export interface StudioServerHandle {
@@ -299,6 +300,15 @@ export interface StudioServerHandle {
299
300
  close: () => Promise<void>;
300
301
  }
301
302
 
303
+ function isKxmdAccountView(value: unknown): boolean {
304
+ if (typeof value !== "object" || value === null) return false;
305
+ const account = value as Record<string, unknown>;
306
+ return account.account === "kxmd"
307
+ && account.database === "kxmd"
308
+ && account.namespace === "kxmd"
309
+ && account.role === "kxmd";
310
+ }
311
+
302
312
  /**
303
313
  * Embedded Web Studio Local Server (Decision Q8 & D14).
304
314
  * Serves the visual DAG, form stepper, and Temporal swimlane dashboards on http://localhost:4242.
@@ -328,6 +338,46 @@ export function createStudioServer(options: StudioServerOptions = {}): StudioSer
328
338
  return;
329
339
  }
330
340
 
341
+ if (url.pathname === "/api/control/account" && req.method === "GET") {
342
+ const base = options.controlUrl ?? process.env.KXM_CONTROL_URL;
343
+ if (!base) {
344
+ res.writeHead(503, { "Content-Type": "application/json" });
345
+ res.end(JSON.stringify({ ok: false, error: "control_unconfigured" }));
346
+ return;
347
+ }
348
+ let upstream: Response;
349
+ try {
350
+ upstream = await fetch(new URL("/v1/account", base), {
351
+ signal: AbortSignal.timeout(3_000),
352
+ });
353
+ } catch {
354
+ res.writeHead(502, { "Content-Type": "application/json" });
355
+ res.end(JSON.stringify({ ok: false, error: "control_unreachable" }));
356
+ return;
357
+ }
358
+ const contentType = upstream.headers.get("content-type");
359
+ if (!contentType?.toLowerCase().startsWith("application/json")) {
360
+ res.writeHead(502, { "Content-Type": "application/json" });
361
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
362
+ return;
363
+ }
364
+ let body: unknown;
365
+ try {
366
+ body = await upstream.json();
367
+ } catch {
368
+ res.writeHead(502, { "Content-Type": "application/json" });
369
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
370
+ return;
371
+ }
372
+ if (upstream.ok && !isKxmdAccountView(body)) {
373
+ res.writeHead(502, { "Content-Type": "application/json" });
374
+ res.end(JSON.stringify({ ok: false, error: "control_response_invalid" }));
375
+ return;
376
+ }
377
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
378
+ res.end(JSON.stringify(body));
379
+ return;
380
+ }
331
381
  if (url.pathname === "/api/layout" && req.method === "GET") {
332
382
  const plan = options.planProvider?.();
333
383
  if (!plan) {
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bash
2
+ # Start PostgreSQL 17 on 127.0.0.1:5432 for the Python MG1 lane.
3
+ # The kontextmind-doks scale set has a client binary and no engine socket, so
4
+ # this job provisions the server from a pinned archive instead of a workflow
5
+ # service. A second call in the same job returns once that server is ready.
6
+
7
+ set -euo pipefail
8
+
9
+ version=17.11.0
10
+ triple=x86_64-unknown-linux-gnu
11
+ sha256=b7a1ba6bae6499d8296e3e81b0171eecfd1766ca9aaa0057e41ad3e844e5e2e0
12
+ archive_name="postgresql-${version}-${triple}.tar.gz"
13
+ url="https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/${archive_name}"
14
+
15
+ if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then
16
+ echo "PostgreSQL 17 CI archive is linux/x86_64 only (got $(uname -s)/$(uname -m))" >&2
17
+ exit 1
18
+ fi
19
+
20
+ work="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/kxm-mg1-postgres"
21
+ prefix="${work}/postgresql-${version}-${triple}"
22
+ pgdata="${work}/data"
23
+ pglog="${work}/postgres.log"
24
+ pwfile="${work}/pwfile"
25
+ mkdir -p "$work"
26
+
27
+ package_for_library() {
28
+ case "$1" in
29
+ libzstd.so.*) echo libzstd1 ;;
30
+ liblz4.so.*) echo liblz4-1 ;;
31
+ libxml2.so.*) echo libxml2 ;;
32
+ liblzma.so.*) echo liblzma5 ;;
33
+ libssl.so.3 | libcrypto.so.3) echo libssl3 ;;
34
+ libicuuc.so.74 | libicui18n.so.74 | libicudata.so.74) echo libicu74 ;;
35
+ libgssapi_krb5.so.*) echo libgssapi-krb5-2 ;;
36
+ libkrb5.so.* | libk5crypto.so.* | libkrb5support.so.*) echo libkrb5-3 ;;
37
+ libcom_err.so.*) echo libcom-err2 ;;
38
+ libkeyutils.so.*) echo libkeyutils1 ;;
39
+ libstdc++.so.*) echo libstdc++6 ;;
40
+ libgcc_s.so.*) echo libgcc-s1 ;;
41
+ libz.so.*) echo zlib1g ;;
42
+ *) return 1 ;;
43
+ esac
44
+ }
45
+
46
+ server_version_num() {
47
+ PGPASSWORD=postgres "${prefix}/bin/psql" \
48
+ -h 127.0.0.1 -p 5432 -U postgres -d postgres \
49
+ -Atqc "SHOW server_version_num"
50
+ }
51
+
52
+ postgres_17_ready() {
53
+ [[ -x "${prefix}/bin/pg_isready" ]] || return 1
54
+ "${prefix}/bin/pg_isready" -h 127.0.0.1 -p 5432 -U postgres >/dev/null 2>&1 || return 1
55
+ local num
56
+ num="$(server_version_num 2>/dev/null)" || return 1
57
+ [[ "$num" -ge 170000 && "$num" -lt 180000 ]]
58
+ }
59
+
60
+ install_missing_libraries() {
61
+ local -a missing=()
62
+ local line
63
+ while IFS= read -r line; do
64
+ [[ -n "$line" ]] && missing+=("$line")
65
+ done < <(ldd "${prefix}/bin/postgres" | awk '/not found$/ { print $1 }')
66
+ if [[ ${#missing[@]} -eq 0 ]]; then
67
+ return 0
68
+ fi
69
+
70
+ local -a packages=()
71
+ local lib pkg
72
+ for lib in "${missing[@]}"; do
73
+ if ! pkg="$(package_for_library "$lib")"; then
74
+ echo "PostgreSQL 17 needs ${lib}, which has no package mapping on this runner" >&2
75
+ ldd "${prefix}/bin/postgres" >&2 || true
76
+ exit 1
77
+ fi
78
+ packages+=("$pkg")
79
+ done
80
+ mapfile -t packages < <(printf '%s\n' "${packages[@]}" | awk '!seen[$0]++')
81
+ echo "Installing shared libraries for PostgreSQL 17: ${packages[*]}"
82
+ sudo -n apt-get update
83
+ sudo -n DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
84
+
85
+ local -a still=()
86
+ while IFS= read -r line; do
87
+ [[ -n "$line" ]] && still+=("$line")
88
+ done < <(ldd "${prefix}/bin/postgres" | awk '/not found$/ { print $1 }')
89
+ if [[ ${#still[@]} -gt 0 ]]; then
90
+ echo "PostgreSQL 17 still needs: ${still[*]}" >&2
91
+ exit 1
92
+ fi
93
+ }
94
+
95
+ if postgres_17_ready; then
96
+ echo "PostgreSQL 17 already ready on 127.0.0.1:5432 (server_version_num=$(server_version_num))"
97
+ exit 0
98
+ fi
99
+
100
+ if [[ ! -x "${prefix}/bin/postgres" ]]; then
101
+ echo "Downloading PostgreSQL ${version}"
102
+ curl --fail --silent --show-error --location --retry 5 --retry-delay 2 \
103
+ --output "${work}/${archive_name}" \
104
+ "$url"
105
+ echo "${sha256} ${work}/${archive_name}" | sha256sum -c -
106
+ tar -xzf "${work}/${archive_name}" -C "$work"
107
+ fi
108
+ install_missing_libraries
109
+
110
+ if [[ ! -s "${pgdata}/PG_VERSION" ]]; then
111
+ printf '%s\n' postgres >"$pwfile"
112
+ chmod 600 "$pwfile"
113
+ "${prefix}/bin/initdb" \
114
+ --username=postgres \
115
+ --pwfile="$pwfile" \
116
+ --auth-host=scram-sha-256 \
117
+ --auth-local=trust \
118
+ --encoding=UTF8 \
119
+ --locale=C \
120
+ --set listen_addresses=127.0.0.1 \
121
+ --set port=5432 \
122
+ --set shared_buffers=16MB \
123
+ --set max_connections=20 \
124
+ --set shared_memory_type=mmap \
125
+ --set dynamic_shared_memory_type=mmap \
126
+ --set huge_pages=off \
127
+ --set fsync=off \
128
+ --set synchronous_commit=off \
129
+ --set full_page_writes=off \
130
+ --pgdata="$pgdata"
131
+ fi
132
+
133
+ if ! "${prefix}/bin/pg_ctl" -D "$pgdata" -l "$pglog" -w -t 30 start; then
134
+ echo "PostgreSQL 17 failed to start" >&2
135
+ cat "$pglog" >&2 || true
136
+ exit 1
137
+ fi
138
+
139
+ num="$(server_version_num)"
140
+ if [[ "$num" -lt 170000 || "$num" -ge 180000 ]]; then
141
+ echo "PostgreSQL 17 required, server_version_num=${num}" >&2
142
+ exit 1
143
+ fi
144
+ echo "PostgreSQL 17 ready on 127.0.0.1:5432 (server_version_num=${num})"