@desplega.ai/agent-swarm 1.67.0 → 1.67.2

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/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.67.0",
5
+ "version": "1.67.2",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.67.0",
3
+ "version": "1.67.2",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -26,22 +26,39 @@ const ENV_KEY = "SECRETS_ENCRYPTION_KEY";
26
26
  const ENV_KEY_FILE = "SECRETS_ENCRYPTION_KEY_FILE";
27
27
  const KEY_FILENAME = ".encryption-key";
28
28
 
29
+ const HEX_32_BYTE_RE = /^[0-9a-fA-F]{64}$/;
30
+
31
+ function keyGenerationHelp(): string {
32
+ return [
33
+ "",
34
+ "How to generate a valid key:",
35
+ " openssl rand -base64 32 # 43-char base64 (recommended)",
36
+ " openssl rand -hex 32 # 64-char hex (also accepted)",
37
+ "",
38
+ "Common mistake: `openssl rand -base64 39` produces a 52-char string that",
39
+ "decodes to 39 bytes — the number passed to `openssl rand` is the decoded",
40
+ "byte count, and AES-256 requires exactly 32.",
41
+ ].join("\n");
42
+ }
43
+
29
44
  function decodeAndValidate(source: string, content: string): Buffer {
30
45
  const trimmed = content.trim();
31
- let decoded: Buffer;
32
- try {
33
- decoded = Buffer.from(trimmed, "base64");
34
- } catch (err) {
35
- throw new Error(
36
- `Invalid encryption key at ${source}: base64 decode failed: ${(err as Error).message}`,
37
- );
38
- }
39
- if (decoded.length !== AES_KEY_BYTES) {
40
- throw new Error(
41
- `Invalid encryption key at ${source}: expected ${AES_KEY_BYTES} bytes after base64 decode, got ${decoded.length} bytes`,
42
- );
46
+
47
+ // Hex path: a 64-char string of [0-9a-fA-F] is unambiguously hex — it would
48
+ // decode to 48 bytes as base64, never 32, so there is no overlap with the
49
+ // base64 happy path.
50
+ if (HEX_32_BYTE_RE.test(trimmed)) {
51
+ const decoded = Buffer.from(trimmed, "hex");
52
+ if (decoded.length === AES_KEY_BYTES) return decoded;
43
53
  }
44
- return decoded;
54
+
55
+ const decoded = Buffer.from(trimmed, "base64");
56
+ if (decoded.length === AES_KEY_BYTES) return decoded;
57
+
58
+ throw new Error(
59
+ `Invalid encryption key at ${source}: expected ${AES_KEY_BYTES} bytes, ` +
60
+ `got ${decoded.length} bytes after base64 decode.\n${keyGenerationHelp()}`,
61
+ );
45
62
  }
46
63
 
47
64
  type ResolveEncryptionKeyOptions = {
package/src/be/db.ts CHANGED
@@ -100,12 +100,21 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
100
100
  database.run("PRAGMA journal_mode = WAL;");
101
101
  database.run("PRAGMA foreign_keys = ON;");
102
102
 
103
- // Load sqlite-vec extension for vector search
103
+ // Load sqlite-vec extension for vector search.
104
+ // In compiled binaries (`bun build --compile`) the JS lives in /$bunfs/ and
105
+ // `require.resolve("sqlite-vec-<platform>/vec0.so")` can't find the native
106
+ // asset — so we prefer an explicit filesystem path when set, and only fall
107
+ // back to the npm resolver for normal dev runs.
104
108
  try {
105
- const sqliteVec = require("sqlite-vec");
106
- sqliteVec.load(database);
109
+ const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
110
+ if (extensionPath) {
111
+ database.loadExtension(extensionPath);
112
+ } else {
113
+ const sqliteVec = require("sqlite-vec");
114
+ sqliteVec.load(database);
115
+ }
107
116
  sqliteVecAvailable = true;
108
- console.log("[db] sqlite-vec loaded");
117
+ console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
109
118
  } catch (err) {
110
119
  console.warn(
111
120
  "[db] sqlite-vec not available, falling back to in-memory cosine:",
@@ -0,0 +1,3 @@
1
+ -- Migration 039: Remove hardcoded seed users from migration 031
2
+ -- Re-add via scripts/backfill-seed-users.sql after deploy
3
+ DELETE FROM users WHERE email IN ('t@desplega.ai', 'e@desplega.ai');
@@ -59,6 +59,8 @@ As the lead agent, you coordinate all worker agents in the swarm.
59
59
  - \`send-task\`: Assign a task to a specific worker or to the general pool. Slack/AgentMail metadata auto-inherits from parent task.
60
60
  - \`store-progress\`: Track coordination notes or update task status
61
61
 
62
+ **User Registration:** When a task arrives from an unknown user (no \`requestedByUserId\`), use the \`manage-user\` tool to register them before proceeding. Resolve their identity from the Slack metadata (user ID, display name) attached to the task.
63
+
62
64
  **Slack:**
63
65
  - \`slack-reply\`: Reply to user in the Slack thread (use taskId for context)
64
66
  - \`slack-read\`: Read thread/channel history (use taskId or channelId)
@@ -66,6 +68,7 @@ As the lead agent, you coordinate all worker agents in the swarm.
66
68
 
67
69
  **Identity:**
68
70
  - \`update-profile\`: Update your own or other agents' profile fields (name, role, capabilities, soulMd, identityMd, heartbeatMd, claudeMd, toolsMd, setupScript)
71
+ - \`manage-user\`: Register or update human users (resolve from Slack/GitHub/GitLab identity)
69
72
 
70
73
  #### Task Routing
71
74
 
@@ -157,6 +157,57 @@ describe("key-bootstrap: validation", () => {
157
157
  expect(() => resolveEncryptionKey(dbPath)).toThrow(/got 16 bytes/);
158
158
  });
159
159
 
160
+ it("error message includes openssl generation commands and the -base64 39 hint", () => {
161
+ process.env[ENV_KEY] = Buffer.alloc(16).toString("base64");
162
+ const dbPath = path.join(tmpDir, "test.sqlite");
163
+ let caught: Error | null = null;
164
+ try {
165
+ resolveEncryptionKey(dbPath);
166
+ } catch (err) {
167
+ caught = err as Error;
168
+ }
169
+ expect(caught).toBeTruthy();
170
+ const msg = caught?.message ?? "";
171
+ expect(msg).toContain("openssl rand -base64 32");
172
+ expect(msg).toContain("openssl rand -hex 32");
173
+ expect(msg).toContain("openssl rand -base64 39");
174
+ });
175
+
176
+ it("accepts a hex-encoded 32-byte key via SECRETS_ENCRYPTION_KEY env var", () => {
177
+ const keyBytes = randomBytes(AES_KEY_BYTES);
178
+ process.env[ENV_KEY] = keyBytes.toString("hex");
179
+ const dbPath = path.join(tmpDir, "test.sqlite");
180
+ const resolved = resolveEncryptionKey(dbPath);
181
+ expect(resolved.equals(keyBytes)).toBe(true);
182
+ });
183
+
184
+ it("accepts a hex-encoded 32-byte key via SECRETS_ENCRYPTION_KEY_FILE", () => {
185
+ const keyBytes = randomBytes(AES_KEY_BYTES);
186
+ const keyPath = path.join(tmpDir, "hex.key");
187
+ writeFileSync(keyPath, keyBytes.toString("hex"));
188
+ process.env[ENV_KEY_FILE] = keyPath;
189
+ const dbPath = path.join(tmpDir, "test.sqlite");
190
+ const resolved = resolveEncryptionKey(dbPath);
191
+ expect(resolved.equals(keyBytes)).toBe(true);
192
+ });
193
+
194
+ it("accepts a hex-encoded 32-byte key via on-disk .encryption-key", () => {
195
+ const keyBytes = randomBytes(AES_KEY_BYTES);
196
+ const keyFilePath = path.join(tmpDir, ".encryption-key");
197
+ writeFileSync(keyFilePath, keyBytes.toString("hex"));
198
+ const dbPath = path.join(tmpDir, "test.sqlite");
199
+ const resolved = resolveEncryptionKey(dbPath);
200
+ expect(resolved.equals(keyBytes)).toBe(true);
201
+ });
202
+
203
+ it("accepts uppercase hex-encoded keys", () => {
204
+ const keyBytes = randomBytes(AES_KEY_BYTES);
205
+ process.env[ENV_KEY] = keyBytes.toString("hex").toUpperCase();
206
+ const dbPath = path.join(tmpDir, "test.sqlite");
207
+ const resolved = resolveEncryptionKey(dbPath);
208
+ expect(resolved.equals(keyBytes)).toBe(true);
209
+ });
210
+
160
211
  it("throws when SECRETS_ENCRYPTION_KEY_FILE points to a malformed file", () => {
161
212
  const keyPath = path.join(tmpDir, "bad.key");
162
213
  writeFileSync(keyPath, Buffer.alloc(10).toString("base64"));