@monster-orange/den 0.3.0 → 0.4.1

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 (79) hide show
  1. package/CHANGELOG.md +593 -0
  2. package/docs/distributions.md +429 -18
  3. package/package.json +6 -1
  4. package/src/shared/actor-token.ts +275 -0
  5. package/src/shared/agent-actor.ts +12 -2
  6. package/src/shared/agents/default-pack/be-dev.md +20 -0
  7. package/src/shared/agents/default-pack/devops.md +14 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  10. package/src/shared/agents/default-pack/planner.md +17 -0
  11. package/src/shared/agents/default-pack/reviewer.md +14 -0
  12. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  13. package/src/shared/agents/den-claude.ts +271 -0
  14. package/src/shared/agents/generic.ts +52 -0
  15. package/src/shared/agents/personas.ts +218 -0
  16. package/src/shared/agents/records.ts +118 -0
  17. package/src/shared/agents/runtime.ts +199 -0
  18. package/src/shared/agents/runtimes.ts +54 -0
  19. package/src/shared/den-config.ts +116 -0
  20. package/src/shared/engine.ts +23 -0
  21. package/src/shared/environment-manager/scope.ts +22 -0
  22. package/src/shared/environment-manager/service-runner.ts +94 -2
  23. package/src/shared/environment-manager/workspaces.ts +19 -0
  24. package/src/shared/profile.ts +15 -4
  25. package/src/shared/secret-env.ts +70 -0
  26. package/src/shared/sessions/client.ts +284 -0
  27. package/src/shared/sessions/den-pty.ts +97 -0
  28. package/src/shared/sessions/env.ts +89 -0
  29. package/src/shared/sessions/host.ts +690 -0
  30. package/src/shared/sessions/names.ts +85 -0
  31. package/src/shared/sessions/paths.ts +27 -0
  32. package/src/shared/sessions/protocol.ts +72 -0
  33. package/src/shared/sessions/pty.ts +141 -0
  34. package/src/shared/sessions/ring-buffer.ts +160 -0
  35. package/src/shared/sessions/types.ts +81 -0
  36. package/src/shared/web-secret.ts +181 -0
  37. package/src/shared/web-session.ts +491 -0
  38. package/src/ui/cli/agent-ops.ts +843 -0
  39. package/src/ui/cli/agents-command.ts +294 -0
  40. package/src/ui/cli/doctor-command.ts +138 -9
  41. package/src/ui/cli/env-probe.ts +266 -0
  42. package/src/ui/cli/flag-parser.ts +32 -3
  43. package/src/ui/cli/index.ts +128 -12
  44. package/src/ui/cli/sessions-command.ts +750 -0
  45. package/src/ui/cli/target-workspace.ts +105 -0
  46. package/src/ui/cli/web-command.ts +536 -14
  47. package/src/ui/mcp/api.ts +7 -0
  48. package/src/ui/mcp/scope.ts +123 -19
  49. package/src/ui/mcp/server.ts +100 -7
  50. package/src/ui/mcp/tools.ts +44 -5
  51. package/src/ui/server/agent-routes.ts +323 -0
  52. package/src/ui/server/audit.ts +111 -22
  53. package/src/ui/server/http-errors.ts +21 -3
  54. package/src/ui/server/index.ts +820 -65
  55. package/src/ui/server/public/agents-page.js +648 -0
  56. package/src/ui/server/public/agents-section.js +299 -0
  57. package/src/ui/server/public/agents.html +439 -0
  58. package/src/ui/server/public/appearance.js +3 -0
  59. package/src/ui/server/public/error-page.js +103 -0
  60. package/src/ui/server/public/header.js +45 -11
  61. package/src/ui/server/public/i18n/es.js +90 -1
  62. package/src/ui/server/public/i18n/ja.js +86 -1
  63. package/src/ui/server/public/local-dev.html +53 -0
  64. package/src/ui/server/public/logs.html +3 -0
  65. package/src/ui/server/public/panel.js +1 -1
  66. package/src/ui/server/public/release.html +8 -0
  67. package/src/ui/server/public/settings-page.js +5 -0
  68. package/src/ui/server/public/settings-registry.js +33 -0
  69. package/src/ui/server/public/shell.js +248 -5
  70. package/src/ui/server/public/topbar.js +51 -2
  71. package/src/ui/server/readonly-routes.ts +25 -2
  72. package/src/ui/server/request-guard.ts +670 -0
  73. package/src/ui/server/route-classes.ts +317 -0
  74. package/src/ui/server/session-routes.ts +445 -0
  75. package/src/ui/server/shutdown-guard.ts +91 -0
  76. package/src/ui/server/sudo-store.ts +29 -3
  77. package/src/ui/server/ui-assets.ts +10 -1
  78. package/src/ui/server/websocket.ts +326 -0
  79. package/src/ui/server/workspace-routes.ts +16 -22
