@monster-orange/den 0.4.2 → 0.4.3
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 +94 -0
- package/README.md +7 -7
- package/docs/distributions.md +126 -13
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +57 -12
- package/src/shared/agents/generic.ts +1 -0
- package/src/shared/agents/runtime-search.ts +416 -0
- package/src/shared/agents/runtime.ts +26 -1
- package/src/shared/agents/runtimes.ts +30 -3
- package/src/shared/den-config.ts +36 -0
- package/src/shared/edge/render.ts +31 -0
- package/src/ui/cli/agent-ops.ts +107 -19
- package/src/ui/cli/agents-command.ts +99 -3
- package/src/ui/cli/sessions-command.ts +9 -4
- package/src/ui/cli/web-command.ts +53 -8
- package/src/ui/server/agent-routes.ts +9 -2
- package/src/ui/server/edge-exposure.ts +143 -0
- package/src/ui/server/index.ts +94 -19
- package/src/ui/server/public/agent-start.js +202 -0
- package/src/ui/server/public/agents-page.js +83 -17
- package/src/ui/server/public/agents-section.js +6 -18
- package/src/ui/server/public/agents.html +70 -0
- package/src/ui/server/public/i18n/es.js +11 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/local-dev.html +43 -14
- package/src/ui/server/public/settings-registry.js +1 -1
- package/src/ui/server/public/shell.js +40 -0
- package/src/ui/server/public/topbar.js +127 -28
- package/src/ui/server/request-guard.ts +122 -7
- package/src/ui/server/ui-assets.ts +2 -0
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,7 +41,9 @@ import * as fs from "fs";
|
|
|
38
41
|
import * as path from "path";
|
|
39
42
|
import {
|
|
40
43
|
AGENTS_RESERVED_KEYS,
|
|
44
|
+
configuredRuntimeBin,
|
|
41
45
|
readAgentsConfig,
|
|
46
|
+
userConfigPath,
|
|
42
47
|
type ResolvedAgentsConfig,
|
|
43
48
|
} from "../../shared/den-config.js";
|
|
44
49
|
import { cliEntrypoint } from "../../shared/engine.js";
|
|
@@ -69,6 +74,12 @@ import {
|
|
|
69
74
|
type LaunchInput,
|
|
70
75
|
type Runtime,
|
|
71
76
|
} from "../../shared/agents/runtimes.js";
|
|
77
|
+
import type { RuntimeDetect } from "../../shared/agents/runtime.js";
|
|
78
|
+
import {
|
|
79
|
+
binSettingKey,
|
|
80
|
+
runtimeVersion,
|
|
81
|
+
type BinSource,
|
|
82
|
+
} from "../../shared/agents/runtime-search.js";
|
|
72
83
|
import { WORKSPACE_NAME_RE } from "../../shared/agent-actor.js";
|
|
73
84
|
import {
|
|
74
85
|
bindActorSession,
|
|
@@ -103,6 +114,10 @@ export interface AgentsDeps extends SessionsDeps {
|
|
|
103
114
|
* exit (default 3000 ms; 0: not at all).
|
|
104
115
|
*/
|
|
105
116
|
exitWatchMs?: number;
|
|
117
|
+
/** `agents.runtimes.<id>.bin` (default: user.yml's; undefined: unset). */
|
|
118
|
+
runtimeBin?: (id: string) => unknown;
|
|
119
|
+
/** The user.yml a person edits to set it (default: config/local/user.yml). */
|
|
120
|
+
userConfigFile?: () => string;
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
export interface AgentsResolved extends Resolved {
|
|
@@ -111,6 +126,8 @@ export interface AgentsResolved extends Resolved {
|
|
|
111
126
|
workspaceDir: (ws: string) => string;
|
|
112
127
|
mcpServer: () => { command: string[] | string; env: Record<string, string> };
|
|
113
128
|
exitWatchMs: number;
|
|
129
|
+
runtimeBin: (id: string) => unknown;
|
|
130
|
+
userConfigFile: () => string;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
/** The early-exit watch: at most this long, polling the host. */
|
|
@@ -199,6 +216,8 @@ export function resolveAgentsDeps(d: AgentsDeps): AgentsResolved {
|
|
|
199
216
|
}),
|
|
200
217
|
mcpServer: d.mcpServer ?? (() => defaultMcpServer(base.cli)),
|
|
201
218
|
exitWatchMs: d.exitWatchMs ?? EXIT_WATCH_MS,
|
|
219
|
+
runtimeBin: d.runtimeBin ?? ((id) => configuredRuntimeBin(id)),
|
|
220
|
+
userConfigFile: d.userConfigFile ?? userConfigPath,
|
|
202
221
|
};
|
|
203
222
|
}
|
|
204
223
|
|
|
@@ -394,29 +413,84 @@ export function listPersonas(d: AgentsResolved): ListedPersona[] {
|
|
|
394
413
|
|
|
395
414
|
export interface RuntimeChoice {
|
|
396
415
|
id: string;
|
|
416
|
+
/** Its name for people ("Claude Code"). */
|
|
417
|
+
label?: string;
|
|
397
418
|
available: boolean;
|
|
398
419
|
/** Why it is unavailable. */
|
|
399
420
|
why?: string;
|
|
400
421
|
version?: string;
|
|
422
|
+
/** The CLI it runs (AG-0.2). */
|
|
423
|
+
path?: string;
|
|
424
|
+
/** Where that path came from: setting, login-shell, well-known or PATH. */
|
|
425
|
+
source?: BinSource;
|
|
426
|
+
/** The directories searched, in order (a runtime with a CLI to find). */
|
|
427
|
+
searched?: string[];
|
|
428
|
+
/** The user.yml key that sets its path (a runtime with a CLI to find). */
|
|
429
|
+
setting?: string;
|
|
401
430
|
/** It declares resume. */
|
|
402
431
|
resume: boolean;
|
|
403
432
|
default: boolean;
|
|
404
433
|
}
|
|
405
434
|
|
|
406
|
-
/**
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Whether runtime `rt` can run here (AG-0.1): its setting
|
|
437
|
+
* (`agents.runtimes.<id>.bin`, from d.runtimeBin), else the search. Runs
|
|
438
|
+
* nothing.
|
|
439
|
+
*/
|
|
440
|
+
export function detectRuntime(d: AgentsResolved, rt: Runtime): RuntimeDetect {
|
|
441
|
+
return rt.detect(d.env, { bin: d.runtimeBin(rt.id) ?? null });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Every registered runtime, and whether it can run here, with the path it
|
|
446
|
+
* runs and its version: its version command (`claude --version`), run once
|
|
447
|
+
* under a timeout and kept, else what its install says.
|
|
448
|
+
*/
|
|
449
|
+
export async function listRuntimes(
|
|
450
|
+
d: AgentsResolved,
|
|
451
|
+
): Promise<RuntimeChoice[]> {
|
|
452
|
+
return Promise.all(
|
|
453
|
+
runtimeIds().map(async (id) => {
|
|
454
|
+
const rt = getRuntime(id)!;
|
|
455
|
+
const det = detectRuntime(d, rt);
|
|
456
|
+
const asked =
|
|
457
|
+
det.available && det.path && rt.find?.versionArgs
|
|
458
|
+
? await runtimeVersion(det.path, rt.find.versionArgs, d.env)
|
|
459
|
+
: undefined;
|
|
460
|
+
const version = asked ?? det.version;
|
|
461
|
+
return {
|
|
462
|
+
id,
|
|
463
|
+
...(rt.label ? { label: rt.label } : {}),
|
|
464
|
+
available: det.available,
|
|
465
|
+
...(det.available ? {} : { why: det.why ?? "unavailable" }),
|
|
466
|
+
...(version ? { version } : {}),
|
|
467
|
+
...(det.path ? { path: det.path } : {}),
|
|
468
|
+
...(det.source ? { source: det.source } : {}),
|
|
469
|
+
...(rt.find
|
|
470
|
+
? { searched: det.searched ?? [], setting: binSettingKey(id) }
|
|
471
|
+
: {}),
|
|
472
|
+
resume: Boolean(rt.resume),
|
|
473
|
+
default: id === DEFAULT_RUNTIME_ID,
|
|
474
|
+
};
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Refused unless `rt` can run here; its detect result when it can. */
|
|
480
|
+
function availableOrRefuse(d: AgentsResolved, rt: Runtime): RuntimeDetect {
|
|
481
|
+
const det = detectRuntime(d, rt);
|
|
482
|
+
if (!det.available)
|
|
483
|
+
throw refused(
|
|
484
|
+
`${rt.id} is unavailable: ${det.why ?? "unknown"}`,
|
|
485
|
+
rt.find
|
|
486
|
+
? [
|
|
487
|
+
`Install it, or set ${binSettingKey(rt.id)} to its absolute path in ${d.userConfigFile()}.`,
|
|
488
|
+
`\`${d.cli} agents doctor\` shows where it looked.`,
|
|
489
|
+
]
|
|
490
|
+
: [],
|
|
491
|
+
503,
|
|
492
|
+
);
|
|
493
|
+
return det;
|
|
420
494
|
}
|
|
421
495
|
|
|
422
496
|
function runtimeFor(id: string): Runtime {
|
|
@@ -460,6 +534,15 @@ interface Launch {
|
|
|
460
534
|
passthrough?: readonly string[];
|
|
461
535
|
/** agents.<id>.grants, recorded with its actor token. */
|
|
462
536
|
grants: readonly string[];
|
|
537
|
+
/** Added to the session's PATH: a configured CLI's own directory. */
|
|
538
|
+
binDir?: string;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** A configured CLI's directory, for its session's PATH (its own tools). */
|
|
542
|
+
function binDirOf(det: RuntimeDetect): { binDir?: string } {
|
|
543
|
+
return det.source === "setting" && det.path
|
|
544
|
+
? { binDir: path.dirname(det.path) }
|
|
545
|
+
: {};
|
|
463
546
|
}
|
|
464
547
|
|
|
465
548
|
/** The host's cap refusal as an AgentError (its hint says what to do). */
|
|
@@ -511,6 +594,7 @@ async function startSession(
|
|
|
511
594
|
launch.env,
|
|
512
595
|
launch.rec.id,
|
|
513
596
|
minted.token,
|
|
597
|
+
launch.binDir ? [launch.binDir] : [],
|
|
514
598
|
),
|
|
515
599
|
cols: (tty && d.stdout.columns) || 80,
|
|
516
600
|
rows: (tty && d.stdout.rows) || 24,
|
|
@@ -601,6 +685,7 @@ function launchInput(
|
|
|
601
685
|
ws: string,
|
|
602
686
|
id: string,
|
|
603
687
|
model: string | undefined,
|
|
688
|
+
bin?: string,
|
|
604
689
|
): LaunchInput {
|
|
605
690
|
const grants = cfg.agents[id]?.grants ?? [];
|
|
606
691
|
for (const g of grants)
|
|
@@ -623,6 +708,7 @@ function launchInput(
|
|
|
623
708
|
filesDir: agentFilesDir(d.stateDir(), id),
|
|
624
709
|
stateDir: d.stateDir(),
|
|
625
710
|
env: d.env,
|
|
711
|
+
...(bin ? { bin } : {}),
|
|
626
712
|
};
|
|
627
713
|
}
|
|
628
714
|
|
|
@@ -681,9 +767,7 @@ export async function startAgent(
|
|
|
681
767
|
const id = req.id ?? defaultId(d.stateDir(), persona.id, ws);
|
|
682
768
|
checkId(where, id);
|
|
683
769
|
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);
|
|
770
|
+
const det = availableOrRefuse(d, rt);
|
|
687
771
|
const model = req.model ?? persona.model;
|
|
688
772
|
|
|
689
773
|
const prev = readRecord(d.stateDir(), id);
|
|
@@ -692,7 +776,7 @@ export async function startAgent(
|
|
|
692
776
|
`agent ${id} belongs to workspace ${prev.workspace}; give this one another --id`,
|
|
693
777
|
);
|
|
694
778
|
ptyOrRefuse(d);
|
|
695
|
-
const input = launchInput(d, cfg, persona, ws, id, model);
|
|
779
|
+
const input = launchInput(d, cfg, persona, ws, id, model, det.path);
|
|
696
780
|
|
|
697
781
|
return withClient(d, async (c) => {
|
|
698
782
|
const sessions = await c.list();
|
|
@@ -721,6 +805,7 @@ export async function startAgent(
|
|
|
721
805
|
env: out.env,
|
|
722
806
|
passthrough: rt.envPassthrough,
|
|
723
807
|
grants: cfg.agents[id]?.grants ?? [],
|
|
808
|
+
...binDirOf(det),
|
|
724
809
|
},
|
|
725
810
|
old,
|
|
726
811
|
{},
|
|
@@ -795,6 +880,7 @@ export async function resumeAgent(
|
|
|
795
880
|
const persona = personas(d, cfg).get(rec.persona);
|
|
796
881
|
if (!persona)
|
|
797
882
|
throw refused(`agent ${rec.id}'s persona "${rec.persona}" is gone`);
|
|
883
|
+
const det = availableOrRefuse(d, rt);
|
|
798
884
|
ptyOrRefuse(d);
|
|
799
885
|
return withClient(d, async (c) => {
|
|
800
886
|
const old = sessionOf(await c.list(), rec);
|
|
@@ -809,6 +895,7 @@ export async function resumeAgent(
|
|
|
809
895
|
rec.workspace,
|
|
810
896
|
rec.id,
|
|
811
897
|
rec.model,
|
|
898
|
+
det.path,
|
|
812
899
|
);
|
|
813
900
|
// Resume in the cwd the agent had, wherever the workspace points now.
|
|
814
901
|
const at = {
|
|
@@ -833,6 +920,7 @@ export async function resumeAgent(
|
|
|
833
920
|
env: out.env,
|
|
834
921
|
passthrough: rt.envPassthrough,
|
|
835
922
|
grants: cfg.agents[rec.id]?.grants ?? [],
|
|
923
|
+
...binDirOf(det),
|
|
836
924
|
},
|
|
837
925
|
old,
|
|
838
926
|
fresh ? {} : { resumedAt: new Date().toISOString() },
|
|
@@ -24,16 +24,25 @@
|
|
|
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) each runtime: its status, the CLI it runs and where
|
|
28
|
+
* that came from (setting, login-shell, PATH, well-known), its
|
|
29
|
+
* version (its version command, once, under a timeout), the
|
|
30
|
+
* directories searched (names only, never an env value), and the
|
|
31
|
+
* fix when it is missing. Exit 1 when the default runtime is
|
|
32
|
+
* unavailable. It resolves the login shell's PATH first, as the
|
|
33
|
+
* web server does at start.
|
|
27
34
|
*
|
|
28
35
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
29
36
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
30
37
|
*/
|
|
31
38
|
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
39
|
+
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
32
40
|
import { HostError } from "../../shared/sessions/client.js";
|
|
33
41
|
import {
|
|
34
42
|
AgentError,
|
|
35
43
|
listAgents,
|
|
36
44
|
listPersonas as personaList,
|
|
45
|
+
listRuntimes,
|
|
37
46
|
loadAgentWorkspaces,
|
|
38
47
|
resolveAgentsDeps,
|
|
39
48
|
resumeAgent,
|
|
@@ -42,6 +51,7 @@ import {
|
|
|
42
51
|
type AgentsDeps,
|
|
43
52
|
type AgentsResolved,
|
|
44
53
|
type EarlyExit,
|
|
54
|
+
type RuntimeChoice,
|
|
45
55
|
type Started,
|
|
46
56
|
} from "./agent-ops.js";
|
|
47
57
|
import {
|
|
@@ -63,7 +73,7 @@ export {
|
|
|
63
73
|
} from "./agent-ops.js";
|
|
64
74
|
|
|
65
75
|
export const AGENTS_HELP = {
|
|
66
|
-
usage: "agents <list|start|stop|resume|personas> ...",
|
|
76
|
+
usage: "agents <list|start|stop|resume|personas|doctor> ...",
|
|
67
77
|
lines: [
|
|
68
78
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
69
79
|
"outlives the web server (see `{cli} sessions`).",
|
|
@@ -78,9 +88,16 @@ export const AGENTS_HELP = {
|
|
|
78
88
|
" a session never prompted starts fresh under the same conversation id",
|
|
79
89
|
"{cli} agents personas [--json]",
|
|
80
90
|
" Den's default pack, then agents.persona_packs (a later pack wins by id)",
|
|
91
|
+
"{cli} agents doctor [--json]",
|
|
92
|
+
" each runtime: status, the CLI it runs and where it came from, its version,",
|
|
93
|
+
" the directories searched, and the fix; exit 1 when the default is missing",
|
|
81
94
|
"",
|
|
82
|
-
"Runtimes: den-claude (default;
|
|
95
|
+
"Runtimes: den-claude (default; Claude Code's claude) and generic (the",
|
|
83
96
|
"persona's argv template). Attach with `{cli} sessions attach <ws>:<id>`.",
|
|
97
|
+
"A runtime's CLI is found as your terminal finds it: agents.runtimes.<id>.bin",
|
|
98
|
+
"in config/local/user.yml (an absolute path) wins, then the login shell's PATH,",
|
|
99
|
+
"this PATH, and its install directories (~/.local/bin, ~/.claude/local,",
|
|
100
|
+
"/opt/homebrew/bin, /usr/local/bin, npm's global bin).",
|
|
84
101
|
"An agent's env is PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG,",
|
|
85
102
|
"LC_*, TZ and TMPDIR, plus what its runtime declares: nothing else of yours.",
|
|
86
103
|
],
|
|
@@ -250,10 +267,87 @@ async function listPersonas(
|
|
|
250
267
|
return 0;
|
|
251
268
|
}
|
|
252
269
|
|
|
270
|
+
export interface AgentsDoctorDeps {
|
|
271
|
+
/** Resolve the login shell's PATH first (default: initLoginPath). */
|
|
272
|
+
loginPath?: () => Promise<void>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const SOURCE_WORD: Record<string, string> = {
|
|
276
|
+
setting: "agents.runtimes setting",
|
|
277
|
+
"login-shell": "login shell PATH",
|
|
278
|
+
PATH: "PATH",
|
|
279
|
+
"well-known": "install directory",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/** `agents doctor`'s lines for one runtime (names and paths, never env values). */
|
|
283
|
+
export function doctorLines(
|
|
284
|
+
r: RuntimeChoice,
|
|
285
|
+
cli: string,
|
|
286
|
+
userConfig: string,
|
|
287
|
+
): string[] {
|
|
288
|
+
const lines = [
|
|
289
|
+
`${r.id}${r.label ? ` (${r.label})` : ""}${r.default ? " [default]" : ""}`,
|
|
290
|
+
r.available
|
|
291
|
+
? " status ✓ available"
|
|
292
|
+
: ` status ✗ unavailable: ${r.why ?? "unknown"}`,
|
|
293
|
+
];
|
|
294
|
+
if (r.path)
|
|
295
|
+
lines.push(
|
|
296
|
+
` path ${r.path}${r.source ? ` (${SOURCE_WORD[r.source] ?? r.source})` : ""}`,
|
|
297
|
+
);
|
|
298
|
+
if (r.available && r.setting)
|
|
299
|
+
lines.push(` version ${r.version ?? "unknown"}`);
|
|
300
|
+
if (!r.setting) lines.push(" path none to find (the persona's argv)");
|
|
301
|
+
else if (r.searched?.length)
|
|
302
|
+
r.searched.forEach((dir, i) =>
|
|
303
|
+
lines.push(` ${i === 0 ? "searched " : " "}${dir}`),
|
|
304
|
+
);
|
|
305
|
+
else if (r.source !== "setting") lines.push(" searched (no directories)");
|
|
306
|
+
if (!r.available && r.setting)
|
|
307
|
+
lines.push(
|
|
308
|
+
` fix install ${r.label ?? r.id}, or set ${r.setting} to its absolute path in ${userConfig}`,
|
|
309
|
+
` then run \`${cli} agents doctor\` again`,
|
|
310
|
+
);
|
|
311
|
+
return lines;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function doctor(
|
|
315
|
+
args: string[],
|
|
316
|
+
d: AgentsResolved,
|
|
317
|
+
deps: AgentsDoctorDeps,
|
|
318
|
+
): Promise<number> {
|
|
319
|
+
const where = "agents doctor";
|
|
320
|
+
const p = parseArgs(where, args, { "--json": false });
|
|
321
|
+
noExtra(where, p);
|
|
322
|
+
await (deps.loginPath ?? (() => initLoginPath({ env: d.env })))();
|
|
323
|
+
const rts = await listRuntimes(d);
|
|
324
|
+
const def = rts.find((r) => r.default);
|
|
325
|
+
const code = def?.available ? 0 : 1;
|
|
326
|
+
if (p.flags["--json"]) {
|
|
327
|
+
d.out(
|
|
328
|
+
JSON.stringify(
|
|
329
|
+
{ runtimes: rts, userConfig: d.userConfigFile() },
|
|
330
|
+
null,
|
|
331
|
+
2,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
return code;
|
|
335
|
+
}
|
|
336
|
+
rts.forEach((r, i) => {
|
|
337
|
+
if (i) d.out("");
|
|
338
|
+
for (const l of doctorLines(r, d.cli, d.userConfigFile())) d.out(l);
|
|
339
|
+
});
|
|
340
|
+
d.out("");
|
|
341
|
+
d.out(
|
|
342
|
+
`\`${d.cli} doctor --probe\` checks the env each runtime's session gets.`,
|
|
343
|
+
);
|
|
344
|
+
return code;
|
|
345
|
+
}
|
|
346
|
+
|
|
253
347
|
/** `<cli> agents ...`; returns the exit code. */
|
|
254
348
|
export async function cmdAgents(
|
|
255
349
|
args: string[],
|
|
256
|
-
deps: AgentsDeps = {},
|
|
350
|
+
deps: AgentsDeps & AgentsDoctorDeps = {},
|
|
257
351
|
): Promise<number> {
|
|
258
352
|
const d = resolveAgentsDeps(deps);
|
|
259
353
|
const sub =
|
|
@@ -274,6 +368,8 @@ export async function cmdAgents(
|
|
|
274
368
|
return await resume(rest, d);
|
|
275
369
|
case "personas":
|
|
276
370
|
return await listPersonas(rest, d);
|
|
371
|
+
case "doctor":
|
|
372
|
+
return await doctor(rest, d, deps);
|
|
277
373
|
default:
|
|
278
374
|
throw new UsageError(`unknown agents command "${sub}"`);
|
|
279
375
|
}
|
|
@@ -27,7 +27,7 @@ import { cliEntrypoint } from "../../shared/engine.js";
|
|
|
27
27
|
import { getDistRoot, getInvokeCwd, getStateDir } from "../../shared/paths.js";
|
|
28
28
|
import { product } from "../../shared/product.js";
|
|
29
29
|
import { ENV_NAME_RE, isCredentialShaped } from "../../shared/secret-env.js";
|
|
30
|
-
import { allScrubEnv } from "../../shared/agents/runtimes.js";
|
|
30
|
+
import { allScrubEnv, withSessionPath } from "../../shared/agents/runtimes.js";
|
|
31
31
|
import {
|
|
32
32
|
ensureHost,
|
|
33
33
|
connectHost,
|
|
@@ -320,14 +320,19 @@ export function contractInfo(s: SessionInfo): SessionInfo {
|
|
|
320
320
|
// ── subcommands ───────────────────────────────────────────────────────────────
|
|
321
321
|
|
|
322
322
|
/**
|
|
323
|
-
* A plain session's env (AG spec §5a-env): the allowlist
|
|
324
|
-
*
|
|
323
|
+
* A plain session's env (AG spec §5a-env): the allowlist (its PATH with
|
|
324
|
+
* the runtime search's directories, AG-0.1), the names the user kept, then
|
|
325
|
+
* every runtime's scrubEnv. `doctor --probe` checks it.
|
|
325
326
|
*/
|
|
326
327
|
export function plainSessionEnv(
|
|
327
328
|
caller: NodeJS.ProcessEnv,
|
|
328
329
|
keep: readonly string[] = [],
|
|
329
330
|
): Record<string, string> {
|
|
330
|
-
return buildSessionEnv({
|
|
331
|
+
return buildSessionEnv({
|
|
332
|
+
caller: withSessionPath(caller),
|
|
333
|
+
keep,
|
|
334
|
+
scrub: allScrubEnv(),
|
|
335
|
+
});
|
|
331
336
|
}
|
|
332
337
|
|
|
333
338
|
/** Who ran the CLI, for an audit line: a login name, printable. */
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
} from "../../shared/web-session.js";
|
|
34
34
|
import { revokeAllActorTokens } from "../../shared/actor-token.js";
|
|
35
35
|
import { engineBin, engineModuleFile } from "../../shared/engine.js";
|
|
36
|
-
import { readWebPort } from "../../shared/edge/config.js";
|
|
36
|
+
import { readEdgeConfig, readWebPort } from "../../shared/edge/config.js";
|
|
37
|
+
import { webRouteHosts } from "../../shared/edge/render.js";
|
|
37
38
|
import {
|
|
38
39
|
ownServer,
|
|
39
40
|
processCwd,
|
|
@@ -1081,7 +1082,7 @@ export function displayWebHelp(): void {
|
|
|
1081
1082
|
console.log(` ${product().cli} web --setup-browser-access`);
|
|
1082
1083
|
console.log(` ${product().cli} web token --print`);
|
|
1083
1084
|
console.log(
|
|
1084
|
-
` ${product().cli} web login [--next <path>] [--origin <url>] [--port <port>]`,
|
|
1085
|
+
` ${product().cli} web login [--next <path>] [--host <name> | --origin <url>] [--port <port>]`,
|
|
1085
1086
|
);
|
|
1086
1087
|
console.log(` ${product().cli} web sessions [--json] [--port <port>]`);
|
|
1087
1088
|
console.log(
|
|
@@ -1392,20 +1393,24 @@ function takePort(args: string[]): { port: number; rest: string[] } {
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
/**
|
|
1395
|
-
* `<cli> web login [--next <path>] [--origin <url>]`: a fresh sign-in link
|
|
1396
|
+
* `<cli> web login [--next <path>] [--host <name> | --origin <url>]`: a fresh sign-in link
|
|
1396
1397
|
* on stdout, alone (the only place a login code is ever printed); a note on
|
|
1397
|
-
* stderr. `--
|
|
1398
|
-
*
|
|
1398
|
+
* stderr. `--host` for an edge hostname (its URL from the edge config),
|
|
1399
|
+
* `--origin` for any allowed origin (a LAN name), where the cookie must be
|
|
1400
|
+
* set (the signed-out page there says which).
|
|
1399
1401
|
*/
|
|
1400
1402
|
export function cmdWebLogin(args: string[], io: CliIo = stdIo): number {
|
|
1401
|
-
const usage = `Usage: ${product().cli} web login [--next <path>] [--origin <url>] [--port <port>]\n`;
|
|
1403
|
+
const usage = `Usage: ${product().cli} web login [--next <path>] [--host <name> | --origin <url>] [--port <port>]\n`;
|
|
1402
1404
|
if (isHelp(args)) {
|
|
1403
1405
|
io.out(
|
|
1404
1406
|
usage +
|
|
1405
1407
|
" Prints a one-time sign-in link (it works once, within 60 s) for one browser.\n" +
|
|
1406
1408
|
" On localhost nothing asks for a sign-in; it is needed when this server is\n" +
|
|
1407
|
-
" reached
|
|
1409
|
+
" reached from another machine (pass the edge hostname as --host, or a LAN\n" +
|
|
1410
|
+
" address as --origin).\n" +
|
|
1408
1411
|
" --next <path> where the browser lands after signing in (same origin only)\n" +
|
|
1412
|
+
" --host <name> an edge hostname that routes here (an edge domain or alias,\n" +
|
|
1413
|
+
" a VPN name among them): the link uses its edge URL\n" +
|
|
1409
1414
|
" --origin <url> the address the browser uses, http(s)://host[:port]\n",
|
|
1410
1415
|
);
|
|
1411
1416
|
return 0;
|
|
@@ -1430,8 +1435,16 @@ export function cmdWebLogin(args: string[], io: CliIo = stdIo): number {
|
|
|
1430
1435
|
: undefined;
|
|
1431
1436
|
const n = val("--next");
|
|
1432
1437
|
const o = val("--origin");
|
|
1438
|
+
const h = val("--host");
|
|
1433
1439
|
if (n !== undefined) next = n;
|
|
1434
|
-
else if (
|
|
1440
|
+
else if (h !== undefined) {
|
|
1441
|
+
const found = edgeOriginFor(h, port);
|
|
1442
|
+
if (typeof found !== "string") {
|
|
1443
|
+
io.err(found.error);
|
|
1444
|
+
return 2;
|
|
1445
|
+
}
|
|
1446
|
+
origin = found;
|
|
1447
|
+
} else if (o !== undefined) {
|
|
1435
1448
|
origin = parseOrigin(o);
|
|
1436
1449
|
if (!origin) {
|
|
1437
1450
|
io.err(
|
|
@@ -1457,6 +1470,38 @@ export function cmdWebLogin(args: string[], io: CliIo = stdIo): number {
|
|
|
1457
1470
|
return 0;
|
|
1458
1471
|
}
|
|
1459
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* `web login --host <name>`: the origin a browser uses for an edge hostname
|
|
1475
|
+
* that routes to this server (edge.routes under any edge domain or alias),
|
|
1476
|
+
* or why there is none and what to add.
|
|
1477
|
+
*/
|
|
1478
|
+
export function edgeOriginFor(
|
|
1479
|
+
name: string,
|
|
1480
|
+
port: number,
|
|
1481
|
+
): string | { error: string } {
|
|
1482
|
+
const want = name.trim().toLowerCase().replace(/\.$/, "");
|
|
1483
|
+
let routes: { host: string; url: string }[] = [];
|
|
1484
|
+
try {
|
|
1485
|
+
routes = webRouteHosts(readEdgeConfig(), new Set([port, readWebPort()]));
|
|
1486
|
+
} catch {
|
|
1487
|
+
routes = [];
|
|
1488
|
+
}
|
|
1489
|
+
const hit = routes.find((r) => r.host === want);
|
|
1490
|
+
if (hit) return hit.url;
|
|
1491
|
+
const known = routes.map((r) => r.host);
|
|
1492
|
+
return {
|
|
1493
|
+
error:
|
|
1494
|
+
`--host ${name} is not a hostname this server answers to` +
|
|
1495
|
+
(known.length
|
|
1496
|
+
? ` (its edge hostnames: ${known.join(", ")}).\n`
|
|
1497
|
+
: " (no edge route reaches this server).\n") +
|
|
1498
|
+
" Add it to the config's edge block: its domain to edge.aliases (a VPN\n" +
|
|
1499
|
+
" name), or a route to this server in edge.routes, then `edge up`. For a\n" +
|
|
1500
|
+
" LAN name (DEN_BIND_HOST and web.allow_plain_http_remote) pass\n" +
|
|
1501
|
+
" --origin http://<name>:<port> instead.\n",
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1460
1505
|
interface SessionRow {
|
|
1461
1506
|
id: string;
|
|
1462
1507
|
createdAt: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Workspaces page's Agents section (AG-4a, AG spec §5c), its routes:
|
|
3
3
|
*
|
|
4
|
-
* GET /api/personas personas and runtimes
|
|
4
|
+
* GET /api/personas personas and runtimes (each
|
|
5
|
+
* with its path, where it came
|
|
6
|
+
* from and its version: AG-0.2)
|
|
5
7
|
* GET /api/workspaces/<ws>/agents the workspace's agents
|
|
6
8
|
* POST /api/workspaces/<ws>/agents start { persona, runtime?, model?, id? }
|
|
7
9
|
* POST /api/workspaces/<ws>/agents/<id>/stop
|
|
@@ -204,7 +206,12 @@ export async function handleAgentRoutes(
|
|
|
204
206
|
if (!m) {
|
|
205
207
|
if (method !== "GET") return methodNotAllowed(res, "GET");
|
|
206
208
|
try {
|
|
207
|
-
sendJson(res, {
|
|
209
|
+
sendJson(res, {
|
|
210
|
+
personas: listPersonas(d),
|
|
211
|
+
runtimes: await listRuntimes(d),
|
|
212
|
+
// AG-0.2: the file a person edits to set a runtime's path.
|
|
213
|
+
userConfig: d.userConfigFile(),
|
|
214
|
+
});
|
|
208
215
|
} catch (e) {
|
|
209
216
|
sendError(res, e);
|
|
210
217
|
}
|