@desplega.ai/agent-swarm 1.87.0 → 1.89.0

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 (102) hide show
  1. package/README.md +5 -1
  2. package/openapi.json +53 -1
  3. package/package.json +6 -5
  4. package/plugin/skills/composio/SKILL.md +98 -0
  5. package/src/be/db.ts +374 -9
  6. package/src/be/migrations/080_skill_system_defaults.sql +8 -0
  7. package/src/be/migrations/081_metrics.sql +39 -0
  8. package/src/be/migrations/082_user_audit_fields.sql +120 -0
  9. package/src/be/modelsdev-cache.json +3825 -2417
  10. package/src/be/seed/registry.ts +3 -2
  11. package/src/be/seed-skills/index.ts +179 -0
  12. package/src/cli.tsx +51 -4
  13. package/src/commands/e2b-stack-wizard.tsx +394 -0
  14. package/src/commands/e2b.ts +1352 -53
  15. package/src/commands/onboard/dashboard-url.ts +29 -0
  16. package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
  17. package/src/commands/onboard.tsx +3 -1
  18. package/src/commands/runner.ts +154 -22
  19. package/src/commands/x.ts +118 -0
  20. package/src/e2b/dispatch.ts +234 -18
  21. package/src/github/handlers.ts +40 -1
  22. package/src/heartbeat/heartbeat.ts +26 -5
  23. package/src/http/active-sessions.ts +32 -1
  24. package/src/http/auth.ts +36 -0
  25. package/src/http/core.ts +20 -16
  26. package/src/http/db-query.ts +20 -0
  27. package/src/http/index.ts +2 -0
  28. package/src/http/memory.ts +13 -1
  29. package/src/http/metrics.ts +447 -0
  30. package/src/http/operator-actor.ts +9 -0
  31. package/src/http/poll.ts +11 -1
  32. package/src/http/skills.ts +53 -0
  33. package/src/http/tasks.ts +4 -1
  34. package/src/http/webhooks.ts +75 -0
  35. package/src/http/workflows.ts +5 -1
  36. package/src/integrations/kapso/client.ts +82 -0
  37. package/src/memory/automatic-task-gate.ts +47 -0
  38. package/src/metrics/version.ts +26 -0
  39. package/src/prompts/base-prompt.ts +24 -1
  40. package/src/prompts/session-templates.ts +74 -0
  41. package/src/providers/claude-adapter.ts +19 -0
  42. package/src/providers/codex-adapter.ts +22 -0
  43. package/src/providers/ctx-mode-env.ts +10 -0
  44. package/src/providers/opencode-adapter.ts +72 -7
  45. package/src/server.ts +10 -1
  46. package/src/slack/blocks.ts +12 -4
  47. package/src/slack/watcher.ts +3 -3
  48. package/src/telemetry.ts +14 -1
  49. package/src/templates.d.ts +4 -0
  50. package/src/tests/base-prompt.test.ts +76 -0
  51. package/src/tests/budget-claim-gate.test.ts +26 -0
  52. package/src/tests/claude-adapter.test.ts +86 -1
  53. package/src/tests/codex-adapter.test.ts +89 -0
  54. package/src/tests/core-auth.test.ts +8 -1
  55. package/src/tests/e2b-dispatch.test.ts +603 -11
  56. package/src/tests/events-http.test.ts +6 -2
  57. package/src/tests/github-handlers-cancel-config.test.ts +262 -0
  58. package/src/tests/heartbeat.test.ts +84 -3
  59. package/src/tests/http-api-integration.test.ts +116 -1
  60. package/src/tests/kapso-client.test.ts +74 -1
  61. package/src/tests/kapso-inbound.test.ts +60 -2
  62. package/src/tests/metrics-http.test.ts +247 -0
  63. package/src/tests/opencode-adapter.test.ts +185 -30
  64. package/src/tests/prompt-template-session.test.ts +4 -2
  65. package/src/tests/runner-repo-autostash.test.ts +117 -0
  66. package/src/tests/runner-requester-profile.test.ts +25 -0
  67. package/src/tests/runner-skills-refresh.test.ts +1 -1
  68. package/src/tests/self-improvement.test.ts +89 -0
  69. package/src/tests/skill-update-scope.test.ts +88 -1
  70. package/src/tests/slack-blocks.test.ts +15 -0
  71. package/src/tests/swarm-x-tool.test.ts +90 -0
  72. package/src/tests/system-default-skills.test.ts +122 -0
  73. package/src/tests/telemetry-init.test.ts +86 -0
  74. package/src/tests/ui-logs-parser.test.ts +271 -0
  75. package/src/tests/user-token-rest-auth.test.ts +129 -0
  76. package/src/tests/workflow-async-v2.test.ts +23 -0
  77. package/src/tests/x-composio.test.ts +122 -0
  78. package/src/tools/create-metric.ts +191 -0
  79. package/src/tools/skills/skill-delete.ts +14 -0
  80. package/src/tools/skills/skill-update.ts +14 -0
  81. package/src/tools/store-progress.ts +19 -5
  82. package/src/tools/swarm-x.ts +116 -0
  83. package/src/tools/tool-config.ts +6 -0
  84. package/src/types.ts +121 -0
  85. package/src/utils/request-auth-context.ts +28 -0
  86. package/src/utils/skills-refresh.ts +2 -2
  87. package/src/workflows/engine.ts +24 -2
  88. package/src/workflows/executors/agent-task.ts +2 -0
  89. package/src/x/composio.ts +295 -0
  90. package/templates/skills/artifacts/config.json +1 -0
  91. package/templates/skills/attio-interaction/SKILL.md +279 -0
  92. package/templates/skills/attio-interaction/config.json +14 -0
  93. package/templates/skills/attio-interaction/content.md +272 -0
  94. package/templates/skills/kv-storage/config.json +1 -0
  95. package/templates/skills/pages/config.json +1 -0
  96. package/templates/skills/scheduled-task-resilience/config.json +1 -0
  97. package/templates/skills/swarm-scripts/SKILL.md +91 -0
  98. package/templates/skills/swarm-scripts/config.json +14 -0
  99. package/templates/skills/swarm-scripts/content.md +86 -0
  100. package/templates/skills/workflow-iterate/config.json +1 -0
  101. package/templates/skills/workflow-structured-output/config.json +1 -0
  102. package/tsconfig.json +2 -1
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { chunkContent } from "../be/chunking";
4
+ import { getTaskById } from "../be/db";
4
5
  import { getEmbeddingProvider, getMemoryStore } from "../be/memory";
