@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.
Files changed (36) hide show
  1. package/README.md +3 -3
  2. package/openapi.json +136 -1
  3. package/package.json +1 -1
  4. package/src/be/boot-scrub-logs.ts +76 -0
  5. package/src/be/db.ts +73 -10
  6. package/src/be/migrations/095_api_key_rate_limit_windows.sql +5 -0
  7. package/src/be/modelsdev-cache.json +89422 -85636
  8. package/src/be/scripts/boot-reembed.ts +57 -17
  9. package/src/be/scripts/embeddings.ts +26 -15
  10. package/src/commands/provider-credentials.ts +37 -15
  11. package/src/commands/runner.ts +68 -0
  12. package/src/http/agents.ts +1 -0
  13. package/src/http/api-keys.ts +51 -0
  14. package/src/http/config.ts +24 -4
  15. package/src/http/index.ts +9 -0
  16. package/src/prompts/session-templates.ts +21 -0
  17. package/src/providers/claude-adapter.ts +1 -0
  18. package/src/providers/codex-adapter.ts +3 -0
  19. package/src/providers/harness-version.ts +49 -2
  20. package/src/providers/pi-mono-adapter.ts +113 -19
  21. package/src/providers/types.ts +37 -9
  22. package/src/tests/api-key-tracking.test.ts +62 -0
  23. package/src/tests/bedrock-model-groups.test.ts +135 -0
  24. package/src/tests/credential-check.test.ts +361 -12
  25. package/src/tests/harness-version.test.ts +47 -0
  26. package/src/tests/opencode-adapter.test.ts +7 -6
  27. package/src/tests/providers/pi-cost.test.ts +7 -6
  28. package/src/tests/rate-limit-event.test.ts +37 -0
  29. package/src/tests/scripts-boot-reembed.test.ts +61 -2
  30. package/src/tests/scripts-embeddings.test.ts +27 -0
  31. package/src/tests/secret-scrubber.test.ts +73 -1
  32. package/src/tools/swarm-config/get-config.ts +9 -1
  33. package/src/tools/swarm-config/list-config.ts +8 -0
  34. package/src/types.ts +21 -0
  35. package/src/utils/error-tracker.ts +59 -0
  36. package/src/utils/secret-scrubber.ts +33 -12
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, getDb, initDb } from "../be/db";
4
+ import { serializeEmbedding } from "../be/embedding";
4
5
  import type { EmbeddingProvider } from "../be/memory/types";
5
6
  import { runBootReembedScripts } from "../be/scripts/boot-reembed";
6
7
  import { upsertScriptByName } from "../be/scripts/db";
@@ -98,7 +99,8 @@ describe("boot-reembed-scripts", () => {
98
99
  provider.reset();
99
100
  await runBootReembedScripts();
100
101
  expect(embeddingCount(result.script.id)).toBe(1);
101
- expect(provider.calls).toHaveLength(1);
102
+ // +1 for the provider probe call ("test") that verifies the provider works
103
+ expect(provider.calls).toHaveLength(2);
102
104
  });
103
105
 
