@desplega.ai/agent-swarm 1.94.0 → 1.96.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 +3 -3
- package/openapi.json +46 -1
- package/package.json +4 -3
- package/src/be/boot-scrub-logs.ts +76 -0
- package/src/be/db.ts +22 -10
- package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
- package/src/be/modelsdev-cache.json +89422 -85636
- package/src/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +37 -9
- package/src/commands/runner.ts +28 -0
- package/src/http/agents.ts +1 -0
- package/src/http/config.ts +24 -4
- package/src/http/index.ts +9 -0
- package/src/http/mcp-oauth.ts +14 -0
- package/src/oauth/mcp-wrapper.ts +14 -0
- package/src/prompts/session-templates.ts +21 -0
- package/src/providers/codex-skill-resolver.ts +22 -8
- package/src/providers/opencode-adapter.ts +20 -2
- package/src/providers/pi-mono-adapter.ts +160 -21
- package/src/providers/types.ts +33 -0
- package/src/tests/bedrock-model-groups.test.ts +135 -0
- package/src/tests/credential-check.test.ts +538 -50
- package/src/tests/harness-provider-resolution.test.ts +23 -0
- package/src/tests/mcp-oauth-queries.test.ts +71 -1
- package/src/tests/mcp-oauth-wrapper.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +29 -1
- package/src/tests/provider-command-format.test.ts +12 -0
- package/src/tests/secret-scrubber.test.ts +73 -1
- package/src/tests/skill-fs-writer.test.ts +7 -1
- package/src/tests/skill-sync.test.ts +15 -3
- 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/swarm-config/get-config.ts +9 -1
- package/src/tools/swarm-config/list-config.ts +8 -0
- package/src/types.ts +22 -0
- package/src/utils/secret-scrubber.ts +33 -12
- package/src/utils/skill-fs-writer.ts +11 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
buildCredStatusReport,
|
|
4
4
|
checkProviderCredentials,
|
|
@@ -11,6 +11,7 @@ import { checkCodexCredentials } from "../providers/codex-adapter";
|
|
|
11
11
|
import { checkDevinCredentials } from "../providers/devin-adapter";
|
|
12
12
|
import { checkOpencodeCredentials } from "../providers/opencode-adapter";
|
|
13
13
|
import { checkPiMonoCredentials } from "../providers/pi-mono-adapter";
|
|
14
|
+
import { AgentCredStatusSchema } from "../types";
|
|
14
15
|
|
|
15
16
|
/** Build a stub `fs` whose `existsSync` returns true only for paths in the set. */
|
|
16
17
|
function fsWith(present: Set<string>): { existsSync(p: string): boolean } {
|
|
@@ -136,63 +137,99 @@ describe("checkCodexCredentials", () => {
|
|
|
136
137
|
|
|
137
138
|
// ─── pi-mono ─────────────────────────────────────────────────────────────────
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Stub probes for Bedrock tests. These replace the real @aws-sdk/client-bedrock
|
|
142
|
+
* `ListFoundationModels` + `ListInferenceProfiles` enumeration so unit tests
|
|
143
|
+
* never hit AWS.
|
|
144
|
+
*/
|
|
145
|
+
const bedrockProbeSuccess = async () => {};
|
|
146
|
+
const bedrockProbeAuthFail = async () => {
|
|
147
|
+
throw new Error("ExpiredTokenException: The security token included in the request is expired");
|
|
148
|
+
};
|
|
149
|
+
const bedrockProbeAccessFail = async () => {
|
|
150
|
+
throw new Error("AccessDeniedException: not authorized to perform: bedrock:ListFoundationModels");
|
|
151
|
+
};
|
|
152
|
+
const bedrockProbeRegionFail = async () => {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"ValidationException: Provided region us-west-99 is not supported by Amazon Bedrock",
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
139
158
|
describe("checkPiMonoCredentials", () => {
|
|
140
159
|
const HOME = "/home/worker";
|
|
141
160
|
const AUTH = `${HOME}/.pi/agent/auth.json`;
|
|
142
161
|
|
|
143
|
-
test("ready (file) when ~/.pi/agent/auth.json exists", () => {
|
|
144
|
-
const status = checkPiMonoCredentials({}, { homeDir: HOME, fs: fsWith(new Set([AUTH])) });
|
|
162
|
+
test("ready (file) when ~/.pi/agent/auth.json exists", async () => {
|
|
163
|
+
const status = await checkPiMonoCredentials({}, { homeDir: HOME, fs: fsWith(new Set([AUTH])) });
|
|
145
164
|
expect(status.ready).toBe(true);
|
|
146
165
|
expect(status.satisfiedBy).toBe("file");
|
|
147
166
|
});
|
|
148
167
|
|
|
149
|
-
test("permissive: ready when MODEL_OVERRIDE unset and any one supported key is present", () => {
|
|
168
|
+
test("permissive: ready when MODEL_OVERRIDE unset and any one supported key is present", async () => {
|
|
150
169
|
expect(
|
|
151
|
-
checkPiMonoCredentials({ ANTHROPIC_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })
|
|
170
|
+
(await checkPiMonoCredentials({ ANTHROPIC_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }))
|
|
171
|
+
.ready,
|
|
152
172
|
).toBe(true);
|
|
153
173
|
expect(
|
|
154
|
-
checkPiMonoCredentials({ OPENROUTER_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })
|
|
174
|
+
(await checkPiMonoCredentials({ OPENROUTER_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }))
|
|
175
|
+
.ready,
|
|
155
176
|
).toBe(true);
|
|
156
177
|
expect(
|
|
157
|
-
checkPiMonoCredentials({ OPENAI_API_KEY: "x" }, { homeDir: HOME, fs: noFiles }).ready,
|
|
178
|
+
(await checkPiMonoCredentials({ OPENAI_API_KEY: "x" }, { homeDir: HOME, fs: noFiles })).ready,
|
|
158
179
|
).toBe(true);
|
|
159
180
|
});
|
|
160
181
|
|
|
161
|
-
test("permissive: not ready when MODEL_OVERRIDE unset and no keys are set", () => {
|
|
162
|
-
const status = checkPiMonoCredentials({}, { homeDir: HOME, fs: noFiles });
|
|
182
|
+
test("permissive: not ready when MODEL_OVERRIDE unset and no keys are set", async () => {
|
|
183
|
+
const status = await checkPiMonoCredentials({}, { homeDir: HOME, fs: noFiles });
|
|
163
184
|
expect(status.ready).toBe(false);
|
|
164
185
|
expect(status.missing).toContain("ANTHROPIC_API_KEY");
|
|
165
186
|
expect(status.missing).toContain("OPENROUTER_API_KEY");
|
|
166
187
|
expect(status.missing).toContain("OPENAI_API_KEY");
|
|
167
188
|
});
|
|
168
189
|
|
|
169
|
-
test("strict: MODEL_OVERRIDE=anthropic/... requires ANTHROPIC_API_KEY", () => {
|
|
190
|
+
test("strict: MODEL_OVERRIDE=anthropic/... requires ANTHROPIC_API_KEY", async () => {
|
|
170
191
|
const env = { MODEL_OVERRIDE: "anthropic/claude-sonnet-4" };
|
|
171
|
-
expect(checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles }).ready).toBe(false);
|
|
192
|
+
expect((await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles })).ready).toBe(false);
|
|
172
193
|
expect(
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
(
|
|
195
|
+
await checkPiMonoCredentials(
|
|
196
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
197
|
+
{ homeDir: HOME, fs: noFiles },
|
|
198
|
+
)
|
|
199
|
+
).ready,
|
|
175
200
|
).toBe(true);
|
|
176
201
|
// OPENROUTER_API_KEY does NOT satisfy an anthropic-prefixed model
|
|
177
202
|
expect(
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
(
|
|
204
|
+
await checkPiMonoCredentials(
|
|
205
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
206
|
+
{ homeDir: HOME, fs: noFiles },
|
|
207
|
+
)
|
|
208
|
+
).ready,
|
|
180
209
|
).toBe(false);
|
|
181
210
|
});
|
|
182
211
|
|
|
183
|
-
test("strict: MODEL_OVERRIDE=openrouter/... requires OPENROUTER_API_KEY", () => {
|
|
212
|
+
test("strict: MODEL_OVERRIDE=openrouter/... requires OPENROUTER_API_KEY", async () => {
|
|
184
213
|
const env = { MODEL_OVERRIDE: "openrouter/google/gemini-2.5-flash-lite" };
|
|
185
214
|
expect(
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
(
|
|
216
|
+
await checkPiMonoCredentials(
|
|
217
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
218
|
+
{ homeDir: HOME, fs: noFiles },
|
|
219
|
+
)
|
|
220
|
+
).ready,
|
|
188
221
|
).toBe(true);
|
|
189
222
|
expect(
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
(
|
|
224
|
+
await checkPiMonoCredentials(
|
|
225
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
226
|
+
{ homeDir: HOME, fs: noFiles },
|
|
227
|
+
)
|
|
228
|
+
).ready,
|
|
192
229
|
).toBe(false);
|
|
193
230
|
});
|
|
194
231
|
|
|
195
|
-
test("shortname `sonnet` accepts ANTHROPIC_API_KEY *or* OPENROUTER_API_KEY", () => {
|
|
232
|
+
test("shortname `sonnet` accepts ANTHROPIC_API_KEY *or* OPENROUTER_API_KEY", async () => {
|
|
196
233
|
// Anthropic-shortname models (sonnet/haiku/opus) prefer the native
|
|
197
234
|
// ANTHROPIC_* credential, but pi-mono-adapter reroutes through the
|
|
198
235
|
// OpenRouter mirror when only OPENROUTER_API_KEY is available — so the
|
|
@@ -201,76 +238,307 @@ describe("checkPiMonoCredentials", () => {
|
|
|
201
238
|
// tracked in HEARTBEAT.md (2026-04-13 → 2026-05-11).
|
|
202
239
|
const env = { MODEL_OVERRIDE: "sonnet" };
|
|
203
240
|
expect(
|
|
204
|
-
|
|
205
|
-
|
|
241
|
+
(
|
|
242
|
+
await checkPiMonoCredentials(
|
|
243
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
244
|
+
{ homeDir: HOME, fs: noFiles },
|
|
245
|
+
)
|
|
246
|
+
).ready,
|
|
206
247
|
).toBe(true);
|
|
207
248
|
expect(
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
(
|
|
250
|
+
await checkPiMonoCredentials(
|
|
251
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
252
|
+
{ homeDir: HOME, fs: noFiles },
|
|
253
|
+
)
|
|
254
|
+
).ready,
|
|
210
255
|
).toBe(true);
|
|
211
256
|
// Neither key set → still not ready, and missing includes both options.
|
|
212
|
-
const empty = checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
257
|
+
const empty = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
213
258
|
expect(empty.ready).toBe(false);
|
|
214
259
|
expect(empty.missing).toContain("ANTHROPIC_API_KEY");
|
|
215
260
|
expect(empty.missing).toContain("OPENROUTER_API_KEY");
|
|
216
261
|
});
|
|
217
262
|
|
|
218
|
-
test("haiku and opus shortnames also accept OPENROUTER_API_KEY", () => {
|
|
263
|
+
test("haiku and opus shortnames also accept OPENROUTER_API_KEY", async () => {
|
|
219
264
|
for (const model of ["haiku", "opus"]) {
|
|
220
265
|
const env = { MODEL_OVERRIDE: model };
|
|
221
266
|
expect(
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
(
|
|
268
|
+
await checkPiMonoCredentials(
|
|
269
|
+
{ ...env, OPENROUTER_API_KEY: "x" },
|
|
270
|
+
{ homeDir: HOME, fs: noFiles },
|
|
271
|
+
)
|
|
272
|
+
).ready,
|
|
224
273
|
).toBe(true);
|
|
225
274
|
}
|
|
226
275
|
});
|
|
227
276
|
|
|
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.
|
|
277
|
+
// ─── amazon-bedrock prefix inference: probe triggered, result depends on creds ─
|
|
278
|
+
// When BEDROCK_AUTH_MODE is absent and MODEL_OVERRIDE starts with
|
|
279
|
+
// "amazon-bedrock/", the probe runs. Tests inject a stub to avoid hitting AWS.
|
|
234
280
|
|
|
235
|
-
test("amazon-bedrock: ready (sdk-delegated)
|
|
236
|
-
const env = {
|
|
237
|
-
|
|
281
|
+
test("amazon-bedrock: probe success → ready (sdk-delegated)", async () => {
|
|
282
|
+
const env = {
|
|
283
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
284
|
+
AWS_REGION: "us-east-1",
|
|
285
|
+
};
|
|
286
|
+
const status = await checkPiMonoCredentials(env, {
|
|
287
|
+
homeDir: HOME,
|
|
288
|
+
fs: noFiles,
|
|
289
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
290
|
+
});
|
|
238
291
|
expect(status.ready).toBe(true);
|
|
239
292
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
240
293
|
expect(status.missing).toEqual([]);
|
|
241
294
|
});
|
|
242
295
|
|
|
243
|
-
test("amazon-bedrock:
|
|
244
|
-
// The Anthropic-shape key is irrelevant
|
|
245
|
-
// AWS Bedrock, not Anthropic. Reporting satisfiedBy="env" would mislead.
|
|
296
|
+
test("amazon-bedrock: probe success even when ANTHROPIC_API_KEY also set (Bedrock wins)", async () => {
|
|
297
|
+
// The Anthropic-shape key is irrelevant — model is routed through AWS Bedrock.
|
|
246
298
|
const env = {
|
|
247
299
|
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
248
300
|
ANTHROPIC_API_KEY: "x",
|
|
301
|
+
AWS_REGION: "us-east-1",
|
|
249
302
|
};
|
|
250
|
-
const status = checkPiMonoCredentials(env, {
|
|
303
|
+
const status = await checkPiMonoCredentials(env, {
|
|
304
|
+
homeDir: HOME,
|
|
305
|
+
fs: noFiles,
|
|
306
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
307
|
+
});
|
|
251
308
|
expect(status.ready).toBe(true);
|
|
252
309
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
253
310
|
});
|
|
254
311
|
|
|
255
|
-
test("amazon-bedrock:
|
|
312
|
+
test("amazon-bedrock: probe success even when auth.json exists (Bedrock wins over file)", async () => {
|
|
256
313
|
// auth.json holds Anthropic/OpenRouter/OpenAI creds — none used by Bedrock.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
314
|
+
const env = {
|
|
315
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
316
|
+
AWS_REGION: "us-east-1",
|
|
317
|
+
};
|
|
318
|
+
const status = await checkPiMonoCredentials(env, {
|
|
260
319
|
homeDir: HOME,
|
|
261
320
|
fs: fsWith(new Set([AUTH])),
|
|
321
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
262
322
|
});
|
|
263
323
|
expect(status.ready).toBe(true);
|
|
264
324
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
265
325
|
});
|
|
266
326
|
|
|
267
|
-
test("amazon-bedrock: provider-prefix match is case-insensitive", () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
327
|
+
test("amazon-bedrock: provider-prefix match is case-insensitive", async () => {
|
|
328
|
+
const env = {
|
|
329
|
+
MODEL_OVERRIDE: "Amazon-Bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
330
|
+
AWS_REGION: "us-east-1",
|
|
331
|
+
};
|
|
332
|
+
const status = await checkPiMonoCredentials(env, {
|
|
333
|
+
homeDir: HOME,
|
|
334
|
+
fs: noFiles,
|
|
335
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
336
|
+
});
|
|
337
|
+
expect(status.ready).toBe(true);
|
|
338
|
+
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("amazon-bedrock: probe auth failure → ready:false with aws-auth hint", async () => {
|
|
342
|
+
const env = {
|
|
343
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
344
|
+
AWS_REGION: "us-east-1",
|
|
345
|
+
};
|
|
346
|
+
const status = await checkPiMonoCredentials(env, {
|
|
347
|
+
homeDir: HOME,
|
|
348
|
+
fs: noFiles,
|
|
349
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
350
|
+
});
|
|
351
|
+
expect(status.ready).toBe(false);
|
|
352
|
+
expect(status.hint).toContain("aws sso login");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("amazon-bedrock: probe access failure → ready:false with aws-access hint", async () => {
|
|
356
|
+
const env = {
|
|
357
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
358
|
+
AWS_REGION: "us-east-1",
|
|
359
|
+
};
|
|
360
|
+
const status = await checkPiMonoCredentials(env, {
|
|
361
|
+
homeDir: HOME,
|
|
362
|
+
fs: noFiles,
|
|
363
|
+
bedrockProbe: bedrockProbeAccessFail,
|
|
364
|
+
});
|
|
365
|
+
expect(status.ready).toBe(false);
|
|
366
|
+
expect(status.hint).toContain("bedrock:InvokeModel");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("amazon-bedrock: probe region failure → ready:false (unclassified hint)", async () => {
|
|
370
|
+
const env = {
|
|
371
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
372
|
+
AWS_REGION: "us-east-1",
|
|
373
|
+
};
|
|
374
|
+
const status = await checkPiMonoCredentials(env, {
|
|
375
|
+
homeDir: HOME,
|
|
376
|
+
fs: noFiles,
|
|
377
|
+
bedrockProbe: bedrockProbeRegionFail,
|
|
378
|
+
});
|
|
379
|
+
expect(status.ready).toBe(false);
|
|
380
|
+
// Not matching a known AWS category → raw probe error surfaced in hint
|
|
381
|
+
expect(status.hint).toBeDefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ─── BEDROCK_AUTH_MODE=sdk: explicit mode, decoupled from MODEL_OVERRIDE ────
|
|
385
|
+
|
|
386
|
+
test("BEDROCK_AUTH_MODE=sdk: probe triggered even without amazon-bedrock/ prefix", async () => {
|
|
387
|
+
// Explicit mode — MODEL_OVERRIDE can be anything (or absent); the Bedrock
|
|
388
|
+
// path is taken because the operator explicitly declared BEDROCK_AUTH_MODE=sdk.
|
|
389
|
+
const env = {
|
|
390
|
+
BEDROCK_AUTH_MODE: "sdk",
|
|
391
|
+
MODEL_OVERRIDE: "some-other-model",
|
|
392
|
+
AWS_REGION: "us-east-1",
|
|
393
|
+
};
|
|
394
|
+
const status = await checkPiMonoCredentials(env, {
|
|
395
|
+
homeDir: HOME,
|
|
396
|
+
fs: noFiles,
|
|
397
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
398
|
+
});
|
|
271
399
|
expect(status.ready).toBe(true);
|
|
272
400
|
expect(status.satisfiedBy).toBe("sdk-delegated");
|
|
273
401
|
});
|
|
402
|
+
|
|
403
|
+
test("BEDROCK_AUTH_MODE=sdk: probe failure → ready:false", async () => {
|
|
404
|
+
const env = { BEDROCK_AUTH_MODE: "sdk", AWS_REGION: "us-east-1" };
|
|
405
|
+
const status = await checkPiMonoCredentials(env, {
|
|
406
|
+
homeDir: HOME,
|
|
407
|
+
fs: noFiles,
|
|
408
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
409
|
+
});
|
|
410
|
+
expect(status.ready).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("BEDROCK_AUTH_MODE=bearer: does NOT trigger the sdk probe (falls through)", async () => {
|
|
414
|
+
// The bearer path is declared/validated but the full implementation is
|
|
415
|
+
// not implemented yet. With no other credentials set it should be not-ready
|
|
416
|
+
// via the standard permissive check, not via the sdk probe.
|
|
417
|
+
const env = { BEDROCK_AUTH_MODE: "bearer" };
|
|
418
|
+
// No other keys set, no auth.json → not-ready from the permissive path.
|
|
419
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
420
|
+
expect(status.ready).toBe(false);
|
|
421
|
+
// Satisfying via any standard key still works for the bearer mode today.
|
|
422
|
+
const withKey = await checkPiMonoCredentials(
|
|
423
|
+
{ BEDROCK_AUTH_MODE: "bearer", ANTHROPIC_API_KEY: "x" },
|
|
424
|
+
{ homeDir: HOME, fs: noFiles },
|
|
425
|
+
);
|
|
426
|
+
expect(withKey.ready).toBe(true);
|
|
427
|
+
expect(withKey.satisfiedBy).toBe("env");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("BEDROCK_AUTH_MODE absent + no MODEL_OVERRIDE=amazon-bedrock: no probe", async () => {
|
|
431
|
+
// Fallback inference: neither BEDROCK_AUTH_MODE nor an amazon-bedrock MODEL_OVERRIDE
|
|
432
|
+
// → standard permissive path, no AWS call.
|
|
433
|
+
const env = { ANTHROPIC_API_KEY: "x" };
|
|
434
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
435
|
+
expect(status.ready).toBe(true);
|
|
436
|
+
expect(status.satisfiedBy).toBe("env");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ─── model enumeration (bedrockModels) ───────────────────────────────────
|
|
440
|
+
|
|
441
|
+
test("probe success with model list → bedrockModels populated", async () => {
|
|
442
|
+
const fakeModels = [
|
|
443
|
+
{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" },
|
|
444
|
+
{ id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5" },
|
|
445
|
+
];
|
|
446
|
+
const env = {
|
|
447
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
448
|
+
AWS_REGION: "us-east-1",
|
|
449
|
+
};
|
|
450
|
+
const status = await checkPiMonoCredentials(env, {
|
|
451
|
+
homeDir: HOME,
|
|
452
|
+
fs: noFiles,
|
|
453
|
+
bedrockProbe: async () => fakeModels,
|
|
454
|
+
});
|
|
455
|
+
expect(status.ready).toBe(true);
|
|
456
|
+
expect(status.bedrockModels).toEqual(fakeModels);
|
|
457
|
+
expect(status.bedrockRegion).toBe("us-east-1");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("probe success with void return → bedrockModels is empty array (backward compat)", async () => {
|
|
461
|
+
// Auth-only stubs return void — should not break enumeration callers.
|
|
462
|
+
const env = {
|
|
463
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
464
|
+
AWS_REGION: "us-east-1",
|
|
465
|
+
};
|
|
466
|
+
const status = await checkPiMonoCredentials(env, {
|
|
467
|
+
homeDir: HOME,
|
|
468
|
+
fs: noFiles,
|
|
469
|
+
bedrockProbe: bedrockProbeSuccess, // returns void
|
|
470
|
+
});
|
|
471
|
+
expect(status.ready).toBe(true);
|
|
472
|
+
expect(status.bedrockModels).toEqual([]);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("probe failure → bedrockModels is empty array and bedrockRegion is set", async () => {
|
|
476
|
+
const env = {
|
|
477
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
478
|
+
AWS_REGION: "us-east-1",
|
|
479
|
+
};
|
|
480
|
+
const status = await checkPiMonoCredentials(env, {
|
|
481
|
+
homeDir: HOME,
|
|
482
|
+
fs: noFiles,
|
|
483
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
484
|
+
});
|
|
485
|
+
expect(status.ready).toBe(false);
|
|
486
|
+
expect(status.bedrockModels).toEqual([]);
|
|
487
|
+
expect(status.bedrockRegion).toBe("us-east-1");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("probe uses AWS_REGION from env for bedrockRegion", async () => {
|
|
491
|
+
const env = {
|
|
492
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
493
|
+
AWS_REGION: "eu-west-1",
|
|
494
|
+
};
|
|
495
|
+
const status = await checkPiMonoCredentials(env, {
|
|
496
|
+
homeDir: HOME,
|
|
497
|
+
fs: noFiles,
|
|
498
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
499
|
+
});
|
|
500
|
+
expect(status.bedrockRegion).toBe("eu-west-1");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ─── region not fabricated when AWS_REGION is unset ───────────────────────
|
|
504
|
+
|
|
505
|
+
test("AWS_REGION unset (sdk mode) → not-ready with set-region hint, no probe, no fabricated region", async () => {
|
|
506
|
+
// No us-east-1 fallback: enumerating a guessed region can differ from where
|
|
507
|
+
// inference runs. The probe must NOT run; report a not-ready Bedrock state.
|
|
508
|
+
let probeCalled = false;
|
|
509
|
+
const env = { BEDROCK_AUTH_MODE: "sdk" };
|
|
510
|
+
const status = await checkPiMonoCredentials(env, {
|
|
511
|
+
homeDir: HOME,
|
|
512
|
+
fs: noFiles,
|
|
513
|
+
bedrockProbe: async () => {
|
|
514
|
+
probeCalled = true;
|
|
515
|
+
return [];
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
expect(probeCalled).toBe(false);
|
|
519
|
+
expect(status.ready).toBe(false);
|
|
520
|
+
expect(status.hint).toContain("AWS_REGION");
|
|
521
|
+
expect(status.bedrockModels).toEqual([]);
|
|
522
|
+
// Empty string sentinel, NOT a fabricated "us-east-1".
|
|
523
|
+
expect(status.bedrockRegion).toBe("");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("AWS_REGION unset (prefix inference) → not-ready, bedrock block still reported", async () => {
|
|
527
|
+
const env = { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0" };
|
|
528
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
529
|
+
expect(status.ready).toBe(false);
|
|
530
|
+
expect(status.bedrockRegion).toBe("");
|
|
531
|
+
expect(status.bedrockRegion).not.toBe("us-east-1");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("non-Bedrock path → bedrockModels and bedrockRegion are undefined", async () => {
|
|
535
|
+
// Standard anthropic key path — no Bedrock probe runs.
|
|
536
|
+
const env = { ANTHROPIC_API_KEY: "x" };
|
|
537
|
+
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
538
|
+
expect(status.ready).toBe(true);
|
|
539
|
+
expect(status.bedrockModels).toBeUndefined();
|
|
540
|
+
expect(status.bedrockRegion).toBeUndefined();
|
|
541
|
+
});
|
|
274
542
|
});
|
|
275
543
|
|
|
276
544
|
// ─── opencode ────────────────────────────────────────────────────────────────
|
|
@@ -451,4 +719,224 @@ describe("buildCredStatusReport", () => {
|
|
|
451
719
|
const snap = await buildCredStatusReport("claude", {}, {}, "post_task");
|
|
452
720
|
expect(snap.reportKind).toBe("post_task");
|
|
453
721
|
});
|
|
722
|
+
|
|
723
|
+
// bedrock block in AgentCredStatus
|
|
724
|
+
test("Bedrock SDK mode: bedrock block included with live model list", async () => {
|
|
725
|
+
const fakeModels = [{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" }];
|
|
726
|
+
const env = {
|
|
727
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
728
|
+
AWS_REGION: "us-east-1",
|
|
729
|
+
};
|
|
730
|
+
const snap = await buildCredStatusReport(
|
|
731
|
+
"pi",
|
|
732
|
+
env,
|
|
733
|
+
{ bedrockProbe: async () => fakeModels },
|
|
734
|
+
"boot",
|
|
735
|
+
);
|
|
736
|
+
expect(snap.ready).toBe(true);
|
|
737
|
+
expect(snap.bedrock).not.toBeNull();
|
|
738
|
+
expect(snap.bedrock?.ready).toBe(true);
|
|
739
|
+
expect(snap.bedrock?.models).toEqual(fakeModels);
|
|
740
|
+
expect(snap.bedrock?.region).toBe("us-east-1");
|
|
741
|
+
expect(typeof snap.bedrock?.probedAt).toBe("number");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("Bedrock SDK mode: probe failure → bedrock block has ready:false and empty models", async () => {
|
|
745
|
+
const env = {
|
|
746
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
747
|
+
AWS_REGION: "us-east-1",
|
|
748
|
+
};
|
|
749
|
+
const snap = await buildCredStatusReport(
|
|
750
|
+
"pi",
|
|
751
|
+
env,
|
|
752
|
+
{ bedrockProbe: bedrockProbeAuthFail },
|
|
753
|
+
"boot",
|
|
754
|
+
);
|
|
755
|
+
expect(snap.ready).toBe(false);
|
|
756
|
+
expect(snap.bedrock).not.toBeNull();
|
|
757
|
+
expect(snap.bedrock?.ready).toBe(false);
|
|
758
|
+
expect(snap.bedrock?.models).toEqual([]);
|
|
759
|
+
expect(snap.bedrock?.error).toBeTruthy();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("non-Bedrock pi mode → bedrock block is null", async () => {
|
|
763
|
+
const HOME = "/home/worker";
|
|
764
|
+
const snap = await buildCredStatusReport(
|
|
765
|
+
"pi",
|
|
766
|
+
{ ANTHROPIC_API_KEY: "x" },
|
|
767
|
+
{ homeDir: HOME, fs: noFiles },
|
|
768
|
+
"boot",
|
|
769
|
+
);
|
|
770
|
+
expect(snap.bedrock).toBeNull();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("non-pi provider → bedrock block is null", async () => {
|
|
774
|
+
const snap = await buildCredStatusReport(
|
|
775
|
+
"claude",
|
|
776
|
+
{ CLAUDE_CODE_OAUTH_TOKEN: "tok" },
|
|
777
|
+
{},
|
|
778
|
+
"boot",
|
|
779
|
+
);
|
|
780
|
+
expect(snap.bedrock).toBeNull();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// ─── schema round-trip ───────────────────────────────────────────────────────
|
|
785
|
+
|
|
786
|
+
describe("AgentCredStatusSchema round-trip with bedrock block", () => {
|
|
787
|
+
test("full bedrock block parses and serializes cleanly", () => {
|
|
788
|
+
const raw = {
|
|
789
|
+
ready: true,
|
|
790
|
+
missing: [],
|
|
791
|
+
satisfiedBy: "sdk-delegated",
|
|
792
|
+
hint: "AWS SDK credentials verified via ListFoundationModels (region: us-east-1).",
|
|
793
|
+
liveTest: null,
|
|
794
|
+
latestModel: null,
|
|
795
|
+
reportedAt: Date.now(),
|
|
796
|
+
reportKind: "boot",
|
|
797
|
+
bedrock: {
|
|
798
|
+
region: "us-east-1",
|
|
799
|
+
probedAt: Date.now(),
|
|
800
|
+
ready: true,
|
|
801
|
+
models: [
|
|
802
|
+
{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" },
|
|
803
|
+
{ id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5" },
|
|
804
|
+
],
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
const parsed = AgentCredStatusSchema.safeParse(raw);
|
|
808
|
+
expect(parsed.success).toBe(true);
|
|
809
|
+
if (parsed.success) {
|
|
810
|
+
expect(parsed.data.bedrock?.models).toHaveLength(2);
|
|
811
|
+
expect(parsed.data.bedrock?.ready).toBe(true);
|
|
812
|
+
expect(parsed.data.bedrock?.region).toBe("us-east-1");
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("bedrock block absent → defaults to null (backward compat)", () => {
|
|
817
|
+
const raw = {
|
|
818
|
+
ready: true,
|
|
819
|
+
missing: [],
|
|
820
|
+
satisfiedBy: "env",
|
|
821
|
+
hint: null,
|
|
822
|
+
liveTest: null,
|
|
823
|
+
latestModel: null,
|
|
824
|
+
reportedAt: Date.now(),
|
|
825
|
+
reportKind: "boot",
|
|
826
|
+
// No bedrock field
|
|
827
|
+
};
|
|
828
|
+
const parsed = AgentCredStatusSchema.safeParse(raw);
|
|
829
|
+
expect(parsed.success).toBe(true);
|
|
830
|
+
if (parsed.success) {
|
|
831
|
+
expect(parsed.data.bedrock).toBeNull();
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("bedrock block with error field parses correctly", () => {
|
|
836
|
+
const raw = {
|
|
837
|
+
ready: false,
|
|
838
|
+
missing: [],
|
|
839
|
+
satisfiedBy: null,
|
|
840
|
+
hint: "ExpiredToken",
|
|
841
|
+
liveTest: null,
|
|
842
|
+
latestModel: null,
|
|
843
|
+
reportedAt: Date.now(),
|
|
844
|
+
reportKind: "boot",
|
|
845
|
+
bedrock: {
|
|
846
|
+
region: "us-east-1",
|
|
847
|
+
probedAt: Date.now(),
|
|
848
|
+
ready: false,
|
|
849
|
+
models: [],
|
|
850
|
+
error: "Token expired — run aws sso login",
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
const parsed = AgentCredStatusSchema.safeParse(raw);
|
|
854
|
+
expect(parsed.success).toBe(true);
|
|
855
|
+
if (parsed.success) {
|
|
856
|
+
expect(parsed.data.bedrock?.ready).toBe(false);
|
|
857
|
+
expect(parsed.data.bedrock?.error).toBeDefined();
|
|
858
|
+
expect(parsed.data.bedrock?.models).toEqual([]);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// ─── usable set = harness-drivable ∩ (ON_DEMAND/ACTIVE FMs ∪ inference profiles) ─
|
|
864
|
+
// The real intersection lives in `runBedrockSdkProbeAndEnumerate`, which the
|
|
865
|
+
// injectable `bedrockProbe` stub bypasses. To exercise the union + filtering +
|
|
866
|
+
// intersection without real AWS credentials, stub `@aws-sdk/client-bedrock` and
|
|
867
|
+
// feed it canned list responses built from REAL pi-ai catalog ids.
|
|
868
|
+
|
|
869
|
+
describe("runBedrockSdkProbeAndEnumerate — intersection logic", () => {
|
|
870
|
+
test("includes inference-profile ids; drops non-ACTIVE and non-drivable ids", async () => {
|
|
871
|
+
const { getModels } = await import("@earendil-works/pi-ai");
|
|
872
|
+
const drivable = getModels("amazon-bedrock");
|
|
873
|
+
const isProfile = (id: string) => /^(us|eu|apac|au|global)\./.test(id);
|
|
874
|
+
const baseModel = drivable.find((m) => !isProfile(m.id));
|
|
875
|
+
const profileModel = drivable.find((m) => isProfile(m.id));
|
|
876
|
+
const legacyDrivable = drivable.find(
|
|
877
|
+
(m) => m.id !== baseModel?.id && m.id !== profileModel?.id,
|
|
878
|
+
);
|
|
879
|
+
if (!baseModel || !profileModel || !legacyDrivable) {
|
|
880
|
+
throw new Error("pi-ai amazon-bedrock catalog missing expected shapes");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Capture which command kinds the client was asked to send.
|
|
884
|
+
const sentKinds: string[] = [];
|
|
885
|
+
mock.module("@aws-sdk/client-bedrock", () => ({
|
|
886
|
+
BedrockClient: class {
|
|
887
|
+
async send(cmd: { __kind: string }) {
|
|
888
|
+
sentKinds.push(cmd.__kind);
|
|
889
|
+
if (cmd.__kind === "fm") {
|
|
890
|
+
return {
|
|
891
|
+
modelSummaries: [
|
|
892
|
+
// ON_DEMAND/TEXT (request-filtered) + ACTIVE → kept.
|
|
893
|
+
{ modelId: baseModel.id, modelLifecycle: { status: "ACTIVE" } },
|
|
894
|
+
// Drivable but NOT ACTIVE → dropped by lifecycle filter.
|
|
895
|
+
{ modelId: legacyDrivable.id, modelLifecycle: { status: "LEGACY" } },
|
|
896
|
+
// ACTIVE but NOT in the pi-ai catalog → dropped by intersection.
|
|
897
|
+
{ modelId: "amazon.not-a-real-pi-id-v9:0", modelLifecycle: { status: "ACTIVE" } },
|
|
898
|
+
],
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
inferenceProfileSummaries: [
|
|
903
|
+
// Cross-region profile id present in the pi-ai catalog → kept.
|
|
904
|
+
// This is exactly the class the old base-only intersection dropped.
|
|
905
|
+
{ inferenceProfileId: profileModel.id },
|
|
906
|
+
// Profile id NOT in the pi-ai catalog → dropped by intersection.
|
|
907
|
+
{ inferenceProfileId: "us.vendor.unknown-profile-v1:0" },
|
|
908
|
+
],
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
ListFoundationModelsCommand: class {
|
|
913
|
+
__kind = "fm";
|
|
914
|
+
constructor(public input: unknown) {}
|
|
915
|
+
},
|
|
916
|
+
ListInferenceProfilesCommand: class {
|
|
917
|
+
__kind = "ip";
|
|
918
|
+
constructor(public input: unknown) {}
|
|
919
|
+
},
|
|
920
|
+
}));
|
|
921
|
+
|
|
922
|
+
const { runBedrockSdkProbeAndEnumerate } = await import("../providers/pi-mono-adapter");
|
|
923
|
+
const usable = await runBedrockSdkProbeAndEnumerate("us-east-1");
|
|
924
|
+
const ids = usable.map((m) => m.id);
|
|
925
|
+
|
|
926
|
+
// Both list calls were made (single bounded round-trip each).
|
|
927
|
+
expect(sentKinds).toContain("fm");
|
|
928
|
+
expect(sentKinds).toContain("ip");
|
|
929
|
+
// Base ACTIVE model kept.
|
|
930
|
+
expect(ids).toContain(baseModel.id);
|
|
931
|
+
// Inference-profile model kept — the regression this fix targets.
|
|
932
|
+
expect(ids).toContain(profileModel.id);
|
|
933
|
+
// Non-ACTIVE drivable dropped; non-catalog ids dropped.
|
|
934
|
+
expect(ids).not.toContain(legacyDrivable.id);
|
|
935
|
+
expect(ids).not.toContain("amazon.not-a-real-pi-id-v9:0");
|
|
936
|
+
expect(ids).not.toContain("us.vendor.unknown-profile-v1:0");
|
|
937
|
+
// Stored ids are pi-ai ids carrying the catalog name.
|
|
938
|
+
expect(usable.find((m) => m.id === profileModel.id)?.name).toBe(profileModel.name);
|
|
939
|
+
|
|
940
|
+
mock.restore();
|
|
941
|
+
});
|
|
454
942
|
});
|