@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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +1264 -46
  3. package/package.json +2 -2
  4. package/src/be/db.ts +563 -9
  5. package/src/be/memory/edges-store.ts +69 -0
  6. package/src/be/memory/providers/sqlite-store.ts +4 -0
  7. package/src/be/memory/raters/explicit-self.ts +22 -0
  8. package/src/be/memory/raters/implicit-citation.ts +44 -0
  9. package/src/be/memory/raters/llm-client.ts +172 -0
  10. package/src/be/memory/raters/llm-summarizer.ts +218 -0
  11. package/src/be/memory/raters/llm.ts +375 -0
  12. package/src/be/memory/raters/noop.ts +14 -0
  13. package/src/be/memory/raters/registry.ts +86 -0
  14. package/src/be/memory/raters/retrieval.ts +88 -0
  15. package/src/be/memory/raters/run-server-raters.ts +97 -0
  16. package/src/be/memory/raters/store.ts +228 -0
  17. package/src/be/memory/raters/types.ts +101 -0
  18. package/src/be/memory/reranker.ts +32 -2
  19. package/src/be/memory/retrieval-store.ts +116 -0
  20. package/src/be/memory/types.ts +3 -0
  21. package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
  22. package/src/be/migrations/052_memory_edges.sql +36 -0
  23. package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
  24. package/src/be/migrations/054_agent_harness_provider.sql +21 -0
  25. package/src/be/migrations/055_agent_cred_status.sql +15 -0
  26. package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
  27. package/src/be/migrations/057_inbox_item_state.sql +27 -0
  28. package/src/be/migrations/058_task_templates.sql +31 -0
  29. package/src/be/swarm-config-guard.ts +24 -0
  30. package/src/commands/credential-wait.ts +186 -0
  31. package/src/commands/provider-credentials.ts +434 -0
  32. package/src/commands/runner.ts +253 -21
  33. package/src/hooks/hook.ts +143 -66
  34. package/src/http/agents.ts +191 -1
  35. package/src/http/config.ts +11 -1
  36. package/src/http/core.ts +5 -0
  37. package/src/http/inbox-state.ts +89 -0
  38. package/src/http/index.ts +10 -0
  39. package/src/http/memory.ts +230 -1
  40. package/src/http/sessions.ts +86 -0
  41. package/src/http/status.ts +665 -0
  42. package/src/http/task-templates.ts +51 -0
  43. package/src/http/tasks.ts +85 -5
  44. package/src/http/users.ts +134 -0
  45. package/src/prompts/memories.ts +62 -0
  46. package/src/providers/claude-adapter.ts +22 -0
  47. package/src/providers/claude-managed-adapter.ts +24 -0
  48. package/src/providers/codex-adapter.ts +43 -1
  49. package/src/providers/devin-adapter.ts +18 -0
  50. package/src/providers/index.ts +7 -0
  51. package/src/providers/opencode-adapter.ts +60 -0
  52. package/src/providers/pi-mono-adapter.ts +71 -0
  53. package/src/providers/types.ts +34 -0
  54. package/src/server.ts +2 -0
  55. package/src/slack/handlers.ts +0 -1
  56. package/src/tests/agents-harness-provider.test.ts +333 -0
  57. package/src/tests/credential-check.test.ts +367 -0
  58. package/src/tests/credential-status-api.test.ts +223 -0
  59. package/src/tests/credential-status-routing.test.ts +150 -0
  60. package/src/tests/credential-wait.test.ts +282 -0
  61. package/src/tests/harness-provider-resolution.test.ts +242 -0
  62. package/src/tests/jira-sync.test.ts +1 -1
  63. package/src/tests/memory-edges.test.ts +722 -0
  64. package/src/tests/memory-rate-endpoint.test.ts +330 -0
  65. package/src/tests/memory-rate-tool.test.ts +252 -0
  66. package/src/tests/memory-rater-e2e.test.ts +578 -0
  67. package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
  68. package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
  69. package/src/tests/memory-rater-llm.test.ts +964 -0
  70. package/src/tests/memory-rater-store.test.ts +249 -0
  71. package/src/tests/memory-reranker.test.ts +161 -2
  72. package/src/tests/migration-runner-regressions.test.ts +17 -2
  73. package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
  74. package/src/tests/run-server-raters.test.ts +291 -0
  75. package/src/tests/sessions.test.ts +141 -0
  76. package/src/tests/status.test.ts +843 -0
  77. package/src/tests/stop-hook-task-resolution.test.ts +98 -0
  78. package/src/tests/template-recommendations.test.ts +148 -0
  79. package/src/tests/tool-annotations.test.ts +2 -2
  80. package/src/tests/use-dismissible-card.test.ts +140 -0
  81. package/src/tools/memory-rate.ts +166 -0
  82. package/src/tools/memory-search.ts +18 -0
  83. package/src/tools/store-progress.ts +37 -0
  84. package/src/tools/swarm-config/set-config.ts +17 -1
  85. package/src/tools/tool-config.ts +1 -0
  86. package/src/types.ts +122 -1
  87. package/src/utils/harness-provider.ts +32 -0
  88. 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
+ }