@monster-orange/den 0.4.0 → 0.4.2

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 (58) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/README.md +88 -158
  3. package/docs/distributions.md +216 -16
  4. package/docs/reference.md +177 -0
  5. package/package.json +3 -1
  6. package/src/shared/actor-token.ts +275 -0
  7. package/src/shared/agent-actor.ts +12 -2
  8. package/src/shared/agents/den-claude.ts +99 -1
  9. package/src/shared/agents/runtime.ts +7 -0
  10. package/src/shared/agents/runtimes.ts +9 -2
  11. package/src/shared/den-config.ts +52 -3
  12. package/src/shared/environment-manager/scope.ts +22 -0
  13. package/src/shared/environment-manager/service-runner.ts +94 -2
  14. package/src/shared/profile.ts +15 -4
  15. package/src/shared/sessions/client.ts +16 -2
  16. package/src/shared/sessions/den-pty.ts +10 -2
  17. package/src/shared/sessions/env.ts +13 -3
  18. package/src/shared/sessions/host.ts +56 -8
  19. package/src/shared/sessions/types.ts +8 -0
  20. package/src/shared/web-secret.ts +181 -0
  21. package/src/shared/web-session.ts +491 -0
  22. package/src/ui/cli/agent-ops.ts +843 -0
  23. package/src/ui/cli/agents-command.ts +97 -615
  24. package/src/ui/cli/flag-parser.ts +21 -0
  25. package/src/ui/cli/index.ts +127 -11
  26. package/src/ui/cli/sessions-command.ts +49 -5
  27. package/src/ui/cli/target-workspace.ts +105 -0
  28. package/src/ui/cli/web-command.ts +542 -14
  29. package/src/ui/mcp/api.ts +7 -0
  30. package/src/ui/mcp/server.ts +74 -6
  31. package/src/ui/server/agent-routes.ts +324 -0
  32. package/src/ui/server/audit.ts +81 -27
  33. package/src/ui/server/http-errors.ts +21 -3
  34. package/src/ui/server/index.ts +821 -66
  35. package/src/ui/server/public/agents-page.js +654 -0
  36. package/src/ui/server/public/agents-section.js +299 -0
  37. package/src/ui/server/public/agents.html +439 -0
  38. package/src/ui/server/public/appearance.js +3 -0
  39. package/src/ui/server/public/error-page.js +103 -0
  40. package/src/ui/server/public/header.js +45 -11
  41. package/src/ui/server/public/i18n/es.js +90 -1
  42. package/src/ui/server/public/i18n/ja.js +86 -1
  43. package/src/ui/server/public/local-dev.html +53 -0
  44. package/src/ui/server/public/logs.html +3 -0
  45. package/src/ui/server/public/panel.js +1 -1
  46. package/src/ui/server/public/release.html +8 -0
  47. package/src/ui/server/public/settings-page.js +5 -0
  48. package/src/ui/server/public/settings-registry.js +33 -0
  49. package/src/ui/server/public/shell.js +248 -5
  50. package/src/ui/server/public/topbar.js +51 -2
  51. package/src/ui/server/request-guard.ts +670 -0
  52. package/src/ui/server/route-classes.ts +317 -0
  53. package/src/ui/server/session-routes.ts +445 -0
  54. package/src/ui/server/shutdown-guard.ts +91 -0
  55. package/src/ui/server/sudo-store.ts +29 -3
  56. package/src/ui/server/ui-assets.ts +10 -1
  57. package/src/ui/server/websocket.ts +326 -0
  58. package/src/ui/server/workspace-routes.ts +13 -7
