@agent-native/core 0.79.20 → 0.79.22

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 (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/deployment.md +133 -0
  4. package/corpus/core/docs/content/faq.md +1 -1
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -0
  6. package/corpus/core/docs/content/tracking.md +3 -3
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
  9. package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
  10. package/corpus/core/src/agent/engine/registry.ts +59 -11
  11. package/corpus/core/src/client/FeedbackButton.tsx +11 -1
  12. package/corpus/core/src/client/agent-engine-key.ts +57 -10
  13. package/corpus/core/src/client/analytics.ts +78 -27
  14. package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
  15. package/corpus/core/src/client/index.ts +2 -0
  16. package/corpus/core/src/client/session-replay.ts +159 -17
  17. package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
  18. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
  19. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
  20. package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
  21. package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
  22. package/corpus/core/src/server/core-routes-plugin.ts +26 -2
  23. package/corpus/core/src/styles/blocks.css +23 -7
  24. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
  25. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +5 -10
  26. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
  29. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
  31. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
  37. package/corpus/templates/analytics/netlify.toml +4 -0
  38. package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
  40. package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
  41. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
  42. package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
  43. package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
  44. package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  45. package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
  46. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +20 -4
  47. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -2
  48. package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-27-desktop-storage-setup-flow.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +6 -0
  52. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  53. package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
  54. package/corpus/templates/clips/desktop/package.json +1 -0
  55. package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
  56. package/corpus/templates/clips/desktop/src/app.tsx +32 -3
  57. package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
  58. package/corpus/templates/clips/desktop/src/main.tsx +2 -0
  59. package/corpus/templates/clips/desktop/src/styles.css +67 -0
  60. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
  61. package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
  64. package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
  65. package/corpus/templates/clips/desktop/vite.config.ts +66 -0
  66. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  68. package/corpus/templates/forms/actions/export-responses.ts +3 -2
  69. package/corpus/templates/forms/actions/list-responses.ts +2 -1
  70. package/corpus/templates/forms/actions/response-insights.ts +13 -4
  71. package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
  72. package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
  73. package/corpus/templates/forms/server/lib/integrations.ts +9 -6
  74. package/corpus/templates/forms/shared/submitter-email.ts +20 -0
  75. package/corpus/templates/forms/shared/types.ts +1 -1
  76. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +6 -0
  79. package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
  80. package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
  81. package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
  82. package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
  83. package/dist/agent/engine/provider-env-vars.d.ts +1 -0
  84. package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
  85. package/dist/agent/engine/provider-env-vars.js +1 -0
  86. package/dist/agent/engine/provider-env-vars.js.map +1 -1
  87. package/dist/agent/engine/registry.d.ts.map +1 -1
  88. package/dist/agent/engine/registry.js +39 -12
  89. package/dist/agent/engine/registry.js.map +1 -1
  90. package/dist/client/FeedbackButton.d.ts.map +1 -1
  91. package/dist/client/FeedbackButton.js +6 -1
  92. package/dist/client/FeedbackButton.js.map +1 -1
  93. package/dist/client/agent-engine-key.d.ts +14 -0
  94. package/dist/client/agent-engine-key.d.ts.map +1 -1
  95. package/dist/client/agent-engine-key.js +35 -10
  96. package/dist/client/agent-engine-key.js.map +1 -1
  97. package/dist/client/analytics.d.ts.map +1 -1
  98. package/dist/client/analytics.js +71 -24
  99. package/dist/client/analytics.js.map +1 -1
  100. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  101. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  102. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  103. package/dist/client/index.d.ts +1 -1
  104. package/dist/client/index.d.ts.map +1 -1
  105. package/dist/client/index.js +1 -1
  106. package/dist/client/index.js.map +1 -1
  107. package/dist/client/session-replay.d.ts +1 -1
  108. package/dist/client/session-replay.d.ts.map +1 -1
  109. package/dist/client/session-replay.js +115 -17
  110. package/dist/client/session-replay.js.map +1 -1
  111. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  112. package/dist/client/settings/SettingsPanel.js +42 -6
  113. package/dist/client/settings/SettingsPanel.js.map +1 -1
  114. package/dist/collab/routes.d.ts +2 -2
  115. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  116. package/dist/notifications/routes.d.ts +1 -1
  117. package/dist/observability/routes.d.ts +7 -7
  118. package/dist/progress/routes.d.ts +1 -1
  119. package/dist/resources/handlers.d.ts +1 -1
  120. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  121. package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
  122. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  123. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  124. package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
  125. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  126. package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
  127. package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
  128. package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
  129. package/dist/server/agent-engine-api-key-route.d.ts +5 -5
  130. package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
  131. package/dist/server/agent-engine-api-key-route.js +71 -11
  132. package/dist/server/agent-engine-api-key-route.js.map +1 -1
  133. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  134. package/dist/server/core-routes-plugin.js +20 -3
  135. package/dist/server/core-routes-plugin.js.map +1 -1
  136. package/dist/styles/blocks.css +23 -7
  137. package/docs/content/deployment.md +133 -0
  138. package/docs/content/faq.md +1 -1
  139. package/docs/content/multi-app-workspace.md +2 -0
  140. package/docs/content/tracking.md +3 -3
  141. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2031
31
- - template files: 4416
30
+ - core files: 2032
31
+ - template files: 4434
@@ -1,5 +1,21 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.79.22
4
+
5
+ ### Patch Changes
6
+
7
+ - cdf41db: Require signed-in email identity for browser session replay by default across templates and send replay timing/event-count metadata with each upload. Apps that intentionally record anonymous replay sessions can opt out with `sessionReplay.requireSignedInUser: false` or `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false`.
8
+ - cdf41db: Keep diagram primitive borders visible in sketchy Plan diagrams and stop the shared feedback button from sending synthetic anonymous Agent Native emails.
9
+ - cdf41db: Add an advanced OpenAI-compatible endpoint URL setting for custom gateways like LiteLLM.
10
+ - cdf41db: Default shared expandable code surfaces to 30 visible lines before the reader
11
+ expands them.
12
+
13
+ ## 0.79.21
14
+
15
+ ### Patch Changes
16
+
17
+ - 3acd677: Clarify workspace Docker Compose deployment for self-hosted VPS installs.
18
+
3
19
  ## 0.79.20
4
20
 
5
21
  ### Patch Changes
@@ -145,6 +145,139 @@ EXPOSE 3000
145
145
  CMD ["node", ".output/server/index.mjs"]
146
146
  ```
147
147
 
148
+ ### Self-hosted workspace with Docker Compose {#workspace-docker-compose}
149
+
150
+ The Dockerfile above is for one standalone app. A workspace is not one Nitro
151
+ server process today: each app still builds to its own `.output/` directory and
152
+ runs its own Node/Nitro server. For a VPS, run one container per workspace app,
153
+ point every app at the same Postgres database and shared production secrets, and
154
+ put a reverse proxy in front that routes path prefixes (`/mail`, `/calendar`,
155
+ ...) to the matching app container.
156
+
157
+ Build each app with the same base path that the proxy will use at runtime. The
158
+ base path is a build-time input for the client bundle and a runtime input for
159
+ the server:
160
+
161
+ ```bash maxLines=8
162
+ APP_BASE_PATH=/mail VITE_APP_BASE_PATH=/mail npx @agent-native/core@latest build
163
+ node .output/server/index.mjs
164
+ ```
165
+
166
+ A reusable workspace Dockerfile can take the app name and path prefix as build
167
+ arguments:
168
+
169
+ ```dockerfile maxLines=12
170
+ FROM node:24-slim AS build
171
+ WORKDIR /workspace
172
+ RUN corepack enable
173
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
174
+ COPY packages ./packages
175
+ COPY apps ./apps
176
+ ARG APP_NAME
177
+ ARG APP_BASE_PATH
178
+ ENV APP_BASE_PATH=$APP_BASE_PATH
179
+ ENV VITE_APP_BASE_PATH=$APP_BASE_PATH
180
+ RUN pnpm install --frozen-lockfile
181
+ RUN pnpm --dir apps/$APP_NAME build
182
+
183
+ FROM node:24-slim
184
+ WORKDIR /app
185
+ ARG APP_NAME
186
+ ARG APP_BASE_PATH
187
+ ENV NODE_ENV=production
188
+ ENV PORT=3000
189
+ ENV APP_BASE_PATH=$APP_BASE_PATH
190
+ COPY --from=build /workspace/apps/$APP_NAME/.output .output
191
+ EXPOSE 3000
192
+ CMD ["node", ".output/server/index.mjs"]
193
+ ```
194
+
195
+ Then compose Postgres, the app containers, and your proxy. This example uses
196
+ Caddy because it can terminate HTTP locally or sit behind Cloudflare's SSL
197
+ proxy, but the important part is the path-prefix routing:
198
+
199
+ ```yaml maxLines=30
200
+ services:
201
+ postgres:
202
+ image: postgres:17
203
+ restart: unless-stopped
204
+ environment:
205
+ POSTGRES_DB: agent_native
206
+ POSTGRES_USER: agent_native
207
+ POSTGRES_PASSWORD: change-me
208
+ volumes:
209
+ - postgres-data:/var/lib/postgresql/data
210
+
211
+ mail:
212
+ build:
213
+ context: .
214
+ dockerfile: Dockerfile.workspace-app
215
+ args:
216
+ APP_NAME: mail
217
+ APP_BASE_PATH: /mail
218
+ restart: unless-stopped
219
+ environment:
220
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
221
+ BETTER_AUTH_URL: https://agents.example.com/mail
222
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
223
+ A2A_SECRET: ${A2A_SECRET}
224
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
225
+ depends_on:
226
+ - postgres
227
+
228
+ calendar:
229
+ build:
230
+ context: .
231
+ dockerfile: Dockerfile.workspace-app
232
+ args:
233
+ APP_NAME: calendar
234
+ APP_BASE_PATH: /calendar
235
+ restart: unless-stopped
236
+ environment:
237
+ DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
238
+ BETTER_AUTH_URL: https://agents.example.com/calendar
239
+ BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
240
+ A2A_SECRET: ${A2A_SECRET}
241
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
242
+ depends_on:
243
+ - postgres
244
+
245
+ proxy:
246
+ image: caddy:2
247
+ restart: unless-stopped
248
+ ports:
249
+ - "80:80"
250
+ - "443:443"
251
+ volumes:
252
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
253
+ depends_on:
254
+ - mail
255
+ - calendar
256
+
257
+ volumes:
258
+ postgres-data:
259
+ ```
260
+
261
+ ```caddyfile
262
+ agents.example.com {
263
+ reverse_proxy /mail* mail:3000
264
+ reverse_proxy /calendar* calendar:3000
265
+ }
266
+ ```
267
+
268
+ Notes for this shape:
269
+
270
+ - Keep `DATABASE_URL`, `BETTER_AUTH_SECRET`, `A2A_SECRET`, and other production
271
+ secrets identical across app containers when you want shared auth, shared
272
+ credentials, and cross-app A2A.
273
+ - Set `BETTER_AUTH_URL` to the public URL for that app path. OAuth callback URLs
274
+ should use the same public URL.
275
+ - Do not rely on the container filesystem for production data; use Postgres (or
276
+ another persistent SQL backend) for `DATABASE_URL`.
277
+ - If Cloudflare terminates TLS in front of the VPS, configure Cloudflare to send
278
+ normal `X-Forwarded-Proto` / `X-Forwarded-Host` headers and route the public
279
+ host to the proxy container.
280
+
148
281
  ## Vercel {#vercel}
149
282
 
150
283
  ```ts
@@ -54,7 +54,7 @@ Yes. Pick any host that runs Node — Netlify, Vercel, Cloudflare, AWS, Deno Dep
54
54
 
55
55
  ### What AI models does it support? {#what-models}
56
56
 
57
- Anthropic Claude, OpenAI (GPT-5 family), Google Gemini, and any provider that speaks the OpenAI API shape (including local models via Ollama). You configure the model in settings; switching is a config change, not a code rewrite. The framework's heaviest tested path is Claude, so that's the default recommendation.
57
+ Anthropic Claude, OpenAI (GPT-5 family), Google Gemini, and any provider that speaks the OpenAI API shape. For OpenAI-compatible gateways like LiteLLM, choose OpenAI in Settings and add the gateway URL under Advanced. You configure the model in settings; switching is a config change, not a code rewrite. The framework's heaviest tested path is Claude, so that's the default recommendation.
58
58
 
59
59
  ### Do I need to know AI/ML? {#do-i-need-to-know-ai}
60
60
 
@@ -289,6 +289,8 @@ These settings only affect read-only page navigation. Framework tools, agent cha
289
289
 
290
290
  Prefer each app on its own domain (`mail.company.com`, `calendar.company.com`)? Every app in the workspace is still an independent deployable — `cd apps/mail && npx @agent-native/core@latest build` behaves exactly like a standalone scaffold. Cross-app A2A then goes through the standard JWT-signed path with a shared `A2A_SECRET`. Cross-domain SSO between separately-deployed apps is handled by identity federation with Dispatch as the hub — see [Cross-App SSO](/docs/cross-app-sso); the unified single-origin deploy avoids needing it.
291
291
 
292
+ For a self-hosted VPS or Docker Compose setup on one origin, build and run one container per app with its own path prefix (`APP_BASE_PATH=/mail`, `APP_BASE_PATH=/calendar`, ...), then route those prefixes through a reverse proxy. See [Deployment — Self-hosted workspace with Docker Compose](/docs/deployment#workspace-docker-compose) for the Dockerfile and compose example.
293
+
292
294
  ### Shared database, shared credentials
293
295
 
294
296
  Whatever you pick, point every app at the same `DATABASE_URL` for cross-app state out of the box: one set of user accounts, one set of organizations, one set of shared settings. If each app has its own database, the workspace pattern still works — you just lose that shared-state story.
@@ -144,7 +144,6 @@ configureTracking({
144
144
  endpoint: "https://analytics.example.com/api/analytics/track",
145
145
  sessionReplay: {
146
146
  enabled: true,
147
- requireSignedInUser: true,
148
147
  sampleRate: 0.1,
149
148
  },
150
149
  getDefaultProps: (_event, props) => ({
@@ -163,11 +162,12 @@ Agent Native template roots already call `configureTracking()`. Hosted template
163
162
  VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
164
163
  VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
165
164
  VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
166
- VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=true
167
165
  VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
168
166
  ```
169
167
 
170
- The browser helper also performs a best-effort, non-blocking read of the current Agent Native auth session. When `requireSignedInUser` or `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH` is enabled, replay does not start unless the session resolves to a signed-in user. Signed-in replays include `userId`/`userEmail` plus `orgId`; if auth gating is disabled, anonymous recordings remain queryable by anonymous visitor, session, app/template, hostname, and path.
168
+ The browser helper also performs a best-effort, non-blocking read of the current Agent Native auth session. Replay is signed-in-only by default: when `sessionReplay` is enabled, recording does not start unless the session resolves to a user email address. Signed-in replays include email-backed `userId`/`userEmail` plus `orgId`.
169
+
170
+ Set `sessionReplay.requireSignedInUser: false` or `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false` only for an intentional anonymous replay deployment. When auth gating is disabled, anonymous recordings remain queryable by anonymous visitor, session, app/template, hostname, and path.
171
171
 
172
172
  Session replay is sampled deterministically per browser session. A `sampleRate` of `0.1` records about 10% of eligible sessions; use `1` when the eligible population is intentionally small, such as logged-in-only dogfooding.
173
173
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.79.20",
3
+ "version": "0.79.22",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -0,0 +1,25 @@
1
+ export const OPENAI_BASE_URL_ENV_VAR = "OPENAI_BASE_URL";
2
+
3
+ export function normalizeOpenAiBaseUrl(value: string): string {
4
+ const trimmed = value.trim();
5
+ if (!trimmed) {
6
+ throw new Error("Endpoint URL is required.");
7
+ }
8
+
9
+ let url: URL;
10
+ try {
11
+ url = new URL(trimmed);
12
+ } catch {
13
+ throw new Error("Endpoint URL must be a valid URL.");
14
+ }
15
+
16
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
17
+ throw new Error("Endpoint URL must start with http:// or https://.");
18
+ }
19
+ if (url.username || url.password) {
20
+ throw new Error("Endpoint URL must not include credentials.");
21
+ }
22
+
23
+ url.hash = "";
24
+ return url.toString().replace(/\/+$/, "");
25
+ }
@@ -16,6 +16,8 @@ export interface ProviderEnvMeta {
16
16
  placeholder: string;
17
17
  }
18
18
 
19
+ export { OPENAI_BASE_URL_ENV_VAR } from "./openai-compatible-endpoint.js";
20
+
19
21
  export const PROVIDER_ENV_META: Record<string, ProviderEnvMeta> = {
20
22
  anthropic: {
21
23
  envVar: "ANTHROPIC_API_KEY",
@@ -18,6 +18,10 @@ import {
18
18
  } from "../../server/credential-provider.js";
19
19
  import { getSetting } from "../../settings/store.js";
20
20
  import { getAgentAppModelDefaultForCurrentRequest } from "../app-model-defaults.js";
21
+ import {
22
+ normalizeOpenAiBaseUrl,
23
+ OPENAI_BASE_URL_ENV_VAR,
24
+ } from "./openai-compatible-endpoint.js";
21
25
  import type { AgentEngine, EngineCapabilities } from "./types.js";
22
26
 
23
27
  const require = createRequire(import.meta.url);
@@ -324,6 +328,39 @@ function engineCreateConfig(
324
328
  };
325
329
  }
326
330
 
331
+ async function resolveOpenAiBaseUrl(): Promise<string | undefined> {
332
+ let raw: string | null | undefined = null;
333
+ try {
334
+ raw = await resolveSecret(OPENAI_BASE_URL_ENV_VAR);
335
+ } catch {
336
+ raw = null;
337
+ }
338
+
339
+ if (!raw && canUseDeployCredentialFallbackForRequest()) {
340
+ raw = readDeployCredentialEnv(OPENAI_BASE_URL_ENV_VAR);
341
+ }
342
+
343
+ return raw ? normalizeOpenAiBaseUrl(raw) : undefined;
344
+ }
345
+
346
+ async function engineCreateConfigForEntry(
347
+ entry: AgentEngineEntry,
348
+ apiKey: string | undefined,
349
+ extra?: Record<string, unknown>,
350
+ ): Promise<Record<string, unknown>> {
351
+ const safeExtra = { ...(extra ?? {}) };
352
+ if (entry.name === "ai-sdk:openai") {
353
+ if (typeof safeExtra.baseURL === "string" && safeExtra.baseUrl == null) {
354
+ safeExtra.baseUrl = normalizeOpenAiBaseUrl(safeExtra.baseURL);
355
+ }
356
+ if (safeExtra.baseUrl == null) {
357
+ const baseUrl = await resolveOpenAiBaseUrl();
358
+ if (baseUrl) safeExtra.baseUrl = baseUrl;
359
+ }
360
+ }
361
+ return engineCreateConfig(apiKey, safeExtra);
362
+ }
363
+
327
364
  /**
328
365
  * True when the stored `agent-engine` row points at a registered engine
329
366
  * AND an API key for it is reachable via the engine's required env vars.
@@ -432,7 +469,9 @@ export async function resolveEngine(
432
469
  `[agent-engine] Unknown engine: "${name}". Registered: ${[..._registry.keys()].join(", ")}`,
433
470
  );
434
471
  assertAgentEnginePackageInstalled(entry);
435
- return entry.create(engineCreateConfig(apiKey, engineConfig));
472
+ return entry.create(
473
+ await engineCreateConfigForEntry(entry, apiKey, engineConfig),
474
+ );
436
475
  }
437
476
 
438
477
  // 3. Explicit string name from options
@@ -443,7 +482,7 @@ export async function resolveEngine(
443
482
  `[agent-engine] Unknown engine: "${engineOption}". Registered: ${[..._registry.keys()].join(", ")}`,
444
483
  );
445
484
  assertAgentEnginePackageInstalled(entry);
446
- return entry.create(engineCreateConfig(apiKey));
485
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey));
447
486
  }
448
487
 
449
488
  // 4. Env var — explicit engine name override
@@ -452,7 +491,7 @@ export async function resolveEngine(
452
491
  const entry = _registry.get(envEngine);
453
492
  if (entry) {
454
493
  assertAgentEnginePackageInstalled(entry);
455
- return entry.create(engineCreateConfig(apiKey));
494
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey));
456
495
  }
457
496
  }
458
497
 
@@ -460,7 +499,7 @@ export async function resolveEngine(
460
499
  if (appDefault?.engine) {
461
500
  const entry = _registry.get(appDefault.engine);
462
501
  if (entry && (await isStoredEngineUsableForRequest(appDefault, entry))) {
463
- return entry.create(engineCreateConfig(apiKey));
502
+ return entry.create(await engineCreateConfigForEntry(entry, apiKey));
464
503
  }
465
504
  }
466
505
 
@@ -477,7 +516,9 @@ export async function resolveEngine(
477
516
  // Builder connection wins over a stale deploy-level/provider key.
478
517
  const detectedFromUser = await detectEngineFromUserSecrets();
479
518
  if (detectedFromUser?.name === "builder") {
480
- return detectedFromUser.create(engineCreateConfig(apiKey));
519
+ return detectedFromUser.create(
520
+ await engineCreateConfigForEntry(detectedFromUser, apiKey),
521
+ );
481
522
  }
482
523
 
483
524
  // 6. Settings store — only when the stored row's API key is reachable.
@@ -490,19 +531,22 @@ export async function resolveEngine(
490
531
  if (storedRaw && typeof storedEngine === "string") {
491
532
  const entry = _registry.get(storedEngine);
492
533
  if (entry && (await isStoredEngineUsableForRequest(storedRaw, entry))) {
493
- return entry.create({
494
- ...engineCreateConfig(
534
+ return entry.create(
535
+ await engineCreateConfigForEntry(
536
+ entry,
495
537
  apiKey,
496
538
  stripInlineApiKeyConfig(
497
539
  storedConfig as Record<string, unknown> | undefined,
498
540
  ),
499
541
  ),
500
- });
542
+ );
501
543
  }
502
544
  }
503
545
 
504
546
  if (detectedFromUser) {
505
- return detectedFromUser.create(engineCreateConfig(apiKey));
547
+ return detectedFromUser.create(
548
+ await engineCreateConfigForEntry(detectedFromUser, apiKey),
549
+ );
506
550
  }
507
551
 
508
552
  // 8. Auto-detect from any provider env var — so just dropping a key in
@@ -510,7 +554,9 @@ export async function resolveEngine(
510
554
  const detected = canUseDeployCredentialFallbackForRequest()
511
555
  ? detectEngineFromEnv()
512
556
  : null;
513
- if (detected) return detected.create(engineCreateConfig(apiKey));
557
+ if (detected) {
558
+ return detected.create(await engineCreateConfigForEntry(detected, apiKey));
559
+ }
514
560
 
515
561
  // 9. Default: anthropic
516
562
  const anthropicEntry = _registry.get("anthropic");
@@ -519,7 +565,9 @@ export async function resolveEngine(
519
565
  "[agent-engine] Default Anthropic engine is not registered. Did builtin.ts fail to load?",
520
566
  );
521
567
  }
522
- return anthropicEntry.create(engineCreateConfig(apiKey));
568
+ return anthropicEntry.create(
569
+ await engineCreateConfigForEntry(anthropicEntry, apiKey),
570
+ );
523
571
  }
524
572
 
525
573
  /**
@@ -20,6 +20,12 @@ import { cn } from "./utils.js";
20
20
  const DEFAULT_FEEDBACK_URL =
21
21
  "https://forms.agent-native.com/f/agent-native-feedback/_16ewV";
22
22
 
23
+ function isSyntheticAgentNativeAnonymousEmail(
24
+ value: string | null | undefined,
25
+ ): boolean {
26
+ return /^anon-[^\s@]+@agent-native\.com$/i.test(value ?? "");
27
+ }
28
+
23
29
  interface ParsedTarget {
24
30
  endpoint: string;
25
31
  slug: string;
@@ -342,7 +348,11 @@ export function FeedbackButton({
342
348
  try {
343
349
  const resolvedSchema = schema ?? (await loadSchema(target));
344
350
  if (!schema) setSchema(resolvedSchema);
345
- const submitterEmail = session?.email;
351
+ const submitterEmail = isSyntheticAgentNativeAnonymousEmail(
352
+ session?.email,
353
+ )
354
+ ? null
355
+ : session?.email;
346
356
  const feedbackContext = getFeedbackClientContext({
347
357
  chatSessionId,
348
358
  storageKey: chatStorageKey,
@@ -29,6 +29,32 @@ export interface SaveAgentEngineApiKeyOptions {
29
29
  scope?: "user" | "org";
30
30
  }
31
31
 
32
+ export interface SaveAgentEngineProviderSettingsOptions {
33
+ provider?: AgentEngineProvider;
34
+ key?: string;
35
+ apiKey?: string;
36
+ baseUrl?: string;
37
+ clearBaseUrl?: boolean;
38
+ scope?: "user" | "org";
39
+ }
40
+
41
+ function resolveProviderEnvVar(
42
+ provider: AgentEngineProvider | undefined,
43
+ key: string | undefined,
44
+ ): string {
45
+ const envVar = key?.trim() || (provider ? PROVIDER_ENV_VAR[provider] : "");
46
+ if (!envVar) {
47
+ throw new Error("Choose an API key provider first.");
48
+ }
49
+ return envVar;
50
+ }
51
+
52
+ function dispatchConfiguredChanged(): void {
53
+ if (typeof window !== "undefined") {
54
+ window.dispatchEvent(new CustomEvent(CONFIGURED_CHANGED_EVENT));
55
+ }
56
+ }
57
+
32
58
  /**
33
59
  * Persist a provider API key for the current owner. Resolves on success.
34
60
  * Throws an Error with a readable message on failure. On success it also
@@ -41,20 +67,43 @@ export async function saveAgentEngineApiKey({
41
67
  apiKey,
42
68
  scope,
43
69
  }: SaveAgentEngineApiKeyOptions): Promise<void> {
44
- const trimmed = apiKey.trim();
45
- if (!trimmed) {
70
+ if (!apiKey.trim()) {
46
71
  throw new Error("Enter an API key first.");
47
72
  }
48
- const envVar = key?.trim() || (provider ? PROVIDER_ENV_VAR[provider] : "");
49
- if (!envVar) {
50
- throw new Error("Choose an API key provider first.");
73
+ await saveAgentEngineProviderSettings({ provider, key, apiKey, scope });
74
+ }
75
+
76
+ /**
77
+ * Persist provider-specific settings for the current owner. This intentionally
78
+ * stays narrow: API keys for built-in BYOK providers, plus OpenAI's optional
79
+ * compatible endpoint URL. Values are stored server-side in scoped secrets.
80
+ */
81
+ export async function saveAgentEngineProviderSettings({
82
+ provider,
83
+ key,
84
+ apiKey,
85
+ baseUrl,
86
+ clearBaseUrl,
87
+ scope,
88
+ }: SaveAgentEngineProviderSettingsOptions): Promise<void> {
89
+ const trimmed = apiKey?.trim() ?? "";
90
+ const endpoint = baseUrl?.trim() ?? "";
91
+ if (!trimmed && !endpoint && !clearBaseUrl) {
92
+ throw new Error("Enter an API key or endpoint URL first.");
51
93
  }
94
+ const envVar = resolveProviderEnvVar(provider, key);
52
95
  const res = await fetch(
53
96
  agentNativePath("/_agent-native/agent-engine/api-key"),
54
97
  {
55
98
  method: "POST",
56
99
  headers: { "Content-Type": "application/json" },
57
- body: JSON.stringify({ key: envVar, value: trimmed, scope }),
100
+ body: JSON.stringify({
101
+ key: envVar,
102
+ ...(trimmed ? { value: trimmed } : {}),
103
+ ...(endpoint ? { baseUrl: endpoint } : {}),
104
+ ...(clearBaseUrl ? { clearBaseUrl: true } : {}),
105
+ scope,
106
+ }),
58
107
  },
59
108
  );
60
109
  if (!res.ok) {
@@ -66,10 +115,8 @@ export async function saveAgentEngineApiKey({
66
115
  message ??
67
116
  (res.status === 401
68
117
  ? "Sign in to save a key, or connect Builder instead."
69
- : `Could not save the key (HTTP ${res.status}).`),
118
+ : `Could not save provider settings (HTTP ${res.status}).`),
70
119
  );
71
120
  }
72
- if (typeof window !== "undefined") {
73
- window.dispatchEvent(new CustomEvent(CONFIGURED_CHANGED_EVENT));
74
- }
121
+ dispatchConfiguredChanged();
75
122
  }