@monster-orange/den 0.2.8 → 0.4.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 (158) hide show
  1. package/CHANGELOG.md +1731 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +730 -6
  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 +12 -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/agents/default-pack/be-dev.md +20 -0
  22. package/src/shared/agents/default-pack/devops.md +14 -0
  23. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  24. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  25. package/src/shared/agents/default-pack/planner.md +17 -0
  26. package/src/shared/agents/default-pack/reviewer.md +14 -0
  27. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  28. package/src/shared/agents/den-claude.ts +173 -0
  29. package/src/shared/agents/generic.ts +52 -0
  30. package/src/shared/agents/personas.ts +218 -0
  31. package/src/shared/agents/records.ts +118 -0
  32. package/src/shared/agents/runtime.ts +192 -0
  33. package/src/shared/agents/runtimes.ts +47 -0
  34. package/src/shared/auth/headless-login.ts +1 -1
  35. package/src/shared/auth/sudo-vault.ts +93 -31
  36. package/src/shared/auth/token-providers/index.ts +7 -3
  37. package/src/shared/brand.ts +7 -0
  38. package/src/shared/config-error.ts +33 -3
  39. package/src/shared/constants.ts +3 -24
  40. package/src/shared/den-config.ts +257 -5
  41. package/src/shared/edge/config.ts +147 -21
  42. package/src/shared/edge/exec.ts +31 -0
  43. package/src/shared/edge/included.ts +169 -0
  44. package/src/shared/edge/render.ts +69 -14
  45. package/src/shared/edge/runtime.ts +128 -121
  46. package/src/shared/edge/status.ts +68 -8
  47. package/src/shared/edge/tls.ts +328 -0
  48. package/src/shared/edge/types.ts +25 -9
  49. package/src/shared/engine.ts +23 -0
  50. package/src/shared/environment-manager/default-workspace.ts +16 -1
  51. package/src/shared/environment-manager/health.ts +73 -38
  52. package/src/shared/environment-manager/scope.ts +168 -0
  53. package/src/shared/environment-manager/service-runner.ts +789 -43
  54. package/src/shared/environment-manager/up-targets.ts +132 -0
  55. package/src/shared/environment-manager/workspaces.ts +19 -0
  56. package/src/shared/health/rollup.ts +48 -0
  57. package/src/shared/open-url.ts +21 -2
  58. package/src/shared/org-sources.ts +78 -59
  59. package/src/shared/org.ts +53 -5
  60. package/src/shared/plugins/manifest.ts +406 -0
  61. package/src/shared/plugins/store.ts +358 -0
  62. package/src/shared/process/legacy-adopt.ts +285 -0
  63. package/src/shared/process/process-group.ts +202 -0
  64. package/src/shared/process/spawn-streaming.ts +7 -0
  65. package/src/shared/secret-env.ts +70 -0
  66. package/src/shared/secrets/doppler.ts +1 -1
  67. package/src/shared/secrets/index.ts +279 -50
  68. package/src/shared/secrets/none.ts +56 -0
  69. package/src/shared/secrets/strategies.ts +202 -0
  70. package/src/shared/secrets/types.ts +1 -1
  71. package/src/shared/sessions/client.ts +277 -0
  72. package/src/shared/sessions/den-pty.ts +97 -0
  73. package/src/shared/sessions/env.ts +79 -0
  74. package/src/shared/sessions/host.ts +667 -0
  75. package/src/shared/sessions/names.ts +85 -0
  76. package/src/shared/sessions/paths.ts +27 -0
  77. package/src/shared/sessions/protocol.ts +72 -0
  78. package/src/shared/sessions/pty.ts +141 -0
  79. package/src/shared/sessions/ring-buffer.ts +160 -0
  80. package/src/shared/sessions/types.ts +81 -0
  81. package/src/shared/smoke/git-remote.ts +40 -0
  82. package/src/shared/stacks/manifest-commands.ts +1 -1
  83. package/src/shared/stacks/namespace.ts +178 -0
  84. package/src/shared/stacks/owners.ts +303 -0
  85. package/src/shared/stacks/render.ts +8 -3
  86. package/src/shared/stacks/runtime.ts +100 -16
  87. package/src/shared/stacks/status.ts +63 -12
  88. package/src/shared/strategies/registry.ts +89 -0
  89. package/src/shared/theme/contract.ts +538 -0
  90. package/src/shared/types/index.ts +12 -3
  91. package/src/ui/cli/agents-command.ts +812 -0
  92. package/src/ui/cli/create.ts +24 -7
  93. package/src/ui/cli/doctor-command.ts +260 -0
  94. package/src/ui/cli/edge-command.ts +56 -10
  95. package/src/ui/cli/env-probe.ts +266 -0
  96. package/src/ui/cli/flag-parser.ts +18 -5
  97. package/src/ui/cli/health-display.ts +25 -0
  98. package/src/ui/cli/index.ts +580 -60
  99. package/src/ui/cli/notice.ts +20 -0
  100. package/src/ui/cli/plugin-command.ts +849 -0
  101. package/src/ui/cli/sessions-command.ts +718 -0
  102. package/src/ui/cli/stack-command.ts +63 -2
  103. package/src/ui/cli/test-runner.ts +11 -9
  104. package/src/ui/cli/theme-command.ts +136 -0
  105. package/src/ui/cli/web-command.ts +47 -23
  106. package/src/ui/mcp/api.ts +28 -2
  107. package/src/ui/mcp/scope.ts +145 -20
  108. package/src/ui/mcp/server.ts +27 -2
  109. package/src/ui/mcp/tools.ts +94 -12
  110. package/src/ui/server/appearance-prefs.ts +95 -0
  111. package/src/ui/server/audit.ts +35 -0
  112. package/src/ui/server/daemon-reaper.ts +29 -1
  113. package/src/ui/server/engine-stamp.ts +83 -0
  114. package/src/ui/server/github-service.ts +23 -44
  115. package/src/ui/server/graph-page.ts +20 -0
  116. package/src/ui/server/graph-prefs.ts +44 -1
  117. package/src/ui/server/http-errors.ts +293 -0
  118. package/src/ui/server/http-helpers.ts +22 -3
  119. package/src/ui/server/index.ts +539 -51
  120. package/src/ui/server/public/appearance.js +531 -0
  121. package/src/ui/server/public/edge-page.js +248 -0
  122. package/src/ui/server/public/edge.html +251 -0
  123. package/src/ui/server/public/environments.html +65 -83
  124. package/src/ui/server/public/error-page.js +247 -0
  125. package/src/ui/server/public/error.html +260 -0
  126. package/src/ui/server/public/graph-model.js +332 -14
  127. package/src/ui/server/public/graph.css +141 -47
  128. package/src/ui/server/public/graph.js +223 -44
  129. package/src/ui/server/public/header.js +396 -600
  130. package/src/ui/server/public/health-fit.js +248 -0
  131. package/src/ui/server/public/health-rollup.js +128 -0
  132. package/src/ui/server/public/i18n/es.js +287 -0
  133. package/src/ui/server/public/i18n/ja.js +276 -0
  134. package/src/ui/server/public/index.html +139 -438
  135. package/src/ui/server/public/local-dev.html +3468 -4044
  136. package/src/ui/server/public/logs.html +71 -208
  137. package/src/ui/server/public/panel.js +14 -2
  138. package/src/ui/server/public/release.html +175 -389
  139. package/src/ui/server/public/services.html +596 -770
  140. package/src/ui/server/public/settings-page.js +759 -0
  141. package/src/ui/server/public/settings-registry.js +521 -0
  142. package/src/ui/server/public/settings.html +590 -0
  143. package/src/ui/server/public/shell.js +3869 -0
  144. package/src/ui/server/public/topbar.js +1413 -0
  145. package/src/ui/server/public/version-label.js +12 -0
  146. package/src/ui/server/public/workspace-picker.js +74 -34
  147. package/src/ui/server/readonly-routes.ts +37 -3
  148. package/src/ui/server/services-list.ts +94 -17
  149. package/src/ui/server/settings-distribution.ts +74 -0
  150. package/src/ui/server/shell-prefs.ts +192 -0
  151. package/src/ui/server/theme-routes.ts +292 -0
  152. package/src/ui/server/theme-runtime.ts +407 -0
  153. package/src/ui/server/ui-assets.ts +16 -1
  154. package/src/ui/server/ui-language.ts +50 -0
  155. package/src/ui/server/workspace-routes.ts +48 -18
  156. package/themes/den/den-theme.json +72 -0
  157. package/themes/den/favicon.ico +0 -0
  158. 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,98 @@ 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