5
6
  import { CANDIDATE_SET_MULTIPLIER } from "../be/memory/constants";
6
7
  import { listEdgesForAgent } from "../be/memory/edges-store";
@@ -13,6 +14,7 @@ import {
13
14
  } from "../be/memory/raters/types";
14
15
  import { rerank } from "../be/memory/reranker";
15
16
  import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retrieval-store";
17
+ import { shouldPersistAutomaticTaskMemory } from "../memory/automatic-task-gate";
16
18
  import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
17
19
  import { route } from "./route-def";
18
20
  import { json, jsonError, parseQueryParams } from "./utils";
@@ -34,6 +36,7 @@ const indexMemory = route({
34
36
  sourceTaskId: z.string().uuid().optional(),
35
37
  sourcePath: z.string().optional(),
36
38
  tags: z.array(z.string()).optional(),
39
+ persistMemory: z.boolean().optional(),
37
40
  }),
38
41
  responses: {
39
42
  202: { description: "Content queued for embedding" },
@@ -249,7 +252,16 @@ export async function handleMemory(
249
252
  const parsed = await indexMemory.parse(req, res, pathSegments, new URLSearchParams());
250
253
  if (!parsed) return true;
251
254
 
252
- const { agentId, content, name, scope, source, sourceTaskId, sourcePath, tags } = parsed.body;
255
+ const { agentId, content, name, scope, source, sourceTaskId, sourcePath, tags, persistMemory } =
256
+ parsed.body;
257
+
258
+ if (source === "session_summary" && sourceTaskId) {
259
+ const sourceTask = getTaskById(sourceTaskId);
260
+ if (sourceTask && !shouldPersistAutomaticTaskMemory(sourceTask, persistMemory)) {
261
+ json(res, { queued: false, memoryIds: [], skipped: "automatic_task_memory_disabled" }, 202);
262
+ return true;
263
+ }
264
+ }
253
265
 
254
266
  // Chunk content and create memories
255
267
  const contentChunks = chunkContent(content);
@@ -0,0 +1,447 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import {
4
+ countAllMetrics,
5
+ countMetricsByAgent,
6
+ createMetric,
7
+ deleteMetric,
8
+ getMetric,
9
+ getMetricVersion,
10
+ getMetricVersions,
11
+ listAllMetrics,
12
+ listMetricsByAgent,
13
+ updateMetric,
14
+ } from "../be/db";
15
+ import { snapshotMetric } from "../metrics/version";
16
+ import {
17
+ type Metric,
18
+ MetricDefinitionSchema,
19
+ type MetricParam,
20
+ type MetricSummary,
21
+ type MetricVariable,
22
+ MetricVersionSchema,
23
+ type MetricWidget,
24
+ } from "../types";
25
+ import { assertSelectOnlyQuery, executeReadOnlyQuery } from "./db-query";
26
+ import { route } from "./route-def";
27
+ import { json, jsonError } from "./utils";
28
+
29
+ const DEFAULT_METRIC_MAX_ROWS = 100;
30
+ const HARD_METRIC_MAX_ROWS = 500;
31
+ const VARIABLE_TOKEN_RE = /^\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}$/;
32
+ const MetricRunBodySchema = z
33
+ .object({
34
+ variables: z
35
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
36
+ .optional(),
37
+ })
38
+ .optional();
39
+
40
+ function slugify(input: string): string {
41
+ const slug = input
42
+ .toLowerCase()
43
+ .normalize("NFKD")
44
+ .replace(/[^a-z0-9]+/g, "-")
45
+ .replace(/^-+|-+$/g, "");
46
+ return slug || "metric";
47
+ }
48
+
49
+ function validateMetricDefinition(definition: unknown) {
50
+ const parsed = MetricDefinitionSchema.parse(definition);
51
+ for (const widget of parsed.widgets) {
52
+ assertSelectOnlyQuery(widget.query.sql);
53
+ }
54
+ return parsed;
55
+ }
56
+
57
+ function coerceVariableValue(variable: MetricVariable, raw: unknown): MetricParam {
58
+ if (raw == null || raw === "") {
59
+ return variable.defaultValue ?? null;
60
+ }
61
+ if (variable.type === "number") {
62
+ const numeric = typeof raw === "number" ? raw : Number(raw);
63
+ if (!Number.isFinite(numeric)) {
64
+ throw new Error(`Metric variable "${variable.key}" must be a number`);
65
+ }
66
+ return numeric;
67
+ }
68
+ if (typeof raw === "boolean" || typeof raw === "number" || typeof raw === "string") {
69
+ return raw;
70
+ }
71
+ return String(raw);
72
+ }
73
+
74
+ function resolveMetricVariables(metric: Metric, provided: Record<string, unknown>) {
75
+ const values: Record<string, MetricParam> = {};
76
+ for (const variable of metric.definition.variables ?? []) {
77
+ const value = coerceVariableValue(variable, provided[variable.key]);
78
+ if (variable.options?.length) {
79
+ const allowed = variable.options.some((option) => option.value === value);
80
+ if (!allowed) {
81
+ throw new Error(`Metric variable "${variable.key}" must match one of its options`);
82
+ }
83
+ }
84
+ values[variable.key] = value;
85
+ }
86
+ return values;
87
+ }
88
+
89
+ function resolveWidgetParams(
90
+ widget: MetricWidget,
91
+ variables: Record<string, MetricParam>,
92
+ ): MetricParam[] {
93
+ return (widget.query.params ?? []).map((param) => {
94
+ if (typeof param !== "string") return param;
95
+ const match = VARIABLE_TOKEN_RE.exec(param);
96
+ if (!match) return param;
97
+ const key = match[1]!;
98
+ if (!(key in variables)) {
99
+ throw new Error(`Metric variable "${key}" is not defined`);
100
+ }
101
+ return variables[key] ?? null;
102
+ });
103
+ }
104
+
105
+ function runMetricWidget(widget: MetricWidget, variables: Record<string, MetricParam>) {
106
+ assertSelectOnlyQuery(widget.query.sql);
107
+ const requestedRows = widget.query.maxRows ?? DEFAULT_METRIC_MAX_ROWS;
108
+ const maxRows = Math.min(requestedRows, HARD_METRIC_MAX_ROWS);
109
+ const result = executeReadOnlyQuery(
110
+ widget.query.sql,
111
+ resolveWidgetParams(widget, variables),
112
+ maxRows,
113
+ );
114
+ return {
115
+ widget,
116
+ result: {
117
+ ...result,
118
+ rows: result.rows.map((row) =>
119
+ Object.fromEntries(result.columns.map((column, index) => [column, row[index]])),
120
+ ),
121
+ truncated: result.total > result.rows.length,
122
+ maxRows,
123
+ },
124
+ };
125
+ }
126
+
127
+ function runMetric(metric: Metric, providedVariables: Record<string, unknown> = {}) {
128
+ const variables = resolveMetricVariables(metric, providedVariables);
129
+ const widgets = metric.definition.widgets.map((widget) => runMetricWidget(widget, variables));
130
+ return {
131
+ metric,
132
+ variables,
133
+ widgets,
134
+ // Kept as the first widget result for older callers during the PR cycle.
135
+ result: widgets[0]?.result,
136
+ };
137
+ }
138
+
139
+ const metricDefinitionBody = z.object({
140
+ slug: z.string().min(1).optional(),
141
+ title: z.string().min(1),
142
+ description: z.string().nullable().optional(),
143
+ definition: MetricDefinitionSchema,
144
+ });
145
+
146
+ const createMetricRoute = route({
147
+ method: "post",
148
+ path: "/api/metrics/definitions",
149
+ pattern: ["api", "metrics", "definitions"],
150
+ summary: "Create a metric definition",
151
+ tags: ["Metrics"],
152
+ body: metricDefinitionBody,
153
+ responses: {
154
+ 201: { description: "Metric created" },
155
+ 400: { description: "Invalid metric definition" },
156
+ 409: { description: "Slug already exists for this agent" },
157
+ },
158
+ });
159
+
160
+ const listMetricsRoute = route({
161
+ method: "get",
162
+ path: "/api/metrics/definitions",
163
+ pattern: ["api", "metrics", "definitions"],
164
+ summary: "List metric definitions",
165
+ tags: ["Metrics"],
166
+ query: z.object({
167
+ agentId: z.string().min(1).optional(),
168
+ limit: z.coerce.number().int().min(1).max(500).optional(),
169
+ offset: z.coerce.number().int().min(0).optional(),
170
+ fields: z.enum(["full", "slim"]).optional(),
171
+ }),
172
+ responses: {
173
+ 200: { description: "Metric definitions" },
174
+ },
175
+ });
176
+
177
+ const getMetricRoute = route({
178
+ method: "get",
179
+ path: "/api/metrics/definitions/{id}",
180
+ pattern: ["api", "metrics", "definitions", null],
181
+ summary: "Get a metric definition",
182
+ tags: ["Metrics"],
183
+ params: z.object({ id: z.string() }),
184
+ responses: {
185
+ 200: { description: "Metric definition" },
186
+ 404: { description: "Metric not found" },
187
+ },
188
+ });
189
+
190
+ const updateMetricRoute = route({
191
+ method: "put",
192
+ path: "/api/metrics/definitions/{id}",
193
+ pattern: ["api", "metrics", "definitions", null],
194
+ summary: "Update a metric definition",
195
+ tags: ["Metrics"],
196
+ params: z.object({ id: z.string() }),
197
+ body: metricDefinitionBody.partial(),
198
+ responses: {
199
+ 200: { description: "Metric updated" },
200
+ 404: { description: "Metric not found" },
201
+ },
202
+ });
203
+
204
+ const deleteMetricRoute = route({
205
+ method: "delete",
206
+ path: "/api/metrics/definitions/{id}",
207
+ pattern: ["api", "metrics", "definitions", null],
208
+ summary: "Delete a metric definition",
209
+ tags: ["Metrics"],
210
+ params: z.object({ id: z.string() }),
211
+ responses: {
212
+ 204: { description: "Metric deleted" },
213
+ 404: { description: "Metric not found" },
214
+ },
215
+ });
216
+
217
+ const runMetricRoute = route({
218
+ method: "post",
219
+ path: "/api/metrics/definitions/{id}/run",
220
+ pattern: ["api", "metrics", "definitions", null, "run"],
221
+ summary: "Run a metric definition",
222
+ tags: ["Metrics"],
223
+ params: z.object({ id: z.string() }),
224
+ body: MetricRunBodySchema,
225
+ responses: {
226
+ 200: { description: "Metric result" },
227
+ 400: { description: "Invalid or disallowed query" },
228
+ 404: { description: "Metric not found" },
229
+ },
230
+ });
231
+
232
+ const listMetricVersionsRoute = route({
233
+ method: "get",
234
+ path: "/api/metrics/definitions/{id}/versions",
235
+ pattern: ["api", "metrics", "definitions", null, "versions"],
236
+ summary: "List metric definition versions",
237
+ tags: ["Metrics"],
238
+ params: z.object({ id: z.string() }),
239
+ responses: {
240
+ 200: { description: "Metric version list" },
241
+ 404: { description: "Metric not found" },
242
+ },
243
+ });
244
+
245
+ const getMetricVersionRoute = route({
246
+ method: "get",
247
+ path: "/api/metrics/definitions/{id}/versions/{version}",
248
+ pattern: ["api", "metrics", "definitions", null, "versions", null],
249
+ summary: "Get a metric definition version",
250
+ tags: ["Metrics"],
251
+ params: z.object({ id: z.string(), version: z.coerce.number().int().min(1) }),
252
+ responses: {
253
+ 200: { description: "Metric version" },
254
+ 404: { description: "Metric or version not found" },
255
+ },
256
+ });
257
+
258
+ const metricSchemaRoute = route({
259
+ method: "get",
260
+ path: "/api/metrics/schema",
261
+ pattern: ["api", "metrics", "schema"],
262
+ summary: "Get the metric definition JSON Schema",
263
+ tags: ["Metrics"],
264
+ responses: {
265
+ 200: { description: "Metric definition JSON Schema" },
266
+ },
267
+ });
268
+
269
+ function metricEditCounter(metricId: string): number {
270
+ const versions = getMetricVersions(metricId);
271
+ return versions.length > 0 ? versions[0]!.version + 1 : 1;
272
+ }
273
+
274
+ export async function handleMetrics(
275
+ req: IncomingMessage,
276
+ res: ServerResponse,
277
+ pathSegments: string[],
278
+ queryParams: URLSearchParams,
279
+ myAgentId: string | undefined,
280
+ ): Promise<boolean> {
281
+ if (metricSchemaRoute.match(req.method, pathSegments)) {
282
+ json(res, { schema: z.toJSONSchema(MetricDefinitionSchema, { target: "draft-7" }) });
283
+ return true;
284
+ }
285
+
286
+ if (createMetricRoute.match(req.method, pathSegments)) {
287
+ const parsed = await createMetricRoute.parse(req, res, pathSegments, queryParams);
288
+ if (!parsed) return true;
289
+ const ownerAgentId = myAgentId ?? "ui";
290
+
291
+ try {
292
+ const definition = validateMetricDefinition(parsed.body.definition);
293
+ const slug = parsed.body.slug ?? slugify(parsed.body.title);
294
+ const metric = createMetric({
295
+ agentId: ownerAgentId,
296
+ slug,
297
+ title: parsed.body.title,
298
+ description: parsed.body.description ?? undefined,
299
+ definition,
300
+ });
301
+ json(res, { id: metric.id, version: 1 }, 201);
302
+ } catch (err) {
303
+ const msg = err instanceof Error ? err.message : String(err);
304
+ if (msg.includes("UNIQUE")) {
305
+ const slug = parsed.body.slug ?? slugify(parsed.body.title);
306
+ jsonError(res, `Metric with slug "${slug}" already exists for this agent`, 409);
307
+ return true;
308
+ }
309
+ jsonError(res, msg);
310
+ }
311
+ return true;
312
+ }
313
+
314
+ if (listMetricsRoute.match(req.method, pathSegments)) {
315
+ const parsed = await listMetricsRoute.parse(req, res, pathSegments, queryParams);
316
+ if (!parsed) return true;
317
+ const limit = parsed.query.limit ?? 100;
318
+ const offset = parsed.query.offset ?? 0;
319
+ const full = parsed.query.fields === "full";
320
+ let metrics: Array<Metric | MetricSummary>;
321
+ let total: number;
322
+ if (parsed.query.agentId) {
323
+ metrics = full
324
+ ? listMetricsByAgent(parsed.query.agentId, limit, offset)
325
+ : listMetricsByAgent(parsed.query.agentId, limit, offset, { slim: true });
326
+ total = countMetricsByAgent(parsed.query.agentId);
327
+ } else {
328
+ metrics = full
329
+ ? listAllMetrics(limit, offset)
330
+ : listAllMetrics(limit, offset, { slim: true });
331
+ total = countAllMetrics();
332
+ }
333
+ json(res, { metrics, total, limit, offset });
334
+ return true;
335
+ }
336
+
337
+ if (runMetricRoute.match(req.method, pathSegments)) {
338
+ const parsed = await runMetricRoute.parse(req, res, pathSegments, queryParams);
339
+ if (!parsed) return true;
340
+ const metric = getMetric(parsed.params.id);
341
+ if (!metric) {
342
+ res.writeHead(404);
343
+ res.end();
344
+ return true;
345
+ }
346
+ try {
347
+ json(res, runMetric(metric, parsed.body?.variables ?? {}));
348
+ } catch (err) {
349
+ jsonError(res, err instanceof Error ? err.message : String(err));
350
+ }
351
+ return true;
352
+ }
353
+
354
+ if (getMetricVersionRoute.match(req.method, pathSegments)) {
355
+ const parsed = await getMetricVersionRoute.parse(req, res, pathSegments, queryParams);
356
+ if (!parsed) return true;
357
+ if (!getMetric(parsed.params.id)) {
358
+ res.writeHead(404);
359
+ res.end();
360
+ return true;
361
+ }
362
+ const version = getMetricVersion(parsed.params.id, parsed.params.version);
363
+ if (!version) {
364
+ res.writeHead(404);
365
+ res.end();
366
+ return true;
367
+ }
368
+ json(res, MetricVersionSchema.parse(version));
369
+ return true;
370
+ }
371
+
372
+ if (listMetricVersionsRoute.match(req.method, pathSegments)) {
373
+ const parsed = await listMetricVersionsRoute.parse(req, res, pathSegments, queryParams);
374
+ if (!parsed) return true;
375
+ if (!getMetric(parsed.params.id)) {
376
+ res.writeHead(404);
377
+ res.end();
378
+ return true;
379
+ }
380
+ json(res, { versions: getMetricVersions(parsed.params.id) });
381
+ return true;
382
+ }
383
+
384
+ if (getMetricRoute.match(req.method, pathSegments)) {
385
+ const parsed = await getMetricRoute.parse(req, res, pathSegments, queryParams);
386
+ if (!parsed) return true;
387
+ const metric = getMetric(parsed.params.id);
388
+ if (!metric) {
389
+ res.writeHead(404);
390
+ res.end();
391
+ return true;
392
+ }
393
+ json(res, metric);
394
+ return true;
395
+ }
396
+
397
+ if (updateMetricRoute.match(req.method, pathSegments)) {
398
+ const parsed = await updateMetricRoute.parse(req, res, pathSegments, queryParams);
399
+ if (!parsed) return true;
400
+ if (!getMetric(parsed.params.id)) {
401
+ res.writeHead(404);
402
+ res.end();
403
+ return true;
404
+ }
405
+ try {
406
+ const definition =
407
+ parsed.body.definition !== undefined
408
+ ? validateMetricDefinition(parsed.body.definition)
409
+ : undefined;
410
+ try {
411
+ snapshotMetric(parsed.params.id, myAgentId);
412
+ } catch {
413
+ // Snapshot failures should not block edits, matching Pages.
414
+ }
415
+ const updated = updateMetric(parsed.params.id, {
416
+ title: parsed.body.title,
417
+ description: parsed.body.description ?? undefined,
418
+ definition,
419
+ slug: parsed.body.slug,
420
+ });
421
+ if (!updated) {
422
+ res.writeHead(404);
423
+ res.end();
424
+ return true;
425
+ }
426
+ json(res, { id: updated.id, version: metricEditCounter(updated.id) });
427
+ } catch (err) {
428
+ jsonError(res, err instanceof Error ? err.message : String(err));
429
+ }
430
+ return true;
431
+ }
432
+
433
+ if (deleteMetricRoute.match(req.method, pathSegments)) {
434
+ const parsed = await deleteMetricRoute.parse(req, res, pathSegments, queryParams);
435
+ if (!parsed) return true;
436
+ if (!deleteMetric(parsed.params.id)) {
437
+ res.writeHead(404);
438
+ res.end();
439
+ return true;
440
+ }
441
+ res.writeHead(204);
442
+ res.end();
443
+ return true;
444
+ }
445
+
446
+ return false;
447
+ }
@@ -17,6 +17,7 @@
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import { fingerprintApiKey, type IdentityActor } from "../be/users";
19
19
  import { getApiKey } from "../utils/api-key";
20
+ import { getRequestAuth } from "../utils/request-auth-context";
20
21
  import { jsonError } from "./utils";
21
22
 
22
23
  /**
@@ -40,6 +41,14 @@ function extractBearer(req: IncomingMessage): string | null {
40
41
  * the request when this returns null.
41
42
  */
42
43
  export function getOperatorActor(req: IncomingMessage, res: ServerResponse): IdentityActor | null {
44
+ const auth = getRequestAuth(req);
45
+ if (auth?.kind === "user") {
46
+ return { kind: "user", id: auth.userId };
47
+ }
48
+ if (auth?.kind === "operator") {
49
+ return { kind: "operator", id: auth.fingerprint };
50
+ }
51
+
43
52
  const rawKey = extractBearer(req);
44
53
  const swarmKey = getApiKey();
45
54
 
package/src/http/poll.ts CHANGED
@@ -86,6 +86,10 @@ const pollTriggers = route({
86
86
  const CHANNEL_ACTIVITY_INTERVAL_MS = 60_000; // Check at most once per 60s
87
87
  let lastChannelActivityCheckAt = 0;
88
88
 
89
+ function getRequesterNotes(notes: string | undefined): string | undefined {
90
+ return typeof notes === "string" && notes.trim().length > 0 ? notes : undefined;
91
+ }
92
+
89
93
  // ─── Cursor Commit Endpoint ─────────────────────────────────────────────────
90
94
 
91
95
  const commitCursorsRoute = route({
@@ -256,6 +260,7 @@ export async function handlePoll(
256
260
  const requestedByUser = pendingTask.requestedByUserId
257
261
  ? getUserById(pendingTask.requestedByUserId)
258
262
  : undefined;
263
+ const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
259
264
 
260
265
  return {
261
266
  trigger: {
@@ -263,7 +268,12 @@ export async function handlePoll(
263
268
  taskId: pendingTask.id,
264
269
  task: { ...pendingTask, status: "in_progress" },
265
270
  ...(requestedByUser && {
266
- requestedBy: { name: requestedByUser.name, email: requestedByUser.email },
271
+ requestedBy: {
272
+ name: requestedByUser.name,
273
+ email: requestedByUser.email,
274
+ role: requestedByUser.role,
275
+ notes: requestedByNotes,
276
+ },
267
277
  }),
268
278
  },
269
279
  };
@@ -16,6 +16,9 @@ import { computeAgentSkillsSignature, syncSkillsToFilesystem } from "../be/skill
16
16
  import { route } from "./route-def";
17
17
  import { json, jsonError } from "./utils";
18
18
 
19
+ const SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE =
20
+ "This skill is system-managed and cannot be edited from the UI; it is re-seeded on each start. Fork it under a new name to customize.";
21
+
19
22
  // ─── Route Definitions ───────────────────────────────────────────────────────
20
23
 
21
24
  const listSkillsRoute = route({
@@ -67,6 +70,7 @@ const createSkillRoute = route({
67
70
  type: z.string().optional(),
68
71
  scope: z.string().optional(),
69
72
  ownerAgentId: z.string().optional(),
73
+ systemDefault: z.boolean().optional(),
70
74
  }),
71
75
  responses: {
72
76
  201: { description: "Skill created" },
@@ -85,6 +89,7 @@ const updateSkillRoute = route({
85
89
  body: z.record(z.string(), z.unknown()),
86
90
  responses: {
87
91
  200: { description: "Skill updated" },
92
+ 403: { description: "System-managed skills cannot be edited" },
88
93
  404: { description: "Skill not found" },
89
94
  },
90
95
  });
@@ -99,6 +104,7 @@ const deleteSkillRoute = route({
99
104
  params: z.object({ id: z.string() }),
100
105
  responses: {
101
106
  200: { description: "Skill deleted" },
107
+ 403: { description: "System-managed skills cannot be deleted" },
102
108
  404: { description: "Skill not found" },
103
109
  },
104
110
  });
@@ -452,6 +458,7 @@ export async function handleSkills(
452
458
  agent: pm.agent,
453
459
  disableModelInvocation: pm.disableModelInvocation,
454
460
  userInvocable: pm.userInvocable,
461
+ systemDefault: parsed.body.systemDefault,
455
462
  });
456
463
  json(res, { skill }, 201);
457
464
  } catch (err) {
@@ -465,6 +472,42 @@ export async function handleSkills(
465
472
  const parsed = await updateSkillRoute.parse(req, res, pathSegments, queryParams);
466
473
  if (!parsed) return true;
467
474
 
475
+ const existing = getSkillById(parsed.params.id);
476
+ if (!existing) {
477
+ jsonError(res, "Skill not found", 404);
478
+ return true;
479
+ }
480
+
481
+ const protectedSystemDefaultFields = [
482
+ "content",
483
+ "name",
484
+ "description",
485
+ "type",
486
+ "scope",
487
+ "ownerAgentId",
488
+ "sourceUrl",
489
+ "sourceRepo",
490
+ "sourcePath",
491
+ "sourceBranch",
492
+ "sourceHash",
493
+ "isComplex",
494
+ "allowedTools",
495
+ "model",
496
+ "effort",
497
+ "context",
498
+ "agent",
499
+ "disableModelInvocation",
500
+ "userInvocable",
501
+ "systemDefault",
502
+ ];
503
+ if (
504
+ existing.systemDefault &&
505
+ protectedSystemDefaultFields.some((field) => Object.hasOwn(parsed.body, field))
506
+ ) {
507
+ jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
508
+ return true;
509
+ }
510
+
468
511
  const updates: Record<string, unknown> = {};
469
512
  for (const [key, value] of Object.entries(parsed.body)) {
470
513
  updates[key] = value;
@@ -498,6 +541,16 @@ export async function handleSkills(
498
541
  const parsed = await deleteSkillRoute.parse(req, res, pathSegments, queryParams);
499
542
  if (!parsed) return true;
500
543
 
544
+ const existing = getSkillById(parsed.params.id);
545
+ if (!existing) {
546
+ jsonError(res, "Skill not found", 404);
547
+ return true;
548
+ }
549
+ if (existing.systemDefault) {
550
+ jsonError(res, SYSTEM_DEFAULT_SKILL_LOCKED_MESSAGE, 403);
551
+ return true;
552
+ }
553
+
501
554
  const deleted = deleteSkill(parsed.params.id);
502
555
  if (!deleted) {
503
556
  jsonError(res, "Skill not found", 404);
package/src/http/tasks.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  ProviderNameSchema,
35
35
  ResumeReasonSchema,
36
36
  } from "../types";
37
+ import { getRequestAuth } from "../utils/request-auth-context";
37
38
  import { route } from "./route-def";
38
39
  import { json, jsonError } from "./utils";
39
40
 
@@ -354,7 +355,9 @@ export async function handleTasks(
354
355
  // Tolerant `requestedByUserId`: prevent the deleted-user race from
355
356
  // becoming a 500 — if the referenced user doesn't exist, log and drop
356
357
  // the field rather than letting the FK fail at INSERT.
357
- let requestedByUserId = parsed.body.requestedByUserId || undefined;
358
+ const auth = getRequestAuth(req);
359
+ let requestedByUserId =
360
+ auth?.kind === "user" ? auth.userId : parsed.body.requestedByUserId || undefined;
358
361
  if (requestedByUserId && !getUserById(requestedByUserId)) {
359
362
  console.warn(
360
363
  `[tasks] requestedByUserId ${requestedByUserId} does not exist — coercing to NULL`,