@desplega.ai/agent-swarm 1.93.0 → 1.95.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.
- package/README.md +2 -2
- package/openapi.json +180 -1
- package/package.json +4 -3
- package/src/be/db.ts +74 -9
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +2060 -198
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/index.ts +3 -2
- package/src/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +14 -8
- package/src/commands/runner.ts +84 -13
- package/src/http/index.ts +13 -2
- package/src/http/mcp-oauth.ts +14 -0
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +7 -0
- package/src/model-tiers.ts +140 -0
- package/src/oauth/mcp-wrapper.ts +14 -0
- package/src/providers/claude-managed-models.ts +9 -0
- package/src/providers/codex-skill-resolver.ts +22 -8
- package/src/providers/opencode-adapter.ts +21 -2
- package/src/providers/pi-mono-adapter.ts +143 -26
- package/src/providers/types.ts +12 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/server-user.ts +8 -2
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/credential-check.test.ts +185 -46
- package/src/tests/harness-provider-resolution.test.ts +23 -0
- package/src/tests/http-api-integration.test.ts +19 -0
- package/src/tests/mcp-oauth-queries.test.ts +71 -1
- package/src/tests/mcp-oauth-wrapper.test.ts +109 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +38 -1
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/provider-command-format.test.ts +12 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +7 -1
- package/src/tests/skill-sync.test.ts +15 -3
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +8 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +30 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +2 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +44 -3
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +11 -3
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -136,63 +136,98 @@ describe("checkCodexCredentials", () => {
|
|
|
136
136
|
|
|
137
137
|
// ─── pi-mono ─────────────────────────────────────────────────────────────────
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Stub probes for Bedrock tests. These replace the real @aws-sdk/client-bedrock
|
|
141
|
+
* ListFoundationModels call so unit tests never hit AWS.
|
|
142
|
+
*/
|
|
143
|
+
const bedrockProbeSuccess = async () => {};
|
|
144
|
+
const bedrockProbeAuthFail = async () => {
|
|
145
|
+
throw new Error("ExpiredTokenException: The security token included in the request is expired");
|
|
146
|
+
};
|
|
147
|
+
const bedrockProbeAccessFail = async () => {
|
|
148
|
+
throw new Error("AccessDeniedException: not authorized to perform: bedrock:ListFoundationModels");
|
|
149
|
+
};
|
|
150
|
+
const bedrockProbeRegionFail = async () => {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"ValidationException: Provided region us-west-99 is not supported by Amazon Bedrock",
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
139
156
|
describe("checkPiMonoCredentials", () => {
|
|
140
157
|
const HOME = "/home/worker";
|
|
141
158
|
const AUTH = `${HOME}/.pi/agent/auth.json`;
|
|
142
159
|
|
|
143
|
-
test("ready (file) when ~/.pi/agent/auth.json exists", () => {
|
|
144
|
-
const status = checkPiMonoCredentials({}, { homeDir: HOME, fs: fsWith(new Set([AUTH])) });
|
|
160
|
+
test("ready (file) when ~/.pi/agent/auth.json exists", async () => {
|
|
161
|
+
const status = await checkPiMonoCredentials({}, { homeDir: HOME, fs: fsWith(new Set([AUTH])) });
|
|
145
162
|
expect(status.ready).toBe(true);
|
|
146
163
|
expect(status.satisfiedBy).toBe("file");
|
|
147
164
|
});
|
|
148
165
|
|
|
149
|
-
test("permissive: ready when MODEL_OVERRIDE unset and any one supported key is present", () => {
|
|
166
|
+
test("permissive: ready when MODEL_OVERRIDE unset and any one supported key is present", async () => {
|
|
150
167
|
expect(
|
|
151
|
-
checkPiMonoCredentials({ ANTHROPIC_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })
|
|
168
|
+
(await checkPiMonoCredentials({ ANTHROPIC_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }))
|
|
169
|
+
.ready,
|
|
152
170
|
).toBe(true);
|
|
153
171
|
expect(
|
|
154
|
-
checkPiMonoCredentials({ OPENROUTER_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })
|
|
172
|
+
(await checkPiMonoCredentials({ OPENROUTER_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }))
|
|
173
|
+
.ready,
|
|
155
174
|
).toBe(true);
|
|
156
175
|
expect(
|
|
157
|
-
checkPiMonoCredentials({ OPENAI_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }).ready,
|
|
176
|
+
(await checkPiMonoCredentials({ OPENAI_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })).ready,
|
|
158
177
|
).toBe(true);
|
|
159
178
|
});
|
|
160
179
|
|
|
161
|
-
test("permissive: not ready when MODEL_OVERRIDE unset and no keys are set", () => {
|
|
162
|
-
const status = checkPiMonoCredentials({}, { homeDir: HOME, fs: noFiles });
|
|
180
|
+
test("permissive: not ready when MODEL_OVERRIDE unset and no keys are set", async () => {
|
|
181
|
+
const status = await checkPiMonoCredentials({}, { homeDir: HOME, fs: noFiles });
|
|
163
182
|
expect(status.ready).toBe(false);
|
|
164
183
|
expect(status.missing).toContain("ANTHROPIC_API_KEY");
|
|
165
184
|
expect(status.missing).toContain("OPENROUTER_API_KEY");
|
|
166
185
|
expect(status.missing).toContain("OPENAI_API_KEY");
|
|
167
186
|
});
|
|
168
187
|
|
|
169
|
-
test("strict: MODEL_OVERRIDE=anthropic/... requires ANTHROPIC_API_KEY", () => {
|
|
188
|
+
test("strict: MODEL_OVERRIDE=anthropic/... requires ANTHROPIC_API_KEY", async () => {
|
|
170
189
|
const env = { MODEL_OVERRIDE: "anthropic/claude-sonnet-4" };
|
|
171
|
-
expect(checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles }).ready).toBe(false);
|
|
190
|
+
expect((await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles })).ready).toBe(false);
|
|
172
191
|
expect(
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
(
|
|
193
|
+
await checkPiMonoCredentials(
|
|
194
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
195
|
+
{ homeDir: HOME, fs: noFiles },
|
|
196
|
+
)
|
|
197
|
+
).ready,
|
|
175
198
|
).toBe(true);
|
|
176
199
|
// OPENROUTER_API_KEY does NOT satisfy an anthropic-prefixed model
|
|
177
200
|
expect(
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
(
|
|
202
|
+
await checkPiMonoCredentials(
|
|
203
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
204
|
+
{ homeDir: HOME, fs: noFiles },
|
|
205
|
+
)
|
|
206
|
+
).ready,
|
|
180
207
|
).toBe(false);
|
|
181
208
|
});
|
|
182
209
|
|
|
183
|
-
test("strict: MODEL_OVERRIDE=openrouter/... requires OPENROUTER_API_KEY", () => {
|
|
210
|
+
test("strict: MODEL_OVERRIDE=openrouter/... requires OPENROUTER_API_KEY", async () => {
|
|
184
211
|
const env = { MODEL_OVERRIDE: "openrouter/google/gemini-2.5-flash-lite" };
|
|
185
212
|
expect(
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
(
|
|
214
|
+
await checkPiMonoCredentials(
|
|
215
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
216
|
+
{ homeDir: HOME, fs: noFiles },
|
|
217
|
+
)
|
|
218
|
+
).ready,
|
|
188
219
|
).toBe(true);
|
|
189
220
|
expect(
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
(
|
|
222
|
+
await checkPiMonoCredentials(
|
|
223
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
224
|
+
{ homeDir: HOME, fs: noFiles },
|
|
225
|
+
)
|
|
226
|
+
).ready,
|
|
192
227
|
).toBe(false);
|
|
193
228
|
});
|
|
194
229
|
|
|
195
|
-
test("shortname `sonnet` accepts ANTHROPIC_API_KEY *or* OPENROUTER_API_KEY", () => {
|
|
230
|
+
test("shortname `sonnet` accepts ANTHROPIC_API_KEY *or* OPENROUTER_API_KEY", async () => {
|
|
196
231
|
// Anthropic-shortname models (sonnet/haiku/opus) prefer the native
|
|
197
232
|
// ANTHROPIC_* credential, but pi-mono-adapter reroutes through the
|
|
198
233
|
// OpenRouter mirror when only OPENROUTER_API_KEY is available — so the
|
|
@@ -201,76 +236,180 @@ describe("checkPiMonoCredentials", () => {
|
|
|
201
236
|
// tracked in HEARTBEAT.md (2026-04-13 → 2026-05-11).
|
|
202
237
|
const env = { MODEL_OVERRIDE: "sonnet" };
|
|
203
238
|
expect(
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
(
|
|
240
|
+
await checkPiMonoCredentials(
|
|
241
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
242
|
+
{ homeDir: HOME, fs: noFiles },
|
|
243
|
+
)
|
|
244
|
+
).ready,
|
|
206
245
|
).toBe(true);
|
|
207
246
|
expect(
|
|
208
|
-
|
|
209
|
-
|
|
247
|
+
(
|
|
248
|
+
await checkPiMonoCredentials(
|
|
249
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
250
|
+
{ homeDir: HOME, fs: noFiles },
|
|
251
|
+
)
|
|
252
|
+
).ready,
|
|
210
253
|
).toBe(true);
|
|
211
254
|
// Neither key set → still not ready, and missing includes both options.
|
|
212
|
-
const empty = checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
255
|
+
const empty = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
213
256
|
expect(empty.ready).toBe(false);
|
|
214
257
|
expect(empty.missing).toContain("ANTHROPIC_API_KEY");
|
|
215
258
|
expect(empty.missing).toContain("OPENROUTER_API_KEY");
|
|
216
259
|
});
|
|
217
260
|
|
|
218
|
-
test("haiku and opus shortnames also accept OPENROUTER_API_KEY", () => {
|
|
261
|
+
test("haiku and opus shortnames also accept OPENROUTER_API_KEY", async () => {
|
|
219
262
|
for (const model of ["haiku", "opus"]) {
|
|
220
263
|
const env = { MODEL_OVERRIDE: model };
|
|
221
264
|
expect(
|
|
222
|
-
|
|
223
|
-
|
|
265
|
+
(
|
|
266
|
+
await checkPiMonoCredentials(
|
|
267
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
268
|
+
{ homeDir: HOME, fs: noFiles },
|
|
269
|
+
)
|
|
270
|
+
).ready,
|
|
224
271
|
).toBe(true);
|
|
225
272
|
}
|
|
226
273
|
});
|
|
227
274
|
|
|
228
|
-
// ─── amazon-bedrock:
|
|
229
|
-
// When
|
|
230
|
-
//
|
|
231
|
-
// web-identity, …). agent-swarm does no presence check beyond detecting the
|
|
232
|
-
// `amazon-bedrock/` prefix — the SDK validates at first inference call.
|
|
233
|
-
// Mirrors the codex auth.json "presence-only" pattern.
|
|
275
|
+
// ─── amazon-bedrock prefix inference: probe triggered, result depends on creds ─
|
|
276
|
+
// When BEDROCK_AUTH_MODE is absent and MODEL_OVERRIDE starts with
|
|
277
|
+
// "amazon-bedrock/", the probe runs. Tests inject a stub to avoid hitting AWS.
|
|
234
278
|
|
|
235
|
-
test("amazon-bedrock: ready (sdk-delegated)
|
|
279
|
+
test("amazon-bedrock: probe success → ready (sdk-delegated)", async () => {
|
|
236
280
|
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
237
|
-
const status = checkPiMonoCredentials(env, {
|
|
281
|
+
const status = await checkPiMonoCredentials(env, {
|
|
282
|
+
homeDir: HOME,
|
|
283
|
+
fs: noFiles,
|
|
284
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
285
|
+
});
|
|
238
286
|
expect(status.ready).toBe(true);
|
|
239
287
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
240
288
|
expect(status.missing).toEqual([]);
|
|
241
289
|
});
|
|
242
290
|
|
|
243
|
-
test("amazon-bedrock:
|
|
244
|
-
// The Anthropic-shape key is irrelevant
|
|
245
|
-
// AWS Bedrock, not Anthropic. Reporting satisfiedBy="env" would mislead.
|
|
291
|
+
test("amazon-bedrock: probe success even when ANTHROPIC_API_KEY also set (Bedrock wins)", async () => {
|
|
292
|
+
// The Anthropic-shape key is irrelevant — model is routed through AWS Bedrock.
|
|
246
293
|
const env = {
|
|
247
294
|
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
248
295
|
ANTHROPIC_API_KEY: "x",
|
|
249
296
|
};
|
|
250
|
-
const status = checkPiMonoCredentials(env, {
|
|
297
|
+
const status = await checkPiMonoCredentials(env, {
|
|
298
|
+
homeDir: HOME,
|
|
299
|
+
fs: noFiles,
|
|
300
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
301
|
+
});
|
|
251
302
|
expect(status.ready).toBe(true);
|
|
252
303
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
253
304
|
});
|
|
254
305
|
|
|
255
|
-
test("amazon-bedrock:
|
|
306
|
+
test("amazon-bedrock: probe success even when auth.json exists (Bedrock wins over file)", async () => {
|
|
256
307
|
// auth.json holds Anthropic/OpenRouter/OpenAI creds — none used by Bedrock.
|
|
257
|
-
// Bedrock branch must win over the file probe.
|
|
258
308
|
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
259
|
-
const status = checkPiMonoCredentials(env, {
|
|
309
|
+
const status = await checkPiMonoCredentials(env, {
|
|
260
310
|
homeDir: HOME,
|
|
261
311
|
fs: fsWith(new Set([AUTH])),
|
|
312
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
262
313
|
});
|
|
263
314
|
expect(status.ready).toBe(true);
|
|
264
315
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
265
316
|
});
|
|
266
317
|
|
|
267
|
-
test("amazon-bedrock: provider-prefix match is case-insensitive", () => {
|
|
268
|
-
// Mirrors modelToCredKeys' .toLowerCase() at line 54 of pi-mono-adapter.
|
|
318
|
+
test("amazon-bedrock: provider-prefix match is case-insensitive", async () => {
|
|
269
319
|
const env = { MODEL_OVERRIDE: "Amazon-Bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
270
|
-
const status = checkPiMonoCredentials(env, {
|
|
320
|
+
const status = await checkPiMonoCredentials(env, {
|
|
321
|
+
homeDir: HOME,
|
|
322
|
+
fs: noFiles,
|
|
323
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
324
|
+
});
|
|
271
325
|
expect(status.ready).toBe(true);
|
|
272
326
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
273
327
|
});
|
|
328
|
+
|
|
329
|
+
test("amazon-bedrock: probe auth failure → ready:false with aws-auth hint", async () => {
|
|
330
|
+
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
331
|
+
const status = await checkPiMonoCredentials(env, {
|
|
332
|
+
homeDir: HOME,
|
|
333
|
+
fs: noFiles,
|
|
334
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
335
|
+
});
|
|
336
|
+
expect(status.ready).toBe(false);
|
|
337
|
+
expect(status.hint).toContain("aws sso login");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("amazon-bedrock: probe access failure → ready:false with aws-access hint", async () => {
|
|
341
|
+
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
342
|
+
const status = await checkPiMonoCredentials(env, {
|
|
343
|
+
homeDir: HOME,
|
|
344
|
+
fs: noFiles,
|
|
345
|
+
bedrockProbe: bedrockProbeAccessFail,
|
|
346
|
+
});
|
|
347
|
+
expect(status.ready).toBe(false);
|
|
348
|
+
expect(status.hint).toContain("bedrock:InvokeModel");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("amazon-bedrock: probe region failure → ready:false (unclassified hint)", async () => {
|
|
352
|
+
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
353
|
+
const status = await checkPiMonoCredentials(env, {
|
|
354
|
+
homeDir: HOME,
|
|
355
|
+
fs: noFiles,
|
|
356
|
+
bedrockProbe: bedrockProbeRegionFail,
|
|
357
|
+
});
|
|
358
|
+
expect(status.ready).toBe(false);
|
|
359
|
+
// Not matching a known AWS category → raw probe error surfaced in hint
|
|
360
|
+
expect(status.hint).toBeDefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ─── BEDROCK_AUTH_MODE=sdk: explicit mode, decoupled from MODEL_OVERRIDE ────
|
|
364
|
+
|
|
365
|
+
test("BEDROCK_AUTH_MODE=sdk: probe triggered even without amazon-bedrock/ prefix", async () => {
|
|
366
|
+
// Explicit mode — MODEL_OVERRIDE can be anything (or absent); the Bedrock
|
|
367
|
+
// path is taken because the operator explicitly declared BEDROCK_AUTH_MODE=sdk.
|
|
368
|
+
const env = { BEDROCK_AUTH_MODE: "sdk", MODEL_OVERRIDE: "some-other-model" };
|
|
369
|
+
const status = await checkPiMonoCredentials(env, {
|
|
370
|
+
homeDir: HOME,
|
|
371
|
+
fs: noFiles,
|
|
372
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
373
|
+
});
|
|
374
|
+
expect(status.ready).toBe(true);
|
|
375
|
+
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("BEDROCK_AUTH_MODE=sdk: probe failure → ready:false", async () => {
|
|
379
|
+
const env = { BEDROCK_AUTH_MODE: "sdk" };
|
|
380
|
+
const status = await checkPiMonoCredentials(env, {
|
|
381
|
+
homeDir: HOME,
|
|
382
|
+
fs: noFiles,
|
|
383
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
384
|
+
});
|
|
385
|
+
expect(status.ready).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("BEDROCK_AUTH_MODE=bearer: does NOT trigger the sdk probe (falls through)", async () => {
|
|
389
|
+
// The bearer path is declared/validated but the full implementation is
|
|
390
|
+
// out of scope for PR1. With no other credentials set it should be not-ready
|
|
391
|
+
// via the standard permissive check, not via the sdk probe.
|
|
392
|
+
const env = { BEDROCK_AUTH_MODE: "bearer" };
|
|
393
|
+
// No other keys set, no auth.json → not-ready from the permissive path.
|
|
394
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
395
|
+
expect(status.ready).toBe(false);
|
|
396
|
+
// Satisfying via any standard key still works for the bearer mode (PR1 scope).
|
|
397
|
+
const withKey = await checkPiMonoCredentials(
|
|
398
|
+
{ BEDROCK_AUTH_MODE: "bearer", ANTHROPIC_API_KEY: "x" },
|
|
399
|
+
{ homeDir: HOME, fs: noFiles },
|
|
400
|
+
);
|
|
401
|
+
expect(withKey.ready).toBe(true);
|
|
402
|
+
expect(withKey.satisfiedBy).toBe("env");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("BEDROCK_AUTH_MODE absent + no MODEL_OVERRIDE=amazon-bedrock: no probe", async () => {
|
|
406
|
+
// Fallback inference: neither BEDROCK_AUTH_MODE nor an amazon-bedrock MODEL_OVERRIDE
|
|
407
|
+
// → standard permissive path, no AWS call.
|
|
408
|
+
const env = { ANTHROPIC_API_KEY: "x" };
|
|
409
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
410
|
+
expect(status.ready).toBe(true);
|
|
411
|
+
expect(status.satisfiedBy).toBe("env");
|
|
412
|
+
});
|
|
274
413
|
});
|
|
275
414
|
|
|
276
415
|
// ─── opencode ────────────────────────────────────────────────────────────────
|
|
@@ -171,6 +171,29 @@ describe("validateConfigValue", () => {
|
|
|
171
171
|
);
|
|
172
172
|
expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", true)).toMatch(/SWARM_USE_CLAUDE_BRIDGE/);
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
test("accepts valid BEDROCK_AUTH_MODE values: sdk, bearer", () => {
|
|
176
|
+
expect(validateConfigValue("BEDROCK_AUTH_MODE", "sdk")).toBeNull();
|
|
177
|
+
expect(validateConfigValue("BEDROCK_AUTH_MODE", "bearer")).toBeNull();
|
|
178
|
+
// key lookup is case-insensitive
|
|
179
|
+
expect(validateConfigValue("bedrock_auth_mode", "sdk")).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("rejects invalid BEDROCK_AUTH_MODE values with a helpful error", () => {
|
|
183
|
+
const badValues = ["Sdk", "SDK", "BEARER", "iam", "sso", "basic", "", " sdk"];
|
|
184
|
+
for (const bad of badValues) {
|
|
185
|
+
const err = validateConfigValue("BEDROCK_AUTH_MODE", bad);
|
|
186
|
+
expect(err).not.toBeNull();
|
|
187
|
+
expect(err).toMatch(/BEDROCK_AUTH_MODE/);
|
|
188
|
+
expect(err).toMatch(/sdk/);
|
|
189
|
+
expect(err).toMatch(/bearer/);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("BEDROCK_AUTH_MODE is optional — absent key is not validated (returns null)", () => {
|
|
194
|
+
// Undefined / unset key → no validator → null (no error)
|
|
195
|
+
expect(validateConfigValue("OTHER_KEY", "sdk")).toBeNull();
|
|
196
|
+
});
|
|
174
197
|
});
|
|
175
198
|
|
|
176
199
|
// ─── getResolvedConfig — scope precedence for HARNESS_PROVIDER ───────────────
|
|
@@ -1012,6 +1012,25 @@ describe("Schedule CRUD", () => {
|
|
|
1012
1012
|
expect(body.task.id).toBeDefined();
|
|
1013
1013
|
});
|
|
1014
1014
|
|
|
1015
|
+
test("POST /api/schedules/:id/run — propagates modelTier to the created task", async () => {
|
|
1016
|
+
const { body: created } = await post("/api/schedules", {
|
|
1017
|
+
body: {
|
|
1018
|
+
name: "model-tier-manual-run",
|
|
1019
|
+
taskTemplate: "Run model tier integration test",
|
|
1020
|
+
cronExpression: "0 * * * *",
|
|
1021
|
+
modelTier: "smart",
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
const { status, body } = await post(`/api/schedules/${created.id}/run`);
|
|
1026
|
+
expect(status).toBe(200);
|
|
1027
|
+
expect(body.task).toBeDefined();
|
|
1028
|
+
expect(body.task.model).toBeUndefined();
|
|
1029
|
+
expect(body.task.modelTier).toBe("smart");
|
|
1030
|
+
|
|
1031
|
+
await del(`/api/schedules/${created.id}`);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1015
1034
|
test("POST /api/schedules/:id/run — disabled schedule returns 400", async () => {
|
|
1016
1035
|
// Disable the schedule first
|
|
1017
1036
|
await put(`/api/schedules/${scheduleId}`, {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createMcpServer,
|
|
6
|
+
createUser,
|
|
7
|
+
getMcpServerById,
|
|
8
|
+
initDb,
|
|
9
|
+
updateMcpServer,
|
|
10
|
+
} from "../be/db";
|
|
4
11
|
import {
|
|
5
12
|
consumeMcpOAuthPending,
|
|
6
13
|
deleteMcpOAuthToken,
|
|
@@ -221,6 +228,69 @@ describe("mcp_oauth_pending (state PK)", () => {
|
|
|
221
228
|
});
|
|
222
229
|
});
|
|
223
230
|
|
|
231
|
+
describe("mcp_servers.extraAuthorizeParams round-trip", () => {
|
|
232
|
+
test("createMcpServer persists extraAuthorizeParams", () => {
|
|
233
|
+
const server = createMcpServer({
|
|
234
|
+
name: "bigquery-mcp",
|
|
235
|
+
transport: "http",
|
|
236
|
+
url: "https://bigquery.googleapis.com/",
|
|
237
|
+
scope: "swarm",
|
|
238
|
+
extraAuthorizeParams: '{"access_type":"offline","prompt":"consent"}',
|
|
239
|
+
});
|
|
240
|
+
expect(server.extraAuthorizeParams).toBe('{"access_type":"offline","prompt":"consent"}');
|
|
241
|
+
|
|
242
|
+
const fetched = getMcpServerById(server.id);
|
|
243
|
+
expect(fetched).not.toBeNull();
|
|
244
|
+
expect(fetched!.extraAuthorizeParams).toBe('{"access_type":"offline","prompt":"consent"}');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("createMcpServer with no extraAuthorizeParams defaults to null", () => {
|
|
248
|
+
const server = createMcpServer({
|
|
249
|
+
name: "hubspot-mcp",
|
|
250
|
+
transport: "http",
|
|
251
|
+
url: "https://api.hubspot.com/",
|
|
252
|
+
scope: "swarm",
|
|
253
|
+
});
|
|
254
|
+
expect(server.extraAuthorizeParams).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("updateMcpServer persists extraAuthorizeParams and bumps version", () => {
|
|
258
|
+
const server = createMcpServer({
|
|
259
|
+
name: "gdrive-mcp",
|
|
260
|
+
transport: "http",
|
|
261
|
+
url: "https://www.googleapis.com/drive/v3/",
|
|
262
|
+
scope: "swarm",
|
|
263
|
+
});
|
|
264
|
+
const versionBefore = server.version;
|
|
265
|
+
|
|
266
|
+
const updated = updateMcpServer(server.id, {
|
|
267
|
+
extraAuthorizeParams: '{"access_type":"offline","prompt":"consent"}',
|
|
268
|
+
});
|
|
269
|
+
expect(updated).not.toBeNull();
|
|
270
|
+
expect(updated!.extraAuthorizeParams).toBe('{"access_type":"offline","prompt":"consent"}');
|
|
271
|
+
expect(updated!.version).toBe(versionBefore + 1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("updateMcpServer can clear extraAuthorizeParams to null without bumping version twice", () => {
|
|
275
|
+
const server = createMcpServer({
|
|
276
|
+
name: "sheets-mcp",
|
|
277
|
+
transport: "http",
|
|
278
|
+
url: "https://sheets.googleapis.com/",
|
|
279
|
+
scope: "swarm",
|
|
280
|
+
extraAuthorizeParams: '{"access_type":"offline"}',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const cleared = updateMcpServer(server.id, { extraAuthorizeParams: undefined });
|
|
284
|
+
// No extraAuthorizeParams key → no version bump, field untouched
|
|
285
|
+
expect(cleared!.extraAuthorizeParams).toBe('{"access_type":"offline"}');
|
|
286
|
+
expect(cleared!.version).toBe(server.version);
|
|
287
|
+
|
|
288
|
+
const nulled = updateMcpServer(server.id, { extraAuthorizeParams: null as unknown as string });
|
|
289
|
+
expect(nulled!.extraAuthorizeParams).toBeNull();
|
|
290
|
+
expect(nulled!.version).toBe(server.version + 1);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
224
294
|
describe("mcp_servers.authMethod accessor", () => {
|
|
225
295
|
test("default is 'static' for newly created servers", () => {
|
|
226
296
|
const server = makeServer("mcp-auth-default");
|
|
@@ -137,6 +137,115 @@ describe("buildAuthorizeUrl (PKCE S256, RFC 8707)", () => {
|
|
|
137
137
|
});
|
|
138
138
|
expect(new URL(result.url).searchParams.has("scope")).toBe(false);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
test("extraParams are appended to the authorize URL (e.g. BigQuery offline access)", async () => {
|
|
142
|
+
const result = await buildAuthorizeUrl({
|
|
143
|
+
authorizeUrl: "https://as.example.com/authorize",
|
|
144
|
+
tokenUrl: "https://as.example.com/token",
|
|
145
|
+
clientId: "bq-client",
|
|
146
|
+
redirectUri: "https://swarm.example.com/callback",
|
|
147
|
+
scopes: ["https://www.googleapis.com/auth/bigquery"],
|
|
148
|
+
resource: "https://bigquery.googleapis.com/",
|
|
149
|
+
extraParams: { access_type: "offline", prompt: "consent" },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const u = new URL(result.url);
|
|
153
|
+
expect(u.searchParams.get("access_type")).toBe("offline");
|
|
154
|
+
expect(u.searchParams.get("prompt")).toBe("consent");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("extraParams cannot override reserved OAuth params (redirect_uri, state, etc.)", async () => {
|
|
158
|
+
const result = await buildAuthorizeUrl({
|
|
159
|
+
authorizeUrl: "https://as.example.com/authorize",
|
|
160
|
+
tokenUrl: "https://as.example.com/token",
|
|
161
|
+
clientId: "c",
|
|
162
|
+
redirectUri: "https://swarm.example.com/cb",
|
|
163
|
+
scopes: ["read"],
|
|
164
|
+
resource: "https://mcp.example.com/",
|
|
165
|
+
state: "safe-state",
|
|
166
|
+
extraParams: {
|
|
167
|
+
redirect_uri: "https://evil.com",
|
|
168
|
+
state: "injected",
|
|
169
|
+
code_challenge: "malicious",
|
|
170
|
+
code_challenge_method: "plain",
|
|
171
|
+
response_type: "token",
|
|
172
|
+
client_id: "attacker",
|
|
173
|
+
scope: "admin",
|
|
174
|
+
resource: "https://evil.com/",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const u = new URL(result.url);
|
|
178
|
+
expect(u.searchParams.get("redirect_uri")).toBe("https://swarm.example.com/cb");
|
|
179
|
+
expect(u.searchParams.get("state")).toBe("safe-state");
|
|
180
|
+
expect(u.searchParams.get("code_challenge_method")).toBe("S256");
|
|
181
|
+
expect(u.searchParams.get("response_type")).toBe("code");
|
|
182
|
+
expect(u.searchParams.get("client_id")).toBe("c");
|
|
183
|
+
expect(u.searchParams.get("resource")).toBe("https://mcp.example.com/");
|
|
184
|
+
// Attacker values must not have landed
|
|
185
|
+
const challenge = u.searchParams.get("code_challenge");
|
|
186
|
+
expect(challenge).not.toBeNull();
|
|
187
|
+
expect(challenge).not.toBe("malicious");
|
|
188
|
+
expect(u.searchParams.get("scope")).toBe("read");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("mixed-case reserved keys in extraParams are rejected (case-insensitive guard)", async () => {
|
|
192
|
+
const result = await buildAuthorizeUrl({
|
|
193
|
+
authorizeUrl: "https://as.example.com/authorize",
|
|
194
|
+
tokenUrl: "https://as.example.com/token",
|
|
195
|
+
clientId: "c",
|
|
196
|
+
redirectUri: "https://swarm.example.com/cb",
|
|
197
|
+
scopes: ["read"],
|
|
198
|
+
resource: "https://mcp.example.com/",
|
|
199
|
+
state: "safe-state",
|
|
200
|
+
extraParams: {
|
|
201
|
+
Redirect_Uri: "https://evil.example",
|
|
202
|
+
STATE: "evil-state",
|
|
203
|
+
Code_Challenge: "malicious-challenge",
|
|
204
|
+
SCOPE: "admin",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const u = new URL(result.url);
|
|
208
|
+
// Attacker mixed-case keys must NOT appear in the URL
|
|
209
|
+
expect(u.searchParams.get("Redirect_Uri")).toBeNull();
|
|
210
|
+
expect(u.searchParams.get("STATE")).toBeNull();
|
|
211
|
+
expect(u.searchParams.get("Code_Challenge")).toBeNull();
|
|
212
|
+
expect(u.searchParams.get("SCOPE")).toBeNull();
|
|
213
|
+
// Core params must retain their original legitimate values
|
|
214
|
+
expect(u.searchParams.get("redirect_uri")).toBe("https://swarm.example.com/cb");
|
|
215
|
+
expect(u.searchParams.get("state")).toBe("safe-state");
|
|
216
|
+
expect(u.searchParams.get("scope")).toBe("read");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("null/undefined extraParams leaves URL unchanged (no blast radius for existing servers)", async () => {
|
|
220
|
+
const withExtra = await buildAuthorizeUrl({
|
|
221
|
+
authorizeUrl: "https://as.example.com/authorize",
|
|
222
|
+
tokenUrl: "https://as.example.com/token",
|
|
223
|
+
clientId: "c",
|
|
224
|
+
redirectUri: "https://swarm.example.com/cb",
|
|
225
|
+
scopes: ["read"],
|
|
226
|
+
resource: "https://mcp.example.com/",
|
|
227
|
+
extraParams: { access_type: "offline" },
|
|
228
|
+
state: "fixed-state",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const withoutExtra = await buildAuthorizeUrl({
|
|
232
|
+
authorizeUrl: "https://as.example.com/authorize",
|
|
233
|
+
tokenUrl: "https://as.example.com/token",
|
|
234
|
+
clientId: "c",
|
|
235
|
+
redirectUri: "https://swarm.example.com/cb",
|
|
236
|
+
scopes: ["read"],
|
|
237
|
+
resource: "https://mcp.example.com/",
|
|
238
|
+
state: "fixed-state",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const uWith = new URL(withExtra.url);
|
|
242
|
+
const uWithout = new URL(withoutExtra.url);
|
|
243
|
+
expect(uWith.searchParams.has("access_type")).toBe(true);
|
|
244
|
+
expect(uWithout.searchParams.has("access_type")).toBe(false);
|
|
245
|
+
// Core params are identical
|
|
246
|
+
expect(uWith.searchParams.get("client_id")).toBe(uWithout.searchParams.get("client_id"));
|
|
247
|
+
expect(uWith.searchParams.get("state")).toBe(uWithout.searchParams.get("state"));
|
|
248
|
+
});
|
|
140
249
|
});
|
|
141
250
|
|
|
142
251
|
// ─── Discovery (PRMD + AS metadata) ──────────────────────────────────────────
|