@desplega.ai/agent-swarm 1.74.4 → 1.76.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 +1 -1
- package/openapi.json +1264 -46
- package/package.json +2 -2
- package/src/be/db.ts +563 -9
- package/src/be/memory/edges-store.ts +69 -0
- package/src/be/memory/providers/sqlite-store.ts +4 -0
- package/src/be/memory/raters/explicit-self.ts +22 -0
- package/src/be/memory/raters/implicit-citation.ts +44 -0
- package/src/be/memory/raters/llm-client.ts +172 -0
- package/src/be/memory/raters/llm-summarizer.ts +218 -0
- package/src/be/memory/raters/llm.ts +375 -0
- package/src/be/memory/raters/noop.ts +14 -0
- package/src/be/memory/raters/registry.ts +86 -0
- package/src/be/memory/raters/retrieval.ts +88 -0
- package/src/be/memory/raters/run-server-raters.ts +97 -0
- package/src/be/memory/raters/store.ts +228 -0
- package/src/be/memory/raters/types.ts +101 -0
- package/src/be/memory/reranker.ts +32 -2
- package/src/be/memory/retrieval-store.ts +116 -0
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
- package/src/be/migrations/052_memory_edges.sql +36 -0
- package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
- package/src/be/migrations/054_agent_harness_provider.sql +21 -0
- package/src/be/migrations/055_agent_cred_status.sql +15 -0
- package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
- package/src/be/migrations/057_inbox_item_state.sql +27 -0
- package/src/be/migrations/058_task_templates.sql +31 -0
- package/src/be/swarm-config-guard.ts +24 -0
- package/src/commands/credential-wait.ts +186 -0
- package/src/commands/provider-credentials.ts +434 -0
- package/src/commands/runner.ts +253 -21
- package/src/hooks/hook.ts +143 -66
- package/src/http/agents.ts +191 -1
- package/src/http/config.ts +11 -1
- package/src/http/core.ts +5 -0
- package/src/http/inbox-state.ts +89 -0
- package/src/http/index.ts +10 -0
- package/src/http/memory.ts +230 -1
- package/src/http/sessions.ts +86 -0
- package/src/http/status.ts +665 -0
- package/src/http/task-templates.ts +51 -0
- package/src/http/tasks.ts +85 -5
- package/src/http/users.ts +134 -0
- package/src/prompts/memories.ts +62 -0
- package/src/providers/claude-adapter.ts +22 -0
- package/src/providers/claude-managed-adapter.ts +24 -0
- package/src/providers/codex-adapter.ts +43 -1
- package/src/providers/devin-adapter.ts +18 -0
- package/src/providers/index.ts +7 -0
- package/src/providers/opencode-adapter.ts +60 -0
- package/src/providers/pi-mono-adapter.ts +71 -0
- package/src/providers/types.ts +34 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +0 -1
- package/src/tests/agents-harness-provider.test.ts +333 -0
- package/src/tests/credential-check.test.ts +367 -0
- package/src/tests/credential-status-api.test.ts +223 -0
- package/src/tests/credential-status-routing.test.ts +150 -0
- package/src/tests/credential-wait.test.ts +282 -0
- package/src/tests/harness-provider-resolution.test.ts +242 -0
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/memory-edges.test.ts +722 -0
- package/src/tests/memory-rate-endpoint.test.ts +330 -0
- package/src/tests/memory-rate-tool.test.ts +252 -0
- package/src/tests/memory-rater-e2e.test.ts +578 -0
- package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
- package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
- package/src/tests/memory-rater-llm.test.ts +964 -0
- package/src/tests/memory-rater-store.test.ts +249 -0
- package/src/tests/memory-reranker.test.ts +161 -2
- package/src/tests/migration-runner-regressions.test.ts +17 -2
- package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
- package/src/tests/run-server-raters.test.ts +291 -0
- package/src/tests/sessions.test.ts +141 -0
- package/src/tests/status.test.ts +843 -0
- package/src/tests/stop-hook-task-resolution.test.ts +98 -0
- package/src/tests/template-recommendations.test.ts +148 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/use-dismissible-card.test.ts +140 -0
- package/src/tools/memory-rate.ts +166 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/store-progress.ts +37 -0
- package/src/tools/swarm-config/set-config.ts +17 -1
- package/src/tools/tool-config.ts +1 -0
- package/src/types.ts +122 -1
- package/src/utils/harness-provider.ts +32 -0
- package/tsconfig.json +0 -2
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic credential check dispatcher (WORKER-ONLY).
|
|
3
|
+
*
|
|
4
|
+
* Lives in `src/commands/` because the predicates value-import worker-harness
|
|
5
|
+
* SDKs (e.g. `@mariozechner/pi-coding-agent` via `pi-mono-adapter.ts`) that
|
|
6
|
+
* have module-load side effects. Importing this file from any module
|
|
7
|
+
* reachable from `src/http.ts` would drag those SDKs into the bun-compiled
|
|
8
|
+
* API binary — which is exactly the bug PR #452 hit at `/usr/local/bin/`.
|
|
9
|
+
*
|
|
10
|
+
* Used by:
|
|
11
|
+
* - The worker boot loop (`src/commands/credential-wait.ts`) to decide
|
|
12
|
+
* whether the worker can claim tasks yet.
|
|
13
|
+
* - The worker post-task hook (`src/commands/runner.ts`) to refresh on
|
|
14
|
+
* harness_provider changes.
|
|
15
|
+
*
|
|
16
|
+
* Reports flow worker → API as JSON via the existing PATCH /agents/:id
|
|
17
|
+
* endpoint (see `AgentCredStatusSchema` in `src/types.ts`). The API never
|
|
18
|
+
* runs the predicate itself — it just reads the agent row.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { checkClaudeCredentials } from "../providers/claude-adapter";
|
|
22
|
+
import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
|
|
23
|
+
import { checkCodexCredentials } from "../providers/codex-adapter";
|
|
24
|
+
import { checkDevinCredentials } from "../providers/devin-adapter";
|
|
25
|
+
import { checkOpencodeCredentials } from "../providers/opencode-adapter";
|
|
26
|
+
import { checkPiMonoCredentials } from "../providers/pi-mono-adapter";
|
|
27
|
+
import type { CredCheckOptions, CredStatus } from "../providers/types";
|
|
28
|
+
import type { AgentCredStatus } from "../types";
|
|
29
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
30
|
+
|
|
31
|
+
export type SupportedProvider = "claude" | "claude-managed" | "codex" | "devin" | "opencode" | "pi";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Static documentation of which env vars each provider considers when running
|
|
35
|
+
* `checkCredentials`. Used by the dashboard to render hints before any worker
|
|
36
|
+
* has reported its dynamic state. The arrays are illustrative — the real
|
|
37
|
+
* authoritative answer always comes from the predicate function (which may
|
|
38
|
+
* fold in `MODEL_OVERRIDE`-conditional logic for pi/opencode and file-based
|
|
39
|
+
* fallbacks for codex/pi/opencode).
|
|
40
|
+
*/
|
|
41
|
+
export const REQUIRED_CRED_VARS_BY_PROVIDER: Record<SupportedProvider, readonly string[]> = {
|
|
42
|
+
claude: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
43
|
+
"claude-managed": [
|
|
44
|
+
"ANTHROPIC_API_KEY",
|
|
45
|
+
"MANAGED_AGENT_ID",
|
|
46
|
+
"MANAGED_ENVIRONMENT_ID",
|
|
47
|
+
"MCP_BASE_URL",
|
|
48
|
+
],
|
|
49
|
+
codex: ["OPENAI_API_KEY", "CODEX_OAUTH"],
|
|
50
|
+
devin: ["DEVIN_API_KEY", "DEVIN_ORG_ID"],
|
|
51
|
+
opencode: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
52
|
+
pi: ["ANTHROPIC_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run the predicate for `provider`. Unknown providers throw — call sites
|
|
57
|
+
* should treat that as a configuration bug, not a user-correctable state.
|
|
58
|
+
*/
|
|
59
|
+
export function checkProviderCredentials(
|
|
60
|
+
provider: string,
|
|
61
|
+
env: Record<string, string | undefined>,
|
|
62
|
+
opts?: CredCheckOptions,
|
|
63
|
+
): CredStatus {
|
|
64
|
+
switch (provider) {
|
|
65
|
+
case "claude":
|
|
66
|
+
return checkClaudeCredentials(env);
|
|
67
|
+
case "claude-managed":
|
|
68
|
+
return checkClaudeManagedCredentials(env);
|
|
69
|
+
case "codex":
|
|
70
|
+
return checkCodexCredentials(env, opts);
|
|
71
|
+
case "devin":
|
|
72
|
+
return checkDevinCredentials(env);
|
|
73
|
+
case "opencode":
|
|
74
|
+
return checkOpencodeCredentials(env, opts);
|
|
75
|
+
case "pi":
|
|
76
|
+
return checkPiMonoCredentials(env, opts);
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(
|
|
79
|
+
`checkProviderCredentials: unknown provider "${provider}". Supported: claude, claude-managed, codex, devin, opencode, pi.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Live "Test connection" dispatcher ───────────────────────────────────────
|
|
85
|
+
// Used by `POST /status/test-connection` on the home page setup checklist.
|
|
86
|
+
// Mirrors `checkProviderCredentials` but issues a real (cheapest possible)
|
|
87
|
+
// upstream call so the user can flip the `harness` milestone to `verified`.
|
|
88
|
+
//
|
|
89
|
+
// Pure function — no DB writes. Lives here (not on `ProviderAdapter`) because
|
|
90
|
+
// adapters are runtime-loaded by workers, while this dispatcher is API-server
|
|
91
|
+
// safe. All errors run through `scrubSecrets` so we never leak the user's
|
|
92
|
+
// own key shape back through the JSON response.
|
|
93
|
+
|
|
94
|
+
const LIVE_TEST_TIMEOUT_MS = 5_000;
|
|
95
|
+
|
|
96
|
+
export interface LiveValidationResult {
|
|
97
|
+
ok: boolean;
|
|
98
|
+
error?: string;
|
|
99
|
+
latency_ms: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function timedFetch(
|
|
103
|
+
url: string,
|
|
104
|
+
init: RequestInit,
|
|
105
|
+
): Promise<{ ok: boolean; status: number; bodyText: string; latency_ms: number }> {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), LIVE_TEST_TIMEOUT_MS);
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
111
|
+
let bodyText = "";
|
|
112
|
+
try {
|
|
113
|
+
bodyText = await res.text();
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore body read errors — status alone is enough for ok/not-ok
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ok: res.ok,
|
|
119
|
+
status: res.status,
|
|
120
|
+
bodyText,
|
|
121
|
+
latency_ms: Date.now() - start,
|
|
122
|
+
};
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function asLiveError(err: unknown, latency_ms: number): LiveValidationResult {
|
|
129
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: scrubSecrets(raw) || "Unknown error",
|
|
133
|
+
latency_ms,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Per-endpoint live-call helpers ──────────────────────────────────────────
|
|
138
|
+
// Each helper accepts the auth material it needs and returns a normalized
|
|
139
|
+
// `LiveValidationResult`. The harness-level dispatcher below picks which
|
|
140
|
+
// helper to call based on which credential is actually present, so OAuth
|
|
141
|
+
// users no longer see "ANTHROPIC_API_KEY is not set" when their adapter is
|
|
142
|
+
// happily running off `CLAUDE_CODE_OAUTH_TOKEN`.
|
|
143
|
+
|
|
144
|
+
async function checkAnthropicApiKey(apiKey: string): Promise<LiveValidationResult> {
|
|
145
|
+
const r = await timedFetch("https://api.anthropic.com/v1/models", {
|
|
146
|
+
method: "GET",
|
|
147
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
148
|
+
});
|
|
149
|
+
if (r.ok) return { ok: true, latency_ms: r.latency_ms };
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
|
|
153
|
+
latency_ms: r.latency_ms,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* OAuth credentials (Claude Pro/Max via `claude` CLI login, ChatGPT via Codex
|
|
159
|
+
* OAuth) are treated as a presence check — we don't issue a live upstream call.
|
|
160
|
+
*
|
|
161
|
+
* Why: OAuth flows include their own refresh logic (handled at adapter boot,
|
|
162
|
+
* not here), the OAuth-bearer-with-/v1/models contract isn't a stable public
|
|
163
|
+
* surface, and a "real" check that fails on a stale-but-refreshable token
|
|
164
|
+
* would be a worse UX than a presence check that passes optimistically. The
|
|
165
|
+
* runtime adapter remains the source of truth.
|
|
166
|
+
*/
|
|
167
|
+
function presenceCheckOk(): LiveValidationResult {
|
|
168
|
+
return { ok: true, latency_ms: 0 };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function checkOpenAiApiKey(apiKey: string): Promise<LiveValidationResult> {
|
|
172
|
+
const r = await timedFetch("https://api.openai.com/v1/models", {
|
|
173
|
+
method: "GET",
|
|
174
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
175
|
+
});
|
|
176
|
+
if (r.ok) return { ok: true, latency_ms: r.latency_ms };
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
|
|
180
|
+
latency_ms: r.latency_ms,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function checkOpenRouter(apiKey: string): Promise<LiveValidationResult> {
|
|
185
|
+
const r = await timedFetch("https://openrouter.ai/api/v1/models", {
|
|
186
|
+
method: "GET",
|
|
187
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
188
|
+
});
|
|
189
|
+
if (r.ok) return { ok: true, latency_ms: r.latency_ms };
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
|
|
193
|
+
latency_ms: r.latency_ms,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Mirror of the disk check in `checkCodexCredentials` — true when the worker
|
|
199
|
+
* has a materialised `~/.codex/auth.json`, which is the canonical "logged in"
|
|
200
|
+
* state for codex (whether the entrypoint wrote it from `CODEX_OAUTH`, from
|
|
201
|
+
* `OPENAI_API_KEY` via `codex login --with-api-key`, or it was baked into the
|
|
202
|
+
* image). The codex adapter handles refresh internally, so this is treated as
|
|
203
|
+
* a presence check at live-test time (no upstream call).
|
|
204
|
+
*/
|
|
205
|
+
function codexAuthFileExists(env: Record<string, string | undefined>): boolean {
|
|
206
|
+
// Delegate to the adapter's own check so the auth.json path stays in one
|
|
207
|
+
// place. `satisfiedBy === "file"` is set iff the file exists on disk.
|
|
208
|
+
return checkCodexCredentials(env).satisfiedBy === "file";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract the OAuth `access_token` from a `CODEX_OAUTH` env blob. The blob is
|
|
213
|
+
* a JSON object shaped like `CodexOAuthCredentials` (`{access, refresh,
|
|
214
|
+
* expires, accountId}`) — see `src/providers/codex-oauth/types.ts`. Returns
|
|
215
|
+
* null on any parse / shape failure (caller falls back to API-key path).
|
|
216
|
+
*/
|
|
217
|
+
function parseCodexOAuthAccess(blob: string | undefined): string | null {
|
|
218
|
+
if (!blob) return null;
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(blob);
|
|
221
|
+
if (typeof parsed?.access === "string" && parsed.access.length > 0) return parsed.access;
|
|
222
|
+
} catch {
|
|
223
|
+
// not JSON — caller falls back
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Issue the cheapest live call per provider to verify credentials work.
|
|
230
|
+
*
|
|
231
|
+
* Credential acceptance is kept in sync with `REQUIRED_CRED_VARS_BY_PROVIDER`
|
|
232
|
+
* and each adapter's `checkCredentials` function:
|
|
233
|
+
*
|
|
234
|
+
* | Harness | Accepted credentials (in resolution order) | Endpoint |
|
|
235
|
+
* |------------------|-------------------------------------------------------------------------|--------------------------------|
|
|
236
|
+
* | `claude` | `CLAUDE_CODE_OAUTH_TOKEN` (Pro/Max OAuth) → `ANTHROPIC_API_KEY` | Anthropic `/v1/models` |
|
|
237
|
+
* | `claude-managed` | `ANTHROPIC_API_KEY` (managed agents always use API key + managed envs) | Anthropic `/v1/models` |
|
|
238
|
+
* | `codex` | `~/.codex/auth.json` (file) → `CODEX_OAUTH` (env OAuth) → `OPENAI_API_KEY` | OpenAI `/v1/models` (api-key path only) |
|
|
239
|
+
* | `opencode` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` (pi-style) | matching provider's `/v1/models` |
|
|
240
|
+
* | `pi` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` | matching provider's `/v1/models` |
|
|
241
|
+
* | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v1/sessions?limit=1` |
|
|
242
|
+
*
|
|
243
|
+
* Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
|
|
244
|
+
* otherwise. Errors are scrubbed via `scrubSecrets` before being returned.
|
|
245
|
+
*/
|
|
246
|
+
export async function validateProviderCredentials(provider: string): Promise<LiveValidationResult> {
|
|
247
|
+
const env = process.env;
|
|
248
|
+
const startedAt = Date.now();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
switch (provider) {
|
|
252
|
+
case "claude": {
|
|
253
|
+
// OAuth (Claude Pro/Max via `claude` CLI login) wins over API key —
|
|
254
|
+
// matches `claude-adapter.ts` and the docker entrypoint precedence.
|
|
255
|
+
// OAuth tokens get a presence check only (see `presenceCheckOk`).
|
|
256
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN) return presenceCheckOk();
|
|
257
|
+
if (env.ANTHROPIC_API_KEY) return checkAnthropicApiKey(env.ANTHROPIC_API_KEY);
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
error: "Set either CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY.",
|
|
261
|
+
latency_ms: Date.now() - startedAt,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
case "claude-managed": {
|
|
265
|
+
// Managed agents always run with an API key — OAuth not supported on
|
|
266
|
+
// the managed-agents path today.
|
|
267
|
+
if (env.ANTHROPIC_API_KEY) return checkAnthropicApiKey(env.ANTHROPIC_API_KEY);
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: "ANTHROPIC_API_KEY is not set.",
|
|
271
|
+
latency_ms: Date.now() - startedAt,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case "codex": {
|
|
275
|
+
// Resolution order matches `checkCodexCredentials`:
|
|
276
|
+
// 1) `~/.codex/auth.json` on disk (canonical state once `codex login`
|
|
277
|
+
// has run, or when the entrypoint pre-materialised it from
|
|
278
|
+
// CODEX_OAUTH / OPENAI_API_KEY). This is the OAuth-equivalent path
|
|
279
|
+
// for codex — refresh logic lives in the adapter, so we only do a
|
|
280
|
+
// presence check (no upstream call).
|
|
281
|
+
// 2) `CODEX_OAUTH` env blob — same OAuth treatment.
|
|
282
|
+
// 3) `OPENAI_API_KEY` env var — live-test against OpenAI `/v1/models`.
|
|
283
|
+
//
|
|
284
|
+
// Without (1), an agent that boots fresh from a credential pool whose
|
|
285
|
+
// entrypoint already wrote auth.json would falsely fail the live test
|
|
286
|
+
// with "Set either CODEX_OAUTH or OPENAI_API_KEY" (observed in prod).
|
|
287
|
+
if (codexAuthFileExists(env)) return presenceCheckOk();
|
|
288
|
+
if (parseCodexOAuthAccess(env.CODEX_OAUTH)) return presenceCheckOk();
|
|
289
|
+
if (env.OPENAI_API_KEY) return checkOpenAiApiKey(env.OPENAI_API_KEY);
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error:
|
|
293
|
+
"No codex credential found (no ~/.codex/auth.json, CODEX_OAUTH, or OPENAI_API_KEY).",
|
|
294
|
+
latency_ms: Date.now() - startedAt,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
case "pi":
|
|
298
|
+
case "opencode": {
|
|
299
|
+
// Both pi-mono and opencode resolve credentials in the same order:
|
|
300
|
+
// OPENROUTER → ANTHROPIC → OPENAI. Live-test against the matching
|
|
301
|
+
// provider's models endpoint.
|
|
302
|
+
if (env.OPENROUTER_API_KEY) return checkOpenRouter(env.OPENROUTER_API_KEY);
|
|
303
|
+
if (env.ANTHROPIC_API_KEY) return checkAnthropicApiKey(env.ANTHROPIC_API_KEY);
|
|
304
|
+
if (env.OPENAI_API_KEY) return checkOpenAiApiKey(env.OPENAI_API_KEY);
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
error:
|
|
308
|
+
"No usable credential found (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY).",
|
|
309
|
+
latency_ms: Date.now() - startedAt,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
case "devin": {
|
|
313
|
+
const apiKey = env.DEVIN_API_KEY;
|
|
314
|
+
if (!apiKey) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: "DEVIN_API_KEY is not set.",
|
|
318
|
+
latency_ms: Date.now() - startedAt,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const baseUrl = env.DEVIN_API_BASE_URL ?? "https://api.devin.ai";
|
|
322
|
+
const r = await timedFetch(`${baseUrl.replace(/\/+$/, "")}/v1/sessions?limit=1`, {
|
|
323
|
+
method: "GET",
|
|
324
|
+
headers: {
|
|
325
|
+
Authorization: `Bearer ${apiKey}`,
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
if (r.ok) return { ok: true, latency_ms: r.latency_ms };
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
|
|
333
|
+
latency_ms: r.latency_ms,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: `Unknown provider "${provider}". Supported: claude, claude-managed, codex, devin, opencode, pi.`,
|
|
340
|
+
latency_ms: Date.now() - startedAt,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return asLiveError(err, Date.now() - startedAt);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ─── Worker-side report composition ──────────────────────────────────────────
|
|
349
|
+
// Composes the JSON the worker POSTs to `PATCH /agents/:id` so the API can
|
|
350
|
+
// expose per-worker credential status without running any provider-specific
|
|
351
|
+
// code itself. See `AgentCredStatusSchema` in `src/types.ts` for the contract.
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Single switch for the opt-out env var. Both `credential-wait.ts` (boot) and
|
|
355
|
+
* `runner.ts` (post-task) honor this; when set, the worker performs no
|
|
356
|
+
* checks and POSTs nothing — the agent row's `cred_status` stays NULL and
|
|
357
|
+
* the dashboard surfaces "unreported".
|
|
358
|
+
*/
|
|
359
|
+
export function isCredCheckDisabled(env: NodeJS.ProcessEnv): boolean {
|
|
360
|
+
return env.CRED_CHECK_DISABLE === "1";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Run the presence check + (when ready) live test, and shape the result into
|
|
365
|
+
* the JSON contract the API stores. `kind` records the trigger — useful for
|
|
366
|
+
* ops debugging and for the dashboard to surface "last verified Xs ago."
|
|
367
|
+
*
|
|
368
|
+
* Both "boot" and "post_task" run the live test today. The cache-hit
|
|
369
|
+
* post-task path skips this function entirely (caller decides), so when this
|
|
370
|
+
* function is called we always do the full check.
|
|
371
|
+
*/
|
|
372
|
+
export async function buildCredStatusReport(
|
|
373
|
+
provider: string,
|
|
374
|
+
env: Record<string, string | undefined>,
|
|
375
|
+
opts: CredCheckOptions = {},
|
|
376
|
+
kind: AgentCredStatus["reportKind"],
|
|
377
|
+
): Promise<AgentCredStatus> {
|
|
378
|
+
const presence = checkProviderCredentials(provider, env, opts);
|
|
379
|
+
let liveTest: AgentCredStatus["liveTest"] = null;
|
|
380
|
+
if (presence.ready) {
|
|
381
|
+
const live = await validateProviderCredentials(provider);
|
|
382
|
+
liveTest = {
|
|
383
|
+
ok: live.ok,
|
|
384
|
+
error: live.error ?? null,
|
|
385
|
+
latency_ms: live.latency_ms,
|
|
386
|
+
testedAt: Date.now(),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
ready: presence.ready,
|
|
391
|
+
missing: presence.missing ?? [],
|
|
392
|
+
satisfiedBy: presence.satisfiedBy ?? null,
|
|
393
|
+
hint: presence.hint ?? null,
|
|
394
|
+
liveTest,
|
|
395
|
+
reportedAt: Date.now(),
|
|
396
|
+
reportKind: kind,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Fire-and-forget POST of a `cred_status` snapshot to the API. Used by the
|
|
402
|
+
* worker boot path (`runner.ts` after `awaitCredentials`) and the post-task
|
|
403
|
+
* cache-miss path (also in `runner.ts`). Failures are logged, not thrown —
|
|
404
|
+
* a stale dashboard is acceptable; blocking the worker is not.
|
|
405
|
+
*
|
|
406
|
+
* Posts to the existing `PUT /api/agents/:id/credential-status` endpoint
|
|
407
|
+
* which (per migration 055) now accepts an optional `cred_status` field.
|
|
408
|
+
* `ready` and `missing` are also included so legacy `agents.credentialMissing`
|
|
409
|
+
* and `agents.status='waiting_for_credentials'` keep tracking.
|
|
410
|
+
*/
|
|
411
|
+
export async function reportCredStatus(
|
|
412
|
+
apiUrl: string,
|
|
413
|
+
apiKey: string,
|
|
414
|
+
agentId: string,
|
|
415
|
+
credStatus: AgentCredStatus,
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
try {
|
|
418
|
+
await fetch(`${apiUrl}/api/agents/${encodeURIComponent(agentId)}/credential-status`, {
|
|
419
|
+
method: "PUT",
|
|
420
|
+
headers: {
|
|
421
|
+
Authorization: `Bearer ${apiKey}`,
|
|
422
|
+
"X-Agent-ID": agentId,
|
|
423
|
+
"Content-Type": "application/json",
|
|
424
|
+
},
|
|
425
|
+
body: JSON.stringify({
|
|
426
|
+
ready: credStatus.ready,
|
|
427
|
+
missing: credStatus.missing,
|
|
428
|
+
cred_status: credStatus,
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
} catch (err) {
|
|
432
|
+
console.warn(`[cred-status] POST failed (non-fatal): ${err}`);
|
|
433
|
+
}
|
|
434
|
+
}
|