@@ -525,10 +525,11 @@ later plugins.
525
525
 
526
526
  ## doctor
527
527
 
528
- `<cli> doctor` lists every strategy kind in the registry (secrets, tls):
529
- the one this distribution selects, whether nothing chose it (implicit),
530
- and whether it can work on this machine, with the reason. It only checks
531
- PATH: it never runs a backend CLI, prompts, or prints a secret. It exits
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
532
533
  1 when a strategy that will be used cannot work (an implicit secrets
533
534
  default is used only when a secret is asked for, so it never fails
534
535
  doctor).
@@ -536,11 +537,15 @@ doctor).
536
537
  ```
537
538
  Acme doctor: strategies
538
539
 
539
- secrets none ✓ available: no secrets backend configured
540
- tls mkcert (implicit) ✗ unavailable: mkcert is not installed
540
+ secrets none ✓ available: no secrets backend configured
541
+ tls mkcert (implicit) ✗ unavailable: mkcert is not installed
541
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
542
544
  ```
543
545
 
546
+ `session-backend` is never a doctor failure: nothing needs a session until
547
+ one is started (see Sessions).
548
+
544
549
  ## den.yml: the product profile
545
550
 
546
551
  Every runtime name derives from `product.name`. Set only what you want to
@@ -558,16 +563,16 @@ product:
558
563
  # workspaces_dir: acme-workspaces
559
564
  ```
560
565
 
561
- | Field | Default | What it names |
562
- | ---------------- | -------------------- | ----------------------------------------------------------------- |
563
- | `name` | `den` | The product; `[a-z][a-z0-9-]*`. Everything below derives from it |
564
- | `cli` | `<name>` | The command shown in help, hints and the web UI |
565
- | `env_prefix` | `<NAME>` (`-` → `_`) | Env vars: `<PREFIX>_ROOT`, `<PREFIX>_HOME`, `<PREFIX>_INVOKE_CWD` |
566
- | `config` | `config/<name>.yml` | The org config, relative to the distribution root |
567
- | `manifest` | `.<name>.yml` | The manifest file a repo carries; the `.json` twin is also read |
568
- | `state_dir` | `~/.<name>` | Per-user state: web server, daemons, logs, history |
569
- | `services_dir` | `.<name>-services` | Managed clones, relative to the distribution root |
570
- | `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 |
571
576
 
572
577
  Env vars, with `ACME` as the prefix:
573
578
 
@@ -1174,6 +1179,86 @@ Den's scripts run with the engine's tsx (`tsxCommand()`), never through `npx`
1174
1179
  or a path relative to the current directory. In Den's own checkout both roots
1175
1180
  are the same directory.
1176
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
+
1177
1262
  ## Running the web UI as a service (macOS)
1178
1263
 
1179
1264
  Den ships a launchd installer that runs the web UI at login and restarts it if
@@ -1323,8 +1408,8 @@ to one workspace for one agent:
1323
1408
 
1324
1409
  - **Pinned.** A tool that takes a `workspace` gets `<ws>` when none is given.
1325
1410
  A call naming another workspace is refused (`Refused: <tool>: this MCP
1326
- server is scoped to workspace "<ws>" (agent:<id>); "<other>" is another
1327
- workspace`) before anything reaches the web server. A refused call is
1411
+ server is scoped to workspace "<ws>" (agent:<id>; no grants); "<other>" is
1412
+ another workspace`; with grants, `grants: <a>, <b>`) before anything reaches the web server. A refused call is
1328
1413
  recorded in the activity trail, read tools included.
1329
1414
  - **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
1330
1415
  take only `<ws>`'s services, `manifest_validate` takes only an absolute path
@@ -1339,6 +1424,16 @@ workspace`) before anything reaches the web server. A refused call is
1339
1424
  entry gains `agent` and `workspace`, and the log line reads `· by
1340
1425
  agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
1341
1426
  <id>)` for every exec, run and sync.
