@monster-orange/den 0.4.25 → 0.4.27
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/CHANGELOG.md +55 -0
- package/docs/distributions.md +74 -20
- package/package.json +1 -1
- package/src/shared/agents/runtime.ts +2 -2
- package/src/shared/hub.ts +304 -82
- package/src/shared/secrets/onepassword.ts +135 -20
- package/src/ui/cli/agent-ops.ts +17 -3
- package/src/ui/cli/doctor-command.ts +11 -2
- package/src/ui/server/agent-routes.ts +79 -1
- package/src/ui/server/hub-connect.ts +433 -0
- package/src/ui/server/public/i18n/es.js +2 -0
- package/src/ui/server/public/i18n/ja.js +2 -0
- package/src/ui/server/public/settings-registry.js +196 -33
- package/src/ui/server/public/settings.html +23 -0
- package/src/ui/server/public/shell.js +1 -0
- package/src/ui/server/public/topbar.js +5 -1
- package/src/ui/server/route-classes.ts +17 -0
package/src/shared/hub.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* workspace's name, then the team's default). The token is never logged,
|
|
20
20
|
* never in a page or an API answer: only in the agent's own 0600 MCP file.
|
|
21
21
|
*/
|
|
22
|
+
import * as crypto from "crypto";
|
|
22
23
|
import { readDenConfig, readUserConfig, type DenConfig } from "./den-config.js";
|
|
23
24
|
import {
|
|
24
25
|
getSecretsProvider,
|
|
@@ -28,6 +29,12 @@ import {
|
|
|
28
29
|
type SecretsConfig,
|
|
29
30
|
type SecretsProvider,
|
|
30
31
|
} from "./secrets/index.js";
|
|
32
|
+
import {
|
|
33
|
+
describeOpTokenSource,
|
|
34
|
+
parseOpTokenSource,
|
|
35
|
+
readOpToken,
|
|
36
|
+
type OpTokenSource,
|
|
37
|
+
} from "./secrets/op-token.js";
|
|
31
38
|
|
|
32
39
|
/** The server's name in an agent's MCP config. */
|
|
33
40
|
export const HUB_SERVER = "hub";
|
|
@@ -41,8 +48,16 @@ export interface HubConfig {
|
|
|
41
48
|
url: string;
|
|
42
49
|
/** The token's ref in this install's store, or null when none is set. */
|
|
43
50
|
tokenRef: SecretRef | null;
|
|
51
|
+
/**
|
|
52
|
+
* HUB-1.7: or a token source, `file:<path>` (mode 600), `keychain:<service>`
|
|
53
|
+
* or `env:<VAR>`: where Connect keeps a token when the install's store
|
|
54
|
+
* can't, or a distribution's own choice.
|
|
55
|
+
*/
|
|
56
|
+
tokenSource?: OpTokenSource;
|
|
44
57
|
/** Where the ref came from, for messages (never the value). */
|
|
45
58
|
tokenFrom: string;
|
|
59
|
+
/** HUB-1.7: Hub's MCP endpoint, as Hub gave it (default `<url>/mcp`). */
|
|
60
|
+
mcpUrl: string;
|
|
46
61
|
/**
|
|
47
62
|
* hub.allow_tools (default true): agents use Hub's tools without asking.
|
|
48
63
|
* Off, they ask like any tool. An install's own deny rule still wins.
|
|
@@ -51,7 +66,12 @@ export interface HubConfig {
|
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
type RawHub =
|
|
54
|
-
| {
|
|
69
|
+
| {
|
|
70
|
+
url?: unknown;
|
|
71
|
+
token?: unknown;
|
|
72
|
+
allow_tools?: unknown;
|
|
73
|
+
mcp_url?: unknown;
|
|
74
|
+
}
|
|
55
75
|
| undefined;
|
|
56
76
|
|
|
57
77
|
function rawHub(den: unknown, user: unknown): RawHub {
|
|
@@ -94,7 +114,11 @@ export function readHubConfig(
|
|
|
94
114
|
const t = h.token;
|
|
95
115
|
let tokenRef: SecretRef | null = null;
|
|
96
116
|
let tokenFrom = "hub.token";
|
|
97
|
-
|
|
117
|
+
let tokenSource: OpTokenSource | undefined;
|
|
118
|
+
if (typeof t === "string" && /^(keychain|file|env):/.test(t.trim())) {
|
|
119
|
+
tokenSource = parseOpTokenSource(t.trim(), "hub.token");
|
|
120
|
+
tokenFrom = `hub.token (${describeOpTokenSource(tokenSource)})`;
|
|
121
|
+
} else if (typeof t === "string" && t.trim()) {
|
|
98
122
|
// The name of a secrets.refs entry.
|
|
99
123
|
tokenFrom = `secrets.refs.${t.trim()}`;
|
|
100
124
|
tokenRef = namedSecretRef(t.trim(), secrets());
|
|
@@ -110,12 +134,28 @@ export function readHubConfig(
|
|
|
110
134
|
tokenRef = ref.item || ref.key || ref.field ? ref : null;
|
|
111
135
|
} else if (t !== undefined && t !== null)
|
|
112
136
|
throw new Error(
|
|
113
|
-
"hub.token must be a secret ref ({ item, field } or { key })
|
|
137
|
+
"hub.token must be a secret ref ({ item, field } or { key }), the name of a secrets.refs entry, or file:<path> / keychain:<service> / env:<VAR>",
|
|
114
138
|
);
|
|
115
139
|
const at = h.allow_tools;
|
|
116
140
|
if (at !== undefined && at !== null && typeof at !== "boolean")
|
|
117
141
|
throw new Error("hub.allow_tools must be true or false");
|
|
118
|
-
|
|
142
|
+
let mcpUrl = `${url}/mcp`;
|
|
143
|
+
if (h.mcp_url !== undefined && h.mcp_url !== null && h.mcp_url !== "") {
|
|
144
|
+
if (typeof h.mcp_url !== "string")
|
|
145
|
+
throw new Error("hub.mcp_url must be a string");
|
|
146
|
+
const m = h.mcp_url.trim();
|
|
147
|
+
const badMcp = hubUrlProblem(m);
|
|
148
|
+
if (badMcp) throw new Error(badMcp.replace("hub.url", "hub.mcp_url"));
|
|
149
|
+
mcpUrl = m;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
url,
|
|
153
|
+
tokenRef,
|
|
154
|
+
...(tokenSource ? { tokenSource } : {}),
|
|
155
|
+
tokenFrom,
|
|
156
|
+
mcpUrl,
|
|
157
|
+
allowTools: at !== false,
|
|
158
|
+
};
|
|
119
159
|
}
|
|
120
160
|
|
|
121
161
|
/**
|
|
@@ -141,6 +181,10 @@ export async function readHubToken(
|
|
|
141
181
|
cfg: HubConfig,
|
|
142
182
|
provider: () => SecretsProvider = getSecretsProvider,
|
|
143
183
|
): Promise<string | null> {
|
|
184
|
+
if (cfg.tokenSource) {
|
|
185
|
+
const r = await readOpToken(cfg.tokenSource).catch(() => null);
|
|
186
|
+
return r?.ok && r.token.trim() ? r.token.trim() : null;
|
|
187
|
+
}
|
|
144
188
|
if (!cfg.tokenRef) return null;
|
|
145
189
|
try {
|
|
146
190
|
const v = await provider().tryGet(cfg.tokenRef);
|
|
@@ -152,7 +196,10 @@ export async function readHubToken(
|
|
|
152
196
|
|
|
153
197
|
/** Who calls, as Hub reads it: plain names, never a secret. */
|
|
154
198
|
export interface HubCaller {
|
|
199
|
+
/** The display name: Hub shows the latest one. */
|
|
155
200
|
agent: string;
|
|
201
|
+
/** HUB-1.7: the agent's stable id: Hub keys attribution on it. */
|
|
202
|
+
agentId?: string;
|
|
156
203
|
session: string;
|
|
157
204
|
workspace: string;
|
|
158
205
|
repos: string[];
|
|
@@ -168,6 +215,7 @@ export function hubHeaders(
|
|
|
168
215
|
return {
|
|
169
216
|
Authorization: `Bearer ${token}`,
|
|
170
217
|
"X-Den-Agent": headerSafe(who.agent),
|
|
218
|
+
...(who.agentId ? { "X-Den-Agent-Id": headerSafe(who.agentId) } : {}),
|
|
171
219
|
"X-Den-Session": headerSafe(who.session),
|
|
172
220
|
"X-Den-Workspace": headerSafe(who.workspace),
|
|
173
221
|
"X-Den-Repos": headerSafe(who.repos.join(",")),
|
|
@@ -185,97 +233,252 @@ export function ownerRepo(
|
|
|
185
233
|
return m ? `${m[1]}/${m[2]}` : null;
|
|
186
234
|
}
|
|
187
235
|
|
|
236
|
+
/** HUB-1.7: who this install is, as Hub's GET /connect/whoami says. */
|
|
188
237
|
export interface HubWhoami {
|
|
189
|
-
|
|
190
|
-
|
|
238
|
+
/** The install's label (Hub's `install`). */
|
|
239
|
+
install?: string;
|
|
240
|
+
role?: string;
|
|
241
|
+
/** The person who connected it. */
|
|
242
|
+
login?: string;
|
|
243
|
+
company?: { slug?: string; name?: string; url?: string };
|
|
244
|
+
/** Hub's MCP endpoint; opaque. */
|
|
245
|
+
mcpUrl?: string;
|
|
191
246
|
}
|
|
192
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Why a call to Hub failed: `rejected` (401: the token isn't accepted:
|
|
250
|
+
* connect again), `forbidden` (403: Hub's own words; ask an admin, never
|
|
251
|
+
* retry), `refused` (another 4xx with Hub's error code), `unreachable`.
|
|
252
|
+
* The message never carries the token.
|
|
253
|
+
*/
|
|
254
|
+
export class HubCallError extends Error {
|
|
255
|
+
constructor(
|
|
256
|
+
readonly kind: "rejected" | "forbidden" | "refused" | "unreachable",
|
|
257
|
+
message: string,
|
|
258
|
+
readonly code?: string,
|
|
259
|
+
) {
|
|
260
|
+
super(message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type HubState =
|
|
265
|
+
| "connected"
|
|
266
|
+
| "no_token"
|
|
267
|
+
| "rejected"
|
|
268
|
+
| "forbidden"
|
|
269
|
+
| "unreachable"
|
|
270
|
+
| "bad_config";
|
|
271
|
+
|
|
193
272
|
export type HubStatus =
|
|
194
273
|
| { configured: false }
|
|
195
274
|
| {
|
|
196
275
|
configured: true;
|
|
197
276
|
url: string;
|
|
198
277
|
connected: boolean;
|
|
278
|
+
/** HUB-1.7: what Settings shows and offers (Connect, Reconnect, …). */
|
|
279
|
+
state: HubState;
|
|
199
280
|
/** hub.allow_tools: agents use Hub's tools without asking. */
|
|
200
281
|
allowTools?: boolean;
|
|
282
|
+
/** The company's name (Hub's company.name). */
|
|
201
283
|
team?: string;
|
|
284
|
+
/** Its page on Hub, for the deep link. */
|
|
285
|
+
teamUrl?: string;
|
|
286
|
+
/** This install's label on Hub. */
|
|
202
287
|
label?: string;
|
|
288
|
+
role?: string;
|
|
289
|
+
login?: string;
|
|
203
290
|
/** Why it isn't connected, in words (never the token). */
|
|
204
291
|
problem?: string;
|
|
205
292
|
};
|
|
206
293
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
type FetchFn = typeof fetch;
|
|
295
|
+
const str = (v: unknown) => (typeof v === "string" && v ? v : undefined);
|
|
296
|
+
|
|
297
|
+
/** One JSON call to Hub, bounded; its answer, or a HubCallError. */
|
|
298
|
+
async function hubCall(
|
|
299
|
+
url: string,
|
|
300
|
+
init: RequestInit,
|
|
301
|
+
opts: { timeoutMs?: number; fetch?: FetchFn },
|
|
302
|
+
): Promise<Record<string, unknown>> {
|
|
303
|
+
const timeout = opts.timeoutMs ?? 5000;
|
|
304
|
+
let res: Response;
|
|
305
|
+
try {
|
|
306
|
+
res = await (opts.fetch ?? fetch)(url, {
|
|
307
|
+
...init,
|
|
308
|
+
redirect: "error",
|
|
309
|
+
signal: AbortSignal.timeout(timeout),
|
|
310
|
+
});
|
|
311
|
+
} catch (e) {
|
|
312
|
+
const name = (e as Error)?.name;
|
|
313
|
+
throw new HubCallError(
|
|
314
|
+
"unreachable",
|
|
315
|
+
name === "TimeoutError" || name === "AbortError"
|
|
316
|
+
? `Hub did not answer in ${timeout} ms`
|
|
317
|
+
: `Hub could not be reached: ${(e as Error)?.message ?? e}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
let body: Record<string, unknown> = {};
|
|
321
|
+
try {
|
|
322
|
+
const v = (await res.json()) as unknown;
|
|
323
|
+
if (v && typeof v === "object") body = v as Record<string, unknown>;
|
|
324
|
+
} catch {
|
|
325
|
+
/* not JSON */
|
|
326
|
+
}
|
|
327
|
+
if (res.ok) return body;
|
|
328
|
+
const said =
|
|
329
|
+
str(body.error_description) ?? str(body.message) ?? str(body.error);
|
|
330
|
+
if (res.status === 401)
|
|
331
|
+
throw new HubCallError(
|
|
332
|
+
"rejected",
|
|
333
|
+
"Hub doesn't accept this install's token (revoked, or replaced by a newer connect): connect again",
|
|
334
|
+
str(body.error),
|
|
335
|
+
);
|
|
336
|
+
if (res.status === 403)
|
|
337
|
+
throw new HubCallError(
|
|
338
|
+
"forbidden",
|
|
339
|
+
`${said ?? "Hub refused this install"}: ask an admin of the company on Hub`,
|
|
340
|
+
str(body.error),
|
|
341
|
+
);
|
|
342
|
+
if (res.status >= 400 && res.status < 500)
|
|
343
|
+
throw new HubCallError(
|
|
344
|
+
"refused",
|
|
345
|
+
`Hub refused it (${res.status}${said ? `: ${said}` : ""})`,
|
|
346
|
+
str(body.error),
|
|
347
|
+
);
|
|
348
|
+
throw new HubCallError(
|
|
349
|
+
"unreachable",
|
|
350
|
+
`Hub answered ${res.status}${said ? `: ${said}` : ""}`,
|
|
351
|
+
);
|
|
224
352
|
}
|
|
225
353
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
* `timeoutMs`; a failure is said in words, without the token.
|
|
230
|
-
*/
|
|
354
|
+
const bearer = (token: string) => ({ Authorization: `Bearer ${token}` });
|
|
355
|
+
|
|
356
|
+
/** Ask Hub who this install is: GET <url>/connect/whoami with the token. */
|
|
231
357
|
export async function hubWhoami(
|
|
232
358
|
url: string,
|
|
233
359
|
token: string,
|
|
234
|
-
opts: { timeoutMs?: number; fetch?:
|
|
360
|
+
opts: { timeoutMs?: number; fetch?: FetchFn } = {},
|
|
235
361
|
): Promise<HubWhoami> {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
362
|
+
const b = await hubCall(
|
|
363
|
+
`${url}/connect/whoami`,
|
|
364
|
+
{
|
|
365
|
+
method: "GET",
|
|
366
|
+
headers: { ...bearer(token), Accept: "application/json" },
|
|
367
|
+
},
|
|
368
|
+
opts,
|
|
369
|
+
);
|
|
370
|
+
const c = (b.company ?? {}) as Record<string, unknown>;
|
|
371
|
+
const company = {
|
|
372
|
+
...(str(c.slug) ? { slug: str(c.slug)! } : {}),
|
|
373
|
+
...(str(c.name) ? { name: str(c.name)! } : {}),
|
|
374
|
+
...(str(c.url) ? { url: str(c.url)! } : {}),
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
...(str(b.install) ? { install: str(b.install)! } : {}),
|
|
378
|
+
...(str(b.role) ? { role: str(b.role)! } : {}),
|
|
379
|
+
...(str(b.login) ? { login: str(b.login)! } : {}),
|
|
380
|
+
...(Object.keys(company).length ? { company } : {}),
|
|
381
|
+
...(str(b.mcp_url) ? { mcpUrl: str(b.mcp_url)! } : {}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** What POST /connect/token gives: the token, and where Hub is. */
|
|
386
|
+
export interface HubGrant {
|
|
387
|
+
token: string;
|
|
388
|
+
install?: string;
|
|
389
|
+
role?: string;
|
|
390
|
+
company?: { slug?: string; name?: string; url?: string };
|
|
391
|
+
/** Hub's canonical origin: kept as hub.url. */
|
|
392
|
+
hubUrl?: string;
|
|
393
|
+
mcpUrl?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Trade the one-time code for the install's token, server to server. */
|
|
397
|
+
export async function hubExchange(
|
|
398
|
+
url: string,
|
|
399
|
+
grant: { code: string; redirectUri: string; verifier: string },
|
|
400
|
+
opts: { timeoutMs?: number; fetch?: FetchFn } = {},
|
|
401
|
+
): Promise<HubGrant> {
|
|
402
|
+
const b = await hubCall(
|
|
403
|
+
`${url}/connect/token`,
|
|
404
|
+
{
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
Accept: "application/json",
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
grant_type: "authorization_code",
|
|
412
|
+
code: grant.code,
|
|
413
|
+
redirect_uri: grant.redirectUri,
|
|
414
|
+
code_verifier: grant.verifier,
|
|
255
415
|
}),
|
|
256
|
-
|
|
416
|
+
},
|
|
417
|
+
{ timeoutMs: 15_000, ...opts },
|
|
418
|
+
);
|
|
419
|
+
const token = str(b.token);
|
|
420
|
+
if (!token || !/^\S{8,}$/.test(token))
|
|
421
|
+
throw new HubCallError("refused", "Hub's answer had no token");
|
|
422
|
+
const c = (b.company ?? {}) as Record<string, unknown>;
|
|
423
|
+
return {
|
|
424
|
+
token,
|
|
425
|
+
...(str(b.install) ? { install: str(b.install)! } : {}),
|
|
426
|
+
...(str(b.role) ? { role: str(b.role)! } : {}),
|
|
427
|
+
company: {
|
|
428
|
+
...(str(c.slug) ? { slug: str(c.slug)! } : {}),
|
|
429
|
+
...(str(c.name) ? { name: str(c.name)! } : {}),
|
|
430
|
+
...(str(c.url) ? { url: str(c.url)! } : {}),
|
|
431
|
+
},
|
|
432
|
+
...(str(b.hub_url) ? { hubUrl: str(b.hub_url)!.replace(/\/+$/, "") } : {}),
|
|
433
|
+
...(str(b.mcp_url) ? { mcpUrl: str(b.mcp_url)! } : {}),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Revoke the install's token on Hub: POST <url>/connect/revoke with it.
|
|
439
|
+
* True when Hub revoked it now, false when it was already dead (401).
|
|
440
|
+
*/
|
|
441
|
+
export async function hubRevoke(
|
|
442
|
+
url: string,
|
|
443
|
+
token: string,
|
|
444
|
+
opts: { timeoutMs?: number; fetch?: FetchFn } = {},
|
|
445
|
+
): Promise<boolean> {
|
|
257
446
|
try {
|
|
258
|
-
await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
447
|
+
await hubCall(
|
|
448
|
+
`${url}/connect/revoke`,
|
|
449
|
+
{
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: { ...bearer(token), Accept: "application/json" },
|
|
452
|
+
},
|
|
453
|
+
opts,
|
|
264
454
|
);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
: {}),
|
|
270
|
-
...(str(r?.label ?? r?.install ?? r?.install_label)
|
|
271
|
-
? { label: str(r?.label ?? r?.install ?? r?.install_label)! }
|
|
272
|
-
: {}),
|
|
273
|
-
};
|
|
274
|
-
} finally {
|
|
275
|
-
await client.close().catch(() => undefined);
|
|
455
|
+
return true;
|
|
456
|
+
} catch (e) {
|
|
457
|
+
if (e instanceof HubCallError && e.kind === "rejected") return false;
|
|
458
|
+
throw e;
|
|
276
459
|
}
|
|
277
460
|
}
|
|
278
461
|
|
|
462
|
+
/** base64url without padding. */
|
|
463
|
+
const b64url = (b: Buffer) =>
|
|
464
|
+
b
|
|
465
|
+
.toString("base64")
|
|
466
|
+
.replace(/\+/g, "-")
|
|
467
|
+
.replace(/\//g, "_")
|
|
468
|
+
.replace(/=+$/, "");
|
|
469
|
+
|
|
470
|
+
/** PKCE (S256): a fresh verifier and its challenge. */
|
|
471
|
+
export function newPkce(): { verifier: string; challenge: string } {
|
|
472
|
+
const verifier = b64url(crypto.randomBytes(32));
|
|
473
|
+
const challenge = b64url(
|
|
474
|
+
crypto.createHash("sha256").update(verifier).digest(),
|
|
475
|
+
);
|
|
476
|
+
return { verifier, challenge };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** An opaque, single-use state value. */
|
|
480
|
+
export const newHubState = () => b64url(crypto.randomBytes(24));
|
|
481
|
+
|
|
279
482
|
/** A message that is safe to show: the token, if it slipped in, cut out. */
|
|
280
483
|
export function scrubToken(msg: string, token: string | null): string {
|
|
281
484
|
return token ? msg.split(token).join("[token]") : msg;
|
|
@@ -297,28 +500,32 @@ export async function hubStatus(
|
|
|
297
500
|
configured: true,
|
|
298
501
|
url: "",
|
|
299
502
|
connected: false,
|
|
503
|
+
state: "bad_config",
|
|
300
504
|
problem: (e as Error).message,
|
|
301
505
|
};
|
|
302
506
|
}
|
|
303
507
|
if (!cfg) return { configured: false };
|
|
304
|
-
|
|
508
|
+
const base = {
|
|
509
|
+
configured: true as const,
|
|
510
|
+
url: cfg.url,
|
|
511
|
+
allowTools: cfg.allowTools,
|
|
512
|
+
};
|
|
513
|
+
if (!cfg.tokenRef && !cfg.tokenSource)
|
|
305
514
|
return {
|
|
306
|
-
|
|
307
|
-
url: cfg.url,
|
|
308
|
-
allowTools: cfg.allowTools,
|
|
515
|
+
...base,
|
|
309
516
|
connected: false,
|
|
517
|
+
state: "no_token",
|
|
310
518
|
problem:
|
|
311
519
|
cfg.tokenFrom === "hub.token"
|
|
312
|
-
? "
|
|
520
|
+
? "not connected yet: Connect signs you in to Hub and sets this install up"
|
|
313
521
|
: `hub.token names ${cfg.tokenFrom}, which is not set for this install's secrets backend`,
|
|
314
522
|
};
|
|
315
523
|
const token = await (deps.token ?? ((c) => readHubToken(c)))(cfg);
|
|
316
524
|
if (!token)
|
|
317
525
|
return {
|
|
318
|
-
|
|
319
|
-
url: cfg.url,
|
|
320
|
-
allowTools: cfg.allowTools,
|
|
526
|
+
...base,
|
|
321
527
|
connected: false,
|
|
528
|
+
state: "no_token",
|
|
322
529
|
problem: `${cfg.tokenFrom} could not be read from this install's secrets store`,
|
|
323
530
|
};
|
|
324
531
|
try {
|
|
@@ -327,19 +534,34 @@ export async function hubStatus(
|
|
|
327
534
|
token,
|
|
328
535
|
);
|
|
329
536
|
return {
|
|
330
|
-
|
|
331
|
-
url: cfg.url,
|
|
332
|
-
allowTools: cfg.allowTools,
|
|
537
|
+
...base,
|
|
333
538
|
connected: true,
|
|
334
|
-
|
|
539
|
+
state: "connected",
|
|
540
|
+
...(who.company?.name ? { team: who.company.name } : {}),
|
|
541
|
+
...(who.company?.url ? { teamUrl: who.company.url } : {}),
|
|
542
|
+
...(who.install ? { label: who.install } : {}),
|
|
543
|
+
...(who.role ? { role: who.role } : {}),
|
|
544
|
+
...(who.login ? { login: who.login } : {}),
|
|
335
545
|
};
|
|
336
546
|
} catch (e) {
|
|
547
|
+
const kind = e instanceof HubCallError ? e.kind : "unreachable";
|
|
337
548
|
return {
|
|
338
|
-
|
|
339
|
-
url: cfg.url,
|
|
340
|
-
allowTools: cfg.allowTools,
|
|
549
|
+
...base,
|
|
341
550
|
connected: false,
|
|
342
|
-
|
|
551
|
+
state:
|
|
552
|
+
kind === "rejected"
|
|
553
|
+
? "rejected"
|
|
554
|
+
: kind === "forbidden"
|
|
555
|
+
? "forbidden"
|
|
556
|
+
: "unreachable",
|
|
557
|
+
// A HubCallError's words stand on their own; anything else is
|
|
558
|
+
// something that kept Hub from answering.
|
|
559
|
+
problem: scrubToken(
|
|
560
|
+
e instanceof HubCallError
|
|
561
|
+
? e.message
|
|
562
|
+
: `Hub did not answer: ${(e as Error)?.message ?? e}`,
|
|
563
|
+
token,
|
|
564
|
+
),
|
|
343
565
|
};
|
|
344
566
|
}
|
|
345
567
|
}
|