104
106
  test("no-ops when all scripts already have embeddings", async () => {
@@ -157,7 +159,64 @@ describe("boot-reembed-scripts", () => {
157
159
 
158
160
  provider.reset();
159
161
  await runBootReembedScripts();
160
- expect(provider.calls).toHaveLength(1);
162
+ // +1 for the provider probe call
163
+ expect(provider.calls).toHaveLength(2);
161
164
  expect(embeddingCount(withoutEmbed.script.id)).toBe(1);
162
165
  });
166
+
167
+ test("re-embeds scripts with wrong-dimension embeddings", async () => {
168
+ const result = await upsertScriptByName({
169
+ name: "wrong-dim",
170
+ scope: "global",
171
+ source: source("wrong-dim"),
172
+ description: "Script with legacy 1536d embedding",
173
+ intent: "Dimension fix test",
174
+ signatureJson,
175
+ });
176
+ expect(embeddingCount(result.script.id)).toBe(1);
177
+
178
+ // Overwrite with a wrong-dimension (1536d) embedding to simulate legacy data
179
+ const wrongDimVector = new Float32Array(1536).fill(0.1);
180
+ getDb().run("UPDATE script_embeddings SET embedding = ? WHERE scriptId = ?", [
181
+ serializeEmbedding(wrongDimVector),
182
+ result.script.id,
183
+ ]);
184
+
185
+ // Verify the wrong dim is stored
186
+ const stored = getDb()
187
+ .prepare<{ len: number }, [string]>(
188
+ "SELECT length(embedding) as len FROM script_embeddings WHERE scriptId = ?",
189
+ )
190
+ .get(result.script.id);
191
+ expect(stored?.len).toBe(1536 * 4);
192
+
193
+ provider.reset();
194
+ await runBootReembedScripts();
195
+ // +1 for the provider probe call
196
+ expect(provider.calls).toHaveLength(2);
197
+
198
+ // Should now have correct-dimension embedding
199
+ const fixed = getDb()
200
+ .prepare<{ len: number }, [string]>(
201
+ "SELECT length(embedding) as len FROM script_embeddings WHERE scriptId = ?",
202
+ )
203
+ .get(result.script.id);
204
+ expect(fixed?.len).toBe(5 * 4); // provider.dimensions = 5
205
+ });
206
+
207
+ test("no-ops when all scripts have correct-dimension embeddings", async () => {
208
+ await upsertScriptByName({
209
+ name: "correct-dim",
210
+ scope: "global",
211
+ source: source("correct"),
212
+ description: "Already correct",
213
+ intent: "No-op test",
214
+ signatureJson,
215
+ });
216
+ expect(totalEmbeddingCount()).toBe(1);
217
+
218
+ provider.reset();
219
+ await runBootReembedScripts();
220
+ expect(provider.calls).toHaveLength(0);
221
+ });
163
222
  });
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, getDb, initDb } from "../be/db";
4
+ import { serializeEmbedding } from "../be/embedding";
4
5
  import type { EmbeddingProvider } from "../be/memory/types";
5
6
  import { getScript, upsertScriptByName } from "../be/scripts/db";
6
7
  import {
@@ -378,4 +379,30 @@ describe("script embeddings", () => {
378
379
  expect(getScript({ name: "delete-embedding", scope: "agent", scopeId: "agent-1" })).toBeNull();
379
380
  expect(embeddingCount(created.script.id)).toBe(0);
380
381
  });
382
+
383
+ test("search skips wrong-dimension embeddings instead of throwing", async () => {
384
+ const good = await upsertFixture({
385
+ name: "good-dim",
386
+ description: "Linear issue triage helper",
387
+ });
388
+ const bad = await upsertFixture({
389
+ name: "bad-dim",
390
+ description: "Linear ticket router",
391
+ });
392
+ expect(embeddingCount(good.script.id)).toBe(1);
393
+ expect(embeddingCount(bad.script.id)).toBe(1);
394
+
395
+ // Overwrite bad-dim's embedding with a wrong-dimension vector (1536d)
396
+ const wrongDimVector = new Float32Array(1536).fill(0.1);
397
+ getDb().run("UPDATE script_embeddings SET embedding = ? WHERE scriptId = ?", [
398
+ serializeEmbedding(wrongDimVector),
399
+ bad.script.id,
400
+ ]);
401
+
402
+ provider.reset();
403
+ const results = await searchScripts({ query: "issue triage", scopeId: "agent-1", limit: 10 });
404
+ // Should not throw, and should only return the good-dim script
405
+ expect(results.map((r) => r.script.name)).toContain("good-dim");
406
+ expect(results.map((r) => r.script.name)).not.toContain("bad-dim");
407
+ });
381
408
  });
@@ -1,5 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { refreshSecretScrubberCache, scrubObject, scrubSecrets } from "../utils/secret-scrubber";
2
+ import {
3
+ clearVolatileSecretsForTesting,
4
+ refreshSecretScrubberCache,
5
+ registerVolatileSecret,
6
+ scrubObject,
7
+ scrubSecrets,
8
+ } from "../utils/secret-scrubber";
3
9
 
4
10
  // Snapshot/restore process.env between tests so env-derived cache entries
5
11
  // don't leak across cases.
@@ -221,6 +227,52 @@ describe("scrubSecrets — regex patterns", () => {
221
227
 
222
228
  expect(out).toBe("OTEL_EXPORTER_OTLP_HEADERS=[REDACTED:signoz_ingestion_key]");
223
229
  });
