@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.
Files changed (38) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +2 -0
  4. package/docs/README.md +1 -0
  5. package/docs/architecture/inventory.md +4 -3
  6. package/docs/contributing/harness-routing-internals.md +28 -27
  7. package/docs/glossary.md +3 -3
  8. package/docs/operations/troubleshooting.md +3 -0
  9. package/docs/operations.md +154 -0
  10. package/docs/reference/cli-reference.md +4 -3
  11. package/docs/reference/configuration.md +11 -1
  12. package/docs/reference/harness-routing.md +17 -15
  13. package/package.json +1 -1
  14. package/plugins/kxm/.claude-plugin/plugin.json +2 -2
  15. package/plugins/kxm/README.md +2 -2
  16. package/plugins/kxm/dist/claude-hook.js +120 -16
  17. package/plugins/kxm/dist/cli.js +14929 -14542
  18. package/plugins/kxm/dist/extension.js +299 -122
  19. package/plugins/kxm/dist/mcp-server.js +7690 -106
  20. package/plugins/kxm/dist/runtime-supervisor.js +151 -4
  21. package/plugins/kxm/dist/runtime.js +158 -11
  22. package/plugins/kxm/package.json +1 -1
  23. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +5 -1
  24. package/plugins/kxm/src/cli/context-skills.ts +64 -10
  25. package/plugins/kxm/src/cli/hub.ts +128 -17
  26. package/plugins/kxm/src/cli/project.ts +7 -3
  27. package/plugins/kxm/src/cli/types.ts +6 -0
  28. package/plugins/kxm/src/cli/workflows.ts +12 -1
  29. package/plugins/kxm/src/cli.ts +23 -5
  30. package/plugins/kxm/src/extension.ts +7 -3
  31. package/plugins/kxm/src/hub-autostart.ts +3 -0
  32. package/plugins/kxm/src/hub-binding.ts +182 -2
  33. package/plugins/kxm/src/hub-env.ts +71 -0
  34. package/plugins/kxm/src/mcp-server.ts +3 -2
  35. package/plugins/kxm/src/project-name.ts +34 -7
  36. package/plugins/kxm/src/runtime-supervisor.ts +9 -3
  37. package/plugins/kxm/src/tenant-status.ts +9 -2
  38. package/scripts/build-runtime.mjs +5 -0
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
@@ -20,8 +21,31 @@ export interface HubBindingRecord {
20
21
  schema: typeof HUB_BINDING_SCHEMA;
21
22
  url: string;
22
23
  boundAt: string;
24
+ /** A forwarded or otherwise remote hub. Loopback in the URL does not make this local. */
25
+ cloud?: true;
26
+ /** Environment variable that holds the hub token. The token itself is never stored. */
27
+ tokenEnv?: string;
28
+ /** Program plus arguments that print the hub token. Not a shell pipeline, and not the token. */
29
+ tokenCommand?: string;
23
30
  }
24
31
 
