@kontextmind/kxm 0.7.164 → 0.7.165
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 +9 -0
- package/README.md +2 -0
- package/docs/README.md +1 -0
- package/docs/architecture/inventory.md +4 -3
- package/docs/glossary.md +3 -3
- package/docs/operations/troubleshooting.md +3 -0
- package/docs/operations.md +154 -0
- package/docs/reference/cli-reference.md +4 -3
- package/docs/reference/configuration.md +11 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +2 -2
- package/plugins/kxm/README.md +2 -2
- package/plugins/kxm/dist/claude-hook.js +120 -16
- package/plugins/kxm/dist/cli.js +14929 -14542
- package/plugins/kxm/dist/extension.js +299 -122
- package/plugins/kxm/dist/mcp-server.js +7690 -106
- package/plugins/kxm/dist/runtime-supervisor.js +151 -4
- package/plugins/kxm/dist/runtime.js +158 -11
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +5 -1
- package/plugins/kxm/src/cli/context-skills.ts +64 -10
- package/plugins/kxm/src/cli/hub.ts +128 -17
- package/plugins/kxm/src/cli/project.ts +7 -3
- package/plugins/kxm/src/cli/types.ts +6 -0
- package/plugins/kxm/src/cli/workflows.ts +12 -1
- package/plugins/kxm/src/cli.ts +23 -5
- package/plugins/kxm/src/extension.ts +7 -3
- package/plugins/kxm/src/hub-autostart.ts +3 -0
- package/plugins/kxm/src/hub-binding.ts +182 -2
- package/plugins/kxm/src/hub-env.ts +71 -0
- package/plugins/kxm/src/mcp-server.ts +3 -2
- package/plugins/kxm/src/project-name.ts +34 -7
- package/plugins/kxm/src/runtime-supervisor.ts +9 -3
- package/plugins/kxm/src/tenant-status.ts +9 -2
- package/scripts/build-runtime.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- **`kxm hub bind --cloud` attaches a client to a remote hub reached through a loopback forward.**
|
|
10
|
+
The binding records `--token-env` and/or `--token-command` and never the token.
|
|
11
|
+
Later commands, including the environment for a Pi worker, use that source and
|
|
12
|
+
do not fall back to this machine's hub-env token. Peer commands and
|
|
13
|
+
`kxm tenant status` resolve one project id: flag or `KXM_PROJECT`, then
|
|
14
|
+
`.kxm/project.yaml`, then `package.json`, then the directory name. The
|
|
15
|
+
SSH-forward recipe is in
|
|
16
|
+
[Operate a KXM hub](docs/operations.md#cross-box-peer-attach).
|
|
17
|
+
|
|
9
18
|
- **A live agent step uses a configurable one-shot timeout, and a cancelling run recovers when its child has already exited.**
|
|
10
19
|
The bound is the step `timeoutMs`, or the project `limits.agentStepTimeoutMs`
|
|
11
20
|
when the step omits it (minimum 60 seconds, default one hour). A wider step
|
package/README.md
CHANGED
|
@@ -105,6 +105,8 @@ You need Node.js 22.19 or newer on the 22.x line, or Node.js 24 or newer, plus G
|
|
|
105
105
|
kxm hub view
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
A hub on another machine, reached through an SSH forward to loopback, uses `kxm hub bind --cloud` so this machine's hub-env token is not sent. The recipe is in [Operate a KXM hub](docs/operations.md#cross-box-peer-attach).
|
|
109
|
+
|
|
108
110
|
Expected output:
|
|
109
111
|
|
|
110
112
|
```text
|
package/docs/README.md
CHANGED
|
@@ -96,6 +96,7 @@ KXM connects coding agents through a durable, authenticated [hub](glossary.md#hu
|
|
|
96
96
|
|
|
97
97
|
| Page | For | What you get |
|
|
98
98
|
|---|---|---|
|
|
99
|
+
| [Operate a KXM hub](operations.md) | Hub operators | Index of the operations guides, and cross-box attach over an SSH forward |
|
|
99
100
|
| [Deploy KXM](operations/deploy.md) | Hub operators | Deployment classes, supervision, and hosted per-tenant hubs behind a proxy |
|
|
100
101
|
| [Monitor KXM](operations/monitoring.md) | Hub operators | `kxm dash`, health and readiness, metrics, logs and alerts |
|
|
101
102
|
| [Back up and restore KXM](operations/backup-and-restore.md) | Hub operators | `kxm backup` and `kxm restore`, the state roots, and a stopped-hub recipe |
|
|
@@ -32,9 +32,10 @@ On macOS the user state root is `Library/Application Support/KXM` under the
|
|
|
32
32
|
home directory (`kxmUserStateRoot` in `plugins/kxm/src/bindings.ts`). Windows
|
|
33
33
|
uses `AppData/Local/KXM`. Other platforms use `$XDG_STATE_HOME/kxm` or
|
|
34
34
|
`.local/state/kxm`. `KXM_STATE_HOME` overrides that root when it is absolute.
|
|
35
|
-
The tracker mentions `docs/operations.md` for the six backup roots. That
|
|
36
|
-
|
|
37
|
-
[Backup and restore](../operations/backup-and-restore.md).
|
|
35
|
+
The tracker mentions `docs/operations.md` for the six backup roots. That page
|
|
36
|
+
indexes the operations guides and links the six roots to
|
|
37
|
+
[Backup and restore](../operations/backup-and-restore.md). It also holds
|
|
38
|
+
[Cross-box peer attach](../operations.md#cross-box-peer-attach).
|
|
38
39
|
|
|
39
40
|
## Related
|
|
40
41
|
|
package/docs/glossary.md
CHANGED
|
@@ -38,7 +38,7 @@ The service that authenticates [agents](#agent), stores [messages](#message) and
|
|
|
38
38
|
|
|
39
39
|
### Hub binding
|
|
40
40
|
|
|
41
|
-
The record that `kxm hub bind <url>` writes to `hub-binding.json` in the [user state root](#user-state-root), naming the hub this machine uses. `kxm hub view` checks it, and the Runtime syncs to it. Binding a remote hub requires a credential.
|
|
41
|
+
The record that `kxm hub bind <url>` writes to `hub-binding.json` in the [user state root](#user-state-root), naming the hub this machine uses. `kxm hub view` checks it, and the Runtime syncs to it. Binding a remote hub requires a credential. `kxm hub bind --cloud` marks a loopback forward as remote and records a token variable or command, never the token.
|
|
42
42
|
|
|
43
43
|
### KontextMind
|
|
44
44
|
|
|
@@ -104,11 +104,11 @@ Another [agent](#agent) in the same [project](#project). `kxm_list` and `kxm pee
|
|
|
104
104
|
|
|
105
105
|
### Project
|
|
106
106
|
|
|
107
|
-
The authentication and discovery namespace on the hub.
|
|
107
|
+
The authentication and discovery namespace on the hub. Every command resolves it the same way: `--project` or `KXM_PROJECT`, then the [project ID](#project-id) in `.kxm/project.yaml`, then the `package.json` name, then the directory name. Its key in the hub's `KXM_PROJECT_TOKENS` holds its token. Agents see only peers in the same project.
|
|
108
108
|
|
|
109
109
|
### Project ID
|
|
110
110
|
|
|
111
|
-
The stable `prj_` identifier in `.kxm/project.yaml`.
|
|
111
|
+
The stable `prj_` identifier in `.kxm/project.yaml`. It is the hub [project](#project) name unless `--project` or `KXM_PROJECT` overrides it. The [Runtime](#runtime) store keeps this id as the local key.
|
|
112
112
|
|
|
113
113
|
### Project token
|
|
114
114
|
|
|
@@ -87,6 +87,9 @@ If a malformed claim remains, read the exact `.pid` JSON in the state directory
|
|
|
87
87
|
| `hub_url_invalid` | Use an `http` or `https` URL without user info, query or fragment |
|
|
88
88
|
| `hub_bind_unauthenticated` | The URL is remote and this machine has no token for the project; export it and bind again |
|
|
89
89
|
| `hub_credential_unreadable` | Repair or remove `hub-env.json` under the user state root |
|
|
90
|
+
| `cloud_token_missing` | A `--cloud` binding's variable is empty and it has no working command. Export the variable or fix `--token-command`. This machine's hub-env token is not sent |
|
|
91
|
+
| `cloud_flag_required` | `--token-env` or `--token-command` was passed without `--cloud` |
|
|
92
|
+
| 401 after binding a loopback forward | The bind was not `--cloud`, so the client sent the local hub-env token. See [Cross-box peer attach](../operations.md#cross-box-peer-attach) |
|
|
90
93
|
|
|
91
94
|
## Claude Code plugin and MCP
|
|
92
95
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Operate a KXM hub
|
|
2
|
+
|
|
3
|
+
This page is the operator index for a [hub](glossary.md#hub): where the other
|
|
4
|
+
operations guides live, and how to attach a client on another machine to a hub
|
|
5
|
+
that is not reachable from the internet. The client in that setup is a local
|
|
6
|
+
`kxm` CLI, plus the Pi and Claude Code agents that CLI starts.
|
|
7
|
+
|
|
8
|
+
## Where each procedure lives
|
|
9
|
+
|
|
10
|
+
| Procedure | Page |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Install the services on a tenant box, including loopback listeners | [Deploy KXM](operations/deploy.md) |
|
|
13
|
+
| Health, readiness, metrics, logs, and alerts | [Monitor KXM](operations/monitoring.md) |
|
|
14
|
+
| Stopped-state backup of all six state roots, and restore | [Back up and restore KXM](operations/backup-and-restore.md) |
|
|
15
|
+
| Package update, plugin reinstall, and schema ceilings | [Upgrade KXM](operations/upgrade.md) |
|
|
16
|
+
| Runtime outbox, sync refusals, and hub leases | [Operate Runtime sync and leases](operations/runtime-sync.md) |
|
|
17
|
+
| Symptoms, causes, and fixes | [Troubleshoot KXM](operations/troubleshooting.md) |
|
|
18
|
+
|
|
19
|
+
The six roots are the checkout, the workspace, workspace state, the user state
|
|
20
|
+
root, user config, and federated telemetry. The stopped-state copy of each one
|
|
21
|
+
is in [Back up and restore KXM](operations/backup-and-restore.md).
|
|
22
|
+
|
|
23
|
+
## Before you begin
|
|
24
|
+
|
|
25
|
+
- The `kxm` CLI is installed on the client machine.
|
|
26
|
+
- You can open SSH to the hub host, and you already have that host's key.
|
|
27
|
+
- The hub's admin token is in a password manager. 1Password CLI (`op`) is the
|
|
28
|
+
usual source. The token is never written into `hub-binding.json`.
|
|
29
|
+
|
|
30
|
+
## Cross-box peer attach
|
|
31
|
+
|
|
32
|
+
Use this when the hub listens on loopback on a private machine and your client
|
|
33
|
+
reaches it through an SSH local forward. A plain `kxm hub bind` of
|
|
34
|
+
`http://127.0.0.1:17331` is a loopback bind: it stores no credential, and later
|
|
35
|
+
commands send this machine's hub-env admin token. The remote hub rejects that
|
|
36
|
+
token with 401. `--cloud` marks the binding remote even though the URL is
|
|
37
|
+
loopback, and names where the token comes from without storing the token.
|
|
38
|
+
|
|
39
|
+
### Open the forward with a pinned host key
|
|
40
|
+
|
|
41
|
+
Pin the host key before the first connection. Do not accept a key on first use.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ssh-keyscan -H 10.31.0.21 >> ~/.ssh/known_hosts
|
|
45
|
+
ssh -N \
|
|
46
|
+
-o ExitOnForwardFailure=yes \
|
|
47
|
+
-o StrictHostKeyChecking=yes \
|
|
48
|
+
-o UserKnownHostsFile="$HOME/.ssh/known_hosts" \
|
|
49
|
+
-L 127.0.0.1:17331:127.0.0.1:7331 \
|
|
50
|
+
user@10.31.0.21
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Leave that SSH session open. Hub traffic then goes to `127.0.0.1:17331` on the
|
|
54
|
+
client and comes out on the hub host's loopback port 7331.
|
|
55
|
+
|
|
56
|
+
<details><summary>PowerShell</summary>
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
ssh-keyscan -H 10.31.0.21 >> $env:USERPROFILE\.ssh\known_hosts
|
|
60
|
+
ssh -N `
|
|
61
|
+
-o ExitOnForwardFailure=yes `
|
|
62
|
+
-o StrictHostKeyChecking=yes `
|
|
63
|
+
-o UserKnownHostsFile="$env:USERPROFILE\.ssh\known_hosts" `
|
|
64
|
+
-L 127.0.0.1:17331:127.0.0.1:7331 `
|
|
65
|
+
user@10.31.0.21
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
</details>
|
|
69
|
+
|
|
70
|
+
### Bind the forward as a cloud hub
|
|
71
|
+
|
|
72
|
+
Pick one token source. The environment variable is enough when the shell
|
|
73
|
+
already holds the token. The command form asks 1Password each time a command
|
|
74
|
+
needs the token. Do not put the token text in `--token-command`: that string
|
|
75
|
+
is stored in the binding.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export KXMD_HUB_TOKEN="$(op read 'op://Private/kxmd/admin-token')"
|
|
79
|
+
kxm hub bind --cloud --token-env KXMD_HUB_TOKEN http://127.0.0.1:17331
|
|
80
|
+
kxm hub view
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or name the 1Password command and skip the variable:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
kxm hub bind --cloud \
|
|
87
|
+
--token-command "op read op://Private/kxmd/admin-token" \
|
|
88
|
+
http://127.0.0.1:17331
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expected output:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
bound hub http://127.0.0.1:17331 · remote · health=on · token is not stored
|
|
95
|
+
hub health=true ready=true · remote hub
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--token-env` and `--token-command` can be combined. A non-empty variable wins.
|
|
99
|
+
Otherwise the command runs. The command is a program and arguments, not a shell
|
|
100
|
+
pipeline (`|`, `&`, `;`, and redirection are refused).
|
|
101
|
+
|
|
102
|
+
<details><summary>PowerShell</summary>
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
$env:KXMD_HUB_TOKEN = op read "op://Private/kxmd/admin-token"
|
|
106
|
+
kxm hub bind --cloud --token-env KXMD_HUB_TOKEN http://127.0.0.1:17331
|
|
107
|
+
kxm hub view
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
### What --cloud changes
|
|
113
|
+
|
|
114
|
+
| | Local bind | `--cloud` bind |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Loopback URL | Scope `loopback`. No credential required | Scope `remote`, including an SSH forward |
|
|
117
|
+
| Token stored in `hub-binding.json` | No | No. The file names `tokenEnv` and/or `tokenCommand` |
|
|
118
|
+
| Credential later commands send | `KXM_AUTH_TOKEN`, then this machine's hub-env | The named source only. Hub-env is not a fallback |
|
|
119
|
+
| Missing source | Agent commands report `project_token_missing` | `cloud_token_missing` or `cloud_token_command_failed`. The local admin token is not sent |
|
|
120
|
+
| `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
|
+
|
|
122
|
+
`kxm hub view`, `kxm tenant status`, `kxm peer`, `kxm workflow` agent commands,
|
|
123
|
+
`kxm context`, Runtime presence, and the environment exported for a Pi worker
|
|
124
|
+
all use that source. `kxm agent worker` puts `KXM_AUTH_TOKEN` in the child
|
|
125
|
+
process environment only. It does not write the token to disk.
|
|
126
|
+
|
|
127
|
+
The hub project id is the same value for peer commands and `kxm tenant status`:
|
|
128
|
+
`--project` or `KXM_PROJECT`, then the `id` in `.kxm/project.yaml`, then
|
|
129
|
+
`package.json` `name`, then the directory name.
|
|
130
|
+
|
|
131
|
+
### Remove the binding
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
kxm hub unbind
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Unbind deletes `hub-binding.json`. It does not delete hub-env, and it does not
|
|
138
|
+
close the SSH forward. Stop the forward by ending the `ssh -N` session.
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
| Symptom | Cause | Fix |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| 401 `invalid_auth` after binding `127.0.0.1:17331` | The bind was not `--cloud`, so the client sent this machine's hub-env admin token | Bind again with `--cloud` and a token source that holds the remote hub's token. Unset a stale `KXM_AUTH_TOKEN` that points at the local hub |
|
|
145
|
+
| `cloud_token_missing` | `KXMD_HUB_TOKEN` (or the variable you named) is unset or empty, and no command is configured | Export the variable or pass `--token-command`. The local hub-env token is intentionally unused |
|
|
146
|
+
| `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 |
|
|
147
|
+
| 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 |
|
|
148
|
+
| `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 |
|
|
149
|
+
|
|
150
|
+
## Next steps
|
|
151
|
+
|
|
152
|
+
- Deploy the hub itself: [Deploy KXM](operations/deploy.md)
|
|
153
|
+
- Read health and metrics: [Monitor KXM](operations/monitoring.md)
|
|
154
|
+
- Command flags: [kxm hub bind](reference/cli-reference.md#kxm-hub-bind)
|
|
@@ -569,16 +569,17 @@ kxm hub stop --wait-ms 8000 --json
|
|
|
569
569
|
### `kxm hub bind`
|
|
570
570
|
|
|
571
571
|
```text
|
|
572
|
-
kxm hub bind <url>
|
|
572
|
+
kxm hub bind <url> [--cloud] [--token-env <name>] [--token-command <command>]
|
|
573
573
|
```
|
|
574
574
|
|
|
575
575
|
Binds this machine to a running hub by writing `hub-binding.json` under the user state root, then probes the hub's health for up to 300 ms. Every hub client uses the binding when `KXM_SERVER_URL` is unset.
|
|
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
|
-
- 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).
|
|
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
|
+
- `--cloud` marks the binding remote even when the URL is a loopback forward. It requires `--token-env`, `--token-command`, or both. The token is not written. A non-empty variable wins; otherwise the command runs. Source flags without `--cloud` fail with `cloud_flag_required`. A missing 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).
|
|
580
581
|
- Mutates the binding file. `--dry-run` validates and prints the plan without writing.
|
|
581
|
-
- JSON keys: `url`, `scope`, `file`, `health` (`on`, `off`, or `unknown`), `probeMs`.
|
|
582
|
+
- JSON keys: `url`, `scope`, `file`, `health` (`on`, `off`, or `unknown`), `probeMs`. A cloud bind also returns `cloud`, and `tokenEnv` or `tokenCommand` when set.
|
|
582
583
|
|
|
583
584
|
```bash
|
|
584
585
|
kxm hub bind http://127.0.0.1:7331 --dry-run
|
|
@@ -108,7 +108,7 @@ Every agent client (the Pi extension, the Claude Code MCP server, and the `kxm p
|
|
|
108
108
|
|---|---|---|
|
|
109
109
|
| `KXM_SERVER_URL` | `http://127.0.0.1:7331` | Hub base URL. For the CLI, the machine's `kxm hub bind` URL is used when this is unset |
|
|
110
110
|
| `KXM_AUTH_TOKEN` | See below | The project token for this agent's project |
|
|
111
|
-
| `KXM_PROJECT` | `name` in `package.json` in the working directory, else the directory name | Project for discovery and authentication |
|
|
111
|
+
| `KXM_PROJECT` | `id` in the nearest `.kxm/project.yaml`, else `name` in `package.json` in the working directory, else the directory name | Project for discovery and authentication. An explicit `--project` flag on `kxm peer list` and `kxm tenant status` wins over this variable. The same resolver is used by peer commands, tenant status, workers, the Pi extension, and the Claude Code MCP server |
|
|
112
112
|
| `KXM_AGENT_NAME` | Harness-specific (see below) | Name peers see; unique among online agents in the project, compared case-insensitively |
|
|
113
113
|
| `KXM_AGENT_PURPOSE` | Harness-specific (see below) | One line peers use to decide what to send this agent |
|
|
114
114
|
|
|
@@ -120,6 +120,16 @@ Every agent client (the Pi extension, the Claude Code MCP server, and the `kxm p
|
|
|
120
120
|
|
|
121
121
|
A clean shutdown marks an identity offline. Reconnecting with the same project and name resumes its durable agent ID and rotates its agent key. If the Claude Code name is already online in the project, the MCP server registers once more as `<name>-<pid>`.
|
|
122
122
|
|
|
123
|
+
A `--cloud` hub binding replaces this table's token lookup for every client, including agents. The token comes from the binding's `--token-env` or `--token-command`, never from this machine's hub-env file, and it is not written to disk. `kxm agent worker` copies it into the child process environment as `KXM_AUTH_TOKEN`.
|
|
124
|
+
|
|
125
|
+
Also check plugin.json is valid JSON.
|
|
126
|
+
|
|
127
|
+
And configuration table cell might be very long. That's ok.
|
|
128
|
+
|
|
129
|
+
Look at harness table "never the admin token" - the new paragraph clarifies cloud. Good.
|
|
130
|
+
|
|
131
|
+
Plugin README add one sentence under which token.
|
|
132
|
+
|
|
123
133
|
## Claude Code plugin settings
|
|
124
134
|
|
|
125
135
|
The plugin asks for `server_url`, `auth_token`, `agent_name`, `agent_purpose` and `project` when you install it, and passes them to its MCP server as `KXM_SERVER_URL`, `KXM_AUTH_TOKEN`, `KXM_AGENT_NAME`, `KXM_AGENT_PURPOSE` and `KXM_PROJECT`. It also sets `KXM_PROJECT_DIR` to the directory Claude Code started in. Change them with `/plugin configure kxm@kxm`. Every field, default and token rule is in [Claude Code plugin settings](config-reference.md#claude-code-plugin-settings) and the [plugin README](../../plugins/kxm/README.md).
|
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.165",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"project": {
|
|
66
66
|
"type": "string",
|
|
67
67
|
"title": "Project",
|
|
68
|
-
"description": "Hub project key; must match a key in the hub KXM_PROJECT_TOKENS. Blank uses the package.json name, then the directory name",
|
|
68
|
+
"description": "Hub project key; must match a key in the hub KXM_PROJECT_TOKENS. Blank uses the .kxm/project.yaml id, then the package.json name, then the directory name",
|
|
69
69
|
"required": false
|
|
70
70
|
}
|
|
71
71
|
}
|
package/plugins/kxm/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Claude Code asks for these options when you install the plugin. Change them late
|
|
|
58
58
|
| `auth_token` | `KXM_AUTH_TOKEN` | Blank | This project's token from the hub's `KXM_PROJECT_TOKENS`. Leave it blank on the machine that runs the hub. Never the hub admin token. Marked sensitive. |
|
|
59
59
|
| `agent_name` | `KXM_AGENT_NAME` | `claude` | Name other agents see. The first active session in a project keeps it; a later concurrent session registers as `<name>-<pid>`. |
|
|
60
60
|
| `agent_purpose` | `KXM_AGENT_PURPOSE` | `Claude Code implementation and review agent` | One line that peers use to decide what to send this agent. |
|
|
61
|
-
| `project` | `KXM_PROJECT` | Blank | Hub project key; must match a key in the hub's `KXM_PROJECT_TOKENS`. Blank uses `
|
|
61
|
+
| `project` | `KXM_PROJECT` | Blank | Hub project key; must match a key in the hub's `KXM_PROJECT_TOKENS`. Blank uses the `id` in `.kxm/project.yaml`, then `name` from `package.json`, then the directory name. |
|
|
62
62
|
|
|
63
63
|
The MCP server also receives `KXM_PROJECT_DIR`, set to the directory Claude Code was started in (`CLAUDE_PROJECT_DIR`). It decides the default project key and whether this is a KXM project (one with a `.kxm/` directory).
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ The MCP server also receives `KXM_PROJECT_DIR`, set to the directory Claude Code
|
|
|
67
67
|
The plugin acts as an agent of one hub project and authenticates with that project's token. It never uses the hub admin token.
|
|
68
68
|
|
|
69
69
|
- On the machine that runs the hub, leave `auth_token` blank. The MCP server then uses the project token the hub saved for this project in `hub-env.json` under the user state root (`KXM_STATE_HOME`, or the platform default listed in [State outside the project](../../docs/reference/config-reference.md#state-outside-the-project)). It uses only that entry, never the admin token saved beside it.
|
|
70
|
-
- On any other machine, enter this project's token at `/plugin configure kxm@kxm`. Get it from whoever runs the hub, through your password manager.
|
|
70
|
+
- On any other machine, enter this project's token at `/plugin configure kxm@kxm`. Get it from whoever runs the hub, through your password manager. When the machine is bound with `kxm hub bind --cloud`, leave `auth_token` blank: the MCP server reads the binding's token source and does not store the token. The recipe is [Cross-box peer attach](../../docs/operations.md#cross-box-peer-attach).
|
|
71
71
|
- Never enter the hub admin token. It is the operator's credential. The hub accepts it for any project that has no token of its own, so an agent holding it could act in projects it was never given.
|
|
72
72
|
- Without a project token, every `kxm_*` tool fails with `KXM has no project token for project <p> on this machine`, and the MCP server does not contact the hub.
|
|
73
73
|
|
|
@@ -568,6 +568,47 @@ function validateHubUrl(raw) {
|
|
|
568
568
|
}
|
|
569
569
|
return parsed.href.replace(/\/$/, "");
|
|
570
570
|
}
|
|
571
|
+
function splitTokenCommand(raw) {
|
|
572
|
+
if (/[|&;<>$`\n\r]/.test(raw)) {
|
|
573
|
+
throw new CloudTokenError(
|
|
574
|
+
"cloud_token_command_invalid",
|
|
575
|
+
"token command must be a program and arguments, not a shell pipeline"
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const args = [];
|
|
579
|
+
let current = "";
|
|
580
|
+
let quote;
|
|
581
|
+
for (const ch of raw) {
|
|
582
|
+
if (quote) {
|
|
583
|
+
if (ch === quote) quote = void 0;
|
|
584
|
+
else current += ch;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (ch === "'" || ch === '"') {
|
|
588
|
+
quote = ch;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (ch === " " || ch === " ") {
|
|
592
|
+
if (current.length > 0) {
|
|
593
|
+
args.push(current);
|
|
594
|
+
current = "";
|
|
595
|
+
}
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
current += ch;
|
|
599
|
+
}
|
|
600
|
+
if (quote) {
|
|
601
|
+
throw new CloudTokenError("cloud_token_command_invalid", "token command has an unclosed quote");
|
|
602
|
+
}
|
|
603
|
+
if (current.length > 0) args.push(current);
|
|
604
|
+
if (args.length === 0) {
|
|
605
|
+
throw new CloudTokenError("cloud_token_command_invalid", "token command is empty");
|
|
606
|
+
}
|
|
607
|
+
return args;
|
|
608
|
+
}
|
|
609
|
+
function isCloudTokenEnvName(value) {
|
|
610
|
+
return TOKEN_ENV_NAME.test(value);
|
|
611
|
+
}
|
|
571
612
|
function isIsoTimestamp(value) {
|
|
572
613
|
if (Number.isNaN(Date.parse(value))) return false;
|
|
573
614
|
return value === new Date(value).toISOString();
|
|
@@ -591,7 +632,9 @@ function readHubBinding(env = process.env) {
|
|
|
591
632
|
}
|
|
592
633
|
const row = parsed;
|
|
593
634
|
const keys = Object.keys(row);
|
|
594
|
-
|
|
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)) {
|
|
595
638
|
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
596
639
|
}
|
|
597
640
|
let url;
|
|
@@ -600,7 +643,34 @@ function readHubBinding(env = process.env) {
|
|
|
600
643
|
} catch {
|
|
601
644
|
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
602
645
|
}
|
|
603
|
-
return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
|
|
646
|
+
if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
|
|
647
|
+
if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
648
|
+
let tokenEnv;
|
|
649
|
+
let tokenCommand;
|
|
650
|
+
if (row.tokenEnv !== void 0) {
|
|
651
|
+
if (typeof row.tokenEnv !== "string" || !isCloudTokenEnvName(row.tokenEnv)) {
|
|
652
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
653
|
+
}
|
|
654
|
+
tokenEnv = row.tokenEnv;
|
|
655
|
+
}
|
|
656
|
+
if (row.tokenCommand !== void 0) {
|
|
657
|
+
if (typeof row.tokenCommand !== "string") throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
658
|
+
try {
|
|
659
|
+
splitTokenCommand(row.tokenCommand);
|
|
660
|
+
} catch {
|
|
661
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
662
|
+
}
|
|
663
|
+
tokenCommand = row.tokenCommand;
|
|
664
|
+
}
|
|
665
|
+
if (!tokenEnv && !tokenCommand) throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
666
|
+
return {
|
|
667
|
+
schema: HUB_BINDING_SCHEMA,
|
|
668
|
+
url,
|
|
669
|
+
boundAt: row.boundAt,
|
|
670
|
+
cloud: true,
|
|
671
|
+
...tokenEnv ? { tokenEnv } : {},
|
|
672
|
+
...tokenCommand ? { tokenCommand } : {}
|
|
673
|
+
};
|
|
604
674
|
}
|
|
605
675
|
async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
|
|
606
676
|
const started = Date.now();
|
|
@@ -625,12 +695,21 @@ async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
|
|
|
625
695
|
clearTimeout(timer);
|
|
626
696
|
}
|
|
627
697
|
}
|
|
628
|
-
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, HubBindingError;
|
|
698
|
+
var HUB_BINDING_SCHEMA, HUB_HEALTH_PROBE_MS, CloudTokenError, TOKEN_ENV_NAME, HubBindingError;
|
|
629
699
|
var init_hub_binding = __esm({
|
|
630
700
|
"plugins/kxm/src/hub-binding.ts"() {
|
|
631
701
|
"use strict";
|
|
632
702
|
HUB_BINDING_SCHEMA = "kxm.hub-binding.v1";
|
|
633
703
|
HUB_HEALTH_PROBE_MS = 300;
|
|
704
|
+
CloudTokenError = class extends Error {
|
|
705
|
+
code;
|
|
706
|
+
constructor(code, message) {
|
|
707
|
+
super(message);
|
|
708
|
+
this.name = "CloudTokenError";
|
|
709
|
+
this.code = code;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
TOKEN_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
634
713
|
HubBindingError = class extends Error {
|
|
635
714
|
constructor(message) {
|
|
636
715
|
super(message);
|
|
@@ -8770,11 +8849,15 @@ var project_name_exports = {};
|
|
|
8770
8849
|
__export(project_name_exports, {
|
|
8771
8850
|
defaultProjectName: () => defaultProjectName
|
|
8772
8851
|
});
|
|
8773
|
-
import { readFileSync as readFileSync7 } from "node:fs";
|
|
8774
|
-
import { basename as basename2, join as join6 } from "node:path";
|
|
8775
|
-
function defaultProjectName(cwd, env = process.env) {
|
|
8852
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
8853
|
+
import { basename as basename2, dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
|
|
8854
|
+
function defaultProjectName(cwd, env = process.env, explicit) {
|
|
8855
|
+
const fromExplicit = explicit?.trim();
|
|
8856
|
+
if (fromExplicit) return fromExplicit;
|
|
8776
8857
|
const fromEnv = env.KXM_PROJECT?.trim();
|
|
8777
8858
|
if (fromEnv) return fromEnv;
|
|
8859
|
+
const fromProject = readProjectYamlId(cwd);
|
|
8860
|
+
if (fromProject) return fromProject;
|
|
8778
8861
|
try {
|
|
8779
8862
|
const pkg = JSON.parse(readFileSync7(join6(cwd, "package.json"), "utf8"));
|
|
8780
8863
|
if (typeof pkg.name === "string" && pkg.name.trim().length > 0) return pkg.name.trim();
|
|
@@ -8782,9 +8865,30 @@ function defaultProjectName(cwd, env = process.env) {
|
|
|
8782
8865
|
}
|
|
8783
8866
|
return basename2(cwd);
|
|
8784
8867
|
}
|
|
8868
|
+
function readProjectYamlId(start) {
|
|
8869
|
+
let dir = resolve5(start);
|
|
8870
|
+
for (; ; ) {
|
|
8871
|
+
const file = join6(dir, ".kxm", "project.yaml");
|
|
8872
|
+
if (existsSync7(file)) {
|
|
8873
|
+
try {
|
|
8874
|
+
const doc = (0, import_yaml2.parse)(readFileSync7(file, "utf8"));
|
|
8875
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc) && typeof doc.id === "string" && doc.id.trim()) {
|
|
8876
|
+
return doc.id.trim();
|
|
8877
|
+
}
|
|
8878
|
+
} catch {
|
|
8879
|
+
}
|
|
8880
|
+
return void 0;
|
|
8881
|
+
}
|
|
8882
|
+
const parent = dirname3(dir);
|
|
8883
|
+
if (parent === dir) return void 0;
|
|
8884
|
+
dir = parent;
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
var import_yaml2;
|
|
8785
8888
|
var init_project_name = __esm({
|
|
8786
8889
|
"plugins/kxm/src/project-name.ts"() {
|
|
8787
8890
|
"use strict";
|
|
8891
|
+
import_yaml2 = __toESM(require_dist(), 1);
|
|
8788
8892
|
}
|
|
8789
8893
|
});
|
|
8790
8894
|
|
|
@@ -8930,9 +9034,9 @@ __export(commands_exports, {
|
|
|
8930
9034
|
timingSafeStringCompare: () => timingSafeStringCompare
|
|
8931
9035
|
});
|
|
8932
9036
|
import { createHash, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
8933
|
-
import { chmodSync, existsSync as
|
|
9037
|
+
import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8934
9038
|
import { homedir as homedir3 } from "node:os";
|
|
8935
|
-
import { dirname as
|
|
9039
|
+
import { dirname as dirname4, join as join7, resolve as resolve6 } from "node:path";
|
|
8936
9040
|
function forwardedHops(handling) {
|
|
8937
9041
|
if (!handling?.length) return void 0;
|
|
8938
9042
|
return {
|
|
@@ -9114,15 +9218,15 @@ function parseSessionToken(token) {
|
|
|
9114
9218
|
return void 0;
|
|
9115
9219
|
}
|
|
9116
9220
|
function resolveUserConfigDirectory(overrideDir) {
|
|
9117
|
-
if (overrideDir) return
|
|
9118
|
-
return
|
|
9221
|
+
if (overrideDir) return resolve6(overrideDir);
|
|
9222
|
+
return resolve6(process.env.KXM_USER_CONFIG_DIR?.trim() || join7(homedir3(), ".config", "kxm"));
|
|
9119
9223
|
}
|
|
9120
9224
|
function sessionTokenPath(userConfigDir) {
|
|
9121
9225
|
return join7(resolveUserConfigDirectory(userConfigDir), "session.token");
|
|
9122
9226
|
}
|
|
9123
9227
|
function persistSessionTokenToDisk(token, options) {
|
|
9124
9228
|
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
9125
|
-
const dir =
|
|
9229
|
+
const dir = dirname4(filePath);
|
|
9126
9230
|
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
9127
9231
|
const mode = options?.mode ?? 384;
|
|
9128
9232
|
writeFileSync5(filePath, `${token.trim()}
|
|
@@ -9135,7 +9239,7 @@ function persistSessionTokenToDisk(token, options) {
|
|
|
9135
9239
|
}
|
|
9136
9240
|
function readSessionTokenFromDisk(options) {
|
|
9137
9241
|
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
9138
|
-
if (!
|
|
9242
|
+
if (!existsSync8(filePath)) return void 0;
|
|
9139
9243
|
try {
|
|
9140
9244
|
const token = readFileSync8(filePath, "utf8").trim();
|
|
9141
9245
|
if (!token) return void 0;
|
|
@@ -9148,7 +9252,7 @@ function readSessionTokenFromDisk(options) {
|
|
|
9148
9252
|
}
|
|
9149
9253
|
function clearSessionTokenFromDisk(options) {
|
|
9150
9254
|
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
9151
|
-
if (!
|
|
9255
|
+
if (!existsSync8(filePath)) return false;
|
|
9152
9256
|
try {
|
|
9153
9257
|
unlinkSync(filePath);
|
|
9154
9258
|
return true;
|
|
@@ -9247,7 +9351,7 @@ function enforceToolPolicy(commandName, env = process.env, options) {
|
|
|
9247
9351
|
return { allowed: true };
|
|
9248
9352
|
}
|
|
9249
9353
|
const tokenFile = sessionTokenPath(env.KXM_USER_CONFIG_DIR);
|
|
9250
|
-
if (
|
|
9354
|
+
if (existsSync8(tokenFile)) {
|
|
9251
9355
|
let tokenRaw;
|
|
9252
9356
|
try {
|
|
9253
9357
|
tokenRaw = readFileSync8(tokenFile, "utf8").trim();
|
|
@@ -9937,7 +10041,7 @@ var init_commands = __esm({
|
|
|
9937
10041
|
|
|
9938
10042
|
// plugins/kxm/src/claude-hook.ts
|
|
9939
10043
|
import { readFileSync as readFileSync9, statSync } from "node:fs";
|
|
9940
|
-
import { join as join8, resolve as
|
|
10044
|
+
import { join as join8, resolve as resolve7 } from "node:path";
|
|
9941
10045
|
|
|
9942
10046
|
// plugins/kxm/src/session-token-hint.ts
|
|
9943
10047
|
var ENV_TEXT = "KXM_SESSION_TOKEN in the environment Claude Code was launched from is malformed or expired, so every kxm_* tool fails with tool_policy_denied. Ask the user to unset or replace KXM_SESSION_TOKEN in the environment Claude Code was launched from, then restart Claude Code.";
|
|
@@ -10010,7 +10114,7 @@ function truncate2(text, max) {
|
|
|
10010
10114
|
}
|
|
10011
10115
|
async function sessionStartContext(input, env) {
|
|
10012
10116
|
const inputCwd = typeof input.cwd === "string" ? input.cwd.trim() : "";
|
|
10013
|
-
const root =
|
|
10117
|
+
const root = resolve7(env.CLAUDE_PROJECT_DIR?.trim() || inputCwd || process.cwd());
|
|
10014
10118
|
if (!isDirectory(join8(root, ".kxm"))) return void 0;
|
|
10015
10119
|
const [
|
|
10016
10120
|
{ resolveSessionHubStatus: resolveSessionHubStatus2 },
|