230
+
231
+ test("redacts Linear OAuth tokens", () => {
232
+ const out = scrubSecrets("Authorization: Bearer lin_oauth_test123abcdef end");
233
+ expect(out).toContain("[REDACTED:linear_oauth]");
234
+ expect(out).not.toContain("lin_oauth_test123");
235
+ });
236
+
237
+ test("redacts Linear API keys", () => {
238
+ const out = scrubSecrets("key=lin_api_test123abcdef tail");
239
+ expect(out).toContain("[REDACTED:linear_api]");
240
+ expect(out).not.toContain("lin_api_test123");
241
+ });
242
+
243
+ test("redacts npm tokens", () => {
244
+ const out = scrubSecrets("npm=npm_abcdefghijklmnopqrstuvwxyz01234");
245
+ expect(out).toContain("[REDACTED:npm_token]");
246
+ expect(out).not.toContain("npm_abcdefghijklmnopqr");
247
+ });
248
+
249
+ test("redacts Atlassian/Jira API tokens", () => {
250
+ const out = scrubSecrets("jira=ATATT3xFfGF0abcdefghijklmnopqrstuvwxyz");
251
+ expect(out).toContain("[REDACTED:atlassian_token]");
252
+ expect(out).not.toContain("ATATT3xFfGF0");
253
+ });
254
+
255
+ test("redacts tokens adjacent to JSON escape sequences (\\n)", () => {
256
+ const content = "data\\nlin_oauth_test123abcdef\\nmore";
257
+ const out = scrubSecrets(content);
258
+ expect(out).toContain("[REDACTED:linear_oauth]");
259
+ expect(out).not.toContain("lin_oauth_test123");
260
+ });
261
+
262
+ test("redacts tokens adjacent to JSON escape sequences (\\t, \\r)", () => {
263
+ const outT = scrubSecrets("field\\tlin_oauth_test123abcdef");
264
+ expect(outT).toContain("[REDACTED:linear_oauth]");
265
+ const outR = scrubSecrets("line\\rlin_oauth_test123abcdef");
266
+ expect(outR).toContain("[REDACTED:linear_oauth]");
267
+ });
268
+
269
+ test("redacts tokens in double-encoded JSON with escape sequences", () => {
270
+ const inner = JSON.stringify({ access_token: "lin_oauth_test123abcdef", scope: "read" });
271
+ const content = `{"output":${JSON.stringify(inner)}}`;
272
+ const out = scrubSecrets(content);
273
+ expect(out).toContain("[REDACTED:linear_oauth]");
274
+ expect(out).not.toContain("lin_oauth_test123");
275
+ });
224
276
  });
225
277
 
226
278
  describe("scrubSecrets — does not over-scrub", () => {
@@ -300,3 +352,23 @@ describe("scrubObject", () => {
300
352
  expect(scrubObject(value)).toEqual({ a: "ok", self: "[Circular]" });
301
353
  });
302
354
  });
355
+
356
+ describe("registerVolatileSecret", () => {
357
+ afterEach(() => {
358
+ clearVolatileSecretsForTesting();
359
+ });
360
+
361
+ test("scrubs a runtime-registered volatile secret", () => {
362
+ const secret = "volatile_runtime_token_1234567890abcdef";
363
+ registerVolatileSecret(secret, "RUNTIME_TOKEN");
364
+ const out = scrubSecrets(`key=${secret}`);
365
+ expect(out).toBe("key=[REDACTED:RUNTIME_TOKEN]");
366
+ expect(out).not.toContain(secret);
367
+ });
368
+
369
+ test("ignores values shorter than the minimum length", () => {
370
+ registerVolatileSecret("short", "TOO_SHORT");
371
+ const out = scrubSecrets("contains short somewhere");
372
+ expect(out).toBe("contains short somewhere");
373
+ });
374
+ });
@@ -3,6 +3,7 @@ import * as z from "zod";
3
3
  import { getResolvedConfig, maskSecrets } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
  import { SwarmConfigSchema } from "@/types";
6
+ import { registerVolatileSecret } from "@/utils/secret-scrubber";
6
7
 
