@monster-orange/den 0.4.2 → 0.4.4
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 +273 -0
- package/README.md +9 -9
- package/docs/distributions.md +313 -50
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +53 -12
- package/src/shared/agents/generic.ts +1 -0
- package/src/shared/agents/runtime-search.ts +543 -0
- package/src/shared/agents/runtime.ts +43 -1
- package/src/shared/agents/runtimes.ts +53 -4
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +92 -2
- package/src/shared/edge/render.ts +31 -0
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +298 -19
- package/src/ui/cli/agents-command.ts +129 -4
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/cli/sessions-command.ts +9 -4
- package/src/ui/cli/web-command.ts +53 -8
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +123 -26
- package/src/ui/server/edge-exposure.ts +143 -0
- package/src/ui/server/index.ts +171 -25
- package/src/ui/server/public/agent-start.js +241 -0
- package/src/ui/server/public/agents-page.js +246 -45
- package/src/ui/server/public/agents-section.js +7 -21
- package/src/ui/server/public/agents.html +55 -1
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +26 -13
- package/src/ui/server/public/i18n/ja.js +25 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +62 -22
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +226 -1
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +67 -22
- package/src/ui/server/public/topbar.js +217 -158
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +123 -8
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/ui-assets.ts +2 -0
- package/src/ui/server/workspace-routes.ts +10 -3
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* what happened, or throws an AgentError saying why it didn't.
|
|
8
8
|
*
|
|
9
9
|
* start resolves the persona (Den's default pack, then
|
|
10
|
-
* agents.persona_packs) and the runtime
|
|
10
|
+
* agents.persona_packs) and the runtime (its CLI from
|
|
11
|
+
* agents.runtimes.<id>.bin in user.yml, else the search:
|
|
12
|
+
* runtime-search.ts, AG-0.1; unavailable, it is refused with the
|
|
13
|
+
* fix), asks the runtime for argv,
|
|
11
14
|
* env and files (its MCP config: `<cli> mcp --workspace <ws>
|
|
12
15
|
* --actor agent:<id>`, plus a `--grant` per agents.<id>.grants),
|
|
13
16
|
* starts `<dist>-<ws>:<id>` in the workspace's dir, and writes the
|
|
@@ -38,9 +41,23 @@ import * as fs from "fs";
|
|
|
38
41
|
import * as path from "path";
|
|
39
42
|
import {
|
|
40
43
|
AGENTS_RESERVED_KEYS,
|
|
44
|
+
configuredRuntimeBin,
|
|
45
|
+
configuredRuntimeCatalog,
|
|
41
46
|
readAgentsConfig,
|
|
47
|
+
setUserRuntimeBin,
|
|
48
|
+
userConfigPath,
|
|
42
49
|
type ResolvedAgentsConfig,
|
|
43
50
|
} from "../../shared/den-config.js";
|
|
51
|
+
import {
|
|
52
|
+
setWorkspaceRuntimeBin,
|
|
53
|
+
workspaceBinKey,
|
|
54
|
+
workspaceRuntimeBin,
|
|
55
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
56
|
+
import {
|
|
57
|
+
BUILTIN_CATALOG,
|
|
58
|
+
type CatalogEntry,
|
|
59
|
+
} from "../../shared/agents/catalog.js";
|
|
60
|
+
import { checkRuntimeBin } from "../../shared/agents/catalog-runtime.js";
|
|
44
61
|
import { cliEntrypoint } from "../../shared/engine.js";
|
|
45
62
|
import { getDistRoot } from "../../shared/paths.js";
|
|
46
63
|
import { envKey } from "../../shared/product.js";
|
|
@@ -65,10 +82,20 @@ import {
|
|
|
65
82
|
getRuntime,
|
|
66
83
|
LaunchError,
|
|
67
84
|
runtimeIds,
|
|
85
|
+
syncConfiguredRuntimes,
|
|
68
86
|
type AgentMcp,
|
|
69
87
|
type LaunchInput,
|
|
70
88
|
type Runtime,
|
|
71
89
|
} from "../../shared/agents/runtimes.js";
|
|
90
|
+
import type { RuntimeDetect } from "../../shared/agents/runtime.js";
|
|
91
|
+
import {
|
|
92
|
+
binSettingKey,
|
|
93
|
+
ensureInteractivePath,
|
|
94
|
+
interactivePathAsked,
|
|
95
|
+
runtimeVersion,
|
|
96
|
+
type BinSetting,
|
|
97
|
+
type BinSource,
|
|
98
|
+
} from "../../shared/agents/runtime-search.js";
|
|
72
99
|
import { WORKSPACE_NAME_RE } from "../../shared/agent-actor.js";
|
|
73
100
|
import {
|
|
74
101
|
bindActorSession,
|
|
@@ -103,6 +130,24 @@ export interface AgentsDeps extends SessionsDeps {
|
|
|
103
130
|
* exit (default 3000 ms; 0: not at all).
|
|
104
131
|
*/
|
|
105
132
|
exitWatchMs?: number;
|
|
133
|
+
/** `agents.runtimes.<id>.bin` (default: user.yml's; undefined: unset). */
|
|
134
|
+
runtimeBin?: (id: string) => unknown;
|
|
135
|
+
/**
|
|
136
|
+
* Workspace `ws`'s `agents.runtimes.<id>.bin` (default: workspaces.yml's;
|
|
137
|
+
* undefined: unset). It wins over runtimeBin (AG-0.5).
|
|
138
|
+
*/
|
|
139
|
+
workspaceRuntimeBin?: (ws: string, id: string) => unknown;
|
|
140
|
+
/** user.yml's runtime catalog entries, checked (default: user.yml's). */
|
|
141
|
+
userCatalog?: (taken: readonly string[]) => CatalogEntry[];
|
|
142
|
+
/** Save a checked path (null clears it): global or the workspace's. */
|
|
143
|
+
saveRuntimeBin?: (
|
|
144
|
+
scope: "global" | "workspace",
|
|
145
|
+
ws: string | undefined,
|
|
146
|
+
id: string,
|
|
147
|
+
bin: string | null,
|
|
148
|
+
) => void;
|
|
149
|
+
/** The user.yml a person edits to set it (default: config/local/user.yml). */
|
|
150
|
+
userConfigFile?: () => string;
|
|
106
151
|
}
|
|
107
152
|
|
|
108
153
|
export interface AgentsResolved extends Resolved {
|
|
@@ -111,6 +156,11 @@ export interface AgentsResolved extends Resolved {
|
|
|
111
156
|
workspaceDir: (ws: string) => string;
|
|
112
157
|
mcpServer: () => { command: string[] | string; env: Record<string, string> };
|
|
113
158
|
exitWatchMs: number;
|
|
159
|
+
runtimeBin: (id: string) => unknown;
|
|
160
|
+
workspaceRuntimeBin: (ws: string, id: string) => unknown;
|
|
161
|
+
userCatalog: (taken: readonly string[]) => CatalogEntry[];
|
|
162
|
+
saveRuntimeBin: NonNullable<AgentsDeps["saveRuntimeBin"]>;
|
|
163
|
+
userConfigFile: () => string;
|
|
114
164
|
}
|
|
115
165
|
|
|
116
166
|
/** The early-exit watch: at most this long, polling the host. */
|
|
@@ -199,6 +249,17 @@ export function resolveAgentsDeps(d: AgentsDeps): AgentsResolved {
|
|
|
199
249
|
}),
|
|
200
250
|
mcpServer: d.mcpServer ?? (() => defaultMcpServer(base.cli)),
|
|
201
251
|
exitWatchMs: d.exitWatchMs ?? EXIT_WATCH_MS,
|
|
252
|
+
runtimeBin: d.runtimeBin ?? ((id) => configuredRuntimeBin(id)),
|
|
253
|
+
workspaceRuntimeBin:
|
|
254
|
+
d.workspaceRuntimeBin ?? ((ws, id) => workspaceRuntimeBin(ws, id)),
|
|
255
|
+
userCatalog: d.userCatalog ?? ((taken) => configuredRuntimeCatalog(taken)),
|
|
256
|
+
saveRuntimeBin:
|
|
257
|
+
d.saveRuntimeBin ??
|
|
258
|
+
((scope, ws, id, bin) =>
|
|
259
|
+
scope === "workspace" && ws
|
|
260
|
+
? setWorkspaceRuntimeBin(ws, id, bin)
|
|
261
|
+
: setUserRuntimeBin(id, bin)),
|
|
262
|
+
userConfigFile: d.userConfigFile ?? userConfigPath,
|
|
202
263
|
};
|
|
203
264
|
}
|
|
204
265
|
|
|
@@ -394,29 +455,225 @@ export function listPersonas(d: AgentsResolved): ListedPersona[] {
|
|
|
394
455
|
|
|
395
456
|
export interface RuntimeChoice {
|
|
396
457
|
id: string;
|
|
458
|
+
/** Its name for people ("Claude Code"). */
|
|
459
|
+
label?: string;
|
|
460
|
+
/** Who makes it ("Anthropic"): a catalog runtime (AG-0.5). */
|
|
461
|
+
vendor?: string;
|
|
462
|
+
/**
|
|
463
|
+
* false: it runs as its own CLI, without Den's tools yet ("Den tools:
|
|
464
|
+
* coming"). Absent for a runtime that isn't from the catalog.
|
|
465
|
+
*/
|
|
466
|
+
denTools?: boolean;
|
|
397
467
|
available: boolean;
|
|
398
468
|
/** Why it is unavailable. */
|
|
399
469
|
why?: string;
|
|
400
470
|
version?: string;
|
|
471
|
+
/** The CLI it runs (AG-0.2). */
|
|
472
|
+
path?: string;
|
|
473
|
+
/**
|
|
474
|
+
* Where that path came from: workspace (its workspaces.yml entry),
|
|
475
|
+
* setting (user.yml), login-shell, PATH, well-known, interactive-shell.
|
|
476
|
+
*/
|
|
477
|
+
source?: BinSource;
|
|
478
|
+
/** The directories searched, in order (a runtime with a CLI to find). */
|
|
479
|
+
searched?: string[];
|
|
480
|
+
/** The user.yml key that sets its path (a runtime with a CLI to find). */
|
|
481
|
+
setting?: string;
|
|
482
|
+
/** The workspace's key for it, when listed for a workspace. */
|
|
483
|
+
workspaceSetting?: string;
|
|
401
484
|
/** It declares resume. */
|
|
402
485
|
resume: boolean;
|
|
403
486
|
default: boolean;
|
|
404
487
|
}
|
|
405
488
|
|
|
406
|
-
/**
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
489
|
+
/**
|
|
490
|
+
* The path set for runtime `id` (AG-0.5): the workspace's (when `ws` is
|
|
491
|
+
* given and it sets one), else user.yml's; null when neither does.
|
|
492
|
+
*/
|
|
493
|
+
export function runtimeSetting(
|
|
494
|
+
d: AgentsResolved,
|
|
495
|
+
id: string,
|
|
496
|
+
ws?: string,
|
|
497
|
+
): BinSetting | null {
|
|
498
|
+
if (ws) {
|
|
499
|
+
const v = d.workspaceRuntimeBin(ws, id);
|
|
500
|
+
if (v !== undefined && v !== null)
|
|
501
|
+
return { value: v, source: "workspace", key: workspaceBinKey(ws, id) };
|
|
502
|
+
}
|
|
503
|
+
const g = d.runtimeBin(id);
|
|
504
|
+
return g === undefined || g === null ? null : { value: g, source: "setting" };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Whether runtime `rt` can run here (AG-0.1, AG-0.5): a path set for the
|
|
509
|
+
* workspace, else user.yml's (`agents.runtimes.<id>.bin`), else the
|
|
510
|
+
* search. Runs nothing.
|
|
511
|
+
*/
|
|
512
|
+
export function detectRuntime(
|
|
513
|
+
d: AgentsResolved,
|
|
514
|
+
rt: Runtime,
|
|
515
|
+
ws?: string,
|
|
516
|
+
): RuntimeDetect {
|
|
517
|
+
return rt.detect(d.env, { bin: runtimeSetting(d, rt.id, ws) });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The catalog entries the distribution and user.yml add, registered as
|
|
522
|
+
* runtimes (AG-0.5). A malformed entry is a refusal naming its key.
|
|
523
|
+
*/
|
|
524
|
+
export function syncCatalog(d: AgentsResolved): void {
|
|
525
|
+
const cfg = agentsConfig(d);
|
|
526
|
+
let extra: CatalogEntry[];
|
|
527
|
+
try {
|
|
528
|
+
extra = [
|
|
529
|
+
...cfg.runtimeCatalog,
|
|
530
|
+
...d.userCatalog([
|
|
531
|
+
...BUILTIN_CATALOG.map((e) => e.id),
|
|
532
|
+
...cfg.runtimeCatalog.map((e) => e.id),
|
|
533
|
+
]),
|
|
534
|
+
];
|
|
535
|
+
} catch (e) {
|
|
536
|
+
throw refused((e as Error).message, [], 422);
|
|
537
|
+
}
|
|
538
|
+
syncConfiguredRuntimes(extra);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* When a catalog runtime is still missing (nothing set, not found on the
|
|
543
|
+
* login PATH, PATH or its install directories), ask the interactive shell
|
|
544
|
+
* for its PATH: once per process, then kept (AG-0.5).
|
|
545
|
+
*/
|
|
546
|
+
async function fallbackSearch(
|
|
547
|
+
d: AgentsResolved,
|
|
548
|
+
rts: Runtime[],
|
|
549
|
+
ws?: string,
|
|
550
|
+
): Promise<void> {
|
|
551
|
+
if (interactivePathAsked()) return;
|
|
552
|
+
const missing = rts.some(
|
|
553
|
+
(rt) =>
|
|
554
|
+
rt.catalog &&
|
|
555
|
+
!runtimeSetting(d, rt.id, ws) &&
|
|
556
|
+
!detectRuntime(d, rt, ws).available,
|
|
557
|
+
);
|
|
558
|
+
if (missing) await ensureInteractivePath({ env: d.env });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The catalog's installed runtimes first, then the ones not found, then
|
|
563
|
+
* the rest (generic: no CLI of its own to find).
|
|
564
|
+
*/
|
|
565
|
+
function rank(r: RuntimeChoice): number {
|
|
566
|
+
if (r.denTools === undefined) return 2;
|
|
567
|
+
return r.available ? 0 : 1;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Every registered runtime, and whether it can run here (for an agent of
|
|
572
|
+
* `ws`, when given: its workspace's path wins), with the path it runs and
|
|
573
|
+
* its version: its version command (`claude --version`), run once under a
|
|
574
|
+
* timeout and kept, else what its install says. Installed ones first.
|
|
575
|
+
*/
|
|
576
|
+
export async function listRuntimes(
|
|
577
|
+
d: AgentsResolved,
|
|
578
|
+
ws?: string,
|
|
579
|
+
): Promise<RuntimeChoice[]> {
|
|
580
|
+
syncCatalog(d);
|
|
581
|
+
const ids = runtimeIds();
|
|
582
|
+
await fallbackSearch(
|
|
583
|
+
d,
|
|
584
|
+
ids.map((id) => getRuntime(id)!),
|
|
585
|
+
ws,
|
|
586
|
+
);
|
|
587
|
+
const rows = await Promise.all(
|
|
588
|
+
ids.map(async (id) => {
|
|
589
|
+
const rt = getRuntime(id)!;
|
|
590
|
+
const det = detectRuntime(d, rt, ws);
|
|
591
|
+
const asked =
|
|
592
|
+
det.available && det.path && rt.find?.versionArgs
|
|
593
|
+
? await runtimeVersion(det.path, rt.find.versionArgs, d.env)
|
|
594
|
+
: undefined;
|
|
595
|
+
const version = asked ?? det.version;
|
|
596
|
+
return {
|
|
597
|
+
id,
|
|
598
|
+
...(rt.label ? { label: rt.label } : {}),
|
|
599
|
+
...(rt.vendor ? { vendor: rt.vendor } : {}),
|
|
600
|
+
...(rt.catalog ? { denTools: rt.denTools === true } : {}),
|
|
601
|
+
available: det.available,
|
|
602
|
+
...(det.available ? {} : { why: det.why ?? "unavailable" }),
|
|
603
|
+
...(version ? { version } : {}),
|
|
604
|
+
...(det.path ? { path: det.path } : {}),
|
|
605
|
+
...(det.source ? { source: det.source } : {}),
|
|
606
|
+
...(rt.find
|
|
607
|
+
? {
|
|
608
|
+
searched: det.searched ?? [],
|
|
609
|
+
setting: binSettingKey(id),
|
|
610
|
+
...(ws ? { workspaceSetting: workspaceBinKey(ws, id) } : {}),
|
|
611
|
+
}
|
|
612
|
+
: {}),
|
|
613
|
+
resume: Boolean(rt.resume),
|
|
614
|
+
default: id === DEFAULT_RUNTIME_ID,
|
|
615
|
+
};
|
|
616
|
+
}),
|
|
617
|
+
);
|
|
618
|
+
return rows
|
|
619
|
+
.map((r, i) => ({ r, i }))
|
|
620
|
+
.sort((a, b) => rank(a.r) - rank(b.r) || a.i - b.i)
|
|
621
|
+
.map((x) => x.r);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Set (or, with null, clear) runtime `id`'s path, global or for workspace
|
|
626
|
+
* `ws` (AG-0.5): its version command runs first, and a path that isn't
|
|
627
|
+
* that runtime is refused. Returns its row as listed after.
|
|
628
|
+
*/
|
|
629
|
+
export async function setRuntimePath(
|
|
630
|
+
d: AgentsResolved,
|
|
631
|
+
req: {
|
|
632
|
+
id: string;
|
|
633
|
+
scope: "global" | "workspace";
|
|
634
|
+
workspace?: string;
|
|
635
|
+
bin: string | null;
|
|
636
|
+
},
|
|
637
|
+
): Promise<RuntimeChoice> {
|
|
638
|
+
syncCatalog(d);
|
|
639
|
+
const rt = runtimeFor(req.id);
|
|
640
|
+
if (!rt.catalog)
|
|
641
|
+
throw usage(`${req.id} has no CLI to find: its path can't be set`);
|
|
642
|
+
let ws: string | undefined;
|
|
643
|
+
if (req.scope === "workspace") {
|
|
644
|
+
ws = checkWorkspaceName("runtime path", req.workspace);
|
|
645
|
+
if (!d.workspaceExists(ws))
|
|
646
|
+
throw usage(`workspace "${ws}" does not exist`, 404);
|
|
647
|
+
}
|
|
648
|
+
if (req.bin !== null) {
|
|
649
|
+
const c = await checkRuntimeBin(rt.catalog, req.bin, d.env);
|
|
650
|
+
if (!c.ok) throw refused(c.why, [], 422);
|
|
651
|
+
d.saveRuntimeBin(req.scope, ws, rt.id, c.path);
|
|
652
|
+
} else d.saveRuntimeBin(req.scope, ws, rt.id, null);
|
|
653
|
+
const rows = await listRuntimes(d, ws);
|
|
654
|
+
return rows.find((r) => r.id === rt.id)!;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Refused unless `rt` can run here; its detect result when it can. */
|
|
658
|
+
async function availableOrRefuse(
|
|
659
|
+
d: AgentsResolved,
|
|
660
|
+
rt: Runtime,
|
|
661
|
+
ws?: string,
|
|
662
|
+
): Promise<RuntimeDetect> {
|
|
663
|
+
await fallbackSearch(d, [rt], ws);
|
|
664
|
+
const det = detectRuntime(d, rt, ws);
|
|
665
|
+
if (!det.available)
|
|
666
|
+
throw refused(
|
|
667
|
+
`${rt.id} is unavailable: ${det.why ?? "unknown"}`,
|
|
668
|
+
rt.find
|
|
669
|
+
? [
|
|
670
|
+
`Install it, or set ${binSettingKey(rt.id)} to its absolute path in ${d.userConfigFile()}.`,
|
|
671
|
+
`\`${d.cli} agents doctor\` shows where it looked.`,
|
|
672
|
+
]
|
|
673
|
+
: [],
|
|
674
|
+
503,
|
|
675
|
+
);
|
|
676
|
+
return det;
|
|
420
677
|
}
|
|
421
678
|
|
|
422
679
|
function runtimeFor(id: string): Runtime {
|
|
@@ -460,6 +717,21 @@ interface Launch {
|
|
|
460
717
|
passthrough?: readonly string[];
|
|
461
718
|
/** agents.<id>.grants, recorded with its actor token. */
|
|
462
719
|
grants: readonly string[];
|
|
720
|
+
/** Added to the session's PATH: a configured CLI's own directory. */
|
|
721
|
+
binDir?: string;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A CLI's directory when the session's PATH wouldn't have it (a path set
|
|
726
|
+
* for it, or found by the interactive shell), for its own tools.
|
|
727
|
+
*/
|
|
728
|
+
function binDirOf(det: RuntimeDetect): { binDir?: string } {
|
|
729
|
+
return (det.source === "setting" ||
|
|
730
|
+
det.source === "workspace" ||
|
|
731
|
+
det.source === "interactive-shell") &&
|
|
732
|
+
det.path
|
|
733
|
+
? { binDir: path.dirname(det.path) }
|
|
734
|
+
: {};
|
|
463
735
|
}
|
|
464
736
|
|
|
465
737
|
/** The host's cap refusal as an AgentError (its hint says what to do). */
|
|
@@ -511,6 +783,7 @@ async function startSession(
|
|
|
511
783
|
launch.env,
|
|
512
784
|
launch.rec.id,
|
|
513
785
|
minted.token,
|
|
786
|
+
launch.binDir ? [launch.binDir] : [],
|
|
514
787
|
),
|
|
515
788
|
cols: (tty && d.stdout.columns) || 80,
|
|
516
789
|
rows: (tty && d.stdout.rows) || 24,
|
|
@@ -601,6 +874,7 @@ function launchInput(
|
|
|
601
874
|
ws: string,
|
|
602
875
|
id: string,
|
|
603
876
|
model: string | undefined,
|
|
877
|
+
bin?: string,
|
|
604
878
|
): LaunchInput {
|
|
605
879
|
const grants = cfg.agents[id]?.grants ?? [];
|
|
606
880
|
for (const g of grants)
|
|
@@ -623,6 +897,7 @@ function launchInput(
|
|
|
623
897
|
filesDir: agentFilesDir(d.stateDir(), id),
|
|
624
898
|
stateDir: d.stateDir(),
|
|
625
899
|
env: d.env,
|
|
900
|
+
...(bin ? { bin } : {}),
|
|
626
901
|
};
|
|
627
902
|
}
|
|
628
903
|
|
|
@@ -680,10 +955,9 @@ export async function startAgent(
|
|
|
680
955
|
);
|
|
681
956
|
const id = req.id ?? defaultId(d.stateDir(), persona.id, ws);
|
|
682
957
|
checkId(where, id);
|
|
958
|
+
syncCatalog(d);
|
|
683
959
|
const rt = runtimeFor(req.runtime ?? persona.runtime ?? DEFAULT_RUNTIME_ID);
|
|
684
|
-
const det =
|
|
685
|
-
if (!det.available)
|
|
686
|
-
throw refused(`${rt.id} is unavailable: ${det.why ?? "unknown"}`, [], 503);
|
|
960
|
+
const det = await availableOrRefuse(d, rt, ws);
|
|
687
961
|
const model = req.model ?? persona.model;
|
|
688
962
|
|
|
689
963
|
const prev = readRecord(d.stateDir(), id);
|
|
@@ -692,7 +966,7 @@ export async function startAgent(
|
|
|
692
966
|
`agent ${id} belongs to workspace ${prev.workspace}; give this one another --id`,
|
|
693
967
|
);
|
|
694
968
|
ptyOrRefuse(d);
|
|
695
|
-
const input = launchInput(d, cfg, persona, ws, id, model);
|
|
969
|
+
const input = launchInput(d, cfg, persona, ws, id, model, det.path);
|
|
696
970
|
|
|
697
971
|
return withClient(d, async (c) => {
|
|
698
972
|
const sessions = await c.list();
|
|
@@ -721,6 +995,7 @@ export async function startAgent(
|
|
|
721
995
|
env: out.env,
|
|
722
996
|
passthrough: rt.envPassthrough,
|
|
723
997
|
grants: cfg.agents[id]?.grants ?? [],
|
|
998
|
+
...binDirOf(det),
|
|
724
999
|
},
|
|
725
1000
|
old,
|
|
726
1001
|
{},
|
|
@@ -786,6 +1061,7 @@ export async function resumeAgent(
|
|
|
786
1061
|
ws?: string,
|
|
787
1062
|
): Promise<Resumed> {
|
|
788
1063
|
const rec = agentRecord(d, id, ws);
|
|
1064
|
+
syncCatalog(d);
|
|
789
1065
|
const rt = runtimeFor(rec.runtime);
|
|
790
1066
|
if (!rt.resume)
|
|
791
1067
|
throw refused(`runtime ${rt.id} does not declare resume`, [
|
|
@@ -795,6 +1071,7 @@ export async function resumeAgent(
|
|
|
795
1071
|
const persona = personas(d, cfg).get(rec.persona);
|
|
796
1072
|
if (!persona)
|
|
797
1073
|
throw refused(`agent ${rec.id}'s persona "${rec.persona}" is gone`);
|
|
1074
|
+
const det = await availableOrRefuse(d, rt, rec.workspace);
|
|
798
1075
|
ptyOrRefuse(d);
|
|
799
1076
|
return withClient(d, async (c) => {
|
|
800
1077
|
const old = sessionOf(await c.list(), rec);
|
|
@@ -809,6 +1086,7 @@ export async function resumeAgent(
|
|
|
809
1086
|
rec.workspace,
|
|
810
1087
|
rec.id,
|
|
811
1088
|
rec.model,
|
|
1089
|
+
det.path,
|
|
812
1090
|
);
|
|
813
1091
|
// Resume in the cwd the agent had, wherever the workspace points now.
|
|
814
1092
|
const at = {
|
|
@@ -833,6 +1111,7 @@ export async function resumeAgent(
|
|
|
833
1111
|
env: out.env,
|
|
834
1112
|
passthrough: rt.envPassthrough,
|
|
835
1113
|
grants: cfg.agents[rec.id]?.grants ?? [],
|
|
1114
|
+
...binDirOf(det),
|
|
836
1115
|
},
|
|
837
1116
|
old,
|
|
838
1117
|
fresh ? {} : { resumedAt: new Date().toISOString() },
|
|
@@ -24,16 +24,27 @@
|
|
|
24
24
|
* running: a new session, in the same cwd, continuing the last run;
|
|
25
25
|
* when that session was never prompted, a fresh start under the
|
|
26
26
|
* same conversation id (there is nothing to continue).
|
|
27
|
+
* doctor (AG-0.3, AG-0.5) each runtime of the catalog, installed
|
|
28
|
+
* first: its status, the CLI it runs and where that came from
|
|
29
|
+
* (workspace or user.yml setting, login shell, PATH, install
|
|
30
|
+
* directory, interactive shell), its
|
|
31
|
+
* version (its version command, once, under a timeout), the
|
|
32
|
+
* directories searched (names only, never an env value), and the
|
|
33
|
+
* fix when it is missing. Exit 1 when the default runtime is
|
|
34
|
+
* unavailable. It resolves the login shell's PATH first, as the
|
|
35
|
+
* web server does at start.
|
|
27
36
|
*
|
|
28
37
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
29
38
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
30
39
|
*/
|
|
31
40
|
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
41
|
+
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
32
42
|
import { HostError } from "../../shared/sessions/client.js";
|
|
33
43
|
import {
|
|
34
44
|
AgentError,
|
|
35
45
|
listAgents,
|
|
36
46
|
listPersonas as personaList,
|
|
47
|
+
listRuntimes,
|
|
37
48
|
loadAgentWorkspaces,
|
|
38
49
|
resolveAgentsDeps,
|
|
39
50
|
resumeAgent,
|
|
@@ -42,6 +53,7 @@ import {
|
|
|
42
53
|
type AgentsDeps,
|
|
43
54
|
type AgentsResolved,
|
|
44
55
|
type EarlyExit,
|
|
56
|
+
type RuntimeChoice,
|
|
45
57
|
type Started,
|
|
46
58
|
} from "./agent-ops.js";
|
|
47
59
|
import {
|
|
@@ -63,7 +75,7 @@ export {
|
|
|
63
75
|
} from "./agent-ops.js";
|
|
64
76
|
|
|
65
77
|
export const AGENTS_HELP = {
|
|
66
|
-
usage: "agents <list|start|stop|resume|personas> ...",
|
|
78
|
+
usage: "agents <list|start|stop|resume|personas|doctor> ...",
|
|
67
79
|
lines: [
|
|
68
80
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
69
81
|
"outlives the web server (see `{cli} sessions`).",
|
|
@@ -78,9 +90,20 @@ export const AGENTS_HELP = {
|
|
|
78
90
|
" a session never prompted starts fresh under the same conversation id",
|
|
79
91
|
"{cli} agents personas [--json]",
|
|
80
92
|
" Den's default pack, then agents.persona_packs (a later pack wins by id)",
|
|
93
|
+
"{cli} agents doctor [-w <ws>] [--json]",
|
|
94
|
+
" each runtime, installed first: status, the CLI it runs and where it came",
|
|
95
|
+
" from, its version, the directories searched, and the fix; exit 1 when the",
|
|
96
|
+
" default is missing. -w: as that workspace's agents find them",
|
|
81
97
|
"",
|
|
82
|
-
"Runtimes: den-claude (default;
|
|
83
|
-
"
|
|
98
|
+
"Runtimes: den-claude (default; Claude Code's claude), codex (OpenAI's Codex",
|
|
99
|
+
"CLI), gemini (Google's Gemini CLI), grok (xAI's Grok Build), and generic (the",
|
|
100
|
+
"persona's argv template). codex, gemini and grok run as their own CLI in the",
|
|
101
|
+
"workspace, without Den's tools yet. Attach with `{cli} sessions attach <ws>:<id>`.",
|
|
102
|
+
"A runtime's CLI is found as your terminal finds it: <ws>.agents.runtimes.<id>.bin",
|
|
103
|
+
"in config/local/workspaces.yml, then agents.runtimes.<id>.bin in",
|
|
104
|
+
"config/local/user.yml (absolute paths), then the login shell's PATH, this PATH,",
|
|
105
|
+
"its install directories (~/.local/bin, /opt/homebrew/bin, /usr/local/bin, npm's",
|
|
106
|
+
"global bin, the vendor's own), and last the interactive shell's PATH.",
|
|
84
107
|
"An agent's env is PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG,",
|
|
85
108
|
"LC_*, TZ and TMPDIR, plus what its runtime declares: nothing else of yours.",
|
|
86
109
|
],
|
|
@@ -250,10 +273,108 @@ async function listPersonas(
|
|
|
250
273
|
return 0;
|
|
251
274
|
}
|
|
252
275
|
|
|
276
|
+
export interface AgentsDoctorDeps {
|
|
277
|
+
/** Resolve the login shell's PATH first (default: initLoginPath). */
|
|
278
|
+
loginPath?: () => Promise<void>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const SOURCE_WORD: Record<string, string> = {
|
|
282
|
+
workspace: "the workspace's agents.runtimes setting",
|
|
283
|
+
setting: "agents.runtimes setting",
|
|
284
|
+
"login-shell": "login shell PATH",
|
|
285
|
+
PATH: "PATH",
|
|
286
|
+
"well-known": "install directory",
|
|
287
|
+
"interactive-shell": "interactive shell PATH",
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** `agents doctor`'s lines for one runtime (names and paths, never env values). */
|
|
291
|
+
export function doctorLines(
|
|
292
|
+
r: RuntimeChoice,
|
|
293
|
+
cli: string,
|
|
294
|
+
userConfig: string,
|
|
295
|
+
/** Directory lists already printed (a repeat is counted, not listed). */
|
|
296
|
+
printed: string[] = [],
|
|
297
|
+
): string[] {
|
|
298
|
+
const lines = [
|
|
299
|
+
`${r.id}${r.label ? ` (${r.label}${r.vendor ? `, ${r.vendor}` : ""})` : ""}${r.default ? " [default]" : ""}${r.denTools === false ? " [Den tools: coming]" : ""}`,
|
|
300
|
+
r.available
|
|
301
|
+
? " status ✓ available"
|
|
302
|
+
: ` status ✗ unavailable: ${r.why ?? "unknown"}`,
|
|
303
|
+
];
|
|
304
|
+
if (r.path)
|
|
305
|
+
lines.push(
|
|
306
|
+
` path ${r.path}${r.source ? ` (${SOURCE_WORD[r.source] ?? r.source})` : ""}`,
|
|
307
|
+
);
|
|
308
|
+
if (r.available && r.setting)
|
|
309
|
+
lines.push(` version ${r.version ?? "unknown"}`);
|
|
310
|
+
if (!r.setting) lines.push(" path none to find (the persona's argv)");
|
|
311
|
+
else if (r.searched?.length) {
|
|
312
|
+
const key = r.searched.join("\0");
|
|
313
|
+
if (printed.includes(key))
|
|
314
|
+
lines.push(
|
|
315
|
+
` searched the same ${r.searched.length} ${r.searched.length === 1 ? "directory" : "directories"} as above`,
|
|
316
|
+
);
|
|
317
|
+
else {
|
|
318
|
+
printed.push(key);
|
|
319
|
+
r.searched.forEach((dir, i) =>
|
|
320
|
+
lines.push(` ${i === 0 ? "searched " : " "}${dir}`),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else if (r.source !== "setting") lines.push(" searched (no directories)");
|
|
324
|
+
if (!r.available && r.setting)
|
|
325
|
+
lines.push(
|
|
326
|
+
` fix install ${r.label ?? r.id}, or set ${r.setting} to its absolute path in ${userConfig}`,
|
|
327
|
+
` then run \`${cli} agents doctor\` again`,
|
|
328
|
+
);
|
|
329
|
+
return lines;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function doctor(
|
|
333
|
+
args: string[],
|
|
334
|
+
d: AgentsResolved,
|
|
335
|
+
deps: AgentsDoctorDeps,
|
|
336
|
+
): Promise<number> {
|
|
337
|
+
const where = "agents doctor";
|
|
338
|
+
const p = parseArgs(where, args, { "--workspace": true, "--json": false });
|
|
339
|
+
noExtra(where, p);
|
|
340
|
+
const ws = workspaceFlag(where, p);
|
|
341
|
+
if (ws && !d.workspaceExists(ws))
|
|
342
|
+
throw new UsageError(`${where}: workspace "${ws}" does not exist`);
|
|
343
|
+
await (deps.loginPath ?? (() => initLoginPath({ env: d.env })))();
|
|
344
|
+
const rts = await listRuntimes(d, ws);
|
|
345
|
+
const def = rts.find((r) => r.default);
|
|
346
|
+
const code = def?.available ? 0 : 1;
|
|
347
|
+
if (p.flags["--json"]) {
|
|
348
|
+
d.out(
|
|
349
|
+
JSON.stringify(
|
|
350
|
+
{
|
|
351
|
+
runtimes: rts,
|
|
352
|
+
...(ws ? { workspace: ws } : {}),
|
|
353
|
+
userConfig: d.userConfigFile(),
|
|
354
|
+
},
|
|
355
|
+
null,
|
|
356
|
+
2,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
return code;
|
|
360
|
+
}
|
|
361
|
+
const printed: string[] = [];
|
|
362
|
+
rts.forEach((r, i) => {
|
|
363
|
+
if (i) d.out("");
|
|
364
|
+
for (const l of doctorLines(r, d.cli, d.userConfigFile(), printed))
|
|
365
|
+
d.out(l);
|
|
366
|
+
});
|
|
367
|
+
d.out("");
|
|
368
|
+
d.out(
|
|
369
|
+
`\`${d.cli} doctor --probe\` checks the env each runtime's session gets.`,
|
|
370
|
+
);
|
|
371
|
+
return code;
|
|
372
|
+
}
|
|
373
|
+
|
|
253
374
|
/** `<cli> agents ...`; returns the exit code. */
|
|
254
375
|
export async function cmdAgents(
|
|
255
376
|
args: string[],
|
|
256
|
-
deps: AgentsDeps = {},
|
|
377
|
+
deps: AgentsDeps & AgentsDoctorDeps = {},
|
|
257
378
|
): Promise<number> {
|
|
258
379
|
const d = resolveAgentsDeps(deps);
|
|
259
380
|
const sub =
|
|
@@ -274,6 +395,10 @@ export async function cmdAgents(
|
|
|
274
395
|
return await resume(rest, d);
|
|
275
396
|
case "personas":
|
|
276
397
|
return await listPersonas(rest, d);
|
|
398
|
+
case "doctor":
|
|
399
|
+
// -w checks the workspace exists, and reads its runtime paths.
|
|
400
|
+
await loadAgentWorkspaces(deps);
|
|
401
|
+
return await doctor(rest, d, deps);
|
|
277
402
|
default:
|
|
278
403
|
throw new UsageError(`unknown agents command "${sub}"`);
|
|
279
404
|
}
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
routerUrls,
|
|
26
26
|
type IncludedRouter,
|
|
27
27
|
} from "../../shared/edge/included.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
edgeRoutes,
|
|
30
|
+
edgeStatus,
|
|
31
|
+
withHttpsServing,
|
|
32
|
+
} from "../../shared/edge/status.js";
|
|
29
33
|
import { tlsYaml } from "../../shared/edge/tls.js";
|
|
30
34
|
import { rejoinRunningStacks } from "../../shared/stacks/status.js";
|
|
31
35
|
|
|
@@ -102,7 +106,7 @@ export async function cmdEdge(args: string[]): Promise<void> {
|
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
108
|
case "status": {
|
|
105
|
-
const st = edgeStatus(cfg);
|
|
109
|
+
const st = await withHttpsServing(edgeStatus(cfg), cfg);
|
|
106
110
|
console.log(chalk.bold(`${readBrand().name} edge — ${st.org}`));
|
|
107
111
|
console.log(` container ${st.container}: ${st.state}`);
|
|
108
112
|
console.log(` ports ${published}`);
|
|
@@ -116,7 +120,16 @@ export async function cmdEdge(args: string[]): Promise<void> {
|
|
|
116
120
|
// browser trust (BOOT-1), so this machine's trust is moot.
|
|
117
121
|
` CA ${caDir(cfg)} — ${!st.ports ? "not needed: fronted" : st.caTrusted === null ? "unknown" : st.caTrusted ? "trusted" : "NOT trusted"}`,
|
|
118
122
|
);
|
|
119
|
-
if (st.
|
|
123
|
+
if (st.certIssuedByDen)
|
|
124
|
+
console.log(` cert ${st.certNames.join(", ")}`);
|
|
125
|
+
else if (st.https.certs === "distribution")
|
|
126
|
+
console.log(
|
|
127
|
+
" cert the distribution's (tls: files): not minted by Den",
|
|
128
|
+
);
|
|
129
|
+
// MCP-2.5: whether HTTPS is actually serving, not only configured.
|
|
130
|
+
console.log(
|
|
131
|
+
` https ${st.https.serving === null ? "not probed" : st.https.serving ? "serving" : "NOT serving"} — ${st.https.detail}`,
|
|
132
|
+
);
|
|
120
133
|
return;
|
|
121
134
|
}
|
|
122
135
|
default:
|
|
@@ -77,11 +77,14 @@ export function formatRollupLabel(r: RepoRollup): string {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Why, for a repo row: the down services ("mailpit down"), and how many
|
|
80
|
-
* services aren't checked ("25 not checked")
|
|
80
|
+
* services aren't checked ("25 not checked"), and how many answered with a
|
|
81
|
+
* redirect ("1 redirect": answering, so neither down nor unchecked).
|
|
82
|
+
* Unchecked never means down.
|
|
81
83
|
*/
|
|
82
84
|
export function rollupReasonText(r: RepoRollup): string {
|
|
83
85
|
return [
|
|
84
86
|
r.down.length ? `${r.down.join(", ")} down` : "",
|
|
87
|
+
r.redirected ? `${r.redirected} redirect` : "",
|
|
85
88
|
r.unchecked ? `${r.unchecked} not checked` : "",
|
|
86
89
|
]
|
|
87
90
|
.filter(Boolean)
|