@desplega.ai/agent-swarm 1.78.1 → 1.79.1

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 (75) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +1335 -236
  3. package/package.json +4 -4
  4. package/plugin/skills/artifacts/SKILL.md +151 -0
  5. package/plugin/skills/artifacts/examples/static-report.sh +1 -1
  6. package/plugin/skills/kv-storage/SKILL.md +168 -0
  7. package/plugin/skills/pages/SKILL.md +423 -0
  8. package/src/artifact-sdk/browser-sdk.ts +396 -19
  9. package/src/be/db.ts +548 -0
  10. package/src/be/migrations/059_pages.sql +34 -0
  11. package/src/be/migrations/060_page_versions.sql +19 -0
  12. package/src/be/migrations/061_kv_store.sql +34 -0
  13. package/src/be/migrations/062_pages_view_count.sql +9 -0
  14. package/src/commands/artifact.ts +17 -11
  15. package/src/commands/provider-credentials.ts +1 -1
  16. package/src/http/index.ts +9 -1
  17. package/src/http/kv.ts +658 -0
  18. package/src/http/page-proxy.ts +213 -0
  19. package/src/http/pages-public.ts +507 -0
  20. package/src/http/pages.ts +608 -0
  21. package/src/http/status.ts +1 -1
  22. package/src/http/utils.ts +68 -5
  23. package/src/pages/version.ts +44 -0
  24. package/src/prompts/session-templates.ts +51 -0
  25. package/src/providers/pi-mono-adapter.ts +3 -3
  26. package/src/providers/pi-mono-extension.ts +1 -1
  27. package/src/server.ts +29 -1
  28. package/src/tasks/context-key.ts +28 -0
  29. package/src/telemetry.ts +65 -1
  30. package/src/tests/artifact-commands.test.ts +92 -0
  31. package/src/tests/artifact-sdk.test.ts +80 -74
  32. package/src/tests/context-key.test.ts +17 -0
  33. package/src/tests/create-page-tool.test.ts +197 -0
  34. package/src/tests/fixtures/sample-json-page.json +52 -0
  35. package/src/tests/kv-http.test.ts +331 -0
  36. package/src/tests/kv-namespace-resolution.test.ts +172 -0
  37. package/src/tests/kv-page-proxy.test.ts +212 -0
  38. package/src/tests/kv-storage.test.ts +227 -0
  39. package/src/tests/kv-tool.test.ts +217 -0
  40. package/src/tests/launch-password-rejection.test.ts +139 -0
  41. package/src/tests/page-proxy-authed.test.ts +146 -0
  42. package/src/tests/page-proxy.test.ts +270 -0
  43. package/src/tests/page-session.test.ts +169 -0
  44. package/src/tests/pages-actions-endpoint.test.ts +102 -0
  45. package/src/tests/pages-authed-mode.test.ts +211 -0
  46. package/src/tests/pages-http.test.ts +193 -0
  47. package/src/tests/pages-list-endpoint.test.ts +149 -0
  48. package/src/tests/pages-password-hash.test.ts +57 -0
  49. package/src/tests/pages-password-mode.test.ts +265 -0
  50. package/src/tests/pages-public-authed-401.test.ts +102 -0
  51. package/src/tests/pages-public-html.test.ts +151 -0
  52. package/src/tests/pages-public-json-redirect.test.ts +86 -0
  53. package/src/tests/pages-storage.test.ts +196 -0
  54. package/src/tests/pages-versioning.test.ts +231 -0
  55. package/src/tests/pages-view-count.test.ts +220 -0
  56. package/src/tests/prompt-template-session.test.ts +3 -2
  57. package/src/tests/skill-update-scope.test.ts +165 -0
  58. package/src/tests/swarm-diff.test.ts +303 -0
  59. package/src/tests/telemetry-init.test.ts +149 -0
  60. package/src/tests/workflow-wait-event.test.ts +4 -7
  61. package/src/tools/create-page.ts +263 -0
  62. package/src/tools/kv/index.ts +5 -0
  63. package/src/tools/kv/kv-delete.ts +89 -0
  64. package/src/tools/kv/kv-get.ts +64 -0
  65. package/src/tools/kv/kv-incr.ts +116 -0
  66. package/src/tools/kv/kv-list.ts +81 -0
  67. package/src/tools/kv/kv-set.ts +194 -0
  68. package/src/tools/kv/resolve-namespace.ts +58 -0
  69. package/src/tools/skills/skill-update.ts +26 -0
  70. package/src/tools/tool-config.ts +10 -0
  71. package/src/types.ts +107 -0
  72. package/src/utils/internal-ai/complete-structured.ts +2 -2
  73. package/src/utils/internal-ai/credentials.ts +3 -3
  74. package/src/utils/page-session.ts +254 -0
  75. package/plugin/skills/artifacts/skill.md +0 -70
