@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 +1 -1
- package/package.json +4 -3
- package/src/be/db.ts +11 -2
- package/src/be/migrations/094_mcp_extra_authorize_params.sql +4 -0
- package/src/be/skill-sync.ts +4 -4
- package/src/be/swarm-config-guard.ts +8 -0
- package/src/commands/provider-credentials.ts +14 -8
- package/src/commands/runner.ts +1 -0
- package/src/http/mcp-oauth.ts +14 -0
- package/src/oauth/mcp-wrapper.ts +14 -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 +65 -20
- package/src/providers/types.ts +12 -0
- package/src/tests/credential-check.test.ts +185 -46
- 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/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/types.ts +1 -0
- package/src/utils/skill-fs-writer.ts +11 -3
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.
|
|
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.
|
|
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;
|
package/src/be/skill-sync.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem sync for skills.
|
|
3
3
|
*
|
|
4
|
-
* Writes installed skills to
|
|
5
|
-
*
|
|
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:
|
|
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
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// Bedrock
|
|
312
|
-
|
|
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:
|
package/src/commands/runner.ts
CHANGED
package/src/http/mcp-oauth.ts
CHANGED
|
@@ -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({
|
package/src/oauth/mcp-wrapper.ts
CHANGED
|
@@ -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
|
|
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: `[
|
|
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: `[
|
|
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: `[
|
|
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:
|
|
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
|
-
|
|
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. `
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
|
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 /
|
|
91
|
-
* creds from a previous login) doesn't get falsely reported as
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
153
|
+
const fsProbe = opts.fs?.existsSync ?? existsSync;
|
|
109
154
|
const authFile = `${homeDir}/.pi/agent/auth.json`;
|
|
110
|
-
if (
|
|
155
|
+
if (fsProbe(authFile)) {
|
|
111
156
|
return { ready: true, missing: [], satisfiedBy: "file" };
|
|
112
157
|
}
|
|
113
158
|
|
package/src/providers/types.ts
CHANGED
|
@@ -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
|
}
|