@monster-orange/den 0.4.3 → 0.4.5

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 (85) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/README.md +12 -11
  3. package/docs/distributions.md +438 -91
  4. package/docs/reference.md +8 -3
  5. package/package.json +1 -1
  6. package/src/shared/agents/catalog-runtime.ts +178 -0
  7. package/src/shared/agents/catalog.ts +343 -0
  8. package/src/shared/agents/den-claude.ts +14 -18
  9. package/src/shared/agents/runtime-search.ts +142 -15
  10. package/src/shared/agents/runtime.ts +18 -1
  11. package/src/shared/agents/runtimes.ts +23 -1
  12. package/src/shared/auth/sudo-vault.ts +131 -11
  13. package/src/shared/cli-command.ts +186 -0
  14. package/src/shared/confirm-code.ts +34 -0
  15. package/src/shared/den-config.ts +64 -3
  16. package/src/shared/dev-metrics/emit.ts +31 -1
  17. package/src/shared/edge/config.ts +39 -0
  18. package/src/shared/edge/render.ts +92 -18
  19. package/src/shared/edge/runtime.ts +57 -1
  20. package/src/shared/edge/status.ts +134 -0
  21. package/src/shared/edge/types.ts +11 -1
  22. package/src/shared/environment-manager/health.ts +82 -8
  23. package/src/shared/environment-manager/workspaces.ts +54 -0
  24. package/src/shared/github-app.ts +94 -0
  25. package/src/shared/github-device.ts +286 -0
  26. package/src/shared/health/not-checked.ts +42 -0
  27. package/src/shared/health/rollup.ts +4 -0
  28. package/src/shared/process/spawn-streaming.ts +8 -3
  29. package/src/shared/secrets/onepassword.ts +27 -4
  30. package/src/shared/sessions/client.ts +5 -1
  31. package/src/shared/sessions/host.ts +29 -1
  32. package/src/shared/sessions/protocol.ts +6 -1
  33. package/src/shared/sessions/types.ts +6 -0
  34. package/src/shared/stacks/status.ts +24 -0
  35. package/src/shared/strategies/registry.ts +5 -0
  36. package/src/shared/types/index.ts +6 -0
  37. package/src/shared/web-owner.ts +91 -0
  38. package/src/shared/web-session.ts +135 -1
  39. package/src/ui/cli/agent-ops.ts +210 -19
  40. package/src/ui/cli/agents-command.ts +50 -21
  41. package/src/ui/cli/edge-command.ts +16 -3
  42. package/src/ui/cli/flag-parser.ts +1 -1
  43. package/src/ui/cli/health-display.ts +4 -1
  44. package/src/ui/cli/web-command.ts +512 -33
  45. package/src/ui/mcp/scope.ts +23 -3
  46. package/src/ui/mcp/server.ts +31 -7
  47. package/src/ui/mcp/tools.ts +327 -20
  48. package/src/ui/mcp/views.ts +450 -0
  49. package/src/ui/server/agent-routes.ts +118 -28
  50. package/src/ui/server/device-flows.ts +274 -0
  51. package/src/ui/server/edge-exposure.ts +16 -4
  52. package/src/ui/server/github-routes.ts +321 -0
  53. package/src/ui/server/http-errors.ts +10 -0
  54. package/src/ui/server/index.ts +540 -33
  55. package/src/ui/server/login-waits.ts +203 -0
  56. package/src/ui/server/pair-requests.ts +237 -0
  57. package/src/ui/server/pair-routes.ts +196 -0
  58. package/src/ui/server/public/agent-start.js +46 -7
  59. package/src/ui/server/public/agents-page.js +232 -97
  60. package/src/ui/server/public/agents-section.js +3 -5
  61. package/src/ui/server/public/agents.html +12 -28
  62. package/src/ui/server/public/error-page.js +452 -40
  63. package/src/ui/server/public/error.html +59 -0
  64. package/src/ui/server/public/graph-model.js +2 -0
  65. package/src/ui/server/public/header.js +2 -3
  66. package/src/ui/server/public/health-rollup.js +20 -8
  67. package/src/ui/server/public/i18n/es.js +89 -21
  68. package/src/ui/server/public/i18n/ja.js +88 -21
  69. package/src/ui/server/public/index.html +17 -4
  70. package/src/ui/server/public/local-dev.html +44 -21
  71. package/src/ui/server/public/qr.js +317 -0
  72. package/src/ui/server/public/services.html +21 -5
  73. package/src/ui/server/public/settings-page.js +10 -2
  74. package/src/ui/server/public/settings-registry.js +617 -1
  75. package/src/ui/server/public/settings.html +90 -1
  76. package/src/ui/server/public/shell.js +151 -33
  77. package/src/ui/server/public/topbar.js +170 -206
  78. package/src/ui/server/public/workspace-picker.js +1 -1
  79. package/src/ui/server/readonly-routes.ts +29 -5
  80. package/src/ui/server/request-guard.ts +103 -36
  81. package/src/ui/server/route-classes.ts +116 -4
  82. package/src/ui/server/session-routes.ts +9 -1
  83. package/src/ui/server/sudo-store.ts +170 -6
  84. package/src/ui/server/ui-assets.ts +2 -0
  85. package/src/ui/server/workspace-routes.ts +10 -3
