@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
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Post-listen backfill: embed scripts that are missing embeddings (e.g. after
|
|
3
|
-
* boot seeding with scriptEmbeddingMode: "skip")
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* boot seeding with scriptEmbeddingMode: "skip") AND re-embed scripts whose
|
|
4
|
+
* stored embedding has the wrong dimension (e.g. 1536d legacy rows vs current
|
|
5
|
+
* 512d). Runs once per boot, async/non-blocking, idempotent, no-op when clean.
|
|
6
6
|
*
|
|
7
7
|
* Mirrors the memory boot-reembed pattern (src/be/memory/boot-reembed.ts).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getDb } from "@/be/db";
|
|
11
11
|
import type { ScriptScope } from "@/types";
|
|
12
|
-
import { embedScript } from "./embeddings";
|
|
12
|
+
import { embeddingProvider, embedScript } from "./embeddings";
|
|
13
13
|
|
|
14
|
-
type
|
|
14
|
+
type ScriptRow = {
|
|
15
15
|
id: string;
|
|
16
16
|
name: string;
|
|
17
17
|
scope: ScriptScope;
|
|
@@ -31,35 +31,65 @@ type ScriptMissingEmbedding = {
|
|
|
31
31
|
updatedAt: string;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
function toScriptRecord(row: ScriptRow) {
|
|
35
|
+
return {
|
|
36
|
+
...row,
|
|
37
|
+
scopeId: row.scopeId ?? null,
|
|
38
|
+
isScratch: row.isScratch === 1,
|
|
39
|
+
typeChecked: row.typeChecked === 1,
|
|
40
|
+
createdByAgentId: row.createdByAgentId ?? null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export async function runBootReembedScripts(): Promise<void> {
|
|
35
45
|
const db = getDb();
|
|
46
|
+
const provider = embeddingProvider();
|
|
47
|
+
const expectedBytes = provider.dimensions * Float32Array.BYTES_PER_ELEMENT;
|
|
36
48
|
|
|
37
49
|
const missing = db
|
|
38
|
-
.prepare<
|
|
50
|
+
.prepare<ScriptRow, []>(
|
|
39
51
|
`SELECT s.* FROM scripts s
|
|
40
52
|
LEFT JOIN script_embeddings e ON e.scriptId = s.id
|
|
41
53
|
WHERE s.isScratch = 0 AND e.scriptId IS NULL`,
|
|
42
54
|
)
|
|
43
55
|
.all();
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
const wrongDim = db
|
|
58
|
+
.prepare<ScriptRow, []>(
|
|
59
|
+
`SELECT s.* FROM scripts s
|
|
60
|
+
JOIN script_embeddings e ON e.scriptId = s.id
|
|
61
|
+
WHERE s.isScratch = 0 AND length(e.embedding) != ${expectedBytes}`,
|
|
62
|
+
)
|
|
63
|
+
.all();
|
|
64
|
+
|
|
65
|
+
if (missing.length === 0 && wrongDim.length === 0) {
|
|
46
66
|
return;
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
console.log(`[boot-reembed-scripts] ${missing.length} scripts missing embeddings`);
|
|
71
|
+
}
|
|
72
|
+
if (wrongDim.length > 0) {
|
|
73
|
+
console.log(
|
|
74
|
+
`[boot-reembed-scripts] ${wrongDim.length} scripts with wrong-dimension embeddings (expected ${expectedBytes} bytes)`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Probe: verify the provider can actually generate embeddings
|
|
79
|
+
const probe = await provider.embed("test");
|
|
80
|
+
if (!probe) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[boot-reembed-scripts] skipped: no working embedding provider (missing OpenAI key?)`,
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
50
86
|
|
|
51
87
|
let embedded = 0;
|
|
52
88
|
let failed = 0;
|
|
53
89
|
|
|
54
|
-
for (const row of missing) {
|
|
90
|
+
for (const row of [...missing, ...wrongDim]) {
|
|
55
91
|
try {
|
|
56
|
-
await embedScript(
|
|
57
|
-
...row,
|
|
58
|
-
scopeId: row.scopeId ?? null,
|
|
59
|
-
isScratch: row.isScratch === 1,
|
|
60
|
-
typeChecked: row.typeChecked === 1,
|
|
61
|
-
createdByAgentId: row.createdByAgentId ?? null,
|
|
62
|
-
});
|
|
92
|
+
await embedScript(toScriptRecord(row));
|
|
63
93
|
embedded++;
|
|
64
94
|
} catch (err) {
|
|
65
95
|
failed++;
|
|
@@ -70,5 +100,15 @@ export async function runBootReembedScripts(): Promise<void> {
|
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
const afterWrongDim =
|
|
104
|
+
db
|
|
105
|
+
.prepare<{ count: number }, []>(
|
|
106
|
+
`SELECT COUNT(*) as count FROM script_embeddings
|
|
107
|
+
WHERE length(embedding) != ${expectedBytes}`,
|
|
108
|
+
)
|
|
109
|
+
.get()?.count ?? 0;
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
`[boot-reembed-scripts] complete: embedded=${embedded} failed=${failed} remaining_wrong_dim=${afterWrongDim}`,
|
|
113
|
+
);
|
|
74
114
|
}
|
|
@@ -42,7 +42,7 @@ export type ScriptSearchResult = {
|
|
|
42
42
|
|
|
43
43
|
let providerOverride: EmbeddingProvider | null = null;
|
|
44
44
|
|
|
45
|
-
function embeddingProvider(): EmbeddingProvider {
|
|
45
|
+
export function embeddingProvider(): EmbeddingProvider {
|
|
46
46
|
return providerOverride ?? getEmbeddingProvider();
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -82,6 +82,13 @@ export async function embedScript(script: ScriptRecord): Promise<void> {
|
|
|
82
82
|
const embedding = await provider.embed(text);
|
|
83
83
|
if (!embedding) return;
|
|
84
84
|
|
|
85
|
+
if (embedding.length !== provider.dimensions) {
|
|
86
|
+
console.error(
|
|
87
|
+
`[script-embed] dimension mismatch for "${script.name}": expected=${provider.dimensions} got=${embedding.length}, skipping`,
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
getDb()
|
|
86
93
|
.prepare(
|
|
87
94
|
`INSERT INTO script_embeddings (
|
|
@@ -204,20 +211,24 @@ export async function searchScripts(args: {
|
|
|
204
211
|
const candidates = candidateRows(args.scope, args.scopeId);
|
|
205
212
|
if (candidates.length === 0) return lexicalFallback(args);
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
const results: ScriptSearchResult[] = [];
|
|
215
|
+
for (const row of candidates) {
|
|
216
|
+
const stored = deserializeEmbedding(row.embedding);
|
|
217
|
+
if (stored.length !== queryEmbedding.length) continue;
|
|
218
|
+
const script = rowToScript(row);
|
|
219
|
+
const semanticScore = cosineSimilarity(queryEmbedding, stored);
|
|
220
|
+
const bonus = nameMatchBonus(script, args.query);
|
|
221
|
+
results.push({
|
|
222
|
+
script,
|
|
223
|
+
score: 0.7 * semanticScore + 0.3 * bonus,
|
|
224
|
+
semanticScore,
|
|
225
|
+
nameMatchBonus: bonus,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (results.length === 0) return lexicalFallback(args);
|
|
230
|
+
|
|
231
|
+
return results.sort((a, b) => b.score - a.score).slice(0, args.limit ?? 10);
|
|
221
232
|
}
|
|
222
233
|
|
|
223
234
|
export async function reembedAllScripts(): Promise<void> {
|
|
@@ -29,6 +29,21 @@ import { scrubSecrets } from "../utils/secret-scrubber";
|
|
|
29
29
|
|
|
30
30
|
export type SupportedProvider = "claude" | "claude-managed" | "codex" | "devin" | "opencode" | "pi";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when the pi harness should use the AWS SDK Bedrock path: either an
|
|
34
|
+
* explicit `BEDROCK_AUTH_MODE=sdk`, or — preserving prefix-inference semantics —
|
|
35
|
+
* `BEDROCK_AUTH_MODE` absent with a `MODEL_OVERRIDE=amazon-bedrock/*` selection.
|
|
36
|
+
* Single source of truth for the gate so the live-test arm and the worker
|
|
37
|
+
* reconcile loop agree with `checkPiMonoCredentials`.
|
|
38
|
+
*/
|
|
39
|
+
export function isBedrockSdkMode(env: Record<string, string | undefined>): boolean {
|
|
40
|
+
const mode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
41
|
+
return (
|
|
42
|
+
mode === "sdk" ||
|
|
43
|
+
(mode === undefined && Boolean(env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/")))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Static documentation of which env vars each provider considers when running
|
|
34
49
|
* `checkCredentials`. Used by the dashboard to render hints before any worker
|
|
@@ -243,7 +258,7 @@ function parseCodexOAuthAccess(blob: string | undefined): string | null {
|
|
|
243
258
|
* | `codex` | `~/.codex/auth.json` (file) → `CODEX_OAUTH` (env OAuth) → `OPENAI_API_KEY` | OpenAI `/v1/models` (api-key path only) |
|
|
244
259
|
* | `opencode` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` (pi-style) | matching provider's `/v1/models` |
|
|
245
260
|
* | `pi` | `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` | matching provider's `/v1/models` |
|
|
246
|
-
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (
|
|
261
|
+
* | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (real check is the worker-side Bedrock enumeration) |
|
|
247
262
|
* | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v1/sessions?limit=1` |
|
|
248
263
|
*
|
|
249
264
|
* Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
|
|
@@ -302,20 +317,14 @@ export async function validateProviderCredentials(provider: string): Promise<Liv
|
|
|
302
317
|
}
|
|
303
318
|
case "pi":
|
|
304
319
|
case "opencode": {
|
|
305
|
-
// For the pi Bedrock path, the real credential check is the
|
|
306
|
-
// `ListFoundationModels`
|
|
307
|
-
// `pi` dynamic-import arm) already ran.
|
|
308
|
-
// in `buildCredStatusReport` — the live-test is a
|
|
309
|
-
// so we never issue a second AWS SDK call here
|
|
310
|
-
// SDK into the wrong binary or make slow IMDS
|
|
311
|
-
//
|
|
312
|
-
|
|
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
|
-
) {
|
|
320
|
+
// For the pi Bedrock path, the real credential check is the AWS SDK
|
|
321
|
+
// enumeration (`ListFoundationModels` + `ListInferenceProfiles`) that
|
|
322
|
+
// `checkProviderCredentials` (the `pi` dynamic-import arm) already ran.
|
|
323
|
+
// That result is already in `buildCredStatusReport` — the live-test is a
|
|
324
|
+
// pass-through / no-op so we never issue a second AWS SDK call here
|
|
325
|
+
// (which would drag the SDK into the wrong binary or make slow IMDS
|
|
326
|
+
// calls on non-EC2 hosts).
|
|
327
|
+
if (provider === "pi" && isBedrockSdkMode(env)) {
|
|
319
328
|
return presenceCheckOk();
|
|
320
329
|
}
|
|
321
330
|
// Both pi-mono and opencode resolve credentials in the same order:
|
|
@@ -408,6 +417,18 @@ export async function buildCredStatusReport(
|
|
|
408
417
|
testedAt: Date.now(),
|
|
409
418
|
};
|
|
410
419
|
}
|
|
420
|
+
// Include the Bedrock enumeration block when the pi probe ran in Bedrock SDK
|
|
421
|
+
// mode (bedrockRegion is only set by checkPiMonoCredentials in that branch).
|
|
422
|
+
const bedrock: AgentCredStatus["bedrock"] =
|
|
423
|
+
presence.bedrockRegion !== undefined
|
|
424
|
+
? {
|
|
425
|
+
region: presence.bedrockRegion,
|
|
426
|
+
probedAt: Date.now(),
|
|
427
|
+
ready: presence.ready,
|
|
428
|
+
models: presence.bedrockModels ?? [],
|
|
429
|
+
error: presence.ready ? undefined : (presence.hint ?? undefined),
|
|
430
|
+
}
|
|
431
|
+
: null;
|
|
411
432
|
return {
|
|
412
433
|
ready: presence.ready,
|
|
413
434
|
missing: presence.missing ?? [],
|
|
@@ -417,6 +438,7 @@ export async function buildCredStatusReport(
|
|
|
417
438
|
latestModel: null,
|
|
418
439
|
reportedAt: Date.now(),
|
|
419
440
|
reportKind: kind,
|
|
441
|
+
bedrock,
|
|
420
442
|
};
|
|
421
443
|
}
|
|
422
444
|
|
package/src/commands/runner.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
isRateLimitMessage,
|
|
46
46
|
MAX_RATE_LIMIT_RESET_MS,
|
|
47
47
|
parseRateLimitResetTime,
|
|
48
|
+
type RateLimitWindowTelemetry,
|
|
48
49
|
resolveCodexCreditsExhaustedCooldownMs,
|
|
49
50
|
} from "../utils/error-tracker.ts";
|
|
50
51
|
import { resolveHarnessProvider } from "../utils/harness-provider.ts";
|
|
@@ -60,6 +61,7 @@ import { resolveClaudeMdPath, syncProfileFilesToServer } from "./profile-sync.ts
|
|
|
60
61
|
import {
|
|
61
62
|
buildCredStatusReport,
|
|
62
63
|
buildLatestModelReport,
|
|
64
|
+
isBedrockSdkMode,
|
|
63
65
|
isCredCheckDisabled,
|
|
64
66
|
reportCredStatus,
|
|
65
67
|
reportLatestModel,
|
|
@@ -1124,6 +1126,35 @@ async function reportKeyRateLimit(
|
|
|
1124
1126
|
}
|
|
1125
1127
|
}
|
|
1126
1128
|
|
|
1129
|
+
async function reportKeyRateLimitWindows(
|
|
1130
|
+
apiUrl: string,
|
|
1131
|
+
apiKey: string,
|
|
1132
|
+
keyType: string,
|
|
1133
|
+
keySuffix: string,
|
|
1134
|
+
keyIndex: number,
|
|
1135
|
+
windows: RateLimitWindowTelemetry,
|
|
1136
|
+
): Promise<void> {
|
|
1137
|
+
if (Object.keys(windows).length === 0) return;
|
|
1138
|
+
try {
|
|
1139
|
+
await fetch(`${apiUrl}/api/keys/report-rate-limit-windows`, {
|
|
1140
|
+
method: "POST",
|
|
1141
|
+
headers: {
|
|
1142
|
+
"Content-Type": "application/json",
|
|
1143
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1144
|
+
},
|
|
1145
|
+
body: JSON.stringify({
|
|
1146
|
+
keyType,
|
|
1147
|
+
keySuffix,
|
|
1148
|
+
keyIndex,
|
|
1149
|
+
windows,
|
|
1150
|
+
}),
|
|
1151
|
+
});
|
|
1152
|
+
console.log(`[credentials] Reported rate-limit windows for key ...${keySuffix}`);
|
|
1153
|
+
} catch {
|
|
1154
|
+
// Non-blocking
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1127
1158
|
/** Clear a stale rate-limit record after a successful task (fire-and-forget) */
|
|
1128
1159
|
async function reportKeyClearRateLimit(
|
|
1129
1160
|
apiUrl: string,
|
|
@@ -3405,6 +3436,17 @@ async function checkCompletedProcesses(
|
|
|
3405
3436
|
rateLimitedUntil,
|
|
3406
3437
|
).catch(() => {});
|
|
3407
3438
|
}
|
|
3439
|
+
|
|
3440
|
+
if (credentialInfo && result.rateLimitWindows) {
|
|
3441
|
+
reportKeyRateLimitWindows(
|
|
3442
|
+
apiConfig.apiUrl,
|
|
3443
|
+
apiConfig.apiKey,
|
|
3444
|
+
credentialInfo.keyType,
|
|
3445
|
+
credentialInfo.keySuffix,
|
|
3446
|
+
credentialInfo.keyIndex,
|
|
3447
|
+
result.rateLimitWindows,
|
|
3448
|
+
).catch(() => {});
|
|
3449
|
+
}
|
|
3408
3450
|
let bridgeDiagnostics: Awaited<ReturnType<typeof getBridgeFailureDiagnostics>> | undefined;
|
|
3409
3451
|
if (result.exitCode !== 0 && harnessProvider === "claude" && workingDir) {
|
|
3410
3452
|
bridgeDiagnostics = await getBridgeFailureDiagnostics(workingDir);
|
|
@@ -3848,6 +3890,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3848
3890
|
let lastHarnessReconcileAt = 0;
|
|
3849
3891
|
const HARNESS_RECONCILE_INTERVAL_MS = 10_000;
|
|
3850
3892
|
|
|
3893
|
+
// Throttle for the periodic Bedrock model-enumeration refresh. The credential
|
|
3894
|
+
// report below only re-runs on a harness_provider change (boot + provider
|
|
3895
|
+
// swap), so enabling Bedrock access after boot would otherwise never reach the
|
|
3896
|
+
// picker. This timer re-runs the enumeration on a fixed interval, decoupled
|
|
3897
|
+
// from the harness-change gate, so the UI stays accurate. 5 minutes keeps it
|
|
3898
|
+
// cheap (one bounded AWS enumeration per tick) while still surfacing newly
|
|
3899
|
+
// granted access within a few minutes.
|
|
3900
|
+
let lastBedrockRefreshAt = 0;
|
|
3901
|
+
const BEDROCK_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
3902
|
+
|
|
3851
3903
|
// Create API config for ping/close
|
|
3852
3904
|
const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
|
|
3853
3905
|
|
|
@@ -4572,6 +4624,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4572
4624
|
.catch((err) =>
|
|
4573
4625
|
console.warn(`[${role}] cred_status post_task report failed (non-fatal): ${err}`),
|
|
4574
4626
|
);
|
|
4627
|
+
} else if (
|
|
4628
|
+
currentHarness === "pi" &&
|
|
4629
|
+
isBedrockSdkMode(process.env) &&
|
|
4630
|
+
Date.now() - lastBedrockRefreshAt > BEDROCK_REFRESH_INTERVAL_MS
|
|
4631
|
+
) {
|
|
4632
|
+
// Bedrock enumeration drifts independently of the harness_provider:
|
|
4633
|
+
// access granted (or revoked) in the AWS console after boot won't flip
|
|
4634
|
+
// the provider, so the harness-change gate above never fires. Re-run the
|
|
4635
|
+
// enumeration on the throttled interval so the picker reflects the live
|
|
4636
|
+
// account state. One bounded AWS round-trip per tick.
|
|
4637
|
+
lastBedrockRefreshAt = Date.now();
|
|
4638
|
+
buildCredStatusReport(currentHarness, process.env, {}, "post_task")
|
|
4639
|
+
.then((snap) => reportCredStatus(apiUrl, apiKey, agentId, snap))
|
|
4640
|
+
.catch((err) =>
|
|
4641
|
+
console.warn(`[${role}] bedrock enumeration refresh failed (non-fatal): ${err}`),
|
|
4642
|
+
);
|
|
4575
4643
|
}
|
|
4576
4644
|
}
|
|
4577
4645
|
|
package/src/http/agents.ts
CHANGED
package/src/http/api-keys.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getKeyCostSummary,
|
|
7
7
|
getKeyStatuses,
|
|
8
8
|
markKeyRateLimited,
|
|
9
|
+
recordKeyRateLimitWindows,
|
|
9
10
|
recordKeyUsage,
|
|
10
11
|
setApiKeyName,
|
|
11
12
|
} from "../be/db";
|
|
@@ -58,6 +59,37 @@ const reportRateLimit = route({
|
|
|
58
59
|
auth: { apiKey: true },
|
|
59
60
|
});
|
|
60
61
|
|
|
62
|
+
const rateLimitWindowSchema = z.object({
|
|
63
|
+
status: z.string(),
|
|
64
|
+
utilization: z.number().optional(),
|
|
65
|
+
resetsAt: z.number().optional(),
|
|
66
|
+
isUsingOverage: z.boolean().optional(),
|
|
67
|
+
surpassedThreshold: z.number().optional(),
|
|
68
|
+
lastSeenAt: z.string().datetime(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const reportRateLimitWindows = route({
|
|
72
|
+
method: "post",
|
|
73
|
+
path: "/api/keys/report-rate-limit-windows",
|
|
74
|
+
pattern: ["api", "keys", "report-rate-limit-windows"],
|
|
75
|
+
summary: "Record provider-emitted rate-limit window telemetry for an API key",
|
|
76
|
+
tags: ["API Keys"],
|
|
77
|
+
body: z.object({
|
|
78
|
+
keyType: z.string(),
|
|
79
|
+
keySuffix: z.string().min(1).max(10),
|
|
80
|
+
keyIndex: z.number().int().min(0),
|
|
81
|
+
windows: z.record(z.string(), rateLimitWindowSchema),
|
|
82
|
+
scope: z.string().optional(),
|
|
83
|
+
scopeId: z.string().optional(),
|
|
84
|
+
}),
|
|
85
|
+
responses: {
|
|
86
|
+
200: { description: "Rate-limit window telemetry recorded" },
|
|
87
|
+
400: { description: "Validation error" },
|
|
88
|
+
401: { description: "Unauthorized" },
|
|
89
|
+
},
|
|
90
|
+
auth: { apiKey: true },
|
|
91
|
+
});
|
|
92
|
+
|
|
61
93
|
const getAvailable = route({
|
|
62
94
|
method: "get",
|
|
63
95
|
path: "/api/keys/available",
|
|
@@ -196,6 +228,25 @@ export async function handleApiKeys(
|
|
|
196
228
|
return true;
|
|
197
229
|
}
|
|
198
230
|
|
|
231
|
+
// POST /api/keys/report-rate-limit-windows
|
|
232
|
+
if (reportRateLimitWindows.match(req.method, pathSegments)) {
|
|
233
|
+
const parsed = await reportRateLimitWindows.parse(req, res, pathSegments, queryParams);
|
|
234
|
+
if (!parsed) return true;
|
|
235
|
+
|
|
236
|
+
const { keyType, keySuffix, keyIndex, windows, scope, scopeId } = parsed.body;
|
|
237
|
+
try {
|
|
238
|
+
recordKeyRateLimitWindows(keyType, keySuffix, keyIndex, windows, scope, scopeId ?? null);
|
|
239
|
+
json(res, { success: true, message: `Rate-limit windows recorded for ...${keySuffix}` });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
jsonError(
|
|
242
|
+
res,
|
|
243
|
+
err instanceof Error ? err.message : "Failed to record rate-limit windows",
|
|
244
|
+
500,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
199
250
|
// GET /api/keys/available
|
|
200
251
|
if (getAvailable.match(req.method, pathSegments)) {
|
|
201
252
|
const parsed = await getAvailable.parse(req, res, pathSegments, queryParams);
|
package/src/http/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
reservedKeyError,
|
|
15
15
|
validateConfigValue,
|
|
16
16
|
} from "../be/swarm-config-guard";
|
|
17
|
+
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
17
18
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
18
19
|
import { route } from "./route-def";
|
|
19
20
|
import { json, jsonError } from "./utils";
|
|
@@ -152,7 +153,15 @@ export async function handleConfig(
|
|
|
152
153
|
parsed.query.agentId || undefined,
|
|
153
154
|
parsed.query.repoId || undefined,
|
|
154
155
|
);
|
|
155
|
-
|
|
156
|
+
const result = includeSecrets ? configs : maskSecrets(configs);
|
|
157
|
+
if (includeSecrets) {
|
|
158
|
+
for (const c of result) {
|
|
159
|
+
if (c.isSecret && c.value) {
|
|
160
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
json(res, { configs: result });
|
|
156
165
|
return true;
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -199,8 +208,11 @@ export async function handleConfig(
|
|
|
199
208
|
jsonError(res, "Config not found", 404);
|
|
200
209
|
return true;
|
|
201
210
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
211
|
+
const singleResult = includeSecrets ? config : maskSecrets([config])[0]!;
|
|
212
|
+
if (includeSecrets && singleResult.isSecret && singleResult.value) {
|
|
213
|
+
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
214
|
+
}
|
|
215
|
+
json(res, singleResult);
|
|
204
216
|
return true;
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -212,7 +224,15 @@ export async function handleConfig(
|
|
|
212
224
|
scope: parsed.query.scope || undefined,
|
|
213
225
|
scopeId: parsed.query.scopeId || undefined,
|
|
214
226
|
});
|
|
215
|
-
|
|
227
|
+
const listResult = includeSecrets ? configs : maskSecrets(configs);
|
|
228
|
+
if (includeSecrets) {
|
|
229
|
+
for (const c of listResult) {
|
|
230
|
+
if (c.isSecret && c.value) {
|
|
231
|
+
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
json(res, { configs: listResult });
|
|
216
236
|
return true;
|
|
217
237
|
}
|
|
218
238
|
|
package/src/http/index.ts
CHANGED
|
@@ -576,6 +576,15 @@ httpServer
|
|
|
576
576
|
.catch((err) => {
|
|
577
577
|
console.error("[boot-reembed-scripts] startup backfill failed (non-fatal):", err);
|
|
578
578
|
});
|
|
579
|
+
|
|
580
|
+
// One-time scrub: retroactively redact any session_logs rows containing
|
|
581
|
+
// sensitive patterns that pre-date the defense-in-depth scrub layer.
|
|
582
|
+
// Idempotent, tracked via seed_state.
|
|
583
|
+
import("../be/boot-scrub-logs")
|
|
584
|
+
.then(({ runBootScrubLogs }) => runBootScrubLogs())
|
|
585
|
+
.catch((err) => {
|
|
586
|
+
console.error("[boot-scrub-logs] startup scrub failed (non-fatal):", err);
|
|
587
|
+
});
|
|
579
588
|
})
|
|
580
589
|
.on("error", (err) => {
|
|
581
590
|
console.error("HTTP Server Error:", err);
|
|
@@ -158,6 +158,27 @@ When you finish a task:
|
|
|
158
158
|
- **Failure**: Use \`store-progress\` with status: "failed" and failureReason: "<what went wrong>"
|
|
159
159
|
|
|
160
160
|
Always include meaningful output - the lead agent reviews your work.
|
|
161
|
+
|
|
162
|
+
#### Credential Hygiene
|
|
163
|
+
|
|
164
|
+
When you retrieve secrets via \`get-config\` (with \`includeSecrets: true\`), **never pass secret values directly on a command line or embed them in tool output**. Command arguments are logged.
|
|
165
|
+
|
|
166
|
+
**Safe pattern:** Write the secret to a temporary \`.env\` file, then source it:
|
|
167
|
+
\`\`\`bash
|
|
168
|
+
# Write to temp file (not logged)
|
|
169
|
+
echo "MY_TOKEN=<value>" > /tmp/.task-env && source /tmp/.task-env
|
|
170
|
+
# Use the variable (value stays out of logs)
|
|
171
|
+
curl -H "Authorization: Bearer $MY_TOKEN" https://api.example.com
|
|
172
|
+
rm /tmp/.task-env
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
**Unsafe pattern (NEVER do this):**
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
# The literal secret appears in the logged command
|
|
178
|
+
curl -H "Authorization: Bearer lin_oauth_abc123..." https://api.example.com
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
The same applies to \`store-progress\` output — never include raw secret values in progress text, output, or failure reasons.
|
|
161
182
|
`,
|
|
162
183
|
variables: [],
|
|
163
184
|
category: "system",
|
|
@@ -1024,6 +1024,7 @@ export class CodexSession implements ProviderSession {
|
|
|
1024
1024
|
isError: true,
|
|
1025
1025
|
failureReason: terminalError.message,
|
|
1026
1026
|
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1027
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows(),
|
|
1027
1028
|
});
|
|
1028
1029
|
return;
|
|
1029
1030
|
}
|
|
@@ -1045,6 +1046,7 @@ export class CodexSession implements ProviderSession {
|
|
|
1045
1046
|
isError,
|
|
1046
1047
|
failureReason: terminalError?.message,
|
|
1047
1048
|
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1049
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows(),
|
|
1048
1050
|
});
|
|
1049
1051
|
} catch (err) {
|
|
1050
1052
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1059,6 +1061,7 @@ export class CodexSession implements ProviderSession {
|
|
|
1059
1061
|
isError: true,
|
|
1060
1062
|
failureReason: message,
|
|
1061
1063
|
rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
|
|
1064
|
+
rateLimitWindows: this.errorTracker.getRateLimitWindows(),
|
|
1062
1065
|
});
|
|
1063
1066
|
} finally {
|
|
1064
1067
|
// Session-end summarization. Pure addition for codex — no behavior to
|
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
type PackageJson = { version?: unknown };
|
|
4
|
+
|
|
5
|
+
type ReadPkgVersionOptions = {
|
|
6
|
+
requirePackageJson?: (specifier: string) => PackageJson;
|
|
7
|
+
spawn?: typeof spawnSync;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const cliVersionCommands: Record<string, { command: string; args: string[] }> = {
|
|
11
|
+
"@earendil-works/pi-coding-agent": { command: "pi", args: ["--version"] },
|
|
12
|
+
"@opencode-ai/sdk": { command: "opencode", args: ["--version"] },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function normalizeVersion(value: unknown): string | undefined {
|
|
16
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseCliVersion(output: string): string | undefined {
|
|
20
|
+
return output.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readCliVersion(packageName: string, spawn: typeof spawnSync): string | undefined {
|
|
24
|
+
const command = cliVersionCommands[packageName];
|
|
25
|
+
if (!command) return undefined;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = spawn(command.command, command.args, {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
return parseCliVersion(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readPkgVersion(
|
|
39
|
+
packageName: string,
|
|
40
|
+
{
|
|
41
|
+
requirePackageJson = (specifier) => require(specifier) as PackageJson,
|
|
42
|
+
spawn = spawnSync,
|
|
43
|
+
}: ReadPkgVersionOptions = {},
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const cliVersion = readCliVersion(packageName, spawn);
|
|
46
|
+
if (cliVersion) return cliVersion;
|
|
47
|
+
|
|
2
48
|
try {
|
|
3
|
-
|
|
49
|
+
const version = normalizeVersion(requirePackageJson(`${packageName}/package.json`).version);
|
|
50
|
+
if (version) return version;
|
|
4
51
|
} catch {
|
|
5
52
|
return undefined;
|
|
6
53
|
}
|