package/README.md CHANGED
@@ -1,169 +1,99 @@
1
- # Den
2
-
3
- Den is a config-driven local development appliance. It starts the repos you
4
- work on in dependency order, puts them behind a local HTTPS edge, gives you a
5
- web UI and a CLI over all of it, and runs smoke, browser, API, journey,
6
- security and performance suites against the environments you declare.
7
-
8
- Den knows nothing about any particular organisation. Everything specific to
9
- yours — product name, repos, environments, hosts, secrets backend, test
10
- accounts — lives in **your distribution**, not in Den.
11
-
12
- > **Status: 0.2.0**: `npm install @monster-orange/den`. Den is pre-1.0; each release lists the changes a distribution makes under **Migrations** in [CHANGELOG.md](CHANGELOG.md).
13
-
14
- ## Make it your own
15
-
16
- 1. **Create** a distribution named after your product:
17
-
18
- ```sh
19
- npx @monster-orange/den create acme # → ./acme
20
- cd acme && npm install
21
- ```
22
-
23
- 2. **Name** it: `den.yml` holds `product: { name: acme }`, so the CLI is
24
- `./bin/acme`, env vars are `ACME_*`, state lives in `~/.acme`. Pass
25
- `--cli`, `--env-prefix` or `--title` to `create`, or edit `den.yml`.
26
- 3. **Brand** it: `brand:` in `config/acme.yml` sets the name, tagline and
27
- banner people see in the CLI and web UI.
28
- 4. **Point it at your repos**: set `github_org` in `config/acme.yml`, then
29
- `./bin/acme register ../shop-api` (or add a manifest per repo under
30
- `manifests/`).
31
- 5. **Run** it: `./bin/acme up`, `./bin/acme web`, `make test`.
32
-
33
- Every field, the bin shim, the make fragment and upgrading are in
34
- [docs/distributions.md](docs/distributions.md);
35
- [examples/distribution/](examples/distribution) is what `create acme` writes.
36
-
37
- ## Distributions
38
-
39
- You run Den through a small repo of your own (a _distribution_):
40
-
41
- ```
42
- my-dev/
43
- ├── den.yml # who this distribution is (below)
44
- ├── package.json # pins an exact @monster-orange/den version
45
- ├── config/<name>.yml # your org config
46
- ├── config/environments/ # one <env>.json per environment
47
- ├── manifests/ # manifests for repos that don't carry their own
48
- ├── tests/pillars/<pillar>/ # your own suites (e.g. tests/pillars/perf/<module>/test.js)
49
- ├── bin/<cli> # shim: DEN_ROOT=<this dir> exec node_modules/@monster-orange/den/bin/den
50
- └── Makefile # include node_modules/@monster-orange/den/make/den.mk
1
+ <p align="center">
2
+ <img src="docs/images/preview/mark.svg" width="72" alt="Den">
3
+ </p>
4
+ <h1 align="center">Den</h1>
5
+ <p align="center"><b>Your whole stack, up and green, in one place. Built so you and your agents work by the same rules.</b></p>
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/@monster-orange/den"><img src="https://img.shields.io/npm/v/@monster-orange/den" alt="npm"></a>
8
+ <img src="https://img.shields.io/npm/l/@monster-orange/den" alt="licence">
9
+ <a href="https://github.com/Monster-Orange/den/actions/workflows/ci.yml"><img src="https://github.com/Monster-Orange/den/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ </p>
11
+
12
+ <picture>
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.1/hero-dark.png">
14
+ <img src="docs/images/0.4.1/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
+ </picture>
16
+
17
+ ## What Den is
18
+
19
+ Den is an opinionated local development framework for solo developers and small teams. You point it at the
20
+ repos you work on. Den starts them in the right order, puts them behind a local edge, and shows you the
21
+ whole thing in one web shell and one CLI: what's up, what's down, why, and the fix.
22
+
23
+ It doesn't replace your tools. Your repos, Docker, your test runners, your secrets store and your agents stay
24
+ yours. Den connects them and turns on good practice from the start.
25
+
26
+ <p align="center">
27
+ <img src="docs/images/0.4.1/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
28
+ </p>
29
+
30
+ ## How it helps
31
+
32
+ - **"How do I run this?" goes away.** Each repo declares how it starts, what it needs and how to check it's
33
+ healthy. Den works out the order and brings everything up, the same way on every machine.
34
+ - **One place to look.** Health, logs, runs, routes and certificates for every service, in one shell, instead
35
+ of eight terminals and a wiki page.
36
+ - **Real hostnames locally.** Every service gets a proper hostname behind a local edge, over HTTPS when you
37
+ turn it on (a local CA with mkcert, or certificates you bring). Cookies, OAuth redirects and CORS behave as
38
+ they do in production.
39
+ - **Several workspaces at once.** Keep feature branches side by side as workspaces. Health covers every active
40
+ one, and `up`, `down`, `restart` and `setup` take `-w <workspace>`, or find the service you name across them.
41
+ - **Tests are part of the loop.** Smoke, browser, API, user journeys, security and performance suites run against
42
+ the environments you declare.
43
+ - **Agents work by the same rules as you.** Start an agent from a persona (planner, orchestrator, developers,
44
+ devops, reviewer, UX designer, or your own) in a workspace. Its terminal opens in the Agents tab and keeps
45
+ running when you close the page or restart the web server. Over MCP it sees your stack with the same guards
46
+ and audit trail as a person, under a token of its own. Agents can read and investigate. Acting is opt-in,
47
+ and the lock still guards starting and stopping things.
48
+ - **Safe on localhost, signed in beyond it.** On localhost there is nothing to sign in to. Reached through the
49
+ edge or your network, every page and call needs a sign-in, and there is no password: `acme web login` prints a
50
+ one-time link.
51
+ - **Opinionated, not locked in.** Good defaults are switched on, and almost everything can be switched off. The
52
+ safety guards stay on. Den never needs our accounts, secrets or certificates. Everything specific to you
53
+ lives in your own repo.
54
+
55
+ <p align="center">
56
+ <img src="docs/images/0.4.1/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
57
+ <img src="docs/images/0.4.1/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
58
+ <img src="docs/images/0.4.1/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
59
+ <img src="docs/images/0.4.1/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
60
+ </p>
61
+
62
+ ## Get started
63
+
64
+ You need Node 22.13 or later, and Docker for anything that runs in containers. mkcert is only needed if you want Den to issue local HTTPS certificates.
65
+
66
+ ```sh
67
+ npx @monster-orange/den create acme # your own Den, named after your product
68
+ cd acme && npm install
69
+ ./bin/acme register ../shop-api # add a repo (repeat for each one)
70
+ ./bin/acme up # start everything, in order
71
+ ./bin/acme web # open the shell
51
72
  ```
52
73
 
53
- `den.yml` names the product. Every runtime name derives from it:
74
+ That's it: `acme` is now your tool, with your name, your CLI and your config, in a repo you own.
75
+ [Step by step →](docs/reference.md#make-it-your-own)
54
76
 
55
- ```yaml
56
- product:
57
- name:
58
- acme # CLI "acme", env vars ACME_*, config/acme.yml,
59
- # manifests .acme.yml, state ~/.acme,
60
- # managed clones .acme-services/, workspaces acme-workspaces/
61
- # Override any of them: cli, env_prefix, config, manifest, state_dir,
62
- # services_dir, workspaces_dir
63
- ```
77
+ ## How it fits together
64
78
 
65
- Den's own checkout is a distribution named `den`, so in this repo the CLI is
66
- `./bin/den`, the config is `config/den.yml`, and env vars are `DEN_*`.
67
-
68
- ## Repos opt in with a manifest
69
-
70
- A repo joins by carrying a manifest at its root (`.<name>.yml`, e.g.
71
- `.den.yml`) whose commands delegate to the repo's own targets. Den never
72
- reaches inside a repo beyond that. When a repo has no manifest, the
73
- distribution can hold one for it in `manifests/<repo>.<name>.yml`; the repo's
74
- own manifest always wins.
75
-
76
- ```yaml
77
- # demo-api: a self-contained service. Nothing it calls, so it starts alone.
78
- version: 1
79
- name: demo-api
80
- application: shop # the application this repo belongs to (den apps)
81
- compose:
82
- files: [docker-compose.yml]
83
- routes:
84
- api: api:8080 # https://api.demo-api.<edge domain>
85
- services:
86
- demo-api:
87
- commands:
88
- local:
89
- setup: make setup
90
- preflight: make test
91
- # OWASP ZAP target for the security pillar (`make test-sec-api env=local`).
92
- security:
93
- api:
94
- hosts:
95
- local: ["https://api.demo-api.globex.localhost"]
96
- ```
79
+ <p align="center"><img src="docs/images/preview/how-it-fits.svg" width="100%" alt="Den, the engine, runs your distribution, which holds your config; the distribution groups repos into workspaces, and each repo declares its services"></p>
97
80
 
98
- That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
99
- [`examples/demo/`](examples/demo/).
100
-
101
- ## What it does
102
-
103
- - **Workspaces.** A workspace is a set of repos. Each is either
104
- _dev-managed_ (your checkout, your branch) or _managed_ (cloned by Den and
105
- kept on a tracking branch).
106
- - **Derived dependency graph.** Start and stop order comes from real signals in
107
- each repo — SDK imports, `*_URL` env vars, IaC references, event rules,
108
- readiness probes — tuned in config with `graph.aliases`, `graph.edges` and
109
- an optional shared local-infra repo (`graph.infra`).
110
- - **Local HTTPS edge.** One local proxy routes `https://<route>.<domain>` to
111
- each service, with an optional project CA.
112
- - **Test pillars.** `smoke`, `browser` (synthetic Playwright), `api`,
113
- `journeys` (agent-driven), `perf` (k6), `security` (OWASP ZAP against hosts
114
- you own) and `suites` (a repo's own commands).
115
- - **Releases.** A release view over the repos and deploy workflows your config
116
- lists.
117
- - **Web UI and CLI** over the same state.
118
-
119
- ## Strategies
120
-
121
- Concerns that vary by org are strategies selected in config, never hard-wired:
122
-
123
- | Concern | Built-in choices | Chosen in |
124
- | ---------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
125
- | Secrets backend | `none`, `env`, `doppler`, `1password` | `secrets.provider` in the org config; per engineer in `config/local/user.yml`; per shell `<PREFIX>_SECRETS_PROVIDER` |
126
- | Test-user tokens | `local-jwt`, `cognito`, `auth0`, `none` | `auth.provider` in `config/environments/<env>.json` |
127
-
128
- A new secrets backend implements `SecretsProvider`
129
- (`src/shared/secrets/types.ts`) and registers as a strategy
130
- (`src/shared/secrets/strategies.ts`); a distribution needs no store at all
131
- (`none`, see "Secrets" in `docs/distributions.md`). A new identity provider implements a token
132
- provider in `src/shared/auth/token-providers/`.
133
-
134
- ## CLI
135
-
136
- ```bash
137
- den up [repo] # start the repo's derived dependency closure (-x deps only, -n dry run)
138
- den down [repo] # stop it cleanly (--with-deps: and what it sits on)
139
- den status # service health for the active workspace
140
- den apps # Org > Application > Repo > Services
141
- den graph [repo] # the derived dependency graph
142
- den logs [repo] # tail a repo's local logs
143
- den use-local [repo] # use your checkout · use-managed [repo] to switch back
144
- den workspace list # workspaces (activate · deactivate · use · create · delete)
145
- den run # run test suites (interactive, or with options)
146
- den web # the web UI
147
- den --help # everything else
148
- ```
81
+ - **Den** is the engine, from npm. It knows nothing about your company.
82
+ - **Your distribution** (`acme`) is a small repo you own. It holds your product name, brand, environments,
83
+ hosts and secrets setup.
84
+ - **Workspaces** group the repos you work on together. **Manifests** in each repo say how its services start,
85
+ connect and get checked.
149
86
 
150
- In a distribution the command is that distribution's `cli` name.
87
+ ## Coming next
151
88
 
152
- ## Developing Den
89
+ One workspace focus with Switch, agent pane layouts, and a Start dialog with personas, roles and a choice of
90
+ runtime and model.
153
91
 
154
- ```bash
155
- npm ci
156
- npm run typecheck
157
- npm run lint
158
- npm test
159
- DEN_ORG_NAMES_FILE=<patterns file> node scripts/org-name-gate.mjs
160
- ```
92
+ ## Docs
161
93
 
162
- CI runs the same gates. The org-name gate fails the build when any file names
163
- an organisation, product, host or account; its patterns are kept out of this
164
- repo (a CI secret) so the list itself is never published. See
165
- [CONTRIBUTING.md](CONTRIBUTING.md).
94
+ [Reference](docs/reference.md) (configuration, manifests, strategies, CLI) · [Distributions](docs/distributions.md) ·
95
+ [Changelog](CHANGELOG.md)
166
96
 
167
- ## License
97
+ Found a bug or have an idea? [Open an issue](https://github.com/Monster-Orange/den/issues/new).
168
98
 
169
- [MIT](LICENSE)
99
+ MIT licence.
@@ -563,16 +563,16 @@ product:
563
563
  # workspaces_dir: acme-workspaces
564
564
  ```
565
565
 
566
- | Field | Default | What it names |
567
- | ---------------- | -------------------- | ----------------------------------------------------------------- |
568
- | `name` | `den` | The product; `[a-z][a-z0-9-]*`. Everything below derives from it |
569
- | `cli` | `<name>` | The command shown in help, hints and the web UI |
570
- | `env_prefix` | `<NAME>` (`-` → `_`) | Env vars: `<PREFIX>_ROOT`, `<PREFIX>_HOME`, `<PREFIX>_INVOKE_CWD` |
571
- | `config` | `config/<name>.yml` | The org config, relative to the distribution root |
572
- | `manifest` | `.<name>.yml` | The manifest file a repo carries; the `.json` twin is also read |
573
- | `state_dir` | `~/.<name>` | Per-user state: web server, daemons, logs, history |
574
- | `services_dir` | `.<name>-services` | Managed clones, relative to the distribution root |
575
- | `workspaces_dir` | `<name>-workspaces` | Workspace checkouts, relative to the distribution root |
566
+ | Field | Default | What it names |
567
+ | ---------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- |
568
+ | `name` | `den` | The product; `[a-z][a-z0-9-]*`. Everything below derives from it |
569
+ | `cli` | `<name>` | The command shown in help, hints and the web UI |
570
+ | `env_prefix` | `<NAME>` (`-` → `_`) | Env vars: `<PREFIX>_ROOT`, `<PREFIX>_HOME`, `<PREFIX>_INVOKE_CWD` |
571
+ | `config` | `config/<name>.yml` | The org config, relative to the distribution root |
572
+ | `manifest` | `.<name>.yml` | The manifest file a repo carries; the `.json` twin is also read |
573
+ | `state_dir` | `~/.<name>` | Per-user state: web server, daemons, logs, history (a relative path is relative to den.yml's directory, not the cwd) |
574
+ | `services_dir` | `.<name>-services` | Managed clones, relative to the distribution root |
575
+ | `workspaces_dir` | `<name>-workspaces` | Workspace checkouts, relative to the distribution root |
576
576
 
577
577
  Env vars, with `ACME` as the prefix:
578
578
 
@@ -1179,6 +1179,86 @@ Den's scripts run with the engine's tsx (`tsxCommand()`), never through `npx`
1179
1179
  or a path relative to the current directory. In Den's own checkout both roots
1180
1180
  are the same directory.
1181
1181
 
1182
+ ## Signing in
1183
+
1184
+ On localhost nothing asks you to sign in: every page, the agents and the
1185
+ terminal work as they are, and the lock (R8) is the only prompt (for
1186
+ privileged actions). Signing in is for when the server is reached beyond
1187
+ this machine: through the edge hostname, a LAN address, or a bind that
1188
+ isn't loopback (`DEN_BIND_HOST`).
1189
+
1190
+ There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
1191
+ the state dir, made at the first `web up`) proves you are this computer's
1192
+ user, and the CLI does that for you:
1193
+
1194
+ ```bash
1195
+ ./bin/acme web open # start if needed, and sign this browser in (a new tab)
1196
+ ./bin/acme web login # print a one-time sign-in link, for another browser
1197
+ ./bin/acme web sessions # the signed-in browsers: id, created, last seen, host, user agent
1198
+ ./bin/acme web logout <id> # sign out one browser (an id prefix from `web sessions`)
1199
+ ./bin/acme web logout # sign out every browser
1200
+ ./bin/acme web logout --all # and rotate web.secret and revoke every agent's token
1201
+ ```
1202
+
1203
+ - **The link** is `http://localhost:<port>/auth?c=<code>`, with `&next=<path>`
1204
+ for where to land (`web login --next /settings`). The code works once,
1205
+ within 60 seconds, and only this server's secret makes one. It is printed
1206
+ only on the one line you asked for (`web login`, or `web up` when it
1207
+ couldn't open the browser), never logged. Through the edge or a LAN name,
1208
+ the cookie belongs to that name: `web login --origin https://acme.acme.localhost`
1209
+ (the signed-out page there shows the exact command).
1210
+ - **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
1211
+ Path=/, and Secure when the request came over HTTPS through the edge. The
1212
+ session lives server-side (`web-sessions.json`, 0600, in the state dir)
1213
+ and never expires: it ends at sign-out, `web logout`, or `web logout
1214
+ --all`. The cookie asks the browser to keep it for 400 days, the most a
1215
+ browser allows. The 0.4.1-dev `web.session.idle` and `web.session.max`
1216
+ keys are ignored; the server logs a warning naming each one it finds.
1217
+
1218
+ `web sessions`' LAST SEEN is written at most once a minute (60 s), so it
1219
+ equals CREATED right after a sign-in.
1220
+
1221
+ - **What needs it.** On loopback, nothing: pages and read-only calls are
1222
+ open, and changes (an agent's start or resume and the terminal included)
1223
+ need only the page's own Origin and CSRF token, which the shell sends.
1224
+ The Host allowlist, the Origin check and the CSRF token are what stop
1225
+ another site or a rebinding DNS name. Through the edge, or from another
1226
+ machine, every page and call needs a signed-in browser (or a bearer). A
1227
+ page there shows "Sign in to use <name>" with the command to run; it
1228
+ never asks for a secret. `/api` answers 401 `signin_required`.
1229
+ - **localhost is the canonical name.** A cookie set on `localhost` isn't
1230
+ sent to `127.0.0.1`, so the links Den prints use `localhost`.
1231
+ - **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
1232
+ needs the browser to send `Sec-Fetch-Site` or `Origin`.
1233
+ - **Scripts and the CLI** are unaffected: the CLI and the MCP server send
1234
+ the person token (`web token --print` prints it for curl). After `web
1235
+ logout --all` the old token is refused; the CLI reads the new one.
1236
+ - **`web logout --all` is the "something leaked" button.** It signs out
1237
+ every browser, rotates `web.secret` (every person token is void) and
1238
+ revokes every agent's token: a running agent's next call gets 401
1239
+ `token_revoked` ("restart or resume the agent to get a new one"). Plain
1240
+ `web logout` (one browser, or every browser) leaves agent tokens alone.
1241
+ - **Another machine.** `DEN_BIND_HOST=0.0.0.0` (or any non-loopback
1242
+ address) starts only with `web.allow_plain_http_remote: true` in
1243
+ `config/local/user.yml`, since a session cookie would otherwise cross the
1244
+ network in clear text. Having the edge in front is not enough: the
1245
+ server's own port would still answer plain HTTP on every interface. Only
1246
+ `config/local/user.yml` is read (a choice for that machine); the key in
1247
+ the distribution config is ignored, and the server logs a warning naming
1248
+ the file. With it, the machine's own addresses and host name are accepted
1249
+ with the port, and every route there needs a session.
1250
+ - **The edge** reaches Den on loopback (macOS, through
1251
+ `host.docker.internal`), so it needs no other bind. On Linux, where the
1252
+ edge container reaches the host on the Docker bridge, the one other bind
1253
+ allowed without the key is the bridge address (`172.16.0.0/12`, for
1254
+ example `DEN_BIND_HOST=172.17.0.1`), and only with the edge configured.
1255
+ - **The edge hostname is believed only from the edge.** A request whose
1256
+ `Host` is the edge hostname is accepted only from a peer the edge could
1257
+ be (loopback, the Docker bridge range or Docker Desktop's VM network);
1258
+ from any other address it is
1259
+ 421 `misdirected`, so a LAN client can't send the edge's name straight to
1260
+ the server's port.
1261
+
1182
1262
  ## Running the web UI as a service (macOS)
1183
1263
 
1184
1264
  Den ships a launchd installer that runs the web UI at login and restarts it if
@@ -1501,9 +1581,16 @@ the profile's `state_dir` (`~/.<name>`), or `<PREFIX>_HOME`.
1501
1581
  - Stopping the host (`sessions host --stop`, or a reboot) ends its
1502
1582
  sessions. They stay listed as exited, with their scrollback, until
1503
1583
  `sessions kill` removes them.
1504
- - `agents.max_concurrent` caps the sessions running at once (default 4):
1584
+ - `sessions.max_concurrent` caps the plain sessions (`sessions start`)
1585
+ running at once (default 8), and `agents.max_concurrent` the agents
1586
+ (default 4). Each counts only its own, so a few plain shells never stop
1587
+ an agent from starting, nor agents a shell. Past either, the start is
1588
+ refused, naming the key ("2 sessions are running and
1589
+ sessions.max_concurrent is 2"); `<cli> validate` names a wrong value.
1505
1590
 
1506
1591
  ```yaml
1592
+ sessions:
1593
+ max_concurrent: 8
1507
1594
  agents:
1508
1595
  max_concurrent: 4
1509
1596
  ```
@@ -1534,7 +1621,7 @@ server scoped to its workspace.
1534
1621
 
1535
1622
  - `start` runs the agent as session `<dist>-<ws>:<id>` in the workspace's
1536
1623
  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
1624
+ `agents.max_concurrent` (agents only: plain sessions never count). The id defaults to the persona's id; an id is
1538
1625
  unique in the distribution, so a second workspace's planner gets
1539
1626
  `planner-<ws>` unless you give `--id`.
1540
1627
  - The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
@@ -1554,6 +1641,80 @@ server scoped to its workspace.
1554
1641
  exits non-zero, Den prints the exit code and its last lines (at most 10,
1555
1642
  secret-shaped values redacted) and exits 1.
1556
1643
 
1644
+ ### On the Workspaces page
1645
+
1646
+ The selected workspace's agents are listed under its repo table, one row
1647
+ per agent: its id and persona, runtime, state (`running`, `stopping`,
1648
+ `stopped`, `ended` or `exited (<code>)`), directory, and when its session
1649
+ started. The workspace header counts them ("2 agents").
1650
+
1651
+ - **Stop** sends its session TERM, as `agents stop` does; the row reads
1652
+ `stopping…` until the session has gone. **Resume** shows only when
1653
+ `agents resume` would run: the runtime declares resume and the agent is
1654
+ not running.
1655
+ - **+ Start agent** picks a persona (Den's pack, then
1656
+ `agents.persona_packs`) and a runtime (the persona's own unless you pick
1657
+ one; one that can't run here is listed, disabled), then Start.
1658
+ - The page and the CLI call the same operations and read the same agent
1659
+ records, so an agent started in one is listed in the other. A workspace
1660
+ lists its own agents only.
1661
+ - Start, Stop and Resume need the lock open (🔓, the same rule as a
1662
+ service's Start), are capped by `agents.max_concurrent`, and are refused
1663
+ for an agent (agents don't start agents from the page). Each one is in the
1664
+ server log's activity, with you as its actor. The agent's environment is
1665
+ the one below, built from the web server's environment.
1666
+ - **Open in Agents →** (the section header) opens the Agents tab for the
1667
+ workspace, and **Open** on a row with a session opens its terminal there.
1668
+
1669
+ The routes: `GET /api/workspaces/<ws>/agents`, `POST
1670
+ /api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, id? }`), `POST
1671
+ /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, and `GET
1672
+ /api/personas` (personas and runtimes). Errors are the usual
1673
+ `{ ok: false, error, code, requestId }`: `locked` (423), `cap` (429),
1674
+ `conflict` (409, e.g. already running, or no resume), `not_found` (404),
1675
+ `exited_at_once` (502, with the session's last lines).
1676
+
1677
+ ### The Agents tab
1678
+
1679
+ "Agents" in the rail (Develop, after Graph) is `/agents`: every session and
1680
+ agent of the workspaces in view, and their terminals in the browser.
1681
+
1682
+ - **The list** is grouped by workspace and follows the scope bar (or the
1683
+ page's `?ws=<ws>`, which "Open in Agents →" sets). Each row has its
1684
+ state, persona (a `sessions start` session is labelled "session"),
1685
+ runtime and directory, and "waiting" when it waits on you (once the
1686
+ session host can tell, AG-6a; not yet). An agent whose
1687
+ token `web logout --all` revoked says "token revoked": stop and resume
1688
+ it, or start it again.
1689
+ - **A pane** is the session's terminal: it shows the scrollback, then the
1690
+ live output, and your keys go to the session; its size follows the
1691
+ pane. Click more rows to tile up to four (a fifth closes the oldest).
1692
+ The URL holds the open panes (`/agents?open=<id>,<id>`), so a link, a
1693
+ reload or Back reopens them.
1694
+ - **Close** (×) detaches, as leaving the page does: the session keeps
1695
+ running, and opening it again replays what you missed. **Stop** (an
1696
+ agent only) is `agents stop`, behind the lock. **+ Start agent** (or
1697
+ `/agents?start=1`) starts one and opens its pane; under All it asks for
1698
+ the workspace.
1699
+ - ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
1700
+ - Several browsers and `<cli> sessions attach` can attach to one session
1701
+ at once and each sees every byte; the terminal's size is the last
1702
+ one's to change it.
1703
+ - Terminals need the optional PTY module (`<cli> doctor`); without it the
1704
+ page lists what there is and says so.
1705
+ - **Who can open one.** The terminal is shell-grade: on localhost the
1706
+ page's own Origin and CSRF token, beyond loopback a signed-in browser
1707
+ (the list says "Sign in to open terminals" otherwise), and an agent's
1708
+ token only its own session's. Each attach and detach is in the server
1709
+ log's activity, with its actor.
1710
+
1711
+ The routes: `GET /api/sessions` (`{ sessions, workspaces, host, backend,
1712
+ io }`) and the WebSocket `/api/sessions/<id>/io` (subprotocol `den.io`, the
1713
+ CSRF token as `den.csrf.<token>`): binary frames are output and input, a
1714
+ text frame `{"type":"resize","cols":c,"rows":r}` resizes, and the server
1715
+ sends `{"type":"session",…}` first and `{"type":"exit","exitCode":n}` when
1716
+ the session ends.
1717
+
1557
1718
  ### An agent's environment
1558
1719
 
1559
1720
  An agent's environment starts empty and gets, in order:
@@ -1564,7 +1725,7 @@ An agent's environment starts empty and gets, in order:
1564
1725
  2. the variables its runtime names (`envPassthrough`; den-claude:
1565
1726
  `CLAUDE_CONFIG_DIR`);
1566
1727
  3. the runtime's launch env;
1567
- 4. `DEN_AGENT=agent:<id>`.
1728
+ 4. `DEN_AGENT=agent:<id>` and `DEN_ACTOR_TOKEN` (below).
1568
1729
 
1569
1730
  Then every runtime's `scrubEnv` is removed (den-claude: `CLAUDECODE` and
1570
1731
  `CLAUDE_CODE_*`), whatever put it there. Nothing else of yours reaches it:
@@ -1585,10 +1746,49 @@ Without the PTY module it says the probe is unavailable; that is not a
1585
1746
  failure. Plain `doctor` never spawns anything; when agents are configured it
1586
1747
  adds a note to run `<cli> doctor --probe`.
1587
1748
 
1749
+ ### An agent's token
1750
+
1751
+ `agents start` (the CLI or the Workspaces page) mints a random `den_at_…`
1752
+ token for the agent and puts it in the session as `DEN_ACTOR_TOKEN`. Den
1753
+ keeps only its hash (`actor-tokens.json`, 0600, in the state dir) with the
1754
+ agent, its workspace, its grants and its session. Stop revokes it, and so
1755
+ does the session's end; resume mints a new one and the old one stops
1756
+ working. The `<cli>` and the MCP server Den starts for the agent send it, so
1757
+ the server audits their calls as the agent, from the token, never from a
1758
+ header (agent headers without the token are recorded as an unverified
1759
+ claim).
1760
+
1761
+ An agent's token reaches only its own session's routes, and never a
1762
+ shell-grade route, `/api/web/*`, sign-in or sign-out, or the lock: it can't
1763
+ unlock, use the sudo password or start another agent (403
1764
+ `agent_forbidden`). A person's credential never reaches a session:
1765
+ `DEN_PERSON_TOKEN`, a person token's value and a path to `web.secret` are
1766
+ always removed, and `sessions start --keep-env` of one is refused and logged.
1767
+ An agent runs as your OS user, so it could still read `web.secret` itself;
1768
+ the token keeps what Den does on its behalf attributable and bounded.
1769
+ **The boundary is the server's token rules** (above), not a deny list.
1770
+
1771
+ **The deny list is a convenience, not the boundary.** den-claude passes
1772
+ Claude Code a settings file (`--settings`) whose `permissions.deny` lists
1773
+ `Read` of `<state dir>/web.secret` and `<state dir>/sessions/**`, and
1774
+ `Bash` rules in Claude Code's wildcard form for the path
1775
+ (`Bash(*<state dir>/web.secret*)`, and each of `cat`, `head`, `tail`,
1776
+ `less`, `more`, `xxd`, `od`, `strings`, `base64` and `cp` followed by it),
1777
+ spelled as an absolute path, under `~` and under `$HOME`, with a path that
1778
+ has spaces also backslash-escaped. It binds cooperating agents only: an
1779
+ agent that ignores its permission config can read the files anyway. And
1780
+ it does not catch every spelling: a path relative to the agent's cwd
1781
+ (`cat ../.den/web.secret`, say), another program, a script or a copy under
1782
+ another name all get past it. The server's token rules (above)
1783
+ are the boundary: whatever an agent reads, its token reaches only its own
1784
+ session's routes, never a shell-grade route or the lock. OS-user isolation
1785
+ (a separate user, a container, a sandbox) is not in 0.4.x. `web logout
1786
+ --all` revokes every agent's token (above, under Signing in).
1787
+
1588
1788
  **What the scope does not do.** An agent's MCP scope binds cooperating
1589
1789
  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
1790
+ the scoped MCP server, and reach other workspaces (with its own token, so
1791
+ it is audited as the agent); WS-3a closes that later. And the user's own Claude Code settings and hooks
1592
1792
  (`~/.claude/settings.json`, its hooks, a project's `.claude/`) load in a
1593
1793
  den-claude agent, as in any `claude` you start.
1594
1794
 
@@ -1634,7 +1834,7 @@ are errors naming the file or the key.
1634
1834
 
1635
1835
  - **`den-claude`** (the default) needs `claude` on PATH; doctor reads its
1636
1836
  version from the install, without running it. It runs `claude --session-id
1637
- <id> --mcp-config <file> --strict-mcp-config [--model <m>]
1837
+ <id> --mcp-config <file> --strict-mcp-config --settings <file> [--model <m>]
1638
1838
  --append-system-prompt <role prompt>`, so Den's MCP server is the agent's
1639
1839
  only one, and resumes with `claude --resume <id>`. Permission prompts stay
1640
1840
  in the agent's terminal: it passes `--permission-mode default` unless the