cartethyia 2.6.2 → 2.6.4

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 (142) hide show
  1. package/dist/dashboard/assets/{BannedPage-BMFQM5pm.js → BannedPage-rvAOt5Ad.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-rvAOt5Ad.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-BP-8qZqc.js → CliToolDetailPage-ZoKZDsob.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-ZoKZDsob.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DpJeG_Op.js → CliToolsPage-1_a6Tw8Y.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-1_a6Tw8Y.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-jNI-D44o.js → CombosPage-Czo7GLxL.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Czo7GLxL.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-CQpDMg8K.js → ConsoleLogPage-BrlBFGtF.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-BrlBFGtF.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-DKU4KjlR.js → CustomizationPage-COvQuDTc.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-COvQuDTc.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-gTD1Qv_d.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-gTD1Qv_d.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-DaUYRu4n.js → LoginPage-BlGbMdYo.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BlGbMdYo.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-DVny4S5_.js → ModelPicker-CA1kLs45.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  19. package/dist/dashboard/assets/{OverviewPage-Cvo6sK7M.js → OverviewPage-BhUFw9Qp.js} +1 -1
  20. package/dist/dashboard/assets/OverviewPage-BhUFw9Qp.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-CrBfX_gj.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrBfX_gj.js.gz +0 -0
  23. package/dist/dashboard/assets/{ProvidersPage-CIo_CRe_.js → ProvidersPage-CZ7sCKxk.js} +1 -1
  24. package/dist/dashboard/assets/ProvidersPage-CZ7sCKxk.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProxyPage-BrsHDiRx.js → ProxyPage-PnJWpC9C.js} +1 -1
  26. package/dist/dashboard/assets/ProxyPage-PnJWpC9C.js.gz +0 -0
  27. package/dist/dashboard/assets/{QuotaPage-DkUwJFpM.js → QuotaPage-CrGTXWjQ.js} +1 -1
  28. package/dist/dashboard/assets/QuotaPage-CrGTXWjQ.js.gz +0 -0
  29. package/dist/dashboard/assets/{SettingsPage-BXK4gIFl.js → SettingsPage-DXjUbFh_.js} +1 -1
  30. package/dist/dashboard/assets/SettingsPage-DXjUbFh_.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-BiIPnecp.js → SetupPage-DCFo3R5_.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DCFo3R5_.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-CclP89S9.js → SortableList-lFdVPMo_.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-lFdVPMo_.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-Djt-Mk-N.js → StudioPage-DPbSWHny.js} +1 -1
  36. package/dist/dashboard/assets/StudioPage-DPbSWHny.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-DaGf9BI1.js → UsagePage-B-CTPrqB.js} +1 -1
  38. package/dist/dashboard/assets/UsagePage-B-CTPrqB.js.gz +0 -0
  39. package/dist/dashboard/assets/app-BUSiYL6F.css +2 -0
  40. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  41. package/dist/dashboard/assets/{badge-CKsF123d.js → badge-Bkz_-RrM.js} +1 -1
  42. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  43. package/dist/dashboard/assets/{banner-8j7uChA9.js → banner-C-7y3VKV.js} +1 -1
  44. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  45. package/dist/dashboard/assets/{card-CdsSlWA3.js → card-BOPBuBzD.js} +1 -1
  46. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  47. package/dist/dashboard/assets/{clipboard-button-uL5aYLfW.js → clipboard-button-e-Mlk0FH.js} +1 -1
  48. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  49. package/dist/dashboard/assets/{dialog-C07hr5_G.js → dialog-Iec_ywbD.js} +1 -1
  50. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  51. package/dist/dashboard/assets/{drawer-BPO1TcKH.js → drawer-Bs8MIIHc.js} +1 -1
  52. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  53. package/dist/dashboard/assets/{entry-Bz_CtDfC.js → entry-BRszm4_Z.js} +1 -1
  54. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  55. package/dist/dashboard/assets/{entry-BY4QChsA.js → entry-BcfdW4C_.js} +3 -3
  56. package/dist/dashboard/assets/entry-BcfdW4C_.js.gz +0 -0
  57. package/dist/dashboard/assets/{entry-GJRbmrpA.js → entry-DcobaKFu.js} +1 -1
  58. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  59. package/dist/dashboard/assets/{github-badge-DmQreSDd.js → github-badge-BuDsrsPn.js} +1 -1
  60. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  61. package/dist/dashboard/assets/{index-isGWt18Z.js → index-_rbyCXD2.js} +2 -2
  62. package/dist/dashboard/assets/index-_rbyCXD2.js.gz +0 -0
  63. package/dist/dashboard/assets/{inline-C9dCYaBZ.js → inline-DToMICwl.js} +1 -1
  64. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  65. package/dist/dashboard/assets/{input-CjCnXweJ.js → input-WlyHWeG2.js} +1 -1
  66. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  67. package/dist/dashboard/assets/{layout-V6y8DAkl.js → layout-DaJ5baJr.js} +1 -1
  68. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  69. package/dist/dashboard/assets/{list-row-CM1AaG_y.js → list-row-B3oSK13z.js} +1 -1
  70. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  71. package/dist/dashboard/assets/{numbers-BOevJzdz.js → numbers-Dulqgys5.js} +1 -1
  72. package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
  73. package/dist/dashboard/assets/{select-Dpp8tE7Z.js → select-BTEzH1iH.js} +1 -1
  74. package/dist/dashboard/assets/select-BTEzH1iH.js.gz +0 -0
  75. package/dist/dashboard/assets/{stack-_gfTdLVB.js → stack-B1w5BM0u.js} +1 -1
  76. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  77. package/dist/dashboard/assets/{state-2c5V4zo7.js → state-DhZBfWgx.js} +1 -1
  78. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  79. package/dist/dashboard/assets/{switch-Dkke7Naf.js → switch-yD7D99Lg.js} +1 -1
  80. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  81. package/dist/dashboard/index.html +1 -1
  82. package/dist/dashboard/index.html.gz +0 -0
  83. package/dist/dashboard/precompressed-assets.json +41 -41
  84. package/package.json +1 -1
  85. package/src/boot/app.ts +11 -1
  86. package/src/console/backup/store.ts +10 -2
  87. package/src/console/providers/catalog/contracts.ts +7 -0
  88. package/src/console/providers/catalog/store.ts +25 -2
  89. package/src/observability/event-query.ts +10 -5
  90. package/src/providers/integrations/buddy/buddy-chat-shared.ts +105 -0
  91. package/src/providers/integrations/buddy/buddy-cli-headers.ts +24 -20
  92. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -2
  93. package/src/providers/integrations/buddy/codebuddy/provider.ts +15 -15
  94. package/src/providers/integrations/buddy/codebuddy/shared.ts +7 -5
  95. package/src/providers/integrations/buddy/workbuddy/provider.ts +13 -12
  96. package/src/providers/integrations/buddy/workbuddy/shared.ts +14 -4
  97. package/src/providers/operations/account-health-service.ts +211 -17
  98. package/dist/dashboard/assets/BannedPage-BMFQM5pm.js.gz +0 -0
  99. package/dist/dashboard/assets/CliToolDetailPage-BP-8qZqc.js.gz +0 -0
  100. package/dist/dashboard/assets/CliToolsPage-DpJeG_Op.js.gz +0 -0
  101. package/dist/dashboard/assets/CombosPage-jNI-D44o.js.gz +0 -0
  102. package/dist/dashboard/assets/ConsoleLogPage-CQpDMg8K.js.gz +0 -0
  103. package/dist/dashboard/assets/CustomizationPage-DKU4KjlR.js.gz +0 -0
  104. package/dist/dashboard/assets/HealthEventsModal-DUKqhSoT.js +0 -1
  105. package/dist/dashboard/assets/HealthEventsModal-DUKqhSoT.js.gz +0 -0
  106. package/dist/dashboard/assets/LoginPage-DaUYRu4n.js.gz +0 -0
  107. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  108. package/dist/dashboard/assets/OverviewPage-Cvo6sK7M.js.gz +0 -0
  109. package/dist/dashboard/assets/ProviderDetailPage-BrL6Yl9w.js +0 -3
  110. package/dist/dashboard/assets/ProviderDetailPage-BrL6Yl9w.js.gz +0 -0
  111. package/dist/dashboard/assets/ProvidersPage-CIo_CRe_.js.gz +0 -0
  112. package/dist/dashboard/assets/ProxyPage-BrsHDiRx.js.gz +0 -0
  113. package/dist/dashboard/assets/QuotaPage-DkUwJFpM.js.gz +0 -0
  114. package/dist/dashboard/assets/SettingsPage-BXK4gIFl.js.gz +0 -0
  115. package/dist/dashboard/assets/SetupPage-BiIPnecp.js.gz +0 -0
  116. package/dist/dashboard/assets/SortableList-CclP89S9.js.gz +0 -0
  117. package/dist/dashboard/assets/StudioPage-Djt-Mk-N.js.gz +0 -0
  118. package/dist/dashboard/assets/UsagePage-DaGf9BI1.js.gz +0 -0
  119. package/dist/dashboard/assets/app-CEeaMHOy.css +0 -2
  120. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  121. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  122. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  123. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  124. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  125. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  126. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  127. package/dist/dashboard/assets/entry-BY4QChsA.js.gz +0 -0
  128. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  129. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  130. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  131. package/dist/dashboard/assets/index-isGWt18Z.js.gz +0 -0
  132. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  133. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  134. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  135. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  136. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  137. package/dist/dashboard/assets/select-Dpp8tE7Z.js.gz +0 -0
  138. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  139. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  140. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  141. /package/dist/dashboard/assets/{app-2KhtHCv9.js → app-MdVfwraB.js} +0 -0
  142. /package/dist/dashboard/assets/{app-2KhtHCv9.js.gz → app-MdVfwraB.js.gz} +0 -0
