@kontextmind/kxm 0.7.164 → 0.7.166
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 +25 -0
- package/README.md +2 -0
- package/docs/README.md +1 -0
- package/docs/architecture/inventory.md +4 -3
- package/docs/concepts/trust-model.md +1 -1
- package/docs/glossary.md +4 -4
- package/docs/operations/troubleshooting.md +4 -1
- package/docs/operations.md +208 -0
- package/docs/reference/cli-reference.md +12 -11
- package/docs/reference/config-reference.md +36 -11
- package/docs/reference/configuration.md +11 -1
- package/docs/reference/tools.md +1 -1
- package/docs/start/quickstart-claude-code.md +2 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +2 -2
- package/plugins/kxm/README.md +4 -4
- package/plugins/kxm/dist/claude-hook.js +511 -56
- package/plugins/kxm/dist/cli.js +15661 -14775
- package/plugins/kxm/dist/extension.js +645 -133
- package/plugins/kxm/dist/mcp-server.js +8152 -81
- package/plugins/kxm/dist/runtime-supervisor.js +804 -155
- package/plugins/kxm/dist/runtime.js +851 -202
- package/plugins/kxm/dist/server.js +466 -58
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +17 -5
- package/plugins/kxm/src/cli/context-skills.ts +64 -10
- package/plugins/kxm/src/cli/hub.ts +192 -22
- package/plugins/kxm/src/cli/project.ts +7 -3
- package/plugins/kxm/src/cli/types.ts +14 -1
- package/plugins/kxm/src/cli/workflows.ts +12 -1
- package/plugins/kxm/src/cli.ts +63 -11
- package/plugins/kxm/src/config.ts +246 -3
- package/plugins/kxm/src/extension.ts +12 -7
- package/plugins/kxm/src/hub-autostart.ts +23 -0
- package/plugins/kxm/src/hub-binding.ts +182 -2
- package/plugins/kxm/src/hub-env.ts +132 -9
- package/plugins/kxm/src/hub-identity.ts +337 -0
- package/plugins/kxm/src/mcp-server.ts +17 -14
- package/plugins/kxm/src/project-name.ts +6 -20
- package/plugins/kxm/src/runtime-supervisor.ts +9 -3
- package/plugins/kxm/src/server.ts +22 -4
- package/plugins/kxm/src/tenant-status.ts +9 -2
- package/scripts/build-runtime.mjs +5 -0
- package/scripts/kxm-hub.mjs +5 -0
package/plugins/kxm/package.json
CHANGED
|
@@ -46,7 +46,7 @@ the hub admin token or a project token.
|
|
|
46
46
|
|---|---|---|
|
|
47
47
|
| `kxm hub start` | Start the hub in the foreground | `--json` |
|
|
48
48
|
| `kxm hub stop` | Request managed hub and worker shutdown | `--wait-ms <ms>` |
|
|
49
|
-
| `kxm hub bind <url>` | Bind this machine to a running hub | http or https URL |
|
|
49
|
+
| `kxm hub bind <url>` | Bind this machine to a running hub and record the non-secret endpoint in `.kxm/config.yaml` | http or https URL. `--token-env` and `--key-op` for either mode. `--cloud` with `--token-env`, `--token-command`, or `--key-op` for an SSH forward to loopback. The token is not stored |
|
|
50
50
|
| `kxm hub unbind` | Remove this machine's hub binding | `--json` |
|
|
51
51
|
| `kxm restore <manifest>` | Replace this project's live stores from a verified backup manifest | `--all-projects`, `--json` |
|
|
52
52
|
|
|
@@ -60,15 +60,27 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
|
|
|
60
60
|
`KXM_AUTH_TOKEN` is unset it generates a long random admin token once and
|
|
61
61
|
persists it in `hub-env.json` (schema `kxm.hub-env.v1`, mode `0600`) under
|
|
62
62
|
the user state root, then reuses it on every restart.
|
|
63
|
-
- `KXM_PROJECT_TOKENS` must list every project's token
|
|
64
|
-
token map rather than merging with it, and the
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
- `KXM_PROJECT_TOKENS` must list every project's token when it is set. It
|
|
64
|
+
replaces the saved token map rather than merging with it, and the
|
|
65
|
+
replacement is saved. The same map can instead live in `.kxm/config.yaml`
|
|
66
|
+
as `hub.projects`, with `op://` references or environment variable names.
|
|
67
|
+
That map is resolved in memory and is not saved. When a hub already serves
|
|
68
|
+
other projects, build the full map with the merge command in the KXM
|
|
69
|
+
documentation's Claude Code quick start
|
|
67
70
|
(docs/start/quickstart-claude-code.md, section "Start the hub") before
|
|
68
71
|
restarting the hub.
|
|
72
|
+
- `kxm hub view` prints the mode, URL, project id, and key source. It does
|
|
73
|
+
not print the token. The project id is the flag, then `KXM_PROJECT`, then
|
|
74
|
+
`hub.local.project` or `hub.cloud.project`, then `.kxm/project.yaml`, then
|
|
75
|
+
the package name, then the directory name.
|
|
69
76
|
- `kxm hub bind` to a remote (non-loopback) URL fails closed with
|
|
70
77
|
`hub_bind_unauthenticated` unless a credential for the current project
|
|
71
78
|
resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`.
|
|
79
|
+
- `kxm hub bind --cloud` is the bind for a loopback SSH forward of a remote
|
|
80
|
+
hub. It never falls back to this machine's hub-env token. Pass
|
|
81
|
+
`--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
|
|
82
|
+
or `--token-command "op read op://..."`. The non-secret fields are written
|
|
83
|
+
to `.kxm/config.yaml`. See docs/operations.md, section "Cross-box peer attach".
|
|
72
84
|
- Hub PID claims record the wrapper and server child PID. A dead wrapper's
|
|
73
85
|
claim is reclaimed automatically, an orphaned server is terminated first,
|
|
74
86
|
and `kxm hub stop` recovers such orphans directly.
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../memory.ts";
|
|
11
11
|
import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "../skills.ts";
|
|
12
12
|
import { writeCompiledWiki } from "../wiki.ts";
|
|
13
|
+
import { CloudTokenError } from "../hub-binding.ts";
|
|
14
|
+
import { resolveControlPlaneAuthToken } from "../hub-env.ts";
|
|
13
15
|
import { print, printPlan, type Runtime } from "./types.ts";
|
|
14
16
|
|
|
15
17
|
/** Authenticated hub POST for context operations. The CLI operates as the
|
|
@@ -19,13 +21,33 @@ export async function hubContextPost(input: {
|
|
|
19
21
|
path: string;
|
|
20
22
|
body: Record<string, unknown>;
|
|
21
23
|
authToken?: string | undefined;
|
|
24
|
+
/** When set, a cloud binding's token source replaces the explicit env token. */
|
|
25
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
26
|
+
command?: string | undefined;
|
|
27
|
+
io?: Runtime["io"] | undefined;
|
|
28
|
+
json?: boolean | undefined;
|
|
22
29
|
fetchImpl: typeof fetch;
|
|
23
|
-
}): Promise<{ ok: boolean; status: number; body: unknown }> {
|
|
30
|
+
}): Promise<{ ok: boolean; status: number; body: unknown; exitCode?: number }> {
|
|
31
|
+
let authToken = input.authToken;
|
|
32
|
+
if (input.env) {
|
|
33
|
+
try {
|
|
34
|
+
authToken = resolveControlPlaneAuthToken(input.env);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof CloudTokenError) {
|
|
37
|
+
const command = input.command ?? "context";
|
|
38
|
+
if (input.io) {
|
|
39
|
+
print(input.io, Boolean(input.json), { ok: false, command, error: error.code, detail: error.message }, error.message);
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, status: 0, body: { error: error.code, detail: error.message }, exitCode: 2 };
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
24
46
|
const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}${input.path}`, {
|
|
25
47
|
method: "POST",
|
|
26
48
|
headers: {
|
|
27
49
|
"content-type": "application/json",
|
|
28
|
-
...(
|
|
50
|
+
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
|
29
51
|
},
|
|
30
52
|
body: JSON.stringify(input.body),
|
|
31
53
|
});
|
|
@@ -64,9 +86,13 @@ export async function cmdContextGet(runtime: Runtime, project: string, options:
|
|
|
64
86
|
serverUrl: runtime.serverUrl,
|
|
65
87
|
path: "/v1/context/get",
|
|
66
88
|
body,
|
|
67
|
-
|
|
89
|
+
env: runtime.env,
|
|
90
|
+
command: "context",
|
|
91
|
+
io: runtime.io,
|
|
92
|
+
json: runtime.json,
|
|
68
93
|
fetchImpl: runtime.fetchImpl,
|
|
69
94
|
});
|
|
95
|
+
if (response.exitCode) return response.exitCode;
|
|
70
96
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context get", status: response.status, ...(response.body as object) }, `context get ${response.ok ? "assembled" : `failed (${response.status})`}`);
|
|
71
97
|
return response.ok ? 0 : 1;
|
|
72
98
|
}
|
|
@@ -81,9 +107,13 @@ export async function cmdContextRecall(runtime: Runtime, project: string, option
|
|
|
81
107
|
serverUrl: runtime.serverUrl,
|
|
82
108
|
path: "/v1/context/recall",
|
|
83
109
|
body,
|
|
84
|
-
|
|
110
|
+
env: runtime.env,
|
|
111
|
+
command: "context",
|
|
112
|
+
io: runtime.io,
|
|
113
|
+
json: runtime.json,
|
|
85
114
|
fetchImpl: runtime.fetchImpl,
|
|
86
115
|
});
|
|
116
|
+
if (response.exitCode) return response.exitCode;
|
|
87
117
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context recall", status: response.status, ...(response.body as object) }, `context recall ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
88
118
|
return response.ok ? 0 : 1;
|
|
89
119
|
}
|
|
@@ -95,9 +125,13 @@ export async function cmdContextState(runtime: Runtime, project: string, key: st
|
|
|
95
125
|
serverUrl: runtime.serverUrl,
|
|
96
126
|
path: "/v1/context/state",
|
|
97
127
|
body,
|
|
98
|
-
|
|
128
|
+
env: runtime.env,
|
|
129
|
+
command: "context",
|
|
130
|
+
io: runtime.io,
|
|
131
|
+
json: runtime.json,
|
|
99
132
|
fetchImpl: runtime.fetchImpl,
|
|
100
133
|
});
|
|
134
|
+
if (response.exitCode) return response.exitCode;
|
|
101
135
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context state", status: response.status, ...(response.body as object) }, `context state ${response.ok ? "resolved" : `failed (${response.status})`}`);
|
|
102
136
|
return response.ok ? 0 : 1;
|
|
103
137
|
}
|
|
@@ -109,9 +143,13 @@ export async function cmdContextEpisode(runtime: Runtime, project: string, optio
|
|
|
109
143
|
serverUrl: runtime.serverUrl,
|
|
110
144
|
path: "/v1/context/episode",
|
|
111
145
|
body,
|
|
112
|
-
|
|
146
|
+
env: runtime.env,
|
|
147
|
+
command: "context",
|
|
148
|
+
io: runtime.io,
|
|
149
|
+
json: runtime.json,
|
|
113
150
|
fetchImpl: runtime.fetchImpl,
|
|
114
151
|
});
|
|
152
|
+
if (response.exitCode) return response.exitCode;
|
|
115
153
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context episode", status: response.status, ...(response.body as object) }, `context episode ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
116
154
|
return response.ok ? 0 : 1;
|
|
117
155
|
}
|
|
@@ -135,9 +173,13 @@ export async function cmdContextPromote(runtime: Runtime, project: string, propo
|
|
|
135
173
|
serverUrl: runtime.serverUrl,
|
|
136
174
|
path: "/v1/context/state/promote",
|
|
137
175
|
body: { project, proposalId, evidence },
|
|
138
|
-
|
|
176
|
+
env: runtime.env,
|
|
177
|
+
command: "context",
|
|
178
|
+
io: runtime.io,
|
|
179
|
+
json: runtime.json,
|
|
139
180
|
fetchImpl: runtime.fetchImpl,
|
|
140
181
|
});
|
|
182
|
+
if (response.exitCode) return response.exitCode;
|
|
141
183
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context promote", status: response.status, ...(response.body as object) }, `context promote ${response.ok ? "recorded" : `failed (${response.status})`}`);
|
|
142
184
|
return response.ok ? 0 : 1;
|
|
143
185
|
}
|
|
@@ -147,9 +189,13 @@ export async function cmdContextExplain(runtime: Runtime, project: string, itemI
|
|
|
147
189
|
serverUrl: runtime.serverUrl,
|
|
148
190
|
path: "/v1/context/explain",
|
|
149
191
|
body: { project, id: itemId },
|
|
150
|
-
|
|
192
|
+
env: runtime.env,
|
|
193
|
+
command: "context",
|
|
194
|
+
io: runtime.io,
|
|
195
|
+
json: runtime.json,
|
|
151
196
|
fetchImpl: runtime.fetchImpl,
|
|
152
197
|
});
|
|
198
|
+
if (response.exitCode) return response.exitCode;
|
|
153
199
|
print(runtime.io, runtime.json, { ok: response.ok, command: "context explain", status: response.status, ...(response.body as object) }, `context explain ${response.ok ? "complete" : `failed (${response.status})`}`);
|
|
154
200
|
return response.ok ? 0 : 1;
|
|
155
201
|
}
|
|
@@ -159,9 +205,13 @@ export async function cmdContextWikiCompile(runtime: Runtime, project: string, o
|
|
|
159
205
|
serverUrl: runtime.serverUrl,
|
|
160
206
|
path: "/v1/context/wiki/compile",
|
|
161
207
|
body: { project },
|
|
162
|
-
|
|
208
|
+
env: runtime.env,
|
|
209
|
+
command: "context",
|
|
210
|
+
io: runtime.io,
|
|
211
|
+
json: runtime.json,
|
|
163
212
|
fetchImpl: runtime.fetchImpl,
|
|
164
213
|
});
|
|
214
|
+
if (response.exitCode) return response.exitCode;
|
|
165
215
|
if (!response.ok) {
|
|
166
216
|
print(runtime.io, runtime.json, { ok: false, command: "context wiki-compile", status: response.status, body: response.body }, `wiki compile failed (${response.status})`);
|
|
167
217
|
return 1;
|
|
@@ -191,9 +241,13 @@ export async function cmdContextWikiLint(runtime: Runtime, project: string): Pro
|
|
|
191
241
|
serverUrl: runtime.serverUrl,
|
|
192
242
|
path: "/v1/context/wiki/compile",
|
|
193
243
|
body: { project },
|
|
194
|
-
|
|
244
|
+
env: runtime.env,
|
|
245
|
+
command: "context",
|
|
246
|
+
io: runtime.io,
|
|
247
|
+
json: runtime.json,
|
|
195
248
|
fetchImpl: runtime.fetchImpl,
|
|
196
249
|
});
|
|
250
|
+
if (response.exitCode) return response.exitCode;
|
|
197
251
|
if (!response.ok) {
|
|
198
252
|
print(runtime.io, runtime.json, { ok: false, command: "context wiki-lint", status: response.status, body: response.body }, `wiki lint failed (${response.status})`);
|
|
199
253
|
return 1;
|
|
@@ -3,20 +3,27 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { redactSecrets } from "../redact.ts";
|
|
5
5
|
import { defaultProjectName } from "../project-name.ts";
|
|
6
|
-
import { hasClientHubCredential, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
6
|
+
import { agentSessionEnv, hasClientHubCredential, HubEnvError, readHubEnvRecord, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
7
|
+
import { deleteKxmConfigValue, findConfigRoot, isHubOpReference, KxmHubConfigError, writeHubEndpoint } from "../config.ts";
|
|
8
|
+
import { describeHubConnection, opReferenceFromTokenCommand, resolveProjectIdentity } from "../hub-identity.ts";
|
|
7
9
|
import { agentWorker, type Worker } from "../envelope.ts";
|
|
8
10
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
|
|
9
11
|
import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
|
|
10
12
|
import {
|
|
11
13
|
HUB_BINDING_SCHEMA,
|
|
14
|
+
CloudTokenError,
|
|
12
15
|
HubBindingError,
|
|
13
16
|
hubBindingFile,
|
|
14
17
|
probeHubHealth,
|
|
15
18
|
hubBindingScope,
|
|
19
|
+
effectiveHubBindingScope,
|
|
20
|
+
isCloudTokenEnvName,
|
|
16
21
|
readHubBinding,
|
|
17
22
|
removeHubBinding,
|
|
23
|
+
splitTokenCommand,
|
|
18
24
|
validateHubUrl,
|
|
19
25
|
writeHubBinding,
|
|
26
|
+
type HubBindingRecord,
|
|
20
27
|
type HubHealth,
|
|
21
28
|
} from "../hub-binding.ts";
|
|
22
29
|
import {
|
|
@@ -57,9 +64,13 @@ import {
|
|
|
57
64
|
type Runtime,
|
|
58
65
|
} from "./types.ts";
|
|
59
66
|
|
|
60
|
-
export async function hubGet(
|
|
67
|
+
export async function hubGet(
|
|
68
|
+
url: string,
|
|
69
|
+
fetchImpl: typeof fetch,
|
|
70
|
+
headers?: Record<string, string>,
|
|
71
|
+
): Promise<{ ok: boolean; status: number; body: unknown }> {
|
|
61
72
|
try {
|
|
62
|
-
const response = await fetchImpl(url);
|
|
73
|
+
const response = await fetchImpl(url, headers ? { headers } : undefined);
|
|
63
74
|
const text = redactSecrets((await response.text()).slice(0, 8_000));
|
|
64
75
|
let body: unknown = text;
|
|
65
76
|
try {
|
|
@@ -147,24 +158,59 @@ export async function refreshKxmUpdateNotice(
|
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
export async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
150
|
-
const
|
|
151
|
-
|
|
161
|
+
const connection = describeHubConnection(runtime.dirs.workdir, runtime.env, {
|
|
162
|
+
hasSavedProjectToken: (project) => {
|
|
163
|
+
try {
|
|
164
|
+
const token = readHubEnvRecord(runtime.env)?.projectTokens?.[project];
|
|
165
|
+
return typeof token === "string" && token.trim().length > 0;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
let headers: Record<string, string> | undefined;
|
|
172
|
+
try {
|
|
173
|
+
if (connection.mode === "cloud") {
|
|
174
|
+
const token = resolveClientHubAuthToken(runtime.env, connection.project, runtime.dirs.workdir);
|
|
175
|
+
if (token) headers = { authorization: `Bearer ${token}` };
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof CloudTokenError) {
|
|
179
|
+
print(runtime.io, runtime.json, { ok: false, command: "hub view", error: error.code, detail: error.message }, error.message);
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
if (!(error instanceof HubBindingError)) throw error;
|
|
183
|
+
}
|
|
184
|
+
const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
|
|
185
|
+
const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
|
|
152
186
|
// Scope on the status line deliberately: "attached across a network" and "attached on
|
|
153
187
|
// this box" are otherwise indistinguishable, and only one of them ships a token.
|
|
154
188
|
// Scope is a property of the URL actually contacted, not of whichever file the
|
|
155
189
|
// binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
|
|
156
190
|
// while probing an override would report "loopback" about a remote request.
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
// A cloud mode is remote when the contacted URL is that endpoint, including a
|
|
192
|
+
// loopback SSH forward.
|
|
193
|
+
const effectiveScope = connection.scope;
|
|
194
|
+
const overridden = connection.urlSource === "env";
|
|
195
|
+
const identity = {
|
|
196
|
+
mode: connection.mode,
|
|
197
|
+
modeSource: connection.modeSource,
|
|
198
|
+
url: connection.url,
|
|
199
|
+
urlSource: connection.urlSource,
|
|
200
|
+
project: connection.project,
|
|
201
|
+
projectSource: connection.source,
|
|
202
|
+
keySource: connection.keySource,
|
|
203
|
+
};
|
|
159
204
|
const payload = {
|
|
160
205
|
ok: health.ok && ready.ok,
|
|
161
206
|
command: "hub view",
|
|
162
207
|
target: { url: runtime.serverUrl, scope: effectiveScope, ...(overridden ? { source: "env" } : {}) },
|
|
208
|
+
identity,
|
|
163
209
|
health: health.body,
|
|
164
210
|
ready: ready.body,
|
|
165
211
|
};
|
|
166
212
|
print(runtime.io, runtime.json, payload,
|
|
167
|
-
`hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""}`);
|
|
213
|
+
`hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}`);
|
|
168
214
|
return payload.ok ? 0 : 1;
|
|
169
215
|
}
|
|
170
216
|
|
|
@@ -191,8 +237,17 @@ export async function cmdDash(runtime: Runtime, options: { screen?: string | und
|
|
|
191
237
|
runtime.io.stderr("kxm dash does not support --json; use kxm hub view\n");
|
|
192
238
|
return 2;
|
|
193
239
|
}
|
|
194
|
-
const project =
|
|
195
|
-
|
|
240
|
+
const project = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
|
|
241
|
+
let authToken: string | undefined;
|
|
242
|
+
try {
|
|
243
|
+
authToken = resolveClientHubAuthToken(runtime.env, project, runtime.dirs.workdir);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof CloudTokenError) {
|
|
246
|
+
print(runtime.io, runtime.json, { ok: false, command: "dash", error: error.code, detail: error.message }, error.message);
|
|
247
|
+
return 2;
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
196
251
|
return await runMeshTui({
|
|
197
252
|
serverUrl: runtime.serverUrl,
|
|
198
253
|
dataPath,
|
|
@@ -254,7 +309,14 @@ export function formatHubBindHealth(health: HubHealth): string {
|
|
|
254
309
|
const HUB_BIND_UNAUTHENTICATED_HINT =
|
|
255
310
|
"export KXM_AUTH_TOKEN (or point KXM_STATE_HOME at the hub-env record that already holds one), then re-run; the hub itself requires a token beyond loopback";
|
|
256
311
|
|
|
257
|
-
export
|
|
312
|
+
export interface HubBindOptions {
|
|
313
|
+
cloud?: boolean | undefined;
|
|
314
|
+
tokenEnv?: string | undefined;
|
|
315
|
+
tokenCommand?: string | undefined;
|
|
316
|
+
keyOp?: string | undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
|
|
258
320
|
let url: string;
|
|
259
321
|
try {
|
|
260
322
|
url = validateHubUrl(rawUrl);
|
|
@@ -270,7 +332,61 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
270
332
|
}
|
|
271
333
|
throw error;
|
|
272
334
|
}
|
|
273
|
-
const
|
|
335
|
+
const cloud = options.cloud === true;
|
|
336
|
+
const tokenEnv = options.tokenEnv?.trim() || undefined;
|
|
337
|
+
const tokenCommand = options.tokenCommand?.trim() || undefined;
|
|
338
|
+
const keyOp = options.keyOp?.trim() || opReferenceFromTokenCommand(tokenCommand ?? "") || undefined;
|
|
339
|
+
if (options.keyOp?.trim() && !isHubOpReference(options.keyOp.trim())) {
|
|
340
|
+
print(
|
|
341
|
+
runtime.io,
|
|
342
|
+
runtime.json,
|
|
343
|
+
{ ok: false, command: "hub bind", error: "hub_config_invalid", url },
|
|
344
|
+
"--key-op must be an op://vault/item/field reference, not a token; no binding was written",
|
|
345
|
+
);
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
if (tokenCommand && !cloud) {
|
|
349
|
+
print(
|
|
350
|
+
runtime.io,
|
|
351
|
+
runtime.json,
|
|
352
|
+
{ ok: false, command: "hub bind", error: "cloud_flag_required", url },
|
|
353
|
+
"--token-command requires --cloud; no binding was written",
|
|
354
|
+
);
|
|
355
|
+
return 2;
|
|
356
|
+
}
|
|
357
|
+
if (cloud && !tokenEnv && !tokenCommand && !keyOp) {
|
|
358
|
+
print(
|
|
359
|
+
runtime.io,
|
|
360
|
+
runtime.json,
|
|
361
|
+
{ ok: false, command: "hub bind", error: "cloud_token_source_required", url },
|
|
362
|
+
"--cloud needs --token-env, --token-command, --key-op, or a combination; the token is not stored",
|
|
363
|
+
);
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
if (tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
|
|
367
|
+
print(
|
|
368
|
+
runtime.io,
|
|
369
|
+
runtime.json,
|
|
370
|
+
{ ok: false, command: "hub bind", error: "cloud_token_env_invalid", url },
|
|
371
|
+
"--token-env must be an environment variable name; no binding was written",
|
|
372
|
+
);
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
375
|
+
if (cloud && tokenCommand) {
|
|
376
|
+
try {
|
|
377
|
+
splitTokenCommand(tokenCommand);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
380
|
+
print(
|
|
381
|
+
runtime.io,
|
|
382
|
+
runtime.json,
|
|
383
|
+
{ ok: false, command: "hub bind", error: "cloud_token_command_invalid", url, detail },
|
|
384
|
+
`${detail}; no binding was written`,
|
|
385
|
+
);
|
|
386
|
+
return 2;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const scope = cloud ? "remote" : hubBindingScope(url);
|
|
274
390
|
// Scope-scoped on purpose, and only for **this command**. A remote binding puts a
|
|
275
391
|
// bearer on a network path, so it is refused when nothing can authenticate it; a
|
|
276
392
|
// stored-but-unusable URL otherwise reads later like a network fault and gets debugged
|
|
@@ -280,13 +396,13 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
280
396
|
// fail `kxm peer list` on loopback. Narrowing the claim is the point; the first version
|
|
281
397
|
// of this guard checked the record *before* the scope and refused loopback binds that had
|
|
282
398
|
// always worked, which is a regression against behaviour predating this slice.
|
|
283
|
-
if (scope === "remote") {
|
|
399
|
+
if (!cloud && scope === "remote") {
|
|
284
400
|
// The project that will actually authenticate: a record holding only another
|
|
285
401
|
// project's token cannot authorise this one.
|
|
286
|
-
const bindProject =
|
|
402
|
+
const bindProject = resolveProjectIdentity(runtime.dirs.workdir, runtime.env).project || "project";
|
|
287
403
|
let credentialReady = false;
|
|
288
404
|
try {
|
|
289
|
-
credentialReady = hasClientHubCredential(runtime.env, bindProject);
|
|
405
|
+
credentialReady = hasClientHubCredential(runtime.env, bindProject, runtime.dirs.workdir);
|
|
290
406
|
} catch (error) {
|
|
291
407
|
// A malformed record is a readable configuration failure, not an uncaught throw
|
|
292
408
|
// past a user-facing entry point.
|
|
@@ -329,14 +445,55 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
329
445
|
}
|
|
330
446
|
}
|
|
331
447
|
const file = hubBindingFile(runtime.env);
|
|
448
|
+
const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : undefined);
|
|
449
|
+
const cloudFields = cloud
|
|
450
|
+
? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(bindingCommand ? { tokenCommand: bindingCommand } : {}) }
|
|
451
|
+
: {};
|
|
452
|
+
const record: HubBindingRecord = {
|
|
453
|
+
schema: HUB_BINDING_SCHEMA,
|
|
454
|
+
url,
|
|
455
|
+
boundAt: new Date().toISOString(),
|
|
456
|
+
...cloudFields,
|
|
457
|
+
};
|
|
458
|
+
const identity = resolveProjectIdentity(runtime.dirs.workdir, runtime.env);
|
|
459
|
+
const configFile = join(findConfigRoot(runtime.dirs.workdir), ".kxm", "config.yaml");
|
|
460
|
+
const key = {
|
|
461
|
+
...(keyOp ? { op: keyOp } : {}),
|
|
462
|
+
...(tokenEnv ? { env: tokenEnv } : {}),
|
|
463
|
+
};
|
|
464
|
+
const cloudNote = cloud ? " · token is not stored" : "";
|
|
332
465
|
if (runtime.dryRun) {
|
|
333
|
-
print(
|
|
466
|
+
print(
|
|
467
|
+
runtime.io,
|
|
468
|
+
runtime.json,
|
|
469
|
+
{ ok: true, command: "hub bind", dryRun: true, url, scope, file, configFile, project: identity.project, ...cloudFields },
|
|
470
|
+
`would bind hub ${url} (${scope})${cloudNote}`,
|
|
471
|
+
);
|
|
334
472
|
return 0;
|
|
335
473
|
}
|
|
336
|
-
|
|
474
|
+
try {
|
|
475
|
+
writeHubEndpoint(runtime.dirs.workdir, {
|
|
476
|
+
mode: cloud ? "cloud" : "local",
|
|
477
|
+
url,
|
|
478
|
+
project: identity.project,
|
|
479
|
+
...(key.op || key.env ? { key } : {}),
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof KxmHubConfigError) {
|
|
483
|
+
print(
|
|
484
|
+
runtime.io,
|
|
485
|
+
runtime.json,
|
|
486
|
+
{ ok: false, command: "hub bind", error: error.code, detail: error.message },
|
|
487
|
+
`${error.message}; no binding was written`,
|
|
488
|
+
);
|
|
489
|
+
return 2;
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
|
|
337
494
|
const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
|
|
338
|
-
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs },
|
|
339
|
-
`bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${scope === "remote" ? " · token leaves this machine" : ""}`);
|
|
495
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
|
|
496
|
+
`bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
|
|
340
497
|
return 0;
|
|
341
498
|
}
|
|
342
499
|
|
|
@@ -364,6 +521,7 @@ export async function cmdHubUnbind(runtime: Runtime): Promise<number> {
|
|
|
364
521
|
return 1;
|
|
365
522
|
}
|
|
366
523
|
removeHubBinding(runtime.env);
|
|
524
|
+
deleteKxmConfigValue(findConfigRoot(runtime.dirs.workdir), "hub.mode");
|
|
367
525
|
if (malformed) {
|
|
368
526
|
print(runtime.io, runtime.json, { ok: true, command: "hub unbind", file }, "unbound hub (record was malformed)");
|
|
369
527
|
return 0;
|
|
@@ -383,7 +541,7 @@ export async function cmdWorker(runtime: Runtime, options: {
|
|
|
383
541
|
freshStart?: boolean | undefined;
|
|
384
542
|
}): Promise<number> {
|
|
385
543
|
const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
|
|
386
|
-
const project =
|
|
544
|
+
const project = defaultProjectName(runtime.dirs.workdir, runtime.env, options.project);
|
|
387
545
|
const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
|
|
388
546
|
const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
|
|
389
547
|
const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
|
|
@@ -392,10 +550,22 @@ export async function cmdWorker(runtime: Runtime, options: {
|
|
|
392
550
|
runtime.io.stderr("worker --session-isolation must be workflow or off\n");
|
|
393
551
|
return 2;
|
|
394
552
|
}
|
|
553
|
+
let sessionEnv: NodeJS.ProcessEnv = { KXM_PROJECT: project };
|
|
554
|
+
if (!runtime.dryRun) {
|
|
555
|
+
try {
|
|
556
|
+
sessionEnv = agentSessionEnv(runtime.dirs.workdir, runtime.env, options.project);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof CloudTokenError || error instanceof HubEnvError) {
|
|
559
|
+
runtime.io.stderr(`${error.message}\n`);
|
|
560
|
+
return 2;
|
|
561
|
+
}
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
395
565
|
const extraEnv = {
|
|
396
566
|
...workspaceEnv(runtime),
|
|
567
|
+
...sessionEnv,
|
|
397
568
|
...(name ? { KXM_AGENT_NAME: name } : {}),
|
|
398
|
-
...(project ? { KXM_PROJECT: project } : {}),
|
|
399
569
|
...(model ? { KXM_WORKER_MODEL: model } : {}),
|
|
400
570
|
...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
|
|
401
571
|
...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
|
|
@@ -675,7 +845,7 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
|
|
|
675
845
|
evidence: health === "unknown" ? "timeout" : "probed",
|
|
676
846
|
online: health === "on",
|
|
677
847
|
url: targetUrl,
|
|
678
|
-
scope:
|
|
848
|
+
scope: effectiveHubBindingScope(targetUrl, runtime.env),
|
|
679
849
|
};
|
|
680
850
|
} else {
|
|
681
851
|
hub = { state: "off", evidence: "unconfigured", online: false };
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
import { kxmRuntimePaths, runtimeError } from "../runtime-store.ts";
|
|
30
30
|
import { assembleTenantStatus, formatTenantStatus } from "../tenant-status.ts";
|
|
31
31
|
import { resolveClientAdminAuthToken } from "../hub-env.ts";
|
|
32
|
+
import { defaultProjectName } from "../project-name.ts";
|
|
33
|
+
import { effectiveHubBindingScope } from "../hub-binding.ts";
|
|
32
34
|
import {
|
|
33
35
|
formatHarnessInventory,
|
|
34
36
|
probeHarnessesAsync,
|
|
@@ -779,7 +781,7 @@ export async function cmdKxmRunList(runtime: Runtime): Promise<number> {
|
|
|
779
781
|
}
|
|
780
782
|
}
|
|
781
783
|
|
|
782
|
-
export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
784
|
+
export async function cmdTenantStatus(runtime: Runtime, options: { project?: string | undefined } = {}): Promise<number> {
|
|
783
785
|
let projectRoot: string | undefined;
|
|
784
786
|
let projectId: string;
|
|
785
787
|
try {
|
|
@@ -789,7 +791,8 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
|
789
791
|
return 1;
|
|
790
792
|
}
|
|
791
793
|
const bundle = loadKxmProject(projectRoot, {});
|
|
792
|
-
|
|
794
|
+
const yamlProjectId = String(bundle.project.value.id);
|
|
795
|
+
projectId = defaultProjectName(runtime.cwd, runtime.env, options.project);
|
|
793
796
|
const root = projectRoot;
|
|
794
797
|
|
|
795
798
|
const payload = await assembleTenantStatus({
|
|
@@ -799,6 +802,7 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
|
799
802
|
// and resolve it inside the hub source so a malformed persisted record degrades that
|
|
800
803
|
// one source instead of aborting the Runtime read with it.
|
|
801
804
|
resolveAdminToken: () => resolveClientAdminAuthToken(runtime.env),
|
|
805
|
+
bindingScope: effectiveHubBindingScope(runtime.serverUrl, runtime.env),
|
|
802
806
|
fetchImpl: runtime.fetchImpl,
|
|
803
807
|
runtime: {
|
|
804
808
|
listRuns: async () => {
|
|
@@ -808,7 +812,7 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
|
808
812
|
if (!handle) {
|
|
809
813
|
throw runtimeError("runtime_supervisor_not_running", "runtime", "no live runtime supervisor on this box");
|
|
810
814
|
}
|
|
811
|
-
const result = await kxmRuntimeRequest(handle, "GET", `/v1/projects/${encodeURIComponent(
|
|
815
|
+
const result = await kxmRuntimeRequest(handle, "GET", `/v1/projects/${encodeURIComponent(yamlProjectId)}/runs?projectRoot=${encodeURIComponent(root)}`);
|
|
812
816
|
const runs = (result.runs ?? []) as Array<Record<string, unknown>>;
|
|
813
817
|
return runs.map((run) => ({
|
|
814
818
|
runId: String(run.runId ?? ""),
|
|
@@ -8,9 +8,11 @@ import { gateWorker, workerResult, type Worker, type WorkerOutcome } from "../en
|
|
|
8
8
|
import { canonicalWorkflowEvidenceKey, type WorkflowEvidenceInput } from "../workflow.ts";
|
|
9
9
|
import {
|
|
10
10
|
HubBindingError,
|
|
11
|
+
effectiveHubBindingScope,
|
|
11
12
|
hubBindingFile,
|
|
12
13
|
readHubBinding,
|
|
13
14
|
} from "../hub-binding.ts";
|
|
15
|
+
import { describeHubConnection } from "../hub-identity.ts";
|
|
14
16
|
import type { InstallProbe } from "../kxm-install-kind.ts";
|
|
15
17
|
|
|
16
18
|
export interface CliSpawnResult {
|
|
@@ -165,6 +167,16 @@ export function printWorker(
|
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
export function hostMode(runtime: Runtime): "local" | "hub" {
|
|
170
|
+
try {
|
|
171
|
+
if (describeHubConnection(runtime.cwd, runtime.env).scope === "remote") return "hub";
|
|
172
|
+
} catch {
|
|
173
|
+
// A malformed binding is classified from the URL below.
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
if (effectiveHubBindingScope(runtime.serverUrl, runtime.env) === "remote") return "hub";
|
|
177
|
+
} catch {
|
|
178
|
+
// A malformed binding is classified from the URL below.
|
|
179
|
+
}
|
|
168
180
|
try {
|
|
169
181
|
const hostname = new URL(runtime.serverUrl).hostname;
|
|
170
182
|
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
|
|
@@ -276,13 +288,14 @@ export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
|
|
|
276
288
|
throw error;
|
|
277
289
|
}
|
|
278
290
|
}
|
|
291
|
+
const connection = describeHubConnection(ctx.cwd, ctx.env);
|
|
279
292
|
return {
|
|
280
293
|
...ctx,
|
|
281
294
|
json: Boolean(opts.json),
|
|
282
295
|
dryRun: Boolean(opts.dryRun),
|
|
283
296
|
...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
|
|
284
297
|
dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
|
|
285
|
-
serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
298
|
+
serverUrl: connection.url || envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
286
299
|
...(boundHubUrl ? { boundHubUrl } : {}),
|
|
287
300
|
fetchImpl: ctx.io.fetchImpl ?? fetch,
|
|
288
301
|
};
|
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
type Runtime,
|
|
38
38
|
} from "./types.ts";
|
|
39
39
|
import { resolvePickItem, type PickCandidate } from "./roles.ts";
|
|
40
|
+
import { CloudTokenError } from "../hub-binding.ts";
|
|
41
|
+
import { resolveControlPlaneAuthToken } from "../hub-env.ts";
|
|
40
42
|
|
|
41
43
|
const CLI_NAME = "kxm";
|
|
42
44
|
|
|
@@ -480,7 +482,16 @@ export async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string
|
|
|
480
482
|
export async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string | undefined; reason?: string | undefined }): Promise<number> {
|
|
481
483
|
const requirementKey = options.requirement?.trim() ?? "";
|
|
482
484
|
const reason = options.reason?.trim() ?? "";
|
|
483
|
-
|
|
485
|
+
let adminToken: string | undefined;
|
|
486
|
+
try {
|
|
487
|
+
adminToken = resolveControlPlaneAuthToken(runtime.env);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof CloudTokenError) {
|
|
490
|
+
runtime.io.stderr(`${error.message}\n`);
|
|
491
|
+
return 2;
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
484
495
|
if (!runId || !stageId || !requirementKey || !reason || !adminToken) {
|
|
485
496
|
runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n");
|
|
486
497
|
return 2;
|