@undefineds.co/xpod 0.4.25 → 0.4.28

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 (155) hide show
  1. package/dist/api/ApiServer.d.ts +1 -0
  2. package/dist/api/ApiServer.js +38 -0
  3. package/dist/api/ai-config/AiConfigStore.js +18 -6
  4. package/dist/api/ai-gateway/connect/index.d.ts +1 -0
  5. package/dist/api/ai-gateway/connect/index.js +39 -18
  6. package/dist/api/ai-gateway/models/ProviderModelsAdapter.js +5 -2
  7. package/dist/api/container/common.js +8 -2
  8. package/dist/api/container/index.js +2 -2
  9. package/dist/api/container/local.js +6 -3
  10. package/dist/api/handlers/AiGatewayHandler.d.ts +1 -0
  11. package/dist/api/handlers/AiGatewayHandler.js +53 -3
  12. package/dist/api/handlers/AiGatewayManagementHandler.js +9 -0
  13. package/dist/api/handlers/TaskHandler.js +15 -10
  14. package/dist/api/network/NetworkEnvironmentConfigurationStore.js +1 -1
  15. package/dist/api/runs/ManagedRunWorker.d.ts +0 -1
  16. package/dist/api/runs/ManagedRunWorker.js +2 -14
  17. package/dist/api/runs/PiAgentRuntimeDriver.d.ts +0 -8
  18. package/dist/api/runs/PiAgentRuntimeDriver.js +65 -21
  19. package/dist/api/runs/RunStateCenter.js +1 -0
  20. package/dist/api/runs/store.d.ts +1 -0
  21. package/dist/api/runs/store.js +13 -0
  22. package/dist/api/tasks/TaskMaterializer.js +13 -6
  23. package/dist/api/tasks/TaskResumeDiagnostics.d.ts +28 -0
  24. package/dist/api/tasks/TaskResumeDiagnostics.js +148 -0
  25. package/dist/api/tasks/TaskService.js +40 -23
  26. package/dist/storage/conditions/StorageETagHandler.d.ts +1 -1
  27. package/dist/storage/conditions/StorageETagHandler.js +13 -4
  28. package/dist/util/task-model-diagnostics.d.ts +67 -0
  29. package/dist/util/task-model-diagnostics.js +118 -0
  30. package/node_modules/@undefineds.co/ai-connections/dist/AiApiKeyPool.d.ts +1 -0
  31. package/node_modules/@undefineds.co/ai-connections/dist/AiApiKeyPool.js +2 -1
  32. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectDialog.d.ts +1 -0
  33. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +1 -0
  34. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +10 -10
  35. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +3 -1
  36. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  37. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +77 -23
  38. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +1 -0
  39. package/node_modules/@undefineds.co/ai-connections/dist/AiSortableCredentialList.js +59 -59
  40. package/node_modules/@undefineds.co/ai-connections/dist/collection-runtime.js +2 -0
  41. package/node_modules/@undefineds.co/ai-connections/dist/collections.js +14 -6
  42. package/node_modules/@undefineds.co/ai-connections/dist/contract/ai-connections-client.d.ts +2 -1
  43. package/node_modules/@undefineds.co/ai-connections/dist/contract/ai-connections-client.js +1 -0
  44. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/gateway-model-roles.d.ts +11 -0
  45. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/gateway-model-roles.js +11 -0
  46. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +2 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +12 -5
  48. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +10 -2
  49. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +10 -0
  50. package/node_modules/@undefineds.co/ai-connections/package.json +1 -1
  51. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +1 -0
  52. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  53. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +60 -13
  54. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +16 -1
  55. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +51 -0
  56. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +14 -18
  57. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  58. package/node_modules/@undefineds.co/shared-ui/dist/list.d.ts +8 -1
  59. package/node_modules/@undefineds.co/shared-ui/dist/list.js +4 -1
  60. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  61. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  62. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  63. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  64. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  65. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  66. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  67. package/node_modules/@undefineds.co/shared-ui/dist/section-header.js +1 -1
  68. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +5 -32
  69. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  70. package/node_modules/@undefineds.co/solid-sdk/package.json +1 -1
  71. package/package.json +2 -2
  72. package/static/app/assets/main.css +1 -1
  73. package/static/app/assets/main.js +1 -1
  74. package/static/dashboard/assets/AccountAuthBoundary-Dv52SZc6.js +1 -0
  75. package/static/dashboard/assets/DesktopOidcCallback-DaT_Rumf.js +1 -0
  76. package/static/dashboard/assets/DevicePages-HjclpIVm.js +2 -0
  77. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +11 -0
  78. package/static/dashboard/assets/{PaneListHeader-Dn5O0mwy.js → PaneListHeader-DtvnRmFq.js} +1 -1
  79. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-D3NOG6ZM.js → PodDeletionAuthorizationPanel-BJk_8rJq.js} +3 -3
  80. package/static/dashboard/assets/PodPage-C4gzEspV.js +1 -0
  81. package/static/dashboard/assets/{SystemSettingsPage-BNQP_Yuc.js → SystemSettingsPage-HaNrP1H1.js} +1 -1
  82. package/static/dashboard/assets/TasksPage-BdO5K2L7.js +1 -0
  83. package/static/dashboard/assets/{WebDesktopEntry-vnb4CZWp.js → WebDesktopEntry-Cj9_ZfJU.js} +1 -1
  84. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +3 -0
  85. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +4 -0
  86. package/static/dashboard/assets/{account-session-DCUmCop1.js → account-session-DlcKNG7k.js} +1 -1
  87. package/static/dashboard/assets/{button-By5Bgvmb.js → button-wslF55uZ.js} +1 -1
  88. package/static/dashboard/assets/collection-runtime-uxxgEu13.js +1 -0
  89. package/static/dashboard/assets/{createLucideIcon-bunMzPms.js → createLucideIcon-BJQl82Qk.js} +1 -1
  90. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +1 -0
  91. package/static/dashboard/assets/{dashboard-mlsg3yaw.js → dashboard-Or0gnkou.js} +2 -2
  92. package/static/dashboard/assets/index-BqFeOML9.js +1 -0
  93. package/static/dashboard/assets/{index-DvMG5pHk.js → index-DgKn1twH.js} +2 -2
  94. package/static/dashboard/assets/{label-Bj03L9xs.js → label-CS6Sx6Zb.js} +1 -1
  95. package/static/dashboard/assets/list-UyhlCFKk.js +1 -0
  96. package/static/dashboard/assets/{nav-item-style-BNqWQY6p.js → nav-item-style-CW1Nnorq.js} +1 -1
  97. package/static/dashboard/assets/{pod-collections-host-KnnGFZHL.js → pod-collections-host-DjK2xf-b.js} +6 -6
  98. package/static/dashboard/assets/{safe-continuation-D9AhcWiB.js → safe-continuation-ClWAhcma.js} +1 -1
  99. package/static/dashboard/assets/{section-header-CLCzbOp_.js → section-header-CEIEcco-.js} +1 -1
  100. package/static/dashboard/assets/{setting-row-DTCec8A2.js → setting-row-CaYv3dfF.js} +1 -1
  101. package/static/dashboard/assets/xpod-remembered-login-BUF9fin1.js +310 -0
  102. package/static/dashboard/dashboard.html +2 -2
  103. package/static/settings/assets/{AccountAuthBoundary-C0og_gIA.js → AccountAuthBoundary-Chqd38oU.js} +1 -1
  104. package/static/settings/assets/{DesktopOidcCallback-C5q4oFZ0.js → DesktopOidcCallback-VUrOSM_q.js} +1 -1
  105. package/static/settings/assets/DevicePages-C6qwvWwu.js +2 -0
  106. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +11 -0
  107. package/static/settings/assets/{PaneListHeader-DqVlMe2D.js → PaneListHeader-B5RkRqr4.js} +1 -1
  108. package/static/settings/assets/{PodDeletionAuthorizationPanel-DgVvarVY.js → PodDeletionAuthorizationPanel-DPM-5Izt.js} +3 -3
  109. package/static/settings/assets/PodPage-CxND6wwc.js +1 -0
  110. package/static/settings/assets/{SystemSettingsPage-Crzm_DrI.js → SystemSettingsPage-C8EdMkW7.js} +1 -1
  111. package/static/settings/assets/TasksPage-DuqptEm8.js +1 -0
  112. package/static/settings/assets/{WebDesktopEntry-BGC5eRF2.js → WebDesktopEntry-BRcyktkv.js} +1 -1
  113. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +3 -0
  114. package/static/settings/assets/XpodShellApp-CsivHJl0.js +4 -0
  115. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +1 -0
  116. package/static/settings/assets/{XpodThemeProvider-CB6LepTA.js → XpodThemeProvider-DK3pIdRG.js} +2 -2
  117. package/static/settings/assets/{account-session-BcLjCj0x.js → account-session-DJOZ03xr.js} +1 -1
  118. package/static/settings/assets/auth-callback-GIBYEz_7.js +1 -0
  119. package/static/settings/assets/{button-BObT8pTE.js → button-D3yW-2o1.js} +1 -1
  120. package/static/settings/assets/collection-runtime-DfGo25mh.js +1 -0
  121. package/static/settings/assets/{createLucideIcon-BVsUxXpZ.js → createLucideIcon-CacwLsmB.js} +1 -1
  122. package/static/settings/assets/{index-ktjMkj7-.js → index-r5KRYsKC.js} +1 -1
  123. package/static/settings/assets/{label-BrqO8_m_.js → label-MJjUmvzG.js} +1 -1
  124. package/static/settings/assets/list-Ce-5k1MX.js +1 -0
  125. package/static/settings/assets/{nav-item-style-Di2CxkDU.js → nav-item-style-ZUbL8KOZ.js} +1 -1
  126. package/static/settings/assets/{pod-collections-host-KnnGFZHL.js → pod-collections-host-DjK2xf-b.js} +6 -6
  127. package/static/settings/assets/{safe-continuation-Dx6ceJpY.js → safe-continuation-vlBZFSlU.js} +1 -1
  128. package/static/settings/assets/{section-header-Dtl7yjVr.js → section-header-DGJdEvQw.js} +1 -1
  129. package/static/settings/assets/{setting-row-BL6Hhe8N.js → setting-row-C7tuFiho.js} +1 -1
  130. package/static/settings/assets/{settings-DaRBxoZk.js → settings-CyQPih1O.js} +1 -1
  131. package/static/settings/assets/xpod-remembered-login-D-TzzT5K.js +567 -0
  132. package/static/settings/auth-callback.html +3 -3
  133. package/static/settings/settings.html +3 -3
  134. package/static/dashboard/assets/AccountAuthBoundary-BvoDydRz.js +0 -1
  135. package/static/dashboard/assets/DesktopOidcCallback-BwIj874x.js +0 -1
  136. package/static/dashboard/assets/DevicePages-jOjMcvsr.js +0 -2
  137. package/static/dashboard/assets/ModelsPage-CMTdfqBR.js +0 -11
  138. package/static/dashboard/assets/PodPage-BueBdJ9_.js +0 -1
  139. package/static/dashboard/assets/TasksPage-CxtLJiSF.js +0 -1
  140. package/static/dashboard/assets/XpodAiConnectionsPodStore-wx3Ttupm.js +0 -18
  141. package/static/dashboard/assets/XpodShellApp-5FWY8-d5.js +0 -4
  142. package/static/dashboard/assets/collection-runtime-BnomuB0E.js +0 -1
  143. package/static/dashboard/assets/dashboard-C64R553B.css +0 -1
  144. package/static/dashboard/assets/xpod-remembered-login-Dr69A0Rv.js +0 -295
  145. package/static/settings/assets/DevicePages-fyHjjucT.js +0 -2
  146. package/static/settings/assets/ModelsPage-EyOByLyE.js +0 -11
  147. package/static/settings/assets/PodPage-DY4Oi9wg.js +0 -1
  148. package/static/settings/assets/TasksPage-CfXJbnlt.js +0 -1
  149. package/static/settings/assets/XpodAiConnectionsPodStore-CXcmgKmQ.js +0 -3
  150. package/static/settings/assets/XpodShellApp-D1PS3Iut.js +0 -4
  151. package/static/settings/assets/XpodThemeProvider-C64R553B.css +0 -1
  152. package/static/settings/assets/auth-callback-DSPQ28ar.js +0 -1
  153. package/static/settings/assets/collection-runtime-DzNbdZGe.js +0 -1
  154. package/static/settings/assets/index-browser-3WfrNbcG.js +0 -273
  155. package/static/settings/assets/xpod-remembered-login-Q66DH12t.js +0 -295
