@monster-orange/den 0.4.12 → 0.4.14
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 +151 -0
- package/README.md +7 -7
- package/docs/distributions.md +189 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +227 -0
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +128 -13
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +33 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +50 -0
- package/src/shared/den-config.ts +22 -1
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +11 -0
- package/src/ui/cli/agent-ops.ts +796 -29
- package/src/ui/cli/agents-command.ts +97 -3
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/api.ts +12 -1
- package/src/ui/mcp/tools.ts +60 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +412 -22
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +693 -49
- package/src/ui/server/public/agents.html +76 -15
- package/src/ui/server/public/i18n/es.js +143 -0
- package/src/ui/server/public/i18n/ja.js +144 -0
- package/src/ui/server/public/persona-ui.css +563 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +22 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +140 -0
- package/src/ui/server/public/topbar.js +2 -2
- package/src/ui/server/request-guard.ts +17 -4
- package/src/ui/server/route-classes.ts +31 -0
- package/src/ui/server/session-routes.ts +21 -5
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -12,8 +12,38 @@
|
|
|
12
12
|
* runs its version command and
|
|
13
13
|
* refuses a path that isn't
|
|
14
14
|
* that runtime (AG-0.5)
|
|
15
|
+
* GET /api/personas/<id> one persona with its
|
|
16
|
+
* instructions (Details)
|
|
17
|
+
* POST /api/personas New persona (AG-10.7): a
|
|
18
|
+
* local one; a person only
|
|
19
|
+
* POST /api/personas/<id> Edit: a local one only
|
|
20
|
+
* (409 read_only otherwise)
|
|
21
|
+
* DELETE /api/personas/<id> Delete: a local one only
|
|
15
22
|
* GET /api/workspaces/<ws>/agents the workspace's agents
|
|
16
|
-
* POST /api/workspaces/<ws>/agents start { persona, runtime?,
|
|
23
|
+
* POST /api/workspaces/<ws>/agents start { persona, runtime?,
|
|
24
|
+
* model?, version?,
|
|
25
|
+
* permission?, allow?,
|
|
26
|
+
* instruction?, id? }
|
|
27
|
+
* (AG-10.1): each checked
|
|
28
|
+
* against the catalog; the
|
|
29
|
+
* instruction is typed into
|
|
30
|
+
* the session once it is
|
|
31
|
+
* ready, never an argument
|
|
32
|
+
* GET /api/workspaces/<ws>/conversations[?runtime=<r>] Bring in
|
|
33
|
+
* (AG-17.1): each runtime's
|
|
34
|
+
* own conversations in the
|
|
35
|
+
* workspace's folder, newest
|
|
36
|
+
* first, with which runtimes
|
|
37
|
+
* can bring one in
|
|
38
|
+
* POST /api/workspaces/<ws>/conversations/<id>/import { persona,
|
|
39
|
+
* runtime?, id? }: the
|
|
40
|
+
* runtime's resume of it, as
|
|
41
|
+
* a Den agent; 409
|
|
42
|
+
* open_elsewhere when it was
|
|
43
|
+
* written in the last minute
|
|
44
|
+
* POST /api/workspaces/<ws>/sessions a plain session: the login
|
|
45
|
+
* shell in the workspace's
|
|
46
|
+
* folder (AG-10.1)
|
|
17
47
|
* POST /api/workspaces/<ws>/agents/<id>/stop
|
|
18
48
|
* POST /api/workspaces/<ws>/agents/<id>/resume the same conversation (AG-14)
|
|
19
49
|
* POST /api/workspaces/<ws>/agents/<id>/fresh a new conversation, same
|
|
@@ -68,15 +98,25 @@ import {
|
|
|
68
98
|
} from "../../shared/den-config.js";
|
|
69
99
|
import {
|
|
70
100
|
readWorkspacesFile,
|
|
101
|
+
repoFolders,
|
|
71
102
|
resolveWorkspaceDir,
|
|
72
103
|
} from "../../shared/environment-manager/workspaces.js";
|
|
73
104
|
import { getStateDir } from "../../shared/paths.js";
|
|
74
105
|
import { HostError } from "../../shared/sessions/client.js";
|
|
75
106
|
import {
|
|
76
107
|
AgentError,
|
|
108
|
+
defaultId,
|
|
77
109
|
agentRecord,
|
|
110
|
+
checkInstruction,
|
|
111
|
+
createPersona,
|
|
112
|
+
deletePersona,
|
|
78
113
|
listAgents,
|
|
79
114
|
listPersonas,
|
|
115
|
+
personaDetail,
|
|
116
|
+
registries,
|
|
117
|
+
sendFirstInstruction,
|
|
118
|
+
startPlainSession,
|
|
119
|
+
updatePersona,
|
|
80
120
|
listRuntimes,
|
|
81
121
|
markInterrupted,
|
|
82
122
|
NOT_IN_WORKSPACE,
|
|
@@ -88,6 +128,8 @@ import {
|
|
|
88
128
|
resumeInterrupted,
|
|
89
129
|
setRuntimePath,
|
|
90
130
|
resolveAgentsDeps,
|
|
131
|
+
importAgent,
|
|
132
|
+
listConversations,
|
|
91
133
|
relaunchAgent,
|
|
92
134
|
resumeAgent,
|
|
93
135
|
startAgent,
|
|
@@ -105,21 +147,30 @@ import {
|
|
|
105
147
|
auditActorOf,
|
|
106
148
|
principalOf,
|
|
107
149
|
} from "./request-guard.js";
|
|
108
|
-
import {
|
|
150
|
+
import {
|
|
151
|
+
agentImportRefusal,
|
|
152
|
+
agentResumeRefusal,
|
|
153
|
+
} from "./agent-resume-grant.js";
|
|
109
154
|
import { readBody, sendJson } from "./http-helpers.js";
|
|
110
155
|
import type { Actor } from "../../shared/agent-actor.js";
|
|
111
156
|
|
|
112
157
|
export interface AgentRouteDeps {
|
|
113
158
|
/** The operations' deps (tests: an in-process host, a temp state dir). */
|
|
114
159
|
agents?: AgentsDeps;
|
|
160
|
+
/** When a first instruction counts its session ready (tests: shorter). */
|
|
161
|
+
instruction?: { quietMs?: number; maxMs?: number };
|
|
115
162
|
}
|
|
116
163
|
|
|
117
164
|
const AGENTS_PATH =
|
|
118
165
|
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|relaunch|remove|name|paths))?$/;
|
|
119
166
|
|
|
120
167
|
const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
|
|
121
|
-
|
|
122
|
-
const
|
|
168
|
+
const PERSONA_PATH = /^\/api\/personas\/([^/]+)$/;
|
|
169
|
+
const PLAIN_PATH = /^\/api\/workspaces\/([^/]+)\/sessions$/;
|
|
170
|
+
const CONVERSATIONS_PATH =
|
|
171
|
+
/^\/api\/workspaces\/([^/]+)\/conversations(?:\/([^/]+)\/import)?$/;
|
|
172
|
+
/** A start's body: a few names, commands and a first instruction. */
|
|
173
|
+
const MAX_BODY = 32 * 1024;
|
|
123
174
|
/** A list of names (a page's workspaces, an Undo's agents). */
|
|
124
175
|
const MANY_BODY = 64 * 1024;
|
|
125
176
|
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -136,6 +187,20 @@ function webDeps(over: AgentsDeps = {}): AgentsResolved {
|
|
|
136
187
|
if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
|
|
137
188
|
return resolveWorkspaceDir(ws, cfg, getReposRoot());
|
|
138
189
|
},
|
|
190
|
+
// AG-17.1: its folder and each by-path repo's, outside it too. A caller
|
|
191
|
+
// that gives its own folder (a test) gives its folders too, or just it.
|
|
192
|
+
...(over.workspaceDir
|
|
193
|
+
? {}
|
|
194
|
+
: {
|
|
195
|
+
workspaceFolders: (ws: string) => {
|
|
196
|
+
const cfg = readWorkspacesFile().workspaces[ws];
|
|
197
|
+
if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
|
|
198
|
+
return [
|
|
199
|
+
resolveWorkspaceDir(ws, cfg, getReposRoot()),
|
|
200
|
+
...repoFolders(cfg),
|
|
201
|
+
];
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
139
204
|
stdout: NO_TTY,
|
|
140
205
|
out: () => undefined,
|
|
141
206
|
err: () => undefined,
|
|
@@ -158,6 +223,7 @@ function sendError(res: http.ServerResponse, e: unknown, ws?: string): void {
|
|
|
158
223
|
error,
|
|
159
224
|
...(e.code && e.code !== NOT_IN_WORKSPACE ? { code: e.code } : {}),
|
|
160
225
|
...(e.hints.length ? { hints: e.hints } : {}),
|
|
226
|
+
...(e.running ? { running: e.running } : {}),
|
|
161
227
|
},
|
|
162
228
|
e.status,
|
|
163
229
|
);
|
|
@@ -174,12 +240,12 @@ function sendError(res: http.ServerResponse, e: unknown, ws?: string): void {
|
|
|
174
240
|
throw e;
|
|
175
241
|
}
|
|
176
242
|
|
|
177
|
-
function
|
|
243
|
+
function sessionTitleOf(
|
|
178
244
|
rec: Started["rec"],
|
|
179
245
|
d: AgentsResolved,
|
|
180
|
-
): {
|
|
246
|
+
): { sessionTitle?: string } {
|
|
181
247
|
const n = agentSessionName(rec, d.stateDir());
|
|
182
|
-
return n ? {
|
|
248
|
+
return n ? { sessionTitle: n } : {};
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
/** A started or resumed agent's answer: its record, or its early exit. */
|
|
@@ -187,6 +253,7 @@ function sendStarted(
|
|
|
187
253
|
res: http.ServerResponse,
|
|
188
254
|
s: Started & { fresh?: boolean },
|
|
189
255
|
d: AgentsResolved,
|
|
256
|
+
extra: Record<string, unknown> = {},
|
|
190
257
|
): void {
|
|
191
258
|
if (s.early) {
|
|
192
259
|
const code =
|
|
@@ -217,21 +284,50 @@ function sendStarted(
|
|
|
217
284
|
// The host's id: the Agents tab opens its pane by it (?open=<id>).
|
|
218
285
|
sessionId: s.info.ref.id,
|
|
219
286
|
startedAt: s.rec.startedAt,
|
|
287
|
+
...(s.rec.model ? { model: s.rec.model } : {}),
|
|
288
|
+
...(s.rec.version ? { version: s.rec.version } : {}),
|
|
289
|
+
...(s.rec.permission ? { permission: s.rec.permission } : {}),
|
|
290
|
+
...(s.rec.overrides?.length ? { overrides: s.rec.overrides } : {}),
|
|
220
291
|
...(s.rec.displayName ? { displayName: s.rec.displayName } : {}),
|
|
221
|
-
// AG-4b: its session
|
|
222
|
-
// lists show it (MCP agent_resume answers with this).
|
|
223
|
-
|
|
292
|
+
// AG-4b: the name its session shows (Den's rename or the runtime's),
|
|
293
|
+
// as the lists show it (MCP agent_resume answers with this). Its
|
|
294
|
+
// handle is `session`.
|
|
295
|
+
...sessionTitleOf(s.rec, d),
|
|
224
296
|
},
|
|
225
297
|
// A resume with nothing to continue yet started fresh (same id).
|
|
226
298
|
...(s.fresh ? { fresh: true } : {}),
|
|
227
299
|
...(s.warnings.length ? { warnings: s.warnings } : {}),
|
|
300
|
+
...extra,
|
|
228
301
|
});
|
|
229
302
|
}
|
|
230
303
|
|
|
231
|
-
/** What a
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
304
|
+
/** What a start's body names: optional strings, a command list, an instruction. */
|
|
305
|
+
interface StartBody {
|
|
306
|
+
persona?: string;
|
|
307
|
+
runtime?: string;
|
|
308
|
+
model?: string;
|
|
309
|
+
id?: string;
|
|
310
|
+
version?: string;
|
|
311
|
+
permission?: string;
|
|
312
|
+
allow?: unknown;
|
|
313
|
+
instruction?: unknown;
|
|
314
|
+
/** AG-10.10: a display name, checked as a rename's is. */
|
|
315
|
+
name?: unknown;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const START_KEYS = [
|
|
319
|
+
"persona",
|
|
320
|
+
"runtime",
|
|
321
|
+
"model",
|
|
322
|
+
"id",
|
|
323
|
+
"version",
|
|
324
|
+
"permission",
|
|
325
|
+
"allow",
|
|
326
|
+
"instruction",
|
|
327
|
+
"name",
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
235
331
|
const raw = await readBody(req).catch(() => "");
|
|
236
332
|
if (raw.length > MAX_BODY)
|
|
237
333
|
throw new AgentError("request too large", { status: 413, usage: true });
|
|
@@ -243,15 +339,40 @@ async function startBody(
|
|
|
243
339
|
}
|
|
244
340
|
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
245
341
|
throw new AgentError("the body must be a JSON object", { usage: true });
|
|
246
|
-
const
|
|
247
|
-
for (const k of
|
|
248
|
-
|
|
342
|
+
const o = v as Record<string, unknown>;
|
|
343
|
+
for (const k of Object.keys(o))
|
|
344
|
+
if (!START_KEYS.includes(k))
|
|
345
|
+
throw new AgentError(
|
|
346
|
+
`${k}: unknown field (a start takes ${START_KEYS.join(", ")})`,
|
|
347
|
+
{ usage: true },
|
|
348
|
+
);
|
|
349
|
+
const out: Record<string, unknown> = {};
|
|
350
|
+
for (const k of [
|
|
351
|
+
"persona",
|
|
352
|
+
"runtime",
|
|
353
|
+
"model",
|
|
354
|
+
"id",
|
|
355
|
+
"version",
|
|
356
|
+
"permission",
|
|
357
|
+
]) {
|
|
358
|
+
const x = o[k];
|
|
249
359
|
if (x === undefined || x === null || x === "") continue;
|
|
250
360
|
if (typeof x !== "string")
|
|
251
361
|
throw new AgentError(`${k} must be a string`, { usage: true });
|
|
362
|
+
if (x.length > 128 || /[^\x21-\x7e]/.test(x))
|
|
363
|
+
throw new AgentError(
|
|
364
|
+
`${k} must be a short name (no spaces or control characters)`,
|
|
365
|
+
{
|
|
366
|
+
usage: true,
|
|
367
|
+
},
|
|
368
|
+
);
|
|
252
369
|
out[k] = x;
|
|
253
370
|
}
|
|
254
|
-
|
|
371
|
+
if (o.allow !== undefined && o.allow !== null) out.allow = o.allow;
|
|
372
|
+
if (o.instruction !== undefined) out.instruction = o.instruction;
|
|
373
|
+
if (o.name !== undefined && o.name !== null && o.name !== "")
|
|
374
|
+
out.name = o.name;
|
|
375
|
+
return out as StartBody;
|
|
255
376
|
}
|
|
256
377
|
|
|
257
378
|
/**
|
|
@@ -267,6 +388,9 @@ export async function handleAgentRoutes(
|
|
|
267
388
|
): Promise<boolean> {
|
|
268
389
|
const m = AGENTS_PATH.exec(pathname);
|
|
269
390
|
const rp = RUNTIME_PATH.exec(pathname);
|
|
391
|
+
const pp = PERSONA_PATH.exec(pathname);
|
|
392
|
+
const plain = PLAIN_PATH.exec(pathname);
|
|
393
|
+
const conv = CONVERSATIONS_PATH.exec(pathname);
|
|
270
394
|
// AG-14's routes over many agents at once.
|
|
271
395
|
const many =
|
|
272
396
|
pathname === "/api/agents/clear-stopped" ||
|
|
@@ -277,22 +401,57 @@ export async function handleAgentRoutes(
|
|
|
277
401
|
pathname !== "/api/agents/runtimes" &&
|
|
278
402
|
!m &&
|
|
279
403
|
!rp &&
|
|
404
|
+
!pp &&
|
|
405
|
+
!plain &&
|
|
406
|
+
!conv &&
|
|
280
407
|
!many
|
|
281
408
|
)
|
|
282
409
|
return false;
|
|
283
410
|
const method = req.method ?? "GET";
|
|
284
411
|
const d = webDeps(deps.agents);
|
|
285
412
|
|
|
413
|
+
if (pp) return personaRoute(req, res, safeDecode(pp[1]), d);
|
|
414
|
+
if (plain) return plainSession(req, res, safeDecode(plain[1]), d);
|
|
415
|
+
if (conv)
|
|
416
|
+
return conversations(
|
|
417
|
+
req,
|
|
418
|
+
res,
|
|
419
|
+
safeDecode(conv[1]),
|
|
420
|
+
conv[2] === undefined ? undefined : safeDecode(conv[2]),
|
|
421
|
+
d,
|
|
422
|
+
);
|
|
423
|
+
if (pathname === "/api/personas" && method === "POST")
|
|
424
|
+
return personaRoute(req, res, null, d);
|
|
425
|
+
|
|
286
426
|
if (rp) return runtimePath(req, res, safeDecode(rp[1]), d);
|
|
287
427
|
if (many) return manyAgents(req, res, pathname, d);
|
|
288
428
|
if (!m) {
|
|
289
429
|
if (method !== "GET" && method !== "HEAD")
|
|
290
|
-
return methodNotAllowed(
|
|
430
|
+
return methodNotAllowed(
|
|
431
|
+
res,
|
|
432
|
+
pathname === "/api/personas" ? "GET, POST" : "GET",
|
|
433
|
+
);
|
|
291
434
|
try {
|
|
292
435
|
const ws = queryWorkspace(req, d);
|
|
293
436
|
const runtimes = await listRuntimes(d, ws);
|
|
294
437
|
sendJson(res, {
|
|
295
|
-
...(pathname === "/api/personas"
|
|
438
|
+
...(pathname === "/api/personas"
|
|
439
|
+
? { personas: listPersonas(d), ...registries(d) }
|
|
440
|
+
: {}),
|
|
441
|
+
// AG-10.10: the id a new start of each persona takes here.
|
|
442
|
+
...(pathname === "/api/personas" && ws
|
|
443
|
+
? {
|
|
444
|
+
startsAs: Object.fromEntries(
|
|
445
|
+
listPersonas(d).flatMap((p) => {
|
|
446
|
+
try {
|
|
447
|
+
return [[p.id, defaultId(d.stateDir(), p.id, ws)]];
|
|
448
|
+
} catch {
|
|
449
|
+
return []; // no free id: the start says so
|
|
450
|
+
}
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
296
455
|
runtimes,
|
|
297
456
|
...(ws ? { workspace: ws } : {}),
|
|
298
457
|
// AG-0.2: the file a person edits to set a runtime's path.
|
|
@@ -403,16 +562,55 @@ export async function handleAgentRoutes(
|
|
|
403
562
|
if (!body.persona)
|
|
404
563
|
throw new AgentError("persona is required", { usage: true });
|
|
405
564
|
what = `start ${body.id ?? body.persona} in ${ws}`;
|
|
565
|
+
const instruction = checkInstruction(body.instruction);
|
|
406
566
|
const s = await startAgent(d, {
|
|
407
567
|
workspace: ws,
|
|
408
568
|
persona: body.persona,
|
|
409
569
|
runtime: body.runtime,
|
|
410
570
|
model: body.model,
|
|
411
571
|
id: body.id,
|
|
572
|
+
...(body.version !== undefined ? { version: body.version } : {}),
|
|
573
|
+
...(body.permission !== undefined
|
|
574
|
+
? { permission: body.permission }
|
|
575
|
+
: {}),
|
|
576
|
+
...(body.allow !== undefined ? { allow: body.allow } : {}),
|
|
577
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
578
|
+
strict: true,
|
|
412
579
|
});
|
|
413
|
-
|
|
580
|
+
const ov = s.rec.overrides?.length
|
|
581
|
+
? `; overrides ${s.rec.overrides.join(", ")}`
|
|
582
|
+
: "";
|
|
583
|
+
what = `start ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}${ov}) in ${ws}`;
|
|
414
584
|
audit(what, s.early ? "exited at once" : undefined);
|
|
415
|
-
sendStarted(res, s, d);
|
|
585
|
+
sendStarted(res, s, d, instruction ? { instruction: "pending" } : {});
|
|
586
|
+
// AG-10.1: its first message, once it is ready: typed into its
|
|
587
|
+
// session (the host's write path), never an argument. Its text is
|
|
588
|
+
// never logged; its length is.
|
|
589
|
+
if (instruction && !s.early) {
|
|
590
|
+
const id = s.rec.id;
|
|
591
|
+
void sendFirstInstruction(
|
|
592
|
+
d,
|
|
593
|
+
s.info.ref.id,
|
|
594
|
+
s.rec.runtime,
|
|
595
|
+
instruction,
|
|
596
|
+
deps.instruction ?? {},
|
|
597
|
+
).then((sent) =>
|
|
598
|
+
recordAction(
|
|
599
|
+
actor,
|
|
600
|
+
{
|
|
601
|
+
verb: "agents",
|
|
602
|
+
detail: `first instruction to ${id} in ${ws} (${instruction.length} characters)`,
|
|
603
|
+
...(sent
|
|
604
|
+
? {}
|
|
605
|
+
: {
|
|
606
|
+
outcome: "refused" as const,
|
|
607
|
+
reason: "its session ended or no host",
|
|
608
|
+
}),
|
|
609
|
+
},
|
|
610
|
+
principalOf(req),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
416
614
|
} else if (verb === "stop") {
|
|
417
615
|
// The answer comes once the session has ended (200, stopped: true),
|
|
418
616
|
// or, still running after the wait, 202 with stillStopping: true.
|
|
@@ -468,6 +666,91 @@ export async function handleAgentRoutes(
|
|
|
468
666
|
return true;
|
|
469
667
|
}
|
|
470
668
|
|
|
669
|
+
const IMPORT_KEYS = ["persona", "runtime", "id"];
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Bring in (AG-17.1): GET lists the workspace's conversations, POST
|
|
673
|
+
* …/<id>/import brings one in. A person's, or an agent's opted in through
|
|
674
|
+
* mcp.actions' agent_import inside its scope. Every import is audited.
|
|
675
|
+
*/
|
|
676
|
+
async function conversations(
|
|
677
|
+
req: http.IncomingMessage,
|
|
678
|
+
res: http.ServerResponse,
|
|
679
|
+
ws: string,
|
|
680
|
+
session: string | undefined,
|
|
681
|
+
d: AgentsResolved,
|
|
682
|
+
): Promise<true> {
|
|
683
|
+
const method = req.method ?? "GET";
|
|
684
|
+
const actor = auditActorOf(req);
|
|
685
|
+
const agent = agentPrincipalOf(req);
|
|
686
|
+
const allowed = () => {
|
|
687
|
+
if (agent) {
|
|
688
|
+
const why = agentImportRefusal(agent, ws, mcpActions());
|
|
689
|
+
if (why) throw new AgentError(why, { status: 403 });
|
|
690
|
+
} else guard(actor);
|
|
691
|
+
};
|
|
692
|
+
if (session === undefined) {
|
|
693
|
+
if (method !== "GET" && method !== "HEAD")
|
|
694
|
+
return methodNotAllowed(res, "GET");
|
|
695
|
+
try {
|
|
696
|
+
allowed();
|
|
697
|
+
const runtime =
|
|
698
|
+
new URL(req.url ?? "/", "http://x").searchParams.get("runtime") ??
|
|
699
|
+
undefined;
|
|
700
|
+
const listed = listConversations(d, ws, runtime);
|
|
701
|
+
sendJson(res, { workspace: ws, folder: d.workspaceDir(ws), ...listed });
|
|
702
|
+
} catch (e) {
|
|
703
|
+
sendError(res, e, ws);
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
if (method !== "POST") return methodNotAllowed(res, "POST");
|
|
708
|
+
let what = `bring in ${session} in ${ws}`;
|
|
709
|
+
const audit = (reason?: string) =>
|
|
710
|
+
recordAction(
|
|
711
|
+
actor,
|
|
712
|
+
{
|
|
713
|
+
verb: "agents",
|
|
714
|
+
detail: what,
|
|
715
|
+
...(reason ? { outcome: "refused" as const, reason } : {}),
|
|
716
|
+
},
|
|
717
|
+
principalOf(req),
|
|
718
|
+
);
|
|
719
|
+
try {
|
|
720
|
+
allowed();
|
|
721
|
+
const body = await jsonBody(req);
|
|
722
|
+
for (const k of Object.keys(body))
|
|
723
|
+
if (!IMPORT_KEYS.includes(k))
|
|
724
|
+
throw new AgentError(
|
|
725
|
+
`${k}: unknown field (an import takes ${IMPORT_KEYS.join(", ")})`,
|
|
726
|
+
{ usage: true },
|
|
727
|
+
);
|
|
728
|
+
const str = (k: string): string | undefined => {
|
|
729
|
+
const v = body[k];
|
|
730
|
+
if (v === undefined || v === null || v === "") return undefined;
|
|
731
|
+
if (typeof v !== "string" || !NAME_RE.test(v))
|
|
732
|
+
throw new AgentError(`${k} must be a name`, { usage: true });
|
|
733
|
+
return v;
|
|
734
|
+
};
|
|
735
|
+
const persona = str("persona");
|
|
736
|
+
if (!persona) throw new AgentError("persona is required", { usage: true });
|
|
737
|
+
const s = await importAgent(d, {
|
|
738
|
+
workspace: ws,
|
|
739
|
+
session,
|
|
740
|
+
persona,
|
|
741
|
+
runtime: str("runtime"),
|
|
742
|
+
id: str("id"),
|
|
743
|
+
});
|
|
744
|
+
what = `bring in ${session} as ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
|
|
745
|
+
audit(s.early ? "exited at once" : undefined);
|
|
746
|
+
sendStarted(res, s, d);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
audit(e instanceof Error ? e.message : String(e));
|
|
749
|
+
sendError(res, e, ws);
|
|
750
|
+
}
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
471
754
|
/** The org config's mcp.actions; none when it can't be read. */
|
|
472
755
|
function mcpActions(): readonly string[] {
|
|
473
756
|
try {
|
|
@@ -586,6 +869,113 @@ function queryWorkspace(
|
|
|
586
869
|
return ws;
|
|
587
870
|
}
|
|
588
871
|
|
|
872
|
+
/**
|
|
873
|
+
* The persona routes (AG-10.6, 10.7): GET one (anyone who may read);
|
|
874
|
+
* POST (new: `id` null; edit: a local one), DELETE (a local one): a
|
|
875
|
+
* person only. An agent's token can't make or change a persona (its
|
|
876
|
+
* permissions are the person's to set). Every change is audited, refused
|
|
877
|
+
* or done; never the instructions themselves.
|
|
878
|
+
*/
|
|
879
|
+
async function personaRoute(
|
|
880
|
+
req: http.IncomingMessage,
|
|
881
|
+
res: http.ServerResponse,
|
|
882
|
+
id: string | null,
|
|
883
|
+
d: AgentsResolved,
|
|
884
|
+
): Promise<true> {
|
|
885
|
+
const method = req.method ?? "GET";
|
|
886
|
+
if (id !== null && (method === "GET" || method === "HEAD")) {
|
|
887
|
+
try {
|
|
888
|
+
sendJson(res, { persona: personaDetail(d, id) });
|
|
889
|
+
} catch (e) {
|
|
890
|
+
sendError(res, e);
|
|
891
|
+
}
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
if (method !== "POST" && !(method === "DELETE" && id !== null))
|
|
895
|
+
return methodNotAllowed(
|
|
896
|
+
res,
|
|
897
|
+
id === null ? "GET, POST" : "GET, POST, DELETE",
|
|
898
|
+
);
|
|
899
|
+
const actor = auditActorOf(req);
|
|
900
|
+
const verb = id === null ? "create" : method === "DELETE" ? "delete" : "edit";
|
|
901
|
+
let what = `persona ${verb}${id ? ` ${id}` : ""}`;
|
|
902
|
+
const audit = (reason?: string) =>
|
|
903
|
+
recordAction(
|
|
904
|
+
actor,
|
|
905
|
+
{
|
|
906
|
+
verb: "agents",
|
|
907
|
+
detail: what,
|
|
908
|
+
...(reason ? { outcome: "refused" as const, reason } : {}),
|
|
909
|
+
},
|
|
910
|
+
principalOf(req),
|
|
911
|
+
);
|
|
912
|
+
try {
|
|
913
|
+
if (actor && !actor.unverified)
|
|
914
|
+
throw new AgentError(
|
|
915
|
+
"an agent can't create, change or delete personas: a person sets what agents may do",
|
|
916
|
+
{ status: 403 },
|
|
917
|
+
);
|
|
918
|
+
if (verb === "delete") {
|
|
919
|
+
deletePersona(d, id!);
|
|
920
|
+
audit();
|
|
921
|
+
sendJson(res, { ok: true, deleted: id });
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
const body = await jsonBody(req);
|
|
925
|
+
const p =
|
|
926
|
+
verb === "create" ? createPersona(d, body) : updatePersona(d, id!, body);
|
|
927
|
+
what = `persona ${verb} ${p.id}`;
|
|
928
|
+
audit();
|
|
929
|
+
sendJson(res, { ok: true, persona: p });
|
|
930
|
+
} catch (e) {
|
|
931
|
+
audit(e instanceof Error ? e.message : String(e));
|
|
932
|
+
sendError(res, e);
|
|
933
|
+
}
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* POST /api/workspaces/<ws>/sessions (AG-10.1 Plain session): a person's
|
|
939
|
+
* login shell in the workspace's folder. A person only; audited.
|
|
940
|
+
*/
|
|
941
|
+
async function plainSession(
|
|
942
|
+
req: http.IncomingMessage,
|
|
943
|
+
res: http.ServerResponse,
|
|
944
|
+
ws: string,
|
|
945
|
+
d: AgentsResolved,
|
|
946
|
+
): Promise<true> {
|
|
947
|
+
if (req.method !== "POST") return methodNotAllowed(res, "POST");
|
|
948
|
+
const actor = auditActorOf(req);
|
|
949
|
+
let what = `plain session in ${ws}`;
|
|
950
|
+
const audit = (reason?: string) =>
|
|
951
|
+
recordAction(
|
|
952
|
+
actor,
|
|
953
|
+
{
|
|
954
|
+
verb: "sessions",
|
|
955
|
+
detail: what,
|
|
956
|
+
...(reason ? { outcome: "refused" as const, reason } : {}),
|
|
957
|
+
},
|
|
958
|
+
principalOf(req),
|
|
959
|
+
);
|
|
960
|
+
try {
|
|
961
|
+
if (actor && !actor.unverified)
|
|
962
|
+
throw new AgentError("an agent can't start a session from the page", {
|
|
963
|
+
status: 403,
|
|
964
|
+
});
|
|
965
|
+
const { info } = await startPlainSession(d, ws);
|
|
966
|
+
what = `plain session ${info.name} in ${ws}`;
|
|
967
|
+
audit();
|
|
968
|
+
sendJson(res, {
|
|
969
|
+
ok: true,
|
|
970
|
+
session: { id: info.ref.id, name: info.name, workspace: ws },
|
|
971
|
+
});
|
|
972
|
+
} catch (e) {
|
|
973
|
+
audit(e instanceof Error ? e.message : String(e));
|
|
974
|
+
sendError(res, e, ws);
|
|
975
|
+
}
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
|
|
589
979
|
/**
|
|
590
980
|
* POST /api/agents/runtimes/<id>/path: a person only, never an agent (it
|
|
591
981
|
* runs the path given). Audited, refused or done; never the path's output.
|
|
@@ -37,9 +37,10 @@ export function personaOptions(personas) {
|
|
|
37
37
|
.join("");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** The help line under the persona select: the picked persona's
|
|
40
|
+
/** The help line under the persona select: the picked persona's description. */
|
|
41
41
|
export function personaHelp(personas, id) {
|
|
42
|
-
|
|
42
|
+
const p = (personas ?? []).find((x) => x.id === id);
|
|
43
|
+
return p?.description ?? p?.role ?? "";
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** A runtime's name for people ("Claude Code"); its id when it has none. */
|