@@ -59,11 +59,26 @@ function parseExpiry(value: unknown): Date | undefined {
59
59
  }
60
60
  return undefined;
61
61
  }
62
- function parseJwtCredential(raw: string): { readonly expiresAt?: Date } | undefined {
62
+ /**
63
+ * Reads the two claims a static token's own payload can answer: when it stops
64
+ * working, and who issued it.
65
+ *
66
+ * The issuer is kept because a static JWT carries no other hint of its origin:
67
+ * an operator who pasted one from the wrong surface has nothing in the console
68
+ * to check it against, and several issuers accept the same token shape.
69
+ */
70
+ function parseJwtCredential(
71
+ raw: string,
72
+ ): { readonly expiresAt?: Date; readonly issuer?: string } | undefined {
63
73
  const payload = decodeJwtPayload(raw.trim());
64
74
  if (payload === undefined) return undefined;
65
75
  const expiresAt = parseExpiry(payload["exp"]);
66
- return expiresAt === undefined ? {} : { expiresAt };
76
+ const iss = payload["iss"];
77
+ const issuer = typeof iss === "string" && iss.trim().length > 0 ? iss.trim() : undefined;
78
+ return {
79
+ ...(expiresAt === undefined ? {} : { expiresAt }),
80
+ ...(issuer === undefined ? {} : { issuer }),
81
+ };
67
82
  }
68
83
 
69
84
  /**
@@ -1124,6 +1139,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1124
1139
  const requestedCredentialMode: CredentialMode = request.credentialMode ?? "auto";
1125
1140
  let effectiveCredentialMode: CredentialMode = requestedCredentialMode;
1126
1141
  let tokenExpiresAt: Date | undefined;
1142
+ let jwtIssuer: string | undefined;
1127
1143
  let oauthRefreshCiphertext: Buffer | undefined;
1128
1144
  let oauthExpiresAt: Date | undefined;
1129
1145
  let oauthHasRefreshToken = false;
@@ -1160,6 +1176,9 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1160
1176
  effectiveCredentialKind = "api_key";
1161
1177
  effectiveCredentialMode = "jwt";
1162
1178
  tokenExpiresAt = parsedJwt.expiresAt;
1179
+ // Kept alongside the expiry: the console shows who issued a static
1180
+ // token, and the secret itself is never readable after this point.
1181
+ jwtIssuer = parsedJwt.issuer;
1163
1182
  } else if (request.credentialKind === "oauth" || hasRefreshToken) {
1164
1183
  effectiveSecret = parts.accessToken;
1165
1184
  effectiveCredentialKind = hasRefreshToken ? "oauth" : "api_key";
@@ -1185,6 +1204,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1185
1204
  ...(request.authState ?? {}),
1186
1205
  credentialMode: effectiveCredentialMode,
1187
1206
  ...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt: tokenExpiresAt.toISOString() }),
1207
+ ...(jwtIssuer === undefined ? {} : { jwtIssuer }),
1188
1208
  };
1189
1209
  const nextSortIndex =
1190
1210
  sortIndexOverride ?? (await this.nextAccountSortIndex(tx, providerId, tenantId));
@@ -1409,6 +1429,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1409
1429
  : undefined;
1410
1430
  const tokenExpiresAt =
1411
1431
  typeof authState?.["tokenExpiresAt"] === "string" ? authState["tokenExpiresAt"] : undefined;
1432
+ const jwtIssuer =
1433
+ typeof authState?.["jwtIssuer"] === "string" ? authState["jwtIssuer"] : undefined;
1412
1434
  return {
1413
1435
  id: row.id,
1414
1436
  providerId: row.providerId,
@@ -1417,6 +1439,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1417
1439
  credentialKind: row.credentialKind,
1418
1440
  ...(credentialMode === undefined ? {} : { credentialMode }),
1419
1441
  ...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt }),
1442
+ ...(jwtIssuer === undefined ? {} : { jwtIssuer }),
1420
1443
  status: row.status,
1421
1444
  ...(usage.inflight === undefined ? {} : { inflight: usage.inflight }),
1422
1445
  usageToday: usage.today,
@@ -215,7 +215,8 @@ export function eventRecordFromRow(row: {
215
215
  */
216
216
  function isPostgres(db: CartethyiaDatabase): boolean {
217
217
  const dialect = (db as unknown as { dialect?: { constructor?: { name?: string } } }).dialect;
218
- return dialect?.constructor?.name === "PostgresDialect";
218
+ const name = dialect?.constructor?.name;
219
+ return name === "PostgresDialect" || name === "PgDialect";
219
220
  }
220
221
 
221
222
  /**
@@ -508,9 +509,14 @@ export async function chartTelemetry(
508
509
  bucketSeconds: number,
509
510
  ): Promise<ChartBucket[]> {
510
511
  const width = Math.max(1, Math.trunc(bucketSeconds));
512
+ // Inlined (not parameterised) so Postgres sees the SELECT and GROUP BY
513
+ // expressions as identical: parameter numbers differing between the two
514
+ // clauses made Postgres treat them as unrelated expressions referencing
515
+ // `created_at`, which 42803-rejected the query.
516
+ const w = sql.raw(String(width));
511
517
  const bucketExpression = isPostgres(db)
512
- ? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${width}) * ${width}`
513
- : sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${width}) * ${width}`;
518
+ ? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${w}) * ${w}`
519
+ : sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${w}) * ${w}`;
514
520
  const rows = await db
515
521
  .select({
516
522
  at: bucketExpression,
@@ -521,8 +527,7 @@ export async function chartTelemetry(
521
527
  })
522
528
  .from(telemetryEvents)
523
529
  .where(scoped(tenantId, since))
524
- .groupBy(bucketExpression)
525
- .orderBy(bucketExpression);
530
+ .groupBy(bucketExpression);
526
531
  return rows.map((row) => ({
527
532
  at: numeric(row.at) * 1000,
528
533
  requests: numeric(row.requests),
@@ -308,6 +308,107 @@ function rewriteBlockedCodeField(message: Record<string, unknown>, field: string
308
308
  }
309
309
  }
310
310
 
311
+ const BUDDY_ZWSP = "\u200b";
312
+
313
+ function escapeBuddyRegexLiteral(value: string): string {
314
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
315
+ }
316
+
317
+ /**
318
+ * Builds a matcher that tolerates the zero-width spaces upstream interleaves
319
+ * between characters, so an identity template is still recognized after a
320
+ * normalizer has split it apart.
321
+ */
322
+ function buddyTemplateSource(text: string): string {
323
+ const words = text
324
+ .split(" ")
325
+ .map((word) => [...word].map(escapeBuddyRegexLiteral).join(`${BUDDY_ZWSP}*`));
326
+ return words.join(`[\\s${BUDDY_ZWSP}]+`) + `${BUDDY_ZWSP}*`;
327
+ }
328
+
329
+ function buddyTemplatePattern(text: string): RegExp {
330
+ return new RegExp(`(?<!\\w)${buddyTemplateSource(text)}(?!\\w)`, "i");
331
+ }
332
+
333
+ // DeepSeek Harness (DSH) role identities. DSH is the harness behind WorkBuddy;
334
+ // a history that still carries its self-description reads to the upstream as a
335
+ // foreign channel, so the hardened client rewrites it back to the WorkBuddy
336
+ // identity rather than forwarding a competing one.
337
+ const DSH_CODING_PATTERN = new RegExp(
338
+ `(?<!\\w)${buddyTemplateSource("You are a coding agent powered by the")}` +
339
+ `[\\s${BUDDY_ZWSP}]+([^\\r\\n]{1,200}?)[\\s${BUDDY_ZWSP}]+` +
340
+ `${buddyTemplateSource("model")}(?!\\w)`,
341
+ "i",
342
+ );
343
+ const DSH_CHANNEL_IDENTITY_PATTERN = buddyTemplatePattern(
344
+ "You are an AI agent powered by DeepSeek Harness",
345
+ );
346
+ const DSH_MINIMAL_PATTERN = buddyTemplatePattern("You are a helpful software engineer assistant");
347
+ const DSH_DIRECT_PATTERN = buddyTemplatePattern("You are DeepSeek Harness");
348
+ const DSH_WEB_GUI_PATTERN = buddyTemplatePattern("DeepSeek Harness Web GUI");
349
+
350
+ /** The identity DSH role prompts are rewritten to; matches the donor gateway. */
351
+ const DSH_WORKBUDDY_IDENTITY = "You are WorkBuddy, a powerful AI assistant";
352
+
353
+ /**
354
+ * Rewrites DeepSeek Harness role identities in one message's text back to the
355
+ * WorkBuddy identity. Port of the donor gateway's always-on
356
+ * `desensitize_channel_body`, scoped to text the wire actually carries.
357
+ *
358
+ * This normalizes a replayed history (or a surviving system turn) that still
359
+ * claims the DSH harness, so a WorkBuddy request never advertises a competing
360
+ * channel identity upstream. The coding persona keeps the model it named:
361
+ * "You are a coding agent powered by the X model" becomes the WorkBuddy
362
+ * identity plus "powered by the X model".
363
+ */
364
+ export function scrubBuddyChannelIdentityText(text: string): string {
365
+ if (!text) return text;
366
+ const withCodingPersona = text.replace(
367
+ DSH_CODING_PATTERN,
368
+ (_match, model: string) =>
369
+ `${DSH_WORKBUDDY_IDENTITY}. This conversation is powered by the ${model.trim()} model`,
370
+ );
371
+ return withCodingPersona
372
+ .replace(DSH_CHANNEL_IDENTITY_PATTERN, DSH_WORKBUDDY_IDENTITY)
373
+ .replace(DSH_MINIMAL_PATTERN, DSH_WORKBUDDY_IDENTITY)
374
+ .replace(DSH_DIRECT_PATTERN, DSH_WORKBUDDY_IDENTITY)
375
+ .replace(DSH_WEB_GUI_PATTERN, "WorkBuddy Web GUI");
376
+ }
377
+
378
+ function scrubBuddyMessageText(message: Record<string, unknown>, field: string): void {
379
+ const value = message[field];
380
+ if (typeof value === "string") {
381
+ const scrubbed = scrubBuddyChannelIdentityText(value);
382
+ if (scrubbed !== value) message[field] = scrubbed;
383
+ return;
384
+ }
385
+ if (!Array.isArray(value)) return;
386
+ for (const part of value) {
387
+ if (!isRecord(part)) continue;
388
+ const text = part["text"];
389
+ if (typeof text === "string") {
390
+ const scrubbed = scrubBuddyChannelIdentityText(text);
391
+ if (scrubbed !== text) part["text"] = scrubbed;
392
+ }
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Applies the DSH identity scrub across a buddy conversation: every
398
+ * `system`/`developer`/`assistant` turn's text and `reasoning_content` is
399
+ * normalized, so a replayed harness history cannot carry a competing channel
400
+ * identity upstream.
401
+ */
402
+ export function scrubBuddyChannelIdentity(messages: Array<Record<string, unknown>>): void {
403
+ for (const message of messages) {
404
+ if (!isRecord(message)) continue;
405
+ const role = message["role"];
406
+ if (role !== "system" && role !== "developer" && role !== "assistant") continue;
407
+ scrubBuddyMessageText(message, "content");
408
+ scrubBuddyMessageText(message, "reasoning_content");
409
+ }
410
+ }
411
+
311
412
  /**
312
413
  * Payload prefix shared by the buddy pre-payload hooks: force `stream=true`,
313
414
  * apply the provider's DeepSeek thinking contract, opt `reasoning_summary` in
@@ -370,6 +471,10 @@ export function buddyPrePayloadCommon(
370
471
  repackBuddyToolResultBlocks(arr);
371
472
  cleanupBuddyOrphanToolPairs(arr);
372
473
  coalesceConsecutiveAssistantMessages(arr);
474
+ // Normalize a replayed harness history before the blocked-code rewrite
475
+ // below: DSH role identities are replaced with the WorkBuddy identity, so
476
+ // a WorkBuddy-family request never forwards a competing channel identity.
477
+ scrubBuddyChannelIdentity(arr);
373
478
  // After every merge: a quoted `11128` in any surviving text blocks the
374
479
  // whole request, and the merges above are what produce the final text.
375
480
  rewriteBuddyBlockedCode(payload);
@@ -2,43 +2,47 @@
2
2
  * The header family only the CLI identity sends.
3
3
  *
4
4
  * Everything here comes from the two reference gateways, which agree: the
5
- * desktop product sends conversation/tracing headers and browser transport,
6
- * while the CLI sends the agent envelope plus the OpenAI Node SDK fingerprint
7
- * and no browser headers at all. A request carrying both halves is a hybrid no
8
- * real client produces, which is what risk control keys on.
5
+ * desktop product sends conversation/tracing headers and a device token, while
6
+ * the CLI sends the agent envelope plus the OpenAI Node SDK fingerprint. The
7
+ * CLI still drops the browser *transport* headers (`origin`, `referer`,
8
+ * `accept-language`) and the device token, but keeps the request-shape markers
9
+ * (`x-requested-with`, `x-codebuddy-request`) the reference sends on every call
10
+ * regardless of transport — see the CLI branches in each provider's
11
+ * `*-shared.ts`.
9
12
  *
10
- * Two references, two fingerprints: the hub derives OS/arch and a Node version
11
- * from its own host, and codebuddy2api pins a fixed Linux/x64/v24.21.0 set.
12
- * Neither can be right for every deployment, so the values here follow the
13
- * host this gateway actually runs on — they are a fingerprint of the process,
14
- * not a claim about a remote machine.
13
+ * The SDK fingerprint is pinned, not host-derived. Both references pin a fixed
14
+ * set (codebuddy2api pins Linux/x64/v24.21.0); this gateway pins the macOS set
15
+ * instead, so every CLI dispatch presents one consistent client fingerprint no
16
+ * matter which machine the gateway runs on. A host-derived fingerprint would
17
+ * advertise whatever OS the process happens to run under — a value no upstream
18
+ * risk model has seen for this client brand, and an easy outlier to key on.
15
19
  */
16
20
 
17
21
  /** OpenAI Node SDK 6.25.0 `x-stainless-*` fingerprint; both references agree. */
18
22
  const STAINLESS_PACKAGE_VERSION = "6.25.0";
19
23
 
20
- function hostOs(): string {
21
- return process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "MacOS" : "Linux";
22
- }
23
-
24
- function hostArch(): string {
25
- return process.arch === "arm64" || process.arch === "ia32" ? process.arch : "x64";
26
- }
24
+ /**
25
+ * The pinned Node runtime the fingerprint claims. Held constant across
26
+ * dispatches: a drifting `process.version` would advertise a different Node
27
+ * build per deployment, which is not what one installed client looks like.
28
+ */
29
+ const STAINLESS_RUNTIME_VERSION = "v24.21.0";
27
30
 
28
31
  /**
29
32
  * The SDK fingerprint headers.
30
33
  *
31
34
  * `x-stainless-timeout` is in the hub only and `x-stainless-retry-count` is in
32
- * both; the SDK sends both, so both go out.
35
+ * both; the SDK sends both, so both go out. The OS/arch pair is the pinned
36
+ * macOS fingerprint rather than this process's host.
33
37
  */
34
38
  export function buddyStainlessHeaders(): Record<string, string> {
35
39
  return {
36
40
  "x-stainless-lang": "js",
37
41
  "x-stainless-package-version": STAINLESS_PACKAGE_VERSION,
38
- "x-stainless-os": hostOs(),
39
- "x-stainless-arch": hostArch(),
42
+ "x-stainless-os": "MacOS",
43
+ "x-stainless-arch": "x64",
40
44
  "x-stainless-runtime": "node",
41
- "x-stainless-runtime-version": process.version,
45
+ "x-stainless-runtime-version": STAINLESS_RUNTIME_VERSION,
42
46
  "x-stainless-retry-count": "0",
43
47
  "x-stainless-timeout": "1200",
44
48
  };
@@ -34,6 +34,11 @@ function newMessageId(): string {
34
34
  return crypto.randomUUID().replaceAll("-", "");
35
35
  }
36
36
 
37
+ /** Renders a 32-char hex id as a dashed UUID (`8-4-4-4-12`). */
38
+ function toUuidHex(hex: string): string {
39
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
40
+ }
41
+
37
42
  /** True when `value` is a valid B3 trace id: 16 or 32 hex characters. */
38
43
  function isValidTraceId(value: string): boolean {
39
44
  if (value.length !== 16 && value.length !== 32) return false;
@@ -53,6 +58,18 @@ export interface BuddyConversationIds {
53
58
  readonly conversationRequestId?: string | undefined;
54
59
  /** Inbound trace id to pass through; falls back to the conversation request id. */
55
60
  readonly traceId?: string | undefined;
61
+ /**
62
+ * Wire format for the two message-scoped ids (`x-conversation-message-id`,
63
+ * `x-request-id`).
64
+ *
65
+ * The reference emits these as dashed UUIDs on the WorkBuddy desktop product
66
+ * and as 32-hex on CLI traffic (`build_attempt_headers` converts only the
67
+ * message/request ids, and only for the WorkBuddy profiles). The turn
68
+ * aggregation key (`x-conversation-request-id` / `x-root-request-id`) and the
69
+ * B3/W3C trace ids stay hex in both styles, because B3's spec accepts only
70
+ * hex. Default `"hex"`.
71
+ */
72
+ readonly messageIdStyle?: "hex" | "uuid" | undefined;
56
73
  }
57
74
 
58
75
  /**
@@ -71,14 +88,17 @@ export function buddyConversationHeaders(
71
88
  ? ids.conversationRequestId.trim()
72
89
  : newMessageId();
73
90
  const messageId = newMessageId();
91
+ // The message-scoped ids may render as a dashed UUID (WorkBuddy desktop) or
92
+ // as 32-hex (CLI); the raw hex is still what the B3/W3C trace ids derive from.
93
+ const messageHeaderValue = ids.messageIdStyle === "uuid" ? toUuidHex(messageId) : messageId;
74
94
  const traceId =
75
95
  ids.traceId && ids.traceId.trim().length > 0 ? ids.traceId.trim() : conversationRequestId;
76
96
  const b3TraceId = isValidTraceId(conversationRequestId) ? conversationRequestId : messageId;
77
97
 
78
98
  const headers: Record<string, string> = {
79
99
  "x-conversation-request-id": conversationRequestId,
80
- "x-conversation-message-id": messageId,
81
- "x-request-id": messageId,
100
+ "x-conversation-message-id": messageHeaderValue,
101
+ "x-request-id": messageHeaderValue,
82
102
  "x-root-request-id": conversationRequestId,
83
103
  "x-trace-id": traceId,
84
104
  // W3C trace context alongside the B3 set below: the upstream reads both,
@@ -48,24 +48,24 @@ export const CODEBUDDY_BASE_URL = providerBaseUrl("cb");
48
48
  /**
49
49
  * Leading system turn for this variant.
50
50
  *
51
- * The upstream validates that the wire *opens* with a `system` turn (code
52
- * `11128`) but does not check its text — the CN variant installs a plain
53
- * engineering-assistant sentence in the same slot and the gateway accepts it.
54
- * So this text is a product choice rather than a wire contract, and the
55
- * previous vendor-branded line ("You are CodeBuddy Code.") made every request
56
- * claim an identity the caller never chose, while also discarding whatever
57
- * system prompt the caller actually sent.
51
+ * The wire contract is that the request *opens* with a `system` turn (code
52
+ * `11128`); the gateway replaces any caller `system`/`developer` turn with this
53
+ * fixed prompt. The text is the CodeBuddy CLI's own leading prompt — the same
54
+ * one the reference client (and the donor gateway's `prepend` prompt mode)
55
+ * sends — so a proxied request is byte-faithful to an official client rather
56
+ * than a neutral stand-in.
58
57
  *
59
- * The prompt is therefore a neutral assistant identity with an explicit
60
- * persona: pragmatic, direct, and honest — no sycophancy, no hedging, no
61
- * invented certainty.
58
+ * Keeping the official identity is forward-compatible: the upstream is not
59
+ * observed to validate this text today, but a proxy that claims the real
60
+ * client's identity cannot be caught out if it starts to. A previous neutral
61
+ * persona ("You are a pragmatic and direct software engineering assistant…")
62
+ * worked, but diverged from every request the official CLI emits.
62
63
  */
63
64
  export const CODEBUDDY_SYSTEM_PROMPT =
64
- "You are a pragmatic and direct software engineering assistant. " +
65
- "Be honest and truthful: state what you know, say plainly when you are unsure " +
66
- "or do not know, and never claim to have done something you have not done. " +
67
- "Prefer concrete answers over filler, and say so when a request is ambiguous " +
68
- "instead of guessing silently.";
65
+ "You are CodeBuddy Code.\n\n" +
66
+ "You are an interactive CLI tool that helps users with software engineering tasks. " +
67
+ "Use the instructions below and the tools available to you to assist the user. " +
68
+ "Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.";
69
69
 
70
70
  function codeBuddyIntlPrePayload(
71
71
  payload: Record<string, unknown>,
@@ -41,11 +41,11 @@ export async function codebuddyHeaders(
41
41
  });
42
42
  if (resolveBuddyIdentity(context, cliIdentity) === "cli") {
43
43
  return {
44
- // The CLI sends `Accept: application/json` and no device token. The
45
- // browser-shaped pair (`X-Requested-With`, `x-codebuddy-request`) stays
46
- // off this branch: a request carrying both halves is a hybrid no real
47
- // client produces, and that is a fidelity choice, not a requirement —
48
- // the live API answers either way.
44
+ // The CLI sends `Accept: application/json` and no device token. It keeps
45
+ // the request-shape markers (`X-Requested-With`, `X-CodeBuddy-Request`)
46
+ // the reference sends on every call regardless of transport, but drops
47
+ // the browser *transport* header (`accept: text/event-stream`) — so the
48
+ // identity is still CLI, not the desktop hybrid.
49
49
  accept: "application/json",
50
50
  "User-Agent": `CLI/${getCodeBuddyVersion()} CodeBuddy/${getCodeBuddyVersion()}`,
51
51
  "X-Product": "SaaS",
@@ -53,6 +53,8 @@ export async function codebuddyHeaders(
53
53
  "X-IDE-Name": "CLI",
54
54
  "X-IDE-Version": getCodeBuddyVersion(),
55
55
  "X-Domain": codebuddyDomain(variant),
56
+ "x-requested-with": "XMLHttpRequest",
57
+ "x-codebuddy-request": "1",
56
58
  // The conversation purpose the reference client states. Not required —
57
59
  // verified against the live API that a request succeeds without it — but
58
60
  // it is part of the client's real request, and matching the client is the
@@ -49,20 +49,21 @@ export const WORKBUDDY_BASE_URL = providerBaseUrl("workbuddy");
49
49
  /**
50
50
  * Leading system turn for this variant.
51
51
  *
52
- * The upstream does validate the leading system turn's *text*, not just that
53
- * the wire opens with one: sending caller text in its place answers
54
- * `400 · 11128 "Illegal API invocation from an unapproved channel"` on
55
- * CodeBuddy, which treats that prompt as the calling channel's identity. So
56
- * this prompt is a wire contract, not merely a product choice — see the
57
- * sibling in `codebuddy.ts`. The previous line named the vendor product, which
58
- * made every request claim an identity the caller never chose.
52
+ * The upstream validates that the wire opens with a `system` turn and treats
53
+ * that prompt as the calling channel's identity: forwarding caller text in its
54
+ * place answers `400 · 11128 "Illegal API invocation from an unapproved
55
+ * channel"`. So this prompt is a wire contract, not merely a product choice —
56
+ * see the sibling in `codebuddy.ts`.
57
+ *
58
+ * The text is WorkBuddy's own leading prompt, matching what the official client
59
+ * (and the donor gateway's `prepend` prompt mode) sends, so a proxied request
60
+ * carries the real client's identity instead of a neutral stand-in. A previous
61
+ * neutral persona was accepted but diverged from every request the official
62
+ * desktop client emits.
59
63
  */
60
64
  export const WORKBUDDY_SYSTEM_PROMPT =
61
- "You are a pragmatic and direct software engineering assistant. " +
62
- "Be honest and truthful: state what you know, say plainly when you are unsure " +
63
- "or do not know, and never claim to have done something you have not done. " +
64
- "Prefer concrete answers over filler, and say so when a request is ambiguous " +
65
- "instead of guessing silently.";
65
+ "You are WorkBuddy, a powerful AI assistant.\n\n" +
66
+ "Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.";
66
67
 
67
68
  /**
68
69
  * Reconstructs generic function declarations when a later tool-history turn
@@ -69,19 +69,29 @@ export async function workbuddyHeaders(
69
69
  clientVersion = getWorkBuddyClientVersion(),
70
70
  cliIdentity?: boolean,
71
71
  ): Promise<Record<string, string>> {
72
+ const identity = resolveBuddyIdentity(context, cliIdentity);
72
73
  const conversation = buddyConversationHeaders({
73
74
  ...(resolveInboundSessionId(context, request) === undefined
74
75
  ? {}
75
76
  : { conversationId: resolveInboundSessionId(context, request) }),
77
+ // The reference emits dashed-UUID message/request ids on the WorkBuddy
78
+ // desktop product and 32-hex on CLI traffic (build_attempt_headers converts
79
+ // only the message/request ids, and only for the WorkBuddy profiles).
80
+ messageIdStyle: identity === "desktop" ? "uuid" : "hex",
76
81
  });
77
82
  const accountId = context?.credential.account_id;
78
- if (resolveBuddyIdentity(context, cliIdentity) === "cli") {
79
- // The reference CLI: issued by the OpenAI Node SDK, so no browser header
80
- // and no device token, and the machine/session device pair does not
81
- // exist — only `X-User-Id`, which is `anonymous` without a uid.
83
+ if (identity === "cli") {
84
+ // The reference CLI: issued by the OpenAI Node SDK, so no browser
85
+ // *transport* header (origin/referer/accept-language) and no device token,
86
+ // and the machine/session device pair does not exist — only `X-User-Id`,
87
+ // which is `anonymous` without a uid. It keeps the request-shape markers
88
+ // (`x-requested-with`, `x-codebuddy-request`) the reference sends on every
89
+ // call regardless of transport.
82
90
  return {
83
91
  accept: "application/json",
84
92
  "user-agent": `CLI/${getWorkBuddyCliVersion()} CodeBuddy/${getWorkBuddyCliVersion()}`,
93
+ "x-requested-with": "XMLHttpRequest",
94
+ "x-codebuddy-request": "1",
85
95
  "x-domain": domain,
86
96
  "x-agent-purpose": "conversation",
87
97
  "x-ide-name": "CLI",