@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.
- package/CHANGELOG.md +593 -0
- package/docs/distributions.md +429 -18
- package/package.json +6 -1
- package/src/shared/actor-token.ts +275 -0
- package/src/shared/agent-actor.ts +12 -2
- package/src/shared/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +271 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +199 -0
- package/src/shared/agents/runtimes.ts +54 -0
- package/src/shared/den-config.ts +116 -0
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/scope.ts +22 -0
- package/src/shared/environment-manager/service-runner.ts +94 -2
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/profile.ts +15 -4
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/sessions/client.ts +284 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +89 -0
- package/src/shared/sessions/host.ts +690 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- package/src/shared/web-secret.ts +181 -0
- package/src/shared/web-session.ts +491 -0
- package/src/ui/cli/agent-ops.ts +843 -0
- package/src/ui/cli/agents-command.ts +294 -0
- package/src/ui/cli/doctor-command.ts +138 -9
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +32 -3
- package/src/ui/cli/index.ts +128 -12
- package/src/ui/cli/sessions-command.ts +750 -0
- package/src/ui/cli/target-workspace.ts +105 -0
- package/src/ui/cli/web-command.ts +536 -14
- package/src/ui/mcp/api.ts +7 -0
- package/src/ui/mcp/scope.ts +123 -19
- package/src/ui/mcp/server.ts +100 -7
- package/src/ui/mcp/tools.ts +44 -5
- package/src/ui/server/agent-routes.ts +323 -0
- package/src/ui/server/audit.ts +111 -22
- package/src/ui/server/http-errors.ts +21 -3
- package/src/ui/server/index.ts +820 -65
- package/src/ui/server/public/agents-page.js +648 -0
- package/src/ui/server/public/agents-section.js +299 -0
- package/src/ui/server/public/agents.html +439 -0
- package/src/ui/server/public/appearance.js +3 -0
- package/src/ui/server/public/error-page.js +103 -0
- package/src/ui/server/public/header.js +45 -11
- package/src/ui/server/public/i18n/es.js +90 -1
- package/src/ui/server/public/i18n/ja.js +86 -1
- package/src/ui/server/public/local-dev.html +53 -0
- package/src/ui/server/public/logs.html +3 -0
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +8 -0
- package/src/ui/server/public/settings-page.js +5 -0
- package/src/ui/server/public/settings-registry.js +33 -0
- package/src/ui/server/public/shell.js +248 -5
- package/src/ui/server/public/topbar.js +51 -2
- package/src/ui/server/readonly-routes.ts +25 -2
- package/src/ui/server/request-guard.ts +670 -0
- package/src/ui/server/route-classes.ts +317 -0
- package/src/ui/server/session-routes.ts +445 -0
- package/src/ui/server/shutdown-guard.ts +91 -0
- package/src/ui/server/sudo-store.ts +29 -3
- package/src/ui/server/ui-assets.ts +10 -1
- package/src/ui/server/websocket.ts +326 -0
- package/src/ui/server/workspace-routes.ts +16 -22
package/docs/distributions.md
CHANGED
|
@@ -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
|
|
530
|
-
and whether it can work on this machine, with the reason.
|
|
531
|
-
|
|
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
|
|
540
|
-
tls
|
|
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
|
|
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
|
+
"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
|
}
|