@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +189 -11
  4. package/package.json +1 -1
  5. package/src/shared/agents/conversations.ts +227 -0
  6. package/src/shared/agents/default-pack/be-dev.md +1 -0
  7. package/src/shared/agents/default-pack/devops.md +1 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +1 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +3 -1
  10. package/src/shared/agents/default-pack/planner.md +1 -0
  11. package/src/shared/agents/default-pack/reviewer.md +1 -0
  12. package/src/shared/agents/default-pack/tester.md +16 -0
  13. package/src/shared/agents/default-pack/ux-designer.md +1 -0
  14. package/src/shared/agents/den-claude.ts +128 -13
  15. package/src/shared/agents/permissions.ts +108 -0
  16. package/src/shared/agents/persona-store.ts +286 -0
  17. package/src/shared/agents/personas.ts +116 -12
  18. package/src/shared/agents/records.ts +33 -0
  19. package/src/shared/agents/roles.ts +288 -0
  20. package/src/shared/agents/runtime.ts +50 -0
  21. package/src/shared/den-config.ts +22 -1
  22. package/src/shared/environment-manager/service-runner.ts +31 -12
  23. package/src/shared/environment-manager/workspaces.ts +11 -0
  24. package/src/ui/cli/agent-ops.ts +796 -29
  25. package/src/ui/cli/agents-command.ts +97 -3
  26. package/src/ui/cli/env-probe.ts +4 -1
  27. package/src/ui/mcp/api.ts +12 -1
  28. package/src/ui/mcp/tools.ts +60 -1
  29. package/src/ui/server/agent-resume-grant.ts +35 -0
  30. package/src/ui/server/agent-routes.ts +412 -22
  31. package/src/ui/server/public/agent-start.js +3 -2
  32. package/src/ui/server/public/agents-page.js +693 -49
  33. package/src/ui/server/public/agents.html +76 -15
  34. package/src/ui/server/public/i18n/es.js +143 -0
  35. package/src/ui/server/public/i18n/ja.js +144 -0
  36. package/src/ui/server/public/persona-ui.css +563 -0
  37. package/src/ui/server/public/persona-ui.js +796 -0
  38. package/src/ui/server/public/settings-page.js +4 -9
  39. package/src/ui/server/public/settings-registry.js +22 -0
  40. package/src/ui/server/public/settings.html +1 -0
  41. package/src/ui/server/public/shell.js +140 -0
  42. package/src/ui/server/public/topbar.js +2 -2
  43. package/src/ui/server/request-guard.ts +17 -4
  44. package/src/ui/server/route-classes.ts +31 -0
  45. package/src/ui/server/session-routes.ts +21 -5
  46. 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?, model?, id? }
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 { agentResumeRefusal } from "./agent-resume-grant.js";
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
- /** A start's body is a few short names. */
122
- const MAX_BODY = 4096;
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 sessionNameOf(
243
+ function sessionTitleOf(
178
244
  rec: Started["rec"],
179
245
  d: AgentsResolved,
180
- ): { sessionName?: string } {
246
+ ): { sessionTitle?: string } {
181
247
  const n = agentSessionName(rec, d.stateDir());
182
- return n ? { sessionName: 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's name (Den's rename or the runtime's), as the
222
- // lists show it (MCP agent_resume answers with this).
223
- ...sessionNameOf(s.rec, d),
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 POST's body names: a few optional strings. */
232
- async function startBody(
233
- req: http.IncomingMessage,
234
- ): Promise<Record<string, string | undefined>> {
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 out: Record<string, string | undefined> = {};
247
- for (const k of ["persona", "runtime", "model", "id"]) {
248
- const x = (v as Record<string, unknown>)[k];
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
- return out;
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(res, "GET");
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" ? { personas: listPersonas(d) } : {}),
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
- what = `start ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
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 role. */
40
+ /** The help line under the persona select: the picked persona's description. */
41
41
  export function personaHelp(personas, id) {
42
- return (personas ?? []).find((p) => p.id === id)?.role ?? "";
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. */