@monster-orange/den 0.1.0 → 0.2.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +1 -1
  3. package/bin/den +7 -0
  4. package/docs/distributions.md +255 -12
  5. package/package.json +4 -2
  6. package/src/pillars/journeys/sandbox.ts +2 -5
  7. package/src/pillars/security/discover.ts +17 -1
  8. package/src/pillars/security/run.ts +31 -3
  9. package/src/shared/agent-actor.ts +77 -0
  10. package/src/shared/den-config.ts +122 -0
  11. package/src/shared/edge/status.ts +74 -0
  12. package/src/shared/environment-manager/health.ts +54 -8
  13. package/src/shared/environment-manager/load-applications.ts +31 -1
  14. package/src/shared/environment-manager/log-tail.ts +5 -4
  15. package/src/shared/environment-manager/opctl.ts +2 -1
  16. package/src/shared/environment-manager/service-container.ts +47 -0
  17. package/src/shared/environment-manager/service-runner.ts +16 -1
  18. package/src/shared/environment-manager/sync-targets.ts +66 -0
  19. package/src/shared/environment-manager/up-plan.ts +25 -1
  20. package/src/shared/manifest/merge-override.ts +75 -0
  21. package/src/shared/manifest/placement.ts +321 -0
  22. package/src/shared/manifest/read-manifest.ts +156 -38
  23. package/src/shared/manifest/schema.ts +14 -0
  24. package/src/shared/manifest/validate-layers.ts +229 -0
  25. package/src/shared/manifest/validate-manifest.ts +66 -6
  26. package/src/shared/paths.ts +12 -0
  27. package/src/shared/redact.ts +107 -0
  28. package/src/shared/stacks/manifest-commands.ts +30 -0
  29. package/src/shared/stacks/status.ts +99 -0
  30. package/src/shared/types/index.ts +23 -0
  31. package/src/ui/cli/applications-command.ts +34 -2
  32. package/src/ui/cli/edge-command.ts +9 -19
  33. package/src/ui/cli/graph-command.ts +50 -4
  34. package/src/ui/cli/index.ts +131 -45
  35. package/src/ui/cli/mcp-main.ts +13 -0
  36. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  37. package/src/ui/cli/stack-command.ts +13 -40
  38. package/src/ui/cli/test-runner.ts +8 -1
  39. package/src/ui/mcp/api.ts +284 -0
  40. package/src/ui/mcp/guards.ts +141 -0
  41. package/src/ui/mcp/server.ts +147 -0
  42. package/src/ui/mcp/tools.ts +565 -0
  43. package/src/ui/server/audit.ts +71 -0
  44. package/src/ui/server/changelog-parser.ts +11 -0
  45. package/src/ui/server/graph-prefs.ts +89 -0
  46. package/src/ui/server/health-row.ts +15 -0
  47. package/src/ui/server/index.ts +175 -61
  48. package/src/ui/server/log-history.ts +4 -3
  49. package/src/ui/server/public/environments.html +4 -2
  50. package/src/ui/server/public/graph-model.js +416 -0
  51. package/src/ui/server/public/graph.css +976 -0
  52. package/src/ui/server/public/graph.js +986 -0
  53. package/src/ui/server/public/header.js +7 -5
  54. package/src/ui/server/public/local-dev.html +130 -2191
  55. package/src/ui/server/public/logs.html +11 -4
  56. package/src/ui/server/public/panel.js +787 -0
  57. package/src/ui/server/public/services.html +6 -1
  58. package/src/ui/server/readonly-routes.ts +200 -0
  59. package/src/ui/server/ui-assets.ts +73 -0
  60. package/src/ui/server/workspace-routes.ts +39 -8
@@ -101,6 +101,7 @@ import {
101
101
  getReposRoot,
102
102
  mergeUserConfig,
103
103
  readDenConfig,
104
+ isProductionEnv,
104
105
  readUserConfig,
105
106
  } from "../../shared/den-config.js";
106
107
  import { deriveGraph } from "../../shared/environment-manager/derivation/derive-graph.js";
@@ -120,7 +121,7 @@ import {
120
121
  } from "./applications-command.js";
121
122
  import { loadApplications } from "../../shared/environment-manager/load-applications.js";
122
123
  import { hasRuntime, repoKind } from "../../shared/repo-kind.js";
123
- import type { RepoKind } from "../../shared/types/index.js";
124
+ import type { RepoKind, ValidationResult } from "../../shared/types/index.js";
124
125
  import { localCommandsFor } from "../../shared/stacks/manifest-commands.js";
