@monster-orange/den 0.4.0 → 0.4.1

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