@kontextmind/kxm 0.7.166 → 0.7.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +18 -0
- package/docs/README.md +1 -0
- package/docs/architecture/hosted-direction.md +9 -4
- package/docs/architecture/inventory.md +1 -0
- package/docs/contributing/ci-and-release.md +6 -1
- package/docs/contributing/decisions.md +153 -0
- package/docs/contributing/learnings.md +54 -53
- package/docs/contributing/operating-rules.md +19 -6
- package/docs/operations/runtime-sync.md +2 -1
- package/docs/operations/troubleshooting.md +4 -0
- package/docs/operations/upgrade.md +18 -3
- package/docs/operations.md +70 -7
- package/docs/reference/cli-reference.md +21 -6
- package/docs/reference/config-reference.md +4 -1
- package/docs/start/install.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +170 -36
- package/plugins/kxm/dist/cli.js +332 -129
- package/plugins/kxm/dist/extension.js +184 -53
- package/plugins/kxm/dist/mcp-server.js +97 -40
- package/plugins/kxm/dist/runtime-supervisor.js +113 -42
- package/plugins/kxm/dist/runtime.js +113 -42
- package/plugins/kxm/dist/server.js +2 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
- package/plugins/kxm/src/cli/hub.ts +42 -7
- package/plugins/kxm/src/cli/system.ts +3 -1
- package/plugins/kxm/src/cli/types.ts +5 -3
- package/plugins/kxm/src/cli.ts +2 -1
- package/plugins/kxm/src/hub-binding.ts +301 -55
- package/plugins/kxm/src/hub-identity.ts +17 -9
- package/plugins/kxm/src/kxm-update.ts +29 -6
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-supervisor.ts +22 -4
- package/scripts/kxm-publish-npm.mjs +47 -1
package/docs/operations.md
CHANGED
|
@@ -88,16 +88,32 @@ kxm hub bind --cloud \
|
|
|
88
88
|
http://127.0.0.1:17331
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Expected output:
|
|
91
|
+
Expected output when `KXMD_HUB_TOKEN` is set:
|
|
92
92
|
|
|
93
93
|
```text
|
|
94
94
|
bound hub http://127.0.0.1:17331 · remote · health=on · token is not stored
|
|
95
95
|
hub health=true ready=true · remote hub · mode=cloud project=prj_example (.kxm/config.yaml hub.cloud.project) key=env:KXMD_HUB_TOKEN
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
With only `--token-command`, and `KXMD_HUB_TOKEN` unset, `kxm hub view` prints
|
|
99
|
+
`key=token-command`. It names the source that would actually supply the token:
|
|
100
|
+
`env:<NAME>` only when that variable is non-empty, otherwise `op:<ref>`,
|
|
101
|
+
`token-command`, `hub-env`, or `missing`. An unset variable is not reported as
|
|
102
|
+
the source.
|
|
103
|
+
|
|
98
104
|
`--token-env` and `--token-command` can be combined. A non-empty variable wins.
|
|
99
105
|
Otherwise the command runs. The command is a program and arguments, not a shell
|
|
100
|
-
pipeline (`|`, `&`, `;`, and redirection are refused).
|
|
106
|
+
pipeline (`|`, `&`, `;`, and redirection are refused). `--token-command` is
|
|
107
|
+
stored only in the machine-wide binding (`hub-binding.cloud.json` under the
|
|
108
|
+
user state root). It is not written to `.kxm/config.yaml`. Config holds
|
|
109
|
+
`key.op` and `key.env` references only.
|
|
110
|
+
|
|
111
|
+
`https://hub.kxmd.dev` is behind an Authentik forward-auth proxy. Clients
|
|
112
|
+
cannot use it: `GET /health` returns the login page (HTTP 200 HTML, or a 302).
|
|
113
|
+
`kxm hub view` treats that as unhealthy (`health=false`, and the JSON `health`
|
|
114
|
+
field is `{ "error": "auth_proxy" }`, not the HTML). `kxm hub bind` refuses
|
|
115
|
+
with `hub_not_kxm` and writes nothing unless you pass `--force`. The supported
|
|
116
|
+
client path is the SSH forward above.
|
|
101
117
|
|
|
102
118
|
<details><summary>PowerShell</summary>
|
|
103
119
|
|
|
@@ -114,8 +130,8 @@ kxm hub view
|
|
|
114
130
|
| | Local bind | `--cloud` bind |
|
|
115
131
|
|---|---|---|
|
|
116
132
|
| Loopback URL | Scope `loopback`. No credential required | Scope `remote`, including an SSH forward |
|
|
117
|
-
| Token stored in
|
|
118
|
-
| Credential later commands send | `KXM_AUTH_TOKEN`, then
|
|
133
|
+
| Token stored in the binding | No | No. `hub-binding.cloud.json` names `tokenEnv` and/or `tokenCommand`. `hub-binding.json` stays a three-key local record, or is omitted, so a 0.7.159 reader does not throw |
|
|
134
|
+
| Credential later commands send | Agent commands use `KXM_AUTH_TOKEN`, then `hub.local` `key.env` / `key.op`, then this project's saved project token. They never use the hub-env admin token (`project_token_missing`). `kxm hub view` and `kxm tenant status` may use the admin token | The active mode's key reference, then the binding's named source. Hub-env is not a fallback |
|
|
119
135
|
| Missing source | Agent commands report `project_token_missing` and name the project id that was sent | `cloud_token_missing` or `cloud_token_command_failed`. The local admin token is not sent |
|
|
120
136
|
| `kxm hub start` from an extension | May start a local hub | Does not start a local hub when the forward is down (`cloud_hub_unreachable`) |
|
|
121
137
|
|
|
@@ -124,6 +140,47 @@ kxm hub view
|
|
|
124
140
|
all use that source. `kxm agent worker` puts `KXM_AUTH_TOKEN` in the child
|
|
125
141
|
process environment only. It does not write the token to disk.
|
|
126
142
|
|
|
143
|
+
A local bind does not give agents the admin token from hub-env. Give them a
|
|
144
|
+
project token in one of these ways:
|
|
145
|
+
|
|
146
|
+
- On the client, export `KXM_AUTH_TOKEN` to that project's token, or set
|
|
147
|
+
`hub.local.key.env` / `hub.local.key.op` in `.kxm/config.yaml`.
|
|
148
|
+
- On the hub, map the project id in `hub.projects` (an `op://` reference or an
|
|
149
|
+
environment variable name) or in `KXM_PROJECT_TOKENS`, keyed by the same id
|
|
150
|
+
the client sends.
|
|
151
|
+
|
|
152
|
+
### Where to store tokens
|
|
153
|
+
|
|
154
|
+
Keep hub admin and project tokens in 1Password and point KXM at `op://` references
|
|
155
|
+
(`--key-op`, `key.op`, or `--token-command "op read op://..."`). Do not paste the
|
|
156
|
+
token into `.kxm/config.yaml`, `hub-binding.json`, or `hub-binding.cloud.json`.
|
|
157
|
+
|
|
158
|
+
A hub host that has no `op` binary should take the token from the environment
|
|
159
|
+
or from a systemd credentials file. The unit in
|
|
160
|
+
[Supervise the hub and Runtime](operations/deploy.md#the-hub) loads
|
|
161
|
+
`EnvironmentFile=/etc/kxm/hub.env` (mode 0600) for `KXM_AUTH_TOKEN` and
|
|
162
|
+
`KXM_PROJECT_TOKENS`.
|
|
163
|
+
|
|
164
|
+
### Restart after an upgrade, and before a cloud bind
|
|
165
|
+
|
|
166
|
+
Restart the Runtime supervisor, the hub, and workers after upgrading and before
|
|
167
|
+
`kxm hub bind --cloud`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
kxm runtime stop
|
|
171
|
+
kxm hub stop
|
|
172
|
+
kxm runtime start
|
|
173
|
+
kxm hub start
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A 0.7.159 supervisor treats any extra key in `hub-binding.json` as
|
|
177
|
+
`malformed hub binding` and stops syncing. This release keeps cloud fields in
|
|
178
|
+
`hub-binding.cloud.json` and leaves `hub-binding.json` as a three-key local
|
|
179
|
+
record, or omits it. An older reader then keeps the local hub instead of
|
|
180
|
+
throwing. A binding newer than this build is ignored with a warning, and sync
|
|
181
|
+
continues as `no_hub` rather than stalling. Processes that were already running
|
|
182
|
+
keep the binding they loaded at start, so restart them after the upgrade.
|
|
183
|
+
|
|
127
184
|
The hub project id is the same value everywhere: `--project` or `KXM_PROJECT`,
|
|
128
185
|
then the active mode's `project` in `.kxm/config.yaml`, then the `id` in
|
|
129
186
|
`.kxm/project.yaml`, then `package.json` `name`, then the directory name.
|
|
@@ -147,7 +204,8 @@ hub:
|
|
|
147
204
|
op: op://Private/kxm/local-project-token
|
|
148
205
|
env: KXM_LOCAL_PROJECT_TOKEN
|
|
149
206
|
cloud:
|
|
150
|
-
|
|
207
|
+
# Clients use an SSH forward. https://hub.kxmd.dev is an Authentik login page.
|
|
208
|
+
url: http://127.0.0.1:17331
|
|
151
209
|
project: prj_example
|
|
152
210
|
key:
|
|
153
211
|
op: op://Private/kxmd/project-token
|
|
@@ -167,7 +225,8 @@ with `hub_config_invalid`.
|
|
|
167
225
|
`kxm hub bind` and `kxm hub bind --cloud` write the mode, URL, project id, and
|
|
168
226
|
key reference. `--key-op` is the `op://` reference for either mode.
|
|
169
227
|
`--token-env` names the variable for either mode. `--token-command` stays
|
|
170
|
-
cloud-only
|
|
228
|
+
cloud-only, and only in `hub-binding.cloud.json`, not in this file.
|
|
229
|
+
`kxm hub view` prints the mode, URL, project id, and key source.
|
|
171
230
|
It does not print the token. `kxm hub unbind` removes the binding and
|
|
172
231
|
`hub.mode`, and leaves the `local` and `cloud` blocks in place.
|
|
173
232
|
|
|
@@ -185,7 +244,8 @@ send the `.kxm/project.yaml` id and the hub has no token for it.
|
|
|
185
244
|
kxm hub unbind
|
|
186
245
|
```
|
|
187
246
|
|
|
188
|
-
Unbind deletes `hub-binding.json` and
|
|
247
|
+
Unbind deletes `hub-binding.json` and `hub-binding.cloud.json`, and clears
|
|
248
|
+
`hub.mode` in `.kxm/config.yaml`.
|
|
189
249
|
The `local` and `cloud` blocks stay, so the next bind can switch mode again.
|
|
190
250
|
It does not delete hub-env, and it does not close the SSH forward. Stop the
|
|
191
251
|
forward by ending the `ssh -N` session.
|
|
@@ -200,6 +260,9 @@ forward by ending the `ssh -N` session.
|
|
|
200
260
|
| `cloud_token_command_failed` | `op` exited non-zero, timed out, or printed more than one line | Run the same command in a terminal and sign in to 1Password. Do not paste the token into the binding |
|
|
201
261
|
| Clients through one SSH host get locked out together | The hub rate-limits by agent id, otherwise by source address. Every forward from one SSH session shares `127.0.0.1` on the hub. `sshd` `MaxStartups` and `MaxSessions` can refuse new forwards first | Give each long-lived agent its own name. Raise the SSH limits on that host if many people share one jump host. Do not point two tenants at one hub |
|
|
202
262
|
| `kxm hub view` says `loopback` for the forward | `--cloud` was omitted | Bind again with `--cloud`. `KXM_SERVER_URL` overrides the binding URL and is labeled on its own |
|
|
263
|
+
| `hub_not_kxm`, or `kxm hub view` prints `auth proxy` with `health=false` | The URL is an Authentik (or other) login page, or a 30x redirect, not a kxm hub. `https://hub.kxmd.dev` is that case | Bind `http://127.0.0.1:17331` through the SSH forward. `--force` writes the binding anyway and is not a working client path |
|
|
264
|
+
| `key=env:KXMD_HUB_TOKEN` while that variable is unset | An older `kxm hub view` named the configured variable even when it was empty | Upgrade. The line names `token-command`, `op:<ref>`, `hub-env`, or `missing` for the source that actually supplies the token |
|
|
265
|
+
| `malformed hub binding` and sync stops after a cloud bind | A supervisor older than this release read cloud fields in `hub-binding.json` | Upgrade, then restart the supervisor, hub, and workers before binding `--cloud` again |
|
|
203
266
|
|
|
204
267
|
## Next steps
|
|
205
268
|
|
|
@@ -477,12 +477,12 @@ Starts, inspects, and stops the local KXM hub, and binds this machine to a hub.
|
|
|
477
477
|
kxm hub view
|
|
478
478
|
```
|
|
479
479
|
|
|
480
|
-
Calls `GET /health` and `GET /ready` on the target hub and reports whether the URL is loopback or remote, plus the mode, project id, and key source in effect. The key source is
|
|
480
|
+
Calls `GET /health` and `GET /ready` on the target hub and reports whether the URL is loopback or remote, plus the mode, project id, and key source in effect. A probe counts only when the response is JSON (`application/json`, or `text/plain` with a JSON object) of the hub shape: `/health` is `{ "ok": true, "agents": <number> }` and `/ready` is `{ "ok": true, "storage": "sqlite"|"memory" }`. An HTTP 200 HTML login page, or a 30x redirect, is not healthy. The JSON `health` and `ready` fields then hold `{ "error": "auth_proxy" }` and never the HTML. The key source is the one that would supply the token: `env:<NAME>` only when that variable is non-empty, `op:<ref>`, `token-command`, `hub-env`, or `missing`. The token is not printed.
|
|
481
481
|
|
|
482
482
|
No command-specific options.
|
|
483
483
|
|
|
484
484
|
- Needs a hub to succeed. Reads only.
|
|
485
|
-
- JSON keys: `target` (`url`, `scope`, and `source: "env"` when `KXM_SERVER_URL` overrides a binding), `identity` (`mode`, `modeSource`, `url`, `urlSource`, `project`, `projectSource`, `keySource`), `health`, `ready`. An unreachable hub reports `{"error":"hub_unreachable"}` for both probes.
|
|
485
|
+
- JSON keys: `target` (`url`, `scope`, and `source: "env"` when `KXM_SERVER_URL` overrides a binding), `identity` (`mode`, `modeSource`, `url`, `urlSource`, `project`, `projectSource`, `keySource`), `health`, `ready`. An unreachable hub reports `{"error":"hub_unreachable"}` for both probes. An auth proxy or redirect reports `{"error":"auth_proxy"}`.
|
|
486
486
|
- Exit 0 when both probes succeed, 1 otherwise. A cloud mode with no resolvable key exits 2 with `cloud_token_missing`.
|
|
487
487
|
|
|
488
488
|
```bash
|
|
@@ -569,15 +569,16 @@ kxm hub stop --wait-ms 8000 --json
|
|
|
569
569
|
### `kxm hub bind`
|
|
570
570
|
|
|
571
571
|
```text
|
|
572
|
-
kxm hub bind <url> [--cloud] [--token-env <name>] [--token-command <command>] [--key-op <op://vault/item/field>]
|
|
572
|
+
kxm hub bind <url> [--cloud] [--token-env <name>] [--token-command <command>] [--key-op <op://vault/item/field>] [--force]
|
|
573
573
|
```
|
|
574
574
|
|
|
575
|
-
Binds this machine to a running hub by writing
|
|
575
|
+
Binds this machine to a running hub by writing the binding under the user state root and the non-secret endpoint (mode, URL, project id, key reference) into `.kxm/config.yaml`, then probes the hub's health for up to 300 ms. A local bind writes `hub-binding.json` (`schema`, `url`, `boundAt` only). A `--cloud` bind writes `hub-binding.cloud.json` and leaves a three-key local file in place, or omits `hub-binding.json`, so a 0.7.159 reader does not throw `malformed hub binding` and stop sync. `--token-command` is stored only in that cloud file, not in `.kxm/config.yaml`. Every hub client uses that endpoint when `KXM_SERVER_URL` is unset. The token is never written.
|
|
576
576
|
|
|
577
577
|
- Arguments: `<url>`, Hub base URL (http or https).
|
|
578
578
|
- A URL with credentials, a query, a fragment, or a scheme other than http or https fails with `hub_url_invalid` (exit 2).
|
|
579
579
|
- A remote (non-loopback) URL is refused with `hub_bind_unauthenticated` (exit 2, `nextAction: "export_kxm_auth_token"`) unless a credential for the current project resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`. A malformed record fails with `hub_credential_unreadable` (exit 2). A loopback URL without `--cloud` still binds with no credential.
|
|
580
580
|
- `--cloud` marks the binding remote even when the URL is a loopback forward. It requires `--token-env`, `--token-command`, `--key-op`, or a combination. The token is not written. A non-empty variable wins; otherwise an `op://` reference is read with `op read`, or the command runs. `--token-command` without `--cloud` fails with `cloud_flag_required`. `--token-env` and `--key-op` are also valid on a local bind and are stored only as references in config. A missing cloud source later fails with `cloud_token_missing` and does not fall back to hub-env. See [Cross-box peer attach](../operations.md#cross-box-peer-attach).
|
|
581
|
+
- The probe accepts a kxm `/health` body. A timeout (`health=unknown`) or a connection failure (`health=off`) still writes the binding. An HTML login page or a 30x redirect is `hub_not_kxm` (exit 2, `kind` `auth_proxy` or `redirect`) and writes nothing. The error names an auth proxy or a redirect. `--force` writes anyway. `https://hub.kxmd.dev` is an Authentik login page; clients use the SSH forward at `http://127.0.0.1:17331`.
|
|
581
582
|
- Mutates the binding file and `.kxm/config.yaml`. `--dry-run` validates and prints the plan without writing.
|
|
582
583
|
- JSON keys: `url`, `scope`, `file`, `configFile`, `project`, `health` (`on`, `off`, or `unknown`), `probeMs`. A cloud bind also returns `cloud`, and `tokenEnv` or `tokenCommand` when set.
|
|
583
584
|
|
|
@@ -984,7 +985,20 @@ Checks for or applies a KXM operator package update, and runs the native updater
|
|
|
984
985
|
|
|
985
986
|
- Arguments: `[harness]`, Harness id (default: every detected harness).
|
|
986
987
|
- `--check` cannot be combined with any other update flag or a harness (exit 2, `scope_conflict`), and at most one of `--self`, `--extensions`, `--models` is allowed (exit 2).
|
|
987
|
-
- From a source checkout, `--check` reports the running version without network access, and `--kxm` is refused (exit 2, `install_kind_source`) with an instruction to `git pull`. Only npm-global installs can apply `--kxm`; other install kinds exit 2 with `install_kind_<kind>`. A GitHub release must publish a sha256 digest for `kxm-<version>.tgz` or the install fails closed (`release_digest_missing`, `release_digest_mismatch`).
|
|
988
|
+
- From a source checkout, `--check` reports the running version without network access, and `--kxm` is refused (exit 2, `install_kind_source`) with an instruction to `git pull`. Only npm-global installs can apply `--kxm`; other install kinds exit 2 with `install_kind_<kind>`. A GitHub release must publish a sha256 digest for `kxm-<version>.tgz` or the install fails closed (`release_digest_missing`, `release_digest_mismatch`). When `gh` is missing, the github source downloads that same asset with `curl --fail --silent --show-error --location` and still verifies the digest before `npm install -g`.
|
|
989
|
+
|
|
990
|
+
On a host with neither `gh` nor a working `kxm`, download and check it yourself:
|
|
991
|
+
|
|
992
|
+
```bash
|
|
993
|
+
version=0.7.166
|
|
994
|
+
curl --fail --silent --show-error --location \
|
|
995
|
+
--output "kxm-${version}.tgz" \
|
|
996
|
+
"https://github.com/kontextmind/kxm/releases/download/v${version}/kxm-${version}.tgz"
|
|
997
|
+
# Compare with the sha256 digest published on the GitHub release asset.
|
|
998
|
+
sha256sum "kxm-${version}.tgz"
|
|
999
|
+
npm install --global --omit=peer "./kxm-${version}.tgz"
|
|
1000
|
+
```
|
|
1001
|
+
|
|
988
1002
|
- Settings come only from `update.yaml` under the user state root (`auto: true` enables auto-apply); a project `.kxm/update.yaml` is ignored with a warning.
|
|
989
1003
|
- Without `--check` or a lone `--kxm`, KXM probes harnesses (as `harness list` does) and runs each updater for the selected scope. An unknown harness id exits 2 (`unknown_harness` step); a failed step exits 1.
|
|
990
1004
|
- Honors `--dry-run`: steps are planned, not run, and the cached update notice is not refreshed.
|
|
@@ -1612,7 +1626,7 @@ Create a KXM run without executing steps. The run pins the project's `homeRuntim
|
|
|
1612
1626
|
- Refuses `--workspace` (exit 2, `workspace_option_unsupported`).
|
|
1613
1627
|
- Needs a KXM project. Starts and uses the Runtime; no hub needed. Honors `--dry-run`, which validates the project and prints the plan without starting the supervisor.
|
|
1614
1628
|
- JSON keys: `idempotent`, `run` (`runId`, `homeRuntimeId`, `status`, `configRevision`), `supervisor` (`runtimeId`, `port`, `started`), and `execution` (`status: not_started`, `mode: live`, `defaultHarness`, `authentication: not_checked`, `prerequisites`, `nextSteps`). Dry run: `projectRoot`, `workflowId`, `configRevision`, `defaultHarness`, `prerequisites`. The obsolete `phase: pre-3a` field is no longer returned.
|
|
1615
|
-
- Errors: `workflow_required` (exit 2), `project_required`, `run_workflow_unknown`, `brief_and_prompt`, `brief_unreadable`, `lane_missing`, `lane_run_open`, `run_failed` with `issues` (any invalid file in the project fails the load, for example `gate_outcome_impossible`), `run_io_failed` (exit 1).
|
|
1629
|
+
- Errors: `workflow_required` (exit 2), `project_required`, `run_workflow_unknown`, `brief_and_prompt`, `brief_unreadable`, `lane_missing`, `lane_run_open`, `run_failed` with `issues` (any invalid file in the project fails the load, for example `gate_outcome_impossible`), `run_io_failed` (exit 1). `project_home_conflict` means this checkout is not the registered home for that project id: a different repository already owns it, the home runtime is immutable, or the primary checkout is already the home row. Use the original checkout, or a worktree of that repository. Do not copy the project id into an unrelated clone, and do not delete the database. `runtime_schema_newer` means the store was written by a newer release than this build. Upgrade KXM. Do not delete the database.
|
|
1616
1630
|
- The `default` workflow that `kxm init` writes does not set `limits.maxAgentTimeMs`, so it can be driven. A workflow that sets that limit is created, but `kxm runs drive` refuses it with `run_handoff_required` (`limit_unsupported`); projects from older `kxm init` templates carry it on `default`.
|
|
1617
1631
|
|
|
1618
1632
|
```bash
|
|
@@ -1826,6 +1840,7 @@ No command-specific options.
|
|
|
1826
1840
|
- Reads only. Never starts the supervisor.
|
|
1827
1841
|
- JSON keys: `running`, `runtimeId`, `pid`, `port`, `state`, `heartbeatAt`, `startedAt`, `sync`.
|
|
1828
1842
|
- Exit 0 when running, 1 when not (the JSON still says `ok: true`).
|
|
1843
|
+
- When the supervisor process is up but `GET /v1/sync/status` fails, the text line is `sync: the supervisor is running but did not answer /v1/sync/status`. The usual causes are a hung supervisor, a token the status command does not hold, or a supervisor started before an upgrade. `kxm runtime stop` and `kxm runtime start` replace it. A binding this build cannot read is a warning and `no_hub`, not a stalled sync; restart after upgrading and before a cloud bind. See [Check sync status](../operations/runtime-sync.md#check-sync-status).
|
|
1829
1844
|
|
|
1830
1845
|
```bash
|
|
1831
1846
|
kxm runtime status
|
|
@@ -1311,6 +1311,8 @@ no recorded cost is never ready.
|
|
|
1311
1311
|
```yaml
|
|
1312
1312
|
# .kxm/config.yaml (project scope) or ~/.config/kxm/config.yaml (user scope).
|
|
1313
1313
|
# The project file wins. Key fields are references, never tokens.
|
|
1314
|
+
# --token-command is not a config field. It lives only in the machine-wide
|
|
1315
|
+
# hub-binding.cloud.json written by `kxm hub bind --cloud`.
|
|
1314
1316
|
hub:
|
|
1315
1317
|
autoStart: background # background | off
|
|
1316
1318
|
mode: local # local | cloud
|
|
@@ -1321,7 +1323,8 @@ hub:
|
|
|
1321
1323
|
op: op://Private/kxm/local-project-token
|
|
1322
1324
|
env: KXM_LOCAL_PROJECT_TOKEN
|
|
1323
1325
|
cloud:
|
|
1324
|
-
|
|
1326
|
+
# Clients use an SSH forward. https://hub.kxmd.dev is an Authentik login page.
|
|
1327
|
+
url: http://127.0.0.1:17331
|
|
1325
1328
|
project: prj_example
|
|
1326
1329
|
key:
|
|
1327
1330
|
op: op://Private/kxmd/project-token
|
package/docs/start/install.md
CHANGED
|
@@ -8,7 +8,7 @@ KXM has three parts you can install: the `kxm` command-line tool, the Claude Cod
|
|
|
8
8
|
- Git.
|
|
9
9
|
- Claude Code, if you want the plugin.
|
|
10
10
|
- Pi, if you want Pi agents. Install it with `npm install --global @earendil-works/pi-coding-agent` and sign in to a model provider as the [Pi documentation](https://pi.dev/docs/latest) describes.
|
|
11
|
-
- The GitHub CLI (`gh`), only if you plan to update with `kxm update --kxm
|
|
11
|
+
- The GitHub CLI (`gh`), only if you plan to update with `kxm update --kxm` from the GitHub source. When `gh` is missing, that command falls back to curl and still checks the release checksum. The manual curl commands are in [Upgrade KXM](../operations/upgrade.md#update-the-cli-without-gh).
|
|
12
12
|
|
|
13
13
|
## Choose what to install
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.168",
|
|
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",
|
|
@@ -556,6 +556,9 @@ function resolveUserStateRoot(env) {
|
|
|
556
556
|
function hubBindingFile(env = process.env) {
|
|
557
557
|
return join3(resolveUserStateRoot(env), "hub-binding.json");
|
|
558
558
|
}
|
|
559
|
+
function hubBindingCloudFile(env = process.env) {
|
|
560
|
+
return join3(resolveUserStateRoot(env), "hub-binding.cloud.json");
|
|
561
|
+
}
|
|
559
562
|
function validateHubUrl(raw) {
|
|
560
563
|
let parsed;
|
|
561
564
|
try {
|
|
@@ -618,33 +621,35 @@ function isAbortError(error) {
|
|
|
618
621
|
error && typeof error === "object" && ("name" in error && error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR")
|
|
619
622
|
);
|
|
620
623
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
function bindingFallbackWarning(file, reason) {
|
|
625
|
+
return `hub binding at ${file} ${reason}; falling back to the local hub. Restart the Runtime supervisor, the hub, and workers after upgrading and before a cloud bind.`;
|
|
626
|
+
}
|
|
627
|
+
function writeBindingJson(file, value) {
|
|
628
|
+
mkdirSync3(dirname(file), { recursive: true, mode: 448 });
|
|
629
|
+
const temporary = join3(dirname(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
|
|
630
|
+
try {
|
|
631
|
+
writeFileSync2(temporary, `${JSON.stringify(value, null, 2)}
|
|
632
|
+
`, { encoding: "utf8", mode: 384 });
|
|
633
|
+
renameSync(temporary, file);
|
|
634
|
+
} finally {
|
|
635
|
+
rmSync(temporary, { force: true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function parseBindingObject(file) {
|
|
639
|
+
if (!existsSync4(file)) return new HubBindingError(`missing hub binding at ${file}`);
|
|
624
640
|
let parsed;
|
|
625
641
|
try {
|
|
626
642
|
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
627
643
|
} catch {
|
|
628
|
-
|
|
644
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
629
645
|
}
|
|
630
646
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
const row = parsed;
|
|
634
|
-
const keys = Object.keys(row);
|
|
635
|
-
const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
|
|
636
|
-
const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
|
|
637
|
-
if (keys.some((key) => !allowed.has(key)) || !cloudish && keys.length !== 3 || row.schema !== HUB_BINDING_SCHEMA || typeof row.url !== "string" || typeof row.boundAt !== "string" || !isIsoTimestamp(row.boundAt)) {
|
|
638
|
-
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
639
|
-
}
|
|
640
|
-
let url;
|
|
641
|
-
try {
|
|
642
|
-
url = validateHubUrl(row.url);
|
|
643
|
-
} catch {
|
|
644
|
-
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
647
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
645
648
|
}
|
|
646
|
-
|
|
647
|
-
|
|
649
|
+
return parsed;
|
|
650
|
+
}
|
|
651
|
+
function cloudRecordFromRow(file, row, url, boundAt) {
|
|
652
|
+
if (row.cloud !== true) return void 0;
|
|
648
653
|
let tokenEnv;
|
|
649
654
|
let tokenCommand;
|
|
650
655
|
if (row.tokenEnv !== void 0) {
|
|
@@ -666,36 +671,163 @@ function readHubBinding(env = process.env) {
|
|
|
666
671
|
return {
|
|
667
672
|
schema: HUB_BINDING_SCHEMA,
|
|
668
673
|
url,
|
|
669
|
-
boundAt
|
|
674
|
+
boundAt,
|
|
670
675
|
cloud: true,
|
|
671
676
|
...tokenEnv ? { tokenEnv } : {},
|
|
672
677
|
...tokenCommand ? { tokenCommand } : {}
|
|
673
678
|
};
|
|
674
679
|
}
|
|
680
|
+
function interpretBindingFile(file) {
|
|
681
|
+
if (!existsSync4(file)) return {};
|
|
682
|
+
const parsed = parseBindingObject(file);
|
|
683
|
+
if (parsed instanceof HubBindingError) throw parsed;
|
|
684
|
+
const keys = Object.keys(parsed);
|
|
685
|
+
const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
|
|
686
|
+
if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
|
|
687
|
+
return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
|
|
688
|
+
}
|
|
689
|
+
if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
|
|
690
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
691
|
+
}
|
|
692
|
+
let url;
|
|
693
|
+
try {
|
|
694
|
+
url = validateHubUrl(parsed.url);
|
|
695
|
+
} catch {
|
|
696
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
697
|
+
}
|
|
698
|
+
const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
|
|
699
|
+
if (!cloudish) {
|
|
700
|
+
if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
701
|
+
return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
|
|
702
|
+
}
|
|
703
|
+
const record = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
|
|
704
|
+
if (!record) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
705
|
+
return { record, legacyCloud: true };
|
|
706
|
+
}
|
|
707
|
+
function loadHubBinding(env = process.env) {
|
|
708
|
+
const cloudFile = hubBindingCloudFile(env);
|
|
709
|
+
const cloud = interpretBindingFile(cloudFile);
|
|
710
|
+
if (cloud.warning && !cloud.record) {
|
|
711
|
+
const local = existsSync4(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
|
|
712
|
+
if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
|
|
713
|
+
return { warning: cloud.warning };
|
|
714
|
+
}
|
|
715
|
+
if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
|
|
716
|
+
const main2 = interpretBindingFile(hubBindingFile(env));
|
|
717
|
+
if (main2.legacyCloud && main2.record) {
|
|
718
|
+
writeBindingJson(cloudFile, main2.record);
|
|
719
|
+
rmSync(hubBindingFile(env), { force: true });
|
|
720
|
+
return {
|
|
721
|
+
record: main2.record,
|
|
722
|
+
warning: bindingFallbackWarning(
|
|
723
|
+
hubBindingFile(env),
|
|
724
|
+
"stored a cloud bind in the pre-0.7.160 file"
|
|
725
|
+
)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { ...main2.record ? { record: main2.record } : {}, ...main2.warning ? { warning: main2.warning } : {} };
|
|
729
|
+
}
|
|
730
|
+
function readHubBinding(env = process.env) {
|
|
731
|
+
return loadHubBinding(env).record;
|
|
732
|
+
}
|
|
733
|
+
function contentTypeBase(header) {
|
|
734
|
+
return (header ?? "").split(";")[0].trim().toLowerCase();
|
|
735
|
+
}
|
|
736
|
+
function looksLikeHtml(contentType, body) {
|
|
737
|
+
if (contentType === "text/html" || contentType === "application/xhtml+xml") return true;
|
|
738
|
+
const sample = body.slice(0, 800).trim().toLowerCase();
|
|
739
|
+
return sample.startsWith("<!doctype html") || sample.startsWith("<html") || sample.includes("<form") && (sample.includes("login") || sample.includes("authentik") || sample.includes("password"));
|
|
740
|
+
}
|
|
741
|
+
function jsonObject(text) {
|
|
742
|
+
try {
|
|
743
|
+
const parsed = JSON.parse(text);
|
|
744
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
745
|
+
return parsed;
|
|
746
|
+
} catch {
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function hubEndpointShape(role, body) {
|
|
751
|
+
if (role === "health") return body.ok === true && typeof body.agents === "number";
|
|
752
|
+
return body.ok === true && (body.storage === "sqlite" || body.storage === "memory");
|
|
753
|
+
}
|
|
754
|
+
function classifyHubResponse(input) {
|
|
755
|
+
const contentType = contentTypeBase(input.contentType);
|
|
756
|
+
if (input.status >= 300 && input.status < 400) {
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
status: input.status,
|
|
760
|
+
kind: "redirect",
|
|
761
|
+
detail: `HTTP ${input.status} redirect; this looks like an auth proxy or a redirect, not a kxm hub`,
|
|
762
|
+
body: { error: "auth_proxy", detail: `HTTP ${input.status} redirect` }
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (looksLikeHtml(contentType, input.bodyText)) {
|
|
766
|
+
return {
|
|
767
|
+
ok: false,
|
|
768
|
+
status: input.status,
|
|
769
|
+
kind: "auth_proxy",
|
|
770
|
+
detail: `HTTP ${input.status} returned an HTML login page${contentType ? ` (${contentType})` : ""}; this looks like an auth proxy, not a kxm hub`,
|
|
771
|
+
body: { error: "auth_proxy", detail: "HTML login page" }
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const jsonType = contentType === "application/json" || contentType === "text/plain" || contentType === "";
|
|
775
|
+
const body = jsonType ? jsonObject(input.bodyText) : void 0;
|
|
776
|
+
if (body && hubEndpointShape(input.role, body) && input.status >= 200 && input.status < 300) {
|
|
777
|
+
if (contentType !== "" && contentType !== "application/json" && contentType !== "text/plain") {
|
|
778
|
+
return {
|
|
779
|
+
ok: false,
|
|
780
|
+
status: input.status,
|
|
781
|
+
kind: "not_hub",
|
|
782
|
+
detail: `HTTP ${input.status} content-type ${contentType} is not application/json`,
|
|
783
|
+
body: { error: "not_hub", detail: contentType }
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return { ok: true, status: input.status, kind: "hub", detail: "kxm hub", body };
|
|
787
|
+
}
|
|
788
|
+
const typeNote = contentType && contentType !== "application/json" ? ` content-type ${contentType}` : "";
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
status: input.status,
|
|
792
|
+
kind: "not_hub",
|
|
793
|
+
detail: `HTTP ${input.status}${typeNote} is not a kxm hub ${input.role} response`,
|
|
794
|
+
body: { error: "not_hub", detail: `HTTP ${input.status}${typeNote}` }
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function readHubEndpoint(url, role, fetchImpl, init = {}) {
|
|
798
|
+
try {
|
|
799
|
+
const response = await fetchImpl(`${url}/${role}`, {
|
|
800
|
+
...init.headers ? { headers: init.headers } : {},
|
|
801
|
+
...init.signal ? { signal: init.signal } : {},
|
|
802
|
+
redirect: "manual"
|
|
803
|
+
});
|
|
804
|
+
const bodyText = await response.text();
|
|
805
|
+
return classifyHubResponse({
|
|
806
|
+
role,
|
|
807
|
+
status: response.status,
|
|
808
|
+
contentType: response.headers.get("content-type"),
|
|
809
|
+
bodyText
|
|
810
|
+
});
|
|
811
|
+
} catch (error) {
|
|
812
|
+
if (isAbortError(error)) {
|
|
813
|
+
return { ok: false, status: 0, kind: "timeout", detail: "no reply within the probe budget", body: { error: "hub_unreachable" } };
|
|
814
|
+
}
|
|
815
|
+
return { ok: false, status: 0, kind: "unreachable", detail: "nothing answered", body: { error: "hub_unreachable" } };
|
|
816
|
+
}
|
|
817
|
+
}
|
|
675
818
|
async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
|
|
676
819
|
const started = Date.now();
|
|
677
820
|
const controller = new AbortController();
|
|
678
821
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
679
822
|
try {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
body = await response.json();
|
|
685
|
-
} catch {
|
|
686
|
-
return { health: "unknown", probeMs: Date.now() - started };
|
|
687
|
-
}
|
|
688
|
-
if (body && typeof body === "object" && body.ok === true) {
|
|
689
|
-
return { health: "on", probeMs: Date.now() - started };
|
|
690
|
-
}
|
|
691
|
-
return { health: "unknown", probeMs: Date.now() - started };
|
|
692
|
-
} catch (error) {
|
|
693
|
-
return { health: isAbortError(error) ? "unknown" : "off", probeMs: Date.now() - started };
|
|
823
|
+
const verdict = await readHubEndpoint(url, "health", fetchImpl, { signal: controller.signal });
|
|
824
|
+
const health = verdict.kind === "hub" ? "on" : verdict.kind === "unreachable" ? "off" : "unknown";
|
|
825
|
+
return { health, probeMs: Date.now() - started, kind: verdict.kind, detail: verdict.detail, status: verdict.status };
|
|
694
826
|
} finally {
|
|
695
827
|
clearTimeout(timer);
|
|
696
828
|
}
|
|
697
829
|
}
|
|
698
|
-
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError;
|
|
830
|
+
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError, LEGACY_BINDING_KEYS, BINDING_KEYS;
|
|
699
831
|
var init_hub_binding = __esm({
|
|
700
832
|
"plugins/kxm/src/hub-binding.ts"() {
|
|
701
833
|
"use strict";
|
|
@@ -716,6 +848,8 @@ var init_hub_binding = __esm({
|
|
|
716
848
|
this.name = "HubBindingError";
|
|
717
849
|
}
|
|
718
850
|
};
|
|
851
|
+
LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
|
|
852
|
+
BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
|
|
719
853
|
}
|
|
720
854
|
});
|
|
721
855
|
|