@desplega.ai/agent-swarm 1.95.0 → 1.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/openapi.json +136 -1
- package/package.json +1 -1
- package/src/be/boot-scrub-logs.ts +76 -0
- package/src/be/db.ts +73 -10
- package/src/be/migrations/095_api_key_rate_limit_windows.sql +5 -0
- package/src/be/modelsdev-cache.json +89422 -85636
- package/src/be/scripts/boot-reembed.ts +57 -17
- package/src/be/scripts/embeddings.ts +26 -15
- package/src/commands/provider-credentials.ts +37 -15
- package/src/commands/runner.ts +68 -0
- package/src/http/agents.ts +1 -0
- package/src/http/api-keys.ts +51 -0
- package/src/http/config.ts +24 -4
- package/src/http/index.ts +9 -0
- package/src/prompts/session-templates.ts +21 -0
- package/src/providers/claude-adapter.ts +1 -0
- package/src/providers/codex-adapter.ts +3 -0
- package/src/providers/harness-version.ts +49 -2
- package/src/providers/pi-mono-adapter.ts +113 -19
- package/src/providers/types.ts +37 -9
- package/src/tests/api-key-tracking.test.ts +62 -0
- package/src/tests/bedrock-model-groups.test.ts +135 -0
- package/src/tests/credential-check.test.ts +361 -12
- package/src/tests/harness-version.test.ts +47 -0
- package/src/tests/opencode-adapter.test.ts +7 -6
- package/src/tests/providers/pi-cost.test.ts +7 -6
- package/src/tests/rate-limit-event.test.ts +37 -0
- package/src/tests/scripts-boot-reembed.test.ts +61 -2
- package/src/tests/scripts-embeddings.test.ts +27 -0
- package/src/tests/secret-scrubber.test.ts +73 -1
- package/src/tools/swarm-config/get-config.ts +9 -1
- package/src/tools/swarm-config/list-config.ts +8 -0
- package/src/types.ts +21 -0
- package/src/utils/error-tracker.ts +59 -0
- package/src/utils/secret-scrubber.ts +33 -12
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ Check [our templates](https://templates.agent-swarm.dev) for a quick start.
|
|
|
127
127
|
- **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
|
|
128
128
|
- **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
|
|
129
129
|
- **Durable script workflows** — launch background script runs, inspect their journals, and track them from the dashboard when a one-shot `script-run` is too small. [Guide →](https://docs.agent-swarm.dev/docs/guides/script-workflow-runs)
|
|
130
|
-
- **Harness & LLM agnostic** — run with Claude Code, Claude Bridge, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, raw LLMs, or opencode. Tasks, schedules, and workflow agent-task nodes can use portable `modelTier` intent (`smol`, `regular`, `smart`, `ultra`) and resolve it per worker/provider at run time. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
|
|
130
|
+
- **Harness & LLM agnostic** — run with Claude Code, Claude Bridge, OpenAI Codex, pi-mono (Anthropic, OpenRouter, or Amazon Bedrock), Devin, Claude Managed Agents, raw LLMs, or opencode. Tasks, schedules, and workflow agent-task nodes can use portable `modelTier` intent (`smol`, `regular`, `smart`, `ultra`) and resolve it per worker/provider at run time. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
|
|
131
131
|
- **Follow-up continuity across all harnesses** — child tasks inherit a bounded prior-task context preamble built from the task chain, so continuity survives restarts and works the same across every provider. [Task lifecycle →](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle)
|
|
132
132
|
- **Skills & MCP servers** — reusable procedural knowledge, bundled skill reference files, and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
|
|
133
133
|
- **External tool-router access** — the `x` command and `swarm_x` MCP tool let humans and agents execute approved third-party routes such as Composio without baking bespoke MCP servers first. [CLI →](https://docs.agent-swarm.dev/docs/reference/cli) · [Composio →](https://docs.agent-swarm.dev/docs/integrations/composio)
|
|
@@ -140,7 +140,7 @@ Check [our templates](https://templates.agent-swarm.dev) for a quick start.
|
|
|
140
140
|
|
|
141
141
|
Need help? Contact us at [contact@desplega.sh](mailto:contact@desplega.sh).
|
|
142
142
|
|
|
143
|
-
**Prerequisites:** [Docker](https://docker.com) and a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) OAuth token (`claude setup-token`).
|
|
143
|
+
**Prerequisites:** [Docker](https://docker.com) and at least one supported harness credential. The default quick start assumes a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) OAuth token (`claude setup-token`), but pi-mono / Bedrock, Codex, Devin, and other provider setups are also supported.
|
|
144
144
|
|
|
145
145
|
The fastest way is the onboarding wizard — it collects credentials, picks presets, and generates a working `docker-compose.yml`:
|
|
146
146
|
|
|
@@ -154,7 +154,7 @@ Prefer manual setup? Clone and run with Docker Compose:
|
|
|
154
154
|
git clone https://github.com/desplega-ai/agent-swarm.git
|
|
155
155
|
cd agent-swarm
|
|
156
156
|
cp .env.docker.example .env
|
|
157
|
-
# edit .env — set API_KEY
|
|
157
|
+
# edit .env — set API_KEY plus the credential for your chosen harness (for example CLAUDE_CODE_OAUTH_TOKEN)
|
|
158
158
|
docker compose -f docker-compose.example.yml --env-file .env up -d
|
|
159
159
|
```
|
|
160
160
|
|
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.97.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": [
|
|
@@ -981,6 +981,51 @@
|
|
|
981
981
|
"post_task"
|
|
982
982
|
],
|
|
983
983
|
"default": "boot"
|
|
984
|
+
},
|
|
985
|
+
"bedrock": {
|
|
986
|
+
"type": [
|
|
987
|
+
"object",
|
|
988
|
+
"null"
|
|
989
|
+
],
|
|
990
|
+
"properties": {
|
|
991
|
+
"region": {
|
|
992
|
+
"type": "string"
|
|
993
|
+
},
|
|
994
|
+
"probedAt": {
|
|
995
|
+
"type": "number"
|
|
996
|
+
},
|
|
997
|
+
"ready": {
|
|
998
|
+
"type": "boolean"
|
|
999
|
+
},
|
|
1000
|
+
"models": {
|
|
1001
|
+
"type": "array",
|
|
1002
|
+
"items": {
|
|
1003
|
+
"type": "object",
|
|
1004
|
+
"properties": {
|
|
1005
|
+
"id": {
|
|
1006
|
+
"type": "string"
|
|
1007
|
+
},
|
|
1008
|
+
"name": {
|
|
1009
|
+
"type": "string"
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
"required": [
|
|
1013
|
+
"id",
|
|
1014
|
+
"name"
|
|
1015
|
+
]
|
|
1016
|
+
},
|
|
1017
|
+
"default": []
|
|
1018
|
+
},
|
|
1019
|
+
"error": {
|
|
1020
|
+
"type": "string"
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
"default": null,
|
|
1024
|
+
"required": [
|
|
1025
|
+
"region",
|
|
1026
|
+
"probedAt",
|
|
1027
|
+
"ready"
|
|
1028
|
+
]
|
|
984
1029
|
}
|
|
985
1030
|
},
|
|
986
1031
|
"required": [
|
|
@@ -2538,6 +2583,96 @@
|
|
|
2538
2583
|
}
|
|
2539
2584
|
}
|
|
2540
2585
|
},
|
|
2586
|
+
"/api/keys/report-rate-limit-windows": {
|
|
2587
|
+
"post": {
|
|
2588
|
+
"summary": "Record provider-emitted rate-limit window telemetry for an API key",
|
|
2589
|
+
"tags": [
|
|
2590
|
+
"API Keys"
|
|
2591
|
+
],
|
|
2592
|
+
"security": [
|
|
2593
|
+
{
|
|
2594
|
+
"bearerAuth": []
|
|
2595
|
+
}
|
|
2596
|
+
],
|
|
2597
|
+
"requestBody": {
|
|
2598
|
+
"content": {
|
|
2599
|
+
"application/json": {
|
|
2600
|
+
"schema": {
|
|
2601
|
+
"type": "object",
|
|
2602
|
+
"properties": {
|
|
2603
|
+
"keyType": {
|
|
2604
|
+
"type": "string"
|
|
2605
|
+
},
|
|
2606
|
+
"keySuffix": {
|
|
2607
|
+
"type": "string",
|
|
2608
|
+
"minLength": 1,
|
|
2609
|
+
"maxLength": 10
|
|
2610
|
+
},
|
|
2611
|
+
"keyIndex": {
|
|
2612
|
+
"type": "integer",
|
|
2613
|
+
"minimum": 0
|
|
2614
|
+
},
|
|
2615
|
+
"windows": {
|
|
2616
|
+
"type": "object",
|
|
2617
|
+
"additionalProperties": {
|
|
2618
|
+
"type": "object",
|
|
2619
|
+
"properties": {
|
|
2620
|
+
"status": {
|
|
2621
|
+
"type": "string"
|
|
2622
|
+
},
|
|
2623
|
+
"utilization": {
|
|
2624
|
+
"type": "number"
|
|
2625
|
+
},
|
|
2626
|
+
"resetsAt": {
|
|
2627
|
+
"type": "number"
|
|
2628
|
+
},
|
|
2629
|
+
"isUsingOverage": {
|
|
2630
|
+
"type": "boolean"
|
|
2631
|
+
},
|
|
2632
|
+
"surpassedThreshold": {
|
|
2633
|
+
"type": "number"
|
|
2634
|
+
},
|
|
2635
|
+
"lastSeenAt": {
|
|
2636
|
+
"type": "string",
|
|
2637
|
+
"format": "date-time"
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
"required": [
|
|
2641
|
+
"status",
|
|
2642
|
+
"lastSeenAt"
|
|
2643
|
+
]
|
|
2644
|
+
}
|
|
2645
|
+
},
|
|
2646
|
+
"scope": {
|
|
2647
|
+
"type": "string"
|
|
2648
|
+
},
|
|
2649
|
+
"scopeId": {
|
|
2650
|
+
"type": "string"
|
|
2651
|
+
}
|
|
2652
|
+
},
|
|
2653
|
+
"required": [
|
|
2654
|
+
"keyType",
|
|
2655
|
+
"keySuffix",
|
|
2656
|
+
"keyIndex",
|
|
2657
|
+
"windows"
|
|
2658
|
+
]
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
"responses": {
|
|
2664
|
+
"200": {
|
|
2665
|
+
"description": "Rate-limit window telemetry recorded"
|
|
2666
|
+
},
|
|
2667
|
+
"400": {
|
|
2668
|
+
"description": "Validation error"
|
|
2669
|
+
},
|
|
2670
|
+
"401": {
|
|
2671
|
+
"description": "Unauthorized"
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
},
|
|
2541
2676
|
"/api/keys/available": {
|
|
2542
2677
|
"get": {
|
|
2543
2678
|
"summary": "Get available (non-rate-limited) key indices for a credential type",
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time boot-scrub: retroactively sanitize session_logs rows that contain
|
|
3
|
+
* sensitive patterns (structural regex matches) which pre-date the defense-in-
|
|
4
|
+
* depth scrub added to createSessionLogs / task persistence paths.
|
|
5
|
+
*
|
|
6
|
+
* Idempotent: already-scrubbed rows are no-ops (scrubSecrets is idempotent).
|
|
7
|
+
* Uses seed_state to avoid re-scanning on subsequent boots.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
11
|
+
import { getDb } from "./db";
|
|
12
|
+
|
|
13
|
+
const SCRUB_KEY = "boot-scrub-logs-v2";
|
|
14
|
+
const BATCH_SIZE = 500;
|
|
15
|
+
|
|
16
|
+
export async function runBootScrubLogs(): Promise<void> {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
|
|
19
|
+
const done = db
|
|
20
|
+
.prepare<{ key: string }, [string, string]>(
|
|
21
|
+
"SELECT key FROM seed_state WHERE kind = ? AND key = ?",
|
|
22
|
+
)
|
|
23
|
+
.get("maintenance", SCRUB_KEY);
|
|
24
|
+
|
|
25
|
+
if (done) return;
|
|
26
|
+
|
|
27
|
+
// ESCAPE '!' makes ! the escape character so !_ matches a literal underscore
|
|
28
|
+
// instead of the LIKE single-char wildcard. Without this, '%npm_%' matches
|
|
29
|
+
// any row containing "npm" + any char (e.g. "npm install"), drowning real
|
|
30
|
+
// token rows when a LIMIT is applied.
|
|
31
|
+
const rows = db
|
|
32
|
+
.prepare<{ id: string; content: string }, []>(
|
|
33
|
+
`SELECT id, content FROM session_logs
|
|
34
|
+
WHERE content LIKE '%lin!_oauth!_%' ESCAPE '!'
|
|
35
|
+
OR content LIKE '%lin!_api!_%' ESCAPE '!'
|
|
36
|
+
OR content LIKE '%npm!_%' ESCAPE '!'
|
|
37
|
+
OR content LIKE '%ATATT%'`,
|
|
38
|
+
)
|
|
39
|
+
.all();
|
|
40
|
+
|
|
41
|
+
if (rows.length === 0) {
|
|
42
|
+
markDone(db);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`[boot-scrub-logs] starting: ${rows.length} candidate rows`);
|
|
47
|
+
|
|
48
|
+
const update = db.prepare("UPDATE session_logs SET content = ? WHERE id = ?");
|
|
49
|
+
let scrubbed = 0;
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
52
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
53
|
+
const tx = db.transaction(() => {
|
|
54
|
+
for (const row of batch) {
|
|
55
|
+
const cleaned = scrubSecrets(row.content);
|
|
56
|
+
if (cleaned !== row.content) {
|
|
57
|
+
update.run(cleaned, row.id);
|
|
58
|
+
scrubbed++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
tx();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
markDone(db);
|
|
66
|
+
console.log(`[boot-scrub-logs] complete: scanned=${rows.length} scrubbed=${scrubbed}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function markDone(db: ReturnType<typeof getDb>) {
|
|
70
|
+
db.run(
|
|
71
|
+
`INSERT INTO seed_state (kind, key, seededHash, seededAt)
|
|
72
|
+
VALUES ('maintenance', ?, 'done', datetime('now'))
|
|
73
|
+
ON CONFLICT (kind, key) DO UPDATE SET seededHash = 'done', seededAt = datetime('now')`,
|
|
74
|
+
[SCRUB_KEY],
|
|
75
|
+
);
|
|
76
|
+
}
|
package/src/be/db.ts
CHANGED
|
@@ -102,6 +102,7 @@ import type {
|
|
|
102
102
|
} from "../types";
|
|
103
103
|
import { FollowUpConfigSchema, isTerminalTaskStatus } from "../types";
|
|
104
104
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
105
|
+
import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
|
|
105
106
|
import { getCurrentRequestUserId } from "../utils/request-auth-context";
|
|
106
107
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
107
108
|
import { decryptSecret, encryptSecret, getEncryptionKey, resolveEncryptionKey } from "./crypto";
|
|
@@ -2100,7 +2101,7 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2100
2101
|
if (!row) return null;
|
|
2101
2102
|
|
|
2102
2103
|
if (output) {
|
|
2103
|
-
row = taskQueries.setOutput().get(output, id);
|
|
2104
|
+
row = taskQueries.setOutput().get(scrubSecrets(output), id);
|
|
2104
2105
|
}
|
|
2105
2106
|
|
|
2106
2107
|
if (row && oldTask) {
|
|
@@ -2141,7 +2142,8 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
2141
2142
|
}
|
|
2142
2143
|
|
|
2143
2144
|
const finishedAt = new Date().toISOString();
|
|
2144
|
-
const
|
|
2145
|
+
const scrubbedReason = scrubSecrets(reason);
|
|
2146
|
+
const row = taskQueries.setFailure().get(scrubbedReason, finishedAt, id);
|
|
2145
2147
|
if (row && oldTask) {
|
|
2146
2148
|
try {
|
|
2147
2149
|
createLogEntry({
|
|
@@ -2150,7 +2152,7 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
2150
2152
|
agentId: row.agentId ?? undefined,
|
|
2151
2153
|
oldValue: oldTask.status,
|
|
2152
2154
|
newValue: "failed",
|
|
2153
|
-
metadata: { reason },
|
|
2155
|
+
metadata: { reason: scrubbedReason },
|
|
2154
2156
|
});
|
|
2155
2157
|
} catch {}
|
|
2156
2158
|
try {
|
|
@@ -2496,21 +2498,22 @@ export function deleteTask(id: string): boolean {
|
|
|
2496
2498
|
}
|
|
2497
2499
|
|
|
2498
2500
|
export function updateTaskProgress(id: string, progress: string): AgentTask | null {
|
|
2499
|
-
const
|
|
2501
|
+
const scrubbedProgress = scrubSecrets(progress);
|
|
2502
|
+
const row = taskQueries.setProgress().get(scrubbedProgress, id);
|
|
2500
2503
|
if (row) {
|
|
2501
2504
|
try {
|
|
2502
2505
|
createLogEntry({
|
|
2503
2506
|
eventType: "task_progress",
|
|
2504
2507
|
taskId: id,
|
|
2505
2508
|
agentId: row.agentId ?? undefined,
|
|
2506
|
-
newValue:
|
|
2509
|
+
newValue: scrubbedProgress,
|
|
2507
2510
|
});
|
|
2508
2511
|
} catch {}
|
|
2509
2512
|
try {
|
|
2510
2513
|
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
2511
2514
|
workflowEventBus.emit("task.progress", {
|
|
2512
2515
|
taskId: id,
|
|
2513
|
-
progress,
|
|
2516
|
+
progress: scrubbedProgress,
|
|
2514
2517
|
agentId: row.agentId,
|
|
2515
2518
|
});
|
|
2516
2519
|
});
|
|
@@ -2791,6 +2794,7 @@ export function createLogEntry(entry: {
|
|
|
2791
2794
|
metadata?: Record<string, unknown>;
|
|
2792
2795
|
}): AgentLog {
|
|
2793
2796
|
const id = crypto.randomUUID();
|
|
2797
|
+
const metaJson = entry.metadata ? JSON.stringify(entry.metadata) : null;
|
|
2794
2798
|
const row = logQueries
|
|
2795
2799
|
.insert()
|
|
2796
2800
|
.get(
|
|
@@ -2799,8 +2803,8 @@ export function createLogEntry(entry: {
|
|
|
2799
2803
|
entry.agentId ?? null,
|
|
2800
2804
|
entry.taskId ?? null,
|
|
2801
2805
|
entry.oldValue ?? null,
|
|
2802
|
-
entry.newValue
|
|
2803
|
-
|
|
2806
|
+
entry.newValue ? scrubSecrets(entry.newValue) : null,
|
|
2807
|
+
metaJson ? scrubSecrets(metaJson) : null,
|
|
2804
2808
|
);
|
|
2805
2809
|
if (!row) throw new Error("Failed to create log entry");
|
|
2806
2810
|
return rowToAgentLog(row);
|
|
@@ -9981,10 +9985,31 @@ export interface ApiKeyStatus {
|
|
|
9981
9985
|
name: string | null;
|
|
9982
9986
|
/** Auto-derived harness provider (claude/pi/codex) — see deriveProviderFromKeyType. */
|
|
9983
9987
|
provider: string;
|
|
9988
|
+
/** Latest provider-emitted rate-limit window snapshots, keyed by window type. */
|
|
9989
|
+
rateLimitWindows: RateLimitWindowTelemetry;
|
|
9984
9990
|
createdAt: string;
|
|
9985
9991
|
updatedAt: string;
|
|
9986
9992
|
}
|
|
9987
9993
|
|
|
9994
|
+
type ApiKeyStatusRow = Omit<ApiKeyStatus, "rateLimitWindows"> & { rateLimitWindows: string | null };
|
|
9995
|
+
|
|
9996
|
+
function parseRateLimitWindowsJson(value: string | null | undefined): RateLimitWindowTelemetry {
|
|
9997
|
+
if (!value) return {};
|
|
9998
|
+
try {
|
|
9999
|
+
const parsed = JSON.parse(value) as unknown;
|
|
10000
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10001
|
+
return parsed as RateLimitWindowTelemetry;
|
|
10002
|
+
}
|
|
10003
|
+
} catch {
|
|
10004
|
+
// Ignore malformed historical values; telemetry is best-effort.
|
|
10005
|
+
}
|
|
10006
|
+
return {};
|
|
10007
|
+
}
|
|
10008
|
+
|
|
10009
|
+
function rowToApiKeyStatus(row: ApiKeyStatusRow): ApiKeyStatus {
|
|
10010
|
+
return { ...row, rateLimitWindows: parseRateLimitWindowsJson(row.rateLimitWindows) };
|
|
10011
|
+
}
|
|
10012
|
+
|
|
9988
10013
|
/**
|
|
9989
10014
|
* Get available (non-rate-limited) key indices for a credential type.
|
|
9990
10015
|
* Automatically clears expired rate limits before returning.
|
|
@@ -10103,6 +10128,43 @@ export function markKeyRateLimited(
|
|
|
10103
10128
|
);
|
|
10104
10129
|
}
|
|
10105
10130
|
|
|
10131
|
+
export function recordKeyRateLimitWindows(
|
|
10132
|
+
keyType: string,
|
|
10133
|
+
keySuffix: string,
|
|
10134
|
+
keyIndex: number,
|
|
10135
|
+
windows: RateLimitWindowTelemetry,
|
|
10136
|
+
scope = "global",
|
|
10137
|
+
scopeId: string | null = null,
|
|
10138
|
+
): void {
|
|
10139
|
+
if (Object.keys(windows).length === 0) return;
|
|
10140
|
+
|
|
10141
|
+
const now = new Date().toISOString();
|
|
10142
|
+
const effectiveScopeId = scopeId ?? "";
|
|
10143
|
+
const provider = deriveProviderFromKeyType(keyType);
|
|
10144
|
+
const db = getDb();
|
|
10145
|
+
const existing = db
|
|
10146
|
+
.prepare<{ rateLimitWindows: string | null }, [string, string, string, string]>(
|
|
10147
|
+
`SELECT rateLimitWindows FROM api_key_status
|
|
10148
|
+
WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?`,
|
|
10149
|
+
)
|
|
10150
|
+
.get(keyType, keySuffix, scope, effectiveScopeId);
|
|
10151
|
+
const serialized = JSON.stringify({
|
|
10152
|
+
...parseRateLimitWindowsJson(existing?.rateLimitWindows),
|
|
10153
|
+
...windows,
|
|
10154
|
+
});
|
|
10155
|
+
|
|
10156
|
+
db.prepare(
|
|
10157
|
+
`INSERT INTO api_key_status (keyType, keySuffix, keyIndex, scope, scopeId, rateLimitWindows, provider, updatedAt)
|
|
10158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
10159
|
+
ON CONFLICT(keyType, keySuffix, scope, scopeId)
|
|
10160
|
+
DO UPDATE SET
|
|
10161
|
+
rateLimitWindows = excluded.rateLimitWindows,
|
|
10162
|
+
keyIndex = excluded.keyIndex,
|
|
10163
|
+
provider = excluded.provider,
|
|
10164
|
+
updatedAt = excluded.updatedAt`,
|
|
10165
|
+
).run(keyType, keySuffix, keyIndex, scope, effectiveScopeId, serialized, provider, now);
|
|
10166
|
+
}
|
|
10167
|
+
|
|
10106
10168
|
/**
|
|
10107
10169
|
* Set or clear the human-friendly `name` label on a pooled credential.
|
|
10108
10170
|
* Identified by the natural key (keyType + keySuffix + scope + scopeId).
|
|
@@ -10174,8 +10236,9 @@ export function getKeyStatuses(
|
|
|
10174
10236
|
|
|
10175
10237
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
10176
10238
|
return db
|
|
10177
|
-
.prepare<
|
|
10178
|
-
.all(...params)
|
|
10239
|
+
.prepare<ApiKeyStatusRow, string[]>(`SELECT * FROM api_key_status ${where} ORDER BY keyIndex`)
|
|
10240
|
+
.all(...params)
|
|
10241
|
+
.map(rowToApiKeyStatus);
|
|
10179
10242
|
}
|
|
10180
10243
|
|
|
10181
10244
|
export interface KeyCostSummary {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Persist provider-emitted rate-limit window telemetry on credential rows.
|
|
2
|
+
-- Shape is JSON keyed by provider window type, e.g.
|
|
3
|
+
-- {"five_hour":{"status":"allowed_warning","utilization":0.82,"resetsAt":1781334000,"isUsingOverage":false,"surpassedThreshold":0.75,"lastSeenAt":"..."}}
|
|
4
|
+
|
|
5
|
+
ALTER TABLE api_key_status ADD COLUMN rateLimitWindows TEXT NOT NULL DEFAULT '{}';
|