@monster-orange/den 0.2.7 → 0.3.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 (139) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +619 -3
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agent-actor.ts +38 -3
  22. package/src/shared/auth/headless-login.ts +1 -1
  23. package/src/shared/auth/sudo-vault.ts +93 -31
  24. package/src/shared/auth/token-providers/index.ts +7 -3
  25. package/src/shared/brand.ts +7 -0
  26. package/src/shared/config-error.ts +63 -0
  27. package/src/shared/constants.ts +3 -24
  28. package/src/shared/den-config.ts +185 -7
  29. package/src/shared/edge/config.ts +147 -21
  30. package/src/shared/edge/exec.ts +31 -0
  31. package/src/shared/edge/included.ts +169 -0
  32. package/src/shared/edge/render.ts +69 -14
  33. package/src/shared/edge/runtime.ts +128 -121
  34. package/src/shared/edge/status.ts +68 -8
  35. package/src/shared/edge/tls.ts +328 -0
  36. package/src/shared/edge/types.ts +25 -9
  37. package/src/shared/environment-manager/catalog.ts +52 -1
  38. package/src/shared/environment-manager/default-workspace.ts +26 -3
  39. package/src/shared/environment-manager/git-remote.ts +49 -0
  40. package/src/shared/environment-manager/health.ts +73 -38
  41. package/src/shared/environment-manager/scope.ts +168 -0
  42. package/src/shared/environment-manager/service-runner.ts +789 -43
  43. package/src/shared/environment-manager/up-targets.ts +132 -0
  44. package/src/shared/environment-manager/workspace-manager.ts +67 -25
  45. package/src/shared/environment-manager/workspace-org.ts +26 -11
  46. package/src/shared/health/rollup.ts +48 -0
  47. package/src/shared/manifest/service-links.ts +20 -8
  48. package/src/shared/open-url.ts +21 -2
  49. package/src/shared/org-sources.ts +328 -0
  50. package/src/shared/org.ts +53 -5
  51. package/src/shared/plugins/manifest.ts +406 -0
  52. package/src/shared/plugins/store.ts +358 -0
  53. package/src/shared/process/legacy-adopt.ts +285 -0
  54. package/src/shared/process/process-group.ts +202 -0
  55. package/src/shared/process/spawn-streaming.ts +7 -0
  56. package/src/shared/secrets/doppler.ts +1 -1
  57. package/src/shared/secrets/index.ts +279 -50
  58. package/src/shared/secrets/none.ts +56 -0
  59. package/src/shared/secrets/strategies.ts +202 -0
  60. package/src/shared/secrets/types.ts +1 -1
  61. package/src/shared/smoke/git-remote.ts +40 -0
  62. package/src/shared/stacks/manifest-commands.ts +1 -1
  63. package/src/shared/stacks/namespace.ts +178 -0
  64. package/src/shared/stacks/owners.ts +303 -0
  65. package/src/shared/stacks/render.ts +8 -3
  66. package/src/shared/stacks/runtime.ts +100 -16
  67. package/src/shared/stacks/status.ts +63 -12
  68. package/src/shared/strategies/registry.ts +89 -0
  69. package/src/shared/theme/contract.ts +538 -0
  70. package/src/shared/types/index.ts +24 -5
  71. package/src/ui/cli/clone-plan.ts +154 -23
  72. package/src/ui/cli/create.ts +24 -7
  73. package/src/ui/cli/doctor-command.ts +131 -0
  74. package/src/ui/cli/edge-command.ts +56 -10
  75. package/src/ui/cli/flag-parser.ts +62 -3
  76. package/src/ui/cli/health-display.ts +25 -0
  77. package/src/ui/cli/index.ts +649 -72
  78. package/src/ui/cli/notice.ts +20 -0
  79. package/src/ui/cli/org-command.ts +12 -2
  80. package/src/ui/cli/plugin-command.ts +849 -0
  81. package/src/ui/cli/stack-command.ts +63 -2
  82. package/src/ui/cli/test-runner.ts +11 -9
  83. package/src/ui/cli/theme-command.ts +136 -0
  84. package/src/ui/cli/up-args.ts +15 -0
  85. package/src/ui/cli/web-command.ts +47 -23
  86. package/src/ui/mcp/api.ts +28 -2
  87. package/src/ui/mcp/scope.ts +298 -0
  88. package/src/ui/mcp/server.ts +70 -8
  89. package/src/ui/mcp/tools.ts +151 -24
  90. package/src/ui/server/appearance-prefs.ts +95 -0
  91. package/src/ui/server/audit.ts +90 -3
  92. package/src/ui/server/daemon-reaper.ts +29 -1
  93. package/src/ui/server/engine-stamp.ts +83 -0
  94. package/src/ui/server/github-service.ts +29 -45
  95. package/src/ui/server/graph-page.ts +20 -0
  96. package/src/ui/server/graph-prefs.ts +44 -1
  97. package/src/ui/server/http-errors.ts +293 -0
  98. package/src/ui/server/http-helpers.ts +22 -3
  99. package/src/ui/server/index.ts +590 -45
  100. package/src/ui/server/public/appearance.js +531 -0
  101. package/src/ui/server/public/edge-page.js +248 -0
  102. package/src/ui/server/public/edge.html +251 -0
  103. package/src/ui/server/public/environments.html +65 -83
  104. package/src/ui/server/public/error-page.js +247 -0
  105. package/src/ui/server/public/error.html +260 -0
  106. package/src/ui/server/public/graph-model.js +332 -14
  107. package/src/ui/server/public/graph.css +141 -47
  108. package/src/ui/server/public/graph.js +223 -44
  109. package/src/ui/server/public/header.js +396 -600
  110. package/src/ui/server/public/health-fit.js +248 -0
  111. package/src/ui/server/public/health-rollup.js +128 -0
  112. package/src/ui/server/public/i18n/es.js +287 -0
  113. package/src/ui/server/public/i18n/ja.js +276 -0
  114. package/src/ui/server/public/index.html +139 -438
  115. package/src/ui/server/public/local-dev.html +3468 -4044
  116. package/src/ui/server/public/logs.html +71 -208
  117. package/src/ui/server/public/panel.js +15 -3
  118. package/src/ui/server/public/release.html +175 -389
  119. package/src/ui/server/public/services.html +596 -770
  120. package/src/ui/server/public/settings-page.js +759 -0
  121. package/src/ui/server/public/settings-registry.js +521 -0
  122. package/src/ui/server/public/settings.html +590 -0
  123. package/src/ui/server/public/shell.js +3869 -0
  124. package/src/ui/server/public/topbar.js +1413 -0
  125. package/src/ui/server/public/version-label.js +12 -0
  126. package/src/ui/server/public/workspace-picker.js +74 -34
  127. package/src/ui/server/readonly-routes.ts +24 -4
  128. package/src/ui/server/release-service.ts +25 -3
  129. package/src/ui/server/services-list.ts +94 -17
  130. package/src/ui/server/settings-distribution.ts +74 -0
  131. package/src/ui/server/shell-prefs.ts +192 -0
  132. package/src/ui/server/theme-routes.ts +292 -0
  133. package/src/ui/server/theme-runtime.ts +407 -0
  134. package/src/ui/server/ui-assets.ts +16 -1
  135. package/src/ui/server/ui-language.ts +50 -0
  136. package/src/ui/server/workspace-routes.ts +55 -9
  137. package/themes/den/den-theme.json +72 -0
  138. package/themes/den/favicon.ico +0 -0
  139. package/themes/den/mark.svg +6 -0
