@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
|
@@ -29,6 +29,21 @@ import { scrubSecrets } from "../utils/secret-scrubber";
|
|
|
29
29
|
|
|
30
30
|
export type SupportedProvider = "claude" | "claude-managed" | "codex" | "devin" | "opencode" | "pi";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when the pi harness should use the AWS SDK Bedrock path: either an
|
|
34
|
+
* explicit `BEDROCK_AUTH_MODE=sdk`, or — preserving prefix-inference semantics —
|
|
35
|
+
* `BEDROCK_AUTH_MODE` absent with a `MODEL_OVERRIDE=amazon-bedrock/*` selection.
|
|
36
|
+
* Single source of truth for the gate so the live-test arm and the worker
|
|
37
|
+
* reconcile loop agree with `checkPiMonoCredentials`.
|
|
38
|
+
*/
|
|
39
|
+
export function isBedrockSdkMode(env: Record<string, string | undefined>): boolean {
|
|
40
|
+
const mode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
41
|
+
return (
|
|
42
|
+
mode === "sdk" ||
|
|
43
|
+
(mode === undefined && Boolean(env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Static documentation of which env vars each provider considers when running
|
|
34
49
|
* `checkCredentials`. Used by the dashboard to render hints before any worker
|
|
@@ -243,7 +258,7 @@ function parseCodexOAuthAccess(blob: string | undefined): string | null {
|
|
|
243
258
|
* | `codex` | `~/.codex/auth.json` (file) → `CODEX_OAUTH` (env OAuth) → `OPENAI_API_KEY` | OpenAI `/v1/models` (api-key path only) |
|
|
244
259
|
* | `opencode` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` (pi-style) | matching provider's `/v1/models` |
|
|
245
260
|
* | `pi` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` | matching provider's `/v1/models` |
|
|
246
|
-
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (
|
|
261
|
+
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (real check is the worker-side Bedrock enumeration) |
|
|
247
262
|
* | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v1/sessions?limit=1` |
|
|
248
263
|
*
|
|
249
264
|
* Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
|
|
@@ -302,20 +317,14 @@ export async function validateProviderCredentials(provider: string): Promise<Liv
|
|
|
302
317
|
}
|
|
303
318
|
case "pi":
|
|
304
319
|
case "opencode": {
|
|
305
|
-
// For the pi Bedrock path, the real credential check is the
|
|
306
|
-
// `ListFoundationModels`
|
|
307
|
-
// `pi` dynamic-import arm) already ran.
|
|
308
|
-
// in `buildCredStatusReport` — the live-test is a
|
|
309
|
-
// so we never issue a second AWS SDK call here
|
|
310
|
-
// SDK into the wrong binary or make slow IMDS
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
provider === "pi" &&
|
|
315
|
-
(env.BEDROCK_AUTH_MODE?.toLowerCase() === "sdk" ||
|
|
316
|
-
(env.BEDROCK_AUTH_MODE === undefined &&
|
|
317
|
-
env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")))
|
|
318
|
-
) {
|
|
320
|
+
// For the pi Bedrock path, the real credential check is the AWS SDK
|
|
321
|
+
// enumeration (`ListFoundationModels` + `ListInferenceProfiles`) that
|
|
322
|
+
// `checkProviderCredentials` (the `pi` dynamic-import arm) already ran.
|
|
323
|
+
// That result is already in `buildCredStatusReport` — the live-test is a
|
|
324
|
+
// pass-through / no-op so we never issue a second AWS SDK call here
|
|
325
|
+
// (which would drag the SDK into the wrong binary or make slow IMDS
|
|
326
|
+
// calls on non-EC2 hosts).
|
|
327
|
+
if (provider === "pi" && isBedrockSdkMode(env)) {
|
|
319
328
|
return presenceCheckOk();
|
|
320
329
|
}
|
|
321
330
|
// Both pi-mono and opencode resolve credentials in the same order:
|
|
@@ -408,6 +417,18 @@ export async function buildCredStatusReport(
|
|
|
408
417
|
testedAt: Date.now(),
|
|
409
418
|
};
|
|
410
419
|
}
|
|
420
|
+
// Include the Bedrock enumeration block when the pi probe ran in Bedrock SDK
|
|
421
|
+
// mode (bedrockRegion is only set by checkPiMonoCredentials in that branch).
|
|
422
|
+
const bedrock: AgentCredStatus["bedrock"] =
|
|
423
|
+
presence.bedrockRegion !== undefined
|
|
424
|
+
? {
|
|
425
|
+
region: presence.bedrockRegion,
|
|
426
|
+
probedAt: Date.now(),
|
|
427
|
+
ready: presence.ready,
|
|
428
|
+
models: presence.bedrockModels ?? [],
|
|
429
|
+
error: presence.ready ? undefined : (presence.hint ?? undefined),
|
|
430
|
+
}
|
|
431
|
+
: null;
|
|
411
432
|
return {
|
|
412
433
|
ready: presence.ready,
|
|
413
434
|
missing: presence.missing ?? [],
|
|
@@ -417,6 +438,7 @@ export async function buildCredStatusReport(
|
|
|
417
438
|
latestModel: null,
|
|
418
439
|
reportedAt: Date.now(),
|
|
419
440
|
reportKind: kind,
|
|
441
|
+
bedrock,
|
|
420
442
|
};
|
|
421
443
|
}
|
|
422
444
|
|
package/src/commands/runner.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { resolveClaudeMdPath, syncProfileFilesToServer } from "./profile-sync.ts
|
|
|
60
60
|
import {
|
|
61
61
|
buildCredStatusReport,
|
|
62
62
|
buildLatestModelReport,
|
|
63
|
+
isBedrockSdkMode,
|
|
63
64
|
isCredCheckDisabled,
|
|
64
65
|
reportCredStatus,
|
|
65
66
|
reportLatestModel,
|
|
@@ -3848,6 +3849,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3848
3849
|
let lastHarnessReconcileAt = 0;
|
|
3849
3850
|
const HARNESS_RECONCILE_INTERVAL_MS = 10_000;
|
|
3850
3851
|
|
|
3852
|
+
// Throttle for the periodic Bedrock model-enumeration refresh. The credential
|
|
3853
|
+
// report below only re-runs on a harness_provider change (boot + provider
|
|
3854
|
+
// swap), so enabling Bedrock access after boot would otherwise never reach the
|
|
3855
|
+
// picker. This timer re-runs the enumeration on a fixed interval, decoupled
|
|
3856
|
+
// from the harness-change gate, so the UI stays accurate. 5 minutes keeps it
|
|
3857
|
+
// cheap (one bounded AWS enumeration per tick) while still surfacing newly
|
|
3858
|
+
// granted access within a few minutes.
|
|
3859
|
+
let lastBedrockRefreshAt = 0;
|
|
3860
|
+
const BEDROCK_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
3861
|
+
|
|
3851
3862
|
// Create API config for ping/close
|
|
3852
3863
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
3853
3864
|
|
|
@@ -4572,6 +4583,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4572
4583
|
.catch((err) =>
|
|
4573
4584
|
console.warn(`[${role}] cred_status post_task report failed (non-fatal): ${err}`),
|
|
4574
4585
|
);
|
|
4586
|
+
} else if (
|
|
4587
|
+
currentHarness === "pi" &&
|
|
4588
|
+
isBedrockSdkMode(process.env) &&
|
|
4589
|
+
Date.now() - lastBedrockRefreshAt > BEDROCK_REFRESH_INTERVAL_MS
|
|
4590
|
+
) {
|
|
4591
|
+
// Bedrock enumeration drifts independently of the harness_provider:
|
|
4592
|
+
// access granted (or revoked) in the AWS console after boot won't flip
|
|
4593
|
+
// the provider, so the harness-change gate above never fires. Re-run the
|
|
4594
|
+
// enumeration on the throttled interval so the picker reflects the live
|
|
4595
|
+
// account state. One bounded AWS round-trip per tick.
|
|
4596
|
+
lastBedrockRefreshAt = Date.now();
|
|
4597
|
+
buildCredStatusReport(currentHarness, process.env, {}, "post_task")
|
|
4598
|
+
.then((snap) => reportCredStatus(apiUrl, apiKey, agentId, snap))
|
|
4599
|
+
.catch((err) =>
|
|
4600
|
+
console.warn(`[${role}] bedrock enumeration refresh failed (non-fatal): ${err}`),
|
|
4601
|
+
);
|
|
4575
4602
|
}
|
|
4576
4603
|
}
|
|
4577
4604
|
|
package/src/http/agents.ts
CHANGED
package/src/http/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
reservedKeyError,
|
|
15
15
|
validateConfigValue,
|
|
16
16
|
} from "../be/swarm-config-guard";
|
|
17
|
+
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
17
18
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
18
19
|
import { route } from "./route-def";
|
|
19
20
|
import { json, jsonError } from "./utils";
|
|
@@ -152,7 +153,15 @@ export async function handleConfig(
|
|
|
152
153
|
parsed.query.agentId || undefined,
|
|
153
154
|
parsed.query.repoId || undefined,
|
|
154
155
|
);
|
|
155
|
-
|
|
156
|
+
const result = includeSecrets ? configs : maskSecrets(configs);
|
|
157
|
+
if (includeSecrets) {
|
|
158
|
+
for (const c of result) {
|
|
159
|
+
if (c.isSecret && c.value) {
|
|
160
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
json(res, { configs: result });
|
|
156
165
|
return true;
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -199,8 +208,11 @@ export async function handleConfig(
|
|
|
199
208
|
jsonError(res, "Config not found", 404);
|
|
200
209
|
return true;
|
|
201
210
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
211
|
+
const singleResult = includeSecrets ? config : maskSecrets([config])[0]!;
|
|
212
|
+
if (includeSecrets && singleResult.isSecret && singleResult.value) {
|
|
213
|
+
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
214
|
+
}
|
|
215
|
+
json(res, singleResult);
|
|
204
216
|
return true;
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -212,7 +224,15 @@ export async function handleConfig(
|
|
|
212
224
|
scope: parsed.query.scope || undefined,
|
|
213
225
|
scopeId: parsed.query.scopeId || undefined,
|
|
214
226
|
});
|
|
215
|
-
|
|
227
|
+
const listResult = includeSecrets ? configs : maskSecrets(configs);
|
|
228
|
+
if (includeSecrets) {
|
|
229
|
+
for (const c of listResult) {
|
|
230
|
+
if (c.isSecret && c.value) {
|
|
231
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
json(res, { configs: listResult });
|
|
216
236
|
return true;
|
|
217
237
|
}
|
|
218
238
|
|
package/src/http/index.ts
CHANGED
|
@@ -576,6 +576,15 @@ httpServer
|
|
|
576
576
|
.catch((err) => {
|
|
577
577
|
console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
|
|
578
578
|
});
|
|
579
|
+
|
|
580
|
+
// One-time scrub: retroactively redact any session_logs rows containing
|
|
581
|
+
// sensitive patterns that pre-date the defense-in-depth scrub layer.
|
|
582
|
+
// Idempotent, tracked via seed_state.
|
|
583
|
+
import("../be/boot-scrub-logs")
|
|
584
|
+
.then(({ runBootScrubLogs }) => runBootScrubLogs())
|
|
585
|
+
.catch((err) => {
|
|
586
|
+
console.error("[boot-scrub-logs] startup scrub failed (non-fatal):", err);
|
|
587
|
+
});
|
|
579
588
|
})
|
|
580
589
|
.on("error", (err) => {
|
|
581
590
|
console.error("HTTP Server Error:", err);
|
|
@@ -158,6 +158,27 @@ When you finish a task:
|
|
|
158
158
|
- **Failure**: Use \`store-progress\` with status: "failed" and failureReason: "<what went wrong>"
|
|
159
159
|
|
|
160
160
|
Always include meaningful output - the lead agent reviews your work.
|
|
161
|
+
|
|
162
|
+
#### Credential Hygiene
|
|
163
|
+
|
|
164
|
+
When you retrieve secrets via \`get-config\` (with \`includeSecrets: true\`), **never pass secret values directly on a command line or embed them in tool output**. Command arguments are logged.
|
|
165
|
+
|
|
166
|
+
**Safe pattern:** Write the secret to a temporary \`.env\` file, then source it:
|
|
167
|
+
\`\`\`bash
|
|
168
|
+
# Write to temp file (not logged)
|
|
169
|
+
echo "MY_TOKEN=<value>" > /tmp/.task-env && source /tmp/.task-env
|
|
170
|
+
# Use the variable (value stays out of logs)
|
|
171
|
+
curl -H "Authorization: Bearer $MY_TOKEN" https://api.example.com
|
|
172
|
+
rm /tmp/.task-env
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
**Unsafe pattern (NEVER do this):**
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
# The literal secret appears in the logged command
|
|
178
|
+
curl -H "Authorization: Bearer lin_oauth_abc123..." https://api.example.com
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
The same applies to \`store-progress\` output — never include raw secret values in progress text, output, or failure reasons.
|
|
161
182
|
`,
|
|
162
183
|
variables: [],
|
|
163
184
|
category: "system",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, lstatSync, symlinkSync, unlinkSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { getModel } from "@earendil-works/pi-ai";
|
|
11
|
+
import { getModel, getModels } from "@earendil-works/pi-ai";
|
|
12
12
|
import type {
|
|
13
13
|
AgentSessionEvent,
|
|
14
14
|
CreateAgentSessionOptions,
|
|
@@ -75,29 +75,99 @@ function modelToCredKeys(modelStr: string | undefined): string[] | null {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
* API
|
|
80
|
-
*
|
|
81
|
-
*
|
|
78
|
+
* Return the pi-ai Bedrock models the harness can actually drive via the
|
|
79
|
+
* Converse API (the catalog from `getModels("amazon-bedrock")`). Each id is a
|
|
80
|
+
* valid pi-ai id — base foundation-model id OR inference-profile id (`us.` /
|
|
81
|
+
* `eu.` / `apac.` / `au.` / `global.` prefixes) — so the matched id round-trips
|
|
82
|
+
* through `MODEL_OVERRIDE=amazon-bedrock/<id>` unchanged. Used as the
|
|
83
|
+
* harness-drivable half of the (drivable ∩ invocable) intersection.
|
|
84
|
+
*/
|
|
85
|
+
function getHarnessDrivableBedrockModels(): Array<{ id: string; name: string }> {
|
|
86
|
+
try {
|
|
87
|
+
return getModels("amazon-bedrock").map((m) => ({ id: m.id, name: m.name }));
|
|
88
|
+
} catch {
|
|
89
|
+
// getModels may throw if the pi-ai catalog is empty or corrupted.
|
|
90
|
+
// Return an empty list — the intersection will be empty too, which is safe.
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enumerate the Bedrock models that are both invocable by this AWS account and
|
|
97
|
+
* drivable by the pi-ai Converse harness, and verify the credential chain in
|
|
98
|
+
* one pass:
|
|
99
|
+
* 1. VERIFY the active credential chain is valid for Bedrock in `region`
|
|
100
|
+
* (the AWS list calls throw on auth/access failure).
|
|
101
|
+
* 2. ENUMERATE usable models = harness-drivable ∩ AWS-invocable, where the
|
|
102
|
+
* AWS-invocable set is:
|
|
103
|
+
* - `ListFoundationModels` filtered to on-demand TEXT models that are
|
|
104
|
+
* `ACTIVE` (base foundation-model ids), UNION
|
|
105
|
+
* - `ListInferenceProfiles` ids (the `us.`/`eu.`/… cross-region profile
|
|
106
|
+
* ids). The newest Claude models on Bedrock are invocable ONLY via an
|
|
107
|
+
* inference profile and never appear in `ListFoundationModels`, so this
|
|
108
|
+
* union is what keeps the current models in the usable list.
|
|
109
|
+
*
|
|
110
|
+
* `ListFoundationModels` reports models that EXIST in the region, not strictly
|
|
111
|
+
* ones the account has enabled access to, so the on-demand/ACTIVE filtering
|
|
112
|
+
* narrows it; base on-demand access-grant is not fully enumerable from the
|
|
113
|
+
* catalog. The inference-profile union is what makes the *current* models
|
|
114
|
+
* accurate. The matched id is stored/displayed as the pi-ai id (the id the
|
|
115
|
+
* harness can drive); ids are matched exactly.
|
|
82
116
|
*
|
|
117
|
+
* Two list calls per refresh, no pagination loops or per-model lookups.
|
|
83
118
|
* Dynamically imported so the API binary never loads `@aws-sdk/client-bedrock`.
|
|
84
119
|
* Tests inject a stub via `CredCheckOptions.bedrockProbe` instead.
|
|
120
|
+
*
|
|
121
|
+
* Returns `Array<{id, name}>` on success; throws on auth/access failure.
|
|
85
122
|
*/
|
|
86
|
-
async function
|
|
87
|
-
|
|
123
|
+
export async function runBedrockSdkProbeAndEnumerate(
|
|
124
|
+
region: string,
|
|
125
|
+
): Promise<Array<{ id: string; name: string }>> {
|
|
126
|
+
const { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } = await import(
|
|
127
|
+
"@aws-sdk/client-bedrock"
|
|
128
|
+
);
|
|
88
129
|
const client = new BedrockClient({ region });
|
|
89
|
-
|
|
130
|
+
|
|
131
|
+
// AWS-invocable set, region-scoped to `region`.
|
|
132
|
+
const invocable = new Set<string>();
|
|
133
|
+
|
|
134
|
+
// Base on-demand TEXT foundation models that are ACTIVE.
|
|
135
|
+
const fmResponse = await client.send(
|
|
136
|
+
new ListFoundationModelsCommand({ byInferenceType: "ON_DEMAND", byOutputModality: "TEXT" }),
|
|
137
|
+
);
|
|
138
|
+
for (const m of fmResponse.modelSummaries ?? []) {
|
|
139
|
+
if (m.modelId && m.modelLifecycle?.status === "ACTIVE") {
|
|
140
|
+
invocable.add(m.modelId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Inference-profile / cross-region ids (`us.`/`eu.`/`apac.`/…). These are the
|
|
145
|
+
// only invocation path for the newest Claude models and are absent from
|
|
146
|
+
// `ListFoundationModels`.
|
|
147
|
+
const profileResponse = await client.send(new ListInferenceProfilesCommand({}));
|
|
148
|
+
for (const p of profileResponse.inferenceProfileSummaries ?? []) {
|
|
149
|
+
if (p.inferenceProfileId) {
|
|
150
|
+
invocable.add(p.inferenceProfileId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Usable = harness-drivable ∩ AWS-invocable, exact-id match. The stored id is
|
|
155
|
+
// the pi-ai id so it round-trips through `getModel("amazon-bedrock", id)`.
|
|
156
|
+
return getHarnessDrivableBedrockModels().filter((m) => invocable.has(m.id));
|
|
90
157
|
}
|
|
91
158
|
|
|
92
159
|
/**
|
|
93
160
|
* Pi-mono is satisfied by ANY of:
|
|
94
161
|
* 1. `BEDROCK_AUTH_MODE=sdk` — or `MODEL_OVERRIDE` selects the
|
|
95
162
|
* `amazon-bedrock` provider (prefix-inference fallback when
|
|
96
|
-
* `BEDROCK_AUTH_MODE` is absent).
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
100
|
-
*
|
|
163
|
+
* `BEDROCK_AUTH_MODE` is absent). The AWS SDK default credential chain is
|
|
164
|
+
* exercised by a real enumeration pass (`ListFoundationModels` +
|
|
165
|
+
* `ListInferenceProfiles`) that both verifies access and lists the usable
|
|
166
|
+
* models. Success → `ready:true, satisfiedBy:"sdk-delegated"` with the
|
|
167
|
+
* enumerated models; failure → `ready:false` with a classified hint;
|
|
168
|
+
* `AWS_REGION` unset → `ready:false` with a set-region hint. The
|
|
169
|
+
* enumeration is worker-only (the pi dynamic-import arm in
|
|
170
|
+
* `checkProviderCredentials`); the API binary never imports the SDK.
|
|
101
171
|
* 2. `~/.pi/agent/auth.json` exists.
|
|
102
172
|
* 3. `MODEL_OVERRIDE` is set to a non-Bedrock provider-prefixed model — only
|
|
103
173
|
* the matching provider's key is required.
|
|
@@ -118,7 +188,7 @@ export async function checkPiMonoCredentials(
|
|
|
118
188
|
// - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
|
|
119
189
|
// "amazon-bedrock/" (preserves today's prefix-inference semantics)
|
|
120
190
|
// BEDROCK_AUTH_MODE=bearer is declared/validated but the full bearer-token
|
|
121
|
-
// path is
|
|
191
|
+
// path is not implemented yet — it falls through to the standard auth check.
|
|
122
192
|
const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
123
193
|
const isBedrockSdk =
|
|
124
194
|
bedrockAuthMode === "sdk" ||
|
|
@@ -126,15 +196,37 @@ export async function checkPiMonoCredentials(
|
|
|
126
196
|
env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
|
|
127
197
|
|
|
128
198
|
if (isBedrockSdk) {
|
|
129
|
-
const region = env.AWS_REGION
|
|
130
|
-
|
|
199
|
+
const region = env.AWS_REGION;
|
|
200
|
+
if (!region) {
|
|
201
|
+
// Do NOT fabricate a region. A guessed `us-east-1` can differ from where
|
|
202
|
+
// inference actually runs, which would enumerate the wrong region's
|
|
203
|
+
// models. Report a not-ready Bedrock state with a hint instead, so the
|
|
204
|
+
// enumeration region always matches the inference region. `bedrockRegion`
|
|
205
|
+
// is an empty string (not undefined) so the report still carries a
|
|
206
|
+
// Bedrock block and the picker can surface the reason.
|
|
207
|
+
return {
|
|
208
|
+
ready: false,
|
|
209
|
+
missing: [],
|
|
210
|
+
hint: "AWS_REGION is not set — set it to the region where your Bedrock models are accessible so model enumeration matches the inference region.",
|
|
211
|
+
bedrockModels: [],
|
|
212
|
+
bedrockRegion: "",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const probe = opts.bedrockProbe ?? (() => runBedrockSdkProbeAndEnumerate(region));
|
|
131
216
|
try {
|
|
132
|
-
await probe();
|
|
217
|
+
const probeResult = await probe();
|
|
218
|
+
// `probeResult` is `Array<{id,name}> | void` — void comes from auth-only
|
|
219
|
+
// stubs that don't exercise enumeration. Treat void as [].
|
|
220
|
+
const bedrockModels: Array<{ id: string; name: string }> = Array.isArray(probeResult)
|
|
221
|
+
? probeResult
|
|
222
|
+
: [];
|
|
133
223
|
return {
|
|
134
224
|
ready: true,
|
|
135
225
|
missing: [],
|
|
136
226
|
satisfiedBy: "sdk-delegated",
|
|
137
|
-
hint: `
|
|
227
|
+
hint: `Bedrock models invocable in ${region} enumerated (${bedrockModels.length} usable; ListFoundationModels + ListInferenceProfiles).`,
|
|
228
|
+
bedrockModels,
|
|
229
|
+
bedrockRegion: region,
|
|
138
230
|
};
|
|
139
231
|
} catch (err) {
|
|
140
232
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -144,7 +236,9 @@ export async function checkPiMonoCredentials(
|
|
|
144
236
|
missing: [],
|
|
145
237
|
hint:
|
|
146
238
|
classification?.message ??
|
|
147
|
-
`AWS Bedrock
|
|
239
|
+
`AWS Bedrock enumeration failed (region: ${region}): ${errorMessage}`,
|
|
240
|
+
bedrockModels: [],
|
|
241
|
+
bedrockRegion: region,
|
|
148
242
|
};
|
|
149
243
|
}
|
|
150
244
|
}
|
package/src/providers/types.ts
CHANGED
|
@@ -181,6 +181,19 @@ export interface CredStatus {
|
|
|
181
181
|
missing: string[];
|
|
182
182
|
satisfiedBy?: "env" | "file" | "side-effect-pending" | "sdk-delegated";
|
|
183
183
|
hint?: string;
|
|
184
|
+
/**
|
|
185
|
+
* Pi-mono Bedrock mode only: usable model list = harness-drivable ∩
|
|
186
|
+
* AWS-invocable (on-demand/ACTIVE foundation models ∪ inference profiles),
|
|
187
|
+
* region-scoped. Empty when enumeration failed (ready===false), when
|
|
188
|
+
* `AWS_REGION` is unset, or when the intersection is empty. Undefined when not
|
|
189
|
+
* in Bedrock mode.
|
|
190
|
+
*/
|
|
191
|
+
bedrockModels?: Array<{ id: string; name: string }>;
|
|
192
|
+
/**
|
|
193
|
+
* Pi-mono Bedrock mode only: AWS region the enumeration ran against. An empty
|
|
194
|
+
* string signals Bedrock mode with `AWS_REGION` unset (no region fabricated).
|
|
195
|
+
*/
|
|
196
|
+
bedrockRegion?: string;
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
/**
|
|
@@ -189,19 +202,27 @@ export interface CredStatus {
|
|
|
189
202
|
* `~/.pi/agent/auth.json`, `~/.local/share/opencode/auth.json`. Tests inject
|
|
190
203
|
* a fake `fs` + `homeDir` to exercise the file-vs-env branches deterministically.
|
|
191
204
|
*
|
|
192
|
-
* `bedrockProbe` is an injectable for the Bedrock SDK
|
|
193
|
-
* `checkPiMonoCredentials`. In production it is left undefined and the
|
|
194
|
-
*
|
|
195
|
-
* `ListFoundationModels`
|
|
205
|
+
* `bedrockProbe` is an injectable for the Bedrock SDK enumeration path in
|
|
206
|
+
* `checkPiMonoCredentials`. In production it is left undefined and the function
|
|
207
|
+
* dynamically imports `@aws-sdk/client-bedrock` to run real
|
|
208
|
+
* `ListFoundationModels` + `ListInferenceProfiles` calls. Tests inject a stub
|
|
209
|
+
* to avoid hitting AWS.
|
|
196
210
|
*/
|
|
197
211
|
export interface CredCheckOptions {
|
|
198
212
|
homeDir?: string;
|
|
199
213
|
fs?: { existsSync(p: string): boolean };
|
|
200
214
|
/**
|
|
201
|
-
* Injectable for Bedrock SDK
|
|
202
|
-
* of the real `@aws-sdk/client-bedrock` `ListFoundationModels`
|
|
203
|
-
* Should throw on auth/access failure (with an
|
|
204
|
-
* or resolve
|
|
215
|
+
* Injectable for the Bedrock SDK enumeration. When provided, called instead
|
|
216
|
+
* of the real `@aws-sdk/client-bedrock` `ListFoundationModels` +
|
|
217
|
+
* `ListInferenceProfiles` calls. Should throw on auth/access failure (with an
|
|
218
|
+
* AWS SDK-shaped error message) or resolve with the intersected
|
|
219
|
+
* (harness-drivable ∩ AWS-invocable) model list on success.
|
|
220
|
+
*
|
|
221
|
+
* Return type is `Array<{id,name}> | undefined` for backward compatibility:
|
|
222
|
+
* existing test stubs that return void (`async () => {}`) are still valid
|
|
223
|
+
* (void is assignable to undefined in TypeScript's structural typing);
|
|
224
|
+
* new tests that need to exercise the model list inject stubs that return
|
|
225
|
+
* an array. Production code always returns the model list.
|
|
205
226
|
*/
|
|
206
|
-
bedrockProbe?: () => Promise<
|
|
227
|
+
bedrockProbe?: () => Promise<Array<{ id: string; name: string }> | undefined>;
|
|
207
228
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for amazon-bedrock model group behaviour in modelGroupsForHarness.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Bedrock group always appears for the pi harness (NEVER blank).
|
|
6
|
+
* - Live worker-reported models are preferred when present.
|
|
7
|
+
* - Static snapshot from modelsdev-cache.json is used as fallback.
|
|
8
|
+
* - Converse-incompatible models listed by AWS but absent from pi-ai's catalog
|
|
9
|
+
* are NOT in the live list (the intersection is worker-side; this test just
|
|
10
|
+
* ensures the UI renders what the worker sent, without adding phantom entries).
|
|
11
|
+
* - Non-pi harnesses do NOT get a Bedrock group.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import {
|
|
16
|
+
type LiveBedrockStatus,
|
|
17
|
+
modelGroupsForHarness,
|
|
18
|
+
} from "../../ui/src/lib/agent-runtime-models";
|
|
19
|
+
|
|
20
|
+
describe("modelGroupsForHarness — Bedrock group for pi harness", () => {
|
|
21
|
+
const configs = undefined;
|
|
22
|
+
const envPresence = undefined;
|
|
23
|
+
|
|
24
|
+
test("pi harness always includes an Amazon Bedrock group (NEVER blank)", () => {
|
|
25
|
+
// No live status provided — falls back to static snapshot.
|
|
26
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence);
|
|
27
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
28
|
+
expect(bedrockGroup).toBeDefined();
|
|
29
|
+
// Static snapshot has 98 models — at least one must be present.
|
|
30
|
+
expect(bedrockGroup!.models.length).toBeGreaterThan(0);
|
|
31
|
+
// All model IDs must be prefixed with the provider.
|
|
32
|
+
for (const m of bedrockGroup!.models) {
|
|
33
|
+
expect(m.id.startsWith("amazon-bedrock/")).toBe(true);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("pi harness with no live report → Bedrock group disabled (auth state unknown)", () => {
|
|
38
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, null);
|
|
39
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
40
|
+
expect(bedrockGroup).toBeDefined();
|
|
41
|
+
expect(bedrockGroup!.enabled).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("pi harness with live report ready:true → Bedrock group enabled + live models", () => {
|
|
45
|
+
const liveStatus: LiveBedrockStatus = {
|
|
46
|
+
ready: true,
|
|
47
|
+
models: [
|
|
48
|
+
{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" },
|
|
49
|
+
{ id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5" },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, liveStatus);
|
|
53
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
54
|
+
expect(bedrockGroup).toBeDefined();
|
|
55
|
+
expect(bedrockGroup!.enabled).toBe(true);
|
|
56
|
+
expect(bedrockGroup!.models).toHaveLength(2);
|
|
57
|
+
expect(bedrockGroup!.models[0]!.id).toBe(
|
|
58
|
+
"amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0",
|
|
59
|
+
);
|
|
60
|
+
expect(bedrockGroup!.models[0]!.label).toBe("Claude Sonnet 4");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("pi harness with live report ready:false → Bedrock group disabled + live models shown", () => {
|
|
64
|
+
// Auth failed but we still show models so the operator can see what's available.
|
|
65
|
+
const liveStatus: LiveBedrockStatus = {
|
|
66
|
+
ready: false,
|
|
67
|
+
models: [{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" }],
|
|
68
|
+
};
|
|
69
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, liveStatus);
|
|
70
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
71
|
+
expect(bedrockGroup!.enabled).toBe(false);
|
|
72
|
+
expect(bedrockGroup!.models).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("pi harness with failed probe surfaces the probe error as disabledReason", () => {
|
|
76
|
+
// A failed probe (ready:false with an error) should surface WHY the group is
|
|
77
|
+
// disabled instead of a silent disable.
|
|
78
|
+
const liveStatus: LiveBedrockStatus = {
|
|
79
|
+
ready: false,
|
|
80
|
+
models: [],
|
|
81
|
+
error: "Token expired — run aws sso login",
|
|
82
|
+
};
|
|
83
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, liveStatus);
|
|
84
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
85
|
+
expect(bedrockGroup!.enabled).toBe(false);
|
|
86
|
+
expect(bedrockGroup!.disabledReason).toBe("Token expired — run aws sso login");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("pi harness with ready:true → no disabledReason and Bedrock icon key", () => {
|
|
90
|
+
const liveStatus: LiveBedrockStatus = {
|
|
91
|
+
ready: true,
|
|
92
|
+
models: [{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" }],
|
|
93
|
+
};
|
|
94
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, liveStatus);
|
|
95
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
96
|
+
expect(bedrockGroup!.disabledReason).toBeUndefined();
|
|
97
|
+
// Bedrock has its own provider icon — it no longer borrows the OpenRouter glyph.
|
|
98
|
+
expect(bedrockGroup!.models[0]!.providerId).toBe("amazon-bedrock");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("pi harness with live report and empty model list → shows empty list (not snapshot fallback)", () => {
|
|
102
|
+
// Worker reported successfully but no models were in the intersection.
|
|
103
|
+
const liveStatus: LiveBedrockStatus = { ready: true, models: [] };
|
|
104
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence, liveStatus);
|
|
105
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
106
|
+
expect(bedrockGroup!.models).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("opencode harness does NOT get a Bedrock group", () => {
|
|
110
|
+
const groups = modelGroupsForHarness("opencode", configs, envPresence);
|
|
111
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
112
|
+
expect(bedrockGroup).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("claude harness does NOT get a Bedrock group", () => {
|
|
116
|
+
const groups = modelGroupsForHarness("claude", configs, envPresence);
|
|
117
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
118
|
+
expect(bedrockGroup).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("codex harness does NOT get a Bedrock group", () => {
|
|
122
|
+
const groups = modelGroupsForHarness("codex", configs, envPresence);
|
|
123
|
+
const bedrockGroup = groups.find((g) => g.provider === "Amazon Bedrock");
|
|
124
|
+
expect(bedrockGroup).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("pi harness still returns openrouter/anthropic/openai snapshot groups alongside Bedrock", () => {
|
|
128
|
+
const groups = modelGroupsForHarness("pi", configs, envPresence);
|
|
129
|
+
const providerNames = groups.map((g) => g.provider);
|
|
130
|
+
expect(providerNames).toContain("OpenRouter");
|
|
131
|
+
expect(providerNames).toContain("Anthropic");
|
|
132
|
+
expect(providerNames).toContain("OpenAI");
|
|
133
|
+
expect(providerNames).toContain("Amazon Bedrock");
|
|
134
|
+
});
|
|
135
|
+
});
|