@desplega.ai/agent-swarm 1.67.1 → 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 +1 -1
- package/package.json +1 -1
- package/src/be/crypto/key-bootstrap.ts +30 -13
- package/src/be/db.ts +13 -4
- package/src/tests/key-bootstrap.test.ts +51 -0
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.
|
|
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
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
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(
|
|
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:",
|
|
@@ -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"));
|