125
126
  import {
126
127
  buildUpPlan,
@@ -131,7 +132,15 @@ import * as fs from "fs";
131
132
  import { readManifest } from "../../shared/manifest/read-manifest.js";
132
133
  import * as nodePath from "path";
133
134
  import { runInit } from "./init.js";
134
- import { validateManifest } from "../../shared/manifest/validate-manifest.js";
135
+ import {
136
+ validateManifest,
137
+ validateManifestLayers,
138
+ } from "../../shared/manifest/validate-manifest.js";
139
+ import {
140
+ placementRefusal,
141
+ placementSuffix,
142
+ servicePlacement,
143
+ } from "../../shared/manifest/placement.js";
135
144
  import {
136
145
  getDistRoot,
137
146
  getEngineRoot,
@@ -290,6 +299,13 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
290
299
  "This org's HTTPS edge: own CA + cert, routes at <name>.<edge.domain>.",
291
300
  ],
292
301
  },
302
+ mcp: {
303
+ usage: "mcp [--port <web port>]",
304
+ lines: [
305
+ "An MCP server over stdio for agents: a client of the running web server",
306
+ `(\`${product().cli} web up\`). Read tools always; actions only as named in mcp.actions.`,
307
+ ],
308
+ },
293
309
  "local-dev": {
294
310
  usage: "local-dev [on|off]",
295
311
  lines: [
@@ -345,6 +361,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
345
361
  "stack",
346
362
  "apps",
347
363
  "applications",
364
+ "mcp",
348
365
  ]);
349
366
 
350
367
  /** Map a raw command token to a PII-safe, registered telemetry command name. */
@@ -362,6 +379,52 @@ function telemetryReason(error: unknown): string {
362
379
  return name && name !== "Object" ? name : "Error";
363
380
  }
364
381
 