@@ -12,6 +12,7 @@ Den's own checkout is itself a distribution, named `den`.
12
12
  - [The bin shim](#the-bin-shim)
13
13
  - [make: den.mk](#make-denmk)
14
14
  - [Two roots](#two-roots)
15
+ - [Dev metrics: local unless you opt in](#dev-metrics-local-unless-you-opt-in)
15
16
  - [Pinning and upgrading](#pinning-and-upgrading)
16
17
  - [Developing Den against a distribution](#developing-den-against-a-distribution)
17
18
 
@@ -352,7 +353,7 @@ where it is, under `secrets.refs`:
352
353
 
353
354
  | Ref | Feature |
354
355
  | -------------------- | ------------------------------------------------------------ |
355
- | `sudo` | The saved local-dev sudo password (auto-unlock, "Save to …") |
356
+ | `sudo` | The saved admin password, for sudo (auto-load, "Save to …") |
356
357
  | `journeys_agent_key` | The journeys agent's API key, when `CURSOR_API_KEY` is unset |
357
358
 
358
359
  ```yaml
@@ -377,19 +378,51 @@ secrets:
377
378
  and `env` need a `key` (or an env-shaped `field`). There are no defaults:
378
379
  a ref that is not set is **not configured**, and its feature says so
379
380
  ("not configured: set secrets.provider and secrets.refs.sudo in
380
- config/acme.yml") instead of running a CLI. The interactive unlock always
381
+ config/acme.yml") instead of running a CLI. Typing the password always
381
382
  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
383
 
386
- The lock (🔒 in the top bar) is asked for only when an action needs it,
387
- on any page: a page that thinks it's locked asks the server again first
388
- (another tab or device may have unlocked it), then, when a password is
389
- stored, loads it from the vault (once a page; a page view alone never
390
- wakes the vault), and only then shows the password field. Its popover is
391
- always on top, even over a dialog, and a dialog that needs the lock (Start
392
- agent) asks for the password itself.
384
+ **Saving writes `refs.sudo` for you** (LK-4, 0.4.5). When `secrets.provider`
385
+ is `1password` or `doppler`, its CLI is available, and no `refs.sudo` is
386
+ set, the admin password prompt (shown when a command is about to run sudo)
387
+ still offers "Save to 1Password". Saving creates the item
388
+ (1Password: a Password item "<Product> admin password", field `password`,
389
+ in your default vault; Doppler: the key `<PREFIX>_LOCAL_ADMIN_PWD`) and
390
+ then writes that ref to `config/local/user.yml`, because the password is
391
+ the person's, not the distribution's. From then on it survives restarts.
392
+ A failed save writes nothing, and its error never carries the password.
393
+ `env` keeps nothing past the process, so it offers no save. A `refs.sudo`
394
+ in the distribution's config always wins over the one in user.yml.
395
+
396
+ `/api/sudo-password/vault-status` reports `backend`, `label`,
397
+ `configured`, `canSave` (saving works with no YAML to edit),
398
+ `backendAvailable`, `reason` and `hasStored` (`opAvailable` is a
399
+ deprecated alias of `backendAvailable`, kept for one minor).
400
+
401
+ The admin password is for sudo, and nothing else (LK-0). It is no sign-in
402
+ and no access check: starting, stopping and resuming agents, sessions and
403
+ terminals, and a service whose commands run no sudo, never ask for it.
404
+ Den asks only when a command it runs is about to run sudo without it:
405
+ every service command (up, down, restart, setup, sync) gets a `sudo` shim
406
+ first on its `PATH`. With a password held, the shim gives it to sudo. With
407
+ none, sudo that needs no password (NOPASSWD, a warm cache) runs as it
408
+ would, and anything else never prompts: the shim prints
409
+ `<cli>: needs-sudo: sudo <command>` and fails, and the run's event stream
410
+ carries `{ "type": "needs-sudo", "command": "sudo <command>", "step":
411
+ "<service>" }` (sudo's own "a password is required" counts too, without
412
+ the command). Only then does the page ask, saying "`sudo <command>` needs
413
+ your admin password", with the 🔑 chip in the top bar while it waits. It
414
+ asks the server first (another tab or device may have set it), then, when
415
+ a password is stored, loads it from the vault (a page view alone never
416
+ wakes the vault), and only then shows the password field, always on top,
417
+ even over a dialog. After the password, the action runs again.
418
+
419
+ Settings › Command line › Admin password says whether it is saved ("Saved
420
+ in 1Password", "Saved for this session" or "Not saved"), with **Forget**:
421
+ `POST /api/sudo-password/forget` drops the password the server holds and,
422
+ when this server saved it to the vault or loaded it from there, clears the
423
+ vault's copy. `GET /api/sudo-password/status` answers `set`,
424
+ `passwordless`, `saved` (`vault`, `session` or null) and `vault` (`{
425
+ configured, label }`, read from the config alone).
393
426
 
394
427
  A `1password` distribution with no refs gets one warning, in the web
395
428
  server's startup log and in `<cli> validate`, with the YAML to add.
@@ -1190,12 +1223,14 @@ are the same directory.
1190
1223
  ## Signing in
1191
1224
 
1192
1225
  On localhost nothing asks you to sign in: every page, the agents and the
1193
- terminal work as they are, and the lock (R8) is the only prompt (for
1194
- privileged actions). The edge counts as local too (0.4.3): opening
1195
- `https://acme.acme.localhost` from this machine asks for nothing.
1196
- Signing in is for when the server is reached from another machine: the
1197
- edge for a client the edge says is elsewhere, a LAN address, or a bind
1198
- that isn't loopback (`DEN_BIND_HOST`).
1226
+ terminal work as they are, and the admin password (R8) is the only prompt,
1227
+ asked only when a command Den runs needs sudo. Through the edge, a browser
1228
+ the edge shows is on this machine asks for nothing either. Signing in is
1229
+ for everything else: the edge for a client it can't place here (0.4.5), a
1230
+ LAN address, or a bind that isn't loopback (`DEN_BIND_HOST`). From another
1231
+ device, sign in with GitHub as the Den's owner, approve the computer with
1232
+ the code its page shows, or open a one-time link (below, and
1233
+ [Signing in with GitHub](#signing-in-with-github)).
1199
1234
 
1200
1235
  There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
1201
1236
  the state dir, made at the first `web up`) proves you are this computer's
@@ -1203,7 +1238,10 @@ user, and the CLI does that for you:
1203
1238
 
1204
1239
  ```bash
1205
1240
  ./bin/acme web open # start if needed, and sign this browser in (a new tab)
1206
- ./bin/acme web login # print a one-time sign-in link, for another browser
1241
+ ./bin/acme web login # a one-time sign-in link and its code; waits until it's used
1242
+ ./bin/acme web login --no-wait # the link alone (asks for no code), and return
1243
+ ./bin/acme web approve ABC-123 # approve the computer showing this code
1244
+ ./bin/acme web approve --list # the computers waiting with a code
1207
1245
  ./bin/acme web sessions # the signed-in browsers: id, created, last seen, host, user agent
1208
1246
  ./bin/acme web logout <id> # sign out one browser (an id prefix from `web sessions`)
1209
1247
  ./bin/acme web logout # sign out every browser
@@ -1220,7 +1258,19 @@ user, and the CLI does that for you:
1220
1258
  among them; its URL comes from the edge config, and a name the edge
1221
1259
  doesn't route here is refused, saying what to add), and
1222
1260
  `web login --origin http://<lan name>:<port>` for any other allowed name
1223
- (the signed-out page there shows the exact command).
1261
+ (the signed-out page there shows the exact command, under "Other ways").
1262
+ - **`web login` waits** (0.4.5). Besides the link it prints an
1263
+ 8-character code ("It asks for this code: ABCD-EFGH") and blocks until
1264
+ the link is used, then says who: "Signed in: Safari on macOS
1265
+ (100.64.0.7) at 10:42". Nobody within the minute: "Expired: nobody used
1266
+ the link", exit 1. The link's page asks for the code before it signs in,
1267
+ so the link alone is useless; once opened it waits 5 minutes for the
1268
+ code. A wrong code is refused; five burn the link ("Refused: too many
1269
+ wrong codes", exit 1), more than 20 wrong a minute (any link) are slowed,
1270
+ and the link still works once. The CLI asks the running server through
1271
+ its bearer and never logs the code. `--no-wait` prints the link alone and
1272
+ returns, as before (no code); with no server running `web login` does
1273
+ that too, and says so.
1224
1274
  - **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
1225
1275
  Path=/, and Secure when the request came over HTTPS through the edge. The
1226
1276
  session lives server-side (`web-sessions.json`, 0600, in the state dir)
@@ -1238,36 +1288,111 @@ user, and the CLI does that for you:
1238
1288
  The Host allowlist, the Origin check and the CSRF token are what stop
1239
1289
  another site or a rebinding DNS name. From another machine, every page
1240
1290
  and call needs a signed-in browser (or a bearer). A page there shows
1241
- "Sign in to use <name>" with the command to run; it never asks for a
1291
+ "Sign in to <name>" and the ways to sign in (below); it never asks for a
1242
1292
  secret. `/api` answers 401 `signin_required`.
1243
- - **The edge counts as local.** A request for the edge hostname from the
1244
- edge (an edge peer, below) is treated as loopback: no sign-in on any
1245
- route, the agents and the terminal included. Host, Origin, CSRF and the
1246
- agent-token rules still apply. Who the client is comes from the left-most
1247
- `X-Forwarded-For` entry the edge sends:
1293
+ - **The signed-out page says where to act.** It names the machine Den runs
1294
+ on ("You're opening Den from another computer. Den runs on
1295
+ **studio**, so it needs to know this is you."), then:
1296
+ - **Sign in with GitHub**, only when a client id is set (below);
1297
+ - **Approve this computer**: a code (`XXX-XXX`) and the command that
1298
+ approves it, to run on that machine (`<cli> web approve <code>`), or
1299
+ Settings › Access › Waiting devices there. The page signs itself in
1300
+ when it's approved and goes to the page you asked for; it counts down
1301
+ the 10 minutes, and an expired code is a click away from a new one.
1302
+ From a computer with SSH access to it, the page gives the one-liner
1303
+ (`ssh <host> '<cli> web approve <code>'`) and, when the host is a name,
1304
+ an Open Terminal (`ssh://`) link;
1305
+ - **Other ways**, collapsed: the one-time link with
1306
+ `web login --origin`.
1307
+
1308
+ Every command the page prints names the machine it runs on and has a
1309
+ copy button. It is `<cli>` when the first `<cli>` on that machine's PATH
1310
+ is this distribution's, else the absolute path of its bin.
1311
+
1312
+ - **The machine's name** on the page is `web.machine_name` from
1313
+ `config/local/user.yml` (Settings › Access › Machine name), else the short
1314
+ host name (up to the first dot). It is never taken from the browser's
1315
+ Host. The ssh one-liner's target is the Host the browser used, without
1316
+ the port (the machine's host name if it had none).
1317
+ - **Codes.** About 30 bits (Crockford base32: no I, L, O or U; typed in any
1318
+ case, the dash optional); the page polls with a separate 128-bit id. A
1319
+ code expires after 10 minutes and works once; at most five wait at once
1320
+ (a sixth drops the oldest). They live in the server's memory only.
1321
+ Approving is rate-limited (10 a minute) and recorded in the activity log
1322
+ with who approved, never the code. The new session is "approved with a
1323
+ code". **Deny** (Settings › Access, or `web deny` through the API) tells
1324
+ the page it was refused.
1325
+ - **Seeing that you're signed in.** The chip at the foot of the rail says
1326
+ "Local" (this machine), "Signed in · this device" (a code or a one-time
1327
+ link) or "@<login>" (GitHub), and opens Settings › Access. The first page
1328
+ after a sign-in says "This device is signed in to Den on <machine>",
1329
+ once. Linked devices lists every session (GitHub, a code, a one-time
1330
+ link): how it signed in, when, from where (the address and the browser
1331
+ and OS), last seen, and **Revoke**; the one you're using says "This
1332
+ device".
1333
+ - **The edge counts as local only when it shows the client is.** A
1334
+ request for the edge hostname from the edge (an edge peer, below) is
1335
+ treated as loopback (no sign-in on any route, the agents and the
1336
+ terminal included) when the client the edge reports is this machine.
1337
+ Host, Origin, CSRF and the agent-token rules still apply. Who the client
1338
+ is comes from the right-most `X-Forwarded-For` entry, the one the edge
1339
+ itself appended (anything left of it came from the client, and is never
1340
+ believed):
1248
1341
  - loopback, or one of this machine's own interface addresses (a LAN
1249
- address, or a VPN's such as a Tailscale `100.x`): local;
1342
+ address, or a VPN's such as a Tailscale `100.x`): local, no sign-in;
1250
1343
  - missing, or a container/VM network's address (the Docker ranges, or a
1251
1344
  subnet of one of this machine's Docker/OrbStack/VM bridges): Den can't
1252
1345
  tell (Docker Desktop and OrbStack on macOS hand the edge their
1253
1346
  gateway's address, and a TCP passthrough in front of the edge hides the
1254
- client), so local;
1255
- - any other address: another machine, which needs a sign-in.
1256
-
1347
+ client), so it signs in: `web.edge_unattested`, below;
1348
+ - any other address: another machine, which signs in.
1349
+
1350
+ `web.edge_unattested: local` in `config/local/user.yml` counts a client
1351
+ Den can't place as local again (0.4.4's behaviour). It is read only from
1352
+ that file (a choice for that machine); the distribution config's key is
1353
+ ignored with a warning. The default is `signin`. A request straight to
1354
+ the server's port on localhost (loopback Host and peer) never signs in.
1257
1355
  With `DEN_BIND_HOST` beyond loopback (a deliberate exposure, other than
1258
- the Linux edge's Docker bridge address) the edge needs a sign-in as
1259
- before. With `forwardedHeaders.insecure` (the edge's default) Traefik
1260
- keeps an `X-Forwarded-For` the client sent, so this is a convenience,
1261
- not a boundary: whoever can reach the edge can use the terminal.
1356
+ the Linux edge's Docker bridge address) the edge always signs in.
1357
+
1358
+ - **The edge believes forwarded headers only from its own networks.** Den
1359
+ renders each entrypoint with `forwardedHeaders.trustedIPs` set to
1360
+ loopback and the Docker ranges (`127.0.0.1/32`, `::1/128`,
1361
+ `172.16.0.0/12`, `192.168.65.0/24`), never `forwardedHeaders.insecure`,
1362
+ so a client can't hand the edge an `X-Forwarded-For` of its own. An
1363
+ `edge.args` entry that sets `forwardedHeaders` or `proxyProtocol` is
1364
+ dropped, and `edge up` says so.
1365
+ - **A TCP passthrough in front of the edge** (a gateway that publishes
1366
+ `:443` and passes TLS through) hides the client: the edge sees the
1367
+ gateway's address. Have the gateway send the PROXY protocol, and list it
1368
+ in `edge.proxy_protocol.trusted_ips` (empty by default; IP addresses or
1369
+ CIDRs; `config/local/user.yml`'s list replaces the team's):
1370
+
1371
+ ```yaml
1372
+ edge:
1373
+ proxy_protocol:
1374
+ trusted_ips: [172.18.0.5]
1375
+ ```
1376
+
1377
+ Every entrypoint then takes the PROXY protocol from those addresses, and
1378
+ the client it carries is what Den sees. Configuring the gateway is the
1379
+ gateway's job.
1380
+
1381
+ - **`edge up` recreates the edge when its command changed.** It compares
1382
+ the running edge's command with the one it renders and says why ("acme-edge's
1383
+ settings changed (forwarded-header trust): recreating it once. Routes
1384
+ drop only while it restarts."). Upgrading to 0.4.5 does this once.
1262
1385
 
1263
1386
  - **"Your edge is reachable from other machines".** When Den can tell, at
1264
1387
  start, that the edge is reachable beyond this machine (an edge hostname
1265
1388
  that resolves to an address that isn't loopback, such as a LAN or
1266
1389
  Tailscale address, or the edge container publishing a port on every
1267
1390
  interface, from `docker inspect`), it writes "Your edge is reachable from
1268
- other machines; anyone who can reach it can use terminals. Auth is
1269
- coming." once to `web.log`, and every page shows it as a banner (dismissed
1270
- per browser).
1391
+ other machines. A browser it can't place as this one signs in first
1392
+ (Sign in with GitHub, or a pairing link)." once to `web.log`, and every
1393
+ page shows it as a banner (dismissed per browser). With
1394
+ `web.edge_unattested: local` it says instead that anyone it can't place
1395
+ can use terminals, and to remove the key to have them sign in.
1271
1396
  - **localhost is the canonical name.** A cookie set on `localhost` isn't
1272
1397
  sent to `127.0.0.1`, so the links Den prints use `localhost`.
1273
1398
  - **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
@@ -1301,6 +1426,76 @@ logout --all` the old token is refused; the CLI reads the new one.
1301
1426
  421 `misdirected`, so a LAN client can't send the edge's name straight to
1302
1427
  the server's port.
1303
1428
 
1429
+ ## Signing in with GitHub
1430
+
1431
+ A Den has one owner: a GitHub account. Once it is linked, that account
1432
+ signs in from any device (a phone, another laptop, a browser through the
1433
+ edge) with **Sign in with GitHub** on the signed-out page. Den stores only
1434
+ the account's id and login, never a token, and no other account gets in.
1435
+
1436
+ ```bash
1437
+ ./bin/acme web owner link # link the owner: GitHub's device flow, in the terminal
1438
+ ./bin/acme web owner # show who it is (also: web owner show, --json)
1439
+ ./bin/acme web owner unlink # forget the owner (signed-in browsers stay signed in)
1440
+ ```
1441
+
1442
+ - **`web owner link`** prints `https://github.com/login/device` and a code.
1443
+ Enter the code there and approve; Den asks GitHub who approved, stores
1444
+ `owner: github:<id>` and the login in `web-owner.json` (0600, in the
1445
+ state dir) and throws the token away. A second link is refused until
1446
+ `web owner unlink`. Settings › Access does the same in the browser
1447
+ (Link your GitHub account, Unlink): shell-grade actions, for a signed-in
1448
+ browser and never an agent's token.
1449
+ - **Sign in with GitHub** on the signed-out page shows a code and
1450
+ `github.com/login/device`. The server runs the flow, and when the owner
1451
+ approves it sets the same never-expiring session cookie a one-time link
1452
+ sets, and the page reloads signed in. Another account sees "@<login>
1453
+ isn't allowed on this Den". The token is thrown away either way. At most
1454
+ three sign-ins wait for GitHub at once; a code expires as GitHub says
1455
+ (15 minutes). With no owner linked the page says to run
1456
+ `web owner link`.
1457
+ - **Settings › Access** lists the linked devices (the browser, how it
1458
+ signed in, the client address when the edge vouched for one, created and
1459
+ last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
1460
+ open terminals included). **Pair a device** makes a one-time link and a
1461
+ QR code for an edge hostname you pick (`web login --host <name>` in the
1462
+ browser): scan it on the other device within 60 seconds.
1463
+ - **`web.github_client_id`** is the GitHub App's client id (public; no
1464
+ secret). The default is Den's own App, **Monster Orange Den**
1465
+ (`Iv23liKL12gnN6WvRV5b`), so Sign in with GitHub works out of the box. A
1466
+ distribution that ships its own App sets its client id here (below);
1467
+ `config/local/user.yml`'s wins over the team's. An empty value
1468
+ (`github_client_id: ""`) turns Sign in with GitHub off: the page then
1469
+ offers the code and the one-time link only.
1470
+ - **Expiring tokens.** If the App hands out expiring tokens (GitHub's
1471
+ answer carries `expires_in` or a `refresh_token`), sign-in still
1472
+ completes, and Den says once, in the log and on stderr, to switch off
1473
+ token expiry in the App's settings ("Expire user authorization tokens");
1474
+ it never prints the token. `GET /api/web/owner` reports it as
1475
+ `tokenExpiry: true`.
1476
+
1477
+ ### The GitHub App (once, by whoever ships the Den)
1478
+
1479
+ Den's default App is Monster Orange Den. A distribution that wants its own
1480
+ (its name on GitHub's approval page) needs a GitHub App with the device
1481
+ flow on. Create it once (organization or account › Settings › Developer settings › GitHub
1482
+ Apps › New GitHub App):
1483
+
1484
+ 1. No callback URL; untick "Request user authorization during
1485
+ installation". Webhook off.
1486
+ 2. **Enable Device Flow**: ticked. The device flow needs only the client
1487
+ id.
1488
+ 3. **Expire user authorization tokens**: off. Den never asks for
1489
+ `offline_access`, and keeps no token either way.
1490
+ 4. Account permissions: none. Den reads only `GET /user` (the id and the
1491
+ login), which needs none. Repository permissions are only for features
1492
+ that act on a repository (Den's default App has Issues for feedback).
1493
+ 5. **Where can this GitHub App be installed**: Any account. GitHub lets only
1494
+ members of the owning organization authorize a private App, so anyone
1495
+ else couldn't sign in.
1496
+ 6. Put the **Client ID** in `web.github_client_id`. Never generate or
1497
+ share a client secret or a private key for Den: it never uses one.
1498
+
1304
1499
  ## Running the web UI as a service (macOS)
1305
1500
 
1306
1501
  Den ships a launchd installer that runs the web UI at login and restarts it if
@@ -1448,19 +1643,40 @@ port.
1448
1643
  `--workspace <ws>` and `--actor agent:<id>` (both or neither) scope the server
1449
1644
  to one workspace for one agent:
1450
1645
 
1451
- - **Pinned.** A tool that takes a `workspace` gets `<ws>` when none is given.
1452
- A call naming another workspace is refused (`Refused: <tool>: this MCP
1453
- server is scoped to workspace "<ws>" (agent:<id>; no grants); "<other>" is
1454
- another workspace`; with grants, `grants: <a>, <b>`) before anything reaches the web server. A refused call is
1455
- recorded in the activity trail, read tools included.
1646
+ - **Pinned.** A tool that takes a `workspace` gets `<ws>`. The server
1647
+ registers each workspace tool with "This workspace only" in its
1648
+ description and without the `workspace` parameter (with grants it stays,
1649
+ a name whose description lists `<ws>` and the granted ones; any other
1650
+ name gets the refusal below, not a schema error). A call naming another
1651
+ workspace is refused
1652
+ (`Refused: <tool>: this MCP server is scoped to workspace "<ws>"
1653
+ (agent:<id>; no grants); "<other>" is another workspace`; with grants,
1654
+ `grants: <a>, <b>`) before anything reaches the web server. The refusal
1655
+ says how to get more: a person adds `<other>` to `agents.<id>.grants` in
1656
+ the org config and restarts the agent; an agent can't grant itself one. A
1657
+ refused call is recorded in the activity trail, read tools included.
1658
+ - **Says so.** Every scoped answer (`status`, `services`, `applications`,
1659
+ `graph`, `stacks`, `workspaces`) carries a `scope` block: the workspace
1660
+ shown, how many other workspaces there are and how much it leaves out,
1661
+ for example `hidden: { repos: 6 }`, and a note pointing at `org_summary`.
1662
+ A scoped `graph` is the workspace's own repos and the edges out of them
1663
+ (`map: "workspace"`, with `outside` naming the repos they depend on
1664
+ elsewhere). Unscoped with no `workspace` it is the org-wide map
1665
+ (`map: "org-wide"`: every active workspace's graph merged, `workspaces`
1666
+ naming them); with one, that workspace's (`map: "workspace"`).
1456
1667
  - **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
1457
1668
  take only `<ws>`'s services, `manifest_validate` takes only an absolute path
1458
1669
  inside one of its repos (or the content), and `workspace_sync` syncs `<ws>`.
1459
1670
  Tests run against the focused workspace, so `tests_available` and
1460
1671
  `run_tests` are refused unless `<ws>` is the focused one.
1461
1672
  - **Not workspace data**, served as unscoped: `environments`,
1462
- `security_latest`, `release_status` and `edge_status` (the org's and the
1463
- distribution's, not a workspace's).
1673
+ `security_latest`, `release_status`, `edge_status` and `org_summary` (the
1674
+ org's and the distribution's, not a workspace's).
1675
+ - **The org at a glance.** `org_summary` gives every agent every workspace
1676
+ and repo with status counts, the graph, the edge and the environments
1677
+ (see [Tools](#tools)). Detail and actions stay scoped: for workspaces
1678
+ other than `<ws>` it carries names and counts only, never logs, env
1679
+ values, commands, URLs, paths or check output.
1464
1680
  - **Stamped.** Every web API call carries `x-den-mcp-agent: agent:<id>` and
1465
1681
  `x-den-mcp-workspace: <ws>` beside the usual audit headers. The activity
1466
1682
  entry gains `agent` and `workspace`, and the log line reads `· by
@@ -1479,7 +1695,9 @@ agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
1479
1695
  - `<ws>` must exist in this distribution; until it does, every tool says so.
1480
1696
  The id is letters, digits, dots, dashes and underscores (at most 64). A bad
1481
1697
  or half-given scope is a usage error, exit 2. `--list` and `--call` take
1482
- the scope too.
1698
+ the scope too: they list and validate against the pinned tools, as the
1699
+ server registers them, and `--call` names its scope on stderr
1700
+ (`acme mcp --call: scoped to <ws> as agent:<id>`).
1483
1701
 
1484
1702
  **It binds cooperating agents only.** The scope lives in the MCP server's
1485
1703
  process. The web server's HTTP API does not check it yet, so an agent that
@@ -1525,22 +1743,60 @@ guard off.
1525
1743
 
1526
1744
  Read tools (always registered):
1527
1745
 
1528
- | Tool | Web API | What |
1529
- | ------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1530
- | `status` | `/api/health` | Service health; `url` (HTTP check) or `check` (shell, or `container <project>/<service>`); `ok` true up, false down, null not known (`pending: true` when a check has not answered) |
1531
- | `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
1532
- | `applications` | `/api/applications` | Org > Application > Repo > Services |
1533
- | `graph` | `/api/graph` | The derived dependency graph and startup order |
1534
- | `workspaces` | `/api/workspaces` | Workspaces, active and focused |
1535
- | `environments` | `/api/environments` | The enabled environment names (never their values) |
1536
- | `logs_search` | `/api/logs/search` | Search services' full log history |
1537
- | `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
1538
- | `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
1539
- | `security_latest` | `/api/security/latest` | The newest security scan per mode |
1540
- | `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
1541
- | `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
1542
- | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health |
1543
- | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names |
1746
+ | Tool | Web API | What |
1747
+ | ------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1748
+ | `status` | `/api/health` | Service health: `counts`, then each service's `state` (`up`, `down`, `redirect`, `pending`, `unknown`, `not-checked`) and, without a plain verdict, `why`; `url` (HTTP check) or `check` (shell, or `container <project>/<service>`); `ok` true up, false down, null not known |
1749
+ | `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
1750
+ | `applications` | `/api/applications` | Org > Application > Repo > Services |
1751
+ | `graph` | `/api/graph` | The derived dependency graph (`map: "org-wide"`, every active workspace's; with `workspace`, that one's and its startup order; scoped, the workspace's own) |
1752
+ | `workspaces` | `/api/workspaces` | Workspaces, active and focused |
1753
+ | `environments` | `/api/environments` | The enabled environment names (never their values) |
1754
+ | `logs_search` | `/api/logs/search` | Search services' full log history |
1755
+ | `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
1756
+ | `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
1757
+ | `security_latest` | `/api/security/latest` | The newest security scan per mode |
1758
+ | `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
1759
+ | `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
1760
+ | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health; an empty list says `why` |
1761
+ | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names; `https` (TLS strategy, who supplies the cert, whether HTTPS is serving) |
1762
+ | `org_summary` | several (read only) | Every workspace and repo with status counts, the graph, the edge and the environments; scoped, `yours` adds your workspace's services |
1763
+
1764
+ **Health states.** A service with no verdict says why in `why`: no health
1765
+ check in the manifest for the env, remote with no check or `target.url`
1766
+ for its env, not deployed in the env (no commands and no check there), no
1767
+ manifest, or checked but no answer yet. A redirect (`302 → /login`) is
1768
+ `redirect` with its status and target (the query is dropped), not down:
1769
+ something answered. It is never green either, so an unexpected redirect
1770
+ (an ALB answering for a stopped service) is not taken for up; list the code
1771
+ in `target.expect` to count it as up. The web health row and `<cli> status`
1772
+ use the same rule: the web row shows a redirect muted (with `status` and `redirect`
1773
+ in `/api/health`), and `<cli> status` as `redirect 302 → /login`.
1774
+
1775
+ **`org_summary`** reads `/api/workspaces`, `/api/health` (local, every
1776
+ active workspace), `/api/graph`, `/api/edge` and `/api/environments`:
1777
+
1778
+ - `workspaces`: each workspace's name, whether it is active, focused or
1779
+ yours, its state (`running`: every checked service answers; `partial`:
1780
+ some are down; `stopped`: all checked ones are down; `unknown`: nothing
1781
+ checked; `inactive`: not polled), its counts (up, down, redirect,
1782
+ pending, not checked) and its repos, each with its rolled-up state.
1783
+ - `graph`: the org-wide map, every active workspace's repos merged (each
1784
+ workspace's graph from the web server; `workspaces` names them, and
1785
+ `unavailable` any whose graph failed), repo names and edge kinds only (no
1786
+ evidence, paths or URLs).
1787
+ - `edge`: up, state, domains, the route count and `https`.
1788
+ - `environments`: the enabled names, never their values.
1789
+ - scoped, `yours`: your workspace's services as `status` gives them.
1790
+
1791
+ **`edge_status.https`** says which TLS strategy is in use (`mkcert`,
1792
+ `files`, `none`), who supplies the cert (`den`, or `distribution` under
1793
+ `tls: files` and the legacy `issue_cert: false`) and whether HTTPS is
1794
+ serving: a TLS handshake on the edge's published https port (`serving`
1795
+ true or false, null when a proxy in front owns the port), with `detail`.
1796
+ `certIssuedByDen` replaces `certIssued`, which is kept for 0.4.4 with a
1797
+ note in `deprecated`: it said whether Den minted the cert, so it was
1798
+ `false` under `tls: files` while HTTPS worked. `edge status` prints the
1799
+ same `https` line.
1544
1800
 
1545
1801
  Action tools (only those named in `mcp.actions`): `service_up`,
1546
1802
  `service_down`, `service_restart` (through `/api/service/exec`),
@@ -1658,7 +1914,7 @@ server scoped to its workspace.
1658
1914
  <cli> agents stop <id> [-w <ws>]
1659
1915
  <cli> agents resume <id> [-w <ws>]
1660
1916
  <cli> agents personas [--json]
1661
- <cli> agents doctor [--json] # can each runtime run here, and why not
1917
+ <cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
1662
1918
  <cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
1663
1919
  ```
1664
1920
 
@@ -1708,9 +1964,10 @@ started. The workspace header counts them ("2 agents").
1708
1964
  - The page and the CLI call the same operations and read the same agent
1709
1965
  records, so an agent started in one is listed in the other. A workspace
1710
1966
  lists its own agents only.
1711
- - Start, Stop and Resume need the lock open (🔓, the same rule as a
1712
- service's Start), are capped by `agents.max_concurrent`, and are refused
1713
- for an agent (agents don't start agents from the page). Each one is in the
1967
+ - Start, Stop and Resume never ask for the admin password (it is for
1968
+ sudo, and no agent action runs sudo), are capped by
1969
+ `agents.max_concurrent`, and are refused for an agent (agents don't
1970
+ start agents from the page). Each one is in the
1714
1971
  server log's activity, with you as its actor. The agent's environment is
1715
1972
  the one below, built from the web server's environment.
1716
1973
  - **Open in Agents →** (the section header) opens the Agents tab for the
@@ -1720,7 +1977,7 @@ The routes: `GET /api/workspaces/<ws>/agents`, `POST
1720
1977
  /api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, id? }`), `POST
1721
1978
  /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, and `GET
1722
1979
  /api/personas` (personas and runtimes). Errors are the usual
1723
- `{ ok: false, error, code, requestId }`: `locked` (423), `cap` (429),
1980
+ `{ ok: false, error, code, requestId }`: `cap` (429),
1724
1981
  `conflict` (409, e.g. already running, or no resume), `not_found` (404),
1725
1982
  `exited_at_once` (502, with the session's last lines).
1726
1983
 
@@ -1743,12 +2000,10 @@ agent of the workspaces in view, and their terminals in the browser.
1743
2000
  reload or Back reopens them.
1744
2001
  - **Close** (×) detaches, as leaving the page does: the session keeps
1745
2002
  running, and opening it again replays what you missed. **Stop** (an
1746
- agent only) is `agents stop`, behind the lock. **+ Start agent** (or
2003
+ agent only) is `agents stop`. **+ Start agent** (or
1747
2004
  `/agents?start=1`) starts one and opens its pane; under All it asks for
1748
2005
  the workspace. It says before Start whether the runtime can run, as the
1749
- Workspaces page's form does. While the lock is closed, the dialog asks
1750
- for the admin password at its top, and Start unlocks, then starts, without
1751
- closing anything.
2006
+ Workspaces page's form does. It never asks for the admin password.
1752
2007
  - ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
1753
2008
  - Several browsers and `<cli> sessions attach` can attach to one session
1754
2009
  at once and each sees every byte; the terminal's size is the last
@@ -1813,8 +2068,9 @@ header (agent headers without the token are recorded as an unverified
1813
2068
  claim).
1814
2069
 
1815
2070
  An agent's token reaches only its own session's routes, and never a
1816
- shell-grade route, `/api/web/*`, sign-in or sign-out, or the lock: it can't
1817
- unlock, use the sudo password or start another agent (403
2071
+ shell-grade route, `/api/web/*`, sign-in or sign-out, or the admin
2072
+ password's routes: it can't set the admin password, use it for sudo or
2073
+ start another agent (403
1818
2074
  `agent_forbidden`). A person's credential never reaches a session:
1819
2075
  `DEN_PERSON_TOKEN`, a person token's value and a path to `web.secret` are
1820
2076
  always removed, and `sessions start --keep-env` of one is refused and logged.
@@ -1835,7 +2091,8 @@ it does not catch every spelling: a path relative to the agent's cwd
1835
2091
  (`cat ../.den/web.secret`, say), another program, a script or a copy under
1836
2092
  another name all get past it. The server's token rules (above)
1837
2093
  are the boundary: whatever an agent reads, its token reaches only its own
1838
- session's routes, never a shell-grade route or the lock. OS-user isolation
2094
+ session's routes, never a shell-grade route or the admin password's.
2095
+ OS-user isolation
1839
2096
  (a separate user, a container, a sandbox) is not in 0.4.x. `web logout
1840
2097
  --all` revokes every agent's token (above, under Signing in).
1841
2098
 
@@ -1884,7 +2141,8 @@ are errors naming the file or the key.
1884
2141
 
1885
2142
  ### Runtimes
1886
2143
 
1887
- `<cli> doctor` lists the runtime kind. Two are built in:
2144
+ `<cli> doctor` lists the runtime kind. Den knows the runtimes in its
2145
+ **catalog** (below); two are special:
1888
2146
 
1889
2147
  - **`den-claude`** (the default) runs Claude Code's `claude`, found as
1890
2148
  below; `<cli> doctor` reads its version from the install, without running
@@ -1899,14 +2157,64 @@ are errors naming the file or the key.
1899
2157
  - **`generic`** runs the persona's `argv` template. `{model}` is the agent's
1900
2158
  model (an error when the template uses it and there is none);
1901
2159
  `{mcp_config}` is the path of the agent's MCP config, written only when the
1902
- template references it. It declares no resume.
2160
+ template references it. It declares no resume. It is startable only for
2161
+ a persona that has an `argv`.
2162
+
2163
+ **The runtime catalog.** Den ships, as data, every vendor CLI it knows how
2164
+ to find. Each entry was checked against the vendor's own docs or installer:
2165
+
2166
+ | id | Vendor | CLI | Found in (after PATH) | Source |
2167
+ | ------------ | --------- | --------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
2168
+ | `den-claude` | Anthropic | Claude Code, `claude` | `~/.local/bin`, `~/.claude/local`, system dirs | [setup](https://code.claude.com/docs/en/setup) |
2169
+ | `codex` | OpenAI | Codex CLI, `codex` | `$CODEX_INSTALL_DIR`, `~/.local/bin`, system dirs | [openai/codex](https://github.com/openai/codex), [installer](https://chatgpt.com/codex/install.sh) |
2170
+ | `gemini` | Google | Gemini CLI, `gemini` | system dirs | [google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) |
2171
+ | `grok` | xAI | Grok Build, `grok` | `$GROK_BIN_DIR`, `~/.grok/bin`, `~/.local/bin`, `/usr/local/bin` | [Grok Build](https://docs.x.ai/build/overview), [installer](https://x.ai/cli/install.sh) |
2172
+
2173
+ "System dirs" are `/opt/homebrew/bin`, `/usr/local/bin` and npm's global
2174
+ bin. Each one's version is `<bin> --version`. Every vendor here publishes
2175
+ an official CLI, so none is left out; a vendor without one is left out
2176
+ rather than guessed at (community CLIs, such as third-party "grok" CLIs,
2177
+ are not xAI's and are not in the catalog).
2178
+
2179
+ Claude Code runs as `den-claude` (above). Every other installed runtime
2180
+ runs its own interactive CLI (`codex`, `gemini`, `grok`) in the
2181
+ workspace's directory; Den's tools (MCP) and permission modes are coming
2182
+ for them, and lists tag them **Den tools: coming**. A runtime is offered
2183
+ as startable only when it can start here: installed (or, for `generic`,
2184
+ with the persona's `argv`).
2185
+
2186
+ A distribution adds entries with `agents.runtime_catalog` in its config,
2187
+ and a person with the same key in `config/local/user.yml`. Both go
2188
+ through the validator the built-in entries pass; an entry can't reuse a
2189
+ catalog id (set a path instead, below):
2190
+
2191
+ ```yaml
2192
+ agents:
2193
+ runtime_catalog:
2194
+ - id: aider # lowercase, starts with a letter
2195
+ vendor: Aider
2196
+ name: Aider
2197
+ bins: [aider] # executable names, no paths
2198
+ version:
2199
+ args: [--version]
2200
+ identity: # at least one: how a path is known to be it
2201
+ output: "aider" # a pattern its version output matches
2202
+ # package: aider-chat (the npm package it resolves into)
2203
+ # realpath_under: [~/.aider] (where it really lives)
2204
+ dirs: [~/.local/bin, $AIDER_DIR, "{system}"]
2205
+ launch:
2206
+ argv: ["{bin}"] # "{bin}" first: the path found
2207
+ ```
1903
2208
 
1904
2209
  **Where a runtime's CLI is found.** As your terminal would find it, not
1905
2210
  only on the web server's own PATH (a launchd service's PATH leaves out
1906
2211
  `~/.local/bin`, where Claude Code's native installer puts `claude`). The
1907
2212
  first match wins:
1908
2213
 
1909
- 1. **`agents.runtimes.<id>.bin`** in `config/local/user.yml`: the absolute
2214
+ 1. **A workspace's own path**, `<ws>.agents.runtimes.<id>.bin` in
2215
+ `config/local/workspaces.yml`, for that workspace's agents. Checked the
2216
+ same way as the next one.
2217
+ 2. **`agents.runtimes.<id>.bin`** in `config/local/user.yml`: the absolute
1910
2218
  path of the runtime's CLI. Paths are per machine, so it is read from
1911
2219
  user.yml only, never from the distribution's config. A bad value (not
1912
2220
  absolute, missing, not an executable file) makes the runtime
@@ -1920,14 +2228,28 @@ first match wins:
1920
2228
  bin: /Users/me/.local/bin/claude
1921
2229
  ```
1922
2230
 
1923
- 2. **Your login shell's PATH**: the web server asks `$SHELL -lc` once at
2231
+ 3. **Your login shell's PATH**: the web server asks `$SHELL -lc` once at
1924
2232
  start, in the background, under a 3-second timeout. On a timeout or
1925
2233
  any failure it is left out.
1926
- 3. **The server's own PATH.**
1927
- 4. **The runtime's install directories.** For den-claude: `~/.local/bin`,
1928
- `~/.claude/local`, `/opt/homebrew/bin`, `/usr/local/bin` and npm's
1929
- global bin (`$npm_config_prefix/bin`, and node's own directory). Den
1930
- never runs npm to find it.
2234
+ 4. **The server's own PATH.**
2235
+ 5. **The runtime's install directories** (its catalog entry's; for
2236
+ den-claude: `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin`,
2237
+ `/usr/local/bin` and npm's global bin, `$npm_config_prefix/bin` and
2238
+ node's own directory). Den never runs npm to find it.
2239
+ 6. **Your interactive shell's PATH**, only while a catalog runtime is
2240
+ still not found: `$SHELL -ilc` once (stdin closed, 3-second timeout,
2241
+ the answer kept), for a PATH set only in an interactive startup file
2242
+ such as `~/.zshrc`. Its source is `interactive-shell`.
2243
+
2244
+ Paths are this machine's only: Den reads them from `config/local` files,
2245
+ never from the distribution's config. **Settings › Agents › Runtimes**
2246
+ shows each runtime's status, version, the path in use and its source.
2247
+ **Set path…** runs the runtime's version command and refuses a path that
2248
+ isn't that runtime (its output, npm package or install location must name
2249
+ it); **Clear** removes it. The **Paths for** selector switches between
2250
+ every workspace (`user.yml`) and one workspace (`workspaces.yml`). Only a
2251
+ person sets a path, never an agent (`POST /api/agents/runtimes/<id>/path`,
2252
+ audited).
1931
2253
 
1932
2254
  Sessions and agents Den starts get the same directories on their PATH
1933
2255
  (and a configured CLI's own directory), so the runtime's own tools are
@@ -1936,17 +2258,19 @@ diagnosis: `DEN_LOGIN_SHELL` (the shell to ask instead of `$SHELL`; `off`
1936
2258
  for none) and `DEN_RUNTIME_DIRS` (a PATH-style list that replaces every
1937
2259
  runtime's install directories; empty for none).
1938
2260
 
1939
- **`<cli> agents doctor`** prints, for each runtime, its status, the CLI it
1940
- runs and where that came from (the `agents.runtimes` setting, the login
1941
- shell's PATH, PATH, or an install directory), its version (`claude
2261
+ **`<cli> agents doctor`** prints, for each runtime (installed first), its
2262
+ status, the CLI it runs and where that came from (the workspace's or the
2263
+ user.yml `agents.runtimes` setting, the login shell's PATH, PATH, an
2264
+ install directory, or the interactive shell's PATH), its version (`claude
1942
2265
  --version`, run once under a timeout), the directories it searched (names
1943
2266
  only, never an environment value), and the fix when it's missing. It
1944
2267
  resolves the login shell's PATH first, as the web server does. It exits 1
1945
- when the default runtime is unavailable; `--json` prints what the Start
1946
- dialog gets.
2268
+ when the default runtime is unavailable; `-w <ws>` uses that workspace's
2269
+ paths; `--json` prints what the Start dialog gets. A search list the same
2270
+ as the one above it prints once.
1947
2271
 
1948
2272
  ```
1949
- den-claude (Claude Code) [default]
2273
+ den-claude (Claude Code, Anthropic) [default]
1950
2274
  status ✗ unavailable: claude not found (searched 5 directories)
1951
2275
  searched /usr/bin
1952
2276
  /bin
@@ -1955,9 +2279,12 @@ den-claude (Claude Code) [default]
1955
2279
  then run `<cli> agents doctor` again
1956
2280
  ```
1957
2281
 
1958
- `GET /api/personas` gives each runtime its `version`, `path`, `source`
1959
- (`setting`, `login-shell`, `well-known` or `PATH`), `searched` and
1960
- `setting` (the user.yml key), and `userConfig`, the user.yml to edit.
2282
+ `GET /api/personas` and `GET /api/agents/runtimes` (each with an optional
2283
+ `?workspace=<ws>`) give each runtime its `vendor`, `denTools`, `version`,
2284
+ `path`, `source` (`workspace`, `setting`, `login-shell`, `PATH`,
2285
+ `well-known` or `interactive-shell`), `searched`, `setting` (the user.yml
2286
+ key) and `workspaceSetting` (the workspaces.yml key), and `userConfig`,
2287
+ the user.yml to edit.
1961
2288
 
1962
2289
  ### Workspace scope and grants
1963
2290
 
@@ -1979,6 +2306,26 @@ with both workspaces named. Any key of `agents:` other than `max_concurrent`
1979
2306
  and `persona_packs` is an agent id, and its only key is `grants`. A grant
1980
2307
  naming a workspace that doesn't exist stops `start`, naming the key.
1981
2308
 
2309
+ ## Dev metrics: local unless you opt in
2310
+
2311
+ Den records each command it runs (its name, surface, duration and exit
2312
+ code, never arguments or values) as one JSON line in
2313
+ `.dev-metrics/events.jsonl` under the distribution root. That file stays
2314
+ on the machine.
2315
+
2316
+ Nothing is sent anywhere unless the engineer opts in, per machine:
2317
+
2318
+ ```yaml
2319
+ # config/local/user.yml (gitignored)
2320
+ metrics:
2321
+ share: true
2322
+ ```
2323
+
2324
+ Only the boolean `true` counts. With it, and a Sentry DSN in
2325
+ `DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
2326
+ also sent, best effort. A DSN on its own never sends. `CI=true` and
2327
+ `DEV_METRICS_DISABLED=1` stop sending even when opted in.
2328
+
1982
2329
  ## Pinning and upgrading
1983
2330
 
1984
2331
  `package.json` pins an exact Den version (`create` writes the version that