@monster-orange/den 0.4.3 → 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 +179 -0
- package/README.md +9 -9
- package/docs/distributions.md +215 -65
- 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 +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +57 -3
- 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 +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- 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 +118 -28
- package/src/ui/server/index.ts +77 -6
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- 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 +15 -13
- package/src/ui/server/public/i18n/ja.js +14 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +28 -17
- 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 +225 -0
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +28 -23
- package/src/ui/server/public/topbar.js +166 -206
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +1 -1
- 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/workspace-routes.ts +10 -3
|
@@ -24,8 +24,10 @@
|
|
|
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
|
|
28
|
-
*
|
|
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
|
|
29
31
|
* version (its version command, once, under a timeout), the
|
|
30
32
|
* directories searched (names only, never an env value), and the
|
|
31
33
|
* fix when it is missing. Exit 1 when the default runtime is
|
|
@@ -88,16 +90,20 @@ export const AGENTS_HELP = {
|
|
|
88
90
|
" a session never prompted starts fresh under the same conversation id",
|
|
89
91
|
"{cli} agents personas [--json]",
|
|
90
92
|
" 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
|
|
93
|
-
" the directories searched, and the fix; exit 1 when the
|
|
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",
|
|
94
97
|
"",
|
|
95
|
-
"Runtimes: den-claude (default; Claude Code's claude)
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
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.",
|
|
101
107
|
"An agent's env is PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG,",
|
|
102
108
|
"LC_*, TZ and TMPDIR, plus what its runtime declares: nothing else of yours.",
|
|
103
109
|
],
|
|
@@ -273,10 +279,12 @@ export interface AgentsDoctorDeps {
|
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
const SOURCE_WORD: Record<string, string> = {
|
|
282
|
+
workspace: "the workspace's agents.runtimes setting",
|
|
276
283
|
setting: "agents.runtimes setting",
|
|
277
284
|
"login-shell": "login shell PATH",
|
|
278
285
|
PATH: "PATH",
|
|
279
286
|
"well-known": "install directory",
|
|
287
|
+
"interactive-shell": "interactive shell PATH",
|
|
280
288
|
};
|
|
281
289
|
|
|
282
290
|
/** `agents doctor`'s lines for one runtime (names and paths, never env values). */
|
|
@@ -284,9 +292,11 @@ export function doctorLines(
|
|
|
284
292
|
r: RuntimeChoice,
|
|
285
293
|
cli: string,
|
|
286
294
|
userConfig: string,
|
|
295
|
+
/** Directory lists already printed (a repeat is counted, not listed). */
|
|
296
|
+
printed: string[] = [],
|
|
287
297
|
): string[] {
|
|
288
298
|
const lines = [
|
|
289
|
-
`${r.id}${r.label ? ` (${r.label})` : ""}${r.default ? " [default]" : ""}`,
|
|
299
|
+
`${r.id}${r.label ? ` (${r.label}${r.vendor ? `, ${r.vendor}` : ""})` : ""}${r.default ? " [default]" : ""}${r.denTools === false ? " [Den tools: coming]" : ""}`,
|
|
290
300
|
r.available
|
|
291
301
|
? " status ✓ available"
|
|
292
302
|
: ` status ✗ unavailable: ${r.why ?? "unknown"}`,
|
|
@@ -298,11 +308,19 @@ export function doctorLines(
|
|
|
298
308
|
if (r.available && r.setting)
|
|
299
309
|
lines.push(` version ${r.version ?? "unknown"}`);
|
|
300
310
|
if (!r.setting) lines.push(" path none to find (the persona's argv)");
|
|
301
|
-
else if (r.searched?.length)
|
|
302
|
-
r.searched.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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)");
|
|
306
324
|
if (!r.available && r.setting)
|
|
307
325
|
lines.push(
|
|
308
326
|
` fix install ${r.label ?? r.id}, or set ${r.setting} to its absolute path in ${userConfig}`,
|
|
@@ -317,25 +335,34 @@ async function doctor(
|
|
|
317
335
|
deps: AgentsDoctorDeps,
|
|
318
336
|
): Promise<number> {
|
|
319
337
|
const where = "agents doctor";
|
|
320
|
-
const p = parseArgs(where, args, { "--json": false });
|
|
338
|
+
const p = parseArgs(where, args, { "--workspace": true, "--json": false });
|
|
321
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`);
|
|
322
343
|
await (deps.loginPath ?? (() => initLoginPath({ env: d.env })))();
|
|
323
|
-
const rts = await listRuntimes(d);
|
|
344
|
+
const rts = await listRuntimes(d, ws);
|
|
324
345
|
const def = rts.find((r) => r.default);
|
|
325
346
|
const code = def?.available ? 0 : 1;
|
|
326
347
|
if (p.flags["--json"]) {
|
|
327
348
|
d.out(
|
|
328
349
|
JSON.stringify(
|
|
329
|
-
{
|
|
350
|
+
{
|
|
351
|
+
runtimes: rts,
|
|
352
|
+
...(ws ? { workspace: ws } : {}),
|
|
353
|
+
userConfig: d.userConfigFile(),
|
|
354
|
+
},
|
|
330
355
|
null,
|
|
331
356
|
2,
|
|
332
357
|
),
|
|
333
358
|
);
|
|
334
359
|
return code;
|
|
335
360
|
}
|
|
361
|
+
const printed: string[] = [];
|
|
336
362
|
rts.forEach((r, i) => {
|
|
337
363
|
if (i) d.out("");
|
|
338
|
-
for (const l of doctorLines(r, d.cli, d.userConfigFile()))
|
|
364
|
+
for (const l of doctorLines(r, d.cli, d.userConfigFile(), printed))
|
|
365
|
+
d.out(l);
|
|
339
366
|
});
|
|
340
367
|
d.out("");
|
|
341
368
|
d.out(
|
|
@@ -369,6 +396,8 @@ export async function cmdAgents(
|
|
|
369
396
|
case "personas":
|
|
370
397
|
return await listPersonas(rest, d);
|
|
371
398
|
case "doctor":
|
|
399
|
+
// -w checks the workspace exists, and reads its runtime paths.
|
|
400
|
+
await loadAgentWorkspaces(deps);
|
|
372
401
|
return await doctor(rest, d, deps);
|
|
373
402
|
default:
|
|
374
403
|
throw new UsageError(`unknown agents command "${sub}"`);
|
|
@@ -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)
|
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,
|