32
+ export type CloudTokenErrorCode = "cloud_token_missing" | "cloud_token_command_failed" | "cloud_token_command_invalid";
33
+
34
+ /** The cloud binding named a token source and that source did not produce a token. */
35
+ export class CloudTokenError extends Error {
36
+ readonly code: CloudTokenErrorCode;
37
+
38
+ constructor(code: CloudTokenErrorCode, message: string) {
39
+ super(message);
40
+ this.name = "CloudTokenError";
41
+ this.code = code;
42
+ }
43
+ }
44
+
45
+ const TOKEN_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
46
+ const TOKEN_COMMAND_TIMEOUT_MS = 15_000;
47
+ const TOKEN_COMMAND_MAX_BUFFER = 4096;
48
+
25
49
  export class HubBindingError extends Error {
26
50
  constructor(message: string) {
27
51
  super(message);
@@ -71,6 +95,132 @@ export function validateHubUrl(raw: string): string {
71
95
  return parsed.href.replace(/\/$/, "");
72
96
  }
73
97
 
98
+ /** A cloud binding's token command is a program and arguments, never a shell. */
99
+ export function splitTokenCommand(raw: string): string[] {
100
+ if (/[|&;<>$`\n\r]/.test(raw)) {
101
+ throw new CloudTokenError(
102
+ "cloud_token_command_invalid",
103
+ "token command must be a program and arguments, not a shell pipeline",
104
+ );
105
+ }
106
+ const args: string[] = [];
107
+ let current = "";
108
+ let quote: "'" | "\"" | undefined;
109
+ for (const ch of raw) {
110
+ if (quote) {
111
+ if (ch === quote) quote = undefined;
112
+ else current += ch;
113
+ continue;
114
+ }
115
+ if (ch === "'" || ch === "\"") {
116
+ quote = ch;
117
+ continue;
118
+ }
119
+ if (ch === " " || ch === "\t") {
120
+ if (current.length > 0) {
121
+ args.push(current);
122
+ current = "";
123
+ }
124
+ continue;
125
+ }
126
+ current += ch;
127
+ }
128
+ if (quote) {
129
+ throw new CloudTokenError("cloud_token_command_invalid", "token command has an unclosed quote");
130
+ }
131
+ if (current.length > 0) args.push(current);
132
+ if (args.length === 0) {
133
+ throw new CloudTokenError("cloud_token_command_invalid", "token command is empty");
134
+ }
135
+ return args;
136
+ }
137
+
138
+ export function isCloudTokenEnvName(value: string): boolean {
139
+ return TOKEN_ENV_NAME.test(value);
140
+ }
141
+
142
+ /**
143
+ * Where this command should say the hub sits. A cloud binding is remote even when its
144
+ * URL is a loopback forward, but only for the URL the binding names. `KXM_SERVER_URL`
145
+ * pointing somewhere else keeps that URL's own scope.
146
+ */
147
+ export function effectiveHubBindingScope(url: string, env: NodeJS.ProcessEnv = process.env): HubBindingScope {
148
+ try {
149
+ const binding = readHubBinding(env);
150
+ if (binding?.cloud) {
151
+ try {
152
+ if (validateHubUrl(url) === binding.url) return "remote";
153
+ } catch {
154
+ // An unparsable override is not the cloud binding.
155
+ }
156
+ }
157
+ } catch (error) {
158
+ if (!(error instanceof HubBindingError)) throw error;
159
+ }
160
+ return hubBindingScope(url);
161
+ }
162
+
163
+ /** Hub URL for a session: explicit env, else the binding, else the local default. */
164
+ export function resolveHubServerUrl(env: NodeJS.ProcessEnv = process.env): string {
165
+ const fromEnv = env.KXM_SERVER_URL?.trim();
166
+ if (fromEnv) return fromEnv.replace(/\/$/, "");
167
+ try {
168
+ const binding = readHubBinding(env);
169
+ if (binding?.url) return binding.url;
170
+ } catch (error) {
171
+ if (!(error instanceof HubBindingError)) throw error;
172
+ }
173
+ return "http://127.0.0.1:7331";
174
+ }
175
+
176
+ /**
177
+ * Resolve the token a cloud binding names. Never reads hub-env and never uses
178
+ * `KXM_AUTH_TOKEN` unless `tokenEnv` is that name. `tokenEnv` wins when it is non-empty;
179
+ * otherwise the command runs. The command is not cached.
180
+ */
181
+ export function resolveCloudHubToken(binding: HubBindingRecord, env: NodeJS.ProcessEnv = process.env): string {
182
+ if (!binding.cloud) {
183
+ throw new CloudTokenError("cloud_token_missing", "cloud hub token is missing; the local hub-env token was not used");
184
+ }
185
+ if (binding.tokenEnv) {
186
+ const value = env[binding.tokenEnv]?.trim();
187
+ if (value) return value;
188
+ }
189
+ if (binding.tokenCommand) return runTokenCommand(binding.tokenCommand, env);
190
+ const source = binding.tokenEnv ? `${binding.tokenEnv} is unset or empty` : "no token source is configured";
191
+ throw new CloudTokenError(
192
+ "cloud_token_missing",
193
+ `cloud hub token is missing: ${source}; the local hub-env token was not used`,
194
+ );
195
+ }
196
+
197
+ function runTokenCommand(command: string, env: NodeJS.ProcessEnv): string {
198
+ const argv = splitTokenCommand(command);
199
+ const [program, ...args] = argv;
200
+ const result = spawnSync(program!, args, {
201
+ encoding: "utf8",
202
+ shell: false,
203
+ timeout: TOKEN_COMMAND_TIMEOUT_MS,
204
+ maxBuffer: TOKEN_COMMAND_MAX_BUFFER,
205
+ env,
206
+ windowsHide: true,
207
+ });
208
+ if (result.error || result.status !== 0) {
209
+ throw new CloudTokenError(
210
+ "cloud_token_command_failed",
211
+ "token command failed; the local hub-env token was not used",
212
+ );
213
+ }
214
+ const stdout = result.stdout ?? "";
215
+ if (stdout.trim().length === 0 || stdout.trim() !== stdout.trim().split(/\r?\n/)[0]) {
216
+ throw new CloudTokenError(
217
+ "cloud_token_command_failed",
218
+ "token command did not print a single token; the local hub-env token was not used",
219
+ );
220
+ }
221
+ return stdout.trim();
222
+ }
223
+
74
224
  /** Loopback literals only; `0.0.0.0`, a LAN address or a hostname are all remote. */
75
225
  export function hubBindingScope(url: string): HubBindingScope {
76
226
  let host: string;
@@ -111,8 +261,11 @@ export function readHubBinding(env: NodeJS.ProcessEnv = process.env): HubBinding
111
261
  }
112
262
  const row = parsed as Record<string, unknown>;
113
263
  const keys = Object.keys(row);
264
+ const allowed = new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
265
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
114
266
  if (
115
- keys.length !== 3
267
+ keys.some((key) => !allowed.has(key))
268
+ || (!cloudish && keys.length !== 3)
116
269
  || row.schema !== HUB_BINDING_SCHEMA
117
270
  || typeof row.url !== "string"
118
271
  || typeof row.boundAt !== "string"
@@ -126,7 +279,34 @@ export function readHubBinding(env: NodeJS.ProcessEnv = process.env): HubBinding
126
279
  } catch {
127
280
  throw new HubBindingError(`malformed hub binding at ${file}`);
128
281
  }
129
- return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
282
+ if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
283
+ if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
284
+ let tokenEnv: string | undefined;
285
+ let tokenCommand: string | undefined;
286
+ if (row.tokenEnv !== undefined) {
287
+ if (typeof row.tokenEnv !== "string" || !isCloudTokenEnvName(row.tokenEnv)) {
288
+ throw new HubBindingError(`malformed hub binding at ${file}`);
289
+ }
290
+ tokenEnv = row.tokenEnv;
291
+ }
292
+ if (row.tokenCommand !== undefined) {
293
+ if (typeof row.tokenCommand !== "string") throw new HubBindingError(`malformed hub binding at ${file}`);
294
+ try {
295
+ splitTokenCommand(row.tokenCommand);
296
+ } catch {
297
+ throw new HubBindingError(`malformed hub binding at ${file}`);
298
+ }
299
+ tokenCommand = row.tokenCommand;
300
+ }
301
+ if (!tokenEnv && !tokenCommand) throw new HubBindingError(`malformed hub binding at ${file}`);
302
+ return {
303
+ schema: HUB_BINDING_SCHEMA,
304
+ url,
305
+ boundAt: row.boundAt,
306
+ cloud: true,
307
+ ...(tokenEnv ? { tokenEnv } : {}),
308
+ ...(tokenCommand ? { tokenCommand } : {}),
309
+ };
130
310
  }
131
311
 
132
312
  export function writeHubBinding(record: HubBindingRecord, env: NodeJS.ProcessEnv = process.env): string {
@@ -2,6 +2,15 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
2
2
  import { homedir } from "node:os";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import {
6
+ CloudTokenError,
7
+ HubBindingError,
8
+ hubBindingFile,
9
+ readHubBinding,
10
+ resolveCloudHubToken,
11
+ type HubBindingRecord,
12
+ } from "./hub-binding.ts";
13
+ import { defaultProjectName } from "./project-name.ts";
5
14
 
6
15
  export const HUB_ENV_SCHEMA = "kxm.hub-env.v1" as const;
7
16
 
@@ -210,6 +219,58 @@ export function resolveHubCredentials(options: ResolveHubCredentialsOptions = {}
210
219
  * only another project's token cannot authorise this one, and a guard that counts it as a
211
220
  * credential stores a binding that will fail exactly like the one it prevented.
212
221
  */
222
+ /** A cloud binding, when this machine has one. A malformed binding fails closed. */
223
+ function activeCloudBinding(env: NodeJS.ProcessEnv): HubBindingRecord | undefined {
224
+ try {
225
+ const binding = readHubBinding(env);
226
+ return binding?.cloud ? binding : undefined;
227
+ } catch (error) {
228
+ if (error instanceof HubBindingError) {
229
+ throw new HubEnvError(
230
+ `${error.message}; refusing to guess a credential — repair or remove ${hubBindingFile(env)}`,
231
+ );
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Credential for control-plane posts that are not agent sessions (context, degrade).
239
+ * Non-cloud keeps the historical rule: only the explicit `KXM_AUTH_TOKEN`, never hub-env.
240
+ * A cloud binding uses its named source and nothing else.
241
+ */
242
+ export function resolveControlPlaneAuthToken(env: NodeJS.ProcessEnv = process.env): string | undefined {
243
+ const cloud = activeCloudBinding(env);
244
+ if (cloud) return resolveCloudHubToken(cloud, env);
245
+ return env.KXM_AUTH_TOKEN?.trim() || undefined;
246
+ }
247
+
248
+ /**
249
+ * Environment a pi or Claude session started through kxm should inherit.
250
+ * The project id comes from the shared resolver. A cloud binding adds
251
+ * `KXM_AUTH_TOKEN` in this object only — it is never written to the binding.
252
+ */
253
+ export function agentSessionEnv(
254
+ cwd: string,
255
+ env: NodeJS.ProcessEnv = process.env,
256
+ explicitProject?: string,
257
+ ): NodeJS.ProcessEnv {
258
+ const project = defaultProjectName(cwd, env, explicitProject);
259
+ const cloud = activeCloudBinding(env);
260
+ let serverUrl = env.KXM_SERVER_URL?.trim();
261
+ if (!serverUrl) {
262
+ try {
263
+ serverUrl = readHubBinding(env)?.url;
264
+ } catch (error) {
265
+ if (!(error instanceof HubBindingError)) throw error;
266
+ }
267
+ }
268
+ const session: NodeJS.ProcessEnv = { KXM_PROJECT: project };
269
+ if (serverUrl) session.KXM_SERVER_URL = serverUrl.replace(/\/$/, "");
270
+ if (cloud) session.KXM_AUTH_TOKEN = resolveCloudHubToken(cloud, env);
271
+ return session;
272
+ }
273
+
213
274
  export function hasClientHubCredential(env: NodeJS.ProcessEnv = process.env, project?: string): boolean {
214
275
  if (env.KXM_AUTH_TOKEN?.trim()) return true;
215
276
  let record: HubEnvRecord | undefined;
@@ -229,6 +290,8 @@ export function hasClientHubCredential(env: NodeJS.ProcessEnv = process.env, pro
229
290
  }
230
291
 
231
292
  export function resolveClientHubAuthToken(env: NodeJS.ProcessEnv, project: string): string | undefined {
293
+ const cloud = activeCloudBinding(env);
294
+ if (cloud) return resolveCloudHubToken(cloud, env);
232
295
  const envToken = env.KXM_AUTH_TOKEN?.trim();
233
296
  if (envToken) return envToken;
234
297
  const record = readHubEnvRecord(env);
@@ -245,6 +308,10 @@ export function resolveClientHubAuthToken(env: NodeJS.ProcessEnv, project: strin
245
308
  * malformed persisted record, same as `resolveClientHubAuthToken`.
246
309
  */
247
310
  export function resolveAgentHubAuthToken(env: NodeJS.ProcessEnv, project: string): string | undefined {
311
+ const cloud = activeCloudBinding(env);
312
+ // A cloud binding's token is the remote hub's credential. It is not this
313
+ // machine's hub-env admin token, which an agent must still never use.
314
+ if (cloud) return resolveCloudHubToken(cloud, env);
248
315
  const envToken = env.KXM_AUTH_TOKEN?.trim();
249
316
  if (envToken) return envToken;
250
317
  const tokens = readHubEnvRecord(env)?.projectTokens;
@@ -277,7 +344,11 @@ export class AgentProjectTokenMissingError extends Error {
277
344
  * Throws on a malformed persisted record, same as `resolveClientHubAuthToken`.
278
345
  */
279
346
  export function resolveClientAdminAuthToken(env: NodeJS.ProcessEnv = process.env): string | undefined {
347
+ const cloud = activeCloudBinding(env);
348
+ if (cloud) return resolveCloudHubToken(cloud, env);
280
349
  const envToken = env.KXM_AUTH_TOKEN?.trim();
281
350
  if (envToken) return envToken;
282
351
  return readHubEnvRecord(env)?.authToken?.trim() || undefined;
283
352
  }
353
+
354
+ export { CloudTokenError };
@@ -5,13 +5,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { HubClient, HubHttpError } from "./client.ts";
7
7
  import { resolveAgentHubAuthToken } from "./hub-env.ts";
8
+ import { resolveHubServerUrl } from "./hub-binding.ts";
8
9
  import { defaultProjectName } from "./project-name.ts";
9
10
  import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } from "./commands.ts";
10
11
  import { deliverInboxNotification } from "./inbox.ts";
11
12
  import type { HubEvent, MessageRecord } from "./protocol.ts";
12
13
  import { sessionTokenFixHint } from "./session-token-hint.ts";
13
14
 
14
- const VERSION = "0.7.163";
15
+ const VERSION = "0.7.165";
15
16
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
17
  const inbox = new Map<string, MessageRecord>();
17
18
  const notifiedInbox = new Set<string>();
@@ -51,7 +52,7 @@ function sessionIdentity(): { projectDir: string; project: string; serverUrl: st
51
52
  return {
52
53
  projectDir,
53
54
  project: defaultProjectName(projectDir, process.env),
54
- serverUrl: process.env.KXM_SERVER_URL?.trim() || "http://127.0.0.1:7331",
55
+ serverUrl: resolveHubServerUrl(process.env),
55
56
  };
56
57
  }
57
58
 
@@ -1,15 +1,21 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, join } from "node:path";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
3
4
 
4
5
  /** Default project identity for a workspace.
5
6
  *
6
- * Precedence: `KXM_PROJECT` environment, then the workspace `package.json`
7
- * `name`, then the workspace directory name. The package.json read is
8
- * best-effort and never walks upward: a missing, malformed, or name-less
9
- * file falls through to the directory name. */
10
- export function defaultProjectName(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
7
+ * Precedence: an explicit flag, then `KXM_PROJECT`, then the `id` in the
8
+ * nearest `.kxm/project.yaml` walking upward from `cwd`, then the workspace
9
+ * `package.json` `name` (that read never walks upward), then the directory
10
+ * name. A missing, malformed, or id-less project file falls through. A missing,
11
+ * malformed, or name-less package.json falls through to the directory name. */
12
+ export function defaultProjectName(cwd: string, env: NodeJS.ProcessEnv = process.env, explicit?: string): string {
13
+ const fromExplicit = explicit?.trim();
14
+ if (fromExplicit) return fromExplicit;
11
15
  const fromEnv = env.KXM_PROJECT?.trim();
12
16
  if (fromEnv) return fromEnv;
17
+ const fromProject = readProjectYamlId(cwd);
18
+ if (fromProject) return fromProject;
13
19
  try {
14
20
  const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")) as { name?: unknown };
15
21
  if (typeof pkg.name === "string" && pkg.name.trim().length > 0) return pkg.name.trim();
@@ -18,3 +24,24 @@ export function defaultProjectName(cwd: string, env: NodeJS.ProcessEnv = process
18
24
  }
19
25
  return basename(cwd);
20
26
  }
27
+
28
+ function readProjectYamlId(start: string): string | undefined {
29
+ let dir = resolve(start);
30
+ for (;;) {
31
+ const file = join(dir, ".kxm", "project.yaml");
32
+ if (existsSync(file)) {
33
+ try {
34
+ const doc = parseYaml(readFileSync(file, "utf8")) as { id?: unknown } | null;
35
+ if (doc && typeof doc === "object" && !Array.isArray(doc) && typeof doc.id === "string" && doc.id.trim()) {
36
+ return doc.id.trim();
37
+ }
38
+ } catch {
39
+ // Malformed YAML falls through to package.json of the original directory.
40
+ }
41
+ return undefined;
42
+ }
43
+ const parent = dirname(dir);
44
+ if (parent === dir) return undefined;
45
+ dir = parent;
46
+ }
47
+ }
@@ -31,7 +31,7 @@ import { createKxmOneShotProducer } from "./oneshot-producer.ts";
31
31
  import { KxmRunScheduler, createKxmSimulatedProducer, recordDriveReceipt, recoverKxmRun, kxmDrivePollProjection } from "./engine.ts";
32
32
  import { kxmDriveSession, kxmOpenDriveSessions } from "./runtime-owner.ts";
33
33
  import { RuntimeHubClient, HubHttpError, type SyncPushResponse } from "./client.ts";
34
- import { readHubBinding } from "./hub-binding.ts";
34
+ import { CloudTokenError, readHubBinding } from "./hub-binding.ts";
35
35
  import { resolveClientHubAuthToken } from "./hub-env.ts";
36
36
  import { createLogger } from "./logger.ts";
37
37
  import type { KxmOutboxRow, KxmOutboxStatus } from "./runtime-store.ts";
@@ -709,8 +709,14 @@ async function startKxmRuntimeSupervisorInner(
709
709
  // BEFORE any event can be appended — so the very first outbox row is
710
710
  // already scrubbed. Registering on the sync tick leaves a window where
711
711
  // appended events retain credentials.
712
- const hubToken = resolveClientHubAuthToken(env, context.projectId);
713
- if (hubToken) context.eventStore.syncRedactor.register(hubToken);
712
+ try {
713
+ const hubToken = resolveClientHubAuthToken(env, context.projectId);
714
+ if (hubToken) context.eventStore.syncRedactor.register(hubToken);
715
+ } catch (error) {
716
+ // A missing cloud token must not stop the local Runtime from opening.
717
+ // The sync tick records the same failure as blocked.
718
+ if (!(error instanceof CloudTokenError)) throw error;
719
+ }
714
720
  for (const key of Object.keys(env)) {
715
721
  if ((key.startsWith("KXM_") && (key.endsWith("_TOKEN") || key.endsWith("_KEY"))) || key.endsWith("_API_KEY") || key.endsWith("_SECRET")) {
716
722
  const value = env[key]?.trim();
@@ -1,4 +1,4 @@
1
- import { hubBindingScope } from "./hub-binding.ts";
1
+ import { CloudTokenError, hubBindingScope, type HubBindingScope } from "./hub-binding.ts";
2
2
 
3
3
  export const TENANT_STATUS_SCHEMA = "kxm.tenant-status.v1" as const;
4
4
 
@@ -163,6 +163,8 @@ export async function assembleTenantStatus(input: {
163
163
  /** Resolves the admin credential. May throw on a malformed persisted record; that
164
164
  * failure belongs to the hub source alone and never aborts the Runtime read. */
165
165
  resolveAdminToken: () => string | undefined;
166
+ /** When set, this is the scope already classified for `hubUrl` (cloud forwards included). */
167
+ bindingScope?: HubBindingScope | undefined;
166
168
  fetchImpl: typeof fetch;
167
169
  runtime: TenantStatusRuntimeReader;
168
170
  hubTimeoutMs?: number | undefined;
@@ -180,6 +182,11 @@ export async function assembleTenantStatus(input: {
180
182
  } catch (error) {
181
183
  // A malformed hub-env record is a configuration failure with a specific repair.
182
184
  // It belongs to this source; the Runtime read proceeds regardless.
185
+ // A cloud binding that cannot produce a token is not that failure: the local
186
+ // hub-env token was not consulted.
187
+ if (error instanceof CloudTokenError) {
188
+ return { state: "unavailable", observedAt: attemptedAt, reason: error.code };
189
+ }
183
190
  void error;
184
191
  return { state: "unavailable", observedAt: attemptedAt, reason: "hub_credential_unreadable" };
185
192
  }
@@ -331,7 +338,7 @@ export async function assembleTenantStatus(input: {
331
338
  project: input.project,
332
339
  generatedAt: at(),
333
340
  hubUrl: input.hubUrl,
334
- bindingScope: hubBindingScope(input.hubUrl),
341
+ bindingScope: input.bindingScope ?? hubBindingScope(input.hubUrl),
335
342
  hub,
336
343
  runtime,
337
344
  runComparison,
@@ -47,9 +47,14 @@ for (const bundlePath of [
47
47
  "plugins/kxm/dist/server.js",
48
48
  "plugins/kxm/dist/runtime-supervisor.js",
49
49
  "plugins/kxm/dist/claude-hook.js",
50
+ // The MCP bundle is produced by build:mcp, then this script runs. It now
51
+ // imports the project-id resolver, which pulls in the YAML package. That
52
+ // package's dynamic require needs the same createRequire shim.
53
+ "plugins/kxm/dist/mcp-server.js",
50
54
  ]) {
51
55
  const bundled = readFileSync(bundlePath, "utf8");
52
56
  if (!bundled.startsWith(shebang)) throw new Error(`runtime bundle is missing its executable shebang: ${bundlePath}`);
57
+ if (bundled.includes("__kxmCreateRequire")) continue;
53
58
  writeFileSync(bundlePath, `${shebang}${createRequire}${bundled.slice(shebang.length)}`);
54
59
  }
55
60