@monster-orange/den 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/README.md +88 -158
  3. package/docs/distributions.md +216 -16
  4. package/docs/reference.md +177 -0
  5. package/package.json +3 -1
  6. package/src/shared/actor-token.ts +275 -0
  7. package/src/shared/agent-actor.ts +12 -2
  8. package/src/shared/agents/den-claude.ts +99 -1
  9. package/src/shared/agents/runtime.ts +7 -0
  10. package/src/shared/agents/runtimes.ts +9 -2
  11. package/src/shared/den-config.ts +52 -3
  12. package/src/shared/environment-manager/scope.ts +22 -0
  13. package/src/shared/environment-manager/service-runner.ts +94 -2
  14. package/src/shared/profile.ts +15 -4
  15. package/src/shared/sessions/client.ts +16 -2
  16. package/src/shared/sessions/den-pty.ts +10 -2
  17. package/src/shared/sessions/env.ts +13 -3
  18. package/src/shared/sessions/host.ts +56 -8
  19. package/src/shared/sessions/types.ts +8 -0
  20. package/src/shared/web-secret.ts +181 -0
  21. package/src/shared/web-session.ts +491 -0
  22. package/src/ui/cli/agent-ops.ts +843 -0
  23. package/src/ui/cli/agents-command.ts +97 -615
  24. package/src/ui/cli/flag-parser.ts +21 -0
  25. package/src/ui/cli/index.ts +127 -11
  26. package/src/ui/cli/sessions-command.ts +49 -5
  27. package/src/ui/cli/target-workspace.ts +105 -0
  28. package/src/ui/cli/web-command.ts +542 -14
  29. package/src/ui/mcp/api.ts +7 -0
  30. package/src/ui/mcp/server.ts +74 -6
  31. package/src/ui/server/agent-routes.ts +324 -0
  32. package/src/ui/server/audit.ts +81 -27
  33. package/src/ui/server/http-errors.ts +21 -3
  34. package/src/ui/server/index.ts +821 -66
  35. package/src/ui/server/public/agents-page.js +654 -0
  36. package/src/ui/server/public/agents-section.js +299 -0
  37. package/src/ui/server/public/agents.html +439 -0
  38. package/src/ui/server/public/appearance.js +3 -0
  39. package/src/ui/server/public/error-page.js +103 -0
  40. package/src/ui/server/public/header.js +45 -11
  41. package/src/ui/server/public/i18n/es.js +90 -1
  42. package/src/ui/server/public/i18n/ja.js +86 -1
  43. package/src/ui/server/public/local-dev.html +53 -0
  44. package/src/ui/server/public/logs.html +3 -0
  45. package/src/ui/server/public/panel.js +1 -1
  46. package/src/ui/server/public/release.html +8 -0
  47. package/src/ui/server/public/settings-page.js +5 -0
  48. package/src/ui/server/public/settings-registry.js +33 -0
  49. package/src/ui/server/public/shell.js +248 -5
  50. package/src/ui/server/public/topbar.js +51 -2
  51. package/src/ui/server/request-guard.ts +670 -0
  52. package/src/ui/server/route-classes.ts +317 -0
  53. package/src/ui/server/session-routes.ts +445 -0
  54. package/src/ui/server/shutdown-guard.ts +91 -0
  55. package/src/ui/server/sudo-store.ts +29 -3
  56. package/src/ui/server/ui-assets.ts +10 -1
  57. package/src/ui/server/websocket.ts +326 -0
  58. package/src/ui/server/workspace-routes.ts +13 -7
