@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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every route the web server dispatches, with its class (SEC-1 spec §3, §4).
|
|
3
|
+
* One source for the request checks (request-guard.ts) and for which routes
|
|
4
|
+
* need a signed-in session (SEC-1b).
|
|
5
|
+
*
|
|
6
|
+
* readonly reads state. On loopback a GET passes with the Host check
|
|
7
|
+
* alone (§6), even when it runs a read-only probe (a health
|
|
8
|
+
* check, a `git rev-parse`, a `docker logs` tail).
|
|
9
|
+
* mutating changes state or runs a declared lifecycle command: config,
|
|
10
|
+
* workspaces, the lock, a deploy, a service's up/down, a test
|
|
11
|
+
* run, a sync, an SSO login (§4: "up, down, restart, …").
|
|
12
|
+
* Origin and CSRF; the session's CSRF when one is signed in.
|
|
13
|
+
* shell-grade a shell for the caller, or what manages them: an agent's
|
|
14
|
+
* start or resume (a runtime in a PTY), the browser
|
|
15
|
+
* sessions, and (AG-4a-term) /api/sessions/*. On loopback
|
|
16
|
+
* Origin and CSRF, as mutating, and no sign-in (Ward,
|
|
17
|
+
* 2026-11-06); an agent's token never reaches one but its
|
|
18
|
+
* own session's.
|
|
19
|
+
*
|
|
20
|
+
* Beyond loopback (the edge hostname, or a peer that isn't loopback) every
|
|
21
|
+
* class needs a signed-in session or a bearer (§4); SIGNIN_FREE lists the
|
|
22
|
+
* only routes that don't (how a browser gets a session, and what it asks
|
|
23
|
+
* first).
|
|
24
|
+
*
|
|
25
|
+
* POST, PUT, PATCH and DELETE are checked as mutations by method, whatever
|
|
26
|
+
* the table says; they are listed so SEC-1b reads one table. A GET that is
|
|
27
|
+
* `mutating` or `shell-grade` (an EventSource can't send a header, so these
|
|
28
|
+
* stream over GET) gets the mutating-GET check in request-guard.ts:
|
|
29
|
+
* same-origin only, or a CLI's bearer.
|
|
30
|
+
*
|
|
31
|
+
* Paths: literal segments, `:name` for one segment, a trailing `*` for a
|
|
32
|
+
* prefix. tests/unit/server/route-classes.test.ts scans the dispatchers'
|
|
33
|
+
* source and fails on a route this table doesn't classify.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export type RouteClass = "readonly" | "mutating" | "shell-grade";
|
|
37
|
+
export type RouteMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
38
|
+
|
|
39
|
+
export interface RouteEntry {
|
|
40
|
+
methods: readonly RouteMethod[];
|
|
41
|
+
path: string;
|
|
42
|
+
cls: RouteClass;
|
|
43
|
+
/** Why, when the class isn't obvious from the name. */
|
|
44
|
+
why?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const R = (path: string, why?: string): RouteEntry => ({
|
|
48
|
+
methods: ["GET", "HEAD"],
|
|
49
|
+
path,
|
|
50
|
+
cls: "readonly",
|
|
51
|
+
...(why ? { why } : {}),
|
|
52
|
+
});
|
|
53
|
+
const M = (methods: RouteMethod[], path: string, why?: string): RouteEntry => ({
|
|
54
|
+
methods,
|
|
55
|
+
path,
|
|
56
|
+
cls: "mutating",
|
|
57
|
+
...(why ? { why } : {}),
|
|
58
|
+
});
|
|
59
|
+
const S = (methods: RouteMethod[], path: string, why: string): RouteEntry => ({
|
|
60
|
+
methods,
|
|
61
|
+
path,
|
|
62
|
+
cls: "shell-grade",
|
|
63
|
+
why,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const ROUTES: readonly RouteEntry[] = [
|
|
67
|
+
// ── Pages and the UI's files ────────────────────────────────────────────
|
|
68
|
+
...[
|
|
69
|
+
"/",
|
|
70
|
+
"/local-dev",
|
|
71
|
+
"/local-dev.html",
|
|
72
|
+
"/graph",
|
|
73
|
+
"/graph.html",
|
|
74
|
+
"/agents",
|
|
75
|
+
"/agents.html",
|
|
76
|
+
"/tests",
|
|
77
|
+
"/index.html",
|
|
78
|
+
"/services",
|
|
79
|
+
"/services.html",
|
|
80
|
+
"/environments",
|
|
81
|
+
"/environments.html",
|
|
82
|
+
"/release",
|
|
83
|
+
"/release.html",
|
|
84
|
+
"/edge",
|
|
85
|
+
"/edge.html",
|
|
86
|
+
"/logs",
|
|
87
|
+
"/settings",
|
|
88
|
+
"/settings.html",
|
|
89
|
+
"/header.js",
|
|
90
|
+
"/perf-report.js",
|
|
91
|
+
"/workspace-picker.js",
|
|
92
|
+
"/favicon.ico",
|
|
93
|
+
"/theme.css",
|
|
94
|
+
"/theme/*",
|
|
95
|
+
"/theme-favicon",
|
|
96
|
+
"/theme-favicon/*",
|
|
97
|
+
"/vendor/*",
|
|
98
|
+
"/report*",
|
|
99
|
+
"/reports/security/*",
|
|
100
|
+
].map((p) => R(p)),
|
|
101
|
+
R("/_den/throw", "a test-only route (DEN_DEBUG_ROUTES): it throws"),
|
|
102
|
+
R("/api/_den/throw", "a test-only route (DEN_DEBUG_ROUTES): it throws"),
|
|
103
|
+
|
|
104
|
+
// ── Read-only /api ──────────────────────────────────────────────────────
|
|
105
|
+
...[
|
|
106
|
+
"/api/orgs",
|
|
107
|
+
"/api/environments",
|
|
108
|
+
"/api/app-url",
|
|
109
|
+
"/api/modules",
|
|
110
|
+
"/api/security/latest",
|
|
111
|
+
"/api/security/plan",
|
|
112
|
+
"/api/stacks",
|
|
113
|
+
"/api/edge",
|
|
114
|
+
"/api/activity",
|
|
115
|
+
"/api/server-log",
|
|
116
|
+
"/api/perf-report/current",
|
|
117
|
+
"/api/health/stream",
|
|
118
|
+
"/api/services",
|
|
119
|
+
"/api/service/daemons",
|
|
120
|
+
"/api/service/history",
|
|
121
|
+
"/api/logs/search",
|
|
122
|
+
"/api/catalog",
|
|
123
|
+
"/api/:cli/config",
|
|
124
|
+
"/api/session",
|
|
125
|
+
"/api/server-info",
|
|
126
|
+
"/api/release/config",
|
|
127
|
+
"/api/release/status",
|
|
128
|
+
"/api/release/runs",
|
|
129
|
+
"/api/release/branches",
|
|
130
|
+
"/api/release/deploy/active",
|
|
131
|
+
"/api/changelog",
|
|
132
|
+
"/api/sudo-password/status",
|
|
133
|
+
"/api/sudo-password/vault-status",
|
|
134
|
+
"/api/scope",
|
|
135
|
+
"/api/settings",
|
|
136
|
+
"/api/themes",
|
|
137
|
+
"/api/workspaces",
|
|
138
|
+
"/api/workspaces/cleanup-preview",
|
|
139
|
+
"/api/github/branches",
|
|
140
|
+
"/api/github/repos",
|
|
141
|
+
"/api/workspaces/available-repos",
|
|
142
|
+
"/api/workspaces/services",
|
|
143
|
+
"/api/workspaces/settings",
|
|
144
|
+
"/api/workspaces/deps-check",
|
|
145
|
+
"/api/graph",
|
|
146
|
+
"/api/applications",
|
|
147
|
+
"/api/personas",
|
|
148
|
+
"/api/workspaces/:ws/agents",
|
|
149
|
+
].map((p) => R(p)),
|
|
150
|
+
R(
|
|
151
|
+
"/api/sessions",
|
|
152
|
+
"lists the sessions and agents (names and states, never their output)",
|
|
153
|
+
),
|
|
154
|
+
R("/api/health", "runs the manifests' health probes: read-only checks"),
|
|
155
|
+
R("/api/test-sources", "git rev-parse in each repo: read-only"),
|
|
156
|
+
R("/api/manifest/validate", "GET validates a file at a path: read-only"),
|
|
157
|
+
R("/api/service/logs", "reads (and follows) a service's log"),
|
|
158
|
+
R("/api/service/taillog", "tails a service's log (tail, docker logs)"),
|
|
159
|
+
R(
|
|
160
|
+
"/api/release/deploy",
|
|
161
|
+
"GET attaches to a deploy session's log; it starts nothing",
|
|
162
|
+
),
|
|
163
|
+
|
|
164
|
+
// ── GETs that run something (SSE, so they are GETs): mutating (§4) ─────
|
|
165
|
+
M(
|
|
166
|
+
["GET", "HEAD"],
|
|
167
|
+
"/api/service/exec",
|
|
168
|
+
"runs a service's lifecycle command (up, down, restart, …)",
|
|
169
|
+
),
|
|
170
|
+
M(["GET", "HEAD"], "/api/run", "starts a test run (playwright, k6, ZAP)"),
|
|
171
|
+
M(
|
|
172
|
+
["GET", "HEAD"],
|
|
173
|
+
"/api/workspace/exec",
|
|
174
|
+
"runs a workspace's lifecycle command over its services",
|
|
175
|
+
),
|
|
176
|
+
M(
|
|
177
|
+
["GET", "HEAD", "POST"],
|
|
178
|
+
"/api/workspace/sync",
|
|
179
|
+
"runs git in the workspace's checkouts (clone, pull)",
|
|
180
|
+
),
|
|
181
|
+
M(["GET", "HEAD"], "/api/aws/login", "runs `aws sso login`"),
|
|
182
|
+
|
|
183
|
+
// ── Sign-in (SEC-1b, §2) ────────────────────────────────────────────────
|
|
184
|
+
M(
|
|
185
|
+
["GET", "HEAD"],
|
|
186
|
+
"/auth",
|
|
187
|
+
"swaps a one-time login code for a session cookie",
|
|
188
|
+
),
|
|
189
|
+
R(
|
|
190
|
+
"/signin",
|
|
191
|
+
"the signed-out landing page (§6b.3): it renders signed out; signed in, it goes on to `next`",
|
|
192
|
+
),
|
|
193
|
+
M(["POST"], "/api/session/logout", "ends this browser's session"),
|
|
194
|
+
S(
|
|
195
|
+
["GET", "HEAD"],
|
|
196
|
+
"/api/web/sessions",
|
|
197
|
+
"lists the browser sessions (`web sessions`)",
|
|
198
|
+
),
|
|
199
|
+
S(
|
|
200
|
+
["POST"],
|
|
201
|
+
"/api/web/logout",
|
|
202
|
+
"ends browser sessions, and rotates web.secret (`web logout`)",
|
|
203
|
+
),
|
|
204
|
+
|
|
205
|
+
// ── Mutations (checked by method) ───────────────────────────────────────
|
|
206
|
+
M(["POST"], "/api/activity", "an agent's refused-call report"),
|
|
207
|
+
M(["POST"], "/api/web/restart", "restarts this server"),
|
|
208
|
+
M(["PUT"], "/api/release/config"),
|
|
209
|
+
M(["POST", "DELETE"], "/api/release/deploy", "starts or dismisses a deploy"),
|
|
210
|
+
M(["POST"], "/api/manifest/validate", "validates posted content (no write)"),
|
|
211
|
+
M(["POST"], "/api/local-dev"),
|
|
212
|
+
M(["POST"], "/api/sudo-password", "opens the lock (R8)"),
|
|
213
|
+
M(["POST"], "/api/sudo-password/load-from-vault", "opens the lock (R8)"),
|
|
214
|
+
M(["POST"], "/api/scope"),
|
|
215
|
+
M(["POST"], "/api/settings"),
|
|
216
|
+
M(
|
|
217
|
+
["POST"],
|
|
218
|
+
"/api/editor/open",
|
|
219
|
+
"opens a checkout in the person's editor app",
|
|
220
|
+
),
|
|
221
|
+
M(
|
|
222
|
+
["POST"],
|
|
223
|
+
"/api/terminal/open",
|
|
224
|
+
"opens a terminal app window at a checkout; nothing is typed into it",
|
|
225
|
+
),
|
|
226
|
+
M(["POST", "DELETE"], "/api/workspaces"),
|
|
227
|
+
M(["POST"], "/api/workspaces/rename"),
|
|
228
|
+
M(["POST"], "/api/workspaces/create"),
|
|
229
|
+
M(["POST"], "/api/workspaces/services/autostart"),
|
|
230
|
+
M(["POST"], "/api/workspaces/services/mode"),
|
|
231
|
+
M(["POST", "DELETE"], "/api/workspaces/services"),
|
|
232
|
+
M(["POST"], "/api/workspaces/settings"),
|
|
233
|
+
M(["POST"], "/api/workspaces/:ws/agents/:id/stop", "TERM to its session"),
|
|
234
|
+
S(
|
|
235
|
+
["POST"],
|
|
236
|
+
"/api/workspaces/:ws/agents",
|
|
237
|
+
"starts an agent: a runtime in a PTY",
|
|
238
|
+
),
|
|
239
|
+
S(
|
|
240
|
+
["POST"],
|
|
241
|
+
"/api/workspaces/:ws/agents/:id/resume",
|
|
242
|
+
"resumes an agent: a runtime in a PTY",
|
|
243
|
+
),
|
|
244
|
+
S(
|
|
245
|
+
["GET", "HEAD"],
|
|
246
|
+
"/api/sessions/:id/io",
|
|
247
|
+
"a session's terminal (AG-4a-term): a WebSocket, its input a shell's",
|
|
248
|
+
),
|
|
249
|
+
S(
|
|
250
|
+
["GET", "HEAD", "POST", "DELETE"],
|
|
251
|
+
"/api/sessions/*",
|
|
252
|
+
"a session's own routes (AG-4a-term's io): an agent's token reaches its own session's only",
|
|
253
|
+
),
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The routes that never need a signed-in session, even beyond loopback:
|
|
258
|
+
* the sign-in itself, the signed-out landing page (it checks the session
|
|
259
|
+
* itself), and the session probe the shell and the signed-out page ask
|
|
260
|
+
* first (it hands out a CSRF token only to a session, or on loopback).
|
|
261
|
+
*/
|
|
262
|
+
export const SIGNIN_FREE: readonly string[] = [
|
|
263
|
+
"/auth",
|
|
264
|
+
"/signin",
|
|
265
|
+
"/api/session",
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
/** The dispatchers' route regexes, by `<file>:<const>`, and their entries. */
|
|
269
|
+
export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
270
|
+
"agent-routes.ts:AGENTS_PATH": [
|
|
271
|
+
"/api/workspaces/:ws/agents",
|
|
272
|
+
"/api/workspaces/:ws/agents/:id/stop",
|
|
273
|
+
"/api/workspaces/:ws/agents/:id/resume",
|
|
274
|
+
],
|
|
275
|
+
"session-routes.ts:IO_PATH": ["/api/sessions/:id/io"],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** Whether `pathname` matches a table path. */
|
|
279
|
+
export function pathMatches(pattern: string, pathname: string): boolean {
|
|
280
|
+
if (pattern.endsWith("*")) return pathname.startsWith(pattern.slice(0, -1));
|
|
281
|
+
const a = pattern.split("/");
|
|
282
|
+
const b = pathname.split("/");
|
|
283
|
+
if (a.length !== b.length) return false;
|
|
284
|
+
return a.every((seg, i) =>
|
|
285
|
+
seg.startsWith(":") ? b[i] !== "" : seg === b[i],
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The class of `method pathname`: the most specific entry that lists the
|
|
291
|
+
* method (a literal path before a pattern, a pattern before a prefix).
|
|
292
|
+
* Null when no entry matches.
|
|
293
|
+
*/
|
|
294
|
+
export function routeClass(
|
|
295
|
+
method: string,
|
|
296
|
+
pathname: string,
|
|
297
|
+
): RouteClass | null {
|
|
298
|
+
const m = method.toUpperCase();
|
|
299
|
+
const hits = ROUTES.filter(
|
|
300
|
+
(e) =>
|
|
301
|
+
(e.methods as readonly string[]).includes(m) &&
|
|
302
|
+
pathMatches(e.path, pathname),
|
|
303
|
+
);
|
|
304
|
+
if (!hits.length) return null;
|
|
305
|
+
const rank = (e: RouteEntry) =>
|
|
306
|
+
e.path.endsWith("*") ? 2 : e.path.includes(":") ? 1 : 0;
|
|
307
|
+
hits.sort((x, y) => rank(x) - rank(y));
|
|
308
|
+
return hits[0].cls;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A GET (or HEAD) that runs or changes something: the mutating-GET check. */
|
|
312
|
+
export function isMutatingGet(method: string, pathname: string): boolean {
|
|
313
|
+
const m = method.toUpperCase();
|
|
314
|
+
if (m !== "GET" && m !== "HEAD") return false;
|
|
315
|
+
const c = routeClass(m, pathname);
|
|
316
|
+
return c === "mutating" || c === "shell-grade";
|
|
317
|
+
}
|