+ session-backend): the one this distribution selects, whether nothing chose
530
+ it (implicit), and whether it can work on this machine, with the reason.
531
+ It only checks PATH (and, for session-backend, whether the PTY module
532
+ loads): it never runs a backend CLI, prompts, or prints a secret. It exits
533
+ 1 when a strategy that will be used cannot work (an implicit secrets
534
+ default is used only when a secret is asked for, so it never fails
535
+ doctor).
536
+
537
+ ```
538
+ Acme doctor: strategies
539
+
540
+ secrets none ✓ available: no secrets backend configured
541
+ tls mkcert (implicit) ✗ unavailable: mkcert is not installed
542
+ ⚠ edge.tls is not set; defaulting to mkcert with a local CA. Set edge.tls explicitly.
543
+ session-backend den-pty (implicit) ✓ available: PTY module loaded
544
+ ```
545
+
546
+ `session-backend` is never a doctor failure: nothing needs a session until
547
+ one is started (see Sessions).
548
+
361
549
  ## den.yml: the product profile
362
550
 
363
551
  Every runtime name derives from `product.name`. Set only what you want to
@@ -421,6 +609,257 @@ brand:
421
609
  Runtime names (env vars, files, directories) always come from `den.yml`,
422
610
  never from the brand.
423
611
 
