@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
|
@@ -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;
|
package/src/ui/mcp/scope.ts
CHANGED
|
@@ -191,6 +191,20 @@ function grantsNote(scope: McpScope): string {
|
|
|
191
191
|
: "no grants";
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* How a person gives `scope`'s agent a grant to `other` (MCP-2.3, AG spec
|
|
196
|
+
* §5b): config in git, then a restart. An agent never grants itself one.
|
|
197
|
+
*/
|
|
198
|
+
export function grantHint(scope: McpScope, other: string): string {
|
|
199
|
+
const id = scope.actor.slice("agent:".length);
|
|
200
|
+
return (
|
|
201
|
+
`To reach it, a person adds "${printable(other)}" to agents.${id}.grants ` +
|
|
202
|
+
`in the org config and restarts the agent (agents start passes --grant); ` +
|
|
203
|
+
"an agent can't grant itself one. For names and status counts of every " +
|
|
204
|
+
"workspace, call org_summary."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
/** The refusal for a call that names `other`; it names the agent's grants. */
|
|
195
209
|
export function otherWorkspace(
|
|
196
210
|
tool: string,
|
|
@@ -199,7 +213,8 @@ export function otherWorkspace(
|
|
|
199
213
|
): ScopeRefusal {
|
|
200
214
|
return new ScopeRefusal(
|
|
201
215
|
`${tool}: this MCP server is scoped to workspace "${scope.workspace}" ` +
|
|
202
|
-
`(${scope.actor}; ${grantsNote(scope)}); "${printable(other)}" is another workspace, so nothing was read or run
|
|
216
|
+
`(${scope.actor}; ${grantsNote(scope)}); "${printable(other)}" is another workspace, so nothing was read or run. ` +
|
|
217
|
+
grantHint(scope, other),
|
|
203
218
|
);
|
|
204
219
|
}
|
|
205
220
|
|
|
@@ -299,8 +314,10 @@ function focusedOf(body: unknown): string | null {
|
|
|
299
314
|
export async function assertScopeExists(
|
|
300
315
|
api: DenApi,
|
|
301
316
|
scope: McpScope,
|
|
317
|
+
/** An /api/workspaces body already read (else it is read now). */
|
|
318
|
+
read?: unknown,
|
|
302
319
|
): Promise<unknown> {
|
|
303
|
-
const body = await api.get("/api/workspaces");
|
|
320
|
+
const body = read ?? (await api.get("/api/workspaces"));
|
|
304
321
|
if (!(scope.workspace in asRecord(asRecord(body).workspaces)))
|
|
305
322
|
throw new ScopeRefusal(
|
|
306
323
|
`this MCP server is scoped to workspace "${scope.workspace}", which does not exist in this distribution.`,
|
|
@@ -375,7 +392,10 @@ export async function assertServicesInScope(
|
|
|
375
392
|
if (outside.length)
|
|
376
393
|
throw new ScopeRefusal(
|
|
377
394
|
`${tool}: ${outside.map(printable).join(", ")} ${outside.length === 1 ? "is" : "are"} not a service ` +
|
|
378
|
-
`of workspace "${scope.workspace}", the only one this MCP server serves; nothing was read
|
|
395
|
+
`of workspace "${scope.workspace}", the only one this MCP server serves; nothing was read. ` +
|
|
396
|
+
"Logs and service details stay inside each workspace (logs can hold " +
|
|
397
|
+
"secrets), and a grant does not widen them; org_summary has every " +
|
|
398
|
+
"workspace's status counts.",
|
|
379
399
|
);
|
|
380
400
|
}
|
|
381
401
|
|
package/src/ui/mcp/server.ts
CHANGED
|
@@ -49,7 +49,9 @@ export function createDenMcpServer(opts: DenMcpOptions): McpServer {
|
|
|
49
49
|
const p = product();
|
|
50
50
|
const session = opts.session ?? randomUUID();
|
|
51
51
|
const log = opts.log ?? ((l: string) => process.stderr.write(`${l}\n`));
|
|
52
|
-
|
|
52
|
+
// Scoped, each workspace tool is registered pinned (MCP-2.2): "this
|
|
53
|
+
// workspace only", and no workspace parameter unless there are grants.
|
|
54
|
+
const tools = toolsFor(opts.cfg, opts.scope);
|
|
53
55
|
const server = new McpServer(
|
|
54
56
|
{ name: p.name, version: engineVersion() ?? "0.0.0" },
|
|
55
57
|
{
|
|
@@ -61,8 +63,11 @@ export function createDenMcpServer(opts: DenMcpOptions): McpServer {
|
|
|
61
63
|
: "Read-only: the org has enabled no actions (mcp.actions).") +
|
|
62
64
|
(opts.scope
|
|
63
65
|
? ` Scoped to workspace "${opts.scope.workspace}" as ${opts.scope.actor}: ` +
|
|
64
|
-
"a call naming another workspace is refused, and lists show this one only
|
|
65
|
-
|
|
66
|
+
"a call naming another workspace is refused, and lists show this one only, " +
|
|
67
|
+
"each with a `scope` block saying how much is hidden. org_summary has " +
|
|
68
|
+
"every workspace and repo (names and status counts), the graph, the " +
|
|
69
|
+
"edge and the environments: start there for the whole picture."
|
|
70
|
+
: " org_summary has the whole org at a glance: start there."),
|
|
66
71
|
},
|
|
67
72
|
);
|
|
68
73
|
|
|
@@ -222,8 +227,10 @@ export async function runOneShot(
|
|
|
222
227
|
opts: OneShotOptions,
|
|
223
228
|
): Promise<number> {
|
|
224
229
|
const out = opts.out ?? ((t: string) => process.stdout.write(t));
|
|
230
|
+
// The tools exactly as the stdio server registers them: pinned when scoped.
|
|
231
|
+
const tools = toolsFor(opts.cfg, opts.scope);
|
|
225
232
|
if (shot.kind === "list") {
|
|
226
|
-
const names =
|
|
233
|
+
const names = tools.map((t) => t.name);
|
|
227
234
|
out(names.length ? `${names.join("\n")}\n` : "");
|
|
228
235
|
return 0;
|
|
229
236
|
}
|
|
@@ -235,11 +242,18 @@ export async function runOneShot(
|
|
|
235
242
|
};
|
|
236
243
|
// The stdio server validates arguments against each tool's schema (the
|
|
237
244
|
// SDK does it); a one-shot must too, and strictly: it is typed by hand, so
|
|
238
|
-
// an unknown or misspelt key is an error, not silently dropped.
|
|
245
|
+
// an unknown or misspelt key is an error, not silently dropped. Scoped,
|
|
246
|
+
// the schema is the pinned one; a `workspace` it dropped (no grants) is
|
|
247
|
+
// still taken, so naming another workspace gets Den's refusal (callTool),
|
|
248
|
+
// never a silent answer about the scope's own.
|
|
239
249
|
let args = shot.args;
|
|
240
|
-
const tool =
|
|
250
|
+
const tool = tools.find((t) => t.name === shot.tool);
|
|
241
251
|
if (tool) {
|
|
242
|
-
const
|
|
252
|
+
const shape =
|
|
253
|
+
opts.scope && !("workspace" in tool.shape) && "workspace" in args
|
|
254
|
+
? { ...tool.shape, workspace: z.string().optional() }
|
|
255
|
+
: tool.shape;
|
|
256
|
+
const parsed = z.object(shape).strict().safeParse(args);
|
|
243
257
|
if (!parsed.success) {
|
|
244
258
|
const err = opts.err ?? ((t: string) => process.stderr.write(t));
|
|
245
259
|
const lines = parsed.error.issues.map((i) => {
|
|
@@ -384,6 +398,16 @@ export async function runMcp(rawArgv: string[]): Promise<void> {
|
|
|
384
398
|
...scopeInfo(scope ?? undefined),
|
|
385
399
|
}),
|
|
386
400
|
});
|
|
401
|
+
// Say the scope on stderr (stdout is the result): a one-shot that ran
|
|
402
|
+
// unscoped must never pass for a scoped one.
|
|
403
|
+
if (scope)
|
|
404
|
+
process.stderr.write(
|
|
405
|
+
`${cli} mcp --call: scoped to ${scope.workspace} as ${scope.actor}` +
|
|
406
|
+
(scope.grants?.length
|
|
407
|
+
? ` · grants: ${scope.grants.join(", ")}`
|
|
408
|
+
: "") +
|
|
409
|
+
"\n",
|
|
410
|
+
);
|
|
387
411
|
process.exitCode = await runOneShot(shot, {
|
|
388
412
|
cfg,
|
|
389
413
|
cli,
|