@monster-orange/den 0.4.17 → 0.4.19

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 (49) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +14 -13
  3. package/docs/distributions.md +138 -25
  4. package/docs/reference.md +1 -1
  5. package/package.json +2 -1
  6. package/skills/install-den/SKILL.md +35 -75
  7. package/src/shared/agents/den-claude.ts +40 -6
  8. package/src/shared/agents/guardrails/stop-hook.ts +30 -4
  9. package/src/shared/agents/guardrails.ts +40 -4
  10. package/src/shared/agents/handback.ts +22 -4
  11. package/src/shared/agents/personas.ts +32 -5
  12. package/src/shared/agents/runtime.ts +8 -0
  13. package/src/shared/agents/server-agent.ts +57 -0
  14. package/src/shared/agents/server-pack/server.md +45 -0
  15. package/src/shared/environment-manager/workspaces.ts +4 -0
  16. package/src/ui/cli/agent-ops.ts +84 -6
  17. package/src/ui/cli/create.ts +2 -2
  18. package/src/ui/cli/index.ts +1 -1
  19. package/src/ui/cli/repo-command.ts +8 -1
  20. package/src/ui/cli/repo-detect.ts +58 -0
  21. package/src/ui/cli/server-agent-ops.ts +411 -0
  22. package/src/ui/mcp/server.ts +29 -12
  23. package/src/ui/mcp/tools.ts +4 -2
  24. package/src/ui/server/agent-routes.ts +10 -1
  25. package/src/ui/server/held-actions.ts +35 -7
  26. package/src/ui/server/index.ts +20 -5
  27. package/src/ui/server/public/agents-page.js +33 -1
  28. package/src/ui/server/public/agents.html +13 -0
  29. package/src/ui/server/public/error-page.js +39 -1
  30. package/src/ui/server/public/graph.css +43 -0
  31. package/src/ui/server/public/graph.js +68 -1
  32. package/src/ui/server/public/header.js +36 -7
  33. package/src/ui/server/public/i18n/es.js +72 -0
  34. package/src/ui/server/public/i18n/ja.js +74 -0
  35. package/src/ui/server/public/local-dev.html +33 -3
  36. package/src/ui/server/public/log-source.js +26 -0
  37. package/src/ui/server/public/logs.html +7 -20
  38. package/src/ui/server/public/persona-ui.css +6 -0
  39. package/src/ui/server/public/server-agent-page.js +415 -0
  40. package/src/ui/server/public/server-agent.html +222 -0
  41. package/src/ui/server/public/services.html +18 -3
  42. package/src/ui/server/public/shell.js +274 -31
  43. package/src/ui/server/public/svc-panel.js +685 -0
  44. package/src/ui/server/public/topbar.js +13 -1
  45. package/src/ui/server/route-classes.ts +21 -0
  46. package/src/ui/server/server-agent-routes.ts +176 -0
  47. package/src/ui/server/session-routes.ts +12 -1
  48. package/src/ui/server/ui-assets.ts +6 -0
  49. package/src/ui/server/workspace-routes.ts +3 -0
@@ -885,6 +885,8 @@ function paletteItems() {
885
885
  });
886
886
  }