1427
+ - **Granted.** `--grant <other>` (repeatable) also serves `<other>`, but only
1428
+ when the org config's `agents.<id>.grants` lists it; otherwise the server
1429
+ exits 2 (an agent can't grant itself one). A call naming a granted
1430
+ workspace is reported to the web server before it runs, and its activity
1431
+ entry names both (`workspace` and `targetWorkspace`; the log line reads
1432
+ `▶ <tool> in workspace <other>, under a grant to agent:<id> of workspace
1433
+ <ws>`). `GET /api/activity?address=workspace:<ws>` lists these entries
1434
+ under either workspace. `workspaces` lists `<ws>` and its grants, and tests run when either
1435
+ is focused. Logs and `manifest_validate` stay on `<ws>`'s own services.
1436
+ `agents start` passes an agent's grants for you (see [Agents](#agents)).
1342
1437
  - `<ws>` must exist in this distribution; until it does, every tool says so.
1343
1438
  The id is letters, digits, dots, dashes and underscores (at most 64). A bad
1344
1439
  or half-given scope is a usage error, exit 2. `--list` and `--call` take
@@ -1445,6 +1540,322 @@ authorise anything), and the MCP server's pid and session id. They travel as
1445
1540
  `acme/server` stream shows an agent's actions as `[agent]`, ending
1446
1541
  `· by agent <client> <version> (mcp pid <pid>, session <id>)`.
1447
1542
 
