@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
@@ -0,0 +1,439 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <script>
7
+ // Mode before first paint: yours (the server fills it in below),
8
+ // else the system's, else the theme's default, else dark.
9
+ (function () {
10
+ var m = "@@DEN_MODE@@";
11
+ if (m !== "dark" && m !== "light") {
12
+ var q = function (s) {
13
+ return (
14
+ window.matchMedia &&
15
+ matchMedia("(prefers-color-scheme: " + s + ")").matches
16
+ );
17
+ };
18
+ m = q("light")
19
+ ? "light"
20
+ : q("dark")
21
+ ? "dark"
22
+ : "@@DEN_DEFAULT_MODE@@";
23
+ }
24
+ document.documentElement.dataset.mode =
25
+ m === "light" ? "light" : "dark";
26
+ })();
27
+ </script>
28
+ <link rel="stylesheet" href="/theme.css" id="theme-css" />
29
+ <link rel="stylesheet" href="/vendor/xterm.css" />
30
+ <title>__DEN_TITLE__ · Agents</title>
31
+ <style>
32
+ /* The Agents tab (AG-4a-term, AG spec §5d): the prototype's
33
+ (den-shell.html) page header (.ph), list column (.agl, 300px) and
34
+ panes (.agp: 1, 2 side by side, 3-4 as 2x2), ported as they are;
35
+ --r is --radius, --r-btn is --radius-button. */
36
+ *,
37
+ *::before,
38
+ *::after {
39
+ box-sizing: border-box;
40
+ }
41
+ html,
42
+ body {
43
+ height: 100%;
44
+ margin: 0;
45
+ background: var(--bg);
46
+ color: var(--text);
47
+ }
48
+ body {
49
+ display: flex;
50
+ flex-direction: column;
51
+ overflow: hidden;
52
+ }
53
+ html body.den-has-shell > #den-content {
54
+ overflow: auto;
55
+ min-width: 0;
56
+ padding: 20px 24px 32px 24px;
57
+ font: 14px/1.45 var(--sans);
58
+ color: var(--text);
59
+ display: block;
60
+ }
61
+ #den-content :focus-visible {
62
+ outline: 2px solid var(--accent);
63
+ outline-offset: 2px;
64
+ }
65
+ .ph {
66
+ display: flex;
67
+ align-items: flex-end;
68
+ gap: 16px;
69
+ flex-wrap: wrap;
70
+ margin-bottom: 16px;
71
+ }
72
+ .ph h1 {
73
+ font: 600 22px/1.2 var(--display);
74
+ margin: 0;
75
+ text-wrap: balance;
76
+ }
77
+ .ph .sub {
78
+ color: var(--muted);
79
+ font-size: 13px;
80
+ margin-top: 4px;
81
+ overflow-wrap: anywhere;
82
+ }
83
+ .ph .act {
84
+ margin-left: auto;
85
+ display: flex;
86
+ gap: 8px;
87
+ }
88
+ .mono {
89
+ font-family: var(--mono);
90
+ }
91
+ .agx {
92
+ display: grid;
93
+ grid-template-columns: 300px minmax(0, 1fr);
94
+ gap: 14px;
95
+ min-height: 420px;
96
+ height: calc(100vh - 300px);
97
+ }
98
+ .agl {
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--radius);
101
+ background: var(--surface);
102
+ overflow: auto;
103
+ min-height: 0;
104
+ }
105
+ .agl .wsh {
106
+ font: 11px var(--mono);
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.07em;
109
+ color: var(--faint);
110
+ padding: 10px 12px 4px;
111
+ display: flex;
112
+ justify-content: space-between;
113
+ }
114
+ .agr {
115
+ display: grid;
116
+ grid-template-columns: auto minmax(0, 1fr) auto;
117
+ gap: 2px 8px;
118
+ align-items: center;
119
+ width: 100%;
120
+ text-align: left;
121
+ border: 0;
122
+ border-left: 2px solid transparent;
123
+ background: none;
124
+ padding: 7px 12px;
125
+ cursor: pointer;
126
+ color: var(--text);
127
+ font: inherit;
128
+ }
129
+ .agr:hover {
130
+ background: var(--sunken);
131
+ }
132
+ .agr[aria-pressed="true"] {
133
+ background: var(--sunken);
134
+ border-left-color: var(--accent);
135
+ }
136
+ .agr .nm {
137
+ font-weight: 500;
138
+ white-space: nowrap;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ }
142
+ .agr .nm .id {
143
+ font-weight: 400;
144
+ color: var(--faint);
145
+ }
146
+ .agr .st {
147
+ font: 12px var(--mono);
148
+ color: var(--faint);
149
+ }
150
+ .agr .agtags {
151
+ grid-column: 2 / 4;
152
+ display: flex;
153
+ flex-wrap: wrap;
154
+ gap: 4px;
155
+ align-items: center;
156
+ margin-top: 2px;
157
+ }
158
+ .agr .meta {
159
+ grid-column: 2 / 4;
160
+ font: 11.5px var(--mono);
161
+ color: var(--faint);
162
+ white-space: nowrap;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ }
166
+ .wbdg {
167
+ font: 10.5px var(--mono);
168
+ color: var(--warn);
169
+ border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
170
+ border-radius: 999px;
171
+ padding: 0 6px;
172
+ }
173
+ .sbdg,
174
+ .tagx {
175
+ font: 10.5px var(--mono);
176
+ color: var(--faint);
177
+ border: 1px dashed var(--border-strong);
178
+ border-radius: 4px;
179
+ padding: 0 4px;
180
+ font-weight: 400;
181
+ }
182
+ .tagx {
183
+ font-size: 10px;
184
+ }
185
+ .tagx.warn {
186
+ color: var(--warn);
187
+ border-color: color-mix(in srgb, var(--warn) 55%, transparent);
188
+ }
189
+ .agp {
190
+ display: grid;
191
+ gap: 10px;
192
+ min-height: 0;
193
+ }
194
+ .agp.n1 {
195
+ grid-template-columns: minmax(0, 1fr);
196
+ grid-template-rows: minmax(0, 1fr);
197
+ }
198
+ .agp.n2 {
199
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
200
+ grid-template-rows: minmax(0, 1fr);
201
+ }
202
+ .agp.n3,
203
+ .agp.n4 {
204
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
205
+ grid-auto-rows: minmax(0, 1fr);
206
+ }
207
+ .pane {
208
+ display: grid;
209
+ grid-template-rows: auto minmax(0, 1fr);
210
+ border: 1px solid var(--border);
211
+ border-radius: var(--radius);
212
+ background: var(--sunken);
213
+ min-height: 0;
214
+ min-width: 0;
215
+ overflow: hidden;
216
+ }
217
+ .pane .bar2 {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 8px;
221
+ padding: 6px 10px;
222
+ border-bottom: 1px solid var(--border);
223
+ background: var(--surface);
224
+ font-size: 13px;
225
+ min-width: 0;
226
+ }
227
+ .pane .bar2 b {
228
+ white-space: nowrap;
229
+ overflow: hidden;
230
+ text-overflow: ellipsis;
231
+ min-width: 0;
232
+ flex: 0 0 auto;
233
+ max-width: 45%;
234
+ }
235
+ .pane .bar2 .tagx {
236
+ flex: none;
237
+ white-space: nowrap;
238
+ }
239
+ .pane .bar2 .where {
240
+ flex: 0 1 auto;
241
+ min-width: 0;
242
+ color: var(--faint);
243
+ white-space: nowrap;
244
+ overflow: hidden;
245
+ text-overflow: ellipsis;
246
+ font-size: 12.5px;
247
+ }
248
+ .pane .bar2 .x {
249
+ margin-left: auto;
250
+ display: flex;
251
+ gap: 6px;
252
+ flex: none;
253
+ }
254
+ /* The body: mono 12px/1.5 (xterm draws inside it). */
255
+ .pane .term {
256
+ min-height: 0;
257
+ min-width: 0;
258
+ padding: 6px 4px 4px 8px;
259
+ font: 12px/1.5 var(--mono);
260
+ color: var(--text);
261
+ overflow: hidden;
262
+ }
263
+ .pane .term .xterm {
264
+ height: 100%;
265
+ }
266
+ /* xterm.css paints its viewport black: the pane's colour instead. */
267
+ .pane .term .xterm .xterm-viewport {
268
+ background-color: var(--sunken);
269
+ }
270
+ .pane .note {
271
+ margin: 0;
272
+ padding: 10px 12px;
273
+ font: 12px/1.5 var(--mono);
274
+ color: var(--muted);
275
+ white-space: pre-wrap;
276
+ }
277
+ .agempty {
278
+ border: 1px dashed var(--border-strong);
279
+ border-radius: var(--radius);
280
+ display: grid;
281
+ place-items: center;
282
+ color: var(--muted);
283
+ padding: 28px;
284
+ text-align: center;
285
+ }
286
+ .signin,
287
+ .agnote {
288
+ margin: 8px 12px;
289
+ padding: 8px 10px;
290
+ border: 1px dashed var(--border-strong);
291
+ border-radius: var(--radius-button);
292
+ font-size: 12.5px;
293
+ color: var(--muted);
294
+ }
295
+ .agl .none {
296
+ padding: 12px;
297
+ color: var(--muted);
298
+ font-size: 12.5px;
299
+ }
300
+ dialog.ag-dlg {
301
+ width: min(440px, calc(100vw - 32px));
302
+ background: var(--raised);
303
+ color: var(--text);
304
+ border: 1px solid var(--border-strong);
305
+ border-radius: 12px;
306
+ box-shadow: var(--shadow);
307
+ padding: 18px 18px 14px;
308
+ }
309
+ dialog.ag-dlg::backdrop {
310
+ background: rgb(0 0 0 / 0.35);
311
+ }
312
+ .ag-dlg form {
313
+ display: grid;
314
+ gap: 12px;
315
+ }
316
+ .ag-dlg h2 {
317
+ margin: 0;
318
+ font: 600 17px var(--display);
319
+ }
320
+ .ag-dlg label {
321
+ display: grid;
322
+ gap: 6px;
323
+ font: 11px var(--mono);
324
+ color: var(--faint);
325
+ text-transform: uppercase;
326
+ letter-spacing: 0.07em;
327
+ }
328
+ .ag-dlg select {
329
+ font: 13px var(--sans);
330
+ text-transform: none;
331
+ letter-spacing: 0;
332
+ border: 1px solid var(--border);
333
+ background: var(--bg);
334
+ color: var(--text);
335
+ border-radius: var(--radius-button);
336
+ padding: 6px 8px;
337
+ }
338
+ .ag-dlg .btns {
339
+ display: flex;
340
+ gap: 8px;
341
+ justify-content: flex-end;
342
+ }
343
+ @media (max-width: 860px) {
344
+ .agx {
345
+ grid-template-columns: minmax(0, 1fr);
346
+ height: auto;
347
+ }
348
+ .ph .act {
349
+ flex-wrap: wrap;
350
+ }
351
+ .agp.n2,
352
+ .agp.n3,
353
+ .agp.n4 {
354
+ grid-template-columns: minmax(0, 1fr);
355
+ }
356
+ .pane {
357
+ min-height: 260px;
358
+ }
359
+ .agl {
360
+ max-height: 50vh;
361
+ }
362
+ }
363
+ </style>
364
+ </head>
365
+ <body>
366
+ <!-- The shell: top bar and rail, the same on every page (shell.js). -->
367
+ <div id="den-shell" data-page="agents"></div>
368
+ <script src="/shell.js"></script>
369
+
370
+ <!-- The page's one main landmark: everything after the shell. -->
371
+ <main id="den-content">
372
+ <div class="ph">
373
+ <div>
374
+ <h1 id="ag-h">Agents</h1>
375
+ <div class="sub" id="ag-sub">
376
+ <span id="ag-count"></span> ·
377
+ <span class="mono" id="ag-url">/agents</span>
378
+ </div>
379
+ </div>
380
+ <div class="act">
381
+ <button type="button" class="den-btn primary" id="ag-start">
382
+ + Start agent
383
+ </button>
384
+ </div>
385
+ </div>
386
+ <div class="agx" id="agx">
387
+ <div
388
+ class="agl"
389
+ id="agl"
390
+ role="region"
391
+ aria-labelledby="ag-h"
392
+ aria-busy="true"
393
+ ></div>
394
+ <div id="agr" class="agempty"></div>
395
+ </div>
396
+ <dialog class="ag-dlg" id="ag-dlg" aria-labelledby="ag-dlg-h">
397
+ <form method="dialog" id="ag-form">
398
+ <h2 id="ag-dlg-h">Start an agent</h2>
399
+ <label id="ag-ws-l"
400
+ ><span id="ag-ws-t">Workspace</span>
401
+ <select name="ws" id="ag-ws"></select
402
+ ></label>
403
+ <label
404
+ ><span id="ag-p-t">Persona</span>
405
+ <select name="persona" id="ag-persona" required></select
406
+ ></label>
407
+ <label
408
+ ><span id="ag-r-t">Runtime</span>
409
+ <select name="runtime" id="ag-runtime"></select
410
+ ></label>
411
+ <div class="btns">
412
+ <button type="button" class="den-btn" id="ag-cancel">Cancel</button>
413
+ <button type="submit" class="den-btn primary" id="ag-go">
414
+ Start
415
+ </button>
416
+ </div>
417
+ </form>
418
+ </dialog>
419
+
420
+ <script type="module">
421
+ import {
422
+ applyDenConfig,
423
+ initChangelog,
424
+ initHealth,
425
+ initLogBadge,
426
+ initWorkspacePill,
427
+ } from "/header.js";
428
+ import { mountAgentsPage } from "/agents-page.js";
429
+ void Promise.all([
430
+ initHealth(),
431
+ initWorkspacePill(),
432
+ applyDenConfig(),
433
+ initChangelog(),
434
+ ]).then(initLogBadge);
435
+ void mountAgentsPage(document.getElementById("den-content"));
436
+ </script>
437
+ </main>
438
+ </body>
439
+ </html>
@@ -513,6 +513,9 @@ let inited = false;
513
513
  export async function initAppearance() {
514
514
  if (inited || typeof document === "undefined") return;
515
515
  inited = true;
516
+ // A refusal page (SEC-1b): /api/themes answers the refusal; the page
517
+ // keeps its first paint.
518
+ if (window.denShell?.apiOff?.() === true) return;
516
519
  const mq = window.matchMedia?.("(prefers-color-scheme: light)");
517
520
  mq?.addEventListener?.("change", () => {
518
521
  if (st.mode === "system") applyMode();
@@ -121,6 +121,77 @@ async function copy(text, btn, t) {
121
121
  setTimeout(() => (btn.textContent = label), 1500);
122
122
  }
123
123
 
124
+ /** `text` with `{cmd}` as inline code (the rest escaped). */
125
+ function withCmd(text, cmd) {
126
+ return esc(text).replace("\u0000", `<code class="mono">${esc(cmd)}</code>`);
127
+ }
128
+
129
+ /**
130
+ * The signed-out page (SEC-1b, SEC-1 spec §2): "Sign in to use the
131
+ * terminal" (a shell-grade route on loopback) or "Sign in to use <name>"
132
+ * (beyond loopback), the command that prints a sign-in link with a copy
133
+ * button, and on 127.0.0.1 or [::1] a link to the same page on localhost,
134
+ * where the cookie is. It never asks for a secret or a token.
135
+ *
136
+ * A link from 127.0.0.1 lands here once without the cookie (a cross-site
137
+ * navigation doesn't carry a SameSite=Strict one); /api/session, asked
138
+ * same-origin, does carry it, so a signed-in browser reloads the page
139
+ * from here (same-origin) and is let through.
140
+ */
141
+ function mountSignedOut(root, sh, d) {
142
+ const t = sh.t;
143
+ const s = d.signin;
144
+ const title = s.expired
145
+ ? t("err.signin.expiredTitle")
146
+ : s.scope === "terminal"
147
+ ? t("err.signin.terminal")
148
+ : t("err.signin.title", { name: s.name || "Den" });
149
+ document.title = document.title.replace(/Error$/, title);
150
+ const lead = s.expired
151
+ ? withCmd(t("err.signin.expired", { cmd: "\u0000" }), s.cmd)
152
+ : withCmd(t("err.signin.lead", { cmd: "\u0000" }), s.cmd);
153
+ root.innerHTML =
154
+ `<div class="ph"><div><h1>${esc(title)}</h1>` +
155
+ `<div class="sub">${esc(t("err.signin.sub"))}</div></div></div>` +
156
+ `<div class="empty" id="den-signin"><p class="lead">${lead}</p>` +
157
+ `<pre class="code" id="den-signin-cmd">${esc(s.cmd)}</pre>` +
158
+ (s.localhost
159
+ ? `<p>${esc(t("err.signin.local", { host: location.host }))}</p>`
160
+ : "") +
161
+ `<p class="den-signin-why">${esc(t("err.signin.why"))}</p>` +
162
+ `<dl class="facts">` +
163
+ `<dt>${esc(t("err.500.id"))}</dt><dd class="mono" id="den-err-id">${esc(d.requestId)}</dd>` +
164
+ `</dl><div class="acts">` +
165
+ (s.localhost
166
+ ? `<a class="btn primary" id="den-signin-local" href="${esc(s.localhost)}">${esc(t("err.421.open"))}</a>`
167
+ : "") +
168
+ `<button type="button" class="btn${s.localhost ? "" : " primary"}" id="den-signin-copy">${esc(t("err.signin.copy"))}</button>` +
169
+ `</div></div>`;
170
+ const btn = root.querySelector("#den-signin-copy");
171
+ btn?.addEventListener("click", () => copy(s.cmd, btn, t));
172
+ if (s.expired || s.localhost) return;
173
+ let tried = false;
174
+ try {
175
+ tried = sessionStorage.getItem("den-signin-reload") === location.href;
176
+ sessionStorage.removeItem("den-signin-reload");
177
+ } catch {
178
+ /* no storage: try once */
179
+ }
180
+ if (tried) return;
181
+ fetch("/api/session", { cache: "no-store", credentials: "same-origin" })
182
+ .then((r) => (r.ok ? r.json() : null))
183
+ .then((b) => {
184
+ if (!b || b.signedIn !== true) return;
185
+ try {
186
+ sessionStorage.setItem("den-signin-reload", location.href);
187
+ } catch {
188
+ /* none */
189
+ }
190
+ location.replace(location.href);
191
+ })
192
+ .catch(() => {});
193
+ }
194
+
124
195
  export function mountErrorPage(root) {
125
196
  const sh = window.denShell;
126
197
  const d = readData();
@@ -219,6 +290,38 @@ export function mountErrorPage(root) {
219
290
  return;
220
291
  }
221
292
 
293
+ if (d.status === 401 && d.code === "signin_required" && d.signin) {
294
+ mountSignedOut(root, sh, d);
295
+ return;
296
+ }
297
+
298
+ if (d.status === 421 || d.status === 403 || d.status === 401) {
299
+ // SEC-1a: the request checks refused it (a host that isn't this
300
+ // server's, a cross-site request). A 421 links to the same path on
301
+ // localhost, the canonical loopback name.
302
+ const title =
303
+ d.status === 421 ? t("err.421.title") : t("err.refused.title");
304
+ document.title = document.title.replace(/Error$/, title);
305
+ const here =
306
+ d.status === 421
307
+ ? `http://localhost${location.port ? `:${location.port}` : ""}${d.path ?? "/"}`
308
+ : null;
309
+ root.innerHTML =
310
+ header(title, `<span class="mono">${esc(d.code ?? "")}</span>`) +
311
+ `<div class="empty"><p class="lead">${esc(d.status === 421 ? t("err.421.lead") : (d.message ?? ""))}</p>` +
312
+ `<dl class="facts">` +
313
+ `<dt>${esc(t("err.500.id"))}</dt><dd class="mono" id="den-err-id">${esc(d.requestId)}</dd>` +
314
+ `</dl><div class="acts">` +
315
+ (here
316
+ ? `<a class="btn primary" id="den-err-local" href="${esc(here)}">${esc(t("err.421.open"))}</a>`
317
+ : `<button type="button" class="btn primary" id="den-err-retry">${esc(t("err.retry"))}</button>`) +
318
+ `</div></div>`;
319
+ root
320
+ .querySelector("#den-err-retry")
321
+ ?.addEventListener("click", () => location.reload());
322
+ return;
323
+ }
324
+
222
325
  document.title = document.title.replace(/Error$/, t("err.500.title"));
223
326
  const what = `${d.method ?? "GET"} ${d.route ?? ""}`;
224
327
  const when = new Date(d.time);