@@ -0,0 +1,194 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, upsertKv } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { KvEntrySchema, KvKeySchema, KvNamespaceSchema, KvValueTypeSchema } from "@/types";
6
+ import { resolveNamespace } from "./resolve-namespace";
7
+
8
+ // 2 MiB cap — mirrors the HTTP enforcement.
9
+ const MAX_KV_BODY_BYTES = 2 * 1024 * 1024;
10
+
11
+ function authError(namespace: string, info: { agentId: string | undefined }): string | null {
12
+ if (namespace.startsWith("task:page:")) {
13
+ // MCP requests don't carry an X-Page-Id; page writes must go through the
14
+ // browser SDK + page proxy.
15
+ return "task:page:* writes require a page-proxy request, not an MCP call";
16
+ }
17
+ if (namespace.startsWith("task:agent:")) {
18
+ const target = namespace.slice("task:agent:".length);
19
+ if (info.agentId && target === info.agentId) return null;
20
+ if (info.agentId) {
21
+ const agent = getAgentById(info.agentId);
22
+ if (agent?.isLead) return null;
23
+ }
24
+ return "writes to another agent's namespace require lead";
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export const registerKvSetTool = (server: McpServer) => {
30
+ createToolRegistrar(server)(
31
+ "kv-set",
32
+ {
33
+ title: "KV Set",
34
+ description:
35
+ "Write a key in the swarm KV store. Upserts atomically. Namespace defaults to your current context. Use `expiresInSec` for opt-in TTL (default: never expires). 2 MiB body cap.",
36
+ annotations: { idempotentHint: true },
37
+
38
+ inputSchema: z.object({
39
+ key: KvKeySchema.describe("KV key (≤512 chars, [a-zA-Z0-9._:/-])."),
40
+ value: z
41
+ .unknown()
42
+ .describe(
43
+ "Value. Stored as JSON by default; pass `valueType: 'string'` or `'integer'` to skip JSON wrapping.",
44
+ ),
45
+ valueType: KvValueTypeSchema.optional().describe(
46
+ "How to encode `value`. Defaults to 'json'. 'integer' is required for INCR.",
47
+ ),
48
+ expiresInSec: z
49
+ .number()
50
+ .int()
51
+ .positive()
52
+ .optional()
53
+ .describe("Optional TTL in seconds. Omit for no expiry."),
54
+ namespace: KvNamespaceSchema.optional().describe(
55
+ "Optional explicit namespace. Defaults to the caller's contextKey.",
56
+ ),
57
+ }),
58
+ outputSchema: z.object({
59
+ yourAgentId: z.string().uuid().optional(),
60
+ success: z.boolean(),
61
+ message: z.string(),
62
+ namespace: z.string().optional(),
63
+ entry: KvEntrySchema.optional(),
64
+ }),
65
+ },
66
+ async ({ key, value, valueType, expiresInSec, namespace }, requestInfo) => {
67
+ const resolved = resolveNamespace(namespace, requestInfo);
68
+ if ("error" in resolved) {
69
+ return {
70
+ content: [{ type: "text", text: resolved.error }],
71
+ structuredContent: {
72
+ yourAgentId: requestInfo.agentId,
73
+ success: false,
74
+ message: resolved.error,
75
+ },
76
+ };
77
+ }
78
+
79
+ const authErr = authError(resolved.namespace, { agentId: requestInfo.agentId });
80
+ if (authErr) {
81
+ return {
82
+ content: [{ type: "text", text: authErr }],
83
+ structuredContent: {
84
+ yourAgentId: requestInfo.agentId,
85
+ success: false,
86
+ message: authErr,
87
+ namespace: resolved.namespace,
88
+ },
89
+ };
90
+ }
91
+
92
+ const finalValueType = valueType ?? "json";
93
+ // Pre-flight encode to surface validation errors as a structured tool
94
+ // response (rather than letting `upsertKv` throw).
95
+ let encodedSize: number;
96
+ try {
97
+ if (finalValueType === "json") {
98
+ const stringified = JSON.stringify(value);
99
+ if (stringified === undefined) {
100
+ const msg = "value is not JSON-encodable";
101
+ return {
102
+ content: [{ type: "text", text: msg }],
103
+ structuredContent: {
104
+ yourAgentId: requestInfo.agentId,
105
+ success: false,
106
+ message: msg,
107
+ namespace: resolved.namespace,
108
+ },
109
+ };
110
+ }
111
+ encodedSize = Buffer.byteLength(stringified, "utf8");
112
+ } else if (finalValueType === "integer") {
113
+ if (typeof value === "number") {
114
+ if (!Number.isInteger(value) || !Number.isSafeInteger(value)) {
115
+ throw new Error("integer value must be a JS-safe integer");
116
+ }
117
+ encodedSize = String(value).length;
118
+ } else if (typeof value === "string" && /^-?\d+$/.test(value)) {
119
+ encodedSize = value.length;
120
+ } else {
121
+ throw new Error("integer value must be a JS-safe integer");
122
+ }
123
+ } else {
124
+ if (typeof value !== "string") {
125
+ throw new Error("string value must be a string");
126
+ }
127
+ encodedSize = Buffer.byteLength(value, "utf8");
128
+ }
129
+ } catch (err) {
130
+ const msg = err instanceof Error ? err.message : "encoding error";
131
+ return {
132
+ content: [{ type: "text", text: msg }],
133
+ structuredContent: {
134
+ yourAgentId: requestInfo.agentId,
135
+ success: false,
136
+ message: msg,
137
+ namespace: resolved.namespace,
138
+ },
139
+ };
140
+ }
141
+
142
+ if (encodedSize > MAX_KV_BODY_BYTES) {
143
+ const msg = `Payload too large (max ${MAX_KV_BODY_BYTES} bytes)`;
144
+ return {
145
+ content: [{ type: "text", text: msg }],
146
+ structuredContent: {
147
+ yourAgentId: requestInfo.agentId,
148
+ success: false,
149
+ message: msg,
150
+ namespace: resolved.namespace,
151
+ },
152
+ };
153
+ }
154
+
155
+ const expiresAt = expiresInSec !== undefined ? Date.now() + expiresInSec * 1000 : null;
156
+
157
+ try {
158
+ const entry = upsertKv({
159
+ namespace: resolved.namespace,
160
+ key,
161
+ value,
162
+ valueType: finalValueType,
163
+ expiresAt,
164
+ });
165
+ return {
166
+ content: [
167
+ {
168
+ type: "text",
169
+ text: `Set "${key}" in "${resolved.namespace}".`,
170
+ },
171
+ ],
172
+ structuredContent: {
173
+ yourAgentId: requestInfo.agentId,
174
+ success: true,
175
+ message: "ok",
176
+ namespace: resolved.namespace,
177
+ entry,
178
+ },
179
+ };
180
+ } catch (err) {
181
+ const msg = err instanceof Error ? err.message : "upsert failed";
182
+ return {
183
+ content: [{ type: "text", text: msg }],
184
+ structuredContent: {
185
+ yourAgentId: requestInfo.agentId,
186
+ success: false,
187
+ message: msg,
188
+ namespace: resolved.namespace,
189
+ },
190
+ };
191
+ }
192
+ },
193
+ );
194
+ };
@@ -0,0 +1,58 @@
1
+ import { getTaskById } from "@/be/db";
2
+ import { agentContextKey } from "@/tasks/context-key";
3
+ import type { RequestInfo } from "@/tools/utils";
4
+
5
+ /**
6
+ * Resolve the KV namespace for an MCP tool call.
7
+ *
8
+ * Tools call this when the user didn't pass an explicit `namespace`. The MCP
9
+ * `RequestInfo` exposes `sourceTaskId` (from `X-Source-Task-Id` header) and
10
+ * `agentId` (from `X-Agent-ID`), mirroring the HTTP-layer precedence:
11
+ *
12
+ * 1. explicit `namespace` (handled by the tool, not us)
13
+ * 2. `sourceTaskId` → that task's `contextKey`
14
+ * 3. `agentId` → `task:agent:<id>`
15
+ * 4. nothing — caller is told to pass `namespace`
16
+ *
17
+ * Note: MCP doesn't carry `X-Page-Id` (pages call the REST surface directly
18
+ * via the browser SDK, not MCP), so page-scoped resolution is HTTP-only.
19
+ */
20
+ export interface ResolvedNamespace {
21
+ namespace: string;
22
+ source: "explicit" | "task" | "agent";
23
+ }
24
+
25
+ export function resolveNamespace(
26
+ explicit: string | undefined,
27
+ info: RequestInfo,
28
+ ): ResolvedNamespace | { error: string } {
29
+ if (explicit && explicit.length > 0) {
30
+ return { namespace: explicit, source: "explicit" };
31
+ }
32
+
33
+ if (info.sourceTaskId) {
34
+ const task = getTaskById(info.sourceTaskId);
35
+ if (task?.contextKey) {
36
+ return { namespace: task.contextKey, source: "task" };
37
+ }
38
+ if (task?.agentId) {
39
+ try {
40
+ return { namespace: agentContextKey({ agentId: task.agentId }), source: "agent" };
41
+ } catch {
42
+ // fall through
43
+ }
44
+ }
45
+ }
46
+
47
+ if (info.agentId) {
48
+ try {
49
+ return { namespace: agentContextKey({ agentId: info.agentId }), source: "agent" };
50
+ } catch {
51
+ // fall through to error
52
+ }
53
+ }
54
+
55
+ return {
56
+ error: "namespace could not be resolved — pass `namespace` or run with X-Agent-ID set",
57
+ };
58
+ }
@@ -16,6 +16,12 @@ export const registerSkillUpdateTool = (server: McpServer) => {
16
16
  skillId: z.string().optional().describe("Skill ID to update"),
17
17
  content: z.string().optional().describe("New SKILL.md content (re-parses frontmatter)"),
18
18
  isEnabled: z.boolean().optional().describe("Toggle enabled/disabled"),
19
+ scope: z
20
+ .enum(["agent", "swarm"])
21
+ .optional()
22
+ .describe(
23
+ "Scope: agent (personal) or swarm (shared). Only leads can promote a skill to swarm scope (used by the skill-approval flow).",
24
+ ),
19
25
  }),
20
26
  outputSchema: z.object({
21
27
  yourAgentId: z.string().uuid().optional(),
@@ -91,6 +97,26 @@ export const registerSkillUpdateTool = (server: McpServer) => {
91
97
  updates.isEnabled = args.isEnabled;
92
98
  }
93
99
 
100
+ if (args.scope !== undefined && args.scope !== existing.scope) {
101
+ // Promoting to swarm scope is the skill-approval path — only leads may do it.
102
+ if (args.scope === "swarm" && !agent?.isLead) {
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: 'Only lead agents can promote a skill to "swarm" scope. Use "skill-publish" to request approval.',
108
+ },
109
+ ],
110
+ structuredContent: {
111
+ yourAgentId: requestInfo.agentId,
112
+ success: false,
113
+ message: "Only lead agents can promote a skill to swarm scope.",
114
+ },
115
+ };
116
+ }
117
+ updates.scope = args.scope;
118
+ }
119
+
94
120
  const skill = updateSkill(args.skillId, updates);