382
+ /** Print one manifest's validation: a status line, then each issue. */
383
+ function printValidation(
384
+ label: string,
385
+ result: ValidationResult,
386
+ layer?: string,
387
+ ): void {
388
+ const tag = layer ? `${layer}, ` : "";
389
+ const plural = (n: number, word: string): string =>
390
+ `${n} ${word}${n === 1 ? "" : "s"}`;
391
+ if (result.valid && result.warnings.length === 0) {
392
+ console.log(
393
+ chalk.green(` ✓ ${label}`) +
394
+ chalk.gray(` (${tag}v${result.version}, no issues)`),
395
+ );
396
+ } else if (result.valid) {
397
+ console.log(
398
+ chalk.yellow(` ⚠ ${label}`) +
399
+ chalk.gray(
400
+ ` (${tag}v${result.version}, ${plural(result.warnings.length, "warning")})`,
401
+ ),
402
+ );
403
+ } else {
404
+ console.log(
405
+ chalk.red(` ✗ ${label}`) +
406
+ chalk.gray(
407
+ ` (${tag}v${result.version}, ${plural(result.errors.length, "error")}${result.warnings.length > 0 ? `, ${plural(result.warnings.length, "warning")}` : ""})`,
408
+ ),
409
+ );
410
+ }
411
+
412
+ for (const err of result.errors) {
413
+ const path = err.path ? chalk.gray(err.path) + " " : "";
414
+ console.log(` ${chalk.red("ERROR")} ${path}${err.message}`);
415
+ if (err.suggestion) {
416
+ console.log(` ${chalk.gray("→ " + err.suggestion)}`);
417
+ }
418
+ }
419
+ for (const warn of result.warnings) {
420
+ const path = warn.path ? chalk.gray(warn.path) + " " : "";
421
+ console.log(` ${chalk.yellow("WARNING")} ${path}${warn.message}`);
422
+ if (warn.suggestion) {
423
+ console.log(` ${chalk.gray("→ " + warn.suggestion)}`);
424
+ }
425
+ }
426
+ }
427
+
365
428
  class DenCLI {
366
429
  private static readonly serviceCommands = new Set([
367
430
  "up",
@@ -631,6 +694,15 @@ class DenCLI {
631
694
  cmdApps(args.slice(1));
632
695
  break;
633
696
 
697
+ case "mcp": {
698
+ // bin/den routes `mcp` to its own entry (stdout is the protocol);
699
+ // this covers the CLI run directly from a checkout.
700
+ await import("./mcp-stdout-guard.js");
701
+ const { runMcp } = await import("../mcp/server.js");
702
+ await runMcp(args.slice(1));
703
+ break;
704
+ }
705
+
634
706
  case "--help":
635
707
  case "-h":
636
708
  this.displayHelp();
@@ -1304,6 +1376,25 @@ class DenCLI {
1304
1376
  printPartialState();
1305
1377
  return;
1306
1378
  }
1379
+ // R13: a remote service is never started or stopped from here. Said,
1380
+ // not failed, so a workspace or app up/down carries on past it.
1381
+ const placed = resolveService(address, workspace);
1382
+ const remote = placed
1383
+ ? placementRefusal(
1384
+ address,
1385
+ servicePlacement(
1386
+ readManifest(placed.repoPath),
1387
+ placed.subsystem,
1388
+ "local",
1389
+ ),
1390
+ (e) => isProductionEnv(e),
1391
+ command === "down" ? "stop" : "start",
1392
+ )
1393
+ : null;
1394
+ if (remote) {
1395
+ console.log(chalk.gray(` • ${remote}`));
1396
+ continue;
1397
+ }
1307
1398
  if (command === "up" && !seedAddresses.has(address)) {
1308
1399
  const resolved = resolveService(address, workspace);
1309
1400
  if (resolved) {
@@ -2442,9 +2533,14 @@ class DenCLI {
2442
2533
  for (const r of results) {
2443
2534
  const healthStr = formatHealthLabel(r.health);
2444
2535
  const reasonStr = formatHealthReason(r);
2536
+ const placed = placementSuffix({
2537
+ runs: r.runs ?? "none",
2538
+ target: r.target ?? null,
2539
+ });
2445
2540
 
2446
2541
  table.push([
2447
- this.labelWithAutostart(r.address, noAutostart),
2542
+ this.labelWithAutostart(r.address, noAutostart) +
2543
+ (placed ? chalk.cyan(` ${placed}`) : ""),
2448
2544
  r.mode === "dev-managed"
2449
2545
  ? chalk.blue("dev-managed")
2450
2546
  : chalk.magenta("managed"),
@@ -2606,13 +2702,19 @@ class DenCLI {
2606
2702
  /* ignore */
2607
2703
  }
2608
2704
 
2705
+ const placed = placementSuffix({
2706
+ runs: r.runs ?? "none",
2707
+ target: r.target ?? null,
2708
+ });
2609
2709
  table.push([
2610
- this.labelWithAutostart(r.address, noAutostart),
2710
+ this.labelWithAutostart(r.address, noAutostart) +
2711
+ (placed ? chalk.cyan(` ${placed}`) : ""),
2611
2712
  r.mode === "dev-managed"
2612
2713
  ? chalk.blue("dev-managed")
2613
2714
  : chalk.magenta("managed"),
2614
2715
  healthStr,
2615
- cmds.join(", "),
2716
+ // A remote service's commands are never run from here.
2717
+ placed ? chalk.gray("—") : cmds.join(", "),
2616
2718
  ]);
2617
2719
  }
2618
2720
 
@@ -3415,10 +3517,26 @@ class DenCLI {
3415
3517
  req.on("error", reject);
3416
3518
  });
3417
3519
  } else {
3418
- const filePath = fs.existsSync(target)
3419
- ? fs.lstatSync(target).isDirectory()
3420
- ? nodePath.join(target, `${product().manifestYml}`)
3421
- : target
3520
+ const isDir = fs.existsSync(target) && fs.lstatSync(target).isDirectory();
3521
+ // A repo folder: validate each layer that applies to it (its own or the
3522
+ // held manifest, then manifests/<repo>.override.yml, then the merge).
3523
+ if (isDir) {
3524
+ const layered = validateManifestLayers(nodePath.resolve(target));
3525
+ if (layered.layers.length > 0) {
3526
+ console.log();
3527
+ for (const l of layered.layers)
3528
+ printValidation(
3529
+ chalk.bold(l.file ?? "merged"),
3530
+ l.result,
3531
+ l.layer === "merged" ? "merged" : l.layer,
3532
+ );
3533
+ console.log();
3534
+ if (!layered.valid) process.exit(1);
3535
+ return;
3536
+ }
3537
+ }
3538
+ const filePath = isDir
3539
+ ? nodePath.join(target, `${product().manifestYml}`)
3422
3540
  : target;
3423
3541
 
3424
3542
  if (!fs.existsSync(filePath)) {
@@ -3434,42 +3552,7 @@ class DenCLI {
3434
3552
  );
3435
3553
 
3436
3554
  console.log();
3437
- if (result.valid && result.warnings.length === 0) {
3438
- console.log(
3439
- chalk.green(` ✓ ${label}`) +
3440
- chalk.gray(` (v${result.version}, no issues)`),
3441
- );
3442
- } else if (result.valid) {
3443
- console.log(
3444
- chalk.yellow(` ⚠ ${label}`) +
3445
- chalk.gray(
3446
- ` (v${result.version}, ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"})`,
3447
- ),
3448
- );
3449
- } else {
3450
- console.log(
3451
- chalk.red(` ✗ ${label}`) +
3452
- chalk.gray(
3453
- ` (v${result.version}, ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}${result.warnings.length > 0 ? `, ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}` : ""})`,
3454
- ),
3455
- );
3456
- }
3457
-
3458
- for (const err of result.errors) {
3459
- const path = err.path ? chalk.gray(err.path) + " " : "";
3460
- console.log(` ${chalk.red("ERROR")} ${path}${err.message}`);
3461
- if (err.suggestion) {
3462
- console.log(` ${chalk.gray("→ " + err.suggestion)}`);
3463
- }
3464
- }
3465
- for (const warn of result.warnings) {
3466
- const path = warn.path ? chalk.gray(warn.path) + " " : "";
3467
- console.log(` ${chalk.yellow("WARNING")} ${path}${warn.message}`);
3468
- if (warn.suggestion) {
3469
- console.log(` ${chalk.gray("→ " + warn.suggestion)}`);
3470
- }
3471
- }
3472
-
3555
+ printValidation(label, result);
3473
3556
  console.log();
3474
3557
  if (!result.valid) process.exit(1);
3475
3558
  }
@@ -3559,6 +3642,9 @@ class DenCLI {
3559
3642
  console.log(
3560
3643
  " create <name> [dir] Scaffold a new distribution of your own (--title, --cli, --env-prefix)",
3561
3644
  );
3645
+ console.log(
3646
+ " mcp [--port <n>] MCP server (stdio) for agents, over the running web server",
3647
+ );
3562
3648
  console.log();
3563
3649
  console.log(" workspace list List workspaces");
3564
3650
  console.log(
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Entry point for `<cli> mcp` (bin/den routes `mcp` here). Kept apart from
3
+ * the main CLI so nothing but the MCP protocol is ever written to stdout.
4
+ */
5
+ import "./mcp-stdout-guard.js";
6
+ import { runMcp } from "../mcp/server.js";
7
+
8
+ runMcp(process.argv.slice(2)).catch((error: unknown) => {
9
+ process.stderr.write(
10
+ `mcp: ${error instanceof Error ? error.message : String(error)}\n`,
11
+ );
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Imported first by the `mcp` entry: stdout carries the MCP protocol only,
3
+ * so anything that logs through console goes to stderr instead.
4
+ */
5
+ const toStderr = (...args: unknown[]): void => console.error(...args);
6
+ console.log = toStderr;
7
+ console.info = toStderr;
8
+ console.debug = toStderr;
9
+
10
+ export {};
@@ -6,15 +6,8 @@
6
6
  * https://<route>.<stack>.<edge.domain>.
7
7
  */
8
8
  import chalk from "chalk";
9
- import { readEdgeConfig } from "../../shared/edge/config.js";
10
9
  import { routeUrl } from "../../shared/edge/render.js";
11
10
  import { edgeUp } from "../../shared/edge/runtime.js";
12
- import { readStacks } from "../../shared/stacks/config.js";
13
- import {
14
- getFocusedWorkspaceName,
15
- getWorkspaceByName,
16
- } from "../../shared/environment-manager/workspaces.js";
17
- import { stackWorkspace } from "../../shared/stacks/render.js";
18
11
  import { projectName, startOrder } from "../../shared/stacks/render.js";
19
12
  import {
20
13
  stackDown,
@@ -22,44 +15,26 @@ import {
22
15
  stackLogs,
23
16
  stackPs,
24
17
  stackUp,
25
- type StackContext,
26
18
  } from "../../shared/stacks/runtime.js";
19
+ import { loadStackContext, stackStatuses } from "../../shared/stacks/status.js";
27
20
 
28
- function workspaceFlag(args: string[]): {
29
- fkWs: string;
30
- ws: string;
31
- service?: string;
32
- rest: string[];
33
- } {
21
+ function serviceFlag(args: string[]): { service?: string; rest: string[] } {
34
22
  const rest: string[] = [];
35
- // The global -w/--workspace is parsed (and checked) before we run and
36
- // arrives as DEN_WORKSPACE_OVERRIDE.
37
- const fkWs =
38
- process.env.DEN_WORKSPACE_OVERRIDE ||
39
- getFocusedWorkspaceName() ||
40
- "default";
41
23
  let service: string | undefined;
42
24
  for (let i = 0; i < args.length; i++) {
43
25
  if (args[i] === "--service") service = args[++i];
44
26
  else rest.push(args[i]);
45
27
  }
46
- const ws = process.env.DEN_STACK_WORKSPACE || stackWorkspace(fkWs);
47
- if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(ws))
48
- throw new Error(
49
- `workspace "${ws}" must be lowercase letters, digits and dashes`,
50
- );
51
- return { fkWs, ws, service, rest };
28
+ return { service, rest };
52
29
  }
53
30
 
54
31
  export async function cmdStack(argv: string[]): Promise<void> {
55
- const { fkWs, ws, service, rest } = workspaceFlag(argv);
32
+ const { service, rest } = serviceFlag(argv);
56
33
  const [sub = "ls", ...args] = rest;
57
- const edge = readEdgeConfig();
58
- // Repo manifests come from that den workspace's checkouts.
59
- const stacks = readStacks(undefined, {
60
- workspace: getWorkspaceByName(fkWs) ? fkWs : undefined,
61
- });
62
- const ctx: StackContext = { edge, stacks, workspace: ws };
34
+ // The global -w/--workspace is parsed (and checked) before we run and
35
+ // arrives as DEN_WORKSPACE_OVERRIDE; loadStackContext reads it.
36
+ const ctx = loadStackContext();
37
+ const { edge, stacks } = ctx;
63
38
  const names = args.filter((a) => !a.startsWith("-"));
64
39
  const pick = () => {
65
40
  if (!names.length)
@@ -71,17 +46,15 @@ export async function cmdStack(argv: string[]): Promise<void> {
71
46
 
72
47
  switch (sub) {
73
48
  case "ls":
74
- for (const s of Object.values(stacks)) {
49
+ for (const s of stackStatuses(ctx)) {
75
50
  console.log(
76
51
  chalk.bold(`${s.name}`) +
77
52
  chalk.gray(
78
- ` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${projectName(s.name, ws)}`,
53
+ ` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${s.project}`,
79
54
  ),
80
55
  );
81
- for (const route of Object.keys(s.routes))
82
- console.log(
83
- ` ${routeUrl(`${route}.${s.name}`, edge)} → ${s.routes[route]}`,
84
- );
56
+ for (const route of s.routes)
57
+ console.log(` ${route.url} → ${route.target}`);
85
58
  if (s.uses.length)
86
59
  console.log(chalk.gray(` uses ${s.uses.join(", ")}`));
87
60
  }
@@ -90,7 +63,7 @@ export async function cmdStack(argv: string[]): Promise<void> {
90
63
  // Re-render the edge so its cert covers every stack, then start the stacks.
91
64
  await edgeUp(edge);
92
65
  for (const n of startOrder(pick(), stacks)) {
93
- console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ws)})`));
66
+ console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ctx.workspace)})`));
94
67
  stackUp(ctx, stacks[n], { build: args.includes("--build") });
95
68
  for (const route of Object.keys(stacks[n].routes))
96
69
  console.log(chalk.green(` ✓ ${routeUrl(`${route}.${n}`, edge)}`));
@@ -447,10 +447,16 @@ export class TestRunner {
447
447
  ): CommandStep[] {
448
448
  const names = options.modules ?? [];
449
449
  const script = "src/pillars/security/run.ts";
450
+ // An explicitly chosen stronger mode is passed on; the entrypoint's
451
+ // per-env ceiling refuses it where it isn't allowed.
452
+ const mode =
453
+ options.securityMode && options.securityMode !== "baseline"
454
+ ? [`--mode=${options.securityMode}`]
455
+ : [];
450
456
  if (names.length === 0) {
451
457
  return [
452
458
  {
453
- ...tsxCommand(script, `--env=${options.environment}`),
459
+ ...tsxCommand(script, `--env=${options.environment}`, ...mode),
454
460
  env: baseEnv,
455
461
  label: "Run all security scans",
456
462
  },
@@ -461,6 +467,7 @@ export class TestRunner {
461
467
  script,
462
468
  `--env=${options.environment}`,
463
469
  `--service=${name}`,
470
+ ...mode,
464
471
  ),
465
472
  env: baseEnv,
466
473
  label: `Run security scan ${name}`,