612
+ ## Plugins
613
+
614
+ A plugin adds to Den without changing it: a theme, a page in the web UI, and
615
+ later other kinds. Its manifest is a `den-plugin.json` that follows the
616
+ [`den.plugin/v1` schema](../schemas/den.plugin.v1.json); a theme can also be a
617
+ bare `den-theme.json`, which Den reads as a plugin that provides one theme
618
+ and asks for nothing.
619
+
620
+ ```jsonc
621
+ {
622
+ "schema": "den.plugin/v1",
623
+ "id": "runbook",
624
+ "name": "Team runbook",
625
+ "version": "1.0.0",
626
+ "license": "MIT", // an SPDX expression, the same rule as a theme's
627
+ "den": ">=0.3.0-0 <0.4.0", // required: the Den versions it runs on
628
+ "maintainer": "platform team",
629
+ "provides": [
630
+ {
631
+ "kind": "page",
632
+ "entry": "page/index.js",
633
+ "nav": { "group": "verify", "label": "nav.runbook" },
634
+ },
635
+ ],
636
+ "capabilities": ["ui.nav", "api.read"],
637
+ }
638
+ ```
639
+
640
+ Every plugin lives in `plugins/<id>/` in the distribution, and gets there in
641
+ one of two ways:
642
+
643
+ | | Local | Added |
644
+ | ------------ | ----------------------------------------------------- | --------------------------------------------------------- |
645
+ | What it is | the distribution's own (its theme, a team page) | someone else's published plugin |
646
+ | How | put it in `plugins/<id>/` and commit it | `<cli> plugin add <repo>[/<subpath>]@<tag\|commit>` |
647
+ | Lock entry | none | `plugins.lock.json`: source, ref, commit, hash, approvals |
648
+ | Integrity | the distribution's own git history and review | `plugin list` fails when its bytes differ from the lock |
649
+ | Capabilities | present is approved: what it declares is what it gets | approved at `add` (`--yes`), kept in the lock |
650
+ | Removing it | delete it in git | `<cli> plugin remove <id>` |
651
+
652
+ ```sh
653
+ <cli> plugin list # every plugin: version, kinds, local or source@ref, lock check
654
+ <cli> plugin validate # every local and added plugin
655
+ <cli> plugin validate runbook # plugins/runbook/ (an id is never a cwd path)
656
+ <cli> plugin validate path/to/dir # one plugin, a distribution, or a repo of them
657
+ <cli> plugin add github.com/acme/den-plugins/runbook@runbook/v1.0.0 --yes
658
+ <cli> plugin update runbook @runbook/v1.1.0
659
+ <cli> plugin remove runbook
660
+ ```
661
+
662
+ - **Every `plugins/<id>/` needs a manifest.** A directory with neither
663
+ `den-plugin.json` nor `den-theme.json` is an error in `list` and
664
+ `validate` ("no den-plugin.json or den-theme.json, so it isn't a
665
+ plugin"), so an id can never collide unseen. So is a directory named
666
+ `den` (the built-in theme's id), a name that is not an id (2–40
667
+ characters: lowercase letters, digits or dashes, starting with a letter),
668
+ and a symlink. `plugin list` prints exactly one row for every directory
669
+ in `plugins/`, even when `plugins.lock.json` can't be read. `theme list`
670
+ shows themes only and doesn't fail on other plugins, but counts the
671
+ plugin dirs with errors and points at `plugin list`.
672
+ - **`den` is the range of Den versions the plugin runs on**, as a semver
673
+ range. Prereleases count, so a range meant to include Den's dev builds
674
+ starts at `-0`: `>=0.3.0-0 <0.4.0` admits 0.3.0-dev.8, while `>=0.3.0`
675
+ does not. `validate` fails a plugin whose range excludes the running Den
676
+ and names both, and on a prerelease Den says when only the `-0` is
677
+ missing.
678
+ - **Load agrees with `validate`.** A theme that fails `validate` for any
679
+ reason (its range, a missing `author` or `maintainer`, a README, a
680
+ capability, contrast) is never offered in the Theme menu and never loads.
681
+ The web UI skips it with one server-log notice and draws with the next
682
+ choice (yours, then `brand.theme`, then `den`); it never fails to start.
683
+ - **What `validate` checks:** the schema; each kind with its own validator
684
+ (a theme as `theme validate` does, a page's entry module on disk); and the
685
+ admission rules: a `maintainer` (for a bare `den-theme.json`, its
686
+ `author`; `theme new` sets it to the brand's name); for a page or server plugin, a README
687
+ (`readme`, default `README.md`) that names its tier and says what it may
688
+ touch and what it must never do (themes are exempt); an SPDX `license`,
689
+ with every bundled font licensed.
690
+ - **Capabilities** come from a closed list: `ui.nav`, `api.read`,
691
+ `api.actions:<id,...>`, `secrets:<ref,...>`, `net:<host,...>`,
692
+ `exec:<binary,...>` and `send:<channel>`. Anything else fails validation.
693
+ - **Commit a local plugin before you trust it.** Den loads what is on disk,
694
+ so it can't tell committed from uncommitted. When a local plugin declares
695
+ `api.actions`, `secrets`, `exec`, `net` or `send` and has files git has not
696
+ committed, `validate` warns, so the change goes through the same review as
697
+ the rest of the distribution. Themes and read-only plugins never warn.
698
+ - **`add`** takes an exact ref: a tag or a full commit SHA (a branch only
699
+ with `--track`, recorded as tracking). It validates the plugin, vendors it
700
+ into `plugins/<id>/` and writes its lock entry, with `kinds` and
701
+ `approved_capabilities` from the manifest. A plugin that asks for
702
+ capabilities needs `--yes`. `add` never replaces a local plugin, and a
703
+ plugin already added from another source must be removed first.
704
+ - **`update <id> @<ref>`** fetches an added plugin again from its locked
705
+ source, and asks again (`--yes`) only when it wants more capabilities than
706
+ were approved. A tracking plugin can be updated without a ref.
707
+ - **A `file://` source is refused.** A path on one machine must never reach
708
+ a committed lock: for your own plugin, put it in `plugins/<id>/` as a local
709
+ plugin; to try a published one before tagging, push a branch and use
710
+ `--track`.
711
+
712
+ ### Where plugins may come from
713
+
714
+ `add` fetches from any git host unless `den.yml` (the product profile at the
715
+ distribution root) lists the ones it allows:
716
+
717
+ ```yaml
718
+ # den.yml
719
+ product:
720
+ name: acme
721
+ plugins:
722
+ allow:
723
+ - github.com/acme # <host>/<owner>
724
+ - git.acme.example # a bare host: every owner there
725
+ ```
726
+
727
+ A source outside the list is refused before anything is fetched, and a value
728
+ that is not a list of strings is an error rather than "allow everything".
729
+
730
+ ## Themes
731
+
732
+ A theme is the web UI's look and feel: color tokens for dark and light, type,
733
+ shape and a mark, in a `den-theme.json` that follows the
734
+ [`den.theme/v1` schema](../schemas/den.theme.v1.json). Den ships its own theme
735
+ (`den`). Others live in git repos; Monster-Orange/den-themes is the public set
736
+ and the guide to making your own
737
+ (<https://github.com/Monster-Orange/den-themes>).
738
+
739
+ ```sh
740
+ <cli> theme add github.com/Monster-Orange/den-themes/themes/paper@paper/v1.0.0
741
+ <cli> theme list
742
+ <cli> theme validate path/to/my-theme
743
+ <cli> theme remove paper
744
+ ```
745
+
746
+ - `add` takes an exact ref: a tag or a full commit SHA. A branch is refused
747
+ unless `--track` (for theme authors), which the lock records as tracking.
748
+ - It validates the theme (schema, contrast in both modes, files) and copies it
749
+ into the distribution's `plugins/<id>/`, with an entry in `plugins.lock.json`
750
+ (kinds `["theme"]`, source, ref, commit, content hash). A theme is a
751
+ plugin, so `theme add | list | validate | remove` are `plugin` filtered to
752
+ kind theme (see [Plugins](#plugins)): the same lock and directory, the
753
+ `file://` refusal and `plugins.allow`. Commit both: a fresh clone has the
754
+ theme offline, and theme changes show up as reviewable diffs.
755
+ - `list` checks every theme against its lock and fails if one has changed.
756
+ - A distribution can also keep its own theme in the repo: `<cli> theme new
757
+ <id>` copies the built-in theme into `plugins/<id>/` to edit (or put one
758
+ there by hand), and committing it gives everyone on the repo the theme.
759
+ It is a local plugin: publishing it to a themes repo is only for sharing
760
+ it with other teams. `list` shows it as `local` and `validate` with no
761
+ argument checks it along with the added ones; `add` never replaces it.
762
+ - Contrast: every text token (`text`, `muted`, `faint`, `accent-text`, and
763
+ the status colors) at 4.5:1 on every ground (`bg`, `surface`, `raised`,
764
+ `sunken`); `accent` and `border-strong` at 3:1; `on-accent` on
765
+ `accent-fill` at 4.5:1. `accent` is never text: Den draws links and accent
766
+ text in `accent-text`.
767
+ - The mark is one image or text: `svg` (with `svg_dark`), `png` (with
768
+ `png_dark`) or `webp` (with `webp_dark`), or `text` (up to 3 characters).
769
+ A mark that gives two image kinds fails validate, naming both. Use a
770
+ raster only for a mark that exists only as one (one that must not be
771
+ vectorised): `validate` checks that the file is what its extension says
772
+ (the PNG signature, or `RIFF….WEBP`), is at most 64 KB, and is at least
773
+ 44×44 px (twice the 22px mark in the top bar). An SVG mark may reference
774
+ nothing outside itself (every `href` and `url()` starts with `#`).
775
+ - `favicon` (optional): the browser-tab icon, a `.png`, `.svg` or `.ico`
776
+ file in the theme, at most 64 KB, its format checked by its first bytes.
777
+
778
+ ```json
779
+ "mark": { "png": "mark.png", "png_dark": "mark-dark.png", "shape": "circle" },
780
+ "favicon": "favicon.png"
781
+ ```
782
+
783
+ ### The active theme
784
+
785
+ The web UI draws every color, font and radius from the active theme. Which
786
+ one is active:
787
+
788
+ 1. the engineer's own choice (`appearance.theme` in `config/local/user.yml`,
789
+ set from the header's theme menu);
790
+ 2. else the distribution's default, `brand.theme` in the org config;
791
+ 3. else `den`.
792
+
793
+ ```yaml
794
+ # config/<name>.yml
795
+ brand:
796
+ theme: paper # an id in plugins/<id>/ (locked or local), or den
797
+ ```
798
+
799
+ An id that is not installed, or no longer validates under this Den, is
800
+ skipped for the next choice, ending at `den`, with one warning in the server
801
+ log; the UI never fails over a theme.
802
+
803
+ Appearance is a per-engineer preference, stored with the other settings
804
+ (`/api/settings`, `appearance`):
805
+
806
+ ```yaml
807
+ # config/local/user.yml (gitignored)
808
+ appearance:
809
+ theme: paper # omit to follow the distribution
810
+ mode: light # system (default) | dark | light
811
+ ```
812
+
813
+ The Theme menu's first entry is "Org default (<name>)": it stores no theme
814
+ (`appearance.theme` null, the same as omitting it), so you follow
815
+ `brand.theme`, and a later `brand.theme` change reaches you. Picking a theme
816
+ by name stores its id, even the theme that is the org default today: that
817
+ pins it, and the menu shows it as "<name> (pinned)" rather than "Org
818
+ default".
819
+
820
+ `system` follows the OS (`prefers-color-scheme`), and the theme's
821
+ `default_mode` when the OS gives none. The header's Mode and Theme menus save
822
+ here and apply at once, without a reload. Changing either writes only that
823
+ block: no edge route, edge config, service or the web service process is
824
+ touched.
825
+
826
+ What the server serves:
827
+
828
+ - `/theme.css`: the active theme as CSS custom properties
829
+ (`:root[data-mode="dark"] { --bg: …; }` and the light block, plus `--sans`,
830
+ `--mono`, `--display`, `--ja`, `--radius`, `--radius-button` and
831
+ `@font-face` for the theme's fonts). Only a validated manifest is rendered.
832
+ - `/theme/<id>/<file>`: only the files the manifest names (the mark's
833
+ `.svg`, `.png` or `.webp`, the favicon, the `.woff2` fonts), from that
834
+ theme's directory, each with its own type (`image/svg+xml`, `image/png`,
835
+ `image/webp`, `image/x-icon`, `font/woff2`).
836
+ - `/theme-favicon/<id>?mode=light|dark&v=<version>-<hash>`: the theme's
837
+ browser-tab icon in that mode: its `favicon` in both modes, else its
838
+ mark's file (the dark variant in dark mode, if it has one, else the base
839
+ file), else (a text mark) Den's own icon. `/theme-favicon` with no id is
840
+ the active theme's, never cached.
841
+ - `/api/themes`: every installed theme (id, name, version, `built-in` |
842
+ `locked` | `local`, ok), the active one (with its `mark` and its
843
+ `favicon` URL for each mode) and `org_default` (the theme no choice
844
+ draws: `brand.theme`, else den).
845
+
846
+ Each page sets `data-mode` on `<html>` before first paint, and its styles use
847
+ only the tokens: a page or plugin that draws in the UI uses `var(--token)`,
848
+ never a color literal (`accent` is never text: `accent-text` is), and only
849
+ `var(--sans)`, `var(--mono)` or `var(--display)` for a font (the fallbacks
850
+ are in `/theme.css`: a stack with no generic family gets Den's after it).
851
+ The header shows the active theme's mark: its SVG, PNG or WebP (the dark
852
+ variant in dark mode, if it has one) as an image at 22px, a raster clipped
853
+ to the mark's `shape`, or its `text` drawn by Den in `shape` on
854
+ `accent-fill`. The browser tab shows the theme's icon for the mode the page
855
+ draws in: the shell sets it (no page links one) and changes it with the
856
+ theme or the mode, System following the OS included. Each URL carries the
857
+ theme's id, version, mode and a hash of the file, so a switch is never
858
+ answered from cache.
859
+ A color from the org config (`orgs.<id>.color`) is only ever a swatch,
860
+ border or dot, never text or a ground: labels are `--text` or `--muted`.
861
+ Links in body content are underlined, so a link never reads by color alone.
862
+
424
863
  ## Workspace blueprint
425
864
 
426
865
  The workspaces a new machine starts with. Read from `defaults:` in the org
@@ -452,7 +891,8 @@ defaults:
452
891
  focus: true
453
892
  ```
454
893
 
455
- Each entry takes `repos`, `branch`, `autostart_off` and `org`, the same
894
+ Each entry takes `repos`, `branch`, `autostart_off`, `org` and
895
+ `stack_namespace` (see [Stack namespaces](#stack-namespaces)), the same
456
896
  fields as `defaults.workspace`, plus `focus`. Seeding creates every named workspace
457
897
  with its repos, activates them all, and focuses the one with `focus: true`
458
898
  (else the first). Set `defaults.workspace` or `defaults.workspaces`, not both:
@@ -592,10 +1032,78 @@ source and an org with no `sources` of that name), is an error naming both
592
1032
  keys. Such an error stops every command that touches orgs, the catalog,
593
1033
  clones or releases (exit 1, before any git call or write), and every web
594
1034
  API route that reads the config answers it as 422 `{ "ok": false, "error":
595
- "<message>", "code": "config_invalid" }`. Without the new keys nothing
1035
+ "<message>", "code": "config_invalid", "requestId": "<id>" }` (0.3: with
1036
+ the `X-Request-Id` header; a page request gets the error page). Without the new keys nothing
596
1037
  changes: `/api/orgs` is byte for byte what 0.2.7 answered. They need Den
597
1038
  0.2.8 or later.
598
1039
 
1040
+ ## Stack namespaces
1041
+
1042
+ A repo's compose stack runs as the Compose project `<stack>-<workspace>`,
1043
+ and its containers, volumes and networks are named from it. The `default`
1044
+ workspace uses `main` (`shop-main`), so its data predates workspaces. That
1045
+ is the default and needs no config.
1046
+
1047
+ A workspace can opt out with `stack_namespace`, which replaces the suffix:
1048
+
1049
+ ```yaml
1050
+ # config/local/workspaces.yml (this machine)
1051
+ workspaces:
1052
+ storefront:
1053
+ stack_namespace: main # shop runs as shop-main, with default's data
1054
+ services: { shop: { mode: dev-managed, path: ~/src/shop } }
1055
+ ```
1056
+
1057
+ ```yaml
1058
+ # org config: on a blueprint workspace, next to org
1059
+ defaults:
1060
+ workspaces:
1061
+ storefront:
1062
+ repos: [shop-web, shop]
1063
+ stack_namespace: main
1064
+ ```
1065
+
1066
+ A workspace's namespace is the first of `stack_namespace` in
1067
+ workspaces.yml, `stack_namespace` on the blueprint workspace of the same
1068
+ name, and its own name (`default`: `main`). `acme workspace seed` writes
1069
+ the blueprint's value into the workspace it seeds. workspaces.yml
1070
+ overriding a different blueprint value is a warning naming both. The value
1071
+ is lowercase letters, digits and dashes; anything else is a config error
1072
+ that names the key.
1073
+
1074
+ Workspaces with the same namespace share their stacks' projects, and so
1075
+ their data; a stack keeps its data when its repo moves between them. A
1076
+ shared project is one instance, keyed by checkout:
1077
+
1078
+ - Two workspaces bringing up the same stack from the same checkout (repo
1079
+ path and branch) share it. `acme down <repo>` or `acme stack down
1080
+ <stack>` from one, while another active workspace in the namespace still
1081
+ holds it, detaches and leaves it running; the last one stops it.
1082
+ `stack down --force` stops it regardless. A workspace that skips the
1083
+ stack as already healthy counts as holding it too.
1084
+ - Bringing it up from a different checkout or branch is refused
1085
+ (`identity_mismatch`), naming both checkouts and the namespace. Stop it
1086
+ where it runs first, or give the workspace its own `stack_namespace`.
1087
+
1088
+ A shared stack's identity is its checkout path, plus the branch only when
1089
+ the workspace entry pins one. Den does not read the checkout's HEAD, so a
1090
+ `git switch` in that checkout does not refuse its co-owners.
1091
+
1092
+ `acme workspace list` adds a Stacks column when a workspace's namespace is
1093
+ not its default, the Workspaces page shows a "stacks: <ns>" chip in the
1094
+ workspace's header, and `GET /api/workspaces` gives `stackNamespaces`:
1095
+ each workspace's `stack_namespace`, its `source` (`workspace`, `blueprint`
1096
+ or `default`), its `default` and whether it `differs`.
1097
+
1098
+ `DEN_STACK_WORKSPACE=<suffix>` overrides the suffix for one command
1099
+ (`DEN_STACK_WORKSPACE=pr-12 acme stack up shop` runs `shop-pr-12`). It wins
1100
+ over `stack_namespace` and bypasses the shared-project bookkeeping. It is a
1101
+ per-command override, not a migration tool: to keep a workspace on another
1102
+ namespace, set `stack_namespace`.
1103
+
1104
+ `stack_namespace` in the blueprint needs Den 0.3.0-dev.36 or later: an
1105
+ older Den rejects the unknown key.
1106
+
599
1107
  ## Releases
600
1108
 
601
1109
  The Release page dispatches each repo's CD workflow on GitHub. A repo's
@@ -812,7 +1320,7 @@ port.
812
1320
  ### Scoped to one workspace
813
1321
 
814
1322
  ```sh
815
- ./bin/acme mcp --workspace smb --actor agent:planner
1323
+ ./bin/acme mcp --workspace north --actor agent:planner
816
1324
  ```
817
1325
 
818
1326
  `--workspace <ws>` and `--actor agent:<id>` (both or neither) scope the server
@@ -820,8 +1328,8 @@ to one workspace for one agent:
820
1328
 
821
1329
  - **Pinned.** A tool that takes a `workspace` gets `<ws>` when none is given.
822
1330
  A call naming another workspace is refused (`Refused: <tool>: this MCP
823
- server is scoped to workspace "<ws>" (agent:<id>); "<other>" is another
824
- workspace`) before anything reaches the web server. A refused call is
1331
+ server is scoped to workspace "<ws>" (agent:<id>; no grants); "<other>" is
1332
+ another workspace`; with grants, `grants: <a>, <b>`) before anything reaches the web server. A refused call is
825
1333
  recorded in the activity trail, read tools included.
826
1334
  - **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
827
1335
  take only `<ws>`'s services, `manifest_validate` takes only an absolute path
@@ -836,6 +1344,16 @@ workspace`) before anything reaches the web server. A refused call is
836
1344
  entry gains `agent` and `workspace`, and the log line reads `· by
837
1345
  agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
838
1346
  <id>)` for every exec, run and sync.
1347
+ - **Granted.** `--grant <other>` (repeatable) also serves `<other>`, but only
1348
+ when the org config's `agents.<id>.grants` lists it; otherwise the server
1349
+ exits 2 (an agent can't grant itself one). A call naming a granted
1350
+ workspace is reported to the web server before it runs, and its activity
1351
+ entry names both (`workspace` and `targetWorkspace`; the log line reads
1352
+ `▶ <tool> in workspace <other>, under a grant to agent:<id> of workspace
1353
+ <ws>`). `GET /api/activity?address=workspace:<ws>` lists these entries
1354
+ under either workspace. `workspaces` lists `<ws>` and its grants, and tests run when either
1355
+ is focused. Logs and `manifest_validate` stay on `<ws>`'s own services.
1356
+ `agents start` passes an agent's grants for you (see [Agents](#agents)).
839
1357
  - `<ws>` must exist in this distribution; until it does, every tool says so.
840
1358
  The id is letters, digits, dots, dashes and underscores (at most 64). A bad
841
1359
  or half-given scope is a usage error, exit 2. `--list` and `--call` take
@@ -847,7 +1365,7 @@ calls the API itself (curl, a browser) is not held to its workspace. Enforcing
847
1365
  the scope in the HTTP server is WS-3a, planned for 0.4. `mcp connect` still
848
1366
  registers the unscoped server; to hand one agent a scoped server, add it to
849
1367
  that agent's config yourself, for example in a project's `.mcp.json`:
850
- `"args": ["mcp", "--workspace", "smb", "--actor", "agent:planner"]`.
1368
+ `"args": ["mcp", "--workspace", "north", "--actor", "agent:planner"]`.
851
1369
 
852
1370
  ### One call from a shell
853
1371
 
@@ -942,6 +1460,212 @@ authorise anything), and the MCP server's pid and session id. They travel as
942
1460
  `acme/server` stream shows an agent's actions as `[agent]`, ending
943
1461
  `· by agent <client> <version> (mcp pid <pid>, session <id>)`.
944
1462
 
1463
+ ## Sessions
1464
+
1465
+ A session is a terminal that keeps running when the web server restarts:
1466
+ an argv in a PTY, owned by the distribution's **session host**. The host
1467
+ is one small detached process (`<cli> sessions host`) that the first
1468
+ client starts on demand; it serves `<stateDir>/sessions.sock` (mode 0600)
1469
+ and logs to `<stateDir>/logs/<cli>/sessions-host.log`. The state dir is
1470
+ the profile's `state_dir` (`~/.<name>`), or `<PREFIX>_HOME`.
1471
+
1472
+ ```
1473
+ <cli> sessions start --workspace <ws> --name <n> [--cwd <dir>] [--keep-env NAME…] -- <argv…>
1474
+ <cli> sessions list [-w <ws>] [--json]
1475
+ <cli> sessions attach <name|id> # ctrl-] detaches; the session keeps running
1476
+ <cli> sessions kill <name|id> [--signal INT|TERM|KILL]
1477
+ <cli> sessions host [--stop]
1478
+ <cli> agents list -w <ws> [--json]
1479
+ ```
1480
+
1481
+ - A session is named `<dist>-<ws>:<n>`. `list -w <ws>` and `agents list
1482
+ -w <ws>` show that workspace's sessions only. Elsewhere a session can be
1483
+ named in full, as `<ws>:<n>`, as a bare `<n>` only one session has, or by
1484
+ its id.
1485
+ - It runs in `--cwd` (default: where you ran the command). Its environment
1486
+ starts empty and gets only `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`,
1487
+ `TERM` (default `xterm-256color`), `COLORTERM`, `LANG`, `LC_*`, `TZ` and
1488
+ `TMPDIR` from yours; `start` names the variables it left out (names, never
1489
+ values). `--keep-env NAME` adds one more: one exact name per flag,
1490
+ repeatable, no globs. It is audited: the session host's log
1491
+ (`<stateDir>/logs/<cli>/sessions-host.log`) gets a line naming each kept
1492
+ variable and who asked, never its value, and says `credential-shaped` when
1493
+ the value or name looks like a credential (it is still passed: you named
1494
+ it). An agent can't use it: `agents start` has no `--keep-env`, and
1495
+ `sessions start` refuses it when run by an agent (`DEN_AGENT` is set).
1496
+ Every runtime's `scrubEnv` (den-claude: `CLAUDECODE`, `CLAUDE_CODE_*`) is
1497
+ removed last, even a name you kept.
1498
+ - Every attachment gets all of its output; attaching prints `[attached to
1499
+ <name>]`, then replays the scrollback (2 MB per session, kept in
1500
+ `<stateDir>/sessions/<id>.log`).
1501
+ - Stopping the host (`sessions host --stop`, or a reboot) ends its
1502
+ sessions. They stay listed as exited, with their scrollback, until
1503
+ `sessions kill` removes them.
1504
+ - `agents.max_concurrent` caps the sessions running at once (default 4):
1505
+
1506
+ ```yaml
1507
+ agents:
1508
+ max_concurrent: 4
1509
+ ```
1510
+
1511
+ - A PTY needs a native module, `@lydell/node-pty`, which Den lists as an
1512
+ optional dependency (prebuilt binaries for macOS, Linux and Windows,
1513
+ nothing downloaded at install). Without it (`npm install
1514
+ --omit=optional`, another platform) everything else works, and `sessions
1515
+ start` and `<cli> doctor` say why den-pty is unavailable.
1516
+ - A state dir whose socket path would pass 103 bytes (the unix socket
1517
+ limit) is refused with that reason; point `<PREFIX>_HOME` somewhere
1518
+ shorter.
1519
+
1520
+ ## Agents
1521
+
1522
+ An agent is a **persona** (who) in a **workspace** (where), run by a
1523
+ **runtime** (which CLI) in a [session](#sessions), acting through Den's MCP
1524
+ server scoped to its workspace.
1525
+
1526
+ ```
1527
+ <cli> agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]
1528
+ <cli> agents list -w <ws> [--json]
1529
+ <cli> agents stop <id> [-w <ws>]
1530
+ <cli> agents resume <id> [-w <ws>]
1531
+ <cli> agents personas [--json]
1532
+ <cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
1533
+ ```
1534
+
1535
+ - `start` runs the agent as session `<dist>-<ws>:<id>` in the workspace's
1536
+ checkout dir (its `directory`, else `den-workspaces/<ws>`), capped by
1537
+ `agents.max_concurrent`. The id defaults to the persona's id; an id is
1538
+ unique in the distribution, so a second workspace's planner gets
1539
+ `planner-<ws>` unless you give `--id`.
1540
+ - The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
1541
+ persona, runtime, model, workspace, cwd, session and times. It never holds
1542
+ the environment or the command line. `list` reads it for persona, runtime
1543
+ and model; a record whose session has gone is listed as `stopped` (after
1544
+ `stop`) or `ended`.
1545
+ - `list` shows agents only: sessions with an agent record. A session from
1546
+ `sessions start` is not an agent; `sessions list` shows it.
1547
+ - `stop` sends its session TERM and marks it stopped. `resume` works only
1548
+ when the runtime declares resume and the session is not running: it starts
1549
+ a new session in the same cwd that continues the last conversation. A
1550
+ session that never got a prompt (no input with a line end) has no
1551
+ conversation, so `resume` starts it fresh under the same conversation id
1552
+ and says so.
1553
+ - After `start` or `resume`, Den watches the session for up to 3 s. If it
1554
+ exits non-zero, Den prints the exit code and its last lines (at most 10,
1555
+ secret-shaped values redacted) and exits 1.
1556
+
1557
+ ### An agent's environment
1558
+
1559
+ An agent's environment starts empty and gets, in order:
1560
+
1561
+ 1. `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM` (default
1562
+ `xterm-256color`), `COLORTERM`, `LANG`, `LC_*`, `TZ` and `TMPDIR` from
1563
+ yours;
1564
+ 2. the variables its runtime names (`envPassthrough`; den-claude:
1565
+ `CLAUDE_CONFIG_DIR`);
1566
+ 3. the runtime's launch env;
1567
+ 4. `DEN_AGENT=agent:<id>`.
1568
+
1569
+ Then every runtime's `scrubEnv` is removed (den-claude: `CLAUDECODE` and
1570
+ `CLAUDE_CODE_*`), whatever put it there. Nothing else of yours reaches it:
1571
+ not your tokens, not your shell's other variables, and not the Claude Code
1572
+ session you started it from (its session id, messaging socket and token,
1573
+ and the child-session marker that turns transcripts off). The agent record
1574
+ still holds no env. The agent's MCP server gets the distribution's location
1575
+ (`DEN_ROOT`, `<PREFIX>_ROOT`, `<PREFIX>_HOME`, when set) in its own config,
1576
+ so it reaches the same distribution.
1577
+
1578
+ `<cli> doctor --probe` checks this through the real session host: it runs a
1579
+ short probe session for a plain session and for each available runtime's
1580
+ agent env (built as `agents start` builds it, running the probe instead of
1581
+ the runtime's CLI). The probe prints key names and whether each is
1582
+ credential-shaped, never values. Doctor exits 1, one row per finding, when a
1583
+ runtime's `scrubEnv` name or a credential-shaped variable reaches either.
1584
+ Without the PTY module it says the probe is unavailable; that is not a
1585
+ failure. Plain `doctor` never spawns anything; when agents are configured it
1586
+ adds a note to run `<cli> doctor --probe`.
1587
+
1588
+ **What the scope does not do.** An agent's MCP scope binds cooperating
1589
+ agents only. A runtime with a shell can still run `<cli>` itself, outside
1590
+ the scoped MCP server, and reach other workspaces; WS-3a and SEC-1 close
1591
+ that later. And the user's own Claude Code settings and hooks
1592
+ (`~/.claude/settings.json`, its hooks, a project's `.claude/`) load in a
1593
+ den-claude agent, as in any `claude` you start.
1594
+
1595
+ ### Personas
1596
+
1597
+ A persona is a markdown file: YAML front matter, then the role prompt. It is
1598
+ a Claude Code agent file, read as it is, plus a few keys of Den's own:
1599
+
1600
+ ```markdown
1601
+ ---
1602
+ name: planner # the id (or `id:`; default: the file name)
1603
+ description: Writes stories and their acceptance criteria. # or `role:`
1604
+ model: some-model # optional; `inherit` means none
1605
+ tools: Read, Grep # hints for the runtime; Den restricts nothing with them
1606
+ standing: true # starts with its workspace (later slices)
1607
+ context: [docs/specs, the design system] # what to read first
1608
+ runtime: generic # default den-claude
1609
+ permission_mode: plan # den-claude only; default `default`
1610
+ argv: ["some-cli", "--model", "{model}", "--mcp-config", "{mcp_config}"]
1611
+ ---
1612
+
1613
+ You are the planner. …
1614
+ ```
1615
+
1616
+ Den ships a default pack: `planner` and `orchestrator` (standing), `fe-dev`,
1617
+ `be-dev`, `devops`, `reviewer` and `ux-designer`. Add your own packs, read in
1618
+ order after Den's; a persona with the same id replaces the earlier one:
1619
+
1620
+ ```yaml
1621
+ agents:
1622
+ persona_packs: [personas, ../some-plugin] # relative to the distribution root
1623
+ ```
1624
+
1625
+ A pack is a directory of persona files. A pack with an `agents/` directory
1626
+ inside (a Claude Code plugin) is read from there, so a plugin's agents are
1627
+ used without converting them. Files without front matter (a README) are
1628
+ skipped; two files with one id in a pack, or a pack that isn't a directory,
1629
+ are errors naming the file or the key.
1630
+
1631
+ ### Runtimes
1632
+
1633
+ `<cli> doctor` lists the runtime kind. Two are built in:
1634
+
1635
+ - **`den-claude`** (the default) needs `claude` on PATH; doctor reads its
1636
+ version from the install, without running it. It runs `claude --session-id
1637
+ <id> --mcp-config <file> --strict-mcp-config [--model <m>]
1638
+ --append-system-prompt <role prompt>`, so Den's MCP server is the agent's
1639
+ only one, and resumes with `claude --resume <id>`. Permission prompts stay
1640
+ in the agent's terminal: it passes `--permission-mode default` unless the
1641
+ persona's `permission_mode` sets one of `default`, `acceptEdits`, `plan` or
1642
+ `bypassPermissions` (anything else is an error naming the key).
1643
+ `bypassPermissions` prints a one-line warning at every start.
1644
+ - **`generic`** runs the persona's `argv` template. `{model}` is the agent's
1645
+ model (an error when the template uses it and there is none);
1646
+ `{mcp_config}` is the path of the agent's MCP config, written only when the
1647
+ template references it. It declares no resume.
1648
+
1649
+ ### Workspace scope and grants
1650
+
1651
+ An agent's MCP config, `<stateDir>/agent-files/<id>/mcp.json` (0600), runs
1652
+ `<cli> mcp --workspace <ws> --actor agent:<id>`: the agent sees and acts on
1653
+ its own workspace only, and every action is recorded as `agent:<id>` (see
1654
+ [Scoped to one workspace](#scoped-to-one-workspace)). A grant widens one
1655
+ agent's scope. It is config in git, reviewed like code; an agent can't give
1656
+ itself one:
1657
+
1658
+ ```yaml
1659
+ agents:
1660
+ planner:
1661
+ grants: [other-workspace]
1662
+ ```
1663
+
1664
+ `start` then adds `--grant other-workspace`, and each call into it is audited
1665
+ with both workspaces named. Any key of `agents:` other than `max_concurrent`
1666
+ and `persona_packs` is an agent id, and its only key is `grants`. A grant
1667
+ naming a workspace that doesn't exist stops `start`, naming the key.
1668
+
945
1669
  ## Pinning and upgrading
946
1670
 
947
1671
  `package.json` pins an exact Den version (`create` writes the version that