@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.
- package/CHANGELOG.md +177 -0
- package/README.md +14 -13
- package/docs/distributions.md +138 -25
- package/docs/reference.md +1 -1
- package/package.json +2 -1
- package/skills/install-den/SKILL.md +35 -75
- package/src/shared/agents/den-claude.ts +40 -6
- package/src/shared/agents/guardrails/stop-hook.ts +30 -4
- package/src/shared/agents/guardrails.ts +40 -4
- package/src/shared/agents/handback.ts +22 -4
- package/src/shared/agents/personas.ts +32 -5
- package/src/shared/agents/runtime.ts +8 -0
- package/src/shared/agents/server-agent.ts +57 -0
- package/src/shared/agents/server-pack/server.md +45 -0
- package/src/shared/environment-manager/workspaces.ts +4 -0
- package/src/ui/cli/agent-ops.ts +84 -6
- package/src/ui/cli/create.ts +2 -2
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/repo-command.ts +8 -1
- package/src/ui/cli/repo-detect.ts +58 -0
- package/src/ui/cli/server-agent-ops.ts +411 -0
- package/src/ui/mcp/server.ts +29 -12
- package/src/ui/mcp/tools.ts +4 -2
- package/src/ui/server/agent-routes.ts +10 -1
- package/src/ui/server/held-actions.ts +35 -7
- package/src/ui/server/index.ts +20 -5
- package/src/ui/server/public/agents-page.js +33 -1
- package/src/ui/server/public/agents.html +13 -0
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +43 -0
- package/src/ui/server/public/graph.js +68 -1
- package/src/ui/server/public/header.js +36 -7
- package/src/ui/server/public/i18n/es.js +72 -0
- package/src/ui/server/public/i18n/ja.js +74 -0
- package/src/ui/server/public/local-dev.html +33 -3
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/persona-ui.css +6 -0
- package/src/ui/server/public/server-agent-page.js +415 -0
- package/src/ui/server/public/server-agent.html +222 -0
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +274 -31
- package/src/ui/server/public/svc-panel.js +685 -0
- package/src/ui/server/public/topbar.js +13 -1
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/server-agent-routes.ts +176 -0
- package/src/ui/server/session-routes.ts +12 -1
- package/src/ui/server/ui-assets.ts +6 -0
- 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) ||
|
|
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;
|