package/CHANGELOG.md CHANGED
@@ -4,6 +4,463 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.4.2] - 2026-11-06
8
+
9
+ The 0.4.1 carry-overs.
10
+
11
+ ### Added
12
+
13
+ - **`sessions.max_concurrent`** (default 8): the cap on plain sessions
14
+ (`sessions start`) running at once. Past it, `sessions start` is refused
15
+ as the agents' cap is: "2 sessions are running and
16
+ sessions.max_concurrent is 2", then "Stop one first (`<cli> sessions
17
+ kill <name>`), or raise sessions.max_concurrent." `<cli> validate` names
18
+ a wrong value of it or of `agents.max_concurrent`.
19
+
20
+ ### Changed
21
+
22
+ - **`agents.max_concurrent` counts agents only.** Plain sessions count
23
+ against `sessions.max_concurrent` instead, so a few plain shells never
24
+ stop an agent from starting (from the CLI, the Workspaces page, the
25
+ Agents tab or MCP). Its refusal says "1 agent is running and
26
+ agents.max_concurrent is 1".
27
+ - **The Agents tab's pane title and Close** name the agent by its id
28
+ (its display name once AG-10 adds one), never its persona; the list row
29
+ still shows the persona, with the id beside it.
30
+ - **den-claude's deny list, in the docs**: it is a convenience, not the
31
+ boundary. It binds cooperating agents only and does not catch every
32
+ spelling (a path relative to the agent's cwd, another program); the
33
+ server's token rules are the boundary.
34
+
35
+ ### Fixed
36
+
37
+ - **A distribution config's `web.allow_plain_http_remote`** (ignored: it is
38
+ read only from `config/local/user.yml`) was warned about only in the
39
+ in-app server log. It is now in `web.log` too (before a bind refusal it
40
+ often explains), in the `web up` and `web restart` output, and in
41
+ `<cli> validate`.
42
+ - **`up --help`**: `-w, --workspace <ws>` ran into its text ("<ws>act in
43
+ that…"); there are two spaces now. `down`, `restart` and `setup --help`
44
+ were already spaced.
45
+
46
+ ### Migrations
47
+
48
+ - **More than 4 plain sessions at once**, or a script that relied on
49
+ `agents.max_concurrent` to cap plain sessions: set
50
+ `sessions.max_concurrent` (default 8). `agents.max_concurrent` no longer
51
+ counts them. A session host already running from 0.4.1 keeps counting
52
+ every session until it restarts (`sessions host --stop`, which ends its
53
+ sessions, or a reboot).
54
+
55
+ ## [0.4.1] - 2026-11-06
56
+
57
+ ### Added
58
+
59
+ - **The Agents tab** (0.4.1-dev.5, AG-4a-term, AG spec §5d): "Agents" in
60
+ the rail under Develop, after Graph, at `/agents`. The list holds every
61
+ session and agent of the workspaces in view (the scope bar, or `?ws=`),
62
+ grouped by workspace: state, persona and runtime, and a waiting badge
63
+ (shown once the host can tell, AG-6a);
64
+ a plain session (`sessions start`) is labelled "session", an agent whose
65
+ token `web logout --all` revoked says "token revoked". Click a row to
66
+ open its terminal (xterm.js, served from Den's own node_modules: no CDN):
67
+ the scrollback, then the live output, and your keys go to the session.
68
+ Up to four panes tile (a fifth closes the oldest), and the URL holds them
69
+ (`/agents?open=<id>,<id>`), so a reload or Back reattaches each with its
70
+ scrollback. A pane's bar has its name, workspace and runtime, Stop (an
71
+ agent, behind the lock) and Close, which detaches: the session keeps
72
+ running, as it does when you leave the page. "+ Start agent" (or
73
+ `/agents?start=1`) asks for the workspace under All, starts it and opens
74
+ its pane. ⌘K offers "Agents › <ws> / <name>" for each and "Start agent…".
75
+ The Workspaces page's Agents section links to it ("Open in Agents →",
76
+ and Open on a row with a session). In English, Japanese and Spanish;
77
+ light and dark; one column on a phone. Without the optional PTY module
78
+ the list says terminals need it, and nothing else changes.
79
+ - **`GET /api/sessions` and the `/api/sessions/<id>/io` WebSocket**
80
+ (0.4.1-dev.5). The list: the sessions and agents of the active
81
+ workspaces, this distribution's only (asking never starts a session
82
+ host). The io socket is the session host's attach: binary frames carry
83
+ output and input, a text frame `{"type":"resize","cols":c,"rows":r}`
84
+ resizes, and the server says `{"type":"session",…}` first and
85
+ `{"type":"exit","exitCode":n}` at the end. Closing it detaches and never
86
+ signals the session; several browsers and the CLI can attach at once
87
+ (the size is the last one's). It is shell-grade: this server's Host, the
88
+ page's Origin, the CSRF token as the `den.csrf.<token>` subprotocol (a
89
+ browser's WebSocket can't send a header), a sign-in beyond loopback, and
90
+ an agent token reaches its own session's io only. Every attach and
91
+ detach is in the audit trail, with the actor from the credential. A
92
+ plain GET of it is 426.
93
+
94
+ - **`web logout --all` revokes every agent's token** (0.4.1-dev.4, SEC-1
95
+ §6c.2): it is the "something leaked" button, so it ends everything. A
96
+ running agent's next call gets 401 `token_revoked`: "This agent's token
97
+ was revoked (`web logout --all`): restart or resume the agent to get a
98
+ new one." (its stdio MCP server refuses to start with one). The output
99
+ says how many it revoked. Plain `web logout` (one browser, or every
100
+ browser) leaves agent tokens alone.
101
+ - **den-claude denies Den's state dir to the agent** (0.4.1-dev.4, SEC-1
102
+ §6c.1): `--settings <file>` (0600) with `permissions.deny` for `Read` and
103
+ `Bash` of `<state>/web.secret` and `<state>/sessions/**`. It binds
104
+ cooperating agents only: an agent runs as the same OS user, and the
105
+ server's token rules are the boundary.
106
+ - **`setup`, `up`, `down` and `restart` across active workspaces**
107
+ (0.4.1-dev.4). `-w <workspace>` names the workspace (`setup` takes one
108
+ now; several still go with `--plan`). With no `-w`, a named service
109
+ resolves in the focus first, then across the other active workspaces, as
110
+ the web API's `&workspace=` does: in exactly one, it runs there (the CLI
111
+ says which); in several, an error lists them with the `-w` command for
112
+ each; in none, or names in different workspaces, an error. Each exits 1
113
+ and runs nothing. Before, a service in an active workspace that wasn't
114
+ the focus made `setup` print nothing and exit 0, `restart` say "not
115
+ found" and `up` say "no derived dependencies".
116
+
117
+ - **Agent actor tokens** (0.4.1-dev.3, SEC-1c, SEC-1 spec §1, §5, §7).
118
+ - **`agents start`** (the CLI and `POST /api/workspaces/<ws>/agents`)
119
+ mints a random `den_at_…` token for the agent and hands it to the
120
+ session as `DEN_ACTOR_TOKEN`, through the §5a-env allowlist's Den
121
+ markers (a caller's, a runtime's or a `--keep-env` value of it is
122
+ dropped). Only its sha256 is stored, in `actor-tokens.json` (0600) in
123
+ the state dir, with the agent, its workspace, its grants and its
124
+ session. One live token per agent: **resume** mints a fresh one and the
125
+ old one stops working; **stop** revokes it, and so does the session's
126
+ end (an exit or a crash), checked when the token is presented.
127
+ - **The scrub always strips a person's credential** from every session's
128
+ env, whatever put it there: `DEN_PERSON_TOKEN`, any value carrying a
129
+ person token, a path to `web.secret`. **`sessions start --keep-env`**
130
+ of one (or of `DEN_ACTOR_TOKEN`) is refused, exit 1, and the session
131
+ host's log records the refusal and who asked (the name, never a value).
132
+ - **The server accepts the agent's bearer.** The CLI inside an agent
133
+ sends its `DEN_ACTOR_TOKEN`, never the person token (an agent without
134
+ one sends none). The audit actor comes from the credential: an entry
135
+ reads `actor: "agent"` only for a verified agent token. Agent headers
136
+ (`X-Den-Agent`, …) next to another credential, or alone, are recorded
137
+ as `claimed` with `unverified: true`, and the Activity panel shows
138
+ them as "(unverified)".
139
+ - **What an agent token can't do**: open another session's routes
140
+ (`/api/sessions/<id>/…`: only its own session; the routes themselves
141
+ come with the terminal), any other shell-grade route (start, resume,
142
+ the session admin routes, so it mints no tokens), `/api/web/*`,
143
+ `/auth`, `/signin`, sign out or rotate; and it never passes the lock
144
+ (R8): no unlock, no sudo password, no prewarmed sudo, and the lock reads
145
+ closed to it. Each answers 403 `agent_forbidden`. A workspace check
146
+ (WS-3a) has its seam in the request guard, allowing everything for now.
147
+ - **The stdio MCP server Den starts for an agent** carries that agent's
148
+ token: its calls are audited as the agent, and `--actor` /
149
+ `--workspace` that don't match the token's record refuse to start.
150
+ - **Signing in the browser** (0.4.1-dev.2, SEC-1b, SEC-1 spec §2, §4). No
151
+ password: reading `web.secret` proves you are this computer's user, and
152
+ the CLI does it.
153
+ - **`<cli> web open`** starts the server if needed and opens a fresh tab
154
+ on a sign-in link; **`<cli> web up`** opens one too when it doesn't find
155
+ an existing tab. **`<cli> web login [--next <path>] [--origin <url>]`**
156
+ prints a fresh link, for another browser.
157
+ The link is `http://localhost:<port>/auth?c=<code>&next=<path>`: the
158
+ code works once, within 60 seconds, and is an HMAC of a random nonce
159
+ and its expiry under `web.secret` (the server records the nonce as
160
+ used). It is printed only on that one line and never logged. `next`
161
+ must be a same-origin path (`//evil`, `http://evil`, `/\evil` go to
162
+ `/`).
163
+ - **`GET /auth?c=…`** swaps the code for a session and answers 303 to
164
+ `next`. A bad, expired or used code gets the signed-out page: "That
165
+ sign-in link expired or was used. Run `<cli> web login` for a new one."
166
+ - **The cookie** `den_session_<port>` (an opaque random id; per port, so
167
+ two distributions on one machine keep their own): HttpOnly,
168
+ SameSite=Strict, Path=/, and Secure when the request came over HTTPS
169
+ (the edge's `X-Forwarded-Proto` counts only on the edge hostname from
170
+ a loopback or Docker peer). The record is server-side, in
171
+ `web-sessions.json` (0600) in the state dir, keyed by an HMAC of the id
172
+ under the secret: created, last seen, the user agent (truncated), the
173
+ host it was issued on and its own CSRF token. It never expires
174
+ (0.4.1-dev.4, above).
175
+ - **`GET /api/session`** answers `{ csrf, actor: "person-browser",
176
+ signedIn: true }` for a signed-in browser, with the session's own CSRF
177
+ token (a signed-in browser's mutations must carry that one). Signed out
178
+ on loopback it answers as before.
179
+ - **The route classes (§4).** On loopback no route needs a sign-in
180
+ (0.4.1-dev.4): read-only is open; mutating and **shell-grade** (an
181
+ agent's start and resume, the session admin routes, later the terminal)
182
+ need Origin and CSRF (as in SEC-1a), or the person's bearer (the CLI and
183
+ MCP need no setup). Beyond loopback (the edge hostname, a LAN name, or
184
+ a peer that isn't loopback): every route needs a session or a bearer,
185
+ so a LAN or edge Host is usable again once signed in. Only `/auth`,
186
+ `/signin`, `/api/session` and the UI's static files need none.
187
+ - **The signed-out page** (beyond loopback), in the shell frame: "Sign in
188
+ to use <name>", "Run `<cli> web login` and open the link." with a copy
189
+ button (through the edge the command names its origin), and on
190
+ `127.0.0.1` or `[::1]` a link to the same page on `localhost`, where the
191
+ cookie is. It never offers a form for a secret or a token. `/api`
192
+ answers 401 `signin_required` (R26's shape). In en, ja and es.
193
+ - **localhost is canonical**: the links Den prints use it, and following
194
+ the signed-out page's link from 127.0.0.1 lands signed in.
195
+ - **`<cli> web sessions [--json]`** lists the signed-in browsers (id
196
+ prefix, created, last seen, host, user agent); **`<cli> web logout
197
+ [<id prefix>]`** signs out one, or every browser; **`--all`** also
198
+ rotates `web.secret`, which ends every session and every person token
199
+ (the CLI re-reads it and keeps working), and (0.4.1-dev.4) revokes every
200
+ agent's token. They ask the running server
201
+ with the person token, or edit the state files when it isn't running.
202
+ - **Settings → Command line → Browser sign-in** says whether this
203
+ browser is signed in, with **Sign out** (`POST /api/session/logout`).
204
+ - **A non-loopback bind refuses to start** (`DEN_BIND_HOST=0.0.0.0` or a
205
+ LAN address) without HTTPS in front (the edge, with `edge.tls`), unless
206
+ `web.allow_plain_http_remote: true` is in `config/local/user.yml`: the
207
+ error says a session cookie would cross the network in clear text.
208
+ With it, the machine's own addresses and host name are accepted with
209
+ the port.
210
+ - **Request checks on every request** (0.4.1-dev.1, SEC-1a, SEC-1 spec §3).
211
+ Hardening for everyone; no sign-in yet (that is SEC-1b).
212
+ - **Host allowlist** (DNS rebinding): `localhost`, `127.0.0.1` and
213
+ `[::1]` with the server's port, plus the edge hostnames that route to
214
+ Den. Anything else answers 421 `misdirected` (R26's JSON for `/api`, the
215
+ shell-frame error page for a page, which links to the same path on
216
+ `localhost`). The product's own name is accepted when `/etc/hosts` maps
217
+ it to 127.0.0.1 (the URL `web up` opens then), and so is a specific
218
+ address `DEN_BIND_HOST` binds.
219
+ - **Origin** on POST, PUT, PATCH, DELETE and every WebSocket upgrade: an
220
+ allowed host, http or https. A missing Origin needs a valid
221
+ `Authorization`. Otherwise 403 `origin_rejected`.
222
+ - **CSRF**: the shell sends `X-Den-CSRF` on every same-origin mutation,
223
+ through `denShell.api()` or a page's own `fetch()`, with the token the
224
+ new `GET /api/session` hands out (`{ csrf, actor }`, same-origin callers
225
+ only). The token is per server start; after a restart the shell fetches
226
+ a fresh one and retries once. A browser mutation without it answers 403
227
+ `csrf`.
228
+ - **GETs that run something** (they stream over GET, for EventSource):
229
+ `/api/service/exec`, `/api/run`, `/api/workspace/exec`,
230
+ `/api/workspace/sync` and `/api/aws/login`. They answer this server's
231
+ own pages only (`Sec-Fetch-Site: same-origin`, or `none` for a typed
232
+ URL), or a caller with neither header and a valid bearer (the CLI,
233
+ MCP, curl). Another site's `<img src>` or no-cors fetch answers 403
234
+ `cross_site` (or `origin_rejected`), and nothing runs.
235
+ - **One route table** (`route-classes.ts`): every route the server
236
+ dispatches is `readonly`, `mutating` or `shell-grade` (§4), by method
237
+ and path; the checks read it, and SEC-1b will too. A test scans the
238
+ dispatchers and fails on a route the table doesn't classify.
239
+ - Other GETs, pages and SSE streams are unchanged on loopback (§6).
240
+ - **`web.secret` and the CLI's person token** (SEC-1a, §5). The server
241
+ creates `web.secret` (32 random bytes, mode 0600) in the state dir at
242
+ start. The CLI and the stdio MCP server send `Authorization: Bearer
243
+ den_pt_…`, derived from it, on every call: nothing to set up. The server
244
+ verifies it in constant time; an invalid bearer answers 401
245
+ `unauthorized`. The audit entry of a call made with it gains `principal:
246
+ "person:<os user>"`, from the credential, never from a header.
247
+ `<cli> web token --print` prints it for curl (stdout only, with a
248
+ one-line note on stderr that it grants your access).
249
+ - **Agents on the Workspaces page** (0.4.1-dev.0, AG-4a, AG spec §5c).
250
+ Under the selected workspace's repo table, one row per agent: id and
251
+ persona, runtime, state (`running`, `stopping`, `stopped`, `ended`,
252
+ `exited (<code>)`), directory and when its session started. **Stop**, and
253
+ **Resume** where the record allows it (the runtime declares resume and the
254
+ agent isn't running). **+ Start agent** picks a persona and a runtime. The
255
+ workspace header counts them ("n agents"). In en, ja and es.
256
+ - Routes: `GET /api/workspaces/<ws>/agents`, `POST
257
+ /api/workspaces/<ws>/agents` (start), `POST
258
+ /api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, `GET
259
+ /api/personas`. They call the CLI's own operations (the new
260
+ `agent-ops.ts`, which `<cli> agents` is now a front for): neither keeps
261
+ state of its own, and a workspace lists its own agents only.
262
+ - Guards: the lock (R8) gates Start, Stop and Resume (423 `locked`;
263
+ the page opens the lock first, as for a service action),
264
+ `agents.max_concurrent` caps starts (429 `cap`), and an agent actor is
265
+ refused (403). Errors are R26's shape and reach the page as error
266
+ toasts. The agent's env is the CLI's allowlist (§5a-env), and every
267
+ action is audited with the person as its actor.
268
+ - Listing never starts a session host: with none running, every agent
269
+ reads `ended`.
270
+ - No terminal and no Open in dev.0 to dev.4: dev.5 adds them (the Agents
271
+ tab, above).
272
+
273
+ ### Changed
274
+
275
+ - **No browser sign-in on loopback, for any route** (0.4.1-dev.4; Ward,
276
+ 2026-11-06; SEC-1 Q-SEC-1). On localhost the agents (start, resume), the
277
+ browser-session routes, `/signin` and, later, the terminal need only what
278
+ any change needs: this server's Host, the page's Origin and its CSRF
279
+ token. The lock (R8) stays the only prompt. A sign-in is still required
280
+ for every route when the server is reached beyond loopback: the edge
281
+ hostname, a LAN name or address, or a peer that isn't loopback (a
282
+ `DEN_BIND_HOST` bind). The Host allowlist, the Origin check,
283
+ `X-Den-CSRF`, `web.secret`, the person token and agent tokens (and what
284
+ an agent token can't do) are unchanged.
285
+ - **A browser session never expires** (0.4.1-dev.4; SEC-1 Q-SEC-2): it ends
286
+ at sign-out, `web logout` or `web logout --all`. The cookie asks to be
287
+ kept 400 days, a browser's most. `web.session.idle` and
288
+ `web.session.max` are gone: if either is still in a config file the
289
+ server logs that it is ignored, naming the key and the file, and starts.
290
+ - **Health covers every active workspace, whatever is in view**
291
+ (0.4.1-dev.4, WS-6.4). `/api/health` and its stream check every active
292
+ workspace's services; each row carries `inView` (one of its workspaces is
293
+ in the view scope). The top bar's chip and drawer count only the rows in
294
+ view; the pages get every row, so the Workspaces page shows a workspace's
295
+ real health when it is outside the view (it read "unknown (not
296
+ checked)").
297
+ - **The stdio MCP server's startup line says `unverified`** (0.4.1-dev.4,
298
+ SEC-1 §6c.3) when no `DEN_ACTOR_TOKEN` verified its `--actor`: "scoped
299
+ to north as agent:planner (unverified: no DEN_ACTOR_TOKEN)".
300
+ - **`/signin` is read-only** (0.4.1-dev.3). Signed in, or on loopback, it
301
+ answers 303 to its (same-origin) `next`; signed out beyond loopback, the
302
+ signed-out page. It was shell-grade, so the route's own check never ran
303
+ beyond loopback.
304
+ - **`web up` and `web restart` print the server's refusal** (0.4.1-dev.3):
305
+ when the server exits at start (a non-loopback bind without HTTPS, a
306
+ wrong `web.allow_plain_http_remote` value) the CLI prints "<name> web refused to start:
307
+ <the reason line>" and then where the log is, instead of waiting out
308
+ "did not become ready". Secrets in the line are redacted.
309
+ - **`web sessions --help`** says that LAST SEEN is written at most once a
310
+ minute, so it equals CREATED right after a sign-in, and that a session
311
+ never expires.
312
+ - **`agents list` shows `stopping`** for an agent that was stopped while its
313
+ session is still on its way out (it was `running`), and its `--json` rows
314
+ gain `startedAt` and `resumable`.
315
+
316
+ ### Fixed
317
+
318
+ - **A non-loopback bind needs `web.allow_plain_http_remote`, edge or no
319
+ edge** (0.4.1-dev.5, a dev.4 dogfood finding). With the edge configured,
320
+ `DEN_BIND_HOST=0.0.0.0` started, but the server's own port still answered
321
+ plain HTTP on every interface. It is refused now unless
322
+ `web.allow_plain_http_remote: true` is in `config/local/user.yml`; the
323
+ one exception is the Docker bridge address (`172.16.0.0/12`, as
324
+ `DEN_BIND_HOST=172.17.0.1`) with the edge configured, for an edge that
325
+ reaches the host there (Linux). The edge on macOS reaches Den on
326
+ loopback and needs neither.
327
+ - **The edge hostname is believed only from the edge** (0.4.1-dev.5): a
328
+ request whose `Host` is the edge hostname from a peer the edge can't be
329
+ (not loopback, the Docker bridge or Docker Desktop's network) is 421
330
+ `misdirected`. A LAN client could send the edge's name straight to a
331
+ non-loopback bind and be treated as coming through the edge.
332
+ - **`web.allow_plain_http_remote` in the distribution config is said to be
333
+ ignored** (0.4.1-dev.5): it is read only from `config/local/user.yml` (a
334
+ choice for one machine); the server logs a warning naming the file it
335
+ found it in, and the refusal names the file to set it in.
336
+ - **den-claude's Bash denies match** (0.4.1-dev.5): Claude Code 2.1.x asked
337
+ for approval of `cat <state>/web.secret` instead of denying it; the
338
+ legacy `Bash(cat <path>:*)` shape didn't match. The rules are Claude
339
+ Code's wildcard form now (`Bash(*<path>*)`, and `cat`, `head`, `tail`,
340
+ `less`, `more`, `xxd`, `od`, `strings`, `base64` and `cp` of it), with
341
+ the path under `~` and `$HOME` too, and a path with spaces escaped. The
342
+ `Read` rules are unchanged.
343
+ - **`restart <repo>/<svc>` of a single-service repo restarts it** after
344
+ `up <repo>` (0.4.1-dev.5, a dev.4 dogfood finding). The record was kept
345
+ under `<repo>`, so the restart found none, adopted the running process
346
+ and printed ✔ without restarting anything, leaving two records on one
347
+ process group. For a repo with one service, `<repo>` and `<repo>/<svc>`
348
+ are one daemon now; a duplicate left by dev.4 is merged at the next
349
+ command; and a restart that adopts a process restarts it.
350
+ - **`up`, `down`, `restart` and `setup --help` mention `-w <workspace>`**
351
+ (0.4.1-dev.5).
352
+ - **A subcommand's `--help` is its own** (0.4.1-dev.4): `web sessions`,
353
+ `web login`, `web logout` and `web token --help` (or `-h`) printed the
354
+ general web help, and `mcp connect|disconnect|clients|config --help`
355
+ printed `mcp`'s, because the dispatcher answered `<command> --help`
356
+ first. `web login`, `web logout` and `web token` have a help of their own
357
+ now (`web token --help` exited 2).
358
+ - **A relative `product.state_dir` is relative to den.yml's directory**
359
+ (0.4.1-dev.4), the distribution root. It was resolved against the cwd, so
360
+ an agent's CLI or MCP server started in its workspace's directory read
361
+ and wrote another state dir (another `web.secret`, other tokens).
362
+ - **Den's test servers are all tracked** (0.4.1-dev.4): every suite and
363
+ helper that starts a web or MCP server starts it in a process group of
364
+ its own and stops the group (tests/child-servers.ts), so none outlives a
365
+ run.
366
+ - **A command name that isn't declared never runs** (0.4.1-dev.3, SEC-1
367
+ §6b.2). `/api/service/exec?cmd=constructor` (or `toString`, any name an
368
+ object inherits) ran `undefined` as a shell command; the service and
369
+ workspace command lookups now read the declared commands only. `/api/run`
370
+ refuses a `module` that isn't a module name (a `..` segment, a slash at
371
+ the start), 400.
372
+ - **A web server whose output's reader is gone no longer spins**
373
+ (0.4.1-dev.3). When the terminal, script or test runner that started the
374
+ server was killed, the next log line failed with EPIPE and the "continue"
375
+ guard for uncaught errors logged that to the equally closed stderr, which
376
+ failed again: a loop at a full core that stopped the server answering,
377
+ for as long as the orphan lived. The server now writes nothing once its
378
+ output is gone, and shuts down (as for SIGTERM: services it started stay
379
+ up). A server `<cli> web up` starts logs to a file and is not affected.
380
+ - **The server's exit is bounded** (0.4.1-dev.3). A SIGTERM or SIGINT with
381
+ no prompt waiting on the person forces the exit after 10 s
382
+ (`DEN_SHUTDOWN_DEADLINE_MS`) if the shutdown hasn't finished, and a
383
+ second signal exits at once (it was ignored, so a stuck shutdown left the
384
+ server deaf to every later SIGTERM).
385
+ - **Den's own test servers never outlive a test run** (0.4.1-dev.3): they
386
+ start in a process group of their own, a test stops the whole group, and
387
+ the run's global teardown (or the next run's setup) stops any group a
388
+ killed or timed-out test left behind.
389
+ - **The Release page with no deploy environment** (`environments: [local]`
390
+ only) no longer asks `/api/release/status?env=undefined` (a 400 on every
391
+ load); it says no deploy environment is configured.
392
+ - **An agent's persona shows on every row** (0.4.1-dev.1), an id that is
393
+ its persona too ("planner persona planner").
394
+ - **The unlock popover names agents** (0.4.1-dev.1): "Unlock to start, stop
395
+ and restart services and agents", in en, ja and es.
396
+ - **Another workspace's agent is a 404 before the lock** (0.4.1-dev.1):
397
+ Stop or Resume of an agent id that belongs to another workspace answers
398
+ "no agent here" even while the lock is closed (it answered 423), and
399
+ still never names the other workspace.
400
+ - **The 421 page and the signed-out page start no polls** (0.4.1-dev.2, a
401
+ consumer's SEC-1a dogfood). Every `/api` call from them would answer the
402
+ same refusal, so the server marks them and the shell, as for an invalid
403
+ config, starts none of its polls and streams (health, the lock, the env
404
+ list, themes, the changelog, the stale-server check); the status chip
405
+ reads "Status unknown" instead of "Checking…" forever.
406
+ - **No config polls under an invalid config.** The server marks every page
407
+ it serves while the config fails validation, and the shell also learns it
408
+ from the first `422 config_invalid` answer. From then on it starts none of
409
+ the polls and streams that read the config (health and its stream, edge,
410
+ workspaces, applications, the org config, the sudo vault status, Logs'
411
+ services, Settings' workspace settings), so the browser console no longer
412
+ logs a "Failed to load resource … 422" line for each of them. A reload
413
+ once the config is fixed starts them again.
414
+
415
+ ### Migrations
416
+
417
+ - **Scripts that POST to `/api` with curl** need `Authorization: Bearer
418
+ $(<cli> web token --print)` or an `Origin` (SEC-1a), and so do curl GETs
419
+ of the routes that run something (`/api/service/exec`, `/api/run`,
420
+ `/api/workspace/exec`, `/api/workspace/sync`, `/api/aws/login`). Other
421
+ GETs work unchanged.
422
+ - **A Host other than `localhost`, `127.0.0.1`, `[::1]` or Den's edge
423
+ hostname** answers 421 (SEC-1a). A server exposed with
424
+ `DEN_BIND_HOST=0.0.0.0` goes through the edge, or sets
425
+ `web.allow_plain_http_remote: true` in `config/local/user.yml`: then the
426
+ machine's own addresses and host name work again, signed in (SEC-1b).
427
+ Without either, a non-loopback bind now refuses to start, saying why.
428
+ - **SEC-1: nothing asks for a sign-in on localhost.** Pages, read-only
429
+ calls, the shell's changes, agents and (later) the terminal work as
430
+ before; the lock (R8) is the only prompt. A sign-in is needed only when
431
+ the server is reached beyond loopback (below).
432
+ - **`web.session.idle` and `web.session.max` are ignored** (a session never
433
+ expires). Remove them; until then the server logs a warning naming each,
434
+ and starts.
435
+ - **Supported browsers for signing in**: Chromium, Firefox and Safari 16.4
436
+ or later. A browser that sends neither `Sec-Fetch-Site` nor `Origin`
437
+ (Safari before 16.4) can't sign in (it can still use localhost, where no
438
+ sign-in is asked).
439
+ - **Edge-fronted or remote users** see the signed-out page once; `<cli> web
440
+ login --origin <the edge's URL>` (the page shows the exact command) signs
441
+ that browser in.
442
+ - **Scripts that call `/api` with curl from loopback**: GETs work
443
+ unchanged; mutations need `Authorization: Bearer $(<cli> web token
444
+ --print)` or an `Origin`. Through the edge every call needs the bearer.
445
+ - **The GETs that run a service's, a workspace's or a test's command**
446
+ (`/api/service/exec`, `/api/run`, `/api/workspace/exec`,
447
+ `/api/workspace/sync`, `/api/aws/login`) are classed `mutating`, as §4
448
+ lists up, down and restart, not `shell-grade`: they need no sign-in on
449
+ loopback, and keep SEC-1a's same-origin-or-bearer check.
450
+ - **Audit entries (SEC-1c).** `actor: "agent"` now means a verified agent
451
+ token. Agent headers without one (an MCP server started with `--actor`
452
+ but no token, a script that sets `X-Den-Agent`) record the person (or
453
+ `user`) as the actor, with the headers under `claimed` and `unverified:
454
+ true`. A reader that filtered on `actor: "agent"` and the headers should
455
+ read `claimed` for those.
456
+ - **An agent's MCP server** is audited as the agent only when it inherits
457
+ the agent's `DEN_ACTOR_TOKEN` from the session (Den starts it so); a
458
+ hand-written MCP entry with `--actor agent:<id>` and no token is now a
459
+ claim.
460
+ - **`sessions start --keep-env DEN_PERSON_TOKEN`** (or of a variable that
461
+ holds a person token or a path to `web.secret`) is refused; it was
462
+ passed with a warning.
463
+
7
464
  ## [0.4.0] - 2026-11-06
8
465
 
9
466
  0.4.0-dev.2 and later are built on 0.3.0 and include all of it (below).