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