@@ -307,6 +307,102 @@ ones (`⇄ hybrid`).
307
307
  production_envs: [prod] # default: [prod, production]
308
308
  ```
309
309
 
310
+ ## Secrets
311
+
312
+ A distribution needs no secrets store. Den has no store, vault, item,
313
+ field, key or account of its own that a distribution is assumed to share.
314
+ Where a distribution keeps its secrets, if anywhere, is its own config.
315
+
316
+ ### Choosing a backend
317
+
318
+ The backend is a strategy, chosen with `secrets.provider` in the org config
319
+ (per engineer in `config/local/user.yml`, per shell with
320
+ `<PREFIX>_SECRETS_PROVIDER`):
321
+
322
+ | `secrets.provider` | What it is |
323
+ | ------------------ | ---------------------------------------------------------------------------------------- |
324
+ | `none` | No store. Nothing is resolved, no CLI is run; `den create` scaffolds this |
325
+ | `env` | Plain environment variables of the process running Den |
326
+ | `doppler` | The `doppler` CLI (`secrets.doppler: { project, config }` for a default scope) |
327
+ | `1password` | The `op` CLI (`secrets.onepassword.account` for the account, else your own `OP_ACCOUNT`) |
328
+
329
+ ```yaml
330
+ # config/acme.yml
331
+ secrets:
332
+ provider: none # or env | doppler | 1password
333
+ ```
334
+
335
+ When `secrets.provider` is not set at all, Den falls back to `doppler`,
336
+ but only at the moment something actually resolves a secret: a
337
+ distribution with no refs, no `secret_env` and no `required` never runs
338
+ `doppler` and never sees a warning. The first time an unset provider does
339
+ resolve a secret, Den prints, once, that the implicit default is deprecated.
340
+ Set the provider explicitly to silence it.
341
+
342
+ Each backend is registered as a strategy (`src/shared/secrets/strategies.ts`,
343
+ through the generic registry in `src/shared/strategies/registry.ts`) with a
344
+ `detect()`: a file check that never prompts. A backend whose CLI is not on
345
+ PATH reports "not available: The Doppler CLI (doppler) is not on PATH"
346
+ instead of failing a spawn.
347
+
348
+ ### Refs: where Den's own features find their secrets
349
+
350
+ Two Den features can use a secret, and only when the distribution says
351
+ where it is, under `secrets.refs`:
352
+
353
+ | Ref | Feature |
354
+ | -------------------- | ------------------------------------------------------------ |
355
+ | `sudo` | The saved local-dev sudo password (auto-unlock, "Save to …") |
356
+ | `journeys_agent_key` | The journeys agent's API key, when `CURSOR_API_KEY` is unset |
357
+
358
+ ```yaml
359
+ secrets:
360
+ provider: 1password
361
+ onepassword: { account: example.1password.com }
362
+ refs:
363
+ sudo: { vault: Private, item: Dev Keys, field: ACME_LOCAL_ADMIN_PWD }
364
+ journeys_agent_key:
365
+ { vault: Private, item: Dev Keys, field: CURSOR_API_KEY }
366
+ ```
367
+
368
+ ```yaml
369
+ secrets:
370
+ provider: doppler # or env
371
+ refs:
372
+ sudo: { key: ACME_LOCAL_ADMIN_PWD }
373
+ journeys_agent_key: { key: CURSOR_API_KEY }
374
+ ```
375
+
376
+ `1password` needs an `item` and a `field` (`vault` is optional); `doppler`
377
+ and `env` need a `key` (or an env-shaped `field`). There are no defaults:
378
+ a ref that is not set is **not configured**, and its feature says so
379
+ ("not configured: set secrets.provider and secrets.refs.sudo in
380
+ config/acme.yml") instead of running a CLI. The interactive unlock always
381
+ works; only the saved password needs `refs.sudo`.
382
+ `/api/sudo-password/vault-status` reports `backend`, `label`,
383
+ `configured`, `backendAvailable`, `reason` and `hasStored` (`opAvailable`
384
+ is a deprecated alias of `backendAvailable`, kept for one minor).
385
+
386
+ A `1password` distribution with no refs gets one warning, in the web
387
+ server's startup log and in `<cli> validate`, with the YAML to add.
388
+
389
+ ### Setup targets
390
+
391
+ `make <cli>-setup-secrets` runs the selected backend's setup: `doppler
392
+ login` and `doppler setup`, the 1Password developer-keys setup, or a note
393
+ for `env`. With `none`, or no provider set, it says there is nothing to set
394
+ up and exits 0.
395
+
396
+ Backend-only make targets come from `make/secrets-<provider>.mk` and exist
397
+ only when that backend is selected: with `secrets.provider: 1password`,
398
+ `make <cli>-setup-1password` (the developer-keys item at
399
+ `secrets.developer_keys: { vault, item }`, else the vault and item of
400
+ `refs.sudo`) and `make auth-configure env=<env>` (your own admin account
401
+ for staging and production). Any other distribution has neither.
402
+
403
+ `make up` and `make web` check the backend only when `secrets.required:
404
+ true`; otherwise the check passes silently, whatever the provider.
405
+
310
406
  ## Secrets in a target's env
311
407
 
312
408
  `secret_env` puts secrets from the configured secrets provider
@@ -358,6 +454,93 @@ OPENAI_API_KEY (names only; not resolved in a dry run)`.
358
454
  (`[A-Za-z_][A-Za-z0-9_]*`). A `stacks:` entry in the org config takes