887
887
  for (const x of palSessions) {
888
+ // ON-6: the server agent is "Ask <name>" below, not a workspace's.
889
+ if (x.server) continue;
888
890
  // AG-4b: the name a person gave the agent, else its persona.
889
891
  const base =
890
892
  x.kind === "agent" ? x.displayName || (x.persona ?? x.label) : x.label;
@@ -923,7 +925,7 @@ function paletteItems() {
923
925
  for (const ws of s?.workspaceNames?.() ?? [])
924
926
  if (!byWs.has(ws)) byWs.set(ws, []);
925
927
  for (const x of palSessions) {
926
- if (!x.workspace) continue;
928
+ if (!x.workspace || x.server) continue;
927
929
  const names = byWs.get(x.workspace) ?? [];
928
930
  names.push(x.label, x.displayName ?? "", x.persona ?? "");
929
931
  byWs.set(x.workspace, names);
@@ -947,6 +949,16 @@ function paletteItems() {
947
949
  keywords: ["agents", "start", "persona"],
948
950
  run: () => go("/agents?start=1"),
949
951
  });
952
+ // ON-6: Ask <name>, the server agent's page.
953
+ const ask = s?.nav?.().find((n) => n.id === "server-agent");
954
+ if (ask)
955
+ items.push({
956
+ key: "agent:ask-server",
957
+ kind: "action",
958
+ label: t("pal.askServer"),
959
+ keywords: ["ask", "server agent", "agent", "help", "setup"],
960
+ run: () => go(ask.href),
961
+ });
950
962
  for (const c of s?.commands?.() ?? [])
951
963
  items.push({
952
964
  key: `cmd:${c.id}`,
@@ -73,6 +73,8 @@ export const ROUTES: readonly RouteEntry[] = [
73
73
  "/graph.html",
74
74
  "/agents",
75
75
  "/agents.html",
76
+ "/server-agent",
77
+ "/server-agent.html",
76
78
  "/tests",
77
79
  "/index.html",
78
80
  "/services",
@@ -174,6 +176,10 @@ export const ROUTES: readonly RouteEntry[] = [
174
176
  "/api/sessions",
175
177
  "lists the sessions and agents (names and states, never their output)",
176
178
  ),
179
+ R(
180
+ "/api/server-agent",
181
+ "the server agent's name, runtime and state (ON-6): reads only",
182
+ ),
177
183
  R("/api/health", "runs the manifests' health probes: read-only checks"),
178
184
  R("/api/test-sources", "git rev-parse in each repo: read-only"),
179
185
  R("/api/manifest/validate", "GET validates a file at a path: read-only"),
@@ -389,6 +395,21 @@ export const ROUTES: readonly RouteEntry[] = [
389
395
  "/api/workspaces/:ws/agents",
390
396
  "starts an agent: a runtime in a PTY",
391
397
  ),
398
+ S(
399
+ ["POST"],
400
+ "/api/server-agent/start",
401
+ "starts the server agent (ON-6): a runtime in a PTY; never an agent's token",
402
+ ),
403
+ M(
404
+ ["POST"],
405
+ "/api/server-agent/runtime",
406
+ "the person's choice of the server agent's runtime (user.yml); never an agent's token",
407
+ ),
408
+ M(
409
+ ["POST"],
410
+ "/api/server-agent/model",
411
+ "the person's choice of the server agent's model (user.yml); never an agent's token",
412
+ ),
392
413
  S(
393
414
  ["POST"],
394
415
  "/api/workspaces/:ws/conversations/:id/import",
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The server agent's routes (ON-6):
3
+ *
4
+ * GET /api/server-agent what it is, its runtime (or why none),
5
+ * whether it runs, whether this Den is
6
+ * fresh, and the manual path
7
+ * POST /api/server-agent/start { runtime? }: start it, or bring it
8
+ * back (resume, relaunch); a person only
9
+ * POST /api/server-agent/runtime { runtime: id | null }: the person's
10
+ * choice (user.yml agents.server_runtime)
11
+ * POST /api/server-agent/model { model: id | null }: the person's
12
+ * model, one of the runtime's own
13
+ * (user.yml agents.server_model)
14
+ *
15
+ * An agent never starts the server agent or picks its runtime: any agent
16
+ * credential or claim is refused (403).
17
+ */
18
+ import type * as http from "http";
19
+ import { HostError } from "../../shared/sessions/client.js";
20
+ import { AgentError, type AgentsDeps } from "../cli/agent-ops.js";
21
+ import {
22
+ serverAgentStatus,
23
+ setServerModel,
24
+ setServerRuntime,
25
+ startServerAgent,
26
+ type ServerAgentDeps,
27
+ } from "../cli/server-agent-ops.js";
28
+ import { webDeps } from "./agent-routes.js";
29
+ import { recordAction } from "./audit.js";
30
+ import { readBody, sendJson } from "./http-helpers.js";
31
+ import {
32
+ agentPrincipalOf,
33
+ auditActorOf,
34
+ principalOf,
35
+ } from "./request-guard.js";
36
+
37
+ export interface ServerAgentRouteDeps {
38
+ agents?: AgentsDeps;
39
+ server?: ServerAgentDeps;
40
+ }
41
+
42
+ const MAX_BODY = 4 * 1024;
43
+
44
+ function sendError(res: http.ServerResponse, e: unknown): void {
45
+ if (e instanceof AgentError) {
46
+ sendJson(
47
+ res,
48
+ {
49
+ ok: false,
50
+ error: e.message,
51
+ ...(e.code ? { code: e.code } : {}),
52
+ ...(e.hints.length ? { hints: e.hints } : {}),
53
+ },
54
+ e.status,
55
+ );
56
+ return;
57
+ }
58
+ if (e instanceof HostError) {
59
+ sendJson(res, { ok: false, error: e.message, code: "session_host" }, 503);
60
+ return;
61
+ }
62
+ sendJson(res, { ok: false, error: (e as Error).message }, 500);
63
+ }
64
+
65
+ async function body(
66
+ req: http.IncomingMessage,
67
+ ): Promise<{ runtime?: unknown; model?: unknown }> {
68
+ const raw = await readBody(req);
69
+ if (raw.length > MAX_BODY)
70
+ throw new AgentError("body too large", { status: 413 });
71
+ if (!raw.trim()) return {};
72
+ let b: unknown;
73
+ try {
74
+ b = JSON.parse(raw);
75
+ } catch {
76
+ throw new AgentError("body must be JSON", { usage: true });
77
+ }
78
+ if (!b || typeof b !== "object" || Array.isArray(b))
79
+ throw new AgentError("body must be an object", { usage: true });
80
+ return b as { runtime?: unknown; model?: unknown };
81
+ }
82
+
83
+ export async function handleServerAgentRoutes(
84
+ req: http.IncomingMessage,
85
+ res: http.ServerResponse,
86
+ pathname: string,
87
+ deps: ServerAgentRouteDeps = {},
88
+ ): Promise<boolean> {
89
+ if (
90
+ pathname !== "/api/server-agent" &&
91
+ pathname !== "/api/server-agent/start" &&
92
+ pathname !== "/api/server-agent/runtime" &&
93
+ pathname !== "/api/server-agent/model"
94
+ )
95
+ return false;
96
+ const method = req.method ?? "GET";
97
+ const d = webDeps(deps.agents);
98
+ if (pathname === "/api/server-agent") {
99
+ if (method !== "GET" && method !== "HEAD") {
100
+ res.setHeader("Allow", "GET");
101
+ sendJson(res, { ok: false, error: "method not allowed" }, 405);
102
+ return true;
103
+ }
104
+ try {
105
+ sendJson(res, await serverAgentStatus(d, deps.server));
106
+ } catch (e) {
107
+ sendError(res, e);
108
+ }
109
+ return true;
110
+ }
111
+ if (method !== "POST") {
112
+ res.setHeader("Allow", "POST");
113
+ sendJson(res, { ok: false, error: "method not allowed" }, 405);
114
+ return true;
115
+ }
116
+ const actor = auditActorOf(req);
117
+ const verb = pathname.endsWith("/start")
118
+ ? "start"
119
+ : pathname.endsWith("/model")
120
+ ? "model"
121
+ : "runtime";
122
+ const audit = (detail: string, reason?: string) =>
123
+ recordAction(
124
+ actor,
125
+ {
126
+ verb: "server-agent",
127
+ detail,
128
+ ...(reason ? { outcome: "refused" as const, reason } : {}),
129
+ },
130
+ principalOf(req),
131
+ );
132
+ try {
133
+ // The person's: an agent's credential, or a claim to be one, never.
134
+ if (agentPrincipalOf(req) || actor)
135
+ throw new AgentError(
136
+ "only a person can start the server agent or pick its runtime or model",
137
+ { status: 403 },
138
+ );
139
+ const b = await body(req);
140
+ if (
141
+ b.runtime !== undefined &&
142
+ b.runtime !== null &&
143
+ typeof b.runtime !== "string"
144
+ )
145
+ throw new AgentError("runtime must be a runtime id", { usage: true });
146
+ if (verb === "model") {
147
+ if (b.model !== null && typeof b.model !== "string")
148
+ throw new AgentError("model must be a model id or null", {
149
+ usage: true,
150
+ });
151
+ const id = (b.model as string | null) ?? null;
152
+ await setServerModel(d, id, deps.server);
153
+ audit(id ? `model ${id}` : "model cleared");
154
+ sendJson(res, { ok: true, ...(await serverAgentStatus(d, deps.server)) });
155
+ return true;
156
+ }
157
+ if (verb === "runtime") {
158
+ const id = (b.runtime as string | null | undefined) ?? null;
159
+ setServerRuntime(id, deps.server);
160
+ audit(id ? `runtime ${id}` : "runtime cleared");
161
+ sendJson(res, { ok: true, ...(await serverAgentStatus(d, deps.server)) });
162
+ return true;
163
+ }
164
+ const s = await startServerAgent(
165
+ d,
166
+ typeof b.runtime === "string" ? { runtime: b.runtime } : {},
167
+ deps.server,
168
+ );
169
+ audit(`${s.how} on ${s.runtime}`);
170
+ sendJson(res, { ok: true, ...s });
171
+ } catch (e) {
172
+ audit(verb, (e as Error).message);
173
+ sendError(res, e);
174
+ }
175
+ return true;
176
+ }
@@ -35,6 +35,10 @@
35
35
  * with the actor from the credential.
36
36
  */
37
37
  import type * as http from "http";
38
+ import {
39
+ SERVER_SCOPE,
40
+ isServerScope,
41
+ } from "../../shared/agents/server-agent.js";
38
42
  import type { Duplex } from "stream";
39
43
  import {
40
44
  getActiveWorkspaces,
@@ -136,6 +140,8 @@ export interface ListedSession {
136
140
  /** `<dist>-<ws>:<name>`. */
137
141
  name: string;
138
142
  workspace: string;
143
+ /** ON-6: the server agent's row (its workspace is the server scope). */
144
+ server?: boolean;
139
145
  kind: "agent" | "session";
140
146
  /** The `<name>` part: an agent's id, a plain session's name. */
141
147
  label: string;
@@ -204,6 +210,8 @@ export function listedSessions(
204
210
  const dist = d.dist();
205
211
  const active = new Set(d.activeWorkspaces());
206
212
  if (also) active.add(also);
213
+ // ON-6: the server agent belongs to no workspace, and is always listed.
214
+ active.add(SERVER_SCOPE);
207
215
  const mine = sessions.filter((s) => {
208
216
  const p = parseSessionName(s.name, dist);
209
217
  return p !== null && active.has(p.workspace);
@@ -224,6 +232,7 @@ export function listedSessions(
224
232
  id: s?.ref.id ?? rec.session.id,
225
233
  name: s?.name ?? rec.sessionName,
226
234
  workspace: rec.workspace,
235
+ ...(isServerScope(rec.workspace) ? { server: true } : {}),
227
236
  kind: "agent",
228
237
  label: rec.id,
229
238
  agent: rec.id,
@@ -376,7 +385,9 @@ function servedSession(
376
385
  const p = parseSessionName(s.name, d.dist());
377
386
  return (
378
387
  p !== null &&
379
- (active.has(p.workspace) || d.workspaceExists(p.workspace)) &&
388
+ (active.has(p.workspace) ||
389
+ isServerScope(p.workspace) ||
390
+ d.workspaceExists(p.workspace)) &&
380
391
  (s.ref.id === target || s.name === target)
381
392
  );
382
393
  }
@@ -48,6 +48,12 @@ export const UI_MODULES: readonly string[] = [
48
48
  "/i18n/es.js",
49
49
  // 0.4.17: one org and workspace rule for Environments and Releases.
50
50
  "/view-scope.js",
51
+ // ON-6: the server agent's page.
52
+ "/server-agent-page.js",
53
+ // 0.4.18: the service panel (rail pins, Graph ⋯, Services names).
54
+ "/svc-panel.js",
55
+ // 0.4.18: where a service's live log comes from (Logs page and panel).
56
+ "/log-source.js",
51
57
  ];
52
58
 
53
59
  /** URL path → file inside node_modules. */
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { hasRuntime, repoKind } from "../../shared/repo-kind.js";
10
+ import { reservedWorkspaceError } from "../../shared/agents/server-agent.js";
10
11
  import { loadApplicationTree } from "../../shared/environment-manager/load-applications.js";
11
12
  import { localCommandsFor } from "../../shared/stacks/manifest-commands.js";
12
13
  import * as http from "http";
@@ -358,6 +359,8 @@ export function workspaceCreateRefusal(
358
359
  if (/["'\\/<>\x00-\x1f]/.test(name))
359
360
  return "name must not contain quotes, slashes, or control characters";
360
361
  if (name.length > 64) return "name must be 64 characters or fewer";
362
+ const reserved = reservedWorkspaceError(name);
363
+ if (reserved) return reserved;
361
364
  if (readWorkspacesFile().workspaces[name])
362
365
  return `workspace "${name}" already exists`;
363
366
  return null;