@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
|
@@ -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.
|
|
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.
|
|
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
|
|
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:
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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
|
|