@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
|
@@ -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,22 @@ 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 { loadHubSettings } from "./config.ts";
|
|
6
|
+
import {
|
|
7
|
+
describeHubConnection,
|
|
8
|
+
projectTokenMissingMessage,
|
|
9
|
+
resolveKeyReference,
|
|
10
|
+
type OpReader,
|
|
11
|
+
type ProjectIdentity,
|
|
12
|
+
} from "./hub-identity.ts";
|
|
13
|
+
import {
|
|
14
|
+
CloudTokenError,
|
|
15
|
+
HubBindingError,
|
|
16
|
+
hubBindingFile,
|
|
17
|
+
readHubBinding,
|
|
18
|
+
resolveCloudHubToken,
|
|
19
|
+
type HubBindingRecord,
|
|
20
|
+
} from "./hub-binding.ts";
|
|
5
21
|
|
|
6
22
|
export const HUB_ENV_SCHEMA = "kxm.hub-env.v1" as const;
|
|
7
23
|
|
|
@@ -210,8 +226,81 @@ export function resolveHubCredentials(options: ResolveHubCredentialsOptions = {}
|
|
|
210
226
|
* only another project's token cannot authorise this one, and a guard that counts it as a
|
|
211
227
|
* credential stores a binding that will fail exactly like the one it prevented.
|
|
212
228
|
*/
|
|
213
|
-
|
|
229
|
+
/** A cloud binding, when this machine has one. A malformed binding fails closed. */
|
|
230
|
+
function activeCloudBinding(env: NodeJS.ProcessEnv): HubBindingRecord | undefined {
|
|
231
|
+
try {
|
|
232
|
+
const binding = readHubBinding(env);
|
|
233
|
+
return binding?.cloud ? binding : undefined;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof HubBindingError) {
|
|
236
|
+
throw new HubEnvError(
|
|
237
|
+
`${error.message}; refusing to guess a credential — repair or remove ${hubBindingFile(env)}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Credential for control-plane posts that are not agent sessions (context, degrade).
|
|
246
|
+
* Non-cloud keeps the historical rule: only the explicit `KXM_AUTH_TOKEN`, never hub-env.
|
|
247
|
+
* A cloud binding uses its named source and nothing else.
|
|
248
|
+
*/
|
|
249
|
+
export function resolveControlPlaneAuthToken(
|
|
250
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
251
|
+
cwd: string = process.cwd(),
|
|
252
|
+
opRead?: OpReader,
|
|
253
|
+
): string | undefined {
|
|
254
|
+
const cloud = cloudModeToken(cwd, env, undefined, opRead);
|
|
255
|
+
if (cloud) return cloud;
|
|
256
|
+
return env.KXM_AUTH_TOKEN?.trim() || undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Environment a pi or Claude session started through kxm should inherit.
|
|
261
|
+
* The project id comes from the shared resolver. A cloud binding adds
|
|
262
|
+
* `KXM_AUTH_TOKEN` in this object only — it is never written to the binding.
|
|
263
|
+
*/
|
|
264
|
+
function cloudModeToken(
|
|
265
|
+
cwd: string,
|
|
266
|
+
env: NodeJS.ProcessEnv,
|
|
267
|
+
explicitProject?: string,
|
|
268
|
+
opRead?: OpReader,
|
|
269
|
+
): string | undefined {
|
|
270
|
+
const connection = describeHubConnection(cwd, env, explicitProject === undefined ? {} : { explicitProject });
|
|
271
|
+
if (connection.mode !== "cloud") return undefined;
|
|
272
|
+
const fromConfig = resolveKeyReference(connection.endpoint?.key, env, opRead);
|
|
273
|
+
if (fromConfig) return fromConfig.value;
|
|
274
|
+
if (connection.binding?.cloud) return resolveCloudHubToken(connection.binding, env);
|
|
275
|
+
throw new CloudTokenError(
|
|
276
|
+
"cloud_token_missing",
|
|
277
|
+
"cloud hub token is missing; the local hub-env token was not used",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function agentSessionEnv(
|
|
282
|
+
cwd: string,
|
|
283
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
284
|
+
explicitProject?: string,
|
|
285
|
+
opRead?: OpReader,
|
|
286
|
+
): NodeJS.ProcessEnv {
|
|
287
|
+
const connection = describeHubConnection(cwd, env, explicitProject === undefined ? {} : { explicitProject });
|
|
288
|
+
const session: NodeJS.ProcessEnv = { KXM_PROJECT: connection.project };
|
|
289
|
+
if (connection.url) session.KXM_SERVER_URL = connection.url.replace(/\/$/, "");
|
|
290
|
+
if (connection.mode === "cloud") {
|
|
291
|
+
const token = cloudModeToken(cwd, env, explicitProject, opRead);
|
|
292
|
+
if (token) session.KXM_AUTH_TOKEN = token;
|
|
293
|
+
}
|
|
294
|
+
return session;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function hasClientHubCredential(env: NodeJS.ProcessEnv = process.env, project?: string, cwd: string = process.cwd()): boolean {
|
|
214
298
|
if (env.KXM_AUTH_TOKEN?.trim()) return true;
|
|
299
|
+
const settings = loadHubSettings(cwd, env);
|
|
300
|
+
const mode = settings.mode ?? (activeCloudBinding(env) ? "cloud" : "local");
|
|
301
|
+
const key = (mode === "cloud" ? settings.cloud : settings.local)?.key;
|
|
302
|
+
if (key?.op) return true;
|
|
303
|
+
if (key?.env && env[key.env]?.trim()) return true;
|
|
215
304
|
let record: HubEnvRecord | undefined;
|
|
216
305
|
try {
|
|
217
306
|
record = readHubEnvRecord(env);
|
|
@@ -228,9 +317,18 @@ export function hasClientHubCredential(env: NodeJS.ProcessEnv = process.env, pro
|
|
|
228
317
|
return Object.values(tokens).some((token) => typeof token === "string" && token.trim().length > 0);
|
|
229
318
|
}
|
|
230
319
|
|
|
231
|
-
export function resolveClientHubAuthToken(
|
|
320
|
+
export function resolveClientHubAuthToken(
|
|
321
|
+
env: NodeJS.ProcessEnv,
|
|
322
|
+
project: string,
|
|
323
|
+
cwd: string = process.cwd(),
|
|
324
|
+
opRead?: OpReader,
|
|
325
|
+
): string | undefined {
|
|
326
|
+
const cloud = cloudModeToken(cwd, env, undefined, opRead);
|
|
327
|
+
if (cloud) return cloud;
|
|
232
328
|
const envToken = env.KXM_AUTH_TOKEN?.trim();
|
|
233
329
|
if (envToken) return envToken;
|
|
330
|
+
const fromConfig = resolveKeyReference(describeHubConnection(cwd, env).endpoint?.key, env, opRead);
|
|
331
|
+
if (fromConfig) return fromConfig.value;
|
|
234
332
|
const record = readHubEnvRecord(env);
|
|
235
333
|
return record?.projectTokens?.[project]?.trim() || record?.authToken?.trim() || undefined;
|
|
236
334
|
}
|
|
@@ -244,9 +342,20 @@ export function resolveClientHubAuthToken(env: NodeJS.ProcessEnv, project: strin
|
|
|
244
342
|
* token for. Operator tools keep `resolveClientHubAuthToken`. Throws HubEnvError on a
|
|
245
343
|
* malformed persisted record, same as `resolveClientHubAuthToken`.
|
|
246
344
|
*/
|
|
247
|
-
export function resolveAgentHubAuthToken(
|
|
345
|
+
export function resolveAgentHubAuthToken(
|
|
346
|
+
env: NodeJS.ProcessEnv,
|
|
347
|
+
project: string,
|
|
348
|
+
cwd: string = process.cwd(),
|
|
349
|
+
opRead?: OpReader,
|
|
350
|
+
): string | undefined {
|
|
351
|
+
const cloud = cloudModeToken(cwd, env, undefined, opRead);
|
|
352
|
+
// A cloud binding's token is the remote hub's credential. It is not this
|
|
353
|
+
// machine's hub-env admin token, which an agent must still never use.
|
|
354
|
+
if (cloud) return cloud;
|
|
248
355
|
const envToken = env.KXM_AUTH_TOKEN?.trim();
|
|
249
356
|
if (envToken) return envToken;
|
|
357
|
+
const fromConfig = resolveKeyReference(describeHubConnection(cwd, env).endpoint?.key, env, opRead);
|
|
358
|
+
if (fromConfig) return fromConfig.value;
|
|
250
359
|
const tokens = readHubEnvRecord(env)?.projectTokens;
|
|
251
360
|
if (!tokens || !Object.hasOwn(tokens, project)) return undefined;
|
|
252
361
|
return tokens[project]?.trim() || undefined;
|
|
@@ -259,13 +368,19 @@ export function resolveAgentHubAuthToken(env: NodeJS.ProcessEnv, project: string
|
|
|
259
368
|
export class AgentProjectTokenMissingError extends Error {
|
|
260
369
|
readonly code = "project_token_missing";
|
|
261
370
|
readonly project: string;
|
|
371
|
+
readonly projectSource: ProjectIdentity["source"] | "unknown";
|
|
372
|
+
readonly hint: string;
|
|
262
373
|
|
|
263
|
-
constructor(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
374
|
+
constructor(projectOrIdentity: string | ProjectIdentity) {
|
|
375
|
+
const identity: ProjectIdentity = typeof projectOrIdentity === "string"
|
|
376
|
+
? { project: projectOrIdentity, source: "env", sourceLabel: "the project id sent to the hub" }
|
|
377
|
+
: projectOrIdentity;
|
|
378
|
+
const message = projectTokenMissingMessage(identity);
|
|
379
|
+
super(message);
|
|
267
380
|
this.name = "AgentProjectTokenMissingError";
|
|
268
|
-
this.project = project;
|
|
381
|
+
this.project = identity.project;
|
|
382
|
+
this.projectSource = typeof projectOrIdentity === "string" ? "unknown" : identity.source;
|
|
383
|
+
this.hint = message;
|
|
269
384
|
}
|
|
270
385
|
}
|
|
271
386
|
|
|
@@ -276,8 +391,16 @@ export class AgentProjectTokenMissingError extends Error {
|
|
|
276
391
|
* refuse — handing the portal a 401 dressed as "configured" is worse than handing it none.
|
|
277
392
|
* Throws on a malformed persisted record, same as `resolveClientHubAuthToken`.
|
|
278
393
|
*/
|
|
279
|
-
export function resolveClientAdminAuthToken(
|
|
394
|
+
export function resolveClientAdminAuthToken(
|
|
395
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
396
|
+
cwd: string = process.cwd(),
|
|
397
|
+
opRead?: OpReader,
|
|
398
|
+
): string | undefined {
|
|
399
|
+
const cloud = cloudModeToken(cwd, env, undefined, opRead);
|
|
400
|
+
if (cloud) return cloud;
|
|
280
401
|
const envToken = env.KXM_AUTH_TOKEN?.trim();
|
|
281
402
|
if (envToken) return envToken;
|
|
282
403
|
return readHubEnvRecord(env)?.authToken?.trim() || undefined;
|
|
283
404
|
}
|
|
405
|
+
|
|
406
|
+
export { CloudTokenError };
|