359
455
  `secret_env` too.
360
456
 
457
+ ## The edge and TLS
458
+
459
+ The edge is one Traefik per org that routes `<name>.<edge.domain>` (and
460
+ every `edge.aliases` domain) to local services. It exists only when the
461
+ org config has an `edge:` block, which needs `org` and `domain`; Den has
462
+ no default domain. How it gets certificates is a strategy, `edge.tls`:
463
+
464
+ ```yaml
465
+ # config/acme.yml
466
+ edge:
467
+ org: acme
468
+ domain: acme.localhost
469
+ tls: none # http only: no https entrypoint, no cert work, no mkcert
470
+ # tls: { strategy: mkcert } # a local CA, minted per machine
471
+ # tls: { strategy: mkcert, ca: ~/path/to/ca } # an existing mkcert CA
472
+ # tls: # your own certs
473
+ # strategy: files
474
+ # include: edge/dynamic # Traefik dynamic config holding them
475
+ # mounts: { edge/certs: /etc/traefik/dynamic/certs }
476
+ # ca: ~/path/to/ca # optional: the CA that signed them
477
+ ```
478
+
479
+ | Strategy | What the edge does |
480
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
481
+ | `none` | Serves http on `edge.http_port` (80) only. Route URLs are `http://`. No cert, no CA, mkcert is never run. |
482
+ | `mkcert` | Issues its own wildcard cert for every domain, workspace and stack zone. Its CA is its own under the state dir (`~/.local/share/<org>/edge/ca`), or `ca`, an existing CA it never mints or uninstalls. |
483
+ | `files` | Issues nothing: the `include` directory (copied beside the generated config) and `mounts` hold the certs. `ca` only tells health checks which CA to trust. |
484
+
485
+ `tls: <id>` is short for `tls: { strategy: <id> }`. `config/local/user.yml`'s
486
+ `edge.tls` replaces the team's whole. An unknown strategy, or a key the
487
+ strategy does not take, is an error naming the built-ins (`none`, `mkcert`,
488
+ `files`). `secret` (cert and key from the secrets strategy) and `acme` are
489
+ later plugins.
490
+
491
+ - **mkcert is checked before it is used.** It is run from PATH, never
492
+ bundled. With it missing, `validate`, `doctor`, `edge up` and `edge trust`
493
+ say "mkcert is not installed" instead of failing a spawn.
494
+ - **`<cli> edge trust` and `untrust` exist under mkcert.** `edge trust` also
495
+ works under `files` with a `ca` (it installs that existing CA). Elsewhere
496
+ they exit 1: "edge trust needs edge.tls.strategy: mkcert (or files with a
497
+ ca)". `untrust` is
498
+ refused for a shared CA (`ca:`), whose trust other devices rely on.
499
+ - **`<cli> create`** scaffolds `tls: none`, with the other forms in a
500
+ comment: one line opts into https.
501
+ - **Legacy keys** still work, mapped to a strategy, with no change to what
502
+ the edge renders: `issue_cert: false` is `files`, `caroot: <ca>` is
503
+ `mkcert` with `ca`, and an `edge:` block with neither is `mkcert` with
504
+ its own CA, warned once per run ("edge.tls is not set; defaulting to
505
+ mkcert with a local CA. Set edge.tls explicitly.") by `validate`,
506
+ `doctor`, `edge up` and `stack up`. `edge.tls` beside a legacy key is an
507
+ error. See the 0.3.0 Migrations in the changelog.
508
+ - **One edge per `edge.org` per machine.** The edge's container and Compose
509
+ project are named after `edge.org` (`<org>-edge`), whatever its state dir
510
+ (`DEN_EDGE_DIR`). Give a scratch or test root that points `DEN_EDGE_DIR`
511
+ (and `DEN_HOME`) elsewhere an `edge.org` of its own: the name comes from
512
+ the org, so nothing else keeps it apart. The edge container is labelled
513
+ with the state dir that owns it (`den.edge.root`). Edge up, edge down and
514
+ stack up (which brings the edge up first) refuse to touch an `<org>-edge`
515
+ another state dir owns. They start nothing and exit non-zero: "edge
516
+ acme-edge belongs to <its dir>; not replacing it". An unlabelled edge
517
+ (from before 0.3.0-dev.40) is adopted only from the default state dir,
518
+ never under an overriding `DEN_EDGE_DIR`. The only override is
519
+ `edge up --take-over`. (0.3.0-dev.40.)
520
+ - **A recreated edge keeps its routes.** After edge up creates, recreates
521
+ or starts the edge, it rejoins the routed networks of every running stack
522
+ whose routes file (`dynamic/stack-<project>.yml`) is in its state dir,
523
+ one line per network. Stacks that are down, and projects with no routes
524
+ file there, are left alone. (0.3.0-dev.40.)
525
+
526
+ ## doctor
527
+
528
+ `<cli> doctor` lists every strategy kind in the registry (secrets, tls):
529
+ the one this distribution selects, whether nothing chose it (implicit),
530
+ and whether it can work on this machine, with the reason. It only checks
531
+ PATH: it never runs a backend CLI, prompts, or prints a secret. It exits
532
+ 1 when a strategy that will be used cannot work (an implicit secrets
533
+ default is used only when a secret is asked for, so it never fails
534
+ doctor).
535
+
536
+ ```
537
+ Acme doctor: strategies
538
+
539
+ secrets none ✓ available: no secrets backend configured
540
+ tls mkcert (implicit) ✗ unavailable: mkcert is not installed
541
+ ⚠ edge.tls is not set; defaulting to mkcert with a local CA. Set edge.tls explicitly.
542
+ ```
543
+
361
544
  ## den.yml: the product profile
362
545
 
363
546
  Every runtime name derives from `product.name`. Set only what you want to
@@ -421,6 +604,257 @@ brand:
421
604
  Runtime names (env vars, files, directories) always come from `den.yml`,
422
605
  never from the brand.
423
606
 
607
+ ## Plugins
608
+
609
+ A plugin adds to Den without changing it: a theme, a page in the web UI, and
610
+ later other kinds. Its manifest is a `den-plugin.json` that follows the
611
+ [`den.plugin/v1` schema](../schemas/den.plugin.v1.json); a theme can also be a
612
+ bare `den-theme.json`, which Den reads as a plugin that provides one theme
613
+ and asks for nothing.
614
+
615
+ ```jsonc
616
+ {
617
+ "schema": "den.plugin/v1",
618
+ "id": "runbook",
619
+ "name": "Team runbook",
620
+ "version": "1.0.0",
621
+ "license": "MIT", // an SPDX expression, the same rule as a theme's
622
+ "den": ">=0.3.0-0 <0.4.0", // required: the Den versions it runs on
623
+ "maintainer": "platform team",
624
+ "provides": [
625
+ {
626
+ "kind": "page",
627
+ "entry": "page/index.js",
628
+ "nav": { "group": "verify", "label": "nav.runbook" },
629
+ },
630
+ ],
631
+ "capabilities": ["ui.nav", "api.read"],
632
+ }
633
+ ```
634
+
635
+ Every plugin lives in `plugins/<id>/` in the distribution, and gets there in
636
+ one of two ways:
637
+
638
+ | | Local | Added |
639
+ | ------------ | ----------------------------------------------------- | --------------------------------------------------------- |
640
+ | What it is | the distribution's own (its theme, a team page) | someone else's published plugin |
641
+ | How | put it in `plugins/<id>/` and commit it | `<cli> plugin add <repo>[/<subpath>]@<tag\|commit>` |
642
+ | Lock entry | none | `plugins.lock.json`: source, ref, commit, hash, approvals |
643
+ | Integrity | the distribution's own git history and review | `plugin list` fails when its bytes differ from the lock |
644
+ | Capabilities | present is approved: what it declares is what it gets | approved at `add` (`--yes`), kept in the lock |
645
+ | Removing it | delete it in git | `<cli> plugin remove <id>` |
646
+
647
+ ```sh
648
+ <cli> plugin list # every plugin: version, kinds, local or source@ref, lock check
649
+ <cli> plugin validate # every local and added plugin
650
+ <cli> plugin validate runbook # plugins/runbook/ (an id is never a cwd path)
651
+ <cli> plugin validate path/to/dir # one plugin, a distribution, or a repo of them
652
+ <cli> plugin add github.com/acme/den-plugins/runbook@runbook/v1.0.0 --yes
653
+ <cli> plugin update runbook @runbook/v1.1.0
654
+ <cli> plugin remove runbook
655
+ ```
656
+
657
+ - **Every `plugins/<id>/` needs a manifest.** A directory with neither
658
+ `den-plugin.json` nor `den-theme.json` is an error in `list` and
659
+ `validate` ("no den-plugin.json or den-theme.json, so it isn't a
660
+ plugin"), so an id can never collide unseen. So is a directory named
661
+ `den` (the built-in theme's id), a name that is not an id (2–40
662
+ characters: lowercase letters, digits or dashes, starting with a letter),
663
+ and a symlink. `plugin list` prints exactly one row for every directory
664
+ in `plugins/`, even when `plugins.lock.json` can't be read. `theme list`
665
+ shows themes only and doesn't fail on other plugins, but counts the
666
+ plugin dirs with errors and points at `plugin list`.
667
+ - **`den` is the range of Den versions the plugin runs on**, as a semver
668
+ range. Prereleases count, so a range meant to include Den's dev builds
669
+ starts at `-0`: `>=0.3.0-0 <0.4.0` admits 0.3.0-dev.8, while `>=0.3.0`
670
+ does not. `validate` fails a plugin whose range excludes the running Den
671
+ and names both, and on a prerelease Den says when only the `-0` is
672
+ missing.
673
+ - **Load agrees with `validate`.** A theme that fails `validate` for any
674
+ reason (its range, a missing `author` or `maintainer`, a README, a
675
+ capability, contrast) is never offered in the Theme menu and never loads.
676
+ The web UI skips it with one server-log notice and draws with the next
677
+ choice (yours, then `brand.theme`, then `den`); it never fails to start.
678
+ - **What `validate` checks:** the schema; each kind with its own validator
679
+ (a theme as `theme validate` does, a page's entry module on disk); and the
680
+ admission rules: a `maintainer` (for a bare `den-theme.json`, its
681
+ `author`; `theme new` sets it to the brand's name); for a page or server plugin, a README
682
+ (`readme`, default `README.md`) that names its tier and says what it may
683
+ touch and what it must never do (themes are exempt); an SPDX `license`,
684
+ with every bundled font licensed.
685
+ - **Capabilities** come from a closed list: `ui.nav`, `api.read`,
686
+ `api.actions:<id,...>`, `secrets:<ref,...>`, `net:<host,...>`,
687
+ `exec:<binary,...>` and `send:<channel>`. Anything else fails validation.
688
+ - **Commit a local plugin before you trust it.** Den loads what is on disk,
689
+ so it can't tell committed from uncommitted. When a local plugin declares
690
+ `api.actions`, `secrets`, `exec`, `net` or `send` and has files git has not
691
+ committed, `validate` warns, so the change goes through the same review as
692
+ the rest of the distribution. Themes and read-only plugins never warn.
693
+ - **`add`** takes an exact ref: a tag or a full commit SHA (a branch only
694
+ with `--track`, recorded as tracking). It validates the plugin, vendors it
695
+ into `plugins/<id>/` and writes its lock entry, with `kinds` and
696
+ `approved_capabilities` from the manifest. A plugin that asks for
697
+ capabilities needs `--yes`. `add` never replaces a local plugin, and a
698
+ plugin already added from another source must be removed first.
699
+ - **`update <id> @<ref>`** fetches an added plugin again from its locked
700
+ source, and asks again (`--yes`) only when it wants more capabilities than
701
+ were approved. A tracking plugin can be updated without a ref.
702
+ - **A `file://` source is refused.** A path on one machine must never reach
703
+ a committed lock: for your own plugin, put it in `plugins/<id>/` as a local
704
+ plugin; to try a published one before tagging, push a branch and use
705
+ `--track`.
706
+
707
+ ### Where plugins may come from
708
+
709
+ `add` fetches from any git host unless `den.yml` (the product profile at the
710
+ distribution root) lists the ones it allows:
711
+
712
+ ```yaml
713
+ # den.yml
714
+ product:
715
+ name: acme
716
+ plugins:
717
+ allow:
718
+ - github.com/acme # <host>/<owner>
719
+ - git.acme.example # a bare host: every owner there
720
+ ```
721
+
722
+ A source outside the list is refused before anything is fetched, and a value
723
+ that is not a list of strings is an error rather than "allow everything".
724
+
725
+ ## Themes
726
+
727
+ A theme is the web UI's look and feel: color tokens for dark and light, type,
728
+ shape and a mark, in a `den-theme.json` that follows the
729
+ [`den.theme/v1` schema](../schemas/den.theme.v1.json). Den ships its own theme
730
+ (`den`). Others live in git repos; Monster-Orange/den-themes is the public set
731
+ and the guide to making your own
732
+ (<https://github.com/Monster-Orange/den-themes>).
733
+
734
+ ```sh
735
+ <cli> theme add github.com/Monster-Orange/den-themes/themes/paper@paper/v1.0.0
736
+ <cli> theme list
737
+ <cli> theme validate path/to/my-theme
738
+ <cli> theme remove paper
739
+ ```
740
+
741
+ - `add` takes an exact ref: a tag or a full commit SHA. A branch is refused
742
+ unless `--track` (for theme authors), which the lock records as tracking.
743
+ - It validates the theme (schema, contrast in both modes, files) and copies it
744
+ into the distribution's `plugins/<id>/`, with an entry in `plugins.lock.json`
745
+ (kinds `["theme"]`, source, ref, commit, content hash). A theme is a
746
+ plugin, so `theme add | list | validate | remove` are `plugin` filtered to
747
+ kind theme (see [Plugins](#plugins)): the same lock and directory, the
748
+ `file://` refusal and `plugins.allow`. Commit both: a fresh clone has the
749
+ theme offline, and theme changes show up as reviewable diffs.
750
+ - `list` checks every theme against its lock and fails if one has changed.
751
+ - A distribution can also keep its own theme in the repo: `<cli> theme new
752
+ <id>` copies the built-in theme into `plugins/<id>/` to edit (or put one
753
+ there by hand), and committing it gives everyone on the repo the theme.
754
+ It is a local plugin: publishing it to a themes repo is only for sharing
755
+ it with other teams. `list` shows it as `local` and `validate` with no
756
+ argument checks it along with the added ones; `add` never replaces it.
757
+ - Contrast: every text token (`text`, `muted`, `faint`, `accent-text`, and
758
+ the status colors) at 4.5:1 on every ground (`bg`, `surface`, `raised`,
759
+ `sunken`); `accent` and `border-strong` at 3:1; `on-accent` on
760
+ `accent-fill` at 4.5:1. `accent` is never text: Den draws links and accent
761
+ text in `accent-text`.
762
+ - The mark is one image or text: `svg` (with `svg_dark`), `png` (with
763
+ `png_dark`) or `webp` (with `webp_dark`), or `text` (up to 3 characters).
764
+ A mark that gives two image kinds fails validate, naming both. Use a
765
+ raster only for a mark that exists only as one (one that must not be
766
+ vectorised): `validate` checks that the file is what its extension says
767
+ (the PNG signature, or `RIFF….WEBP`), is at most 64 KB, and is at least
768
+ 44×44 px (twice the 22px mark in the top bar). An SVG mark may reference
769
+ nothing outside itself (every `href` and `url()` starts with `#`).
770
+ - `favicon` (optional): the browser-tab icon, a `.png`, `.svg` or `.ico`
771
+ file in the theme, at most 64 KB, its format checked by its first bytes.
772
+
773
+ ```json
774
+ "mark": { "png": "mark.png", "png_dark": "mark-dark.png", "shape": "circle" },
775
+ "favicon": "favicon.png"
776
+ ```
777
+
778
+ ### The active theme
779
+
780
+ The web UI draws every color, font and radius from the active theme. Which
781
+ one is active:
782
+
783
+ 1. the engineer's own choice (`appearance.theme` in `config/local/user.yml`,
784
+ set from the header's theme menu);
785
+ 2. else the distribution's default, `brand.theme` in the org config;
786
+ 3. else `den`.
787
+
788
+ ```yaml
789
+ # config/<name>.yml
790
+ brand:
791
+ theme: paper # an id in plugins/<id>/ (locked or local), or den
792
+ ```
793
+
794
+ An id that is not installed, or no longer validates under this Den, is
795
+ skipped for the next choice, ending at `den`, with one warning in the server
796
+ log; the UI never fails over a theme.
797
+
798
+ Appearance is a per-engineer preference, stored with the other settings
799
+ (`/api/settings`, `appearance`):
800
+
801
+ ```yaml
802
+ # config/local/user.yml (gitignored)
803
+ appearance:
804
+ theme: paper # omit to follow the distribution
805
+ mode: light # system (default) | dark | light
806
+ ```
807
+
808
+ The Theme menu's first entry is "Org default (<name>)": it stores no theme
809
+ (`appearance.theme` null, the same as omitting it), so you follow
810
+ `brand.theme`, and a later `brand.theme` change reaches you. Picking a theme
811
+ by name stores its id, even the theme that is the org default today: that
812
+ pins it, and the menu shows it as "<name> (pinned)" rather than "Org
813
+ default".
814
+
815
+ `system` follows the OS (`prefers-color-scheme`), and the theme's
816
+ `default_mode` when the OS gives none. The header's Mode and Theme menus save
817
+ here and apply at once, without a reload. Changing either writes only that
818
+ block: no edge route, edge config, service or the web service process is
819
+ touched.
820
+
821
+ What the server serves:
822
+
823
+ - `/theme.css`: the active theme as CSS custom properties
824
+ (`:root[data-mode="dark"] { --bg: …; }` and the light block, plus `--sans`,
825
+ `--mono`, `--display`, `--ja`, `--radius`, `--radius-button` and
826
+ `@font-face` for the theme's fonts). Only a validated manifest is rendered.
827
+ - `/theme/<id>/<file>`: only the files the manifest names (the mark's
828
+ `.svg`, `.png` or `.webp`, the favicon, the `.woff2` fonts), from that
829
+ theme's directory, each with its own type (`image/svg+xml`, `image/png`,
830
+ `image/webp`, `image/x-icon`, `font/woff2`).
831
+ - `/theme-favicon/<id>?mode=light|dark&v=<version>-<hash>`: the theme's
832
+ browser-tab icon in that mode: its `favicon` in both modes, else its
833
+ mark's file (the dark variant in dark mode, if it has one, else the base
834
+ file), else (a text mark) Den's own icon. `/theme-favicon` with no id is
835
+ the active theme's, never cached.
836
+ - `/api/themes`: every installed theme (id, name, version, `built-in` |
837
+ `locked` | `local`, ok), the active one (with its `mark` and its
838
+ `favicon` URL for each mode) and `org_default` (the theme no choice
839
+ draws: `brand.theme`, else den).
840
+
841
+ Each page sets `data-mode` on `<html>` before first paint, and its styles use
842
+ only the tokens: a page or plugin that draws in the UI uses `var(--token)`,
843
+ never a color literal (`accent` is never text: `accent-text` is), and only
844
+ `var(--sans)`, `var(--mono)` or `var(--display)` for a font (the fallbacks
845
+ are in `/theme.css`: a stack with no generic family gets Den's after it).
846
+ The header shows the active theme's mark: its SVG, PNG or WebP (the dark
847
+ variant in dark mode, if it has one) as an image at 22px, a raster clipped
848
+ to the mark's `shape`, or its `text` drawn by Den in `shape` on
849
+ `accent-fill`. The browser tab shows the theme's icon for the mode the page
850
+ draws in: the shell sets it (no page links one) and changes it with the
851
+ theme or the mode, System following the OS included. Each URL carries the
852
+ theme's id, version, mode and a hash of the file, so a switch is never
853
+ answered from cache.
854
+ A color from the org config (`orgs.<id>.color`) is only ever a swatch,
855
+ border or dot, never text or a ground: labels are `--text` or `--muted`.
856
+ Links in body content are underlined, so a link never reads by color alone.
857
+
424
858
  ## Workspace blueprint
425
859
 
426
860
  The workspaces a new machine starts with. Read from `defaults:` in the org
@@ -452,7 +886,8 @@ defaults:
452
886
  focus: true
453
887
  ```
454
888
 
455
- Each entry takes `repos`, `branch`, `autostart_off` and `org`, the same
889
+ Each entry takes `repos`, `branch`, `autostart_off`, `org` and
890
+ `stack_namespace` (see [Stack namespaces](#stack-namespaces)), the same
456
891
  fields as `defaults.workspace`, plus `focus`. Seeding creates every named workspace
457
892
  with its repos, activates them all, and focuses the one with `focus: true`
458
893
  (else the first). Set `defaults.workspace` or `defaults.workspaces`, not both:
@@ -473,8 +908,14 @@ To see what a setup would clone before it clones anything, run
473
908
  `acme setup --plan`: each repo with its org and clone URL, and whether it
474
909
  is cloned yet. On a fresh machine it plans the blueprint (every workspace
475
910
  `acme workspace seed` would create); once seeded, the focused workspace.
476
- `acme up --plan` lists the same lines for the repos it would start. Both
477
- are read-only: nothing is cloned, seeded or written.
911
+ `-w <workspace>` (repeatable) plans those workspaces instead, active or not,
912
+ and `--all-active` every active one; on a fresh machine `-w` narrows the
913
+ blueprint's plan. Repos that only workspaces outside the plan hold are named
914
+ in a closing hint. A dev-managed repo shows the branch its checkout has out
915
+ (read from its `.git/HEAD`), not a configured one. `acme up --plan` lists
916
+ the same lines for the repos it would start, and exits 1 when nothing
917
+ resolves (a name no workspace has, an empty workspace). Both are read-only:
918
+ nothing is cloned, seeded or written.
478
919
 
479
920
  ## Orgs
480
921
 
@@ -523,6 +964,141 @@ its workspace's org is a warning in both lists, never an error.
523
964
  `org:` in the blueprint needs Den 0.2.7 or later: an older Den rejects the
524
965
  unknown key. Pin 0.2.7 before you commit it.
525
966
 
967
+ ### Sources and repos (0.2.8)
968
+
969
+ An org is Den's own: its id is any key of `orgs:`, and it needs no git
970
+ host. Two optional keys bind it to where its repos live:
971
+
972
+ ```yaml
973
+ github_org: acme
974
+ orgs:
975
+ acme-group: # one org, two GitHub owners
976
+ label: Acme Group
977
+ sources:
978
+ - { host: github, owner: acme }
979
+ - { host: github, owner: acme-labs }
980
+ globex-billing: # one owner split across two orgs
981
+ repos: [globex-invoices]
982
+ globex:
983
+ sources: [{ host: github, owner: globex }]
984
+ initech: # no git host at all
985
+ sources: []
986
+ repos: [widget]
987
+ ```
988
+
989
+ - `orgs.<id>.sources` is a list of `{host, owner}` (or `{host, group}`). A
990
+ source claims that owner's repos for the org. `host: github` is built
991
+ in; any other host id (`gitlab`, …) claims repos whose `remote:` is on
992
+ `<host>.com` or a `<host>.` hostname, and clones them from that remote.
993
+ - `orgs.<id>.repos` lists repos that belong to the org whatever their
994
+ owner.
995
+ - An entry with neither key is what it was before 0.2.8: the GitHub owner
996
+ of its own name.
997
+
998
+ The catalog (`config/local/service-catalog.yml`) takes a `remote:` for any
999
+ host, beside `gh: owner/repo`, which stays as the GitHub shorthand:
1000
+
1001
+ ```yaml
1002
+ widget: { remote: git@git.initech.example:tools/widget.git }
1003
+ ```
1004
+
1005
+ A repo's org is the org whose `repos` lists it, else the org with a source
1006
+ that claims its owner, else its GitHub owner as before. That org is what
1007
+ `/api/orgs?repos=1` maps the repo to and what infers a workspace's org (step
1008
+ 3 above). A claimed owner is not listed as an org of its own, and a
1009
+ `github_org` that names one makes the claiming org the default. Service rows
1010
+ (`/api/services`, `/api/health`) keep `org` as the repo's GitHub owner.
1011
+
1012
+ A repo clones from its source: a GitHub source gives
1013
+ `https://github.com/<owner>/<repo>.git` with your `gh` as credential helper.
1014
+ A repo with no source that claims it clones and pulls from its `remote:`
1015
+ through plain git, with your own git credential helper; Den never hands it
1016
+ `gh` or any credential of its own. A remote must be `https://`, `ssh://`,
1017
+ `git://`, `file://`, `user@host:path` or an absolute path. `setup --plan`
1018
+ shows it as the clone URL, with any user info masked.
1019
+
1020
+ An org with no GitHub source has no repo discovery (`register` lists
1021
+ nothing for it) and its repos are left off the Release page.
1022
+
1023
+ These keys are checked when den.yml is read, and a bad value is an error
1024
+ that names its key (`orgs.initech.sources[0].owner must be …`). A repo
1025
+ listed by two orgs, or an owner claimed by two orgs' sources (or by a
1026
+ source and an org with no `sources` of that name), is an error naming both
1027
+ keys. Such an error stops every command that touches orgs, the catalog,
1028
+ clones or releases (exit 1, before any git call or write), and every web
1029
+ API route that reads the config answers it as 422 `{ "ok": false, "error":
1030
+ "<message>", "code": "config_invalid", "requestId": "<id>" }` (0.3: with
1031
+ the `X-Request-Id` header; a page request gets the error page). Without the new keys nothing
1032
+ changes: `/api/orgs` is byte for byte what 0.2.7 answered. They need Den
1033
+ 0.2.8 or later.
1034
+
1035
+ ## Stack namespaces
1036
+
1037
+ A repo's compose stack runs as the Compose project `<stack>-<workspace>`,
1038
+ and its containers, volumes and networks are named from it. The `default`
1039
+ workspace uses `main` (`shop-main`), so its data predates workspaces. That
1040
+ is the default and needs no config.
1041
+
1042
+ A workspace can opt out with `stack_namespace`, which replaces the suffix:
1043
+
1044
+ ```yaml
1045
+ # config/local/workspaces.yml (this machine)
1046
+ workspaces:
1047
+ storefront:
1048
+ stack_namespace: main # shop runs as shop-main, with default's data
1049
+ services: { shop: { mode: dev-managed, path: ~/src/shop } }
1050
+ ```
1051
+
1052
+ ```yaml
1053
+ # org config: on a blueprint workspace, next to org
1054
+ defaults:
1055
+ workspaces:
1056
+ storefront:
1057
+ repos: [shop-web, shop]
1058
+ stack_namespace: main
1059
+ ```
1060
+
1061
+ A workspace's namespace is the first of `stack_namespace` in
1062
+ workspaces.yml, `stack_namespace` on the blueprint workspace of the same
1063
+ name, and its own name (`default`: `main`). `acme workspace seed` writes
1064
+ the blueprint's value into the workspace it seeds. workspaces.yml
1065
+ overriding a different blueprint value is a warning naming both. The value
1066
+ is lowercase letters, digits and dashes; anything else is a config error
1067
+ that names the key.
1068
+
1069
+ Workspaces with the same namespace share their stacks' projects, and so
1070
+ their data; a stack keeps its data when its repo moves between them. A
1071
+ shared project is one instance, keyed by checkout:
1072
+
1073
+ - Two workspaces bringing up the same stack from the same checkout (repo
1074
+ path and branch) share it. `acme down <repo>` or `acme stack down
1075
+ <stack>` from one, while another active workspace in the namespace still
1076
+ holds it, detaches and leaves it running; the last one stops it.
1077
+ `stack down --force` stops it regardless. A workspace that skips the
1078
+ stack as already healthy counts as holding it too.
1079
+ - Bringing it up from a different checkout or branch is refused
1080
+ (`identity_mismatch`), naming both checkouts and the namespace. Stop it
1081
+ where it runs first, or give the workspace its own `stack_namespace`.
1082
+
1083
+ A shared stack's identity is its checkout path, plus the branch only when
1084
+ the workspace entry pins one. Den does not read the checkout's HEAD, so a
1085
+ `git switch` in that checkout does not refuse its co-owners.
1086
+
1087
+ `acme workspace list` adds a Stacks column when a workspace's namespace is
1088
+ not its default, the Workspaces page shows a "stacks: <ns>" chip in the
1089
+ workspace's header, and `GET /api/workspaces` gives `stackNamespaces`:
1090
+ each workspace's `stack_namespace`, its `source` (`workspace`, `blueprint`
1091
+ or `default`), its `default` and whether it `differs`.
1092
+
1093
+ `DEN_STACK_WORKSPACE=<suffix>` overrides the suffix for one command
1094
+ (`DEN_STACK_WORKSPACE=pr-12 acme stack up shop` runs `shop-pr-12`). It wins
1095
+ over `stack_namespace` and bypasses the shared-project bookkeeping. It is a
1096
+ per-command override, not a migration tool: to keep a workspace on another
1097
+ namespace, set `stack_namespace`.
1098
+
1099
+ `stack_namespace` in the blueprint needs Den 0.3.0-dev.36 or later: an
1100
+ older Den rejects the unknown key.
1101
+
526
1102
  ## Releases
527
1103
 
528
1104
  The Release page dispatches each repo's CD workflow on GitHub. A repo's
@@ -736,6 +1312,46 @@ everyone's checkout is in the same place.
736
1312
  `--port <n>` (on `mcp` or `mcp connect`) points it at a web server on another
737
1313
  port.
738
1314
 
1315
+ ### Scoped to one workspace
1316
+
1317
+ ```sh
1318
+ ./bin/acme mcp --workspace north --actor agent:planner
1319
+ ```
1320
+
1321
+ `--workspace <ws>` and `--actor agent:<id>` (both or neither) scope the server
1322
+ to one workspace for one agent:
1323
+
1324
+ - **Pinned.** A tool that takes a `workspace` gets `<ws>` when none is given.
1325
+ A call naming another workspace is refused (`Refused: <tool>: this MCP
1326
+ server is scoped to workspace "<ws>" (agent:<id>); "<other>" is another
1327
+ workspace`) before anything reaches the web server. A refused call is
1328
+ recorded in the activity trail, read tools included.
1329
+ - **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
1330
+ take only `<ws>`'s services, `manifest_validate` takes only an absolute path
1331
+ inside one of its repos (or the content), and `workspace_sync` syncs `<ws>`.
1332
+ Tests run against the focused workspace, so `tests_available` and
1333
+ `run_tests` are refused unless `<ws>` is the focused one.
1334
+ - **Not workspace data**, served as unscoped: `environments`,
1335
+ `security_latest`, `release_status` and `edge_status` (the org's and the
1336
+ distribution's, not a workspace's).
1337
+ - **Stamped.** Every web API call carries `x-den-mcp-agent: agent:<id>` and
1338
+ `x-den-mcp-workspace: <ws>` beside the usual audit headers. The activity
1339
+ entry gains `agent` and `workspace`, and the log line reads `· by
1340
+ agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
1341
+ <id>)` for every exec, run and sync.
1342
+ - `<ws>` must exist in this distribution; until it does, every tool says so.
1343
+ The id is letters, digits, dots, dashes and underscores (at most 64). A bad
1344
+ or half-given scope is a usage error, exit 2. `--list` and `--call` take
1345
+ the scope too.
1346
+
1347
+ **It binds cooperating agents only.** The scope lives in the MCP server's
1348
+ process. The web server's HTTP API does not check it yet, so an agent that
1349
+ calls the API itself (curl, a browser) is not held to its workspace. Enforcing
1350
+ the scope in the HTTP server is WS-3a, planned for 0.4. `mcp connect` still
1351
+ registers the unscoped server; to hand one agent a scoped server, add it to
1352
+ that agent's config yourself, for example in a project's `.mcp.json`:
1353
+ `"args": ["mcp", "--workspace", "north", "--actor", "agent:planner"]`.
1354
+
739
1355
  ### One call from a shell
740
1356
 
741
1357
  ```sh