@kontextmind/kxm 0.7.163 → 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 +22 -0
- package/README.md +2 -0
- package/docs/README.md +1 -0
- package/docs/architecture/inventory.md +4 -3
- package/docs/contributing/harness-routing-internals.md +28 -27
- 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/docs/reference/harness-routing.md +17 -15
- 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
|
@@ -3,20 +3,26 @@ 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, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
7
7
|
import { agentWorker, type Worker } from "../envelope.ts";
|
|
8
8
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
|
|
9
9
|
import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
|
|
10
10
|
import {
|
|
11
11
|
HUB_BINDING_SCHEMA,
|
|
12
|
+
CloudTokenError,
|
|
12
13
|
HubBindingError,
|
|
13
14
|
hubBindingFile,
|
|
14
15
|
probeHubHealth,
|
|
15
16
|
hubBindingScope,
|
|
17
|
+
effectiveHubBindingScope,
|
|
18
|
+
isCloudTokenEnvName,
|
|
16
19
|
readHubBinding,
|
|
17
20
|
removeHubBinding,
|
|
21
|
+
splitTokenCommand,
|
|
18
22
|
validateHubUrl,
|
|
19
23
|
writeHubBinding,
|
|
24
|
+
resolveCloudHubToken,
|
|
25
|
+
type HubBindingRecord,
|
|
20
26
|
type HubHealth,
|
|
21
27
|
} from "../hub-binding.ts";
|
|
22
28
|
import {
|
|
@@ -57,9 +63,13 @@ import {
|
|
|
57
63
|
type Runtime,
|
|
58
64
|
} from "./types.ts";
|
|
59
65
|
|
|
60
|
-
export async function hubGet(
|
|
66
|
+
export async function hubGet(
|
|
67
|
+
url: string,
|
|
68
|
+
fetchImpl: typeof fetch,
|
|
69
|
+
headers?: Record<string, string>,
|
|
70
|
+
): Promise<{ ok: boolean; status: number; body: unknown }> {
|
|
61
71
|
try {
|
|
62
|
-
const response = await fetchImpl(url);
|
|
72
|
+
const response = await fetchImpl(url, headers ? { headers } : undefined);
|
|
63
73
|
const text = redactSecrets((await response.text()).slice(0, 8_000));
|
|
64
74
|
let body: unknown = text;
|
|
65
75
|
try {
|
|
@@ -147,14 +157,29 @@ export async function refreshKxmUpdateNotice(
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
export async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
let headers: Record<string, string> | undefined;
|
|
161
|
+
try {
|
|
162
|
+
const binding = readHubBinding(runtime.env);
|
|
163
|
+
if (binding?.cloud) {
|
|
164
|
+
headers = { authorization: `Bearer ${resolveCloudHubToken(binding, runtime.env)}` };
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof CloudTokenError) {
|
|
168
|
+
print(runtime.io, runtime.json, { ok: false, command: "hub view", error: error.code, detail: error.message }, error.message);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
if (!(error instanceof HubBindingError)) throw error;
|
|
172
|
+
}
|
|
173
|
+
const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
|
|
174
|
+
const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
|
|
152
175
|
// Scope on the status line deliberately: "attached across a network" and "attached on
|
|
153
176
|
// this box" are otherwise indistinguishable, and only one of them ships a token.
|
|
154
177
|
// Scope is a property of the URL actually contacted, not of whichever file the
|
|
155
178
|
// binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
|
|
156
179
|
// while probing an override would report "loopback" about a remote request.
|
|
157
|
-
|
|
180
|
+
// A cloud binding is remote when the contacted URL is that binding, including a
|
|
181
|
+
// loopback SSH forward.
|
|
182
|
+
const effectiveScope = effectiveHubBindingScope(runtime.serverUrl, runtime.env);
|
|
158
183
|
const overridden = Boolean(runtime.boundHubUrl && runtime.boundHubUrl !== runtime.serverUrl);
|
|
159
184
|
const payload = {
|
|
160
185
|
ok: health.ok && ready.ok,
|
|
@@ -192,7 +217,16 @@ export async function cmdDash(runtime: Runtime, options: { screen?: string | und
|
|
|
192
217
|
return 2;
|
|
193
218
|
}
|
|
194
219
|
const project = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
|
|
195
|
-
|
|
220
|
+
let authToken: string | undefined;
|
|
221
|
+
try {
|
|
222
|
+
authToken = resolveClientHubAuthToken(runtime.env, project);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof CloudTokenError) {
|
|
225
|
+
print(runtime.io, runtime.json, { ok: false, command: "dash", error: error.code, detail: error.message }, error.message);
|
|
226
|
+
return 2;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
196
230
|
return await runMeshTui({
|
|
197
231
|
serverUrl: runtime.serverUrl,
|
|
198
232
|
dataPath,
|
|
@@ -254,7 +288,13 @@ export function formatHubBindHealth(health: HubHealth): string {
|
|
|
254
288
|
const HUB_BIND_UNAUTHENTICATED_HINT =
|
|
255
289
|
"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
290
|
|
|
257
|
-
export
|
|
291
|
+
export interface HubBindOptions {
|
|
292
|
+
cloud?: boolean | undefined;
|
|
293
|
+
tokenEnv?: string | undefined;
|
|
294
|
+
tokenCommand?: string | undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
|
|
258
298
|
let url: string;
|
|
259
299
|
try {
|
|
260
300
|
url = validateHubUrl(rawUrl);
|
|
@@ -270,7 +310,51 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
270
310
|
}
|
|
271
311
|
throw error;
|
|
272
312
|
}
|
|
273
|
-
const
|
|
313
|
+
const cloud = options.cloud === true;
|
|
314
|
+
const tokenEnv = options.tokenEnv?.trim() || undefined;
|
|
315
|
+
const tokenCommand = options.tokenCommand?.trim() || undefined;
|
|
316
|
+
if ((tokenEnv || tokenCommand) && !cloud) {
|
|
317
|
+
print(
|
|
318
|
+
runtime.io,
|
|
319
|
+
runtime.json,
|
|
320
|
+
{ ok: false, command: "hub bind", error: "cloud_flag_required", url },
|
|
321
|
+
"--token-env and --token-command require --cloud; no binding was written",
|
|
322
|
+
);
|
|
323
|
+
return 2;
|
|
324
|
+
}
|
|
325
|
+
if (cloud && !tokenEnv && !tokenCommand) {
|
|
326
|
+
print(
|
|
327
|
+
runtime.io,
|
|
328
|
+
runtime.json,
|
|
329
|
+
{ ok: false, command: "hub bind", error: "cloud_token_source_required", url },
|
|
330
|
+
"--cloud needs --token-env, --token-command, or both; the token is not stored in the binding",
|
|
331
|
+
);
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
if (cloud && tokenEnv && !isCloudTokenEnvName(tokenEnv)) {
|
|
335
|
+
print(
|
|
336
|
+
runtime.io,
|
|
337
|
+
runtime.json,
|
|
338
|
+
{ ok: false, command: "hub bind", error: "cloud_token_env_invalid", url },
|
|
339
|
+
"--token-env must be an environment variable name; no binding was written",
|
|
340
|
+
);
|
|
341
|
+
return 2;
|
|
342
|
+
}
|
|
343
|
+
if (cloud && tokenCommand) {
|
|
344
|
+
try {
|
|
345
|
+
splitTokenCommand(tokenCommand);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
348
|
+
print(
|
|
349
|
+
runtime.io,
|
|
350
|
+
runtime.json,
|
|
351
|
+
{ ok: false, command: "hub bind", error: "cloud_token_command_invalid", url, detail },
|
|
352
|
+
`${detail}; no binding was written`,
|
|
353
|
+
);
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const scope = cloud ? "remote" : hubBindingScope(url);
|
|
274
358
|
// Scope-scoped on purpose, and only for **this command**. A remote binding puts a
|
|
275
359
|
// bearer on a network path, so it is refused when nothing can authenticate it; a
|
|
276
360
|
// stored-but-unusable URL otherwise reads later like a network fault and gets debugged
|
|
@@ -280,7 +364,7 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
280
364
|
// fail `kxm peer list` on loopback. Narrowing the claim is the point; the first version
|
|
281
365
|
// of this guard checked the record *before* the scope and refused loopback binds that had
|
|
282
366
|
// always worked, which is a regression against behaviour predating this slice.
|
|
283
|
-
if (scope === "remote") {
|
|
367
|
+
if (!cloud && scope === "remote") {
|
|
284
368
|
// The project that will actually authenticate: a record holding only another
|
|
285
369
|
// project's token cannot authorise this one.
|
|
286
370
|
const bindProject = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
|
|
@@ -329,14 +413,29 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
329
413
|
}
|
|
330
414
|
}
|
|
331
415
|
const file = hubBindingFile(runtime.env);
|
|
416
|
+
const cloudFields = cloud
|
|
417
|
+
? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(tokenCommand ? { tokenCommand } : {}) }
|
|
418
|
+
: {};
|
|
419
|
+
const record: HubBindingRecord = {
|
|
420
|
+
schema: HUB_BINDING_SCHEMA,
|
|
421
|
+
url,
|
|
422
|
+
boundAt: new Date().toISOString(),
|
|
423
|
+
...cloudFields,
|
|
424
|
+
};
|
|
425
|
+
const cloudNote = cloud ? " · token is not stored" : "";
|
|
332
426
|
if (runtime.dryRun) {
|
|
333
|
-
print(
|
|
427
|
+
print(
|
|
428
|
+
runtime.io,
|
|
429
|
+
runtime.json,
|
|
430
|
+
{ ok: true, command: "hub bind", dryRun: true, url, scope, file, ...cloudFields },
|
|
431
|
+
`would bind hub ${url} (${scope})${cloudNote}`,
|
|
432
|
+
);
|
|
334
433
|
return 0;
|
|
335
434
|
}
|
|
336
|
-
writeHubBinding({
|
|
435
|
+
writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
|
|
337
436
|
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" : ""}`);
|
|
437
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs, ...cloudFields },
|
|
438
|
+
`bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
|
|
340
439
|
return 0;
|
|
341
440
|
}
|
|
342
441
|
|
|
@@ -383,7 +482,7 @@ export async function cmdWorker(runtime: Runtime, options: {
|
|
|
383
482
|
freshStart?: boolean | undefined;
|
|
384
483
|
}): Promise<number> {
|
|
385
484
|
const name = options.name?.trim() || runtime.env.KXM_AGENT_NAME?.trim();
|
|
386
|
-
const project =
|
|
485
|
+
const project = defaultProjectName(runtime.dirs.workdir, runtime.env, options.project);
|
|
387
486
|
const model = options.model?.trim() || runtime.env.KXM_WORKER_MODEL?.trim();
|
|
388
487
|
const fallbackModels = options.fallbackModels?.trim() || runtime.env.KXM_WORKER_FALLBACK_MODELS?.trim();
|
|
389
488
|
const tools = options.tools?.trim() || runtime.env.KXM_WORKER_TOOLS?.trim();
|
|
@@ -392,10 +491,22 @@ export async function cmdWorker(runtime: Runtime, options: {
|
|
|
392
491
|
runtime.io.stderr("worker --session-isolation must be workflow or off\n");
|
|
393
492
|
return 2;
|
|
394
493
|
}
|
|
494
|
+
let sessionEnv: NodeJS.ProcessEnv = { KXM_PROJECT: project };
|
|
495
|
+
if (!runtime.dryRun) {
|
|
496
|
+
try {
|
|
497
|
+
sessionEnv = agentSessionEnv(runtime.dirs.workdir, runtime.env, options.project);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof CloudTokenError || error instanceof HubEnvError) {
|
|
500
|
+
runtime.io.stderr(`${error.message}\n`);
|
|
501
|
+
return 2;
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
395
506
|
const extraEnv = {
|
|
396
507
|
...workspaceEnv(runtime),
|
|
508
|
+
...sessionEnv,
|
|
397
509
|
...(name ? { KXM_AGENT_NAME: name } : {}),
|
|
398
|
-
...(project ? { KXM_PROJECT: project } : {}),
|
|
399
510
|
...(model ? { KXM_WORKER_MODEL: model } : {}),
|
|
400
511
|
...(fallbackModels ? { KXM_WORKER_FALLBACK_MODELS: fallbackModels } : {}),
|
|
401
512
|
...(tools ? { KXM_WORKER_TOOLS: tools } : {}),
|
|
@@ -675,7 +786,7 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
|
|
|
675
786
|
evidence: health === "unknown" ? "timeout" : "probed",
|
|
676
787
|
online: health === "on",
|
|
677
788
|
url: targetUrl,
|
|
678
|
-
scope:
|
|
789
|
+
scope: effectiveHubBindingScope(targetUrl, runtime.env),
|
|
679
790
|
};
|
|
680
791
|
} else {
|
|
681
792
|
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,6 +8,7 @@ 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";
|
|
@@ -165,6 +166,11 @@ export function printWorker(
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export function hostMode(runtime: Runtime): "local" | "hub" {
|
|
169
|
+
try {
|
|
170
|
+
if (effectiveHubBindingScope(runtime.serverUrl, runtime.env) === "remote") return "hub";
|
|
171
|
+
} catch {
|
|
172
|
+
// A malformed binding is classified from the URL below.
|
|
173
|
+
}
|
|
168
174
|
try {
|
|
169
175
|
const hostname = new URL(runtime.serverUrl).hostname;
|
|
170
176
|
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
|
|
@@ -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;
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { readInstalledKxmVersion } from "./kxm-update.ts";
|
|
|
19
19
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
20
20
|
import { HubClient } from "./client.ts";
|
|
21
21
|
import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
|
|
22
|
+
import { CloudTokenError } from "./hub-binding.ts";
|
|
22
23
|
import { defaultProjectName } from "./project-name.ts";
|
|
23
24
|
import {
|
|
24
25
|
AGENT_COMMANDS_MAP,
|
|
@@ -310,6 +311,9 @@ async function dispatchAgentCliCommand(
|
|
|
310
311
|
return 2;
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
const explicitProject = typeof rawArgs.project === "string" ? rawArgs.project.trim() : "";
|
|
315
|
+
if (explicitProject) runtime = { ...runtime, env: { ...runtime.env, KXM_PROJECT: explicitProject } };
|
|
316
|
+
|
|
313
317
|
let args: Record<string, unknown> = {};
|
|
314
318
|
if (typeof rawArgs.payload === "string" && rawArgs.payload.trim()) {
|
|
315
319
|
try {
|
|
@@ -324,7 +328,7 @@ async function dispatchAgentCliCommand(
|
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
for (const [key, val] of Object.entries(rawArgs)) {
|
|
327
|
-
if (val !== undefined && key !== "payload") {
|
|
331
|
+
if (val !== undefined && key !== "payload" && key !== "project") {
|
|
328
332
|
if (key === "timeoutMs" || key === "ttlMs" || key === "attempt") {
|
|
329
333
|
args[key] = Number(val);
|
|
330
334
|
} else if (key === "targets" && typeof val === "string") {
|
|
@@ -388,6 +392,15 @@ async function dispatchAgentCliCommand(
|
|
|
388
392
|
);
|
|
389
393
|
return 2;
|
|
390
394
|
}
|
|
395
|
+
if (error instanceof CloudTokenError) {
|
|
396
|
+
print(
|
|
397
|
+
runtime.io,
|
|
398
|
+
runtime.json,
|
|
399
|
+
{ ok: false, error: error.code, detail: message },
|
|
400
|
+
message,
|
|
401
|
+
);
|
|
402
|
+
return 2;
|
|
403
|
+
}
|
|
391
404
|
print(runtime.io, runtime.json, { ok: false, error: "command_failed", detail: message }, `command failed: ${message}`);
|
|
392
405
|
return 1;
|
|
393
406
|
} finally {
|
|
@@ -649,8 +662,9 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
649
662
|
const tenantCmd = addGlobalOptions(program.command("tenant").description("Composed tenant reads for machine clients (portal)"));
|
|
650
663
|
tenantCmd.helpCommand("help", "Show tenant help");
|
|
651
664
|
addGlobalOptions(tenantCmd.command("status").description("Read hub metadata and authoritative Runtime run state as one labeled view"))
|
|
652
|
-
.
|
|
653
|
-
|
|
665
|
+
.option("--project <id>", "Hub project; overrides KXM_PROJECT for this command")
|
|
666
|
+
.action(async function tenantStatusAction(this: Command, options: { project?: string }) {
|
|
667
|
+
result.code = await cmdTenantStatus(runtimeFrom(ctx, this), options);
|
|
654
668
|
});
|
|
655
669
|
|
|
656
670
|
const pricesCmd = addGlobalOptions(program.command("prices").description("Stamp the local list-price catalog. Estimates stay unknown until today's stamp"));
|
|
@@ -816,6 +830,7 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
816
830
|
peer.helpCommand("help", "Show peer help");
|
|
817
831
|
|
|
818
832
|
addGlobalOptions(peer.command("list").description("List peer agents in this project's hub pool with host and presence"))
|
|
833
|
+
.option("--project <id>", "Hub project; overrides KXM_PROJECT for this command")
|
|
819
834
|
.option("--include-offline", "Also list registered peers whose hub lease has expired")
|
|
820
835
|
.option("--payload <json>", "JSON payload")
|
|
821
836
|
.action(async function peerListAction(this: Command, opts?: Record<string, unknown>) {
|
|
@@ -1326,8 +1341,11 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
1326
1341
|
result.code = await cmdStop(runtimeFrom(ctx, this), options.waitMs);
|
|
1327
1342
|
});
|
|
1328
1343
|
addGlobalOptions(hub.command("bind").description("Bind this machine to a running hub").argument("<url>", "Hub base URL (http or https)"))
|
|
1329
|
-
.
|
|
1330
|
-
|
|
1344
|
+
.option("--cloud", "Mark the hub remote even when the URL is a loopback forward")
|
|
1345
|
+
.option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
|
|
1346
|
+
.option("--token-command <command>", "Program that prints the hub token; the token is not stored")
|
|
1347
|
+
.action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string }) {
|
|
1348
|
+
result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
|
|
1331
1349
|
});
|
|
1332
1350
|
addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));
|
|
1333
1351
|
addGlobalOptions(program.command("dash").description("Live screens for headless agents, tasks, workflows, and plans")
|
|
@@ -7,6 +7,7 @@ import { HubClient, HubHttpError } from "./client.ts";
|
|
|
7
7
|
import { loadKxmConfig } from "./config.ts";
|
|
8
8
|
import { ensureHubRunning, hubAutoStartMode } from "./hub-autostart.ts";
|
|
9
9
|
import { AgentProjectTokenMissingError, resolveAgentHubAuthToken } from "./hub-env.ts";
|
|
10
|
+
import { CloudTokenError, resolveHubServerUrl } from "./hub-binding.ts";
|
|
10
11
|
import { defaultProjectName } from "./project-name.ts";
|
|
11
12
|
import { nousFactoryWork, type NousRegistrationReport } from "./nous-pi.ts";
|
|
12
13
|
import {
|
|
@@ -346,7 +347,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
async function showKxmHub(ctx: { ui: { notify: (message: string, type: "info" | "warning" | "error") => void } }): Promise<void> {
|
|
349
|
-
const url = (process.env
|
|
350
|
+
const url = resolveHubServerUrl(process.env);
|
|
350
351
|
let health = "unreachable";
|
|
351
352
|
try {
|
|
352
353
|
const response = await fetch(`${url}/health`);
|
|
@@ -676,7 +677,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
676
677
|
return;
|
|
677
678
|
}
|
|
678
679
|
currentPiSessionId = ctx.sessionManager?.getSessionId();
|
|
679
|
-
const serverUrl = process.env
|
|
680
|
+
const serverUrl = resolveHubServerUrl(process.env);
|
|
680
681
|
const project = defaultProjectName(ctx.cwd, process.env);
|
|
681
682
|
const name = process.env.KXM_AGENT_NAME ?? pi.getSessionName() ?? `pi-${process.pid}`;
|
|
682
683
|
agentName = name;
|
|
@@ -710,7 +711,10 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
710
711
|
try {
|
|
711
712
|
hubAuthToken = resolveAgentHubAuthToken(process.env, project);
|
|
712
713
|
} catch (error) {
|
|
713
|
-
|
|
714
|
+
const detail = error instanceof CloudTokenError
|
|
715
|
+
? error.message
|
|
716
|
+
: `kxm could not read persisted hub credentials: ${error instanceof Error ? error.message : String(error)}`;
|
|
717
|
+
ctx.ui.notify(detail, "error");
|
|
714
718
|
await applySessionChrome(ctx, event, true);
|
|
715
719
|
return;
|
|
716
720
|
}
|
|
@@ -133,6 +133,9 @@ export async function ensureHubRunning(options: EnsureHubOptions): Promise<Ensur
|
|
|
133
133
|
if (binding) {
|
|
134
134
|
const { health } = await probeHubHealth(binding.url, fetchImpl);
|
|
135
135
|
if (health === "on") return { status: "bound-healthy", url: binding.url };
|
|
136
|
+
// A cloud binding names a remote hub. A dead forward must not start a local hub
|
|
137
|
+
// that would then authenticate with this machine's hub-env token.
|
|
138
|
+
if (binding.cloud) return { status: "failed", reason: "cloud_hub_unreachable", logPath: "" };
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
// 2. A live local claim means a hub (started any way) already owns this
|