1543
+ ## Sessions
1544
+
1545
+ A session is a terminal that keeps running when the web server restarts:
1546
+ an argv in a PTY, owned by the distribution's **session host**. The host
1547
+ is one small detached process (`<cli> sessions host`) that the first
1548
+ client starts on demand; it serves `<stateDir>/sessions.sock` (mode 0600)
1549
+ and logs to `<stateDir>/logs/<cli>/sessions-host.log`. The state dir is
1550
+ the profile's `state_dir` (`~/.<name>`), or `<PREFIX>_HOME`.
1551
+
1552
+ ```
1553
+ <cli> sessions start --workspace <ws> --name <n> [--cwd <dir>] [--keep-env NAME…] -- <argv…>
1554
+ <cli> sessions list [-w <ws>] [--json]
1555
+ <cli> sessions attach <name|id> # ctrl-] detaches; the session keeps running
1556
+ <cli> sessions kill <name|id> [--signal INT|TERM|KILL]
1557
+ <cli> sessions host [--stop]
1558
+ <cli> agents list -w <ws> [--json]
1559
+ ```
1560
+
1561
+ - A session is named `<dist>-<ws>:<n>`. `list -w <ws>` and `agents list
1562
+ -w <ws>` show that workspace's sessions only. Elsewhere a session can be
1563
+ named in full, as `<ws>:<n>`, as a bare `<n>` only one session has, or by
1564
+ its id.
1565
+ - It runs in `--cwd` (default: where you ran the command). Its environment
1566
+ starts empty and gets only `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`,
1567
+ `TERM` (default `xterm-256color`), `COLORTERM`, `LANG`, `LC_*`, `TZ` and
1568
+ `TMPDIR` from yours; `start` names the variables it left out (names, never
1569
+ values). `--keep-env NAME` adds one more: one exact name per flag,
1570
+ repeatable, no globs. It is audited: the session host's log
1571
+ (`<stateDir>/logs/<cli>/sessions-host.log`) gets a line naming each kept
1572
+ variable and who asked, never its value, and says `credential-shaped` when
1573
+ the value or name looks like a credential (it is still passed: you named
1574
+ it). An agent can't use it: `agents start` has no `--keep-env`, and
1575
+ `sessions start` refuses it when run by an agent (`DEN_AGENT` is set).
1576
+ Every runtime's `scrubEnv` (den-claude: `CLAUDECODE`, `CLAUDE_CODE_*`) is
1577
+ removed last, even a name you kept.
1578
+ - Every attachment gets all of its output; attaching prints `[attached to
1579
+ <name>]`, then replays the scrollback (2 MB per session, kept in
1580
+ `<stateDir>/sessions/<id>.log`).
1581
+ - Stopping the host (`sessions host --stop`, or a reboot) ends its
1582
+ sessions. They stay listed as exited, with their scrollback, until
1583
+ `sessions kill` removes them.
1584
+ - `agents.max_concurrent` caps the sessions running at once (default 4):
1585
+
1586
+ ```yaml
1587
+ agents:
1588
+ max_concurrent: 4
1589
+ ```
1590
+
1591
+ - A PTY needs a native module, `@lydell/node-pty`, which Den lists as an
1592
+ optional dependency (prebuilt binaries for macOS, Linux and Windows,
1593
+ nothing downloaded at install). Without it (`npm install
1594
+ --omit=optional`, another platform) everything else works, and `sessions
1595
+ start` and `<cli> doctor` say why den-pty is unavailable.
1596
+ - A state dir whose socket path would pass 103 bytes (the unix socket
1597
+ limit) is refused with that reason; point `<PREFIX>_HOME` somewhere
1598
+ shorter.
1599
+
1600
+ ## Agents
1601
+
1602
+ An agent is a **persona** (who) in a **workspace** (where), run by a
1603
+ **runtime** (which CLI) in a [session](#sessions), acting through Den's MCP
1604
+ server scoped to its workspace.
1605
+
1606
+ ```
1607
+ <cli> agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]
1608
+ <cli> agents list -w <ws> [--json]
1609
+ <cli> agents stop <id> [-w <ws>]
1610
+ <cli> agents resume <id> [-w <ws>]
1611
+ <cli> agents personas [--json]
1612
+ <cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
1613
+ ```
1614
+
1615
+ - `start` runs the agent as session `<dist>-<ws>:<id>` in the workspace's
1616
+ checkout dir (its `directory`, else `den-workspaces/<ws>`), capped by
1617
+ `agents.max_concurrent`. The id defaults to the persona's id; an id is
1618
+ unique in the distribution, so a second workspace's planner gets
1619
+ `planner-<ws>` unless you give `--id`.
1620
+ - The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
1621
+ persona, runtime, model, workspace, cwd, session and times. It never holds
1622
+ the environment or the command line. `list` reads it for persona, runtime
1623
+ and model; a record whose session has gone is listed as `stopped` (after
1624
+ `stop`) or `ended`.
1625
+ - `list` shows agents only: sessions with an agent record. A session from
1626
+ `sessions start` is not an agent; `sessions list` shows it.
1627
+ - `stop` sends its session TERM and marks it stopped. `resume` works only
1628
+ when the runtime declares resume and the session is not running: it starts
1629
+ a new session in the same cwd that continues the last conversation. A
1630
+ session that never got a prompt (no input with a line end) has no
1631
+ conversation, so `resume` starts it fresh under the same conversation id
1632
+ and says so.
1633
+ - After `start` or `resume`, Den watches the session for up to 3 s. If it
1634
+ exits non-zero, Den prints the exit code and its last lines (at most 10,
1635
+ secret-shaped values redacted) and exits 1.
1636
+
1637
+ ### On the Workspaces page
1638
+
1639
+ The selected workspace's agents are listed under its repo table, one row
1640
+ per agent: its id and persona, runtime, state (`running`, `stopping`,
1641
+ `stopped`, `ended` or `exited (<code>)`), directory, and when its session
1642
+ started. The workspace header counts them ("2 agents").
1643
+
1644
+ - **Stop** sends its session TERM, as `agents stop` does; the row reads
1645
+ `stopping…` until the session has gone. **Resume** shows only when
1646
+ `agents resume` would run: the runtime declares resume and the agent is
1647
+ not running.
1648
+ - **+ Start agent** picks a persona (Den's pack, then
1649
+ `agents.persona_packs`) and a runtime (the persona's own unless you pick
1650
+ one; one that can't run here is listed, disabled), then Start.
1651
+ - The page and the CLI call the same operations and read the same agent
1652
+ records, so an agent started in one is listed in the other. A workspace
1653
+ lists its own agents only.
1654
+ - Start, Stop and Resume need the lock open (🔓, the same rule as a
1655
+ service's Start), are capped by `agents.max_concurrent`, and are refused
1656
+ for an agent (agents don't start agents from the page). Each one is in the
1657
+ server log's activity, with you as its actor. The agent's environment is
1658
+ the one below, built from the web server's environment.
1659
+ - **Open in Agents →** (the section header) opens the Agents tab for the
1660
+ workspace, and **Open** on a row with a session opens its terminal there.
1661
+
1662
+ The routes: `GET /api/workspaces/<ws>/agents`, `POST
1663
+ /api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, id? }`), `POST
1664
+ /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, and `GET
1665
+ /api/personas` (personas and runtimes). Errors are the usual
1666
+ `{ ok: false, error, code, requestId }`: `locked` (423), `cap` (429),
1667
+ `conflict` (409, e.g. already running, or no resume), `not_found` (404),
1668
+ `exited_at_once` (502, with the session's last lines).
1669
+
1670
+ ### The Agents tab
1671
+
1672
+ "Agents" in the rail (Develop, after Graph) is `/agents`: every session and
1673
+ agent of the workspaces in view, and their terminals in the browser.
1674
+
1675
+ - **The list** is grouped by workspace and follows the scope bar (or the
1676
+ page's `?ws=<ws>`, which "Open in Agents →" sets). Each row has its
1677
+ state, persona (a `sessions start` session is labelled "session"),
1678
+ runtime and directory, and "waiting" when it waits on you (once the
1679
+ session host can tell, AG-6a; not yet). An agent whose
1680
+ token `web logout --all` revoked says "token revoked": stop and resume
1681
+ it, or start it again.
1682
+ - **A pane** is the session's terminal: it shows the scrollback, then the
1683
+ live output, and your keys go to the session; its size follows the
1684
+ pane. Click more rows to tile up to four (a fifth closes the oldest).
1685
+ The URL holds the open panes (`/agents?open=<id>,<id>`), so a link, a
1686
+ reload or Back reopens them.
1687
+ - **Close** (×) detaches, as leaving the page does: the session keeps
1688
+ running, and opening it again replays what you missed. **Stop** (an
1689
+ agent only) is `agents stop`, behind the lock. **+ Start agent** (or
1690
+ `/agents?start=1`) starts one and opens its pane; under All it asks for
1691
+ the workspace.
1692
+ - ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
1693
+ - Several browsers and `<cli> sessions attach` can attach to one session
1694
+ at once and each sees every byte; the terminal's size is the last
1695
+ one's to change it.
1696
+ - Terminals need the optional PTY module (`<cli> doctor`); without it the
1697
+ page lists what there is and says so.
1698
+ - **Who can open one.** The terminal is shell-grade: on localhost the
1699
+ page's own Origin and CSRF token, beyond loopback a signed-in browser
1700
+ (the list says "Sign in to open terminals" otherwise), and an agent's
1701
+ token only its own session's. Each attach and detach is in the server
1702
+ log's activity, with its actor.
1703
+
1704
+ The routes: `GET /api/sessions` (`{ sessions, workspaces, host, backend,
1705
+ io }`) and the WebSocket `/api/sessions/<id>/io` (subprotocol `den.io`, the
1706
+ CSRF token as `den.csrf.<token>`): binary frames are output and input, a
1707
+ text frame `{"type":"resize","cols":c,"rows":r}` resizes, and the server
1708
+ sends `{"type":"session",…}` first and `{"type":"exit","exitCode":n}` when
1709
+ the session ends.
1710
+
1711
+ ### An agent's environment
1712
+
1713
+ An agent's environment starts empty and gets, in order:
1714
+
1715
+ 1. `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM` (default
1716
+ `xterm-256color`), `COLORTERM`, `LANG`, `LC_*`, `TZ` and `TMPDIR` from
1717
+ yours;
1718
+ 2. the variables its runtime names (`envPassthrough`; den-claude:
1719
+ `CLAUDE_CONFIG_DIR`);
1720
+ 3. the runtime's launch env;
1721
+ 4. `DEN_AGENT=agent:<id>` and `DEN_ACTOR_TOKEN` (below).
1722
+
1723
+ Then every runtime's `scrubEnv` is removed (den-claude: `CLAUDECODE` and
1724
+ `CLAUDE_CODE_*`), whatever put it there. Nothing else of yours reaches it:
1725
+ not your tokens, not your shell's other variables, and not the Claude Code
1726
+ session you started it from (its session id, messaging socket and token,
1727
+ and the child-session marker that turns transcripts off). The agent record
1728
+ still holds no env. The agent's MCP server gets the distribution's location
1729
+ (`DEN_ROOT`, `<PREFIX>_ROOT`, `<PREFIX>_HOME`, when set) in its own config,
1730
+ so it reaches the same distribution.
1731
+
1732
+ `<cli> doctor --probe` checks this through the real session host: it runs a
1733
+ short probe session for a plain session and for each available runtime's
1734
+ agent env (built as `agents start` builds it, running the probe instead of
1735
+ the runtime's CLI). The probe prints key names and whether each is
1736
+ credential-shaped, never values. Doctor exits 1, one row per finding, when a
1737
+ runtime's `scrubEnv` name or a credential-shaped variable reaches either.
1738
+ Without the PTY module it says the probe is unavailable; that is not a
1739
+ failure. Plain `doctor` never spawns anything; when agents are configured it
1740
+ adds a note to run `<cli> doctor --probe`.
1741
+
1742
+ ### An agent's token
1743
+
1744
+ `agents start` (the CLI or the Workspaces page) mints a random `den_at_…`
1745
+ token for the agent and puts it in the session as `DEN_ACTOR_TOKEN`. Den
1746
+ keeps only its hash (`actor-tokens.json`, 0600, in the state dir) with the
1747
+ agent, its workspace, its grants and its session. Stop revokes it, and so
1748
+ does the session's end; resume mints a new one and the old one stops
1749
+ working. The `<cli>` and the MCP server Den starts for the agent send it, so
1750
+ the server audits their calls as the agent, from the token, never from a
1751
+ header (agent headers without the token are recorded as an unverified
1752
+ claim).
1753
+
1754
+ An agent's token reaches only its own session's routes, and never a
1755
+ shell-grade route, `/api/web/*`, sign-in or sign-out, or the lock: it can't
1756
+ unlock, use the sudo password or start another agent (403
1757
+ `agent_forbidden`). A person's credential never reaches a session:
1758
+ `DEN_PERSON_TOKEN`, a person token's value and a path to `web.secret` are
1759
+ always removed, and `sessions start --keep-env` of one is refused and logged.
1760
+ An agent runs as your OS user, so it could still read `web.secret` itself;
1761
+ the token keeps what Den does on its behalf attributable and bounded.
1762
+ **The boundary is the server's token rules** (above), not a deny list.
1763
+
1764
+ As a cooperative mitigation, den-claude passes Claude Code a settings file
1765
+ (`--settings`) whose `permissions.deny` lists `Read` of `<state
1766
+ dir>/web.secret` and `<state dir>/sessions/**`, and `Bash` rules in Claude
1767
+ Code's wildcard form for the path (`Bash(*<state dir>/web.secret*)`, and
1768
+ each of `cat`, `head`, `tail`, `less`, `more`, `xxd`, `od`, `strings`,
1769
+ `base64` and `cp` followed by it), spelled as an absolute path, under `~`
1770
+ and under `$HOME`, with a path that has spaces also backslash-escaped. A
1771
+ deny list can't name every way to read a file (another program, a script,
1772
+ a copy under another name), so it is a speed bump. **It binds cooperating
1773
+ agents only**: an agent that ignores its permission config, or reads the
1774
+ file some other way, can still read them. OS-user isolation (a separate
1775
+ user, a container, a sandbox) is not in 0.4.x. `web logout --all` revokes every agent's token
1776
+ (above, under Signing in).
1777
+
1778
+ **What the scope does not do.** An agent's MCP scope binds cooperating
1779
+ agents only. A runtime with a shell can still run `<cli>` itself, outside
1780
+ the scoped MCP server, and reach other workspaces (with its own token, so
1781
+ it is audited as the agent); WS-3a closes that later. And the user's own Claude Code settings and hooks
1782
+ (`~/.claude/settings.json`, its hooks, a project's `.claude/`) load in a
1783
+ den-claude agent, as in any `claude` you start.
1784
+
1785
+ ### Personas
1786
+
1787
+ A persona is a markdown file: YAML front matter, then the role prompt. It is
1788
+ a Claude Code agent file, read as it is, plus a few keys of Den's own:
1789
+
1790
+ ```markdown
1791
+ ---
1792
+ name: planner # the id (or `id:`; default: the file name)
1793
+ description: Writes stories and their acceptance criteria. # or `role:`
1794
+ model: some-model # optional; `inherit` means none
1795
+ tools: Read, Grep # hints for the runtime; Den restricts nothing with them
1796
+ standing: true # starts with its workspace (later slices)
1797
+ context: [docs/specs, the design system] # what to read first
1798
+ runtime: generic # default den-claude
1799
+ permission_mode: plan # den-claude only; default `default`
1800
+ argv: ["some-cli", "--model", "{model}", "--mcp-config", "{mcp_config}"]
1801
+ ---
1802
+
1803
+ You are the planner. …
1804
+ ```
1805
+
1806
+ Den ships a default pack: `planner` and `orchestrator` (standing), `fe-dev`,
1807
+ `be-dev`, `devops`, `reviewer` and `ux-designer`. Add your own packs, read in
1808
+ order after Den's; a persona with the same id replaces the earlier one:
1809
+
1810
+ ```yaml
1811
+ agents:
1812
+ persona_packs: [personas, ../some-plugin] # relative to the distribution root
1813
+ ```
1814
+
1815
+ A pack is a directory of persona files. A pack with an `agents/` directory
1816
+ inside (a Claude Code plugin) is read from there, so a plugin's agents are
1817
+ used without converting them. Files without front matter (a README) are
1818
+ skipped; two files with one id in a pack, or a pack that isn't a directory,
1819
+ are errors naming the file or the key.
1820
+
1821
+ ### Runtimes
1822
+
1823
+ `<cli> doctor` lists the runtime kind. Two are built in:
1824
+
1825
+ - **`den-claude`** (the default) needs `claude` on PATH; doctor reads its
1826
+ version from the install, without running it. It runs `claude --session-id
1827
+ <id> --mcp-config <file> --strict-mcp-config --settings <file> [--model <m>]
1828
+ --append-system-prompt <role prompt>`, so Den's MCP server is the agent's
1829
+ only one, and resumes with `claude --resume <id>`. Permission prompts stay
1830
+ in the agent's terminal: it passes `--permission-mode default` unless the
1831
+ persona's `permission_mode` sets one of `default`, `acceptEdits`, `plan` or
1832
+ `bypassPermissions` (anything else is an error naming the key).
1833
+ `bypassPermissions` prints a one-line warning at every start.
1834
+ - **`generic`** runs the persona's `argv` template. `{model}` is the agent's
1835
+ model (an error when the template uses it and there is none);
1836
+ `{mcp_config}` is the path of the agent's MCP config, written only when the
1837
+ template references it. It declares no resume.
1838
+
1839
+ ### Workspace scope and grants
1840
+
1841
+ An agent's MCP config, `<stateDir>/agent-files/<id>/mcp.json` (0600), runs
1842
+ `<cli> mcp --workspace <ws> --actor agent:<id>`: the agent sees and acts on
1843
+ its own workspace only, and every action is recorded as `agent:<id>` (see
1844
+ [Scoped to one workspace](#scoped-to-one-workspace)). A grant widens one
1845
+ agent's scope. It is config in git, reviewed like code; an agent can't give
1846
+ itself one:
1847
+
1848
+ ```yaml
1849
+ agents:
1850
+ planner:
1851
+ grants: [other-workspace]
1852
+ ```
1853
+
1854
+ `start` then adds `--grant other-workspace`, and each call into it is audited
1855
+ with both workspaces named. Any key of `agents:` other than `max_concurrent`
1856
+ and `persona_packs` is an agent id, and its only key is `grants`. A grant
1857
+ naming a workspace that doesn't exist stops `start`, naming the key.
1858
+
1448
1859
  ## Pinning and upgrading
1449
1860
 
1450
1861
  `package.json` pins an exact Den version (`create` writes the version that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,6 +62,8 @@
62
62
  "@modelcontextprotocol/sdk": "1.32.1",
63
63
  "@playwright/test": "^1.61.1",
64
64
  "@slack/webhook": "^7.0.2",
65
+ "@xterm/addon-fit": "0.11.0",
66
+ "@xterm/xterm": "6.0.0",
65
67
  "ajv": "^8.12.0",
66
68
  "ajv-formats": "^2.1.1",
67
69
  "auth0": "^4.3.1",
@@ -111,5 +113,8 @@
111
113
  "homepage": "https://github.com/Monster-Orange/den",
112
114
  "bugs": {
113
115
  "url": "https://github.com/Monster-Orange/den/issues"
116
+ },
117
+ "optionalDependencies": {
118
+ "@lydell/node-pty": "1.1.0"
114
119
  }
115
120
  }