@desplega.ai/agent-swarm 1.94.0 → 1.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.94.0",
5
+ "version": "1.95.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.94.0",
3
+ "version": "1.95.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -108,13 +108,14 @@
108
108
  "@ai-sdk/openai": "^3.0.41",
109
109
  "@anthropic-ai/sdk": "^0.93.0",
110
110
  "@asteasolutions/zod-to-openapi": "^8.0.0",
111
+ "@aws-sdk/client-bedrock": "3.1048.0",
111
112
  "@desplega.ai/business-use": "^0.4.2",
112
113
  "@desplega.ai/localtunnel": "^2.2.0",
113
- "@inkjs/ui": "^2.0.0",
114
- "@linear/sdk": "^77.0.0",
115
114
  "@earendil-works/pi-agent-core": "^0.79.1",
116
115
  "@earendil-works/pi-ai": "^0.79.1",
117
116
  "@earendil-works/pi-coding-agent": "^0.79.1",
117
+ "@inkjs/ui": "^2.0.0",
118
+ "@linear/sdk": "^77.0.0",
118
119
  "@modelcontextprotocol/sdk": "^1.25.1",
119
120
  "@openai/codex-sdk": "^0.139.0",
120
121
  "@opencode-ai/sdk": "^1.16.2",
package/src/be/db.ts CHANGED
@@ -9437,6 +9437,7 @@ type McpServerRow = {
9437
9437
  headers: string | null;
9438
9438
  envConfigKeys: string | null;
9439
9439
  headerConfigKeys: string | null;
9440
+ extraAuthorizeParams: string | null;
9440
9441
  authMethod: string | null;
9441
9442
  isEnabled: number;
9442
9443
  version: number;
@@ -9468,6 +9469,7 @@ function rowToMcpServer(row: McpServerRow): McpServer {
9468
9469
  headers: row.headers,
9469
9470
  envConfigKeys: row.envConfigKeys,
9470
9471
  headerConfigKeys: row.headerConfigKeys,
9472
+ extraAuthorizeParams: row.extraAuthorizeParams,
9471
9473
  authMethod: (row.authMethod as McpServer["authMethod"]) ?? "static",
9472
9474
  isEnabled: row.isEnabled === 1,
9473
9475
  version: row.version,
@@ -9506,6 +9508,7 @@ export interface McpServerInsert {
9506
9508
  headers?: string;
9507
9509
  envConfigKeys?: string;
9508
9510
  headerConfigKeys?: string;
9511
+ extraAuthorizeParams?: string;
9509
9512
  }
9510
9513
 
9511
9514
  export function createMcpServer(data: McpServerInsert): McpServer {
@@ -9517,9 +9520,9 @@ export function createMcpServer(data: McpServerInsert): McpServer {
9517
9520
  `INSERT INTO mcp_servers (
9518
9521
  id, name, description, scope, ownerAgentId, transport,
9519
9522
  command, args, url, headers,
9520
- envConfigKeys, headerConfigKeys,
9523
+ envConfigKeys, headerConfigKeys, extraAuthorizeParams,
9521
9524
  isEnabled, version, createdAt, lastUpdatedAt
9522
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
9525
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
9523
9526
  )
9524
9527
  .get(
9525
9528
  id,
@@ -9534,6 +9537,7 @@ export function createMcpServer(data: McpServerInsert): McpServer {
9534
9537
  data.headers ?? null,
9535
9538
  data.envConfigKeys ?? null,
9536
9539
  data.headerConfigKeys ?? null,
9540
+ data.extraAuthorizeParams ?? null,
9537
9541
  now,
9538
9542
  now,
9539
9543
  );
@@ -9596,6 +9600,10 @@ export function updateMcpServer(
9596
9600
  sets.push("headerConfigKeys = ?");
9597
9601
  params.push(updates.headerConfigKeys ?? null);
9598
9602
  }
9603
+ if (updates.extraAuthorizeParams !== undefined) {
9604
+ sets.push("extraAuthorizeParams = ?");
9605
+ params.push(updates.extraAuthorizeParams ?? null);
9606
+ }
9599
9607
  if (updates.isEnabled !== undefined) {
9600
9608
  sets.push("isEnabled = ?");
9601
9609
  params.push(updates.isEnabled ? 1 : 0);
@@ -9617,6 +9625,7 @@ export function updateMcpServer(
9617
9625
  "headers",
9618
9626
  "envConfigKeys",
9619
9627
  "headerConfigKeys",
9628
+ "extraAuthorizeParams",
9620
9629
  "transport",
9621
9630
  ];
9622
9631
  if (configFields.some((f) => (updates as Record<string, unknown>)[f] !== undefined)) {
@@ -0,0 +1,4 @@
1
+ -- Extra OAuth authorize-request params, applied at authorize time only.
2
+ -- JSON object string of flat string->string pairs, e.g. {"access_type":"offline","prompt":"consent"}.
3
+ -- NULL (default) => authorize URL is unchanged from today. Provider-agnostic.
4
+ ALTER TABLE mcp_servers ADD COLUMN extraAuthorizeParams TEXT;
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Filesystem sync for skills.
3
3
  *
4
- * Writes installed skills to ~/.claude/skills/<name>/SKILL.md,
5
- * ~/.pi/agent/skills/<name>/SKILL.md, and ~/.codex/skills/<name>/SKILL.md
6
- * so Claude Code, Pi, and Codex discover them natively.
4
+ * Writes installed skills to every local harness skill tree so Claude Code,
5
+ * Pi, Codex, OpenCode, and AGENTS.md-compatible adapters can discover them.
7
6
  *
8
7
  * This runs on the API side — workers call it via POST /api/skills/sync-filesystem.
9
8
  * The actual FS write logic lives in the worker-safe src/utils/skill-fs-writer.ts
@@ -13,6 +12,7 @@
13
12
  import { homedir } from "node:os";
14
13
  import {
15
14
  type SkillFsEntry,
15
+ type SkillHarnessTarget,
16
16
  type SkillSyncResult,
17
17
  writeSkillsToFilesystem,
18
18
  } from "../utils/skill-fs-writer";
@@ -32,7 +32,7 @@ export type { SkillSyncResult };
32
32
  */
33
33
  export function syncSkillsToFilesystem(
34
34
  agentId: string,
35
- harnessType: "claude" | "pi" | "codex" | "all" = "all",
35
+ harnessType: SkillHarnessTarget = "all",
36
36
  homeOverride?: string,
37
37
  ): SkillSyncResult {
38
38
  const skills = getAgentSkills(agentId);
@@ -58,6 +58,14 @@ const VALIDATED_KEYS: Record<string, (value: unknown) => string | null> = {
58
58
  if (["true", "false", "1", "0"].includes(normalized)) return null;
59
59
  return "Invalid SWARM_USE_CLAUDE_BRIDGE value (must be one of: true, false, 1, 0)";
60
60
  },
61
+ // AWS credential mode for the Bedrock path on the pi harness.
62
+ // sdk — AWS SDK default credential chain (env, ~/.aws/*, SSO, IMDS, …)
63
+ // bearer — explicit bearer token via AWS_BEARER_TOKEN_BEDROCK (future/Mantle)
64
+ // When absent the worker infers the mode from MODEL_OVERRIDE (sdk semantics).
65
+ BEDROCK_AUTH_MODE: (value) => {
66
+ if (value === "sdk" || value === "bearer") return null;
67
+ return "Invalid BEDROCK_AUTH_MODE value (must be one of: sdk, bearer)";
68
+ },
61
69
  };
62
70
 
63
71
  export function validateConfigValue(key: string, value: unknown): string | null {
@@ -302,14 +302,20 @@ export async function validateProviderCredentials(provider: string): Promise<Liv
302
302
  }
303
303
  case "pi":
304
304
  case "opencode": {
305
- // pi-mono with MODEL_OVERRIDE=amazon-bedrock/* delegates credential
306
- // resolution to the AWS SDK default chain (env, ~/.aws/*, SSO, IMDS,
307
- // assume-role, ). pi-ai exposes no Bedrock-specific check we could
308
- // call here, and the SDK chain may issue slow IMDS network calls on
309
- // non-EC2 hosts so the live test is a presence check, mirroring the
310
- // codex-OAuth pattern above. Real validation happens at the first
311
- // Bedrock inference call.
312
- if (provider === "pi" && env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")) {
305
+ // For the pi Bedrock path, the real credential check is the
306
+ // `ListFoundationModels` probe that `checkProviderCredentials` (the
307
+ // `pi` dynamic-import arm) already ran. That probe result is already
308
+ // in `buildCredStatusReport` the live-test is a pass-through / no-op
309
+ // so we never issue a second AWS SDK call here (which would drag the
310
+ // SDK into the wrong binary or make slow IMDS calls on non-EC2 hosts).
311
+ // Bedrock mode: explicit BEDROCK_AUTH_MODE=sdk OR
312
+ // absent BEDROCK_AUTH_MODE + amazon-bedrock/ MODEL_OVERRIDE prefix.
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
+ ) {
313
319
  return presenceCheckOk();
314
320
  }
315
321
  // Both pi-mono and opencode resolve credentials in the same order:
@@ -436,6 +436,7 @@ const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
436
436
  "MODEL_OVERRIDE",
437
437
  "AGENT_FS_SHARED_ORG_ID",
438
438
  "SWARM_USE_CLAUDE_BRIDGE",
439
+ "BEDROCK_AUTH_MODE",
439
440
  ]);
440
441
 
441
442
  /**
@@ -362,6 +362,19 @@ async function prepareAuthorizeFlow(
362
362
 
363
363
  const scopes = q.scopes ? splitScopes(q.scopes) : client.scopes;
364
364
 
365
+ let extraParams: Record<string, string> | undefined;
366
+ if (server.extraAuthorizeParams) {
367
+ try {
368
+ const parsed = JSON.parse(server.extraAuthorizeParams);
369
+ if (parsed && typeof parsed === "object") {
370
+ extraParams = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
371
+ }
372
+ } catch {
373
+ // Malformed config must never break the authorize flow — log + ignore.
374
+ console.warn(`[mcp-oauth] Ignoring malformed extraAuthorizeParams for server ${mcpServerId}`);
375
+ }
376
+ }
377
+
365
378
  const built = await buildAuthorizeUrl({
366
379
  authorizeUrl: client.authorizeUrl,
367
380
  tokenUrl: client.tokenUrl,
@@ -369,6 +382,7 @@ async function prepareAuthorizeFlow(
369
382
  redirectUri: callbackRedirectUri(),
370
383
  scopes,
371
384
  resource: client.resourceUrl,
385
+ extraParams,
372
386
  });
373
387
 
374
388
  insertMcpOAuthPending({
@@ -287,7 +287,21 @@ export async function buildAuthorizeUrl(input: BuildAuthorizeInput): Promise<Bui
287
287
  url.searchParams.set("resource", input.resource);
288
288
 
289
289
  if (input.extraParams) {
290
+ const RESERVED = new Set([
291
+ "response_type",
292
+ "client_id",
293
+ "redirect_uri",
294
+ "scope",
295
+ "state",
296
+ "code_challenge",
297
+ "code_challenge_method",
298
+ "resource",
299
+ ]);
290
300
  for (const [k, v] of Object.entries(input.extraParams)) {
301
+ if (RESERVED.has(k.toLowerCase())) {
302
+ console.warn(`[mcp-oauth] extraParams key "${k}" is reserved and skipped`);
303
+ continue;
304
+ }
291
305
  url.searchParams.set(k, v);
292
306
  }
293
307
  }
@@ -63,6 +63,21 @@ export async function resolveCodexPrompt(
63
63
  prompt: string,
64
64
  skillsDir?: string,
65
65
  emit?: (event: ProviderEvent) => void,
66
+ ): Promise<string> {
67
+ return resolveSlashSkillPrompt(prompt, {
68
+ providerLabel: "codex",
69
+ skillsDir: skillsDir ?? defaultSkillsDir(),
70
+ emit,
71
+ });
72
+ }
73
+
74
+ export async function resolveSlashSkillPrompt(
75
+ prompt: string,
76
+ opts: {
77
+ providerLabel: string;
78
+ skillsDir: string;
79
+ emit?: (event: ProviderEvent) => void;
80
+ },
66
81
  ): Promise<string> {
67
82
  if (!prompt) {
68
83
  return prompt;
@@ -81,15 +96,14 @@ export async function resolveCodexPrompt(
81
96
 
82
97
  const commandName: string = match[1];
83
98
  const trailingArgs: string = match[2] ?? "";
84
- const dir = skillsDir ?? defaultSkillsDir();
85
- const skillPath = join(dir, commandName, "SKILL.md");
99
+ const skillPath = join(opts.skillsDir, commandName, "SKILL.md");
86
100
 
87
101
  const file = Bun.file(skillPath);
88
102
  const exists = await file.exists();
89
103
  if (!exists) {
90
- emit?.({
104
+ opts.emit?.({
91
105
  type: "raw_stderr",
92
- content: `[codex] skill resolver: SKILL.md not found for /${commandName} (looked in ${skillPath})\n`,
106
+ content: `[${opts.providerLabel}] skill resolver: SKILL.md not found for /${commandName} (looked in ${skillPath})\n`,
93
107
  });
94
108
  return prompt;
95
109
  }
@@ -99,17 +113,17 @@ export async function resolveCodexPrompt(
99
113
  skillContent = await file.text();
100
114
  } catch (err) {
101
115
  const message = err instanceof Error ? err.message : String(err);
102
- emit?.({
116
+ opts.emit?.({
103
117
  type: "raw_stderr",
104
- content: `[codex] skill resolver: failed to read SKILL.md for /${commandName}: ${message}\n`,
118
+ content: `[${opts.providerLabel}] skill resolver: failed to read SKILL.md for /${commandName}: ${message}\n`,
105
119
  });
106
120
  return prompt;
107
121
  }
108
122
 
109
123
  if (skillContent.length > MAX_SKILL_CHARS) {
110
- emit?.({
124
+ opts.emit?.({
111
125
  type: "raw_stderr",
112
- content: `[codex] skill resolver: SKILL.md for /${commandName} exceeds ${MAX_SKILL_CHARS} chars (${skillContent.length}), truncating\n`,
126
+ content: `[${opts.providerLabel}] skill resolver: SKILL.md for /${commandName} exceeds ${MAX_SKILL_CHARS} chars (${skillContent.length}), truncating\n`,
113
127
  });
114
128
  skillContent = skillContent.slice(0, MAX_SKILL_CHARS);
115
129
  }
@@ -20,6 +20,7 @@ import {
20
20
  import { validateOpencodeCredentials } from "../utils/credentials";
21
21
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
22
22
  import { scrubSecrets } from "../utils/secret-scrubber";
23
+ import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
23
24
  import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
24
25
  import { readPkgVersion } from "./harness-version";
25
26
  import type {
@@ -102,6 +103,13 @@ function isAssistantMessage(msg: unknown): msg is AssistantMessage {
102
103
  }
103
104
 
104
105
  const DOCKER_PLUGIN_PATH = "/home/worker/.config/opencode/plugins/agent-swarm.ts";
106
+
107
+ function defaultOpencodeSkillsDir(): string {
108
+ if (process.env.OPENCODE_SKILLS_DIR) {
109
+ return process.env.OPENCODE_SKILLS_DIR;
110
+ }
111
+ return join(process.env.HOME ?? "/home/worker", ".opencode", "skills");
112
+ }
105
113
  const MODEL_CACHE_REFRESH_TIMEOUT_MS = 15_000;
106
114
 
107
115
  function isOpenRouterModel(model: string | undefined): boolean {
@@ -291,6 +299,10 @@ export class OpencodeSession implements ProviderSession {
291
299
  });
292
300
  }
293
301
 
302
+ emitProviderEvent(event: ProviderEvent): void {
303
+ this.emit(event);
304
+ }
305
+
294
306
  onEvent(listener: (event: ProviderEvent) => void): void {
295
307
  const wasEmpty = this.listeners.length === 0;
296
308
  this.listeners.push(listener);
@@ -738,13 +750,19 @@ export class OpencodeAdapter implements ProviderAdapter {
738
750
 
739
751
  let promptRefreshAttempted = false;
740
752
  let promptRefreshPromise: Promise<boolean> | undefined;
753
+ let session: OpencodeSession | undefined;
741
754
  const sendPrompt = async () => {
755
+ const resolvedPrompt = await resolveSlashSkillPrompt(config.prompt, {
756
+ providerLabel: "opencode",
757
+ skillsDir: defaultOpencodeSkillsDir(),
758
+ emit: (event) => session?.emitProviderEvent(event),
759
+ });
742
760
  await client.session.prompt({
743
761
  path: { id: sessionId },
744
762
  query: { directory: config.cwd },
745
763
  body: {
746
764
  agent: agentName,
747
- parts: [{ type: "text", text: config.prompt }],
765
+ parts: [{ type: "text", text: resolvedPrompt }],
748
766
  },
749
767
  });
750
768
  };
@@ -760,7 +778,7 @@ export class OpencodeAdapter implements ProviderAdapter {
760
778
  return await promptRefreshPromise;
761
779
  };
762
780
 
763
- const session = new OpencodeSession(
781
+ session = new OpencodeSession(
764
782
  sessionId,
765
783
  server,
766
784
  config.model,
@@ -74,40 +74,85 @@ function modelToCredKeys(modelStr: string | undefined): string[] | null {
74
74
  return null;
75
75
  }
76
76
 
77
+ /**
78
+ * Run a single `ListFoundationModels` call against the AWS Bedrock management
79
+ * API to verify that the active credential chain is valid for Bedrock in the
80
+ * configured region. Returns the client directly (callers discard the model
81
+ * list — only the throw/no-throw distinction is the signal).
82
+ *
83
+ * Dynamically imported so the API binary never loads `@aws-sdk/client-bedrock`.
84
+ * Tests inject a stub via `CredCheckOptions.bedrockProbe` instead.
85
+ */
86
+ async function runBedrockSdkProbe(region: string): Promise<void> {
87
+ const { BedrockClient, ListFoundationModelsCommand } = await import("@aws-sdk/client-bedrock");
88
+ const client = new BedrockClient({ region });
89
+ await client.send(new ListFoundationModelsCommand({}));
90
+ }
91
+
77
92
  /**
78
93
  * Pi-mono is satisfied by ANY of:
79
- * 1. `MODEL_OVERRIDE` selects the `amazon-bedrock` provider — credential
80
- * resolution is delegated to the AWS SDK's default chain at first
81
- * inference call. agent-swarm does no presence check; if creds are
82
- * missing the SDK error surfaces in the session log.
94
+ * 1. `BEDROCK_AUTH_MODE=sdk` or `MODEL_OVERRIDE` selects the
95
+ * `amazon-bedrock` provider (prefix-inference fallback when
96
+ * `BEDROCK_AUTH_MODE` is absent). A real `ListFoundationModels` probe
97
+ * is issued via the AWS SDK default credential chain. Success
98
+ * `ready:true, satisfiedBy:"sdk-delegated"`; failure → `ready:false`
99
+ * with a classified hint. The probe is worker-only (the pi dynamic-import
100
+ * arm in `checkProviderCredentials`); the API binary never imports the SDK.
83
101
  * 2. `~/.pi/agent/auth.json` exists.
84
- * 3. `MODEL_OVERRIDE` is set to a provider-prefixed model — only the
85
- * matching provider's key is required.
102
+ * 3. `MODEL_OVERRIDE` is set to a non-Bedrock provider-prefixed model — only
103
+ * the matching provider's key is required.
86
104
  * 4. `MODEL_OVERRIDE` is empty / unprefixed — any one of the supported
87
105
  * keys (ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OPENAI_API_KEY) is
88
106
  * enough.
89
107
  *
90
- * Bedrock is checked first so a stale `auth.json` (Anthropic / OpenRouter
91
- * creds from a previous login) doesn't get falsely reported as the
92
- * satisfying source when the model is actually going to AWS.
108
+ * The Bedrock branch is checked first so a stale `auth.json` (Anthropic /
109
+ * OpenRouter creds from a previous login) doesn't get falsely reported as
110
+ * the satisfying source when the model is actually going to AWS.
93
111
  */
94
- export function checkPiMonoCredentials(
112
+ export async function checkPiMonoCredentials(
95
113
  env: Record<string, string | undefined>,
96
114
  opts: CredCheckOptions = {},
97
- ): CredStatus {
98
- if (env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")) {
99
- return {
100
- ready: true,
101
- missing: [],
102
- satisfiedBy: "sdk-delegated",
103
- hint: "AWS SDK will resolve credentials at first Bedrock call (env, ~/.aws/*, SSO, IMDS, etc.).",
104
- };
115
+ ): Promise<CredStatus> {
116
+ // Determine Bedrock SDK mode:
117
+ // - Explicit: BEDROCK_AUTH_MODE=sdk
118
+ // - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
119
+ // "amazon-bedrock/" (preserves today's prefix-inference semantics)
120
+ // BEDROCK_AUTH_MODE=bearer is declared/validated but the full bearer-token
121
+ // path is out of scope for PR1 it falls through to the standard auth check.
122
+ const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
123
+ const isBedrockSdk =
124
+ bedrockAuthMode === "sdk" ||
125
+ (bedrockAuthMode === undefined &&
126
+ env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
127
+
128
+ if (isBedrockSdk) {
129
+ const region = env.AWS_REGION ?? "us-east-1";
130
+ const probe = opts.bedrockProbe ?? (() => runBedrockSdkProbe(region));
131
+ try {
132
+ await probe();
133
+ return {
134
+ ready: true,
135
+ missing: [],
136
+ satisfiedBy: "sdk-delegated",
137
+ hint: `AWS SDK credentials verified via ListFoundationModels (region: ${region}).`,
138
+ };
139
+ } catch (err) {
140
+ const errorMessage = err instanceof Error ? err.message : String(err);
141
+ const classification = classifyAwsSdkError(errorMessage);
142
+ return {
143
+ ready: false,
144
+ missing: [],
145
+ hint:
146
+ classification?.message ??
147
+ `AWS Bedrock credential probe failed (region: ${region}): ${errorMessage}`,
148
+ };
149
+ }
105
150
  }
106
151
 
107
152
  const homeDir = opts.homeDir ?? env.HOME ?? "/root";
108
- const probe = opts.fs?.existsSync ?? existsSync;
153
+ const fsProbe = opts.fs?.existsSync ?? existsSync;
109
154
  const authFile = `${homeDir}/.pi/agent/auth.json`;
110
- if (probe(authFile)) {
155
+ if (fsProbe(authFile)) {
111
156
  return { ready: true, missing: [], satisfiedBy: "file" };
112
157
  }
113
158
 
@@ -188,8 +188,20 @@ export interface CredStatus {
188
188
  * pi/opencode predicates probe the filesystem for `~/.codex/auth.json`,
189
189
  * `~/.pi/agent/auth.json`, `~/.local/share/opencode/auth.json`. Tests inject
190
190
  * a fake `fs` + `homeDir` to exercise the file-vs-env branches deterministically.
191
+ *
192
+ * `bedrockProbe` is an injectable for the Bedrock SDK probe path in
193
+ * `checkPiMonoCredentials`. In production it is left undefined and the
194
+ * function dynamically imports `@aws-sdk/client-bedrock` to run a real
195
+ * `ListFoundationModels` call. Tests inject a stub to avoid hitting AWS.
191
196
  */
192
197
  export interface CredCheckOptions {
193
198
  homeDir?: string;
194
199
  fs?: { existsSync(p: string): boolean };
200
+ /**
201
+ * Injectable for Bedrock SDK credential probe. When provided, called instead
202
+ * of the real `@aws-sdk/client-bedrock` `ListFoundationModels` call.
203
+ * Should throw on auth/access failure (with an AWS SDK-shaped error message)
204
+ * or resolve on success.
205
+ */
206
+ bedrockProbe?: () => Promise<void>;
195
207
  }