@@ -110,6 +110,7 @@ export declare class ApiServer {
110
110
  getHttpServer(): Server | undefined;
111
111
  address(): AddressInfo | string | null;
112
112
  private handleRequest;
113
+ private observeTaskGatewayResponse;
113
114
  private findRoute;
114
115
  private pathToRegex;
115
116
  private handleCors;
@@ -4,6 +4,7 @@ exports.ApiServer = void 0;
4
4
  const node_http_1 = require("node:http");
5
5
  const global_logger_factory_1 = require("global-logger-factory");
6
6
  const NodeRuntimeHost_1 = require("../runtime/host/node/NodeRuntimeHost");
7
+ const task_model_diagnostics_1 = require("../util/task-model-diagnostics");
7
8
  /**
8
9
  * Standalone API Server
9
10
  */
@@ -183,6 +184,7 @@ class ApiServer {
183
184
  const method = request.method?.toUpperCase() ?? 'GET';
184
185
  const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
185
186
  const path = url.pathname;
187
+ this.observeTaskGatewayResponse(request, response, method, path);
186
188
  // Handle CORS preflight
187
189
  if (method === 'OPTIONS') {
188
190
  this.handleCors(request, response);
@@ -241,6 +243,42 @@ class ApiServer {
241
243
  }
242
244
  }
243
245
  }
246
+ observeTaskGatewayResponse(request, response, method, path) {
247
+ if (method !== 'POST')
248
+ return;
249
+ const route = path === '/v1/chat/completions' ? 'chat_completions' :
250
+ path === '/v1/responses' ? 'responses' :
251
+ path === '/v1/messages' ? 'anthropic_messages' : undefined;
252
+ if (!route)
253
+ return;
254
+ const correlationHash = (0, task_model_diagnostics_1.hashTaskModelDiagnosticSession)(request.headers['x-opencode-session']);
255
+ if (!correlationHash)
256
+ return;
257
+ const startedAt = Date.now();
258
+ const observe = (statusSource) => {
259
+ response.off('finish', onFinish);
260
+ response.off('close', onClose);
261
+ // An unsent close has no actual caller HTTP status, regardless of statusCode.
262
+ if (statusSource === 'response_closed' && !response.headersSent)
263
+ return;
264
+ try {
265
+ const receipt = (0, task_model_diagnostics_1.selectTaskModelDiagnosticReceipt)({
266
+ event: 'xpod.task-gateway-http-diagnostic', schemaVersion: 1, scope: 'session',
267
+ correlationHash, route, callerHTTPstatus: response.statusCode, statusSource,
268
+ durationMs: Math.max(0, Math.min(2147483647, Date.now() - startedAt)),
269
+ });
270
+ if (receipt)
271
+ this.logger.error(JSON.stringify(receipt));
272
+ }
273
+ catch {
274
+ // Diagnostics are observational; a failing sink must not affect HTTP or cleanup.
275
+ }
276
+ };
277
+ const onFinish = () => observe('response_finished');
278
+ const onClose = () => observe('response_closed');
279
+ response.once('finish', onFinish);
280
+ response.once('close', onClose);
281
+ }
244
282
  findRoute(method, path) {
245
283
  for (const route of this.routes) {
246
284
  if (!route.allMethods && route.method !== method) {
@@ -17,7 +17,7 @@ class DrizzlePodAiConfigStore {
17
17
  db.findById(models_1.aiConfigResource, id),
18
18
  db.findById(XpodAiConfigSchema_1.xpodAiConfigResource, id),
19
19
  ]);
20
- return policyFromRows(sharedRow, productRow);
20
+ return policyFromRows(sharedRow, productRow, input.podUrl);
21
21
  }
22
22
  async update(input) {
23
23
  const { db } = await this.open(input);
@@ -26,7 +26,7 @@ class DrizzlePodAiConfigStore {
26
26
  db.findById(models_1.aiConfigResource, id),
27
27
  db.findById(XpodAiConfigSchema_1.xpodAiConfigResource, id),
28
28
  ]);
29
- const mergedPolicy = mergePolicy(policyFromRows(sharedCurrent, productCurrent), input.patch, this.now());
29
+ const mergedPolicy = mergePolicy(policyFromRows(sharedCurrent, productCurrent, input.podUrl), input.patch, this.now(), input.podUrl);
30
30
  await Promise.all([
31
31
  writeRow(db, models_1.aiConfigResource, id, sharedCurrent, sharedRowFromPolicy(mergedPolicy)),
32
32
  writeRow(db, XpodAiConfigSchema_1.xpodAiConfigResource, id, productCurrent, productRowFromPolicy(mergedPolicy)),
@@ -49,7 +49,7 @@ exports.DrizzlePodAiConfigStore = DrizzlePodAiConfigStore;
49
49
  function aiConfigResourceId() {
50
50
  return models_1.aiConfigResource.buildId({ id: 'config' });
51
51
  }
52
- function policyFromRows(sharedRow, productRow) {
52
+ function policyFromRows(sharedRow, productRow, podUrl) {
53
53
  const defaults = defaultPolicy();
54
54
  if (!sharedRow && !productRow)
55
55
  return defaults;
@@ -58,7 +58,7 @@ function policyFromRows(sharedRow, productRow) {
58
58
  const models = {};
59
59
  for (const key of modelKeys) {
60
60
  if (typeof shared[key] === 'string' && shared[key])
61
- models[key] = shared[key];
61
+ models[key] = canonicalModelReference(shared[key], podUrl);
62
62
  }
63
63
  return {
64
64
  schemaVersion: '1.0',
@@ -92,14 +92,14 @@ function policyFromRows(sharedRow, productRow) {
92
92
  updatedAt: isoDate(shared.updatedAt),
93
93
  };
94
94
  }
95
- function mergePolicy(current, patch, now) {
95
+ function mergePolicy(current, patch, now, podUrl) {
96
96
  const models = { ...current.models };
97
97
  for (const key of modelKeys) {
98
98
  const value = patch.models?.[key];
99
99
  if (value === null)
100
100
  delete models[key];
101
101
  else if (value !== undefined)
102
- models[key] = value;
102
+ models[key] = canonicalModelReference(value, podUrl);
103
103
  }
104
104
  return {
105
105
  schemaVersion: '1.0',
@@ -110,6 +110,18 @@ function mergePolicy(current, patch, now) {
110
110
  updatedAt: now.toISOString(),
111
111
  };
112
112
  }
113
+ /** API refs may be Pod-root-relative; linked ORM columns require target-relative ids or absolute IRIs. */
114
+ function canonicalModelReference(ref, podUrl) {
115
+ if (/^[a-z][a-z\d+.-]*:/i.test(ref)) {
116
+ // Validate without serializing: external IRI lexical identity must survive.
117
+ new URL(ref);
118
+ return ref;
119
+ }
120
+ const parsed = models_1.aiModelResource.parseRef(ref);
121
+ if (!parsed)
122
+ throw new Error('Invalid AI model resource reference');
123
+ return models_1.aiModelResource.buildIri(podUrl, { id: parsed.resourceId });
124
+ }
113
125
  function sharedRowFromPolicy(policy) {
114
126
  return {
115
127
  ...Object.fromEntries(modelKeys.map((key) => [key, policy.models[key] ?? null])),
@@ -687,6 +687,7 @@ export declare class ProviderConnectService {
687
687
  credentialId: string;
688
688
  expectedVersion: number;
689
689
  patch: {
690
+ apiKey?: string;
690
691
  label?: string;
691
692
  enabled?: boolean;
692
693
  priority?: number;
@@ -1874,28 +1874,49 @@ class ProviderConnectService {
1874
1874
  throw new Error('credential_pool_not_configured');
1875
1875
  }
1876
1876
  const existing = await this.credentialRepository.getCredentialById(input);
1877
- const metadata = metadataFromRowValue(existing?.metadata) ?? {};
1877
+ if (!existing)
1878
+ return undefined;
1879
+ if (input.expectedVersion !== (existing.version ?? 0))
1880
+ throw new Error('credential_version_conflict');
1881
+ const replacingKey = Object.prototype.hasOwnProperty.call(input.patch, 'apiKey');
1882
+ let replacement = {};
1883
+ if (replacingKey) {
1884
+ if (typeof input.patch.apiKey !== 'string' || !input.patch.apiKey.trim())
1885
+ throw new Error('invalid_api_key');
1886
+ if (existing.authMode !== 'apiKey')
1887
+ throw new Error('credential_auth_mode_mismatch');
1888
+ if (!this.vault)
1889
+ throw new Error('credential_pool_not_configured');
1890
+ replacement = {
1891
+ encryptedSecret: await this.vault.seal({ webId: input.webId }, existing.credentialIri, existing.provider, { type: 'apiKey', apiKey: input.patch.apiKey.trim() }),
1892
+ reauthRequired: false, failCount: 0, lastFailureCode: undefined,
1893
+ lastFailureAt: undefined, rateLimitResetAt: undefined,
1894
+ health: (input.patch.enabled ?? existing.enabled) === false ? 'disabled' : 'unknown',
1895
+ };
1896
+ }
1897
+ const metadata = metadataFromRowValue(existing.metadata) ?? {};
1878
1898
  const updated = await this.credentialRepository.updateCredential({
1879
1899
  ...input,
1880
- patch: metadataWithoutUndefined({
1881
- accountLabel: input.patch.label,
1882
- enabled: input.patch.enabled,
1883
- priority: input.patch.priority,
1884
- proxyUrl: input.patch.proxyUrl === undefined
1885
- ? undefined
1886
- : (0, provider_http_transport_1.normalizeProviderProxyUrl)(input.patch.proxyUrl),
1887
- health: input.patch.enabled === false ? 'disabled' : undefined,
1888
- metadata: metadataWithoutUndefined({
1889
- ...metadata,
1890
- baseUrl: input.patch.baseUrl ?? metadata.baseUrl,
1900
+ patch: { ...metadataWithoutUndefined({
1901
+ accountLabel: input.patch.label,
1902
+ enabled: input.patch.enabled,
1903
+ priority: input.patch.priority,
1891
1904
  proxyUrl: input.patch.proxyUrl === undefined
1892
- ? metadata.proxyUrl
1905
+ ? undefined
1893
1906
  : (0, provider_http_transport_1.normalizeProviderProxyUrl)(input.patch.proxyUrl),
1894
- priority: input.patch.priority ?? metadata.priority,
1895
- enabled: input.patch.enabled ?? metadata.enabled,
1896
- health: input.patch.enabled === false ? 'disabled' : metadata.health,
1897
- }),
1898
- }),
1907
+ health: input.patch.enabled === false ? 'disabled' : undefined,
1908
+ metadata: metadataWithoutUndefined({
1909
+ ...metadata,
1910
+ baseUrl: input.patch.baseUrl ?? metadata.baseUrl,
1911
+ proxyUrl: input.patch.proxyUrl === undefined
1912
+ ? metadata.proxyUrl
1913
+ : (0, provider_http_transport_1.normalizeProviderProxyUrl)(input.patch.proxyUrl),
1914
+ priority: input.patch.priority ?? metadata.priority,
1915
+ enabled: input.patch.enabled ?? metadata.enabled,
1916
+ health: replacingKey ? replacement.health : input.patch.enabled === false ? 'disabled' : metadata.health,
1917
+ ...(replacingKey ? { maskedHint: maskApiKey(input.patch.apiKey.trim()) } : {}),
1918
+ }),
1919
+ }), ...replacement },
1899
1920
  });
1900
1921
  return updated ? publicPoolCredentialSummary(updated) : undefined;
1901
1922
  }
@@ -29,12 +29,15 @@ class ProviderModelsResponseError extends Error {
29
29
  }
30
30
  }
31
31
  exports.ProviderModelsResponseError = ProviderModelsResponseError;
32
+ // Codex protocol compatibility baseline, independent of Xpod and ACP wrapper versions.
33
+ // Verified release: https://github.com/openai/codex/releases/tag/rust-v0.160.0
34
+ const CODEX_MODELS_CLIENT_VERSION = '0.160.0';
32
35
  /** Discovers the models currently enabled for a ChatGPT Codex subscription. */
33
36
  class CodexSubscriptionModelsAdapter {
34
37
  constructor(options = {}) {
35
38
  this.protocol = 'codex-models';
36
39
  this.transport = options.transport ?? new provider_http_transport_1.ProviderHttpTransport();
37
- this.clientVersion = options.clientVersion ?? '0.0.0';
40
+ this.clientVersion = options.clientVersion ?? CODEX_MODELS_CLIENT_VERSION;
38
41
  }
39
42
  async fetch(input) {
40
43
  const accessToken = stringSecret(input.secret, 'accessToken', 'access_token');
@@ -57,7 +60,7 @@ class CodexSubscriptionModelsAdapter {
57
60
  const models = Array.isArray(body?.models) ? body.models : [];
58
61
  return models
59
62
  .filter((model) => Boolean(model) && typeof model === 'object')
60
- .filter((model) => model.visibility !== 'hide')
63
+ .filter((model) => model.visibility === 'list')
61
64
  .map((model) => ({
62
65
  id: String(model.slug ?? model.id ?? '').trim(),
63
66
  ...(typeof model.display_name === 'string' && model.display_name.trim()
@@ -88,13 +88,19 @@ function resolveHostedPodCssBaseUrl(config) {
88
88
  // instead of a foreign loopback port.
89
89
  return config.solidBaseUrl;
90
90
  }
91
- function resolveAiConnectionsBaseUrl(config) {
92
- const origin = config.publicUrl
91
+ function resolveAiConnectionsCanonicalOrigin(config) {
92
+ return config.publicUrl
93
93
  ?? process.env.XPOD_PUBLIC_URL
94
94
  ?? process.env.CSS_BASE_URL
95
95
  ?? `http://${config.host === '0.0.0.0' ? '127.0.0.1' : config.host}:${config.port}`;
96
+ }
97
+ function aiConnectionsV1Url(origin) {
96
98
  return new URL('/v1', origin.endsWith('/') ? origin : `${origin}/`).toString().replace(/\/$/u, '');
97
99
  }
100
+ /** The canonical identity realm: token audience/issuer and the two authenticators use this. */
101
+ function resolveAiConnectionsBaseUrl(config) {
102
+ return aiConnectionsV1Url(resolveAiConnectionsCanonicalOrigin(config));
103
+ }
98
104
  function credentialVaultForConfig(config) {
99
105
  return new PlaintextCredentialVault_1.PlaintextCredentialVault({
100
106
  legacyVault: config.secretCellCredentialVaultFactory?.(),
@@ -220,8 +220,8 @@ function loadConfigFromEnv() {
220
220
  baseStorageDomain: process.env.CSS_BASE_STORAGE_DOMAIN,
221
221
  cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
222
222
  cloudflareApiToken: process.env.CLOUDFLARE_API_TOKEN,
223
- tencentDnsSecretId: process.env.TENCENT_DNS_SECRET_ID,
224
- tencentDnsSecretKey: process.env.TENCENT_DNS_SECRET_KEY,
223
+ tencentDnsSecretId: process.env.TENCENT_DNSPOD_ID,
224
+ tencentDnsSecretKey: process.env.TENCENT_DNSPOD_TOKEN,
225
225
  },
226
226
  // Local 托管式:连接 Cloud
227
227
  cloudApiEndpoint,
@@ -137,9 +137,12 @@ function registerLocalServices(container) {
137
137
  // The settings page lets an operator pick the DNS provider, so the runtime honours that
138
138
  // choice instead of always wiring Cloudflare and ignoring a saved "tencent".
139
139
  const dnsProviderId = process.env.XPOD_DNS_PROVIDER?.trim().toLowerCase() || 'cloudflare';
140
- const tencentDnsToken = process.env.XPOD_TENCENT_DNS_TOKEN?.trim();
141
- const tencentDnsTokenId = process.env.XPOD_TENCENT_DNS_TOKEN_ID?.trim();
142
- const dnsCredentialReady = dnsProviderId === 'tencent' ? Boolean(tencentDnsToken) : Boolean(apiToken);
140
+ const tencentDnsToken = process.env.TENCENT_DNSPOD_TOKEN?.trim();
141
+ const tencentDnsTokenId = process.env.TENCENT_DNSPOD_ID?.trim();
142
+ // DNSPod 需要 ID 与 Token 同时具备;只看 token 会注册一个实际禁用的 provider(静默失效)。
143
+ const dnsCredentialReady = dnsProviderId === 'tencent'
144
+ ? Boolean(tencentDnsToken && tencentDnsTokenId)
145
+ : Boolean(apiToken);
143
146
  if (dnsCredentialReady && baseDomain) {
144
147
  console.log(`[Local] Self-hosted DNS mode detected (provider: ${dnsProviderId})`);
145
148
  container.register({
@@ -18,6 +18,7 @@ export declare class AiGatewayHandler {
18
18
  handleModels(request: AuthenticatedRequest, response: ServerResponse): Promise<void>;
19
19
  handleAcceptanceProvenance(request: AuthenticatedRequest, response: ServerResponse): Promise<void>;
20
20
  private sendEventStream;
21
+ private logTaskGatewayFailure;
21
22
  private logInference;
22
23
  private writeTerminalStreamError;
23
24
  private sendGatewayError;
@@ -6,7 +6,9 @@ const global_logger_factory_1 = require("global-logger-factory");
6
6
  const readBoundedJsonBody_1 = require("../http/readBoundedJsonBody");
7
7
  const errors_1 = require("../ai-gateway/errors");
8
8
  const InvocationMetadata_1 = require("../ai-gateway/InvocationMetadata");
9
+ const task_model_diagnostics_1 = require("../../util/task-model-diagnostics");
9
10
  const DEFAULT_JSON_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
11
+ const TASK_GATEWAY_CODES = new Set(task_model_diagnostics_1.TASK_GATEWAY_DIAGNOSTIC_CODES);
10
12
  function registerAiGatewayRoutes(server, options) {
11
13
  const handler = new AiGatewayHandler(options);
12
14
  server.post('/v1/responses', (request, response) => handler.handleInference(request, response, 'responses'));
@@ -25,12 +27,20 @@ class AiGatewayHandler {
25
27
  }
26
28
  async handleInference(request, response, protocol) {
27
29
  const startedAt = Date.now();
30
+ // Observation only: preserve the existing body/header validation ordering.
31
+ let correlationHash;
32
+ try {
33
+ correlationHash = (0, task_model_diagnostics_1.hashTaskModelDiagnosticSession)((0, InvocationMetadata_1.readGatewayInvocationMetadata)(request.headers, request.rawHeaders).sessionId);
34
+ }
35
+ catch { /* Invalid invocation metadata is rejected by the existing path. */ }
28
36
  const bodyResult = await (0, readBoundedJsonBody_1.readBoundedJsonBody)(request, { limitBytes: this.jsonBodyLimitBytes });
29
37
  if (!bodyResult.ok) {
30
- this.sendGatewayError(response, new errors_1.GatewayProtocolError(bodyResult.error, {
38
+ const error = new errors_1.GatewayProtocolError(bodyResult.error, {
31
39
  code: 'invalid_request',
32
40
  status: bodyResult.status,
33
- }));
41
+ });
42
+ this.sendGatewayError(response, error);
43
+ this.logTaskGatewayFailure(correlationHash, response, protocol, startedAt, false, taskGatewayFailure(error));
34
44
  return;
35
45
  }
36
46
  const model = readRequestedModel(bodyResult.value);
@@ -69,16 +79,25 @@ class AiGatewayHandler {
69
79
  });
70
80
  const outcome = await this.sendEventStream(response, execution.frontend, execution.events);
71
81
  this.logInference({ protocol, model, stream: true, startedAt, ...outcome });
82
+ if (outcome.failure) {
83
+ this.logTaskGatewayFailure(correlationHash, response, protocol, startedAt, true, outcome.failure);
84
+ }
72
85
  }
73
86
  catch (error) {
74
87
  if (controller.signal.aborted || response.destroyed) {
75
88
  return;
76
89
  }
77
90
  if (response.headersSent) {
78
- await this.writeTerminalStreamError(response, error);
91
+ try {
92
+ await this.writeTerminalStreamError(response, error);
93
+ }
94
+ finally {
95
+ this.logTaskGatewayFailure(correlationHash, response, protocol, startedAt, isStreamRequest(bodyResult.value), taskGatewayFailure(error));
96
+ }
79
97
  return;
80
98
  }
81
99
  this.sendGatewayError(response, error);
100
+ this.logTaskGatewayFailure(correlationHash, response, protocol, startedAt, false, taskGatewayFailure(error));
82
101
  }
83
102
  finally {
84
103
  response.off('close', abort);
@@ -175,6 +194,7 @@ class AiGatewayHandler {
175
194
  }
176
195
  catch (error) {
177
196
  outcome.outcome = disconnected ? 'disconnected' : 'failed';
197
+ outcome.failure = taskGatewayFailure(error);
178
198
  await returnIterator();
179
199
  if (!disconnected) {
180
200
  await this.writeTerminalStreamError(response, error);
@@ -188,9 +208,26 @@ class AiGatewayHandler {
188
208
  }
189
209
  if (disconnected) {
190
210
  outcome.outcome = 'disconnected';
211
+ outcome.failure ??= taskGatewayFailure(undefined);
191
212
  }
192
213
  return outcome;
193
214
  }
215
+ logTaskGatewayFailure(correlationHash, response, protocol, startedAt, streamOpen, failure) {
216
+ if (!correlationHash || !diagnosticHTTPStatus(response.statusCode))
217
+ return;
218
+ const route = protocol === 'chatCompletions' ? 'chat_completions'
219
+ : protocol === 'responses' ? 'responses' : 'anthropic_messages';
220
+ try {
221
+ const receipt = (0, task_model_diagnostics_1.selectTaskModelDiagnosticReceipt)({
222
+ event: 'xpod.task-gateway-diagnostic', schemaVersion: 1, scope: 'session', correlationHash, route,
223
+ callerHTTPstatus: response.statusCode, ...failure, streamOpen,
224
+ durationMs: Math.min(2_147_483_647, Math.max(0, Math.floor(Date.now() - startedAt))),
225
+ });
226
+ if (receipt)
227
+ this.logger.error(JSON.stringify(receipt));
228
+ }
229
+ catch { /* An observational receipt must not alter the response or cleanup. */ }
230
+ }
194
231
  logInference(summary) {
195
232
  const fields = [
196
233
  `protocol=${summary.protocol}`,
@@ -232,6 +269,19 @@ class AiGatewayHandler {
232
269
  }
233
270
  }
234
271
  exports.AiGatewayHandler = AiGatewayHandler;
272
+ function diagnosticHTTPStatus(value) {
273
+ return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599 ? value : undefined;
274
+ }
275
+ function taskGatewayFailure(error) {
276
+ const code = (0, errors_1.normalizeGatewayError)(error).error.code;
277
+ const source = error && typeof error === 'object' ? error : undefined;
278
+ const cause = source?.cause && typeof source.cause === 'object' ? source.cause : undefined;
279
+ const underlyingErrorStatus = diagnosticHTTPStatus(cause?.status) ?? diagnosticHTTPStatus(source?.status);
280
+ return {
281
+ code: TASK_GATEWAY_CODES.has(code) ? code : 'internal_error',
282
+ ...(underlyingErrorStatus !== undefined ? { underlyingErrorStatus } : {}),
283
+ };
284
+ }
235
285
  async function writeSerializedEvents(response, serialized) {
236
286
  for (const event of Array.isArray(serialized) ? serialized : [serialized]) {
237
287
  await writeWithBackpressure(response, `data: ${JSON.stringify(event)}\n\n`);
@@ -1200,6 +1200,11 @@ function publicGatewayAccessKeyRecord(record, plaintextAvailable, plaintext) {
1200
1200
  }
1201
1201
  function normalizeCredentialPatch(body) {
1202
1202
  const patch = {};
1203
+ if (Object.prototype.hasOwnProperty.call(body, 'apiKey')) {
1204
+ if (typeof body.apiKey !== 'string' || !body.apiKey.trim())
1205
+ return undefined;
1206
+ patch.apiKey = body.apiKey.trim();
1207
+ }
1203
1208
  if (body.label !== undefined) {
1204
1209
  const label = normalizeOptionalString(body.label);
1205
1210
  if (!label) {
@@ -1330,6 +1335,10 @@ function sendCredentialPoolError(response, error) {
1330
1335
  sendJson(response, message === 'local_session_refresh_failed' ? 502 : 409, { error: message });
1331
1336
  return;
1332
1337
  }
1338
+ if (message === 'invalid_api_key' || message === 'credential_auth_mode_mismatch') {
1339
+ sendJson(response, 400, { error: message });
1340
+ return;
1341
+ }
1333
1342
  if (message === 'credential_version_conflict') {
1334
1343
  sendJson(response, 409, { error: 'credential_version_conflict' });
1335
1344
  return;
@@ -2,11 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerTaskRoutes = registerTaskRoutes;
4
4
  exports.projectTask = projectTask;
5
+ const TaskResumeDiagnostics_1 = require("../tasks/TaskResumeDiagnostics");
5
6
  const RunCancellation_1 = require("../runs/RunCancellation");
6
7
  const PodAccessFailureResponse_1 = require("./PodAccessFailureResponse");
7
8
  /** UI reads durable Pod projections; the scheduler is never a query backend. */
8
9
  function registerTaskRoutes(server, options) {
9
- const guarded = (handler) => async (request, response) => {
10
+ const guarded = (handler, resume = false) => async (request, response) => {
10
11
  if (request.auth?.type !== 'solid') {
11
12
  send(response, 401, { error: 'Authentication required' });
12
13
  return;
@@ -17,7 +18,7 @@ function registerTaskRoutes(server, options) {
17
18
  catch (error) {
18
19
  if ((0, PodAccessFailureResponse_1.sendPodAccessFailure)(response, error))
19
20
  return;
20
- send(response, 400, { error: error instanceof Error ? error.message : 'Task request failed' });
21
+ send(response, 400, { error: error instanceof Error ? error.message : 'Task request failed', ...(resume ? { taskResumeStage: (0, TaskResumeDiagnostics_1.getTaskResumeStage)(error) ?? 'route_request', taskResumeErrorType: (0, TaskResumeDiagnostics_1.getTaskResumeErrorType)(error), taskResumeFailure: (0, TaskResumeDiagnostics_1.getTaskResumeFailure)(error) } : {}) });
21
22
  }
22
23
  };
23
24
  const idOf = (request) => {
@@ -96,16 +97,20 @@ function registerTaskRoutes(server, options) {
96
97
  return { runs: (await options.runStore.listRuns({ task: task.id }, context)).map(projectRun) };
97
98
  }));
98
99
  server.post('/api/tasks/resume', guarded(async (request, context, owner) => {
99
- const input = await body(request);
100
- const approval = requiredString(input.approval, 'Approval');
101
- const target = idOf(request);
102
- const run = (await options.runStore.listRuns({}, context)).find(item => item.id === target)
103
- ?? (target.startsWith('https://') || target.startsWith('http://') ? undefined : await options.runStore.loadRun(target, context));
104
- if (!run)
105
- throw new Error('Run not found');
100
+ const { approval, target } = await (0, TaskResumeDiagnostics_1.withTaskResumeStage)('route_validation', async () => {
101
+ const input = await body(request);
102
+ return { approval: requiredString(input.approval, 'Approval'), target: idOf(request) };
103
+ });
104
+ const run = await (0, TaskResumeDiagnostics_1.withTaskResumeStage)('route_run_read', async () => {
105
+ const run = (await options.runStore.listRuns({}, context)).find(item => item.id === target)
106
+ ?? (target.startsWith('https://') || target.startsWith('http://') ? undefined : await options.runStore.loadRun(target, context));
107
+ if (!run)
108
+ throw new Error('Run not found');
109
+ return run;
110
+ });
106
111
  const result = await options.taskService.resumeApprovedRun({ runId: run.id, approval, owner }, context, options.resolveExecutionContext);
107
112
  return { ...result, run: projectRun(result.run) };
108
- }));
113
+ }, true));
109
114
  server.get('/api/tasks/selection', guarded(async (request, context, owner) => {
110
115
  const target = idOf(request);
111
116
  const runs = await options.runStore.listRuns({}, context);
@@ -159,7 +159,7 @@ function firstString(...values) {
159
159
  function splitList(value) { return value?.split(',').map((item) => item.trim()).filter(Boolean) ?? []; }
160
160
  function positiveInteger(value, fallback) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; }
161
161
  function dnsCredential(env, provider) { return env[dnsCredentialKey(provider)]; }
162
- function dnsCredentialKey(provider) { return provider === 'tencent' ? 'XPOD_TENCENT_DNS_TOKEN' : provider === 'cloudflare' ? 'CLOUDFLARE_API_TOKEN' : 'XPOD_DNS_PROVIDER_TOKEN'; }
162
+ function dnsCredentialKey(provider) { return provider === 'tencent' ? 'TENCENT_DNSPOD_TOKEN' : provider === 'cloudflare' ? 'CLOUDFLARE_API_TOKEN' : 'XPOD_DNS_PROVIDER_TOKEN'; }
163
163
  function p2pFallback(value) { return value === 'never' || value === 'prefer-p2p' ? value : 'when-direct-unavailable'; }
164
164
  function plainStringRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && Object.values(value).every((item) => typeof item === 'string'); }
165
165
  function boolString(value) { return value === undefined ? undefined : String(value); }
@@ -37,7 +37,6 @@ export declare class ManagedRunWorker<TContext = StoreContext> {
37
37
  private loadRunInput;
38
38
  private resolveUserMessage;
39
39
  private resolveRuntimeConfig;
40
- private authBindingIdFromRun;
41
40
  private asRuntimeConfig;
42
41
  private parseRunner;
43
42
  private isRunnerType;
@@ -76,7 +76,7 @@ class ManagedRunWorker {
76
76
  conversation,
77
77
  retrievedContext,
78
78
  config: runtimeConfig,
79
- authBindingId: this.authBindingIdFromRun(run),
79
+ authBindingId: (0, store_1.resolveRunAuthBindingId)(run),
80
80
  context: context,
81
81
  })) {
82
82
  const cancelled = await finishCancellation();
@@ -209,7 +209,7 @@ class ManagedRunWorker {
209
209
  conversation,
210
210
  retrievedContext,
211
211
  config: loaded.runtimeConfig,
212
- authBindingId: this.authBindingIdFromRun(loaded.run),
212
+ authBindingId: (0, store_1.resolveRunAuthBindingId)(loaded.run),
213
213
  context: context,
214
214
  };
215
215
  }
@@ -266,18 +266,6 @@ class ManagedRunWorker {
266
266
  runner: this.parseRunner(run.runner),
267
267
  };
268
268
  }
269
- authBindingIdFromRun(run) {
270
- const authBindingId = run.metadata?.authBindingId;
271
- if (typeof authBindingId === 'string' && authBindingId.length > 0) {
272
- return authBindingId;
273
- }
274
- const authBinding = run.metadata?.authBinding;
275
- if (authBinding && typeof authBinding === 'object') {
276
- const id = authBinding.id;
277
- return typeof id === 'string' && id.length > 0 ? id : undefined;
278
- }
279
- return undefined;
280
- }
281
269
  asRuntimeConfig(value) {
282
270
  if (!value || typeof value !== 'object') {
283
271
  return undefined;
@@ -97,14 +97,6 @@ export declare class PiAgentRuntimeDriver implements RunExecutionBackend {
97
97
  private workerEnv;
98
98
  private extractText;
99
99
  private formatError;
100
- /**
101
- * Record a failed assistant turn with allowlisted wire facts only.
102
- *
103
- * The provider `errorMessage` can contain credentials, prompts, or response bodies, so it never
104
- * leaves this method. What is kept is enough to place the failure: the protocol/provider/model
105
- * and a stable coarse classification derived from the shape of the message (never its text).
106
- */
107
- private logAssistantFailure;
108
100
  private startupErrorToEvent;
109
101
  private logWorkspaceRollbackError;
110
102
  private loadPiSdk;