95
121
  if (!skill) {
96
122
  return {
@@ -144,6 +144,16 @@ export const DEFERRED_TOOLS = new Set([
144
144
  "resolve-user",
145
145
  "manage-user",
146
146
 
147
+ // Pages (1)
148
+ "create_page",
149
+
150
+ // KV store (5)
151
+ "kv-get",
152
+ "kv-set",
153
+ "kv-delete",
154
+ "kv-incr",
155
+ "kv-list",
156
+
147
157
  // Other (3)
148
158
  "cancel-task",
149
159
  "inject-learning",
package/src/types.ts CHANGED
@@ -1087,6 +1087,61 @@ export const WorkflowVersionSchema = z.object({
1087
1087
  });
1088
1088
  export type WorkflowVersion = z.infer<typeof WorkflowVersionSchema>;
1089
1089
 
1090
+ // ---------------------------------------------------------------------------
1091
+ // Pages — DB-backed lightweight artifacts (HTML or JSON spec) stored in
1092
+ // SQLite and served at /p/:id. See plan: thoughts/taras/plans/2026-05-12-db-backed-pages/.
1093
+ // PageContentTypeSchema + PageAuthModeSchema MUST stay in sync with the SQL
1094
+ // CHECK constraints in src/be/migrations/059_pages.sql.
1095
+ // ---------------------------------------------------------------------------
1096
+
1097
+ export const PageContentTypeSchema = z.enum(["text/html", "application/json"]);
1098
+ export type PageContentType = z.infer<typeof PageContentTypeSchema>;
1099
+
1100
+ export const PageAuthModeSchema = z.enum(["public", "authed", "password"]);
1101
+ export type PageAuthMode = z.infer<typeof PageAuthModeSchema>;
1102
+
1103
+ // PageSnapshot captures the mutable content fields frozen per-version in
1104
+ // page_versions.snapshot. Omits id / agentId / slug / timestamps (these are
1105
+ // invariant across versions for a given page id; the slug is a parent-only
1106
+ // identifier).
1107
+ export const PageSnapshotSchema = z.object({
1108
+ title: z.string(),
1109
+ description: z.string().optional(),
1110
+ contentType: PageContentTypeSchema,
1111
+ authMode: PageAuthModeSchema,
1112
+ passwordHash: z.string().optional(),
1113
+ body: z.string(),
1114
+ needsCredentials: z.array(z.string()).optional(),
1115
+ });
1116
+ export type PageSnapshot = z.infer<typeof PageSnapshotSchema>;
1117
+
1118
+ export const PageSchema = z.object({
1119
+ id: z.string(),
1120
+ agentId: z.string(),
1121
+ slug: z.string(),
1122
+ title: z.string(),
1123
+ description: z.string().optional(),
1124
+ contentType: PageContentTypeSchema,
1125
+ authMode: PageAuthModeSchema,
1126
+ passwordHash: z.string().optional(),
1127
+ body: z.string(),
1128
+ needsCredentials: z.array(z.string()).optional(),
1129
+ viewCount: z.number().int().min(0).default(0),
1130
+ createdAt: z.string(),
1131
+ updatedAt: z.string(),
1132
+ });
1133
+ export type Page = z.infer<typeof PageSchema>;
1134
+
1135
+ export const PageVersionSchema = z.object({
1136
+ id: z.string(),
1137
+ pageId: z.string(),
1138
+ version: z.number().int().min(1),
1139
+ snapshot: PageSnapshotSchema,
1140
+ changedByAgentId: z.string().optional(),
1141
+ createdAt: z.string(),
1142
+ });
1143
+ export type PageVersion = z.infer<typeof PageVersionSchema>;
1144
+
1090
1145
  // --- Workflow Run ---
1091
1146
 
1092
1147
  export const WorkflowRunStatusSchema = z.enum([
@@ -1426,3 +1481,55 @@ export const BudgetRefusedTriggerSchema = z.object({
1426
1481
  resetAt: z.string(), // ISO 8601, next UTC midnight
1427
1482
  });
1428
1483
  export type BudgetRefusedTrigger = z.infer<typeof BudgetRefusedTriggerSchema>;
1484
+
1485
+ // ─── KV store ────────────────────────────────────────────────────────────────
1486
+
1487
+ /**
1488
+ * `value_type` of a KV entry.
1489
+ *
1490
+ * - `'json'` — `value` is JSON-encoded; default.
1491
+ * - `'string'` — `value` is the raw UTF-8 string verbatim.
1492
+ * - `'integer'` — `value` is the decimal-string form of a JS-safe integer.
1493
+ * Required by INCR; mixing with 'json'/'string' returns 409.
1494
+ */
1495
+ export const KvValueTypeSchema = z.enum(["json", "string", "integer"]);
1496
+ export type KvValueType = z.infer<typeof KvValueTypeSchema>;
1497
+
1498
+ /** Shared regex for both namespace and key — keeps colons/slashes welcome for
1499
+ * sub-namespacing inside a single key. Matches the `contextKey` schema in
1500
+ * `src/tasks/context-key.ts`; we don't enforce it parses as a known family.
1501
+ *
1502
+ * `%` is accepted because path-segment params arrive percent-encoded on the
1503
+ * REST surface (e.g. `:` → `%3A` after `encodeURIComponent`); the kv handler
1504
+ * decodes the segment before persisting, and the decoded form is itself a
1505
+ * subset of this regex (no `%` chars in legal contextKeys).
1506
+ */
1507
+ export const KV_NAME_REGEX = /^[a-zA-Z0-9._:/%-]{1,512}$/;
1508
+
1509
+ export const KvNamespaceSchema = z
1510
+ .string()
1511
+ .min(1)
1512
+ .max(512)
1513
+ .regex(KV_NAME_REGEX, "namespace must match [a-zA-Z0-9._:/%-]{1,512}");
1514
+
1515
+ export const KvKeySchema = z
1516
+ .string()
1517
+ .min(1)
1518
+ .max(512)
1519
+ .regex(KV_NAME_REGEX, "key must match [a-zA-Z0-9._:/%-]{1,512}");
1520
+
1521
+ /**
1522
+ * A single KV row, as returned by the API. `value` is decoded per
1523
+ * `valueType`: `'json'` returns the parsed JS value, `'string'` returns the
1524
+ * raw string, `'integer'` returns a number.
1525
+ */
1526
+ export const KvEntrySchema = z.object({
1527
+ namespace: z.string(),
1528
+ key: z.string(),
1529
+ value: z.unknown(),
1530
+ valueType: KvValueTypeSchema,
1531
+ expiresAt: z.number().int().nullable(),
1532
+ createdAt: z.number().int(),
1533
+ updatedAt: z.number().int(),
1534
+ });
1535
+ export type KvEntry = z.infer<typeof KvEntrySchema>;
@@ -15,8 +15,8 @@
15
15
  * Worker-safe: uses fetch() only, no bun:sqlite import.
16
16
  */
17
17
 
18
- import type { ToolCall } from "@mariozechner/pi-ai";
19
- import { complete, getModel } from "@mariozechner/pi-ai";
18
+ import type { ToolCall } from "@earendil-works/pi-ai";
19
+ import { complete, getModel } from "@earendil-works/pi-ai";
20
20
  import type { TSchema } from "typebox";
21
21
  import { z } from "zod";
22
22
  import { type ResolvedCredential, resolveCredential } from "./credentials.js";
@@ -15,9 +15,9 @@
15
15
  * Worker-safe: uses fetch() only, no bun:sqlite import.
16
16
  */
17
17
 
18
- import type { OAuthCredentials } from "@mariozechner/pi-ai";
19
- import { getEnvApiKey } from "@mariozechner/pi-ai";
20
- import { getOAuthApiKey } from "@mariozechner/pi-ai/oauth";
18
+ import type { OAuthCredentials } from "@earendil-works/pi-ai";
19
+ import { getEnvApiKey } from "@earendil-works/pi-ai";
20
+ import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth";
21
21
  import { getValidCodexOAuth, persistCodexOAuth } from "../../providers/codex-oauth/storage.js";
22
22
  import { type CredentialKind, DEFAULT_MODEL, resolveModelString } from "./models.js";
23
23