@desplega.ai/agent-swarm 1.95.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 +1 -1
- package/src/be/boot-scrub-logs.ts +76 -0
- package/src/be/db.ts +11 -8
- package/src/be/modelsdev-cache.json +89422 -85636
- package/src/commands/provider-credentials.ts +37 -15
- package/src/commands/runner.ts +27 -0
- package/src/http/agents.ts +1 -0
- package/src/http/config.ts +24 -4
- package/src/http/index.ts +9 -0
- package/src/prompts/session-templates.ts +21 -0
- package/src/providers/pi-mono-adapter.ts +113 -19
- package/src/providers/types.ts +30 -9
- package/src/tests/bedrock-model-groups.test.ts +135 -0
- package/src/tests/credential-check.test.ts +361 -12
- package/src/tests/secret-scrubber.test.ts +73 -1
- package/src/tools/swarm-config/get-config.ts +9 -1
- package/src/tools/swarm-config/list-config.ts +8 -0
- package/src/types.ts +21 -0
- package/src/utils/secret-scrubber.ts +33 -12
|
@@ -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 } {
|
|
@@ -138,7 +139,8 @@ describe("checkCodexCredentials", () => {
|
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Stub probes for Bedrock tests. These replace the real @aws-sdk/client-bedrock
|
|
141
|
-
* ListFoundationModels
|
|
142
|
+
* `ListFoundationModels` + `ListInferenceProfiles` enumeration so unit tests
|
|
143
|
+
* never hit AWS.
|
|
142
144
|
*/
|
|
143
145
|
const bedrockProbeSuccess = async () => {};
|
|
144
146
|
const bedrockProbeAuthFail = async () => {
|
|
@@ -277,7 +279,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
277
279
|
// "amazon-bedrock/", the probe runs. Tests inject a stub to avoid hitting AWS.
|
|
278
280
|
|
|
279
281
|
test("amazon-bedrock: probe success → ready (sdk-delegated)", async () => {
|
|
280
|
-
const env = {
|
|
282
|
+
const env = {
|
|
283
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
284
|
+
AWS_REGION: "us-east-1",
|
|
285
|
+
};
|
|
281
286
|
const status = await checkPiMonoCredentials(env, {
|
|
282
287
|
homeDir: HOME,
|
|
283
288
|
fs: noFiles,
|
|
@@ -293,6 +298,7 @@ describe("checkPiMonoCredentials", () => {
|
|
|
293
298
|
const env = {
|
|
294
299
|
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
295
300
|
ANTHROPIC_API_KEY: "x",
|
|
301
|
+
AWS_REGION: "us-east-1",
|
|
296
302
|
};
|
|
297
303
|
const status = await checkPiMonoCredentials(env, {
|
|
298
304
|
homeDir: HOME,
|
|
@@ -305,7 +311,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
305
311
|
|
|
306
312
|
test("amazon-bedrock: probe success even when auth.json exists (Bedrock wins over file)", async () => {
|
|
307
313
|
// auth.json holds Anthropic/OpenRouter/OpenAI creds — none used by Bedrock.
|
|
308
|
-
const env = {
|
|
314
|
+
const env = {
|
|
315
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
316
|
+
AWS_REGION: "us-east-1",
|
|
317
|
+
};
|
|
309
318
|
const status = await checkPiMonoCredentials(env, {
|
|
310
319
|
homeDir: HOME,
|
|
311
320
|
fs: fsWith(new Set([AUTH])),
|
|
@@ -316,7 +325,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
316
325
|
});
|
|
317
326
|
|
|
318
327
|
test("amazon-bedrock: provider-prefix match is case-insensitive", async () => {
|
|
319
|
-
const env = {
|
|
328
|
+
const env = {
|
|
329
|
+
MODEL_OVERRIDE: "Amazon-Bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
330
|
+
AWS_REGION: "us-east-1",
|
|
331
|
+
};
|
|
320
332
|
const status = await checkPiMonoCredentials(env, {
|
|
321
333
|
homeDir: HOME,
|
|
322
334
|
fs: noFiles,
|
|
@@ -327,7 +339,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
327
339
|
});
|
|
328
340
|
|
|
329
341
|
test("amazon-bedrock: probe auth failure → ready:false with aws-auth hint", async () => {
|
|
330
|
-
const env = {
|
|
342
|
+
const env = {
|
|
343
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
344
|
+
AWS_REGION: "us-east-1",
|
|
345
|
+
};
|
|
331
346
|
const status = await checkPiMonoCredentials(env, {
|
|
332
347
|
homeDir: HOME,
|
|
333
348
|
fs: noFiles,
|
|
@@ -338,7 +353,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
338
353
|
});
|
|
339
354
|
|
|
340
355
|
test("amazon-bedrock: probe access failure → ready:false with aws-access hint", async () => {
|
|
341
|
-
const env = {
|
|
356
|
+
const env = {
|
|
357
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
358
|
+
AWS_REGION: "us-east-1",
|
|
359
|
+
};
|
|
342
360
|
const status = await checkPiMonoCredentials(env, {
|
|
343
361
|
homeDir: HOME,
|
|
344
362
|
fs: noFiles,
|
|
@@ -349,7 +367,10 @@ describe("checkPiMonoCredentials", () => {
|
|
|
349
367
|
});
|
|
350
368
|
|
|
351
369
|
test("amazon-bedrock: probe region failure → ready:false (unclassified hint)", async () => {
|
|
352
|
-
const env = {
|
|
370
|
+
const env = {
|
|
371
|
+
MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
372
|
+
AWS_REGION: "us-east-1",
|
|
373
|
+
};
|
|
353
374
|
const status = await checkPiMonoCredentials(env, {
|
|
354
375
|
homeDir: HOME,
|
|
355
376
|
fs: noFiles,
|
|
@@ -365,7 +386,11 @@ describe("checkPiMonoCredentials", () => {
|
|
|
365
386
|
test("BEDROCK_AUTH_MODE=sdk: probe triggered even without amazon-bedrock/ prefix", async () => {
|
|
366
387
|
// Explicit mode — MODEL_OVERRIDE can be anything (or absent); the Bedrock
|
|
367
388
|
// path is taken because the operator explicitly declared BEDROCK_AUTH_MODE=sdk.
|
|
368
|
-
const env = {
|
|
389
|
+
const env = {
|
|
390
|
+
BEDROCK_AUTH_MODE: "sdk",
|
|
391
|
+
MODEL_OVERRIDE: "some-other-model",
|
|
392
|
+
AWS_REGION: "us-east-1",
|
|
393
|
+
};
|
|
369
394
|
const status = await checkPiMonoCredentials(env, {
|
|
370
395
|
homeDir: HOME,
|
|
371
396
|
fs: noFiles,
|
|
@@ -376,7 +401,7 @@ describe("checkPiMonoCredentials", () => {
|
|
|
376
401
|
});
|
|
377
402
|
|
|
378
403
|
test("BEDROCK_AUTH_MODE=sdk: probe failure → ready:false", async () => {
|
|
379
|
-
const env = { BEDROCK_AUTH_MODE: "sdk" };
|
|
404
|
+
const env = { BEDROCK_AUTH_MODE: "sdk", AWS_REGION: "us-east-1" };
|
|
380
405
|
const status = await checkPiMonoCredentials(env, {
|
|
381
406
|
homeDir: HOME,
|
|
382
407
|
fs: noFiles,
|
|
@@ -387,13 +412,13 @@ describe("checkPiMonoCredentials", () => {
|
|
|
387
412
|
|
|
388
413
|
test("BEDROCK_AUTH_MODE=bearer: does NOT trigger the sdk probe (falls through)", async () => {
|
|
389
414
|
// The bearer path is declared/validated but the full implementation is
|
|
390
|
-
//
|
|
415
|
+
// not implemented yet. With no other credentials set it should be not-ready
|
|
391
416
|
// via the standard permissive check, not via the sdk probe.
|
|
392
417
|
const env = { BEDROCK_AUTH_MODE: "bearer" };
|
|
393
418
|
// No other keys set, no auth.json → not-ready from the permissive path.
|
|
394
419
|
const status = await checkPiMonoCredentials(env, { homeDir: HOME, fs: noFiles });
|
|
395
420
|
expect(status.ready).toBe(false);
|
|
396
|
-
// Satisfying via any standard key still works for the bearer mode
|
|
421
|
+
// Satisfying via any standard key still works for the bearer mode today.
|
|
397
422
|
const withKey = await checkPiMonoCredentials(
|
|
398
423
|
{ BEDROCK_AUTH_MODE: "bearer", ANTHROPIC_API_KEY: "x" },
|
|
399
424
|
{ homeDir: HOME, fs: noFiles },
|
|
@@ -410,6 +435,110 @@ describe("checkPiMonoCredentials", () => {
|
|
|
410
435
|
expect(status.ready).toBe(true);
|
|
411
436
|
expect(status.satisfiedBy).toBe("env");
|
|
412
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
|
+
});
|
|
413
542
|
});
|
|
414
543
|
|
|
415
544
|
// ─── opencode ────────────────────────────────────────────────────────────────
|
|
@@ -590,4 +719,224 @@ describe("buildCredStatusReport", () => {
|
|
|
590
719
|
const snap = await buildCredStatusReport("claude", {}, {}, "post_task");
|
|
591
720
|
expect(snap.reportKind).toBe("post_task");
|
|
592
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
|
+
});
|
|
593
942
|
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
clearVolatileSecretsForTesting,
|
|
4
|
+
refreshSecretScrubberCache,
|
|
5
|
+
registerVolatileSecret,
|
|
6
|
+
scrubObject,
|
|
7
|
+
scrubSecrets,
|
|
8
|
+
} from "../utils/secret-scrubber";
|
|
3
9
|
|
|
4
10
|
// Snapshot/restore process.env between tests so env-derived cache entries
|
|
5
11
|
// don't leak across cases.
|
|
@@ -221,6 +227,52 @@ describe("scrubSecrets — regex patterns", () => {
|
|
|
221
227
|
|
|
222
228
|
expect(out).toBe("OTEL_EXPORTER_OTLP_HEADERS=[REDACTED:signoz_ingestion_key]");
|
|
223
229
|
});
|
|
230
|
+
|
|
231
|
+
test("redacts Linear OAuth tokens", () => {
|
|
232
|
+
const out = scrubSecrets("Authorization: Bearer lin_oauth_test123abcdef end");
|
|
233
|
+
expect(out).toContain("[REDACTED:linear_oauth]");
|
|
234
|
+
expect(out).not.toContain("lin_oauth_test123");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("redacts Linear API keys", () => {
|
|
238
|
+
const out = scrubSecrets("key=lin_api_test123abcdef tail");
|
|
239
|
+
expect(out).toContain("[REDACTED:linear_api]");
|
|
240
|
+
expect(out).not.toContain("lin_api_test123");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("redacts npm tokens", () => {
|
|
244
|
+
const out = scrubSecrets("npm=npm_abcdefghijklmnopqrstuvwxyz01234");
|
|
245
|
+
expect(out).toContain("[REDACTED:npm_token]");
|
|
246
|
+
expect(out).not.toContain("npm_abcdefghijklmnopqr");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("redacts Atlassian/Jira API tokens", () => {
|
|
250
|
+
const out = scrubSecrets("jira=ATATT3xFfGF0abcdefghijklmnopqrstuvwxyz");
|
|
251
|
+
expect(out).toContain("[REDACTED:atlassian_token]");
|
|
252
|
+
expect(out).not.toContain("ATATT3xFfGF0");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("redacts tokens adjacent to JSON escape sequences (\\n)", () => {
|
|
256
|
+
const content = "data\\nlin_oauth_test123abcdef\\nmore";
|
|
257
|
+
const out = scrubSecrets(content);
|
|
258
|
+
expect(out).toContain("[REDACTED:linear_oauth]");
|
|
259
|
+
expect(out).not.toContain("lin_oauth_test123");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("redacts tokens adjacent to JSON escape sequences (\\t, \\r)", () => {
|
|
263
|
+
const outT = scrubSecrets("field\\tlin_oauth_test123abcdef");
|
|
264
|
+
expect(outT).toContain("[REDACTED:linear_oauth]");
|
|
265
|
+
const outR = scrubSecrets("line\\rlin_oauth_test123abcdef");
|
|
266
|
+
expect(outR).toContain("[REDACTED:linear_oauth]");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("redacts tokens in double-encoded JSON with escape sequences", () => {
|
|
270
|
+
const inner = JSON.stringify({ access_token: "lin_oauth_test123abcdef", scope: "read" });
|
|
271
|
+
const content = `{"output":${JSON.stringify(inner)}}`;
|
|
272
|
+
const out = scrubSecrets(content);
|
|
273
|
+
expect(out).toContain("[REDACTED:linear_oauth]");
|
|
274
|
+
expect(out).not.toContain("lin_oauth_test123");
|
|
275
|
+
});
|
|
224
276
|
});
|
|
225
277
|
|
|
226
278
|
describe("scrubSecrets — does not over-scrub", () => {
|
|
@@ -300,3 +352,23 @@ describe("scrubObject", () => {
|
|
|
300
352
|
expect(scrubObject(value)).toEqual({ a: "ok", self: "[Circular]" });
|
|
301
353
|
});
|
|
302
354
|
});
|
|
355
|
+
|
|
356
|
+
describe("registerVolatileSecret", () => {
|
|
357
|
+
afterEach(() => {
|
|
358
|
+
clearVolatileSecretsForTesting();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("scrubs a runtime-registered volatile secret", () => {
|
|
362
|
+
const secret = "volatile_runtime_token_1234567890abcdef";
|
|
363
|
+
registerVolatileSecret(secret, "RUNTIME_TOKEN");
|
|
364
|
+
const out = scrubSecrets(`key=${secret}`);
|
|
365
|
+
expect(out).toBe("key=[REDACTED:RUNTIME_TOKEN]");
|
|
366
|
+
expect(out).not.toContain(secret);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("ignores values shorter than the minimum length", () => {
|
|
370
|
+
registerVolatileSecret("short", "TOO_SHORT");
|
|
371
|
+
const out = scrubSecrets("contains short somewhere");
|
|
372
|
+
expect(out).toBe("contains short somewhere");
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -3,6 +3,7 @@ import * as z from "zod";
|
|
|
3
3
|
import { getResolvedConfig, maskSecrets } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
5
|
import { SwarmConfigSchema } from "@/types";
|
|
6
|
+
import { registerVolatileSecret } from "@/utils/secret-scrubber";
|
|
6
7
|
|
|
7
8
|
export const registerGetConfigTool = (server: McpServer) => {
|
|
8
9
|
createToolRegistrar(server)(
|
|
@@ -10,7 +11,7 @@ export const registerGetConfigTool = (server: McpServer) => {
|
|
|
10
11
|
{
|
|
11
12
|
title: "Get Config",
|
|
12
13
|
description:
|
|
13
|
-
"Get resolved configuration values with scope resolution (repo > agent > global). Returns one entry per unique key with the most-specific scope winning. Use includeSecrets=true to see secret values.",
|
|
14
|
+
"Get resolved configuration values with scope resolution (repo > agent > global). Returns one entry per unique key with the most-specific scope winning. Use includeSecrets=true to see secret values. IMPORTANT: never pass returned secret values directly on a command line — write them to a temp .env file and source it instead, so the literal value stays out of logged commands.",
|
|
14
15
|
annotations: { readOnlyHint: true },
|
|
15
16
|
|
|
16
17
|
inputSchema: z.object({
|
|
@@ -62,6 +63,13 @@ export const registerGetConfigTool = (server: McpServer) => {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const result = includeSecrets ? configs : maskSecrets(configs);
|
|
66
|
+
if (includeSecrets) {
|
|
67
|
+
for (const c of result) {
|
|
68
|
+
if (c.isSecret && c.value) {
|
|
69
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
const count = result.length;
|
|
66
74
|
|
|
67
75
|
const configList =
|
|
@@ -3,6 +3,7 @@ import * as z from "zod";
|
|
|
3
3
|
import { getSwarmConfigs, maskSecrets } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
5
|
import { SwarmConfigSchema, SwarmConfigScopeSchema } from "@/types";
|
|
6
|
+
import { registerVolatileSecret } from "@/utils/secret-scrubber";
|
|
6
7
|
|
|
7
8
|
export const registerListConfigTool = (server: McpServer) => {
|
|
8
9
|
createToolRegistrar(server)(
|
|
@@ -53,6 +54,13 @@ export const registerListConfigTool = (server: McpServer) => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
const result = includeSecrets ? configs : maskSecrets(configs);
|
|
57
|
+
if (includeSecrets) {
|
|
58
|
+
for (const c of result) {
|
|
59
|
+
if (c.isSecret && c.value) {
|
|
60
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
56
64
|
const count = result.length;
|
|
57
65
|
|
|
58
66
|
const configList =
|
package/src/types.ts
CHANGED
|
@@ -553,6 +553,25 @@ export const AgentLatestModelSchema = z.object({
|
|
|
553
553
|
});
|
|
554
554
|
export type AgentLatestModel = z.infer<typeof AgentLatestModelSchema>;
|
|
555
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Worker-reported Bedrock enumeration block. Only present when the pi harness
|
|
558
|
+
* is in Bedrock SDK mode (`BEDROCK_AUTH_MODE=sdk` or
|
|
559
|
+
* `MODEL_OVERRIDE=amazon-bedrock/*`). Rides inside `cred_status` JSON (no new
|
|
560
|
+
* DB column). `models` is the intersection of the models invocable by this
|
|
561
|
+
* account/region (on-demand/ACTIVE foundation models ∪ inference profiles) with
|
|
562
|
+
* the set the pi-ai Converse harness can actually drive — Converse-incompatible
|
|
563
|
+
* entries (e.g. OpenAI models listed in the account) are excluded. An empty
|
|
564
|
+
* `region` means Bedrock mode with `AWS_REGION` unset (no region fabricated).
|
|
565
|
+
*/
|
|
566
|
+
export const AgentBedrockStatusSchema = z.object({
|
|
567
|
+
region: z.string(),
|
|
568
|
+
probedAt: z.number(), // unix ms
|
|
569
|
+
ready: z.boolean(),
|
|
570
|
+
models: z.array(z.object({ id: z.string(), name: z.string() })).default([]),
|
|
571
|
+
error: z.string().optional(),
|
|
572
|
+
});
|
|
573
|
+
export type AgentBedrockStatus = z.infer<typeof AgentBedrockStatusSchema>;
|
|
574
|
+
|
|
556
575
|
export const AgentCredStatusSchema = z.object({
|
|
557
576
|
ready: z.boolean(),
|
|
558
577
|
missing: z.array(z.string()).default([]),
|
|
@@ -565,6 +584,8 @@ export const AgentCredStatusSchema = z.object({
|
|
|
565
584
|
latestModel: AgentLatestModelSchema.nullable().default(null),
|
|
566
585
|
reportedAt: z.number(), // unix ms
|
|
567
586
|
reportKind: z.enum(["boot", "post_task"]).default("boot"),
|
|
587
|
+
/** Pi-mono Bedrock enumeration block — null when not in Bedrock mode. */
|
|
588
|
+
bedrock: AgentBedrockStatusSchema.nullable().default(null),
|
|
568
589
|
});
|
|
569
590
|
export type AgentCredStatus = z.infer<typeof AgentCredStatusSchema>;
|
|
570
591
|
|