7
8
  export const registerGetConfigTool = (server: McpServer) => {
8
9
  createToolRegistrar(server)(
@@ -10,7 +11,7 @@ export const registerGetConfigTool = (server: McpServer) => {
10
11
  {
11
12
  title: "Get Config",
12
13
  description:
13
- "Get resolved configuration values with scope resolution (repo > agent > global). Returns one entry per unique key with the most-specific scope winning. Use includeSecrets=true to see secret values.",
14
+ "Get resolved configuration values with scope resolution (repo > agent > global). Returns one entry per unique key with the most-specific scope winning. Use includeSecrets=true to see secret values. IMPORTANT: never pass returned secret values directly on a command line — write them to a temp .env file and source it instead, so the literal value stays out of logged commands.",
14
15
  annotations: { readOnlyHint: true },
15
16
 
16
17
  inputSchema: z.object({
@@ -62,6 +63,13 @@ export const registerGetConfigTool = (server: McpServer) => {
62
63
  }
63
64
 
64
65
  const result = includeSecrets ? configs : maskSecrets(configs);
66
+ if (includeSecrets) {
67
+ for (const c of result) {
68
+ if (c.isSecret && c.value) {
69
+ registerVolatileSecret(c.value, `config:${c.key}`);
70
+ }
71
+ }
72
+ }
65
73
  const count = result.length;
66
74
 
67
75
  const configList =
@@ -3,6 +3,7 @@ import * as z from "zod";
3
3
  import { getSwarmConfigs, maskSecrets } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
  import { SwarmConfigSchema, SwarmConfigScopeSchema } from "@/types";
6
+ import { registerVolatileSecret } from "@/utils/secret-scrubber";
6
7
 
7
8
  export const registerListConfigTool = (server: McpServer) => {
8
9
  createToolRegistrar(server)(
@@ -53,6 +54,13 @@ export const registerListConfigTool = (server: McpServer) => {
53
54
  });
54
55
 
55
56
  const result = includeSecrets ? configs : maskSecrets(configs);
57
+ if (includeSecrets) {
58
+ for (const c of result) {
59
+ if (c.isSecret && c.value) {
60
+ registerVolatileSecret(c.value, `config:${c.key}`);
61
+ }
62
+ }
63
+ }
56
64
  const count = result.length;
57
65
 
58
66
  const configList =
package/src/types.ts CHANGED
@@ -553,6 +553,25 @@ export const AgentLatestModelSchema = z.object({
553
553
  });
554
554
  export type AgentLatestModel = z.infer<typeof AgentLatestModelSchema>;
555
555
 
556
+ /**
557
+ * Worker-reported Bedrock enumeration block. Only present when the pi harness
558
+ * is in Bedrock SDK mode (`BEDROCK_AUTH_MODE=sdk` or
559
+ * `MODEL_OVERRIDE=amazon-bedrock/*`). Rides inside `cred_status` JSON (no new
560
+ * DB column). `models` is the intersection of the models invocable by this
561
+ * account/region (on-demand/ACTIVE foundation models ∪ inference profiles) with
562
+ * the set the pi-ai Converse harness can actually drive — Converse-incompatible
563
+ * entries (e.g. OpenAI models listed in the account) are excluded. An empty
564
+ * `region` means Bedrock mode with `AWS_REGION` unset (no region fabricated).
565
+ */
566
+ export const AgentBedrockStatusSchema = z.object({
567
+ region: z.string(),
568
+ probedAt: z.number(), // unix ms
569
+ ready: z.boolean(),
570
+ models: z.array(z.object({ id: z.string(), name: z.string() })).default([]),
571
+ error: z.string().optional(),
572
+ });
573
+ export type AgentBedrockStatus = z.infer<typeof AgentBedrockStatusSchema>;
574
+
556
575
  export const AgentCredStatusSchema = z.object({
557
576
  ready: z.boolean(),
558
577
  missing: z.array(z.string()).default([]),
@@ -565,6 +584,8 @@ export const AgentCredStatusSchema = z.object({
565
584
  latestModel: AgentLatestModelSchema.nullable().default(null),
566
585
  reportedAt: z.number(), // unix ms
567
586
  reportKind: z.enum(["boot", "post_task"]).default("boot"),
587
+ /** Pi-mono Bedrock enumeration block — null when not in Bedrock mode. */
588
+ bedrock: AgentBedrockStatusSchema.nullable().default(null),
568
589
  });
569
590
  export type AgentCredStatus = z.infer<typeof AgentCredStatusSchema>;
570
591
 
@@ -10,6 +10,54 @@ export interface ErrorSignal {
10
10
  timestamp: string;
11
11
  }
12
12
 
13
+ export interface RateLimitWindowInfo {
14
+ status: string;
15
+ utilization?: number;
16
+ resetsAt?: number;
17
+ isUsingOverage?: boolean;
18
+ surpassedThreshold?: number;
19
+ lastSeenAt: string;
20
+ }
21
+
22
+ export type RateLimitWindowTelemetry = Record<string, RateLimitWindowInfo>;
23
+
24
+ export function parseRateLimitWindowTelemetry(
25
+ json: Record<string, unknown>,
26
+ lastSeenAt = new Date().toISOString(),
27
+ ): { rateLimitType: string; info: RateLimitWindowInfo } | null {
28
+ try {
29
+ if (json.type !== "rate_limit_event") return null;
30
+ const rawInfo = json.rate_limit_info;
31
+ if (!rawInfo || typeof rawInfo !== "object") return null;
32
+
33
+ const info = rawInfo as Record<string, unknown>;
34
+ if (typeof info.status !== "string" || info.status.length === 0) return null;
35
+ if (typeof info.rateLimitType !== "string" || info.rateLimitType.length === 0) return null;
36
+
37
+ const window: RateLimitWindowInfo = {
38
+ status: info.status,
39
+ lastSeenAt,
40
+ };
41
+
42
+ if (typeof info.utilization === "number" && Number.isFinite(info.utilization)) {
43
+ window.utilization = info.utilization;
44
+ }
45
+ if (typeof info.resetsAt === "number" && Number.isFinite(info.resetsAt) && info.resetsAt > 0) {
46
+ window.resetsAt = info.resetsAt;
47
+ }
48
+ if (typeof info.isUsingOverage === "boolean") {
49
+ window.isUsingOverage = info.isUsingOverage;
50
+ }
51
+ if (typeof info.surpassedThreshold === "number" && Number.isFinite(info.surpassedThreshold)) {
52
+ window.surpassedThreshold = info.surpassedThreshold;
53
+ }
54
+
55
+ return { rateLimitType: info.rateLimitType, info: window };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
13
61
  /**
14
62
  * Maximum cooldown horizon for a rate-limit reset. A weekly OAuth limit resets
15
63
  * up to ~7 days out, so the cap must be at least that or a weekly-limited key
@@ -85,6 +133,7 @@ export class SessionErrorTracker {
85
133
  private errors: ErrorSignal[] = [];
86
134
  /** Stashed reset time (ms) from the last rejected rate_limit_event in this session. */
87
135
  private rateLimitResetAtMs: number | undefined;
136
+ private rateLimitWindows: RateLimitWindowTelemetry = {};
88
137
 
89
138
  /** Record an error from an assistant message with message.error field */
90
139
  addApiError(errorCategory: string, message: string): void {
@@ -139,6 +188,11 @@ export class SessionErrorTracker {
139
188
  const info = json.rate_limit_info as Record<string, unknown> | undefined;
140
189
  if (!info) return;
141
190
 
191
+ const telemetry = parseRateLimitWindowTelemetry(json);
192
+ if (telemetry) {
193
+ this.rateLimitWindows[telemetry.rateLimitType] = telemetry.info;
194
+ }
195
+
142
196
  if (info.status !== "rejected") return;
143
197
 
144
198
  const resetsAtSec = info.resetsAt;
@@ -185,6 +239,11 @@ export class SessionErrorTracker {
185
239
  return new Date(this.rateLimitResetAtMs).toISOString();
186
240
  }
187
241
 
242
+ getRateLimitWindows(): RateLimitWindowTelemetry | undefined {
243
+ if (Object.keys(this.rateLimitWindows).length === 0) return undefined;
244
+ return { ...this.rateLimitWindows };
245
+ }
246
+
188
247
  hasErrors(): boolean {
189
248
  return this.errors.length > 0;
190
249
  }
@@ -93,42 +93,63 @@ const MIN_VALUE_LENGTH = 12;
93
93
  * Order matters when one pattern is a prefix of another (e.g. `sk-ant-` must
94
94
  * match before the more general `sk-`).
95
95
  */
96
+
97
+ // Leading word boundary that also matches after JSON escape sequences (\n, \t,
98
+ // \r, etc.) where the trailing char is alphanumeric and defeats standard \b.
99
+ const TB = String.raw`(?:(?<=\\[nrtbfu0])|(?<!\w))`;
100
+
96
101
  const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
97
102
  // GitHub fine-grained PATs
98
103
  { name: "github_pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
99
104
  // GitHub classic/OAuth tokens (ghp_, gho_, ghu_, ghs_, ghr_)
100
- { name: "github_token", re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
105
+ { name: "github_token", re: new RegExp(String.raw`${TB}gh[pousr]_[A-Za-z0-9]{20,}\b`, "g") },
101
106
  // GitLab personal access tokens
102
- { name: "gitlab_pat", re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
107
+ { name: "gitlab_pat", re: new RegExp(String.raw`${TB}glpat-[A-Za-z0-9_-]{20,}\b`, "g") },
103
108
  // Anthropic API keys (must match before the generic sk- rule below)
104
- { name: "anthropic_key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
109
+ { name: "anthropic_key", re: new RegExp(String.raw`${TB}sk-ant-[A-Za-z0-9_-]{20,}\b`, "g") },
105
110
  // OpenAI project keys
106
- { name: "openai_proj_key", re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g },
111
+ { name: "openai_proj_key", re: new RegExp(String.raw`${TB}sk-proj-[A-Za-z0-9_-]{20,}\b`, "g") },
107
112
  // OpenRouter keys
108
- { name: "openrouter_key", re: /\bsk-or-(?:v1-)?[A-Za-z0-9_-]{20,}\b/g },
113
+ {
114
+ name: "openrouter_key",
115
+ re: new RegExp(String.raw`${TB}sk-or-(?:v1-)?[A-Za-z0-9_-]{20,}\b`, "g"),
116
+ },
109
117
  // Generic sk- legacy OpenAI keys (must come AFTER the ant/proj/or variants)
110
- { name: "sk_key", re: /\bsk-[A-Za-z0-9]{20,}\b/g },
118
+ { name: "sk_key", re: new RegExp(String.raw`${TB}sk-[A-Za-z0-9]{20,}\b`, "g") },
111
119
  // Slack tokens
112
- { name: "slack_token", re: /\bxox[baprseo]-[A-Za-z0-9-]{10,}\b/g },
120
+ { name: "slack_token", re: new RegExp(String.raw`${TB}xox[baprseo]-[A-Za-z0-9-]{10,}\b`, "g") },
113
121
  // AWS access key IDs
114
- { name: "aws_access_key", re: /\bAKIA[0-9A-Z]{16}\b/g },
122
+ { name: "aws_access_key", re: new RegExp(String.raw`${TB}AKIA[0-9A-Z]{16}\b`, "g") },
115
123
  // Google API keys
116
- { name: "google_api_key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
124
+ { name: "google_api_key", re: new RegExp(String.raw`${TB}AIza[A-Za-z0-9_-]{35}\b`, "g") },
117
125
  // JWTs (3 dot-separated base64url segments)
118
126
  {
119
127
  name: "jwt",
120
- re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
128
+ re: new RegExp(
129
+ String.raw`${TB}eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`,
130
+ "g",
131
+ ),
121
132
  },
122
133
  // SigNoz Cloud OTLP auth header values.
123
134
  {
124
135
  name: "signoz_ingestion_key",
125
- re: /\bsignoz-ingestion-key=[A-Za-z0-9._~+/-]{20,}={0,2}\b/g,
136
+ re: new RegExp(String.raw`${TB}signoz-ingestion-key=[A-Za-z0-9._~+/-]{20,}={0,2}\b`, "g"),
137
+ },
138
+ // Linear OAuth tokens and API keys
139
+ { name: "linear_oauth", re: new RegExp(String.raw`${TB}lin_oauth_[A-Za-z0-9_-]{10,}\b`, "g") },
140
+ { name: "linear_api", re: new RegExp(String.raw`${TB}lin_api_[A-Za-z0-9_-]{10,}\b`, "g") },
141
+ // npm tokens
142
+ { name: "npm_token", re: new RegExp(String.raw`${TB}npm_[A-Za-z0-9_-]{20,}\b`, "g") },
143
+ // Jira API tokens (Atlassian cloud)
144
+ {
145
+ name: "atlassian_token",
146
+ re: new RegExp(String.raw`${TB}ATATT[A-Za-z0-9_-]{20,}\b`, "g"),
126
147
  },
127
148
  // Agent-swarm MCP user tokens (`aswt_<base62-20+>`). Schema lands in
128
149
  // migration 064; mint/revoke endpoints ship with the MCP-token plan.
129
150
  // Rule lives here now so plaintexts never leak into logs once endpoints
130
151
  // come online.
131
- { name: "mcp_token", re: /\baswt_[A-Za-z0-9]{20,}\b/g },
152
+ { name: "mcp_token", re: new RegExp(String.raw`${TB}aswt_[A-Za-z0-9]{20,}\b`, "g") },
132
153
  ];
133
154
 
134
155
  interface EnvValueEntry {