@agent-native/core 0.7.19 → 0.7.20

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 (258) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  3. package/dist/agent/engine/builder-engine.js +45 -2
  4. package/dist/agent/engine/builder-engine.js.map +1 -1
  5. package/dist/agent/loop-settings.d.ts +37 -0
  6. package/dist/agent/loop-settings.d.ts.map +1 -0
  7. package/dist/agent/loop-settings.js +127 -0
  8. package/dist/agent/loop-settings.js.map +1 -0
  9. package/dist/agent/production-agent.d.ts +8 -0
  10. package/dist/agent/production-agent.d.ts.map +1 -1
  11. package/dist/agent/production-agent.js +268 -29
  12. package/dist/agent/production-agent.js.map +1 -1
  13. package/dist/agent/run-manager.d.ts.map +1 -1
  14. package/dist/agent/run-manager.js +76 -3
  15. package/dist/agent/run-manager.js.map +1 -1
  16. package/dist/agent/run-store.d.ts +1 -1
  17. package/dist/agent/run-store.d.ts.map +1 -1
  18. package/dist/agent/run-store.js +65 -2
  19. package/dist/agent/run-store.js.map +1 -1
  20. package/dist/agent/thread-data-builder.d.ts +3 -0
  21. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  22. package/dist/agent/thread-data-builder.js +52 -10
  23. package/dist/agent/thread-data-builder.js.map +1 -1
  24. package/dist/agent/tool-search.d.ts +37 -0
  25. package/dist/agent/tool-search.d.ts.map +1 -0
  26. package/dist/agent/tool-search.js +201 -0
  27. package/dist/agent/tool-search.js.map +1 -0
  28. package/dist/agent/types.d.ts +8 -1
  29. package/dist/agent/types.d.ts.map +1 -1
  30. package/dist/agent/types.js.map +1 -1
  31. package/dist/cli/create.d.ts.map +1 -1
  32. package/dist/cli/create.js +44 -9
  33. package/dist/cli/create.js.map +1 -1
  34. package/dist/cli/workspacify.d.ts +2 -0
  35. package/dist/cli/workspacify.d.ts.map +1 -1
  36. package/dist/cli/workspacify.js +34 -1
  37. package/dist/cli/workspacify.js.map +1 -1
  38. package/dist/client/AssistantChat.d.ts.map +1 -1
  39. package/dist/client/AssistantChat.js +277 -18
  40. package/dist/client/AssistantChat.js.map +1 -1
  41. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  42. package/dist/client/ConnectBuilderCard.js +1 -1
  43. package/dist/client/ConnectBuilderCard.js.map +1 -1
  44. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  45. package/dist/client/MultiTabAssistantChat.js +14 -6
  46. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  47. package/dist/client/NewWorkspaceAppFlow.d.ts +14 -0
  48. package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -0
  49. package/dist/client/NewWorkspaceAppFlow.js +200 -0
  50. package/dist/client/NewWorkspaceAppFlow.js.map +1 -0
  51. package/dist/client/PoweredByBadge.d.ts +10 -1
  52. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  53. package/dist/client/PoweredByBadge.js +120 -8
  54. package/dist/client/PoweredByBadge.js.map +1 -1
  55. package/dist/client/agent-chat-adapter.d.ts +3 -5
  56. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  57. package/dist/client/agent-chat-adapter.js +26 -19
  58. package/dist/client/agent-chat-adapter.js.map +1 -1
  59. package/dist/client/agent-chat.d.ts.map +1 -1
  60. package/dist/client/agent-chat.js +15 -3
  61. package/dist/client/agent-chat.js.map +1 -1
  62. package/dist/client/analytics.d.ts +1 -1
  63. package/dist/client/analytics.d.ts.map +1 -1
  64. package/dist/client/analytics.js +141 -1
  65. package/dist/client/analytics.js.map +1 -1
  66. package/dist/client/builder-frame.d.ts +10 -0
  67. package/dist/client/builder-frame.d.ts.map +1 -0
  68. package/dist/client/builder-frame.js +94 -0
  69. package/dist/client/builder-frame.js.map +1 -0
  70. package/dist/client/composer/MentionPopover.d.ts.map +1 -1
  71. package/dist/client/composer/MentionPopover.js +5 -1
  72. package/dist/client/composer/MentionPopover.js.map +1 -1
  73. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  74. package/dist/client/composer/TiptapComposer.js +11 -6
  75. package/dist/client/composer/TiptapComposer.js.map +1 -1
  76. package/dist/client/error-format.d.ts +20 -1
  77. package/dist/client/error-format.d.ts.map +1 -1
  78. package/dist/client/error-format.js +53 -5
  79. package/dist/client/error-format.js.map +1 -1
  80. package/dist/client/index.d.ts +3 -1
  81. package/dist/client/index.d.ts.map +1 -1
  82. package/dist/client/index.js +3 -1
  83. package/dist/client/index.js.map +1 -1
  84. package/dist/client/onboarding/OnboardingPanel.d.ts.map +1 -1
  85. package/dist/client/onboarding/OnboardingPanel.js +88 -6
  86. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  87. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  88. package/dist/client/settings/SettingsPanel.js +145 -9
  89. package/dist/client/settings/SettingsPanel.js.map +1 -1
  90. package/dist/client/settings/useBuilderStatus.d.ts +13 -0
  91. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  92. package/dist/client/settings/useBuilderStatus.js +50 -9
  93. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  94. package/dist/client/sse-event-processor.d.ts +3 -0
  95. package/dist/client/sse-event-processor.d.ts.map +1 -1
  96. package/dist/client/sse-event-processor.js +88 -7
  97. package/dist/client/sse-event-processor.js.map +1 -1
  98. package/dist/client/tools/ToolsListPage.d.ts.map +1 -1
  99. package/dist/client/tools/ToolsListPage.js +16 -1
  100. package/dist/client/tools/ToolsListPage.js.map +1 -1
  101. package/dist/client/tools/ToolsSidebarSection.d.ts.map +1 -1
  102. package/dist/client/tools/ToolsSidebarSection.js +63 -8
  103. package/dist/client/tools/ToolsSidebarSection.js.map +1 -1
  104. package/dist/client/tools/tool-order.d.ts +7 -0
  105. package/dist/client/tools/tool-order.d.ts.map +1 -0
  106. package/dist/client/tools/tool-order.js +47 -0
  107. package/dist/client/tools/tool-order.js.map +1 -0
  108. package/dist/client/transcription/BuilderTranscriptionCta.d.ts.map +1 -1
  109. package/dist/client/transcription/BuilderTranscriptionCta.js +71 -6
  110. package/dist/client/transcription/BuilderTranscriptionCta.js.map +1 -1
  111. package/dist/client/use-send-to-agent-chat.d.ts.map +1 -1
  112. package/dist/client/use-send-to-agent-chat.js +11 -3
  113. package/dist/client/use-send-to-agent-chat.js.map +1 -1
  114. package/dist/client/useProductionAgent.d.ts.map +1 -1
  115. package/dist/client/useProductionAgent.js +1 -1
  116. package/dist/client/useProductionAgent.js.map +1 -1
  117. package/dist/db/client.d.ts.map +1 -1
  118. package/dist/db/client.js +5 -1
  119. package/dist/db/client.js.map +1 -1
  120. package/dist/deploy/build.d.ts +1 -0
  121. package/dist/deploy/build.d.ts.map +1 -1
  122. package/dist/deploy/build.js +4 -1
  123. package/dist/deploy/build.js.map +1 -1
  124. package/dist/oauth-tokens/index.d.ts +1 -1
  125. package/dist/oauth-tokens/index.d.ts.map +1 -1
  126. package/dist/oauth-tokens/index.js +1 -1
  127. package/dist/oauth-tokens/index.js.map +1 -1
  128. package/dist/oauth-tokens/store.d.ts.map +1 -1
  129. package/dist/oauth-tokens/store.js +6 -0
  130. package/dist/oauth-tokens/store.js.map +1 -1
  131. package/dist/observability/store.d.ts.map +1 -1
  132. package/dist/observability/store.js +19 -19
  133. package/dist/observability/store.js.map +1 -1
  134. package/dist/onboarding/default-steps.d.ts.map +1 -1
  135. package/dist/onboarding/default-steps.js +95 -61
  136. package/dist/onboarding/default-steps.js.map +1 -1
  137. package/dist/onboarding/plugin.d.ts.map +1 -1
  138. package/dist/onboarding/plugin.js +17 -8
  139. package/dist/onboarding/plugin.js.map +1 -1
  140. package/dist/org/migrations.js +2 -2
  141. package/dist/org/migrations.js.map +1 -1
  142. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  143. package/dist/scripts/agent-engines/list-agent-engines.js +2 -3
  144. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  145. package/dist/scripts/db/exec.d.ts +2 -1
  146. package/dist/scripts/db/exec.d.ts.map +1 -1
  147. package/dist/scripts/db/exec.js +264 -61
  148. package/dist/scripts/db/exec.js.map +1 -1
  149. package/dist/scripts/db/schema.d.ts.map +1 -1
  150. package/dist/scripts/db/schema.js +16 -4
  151. package/dist/scripts/db/schema.js.map +1 -1
  152. package/dist/scripts/dev/index.d.ts.map +1 -1
  153. package/dist/scripts/dev/index.js +36 -11
  154. package/dist/scripts/dev/index.js.map +1 -1
  155. package/dist/scripts/manage-agent-loop-settings.d.ts +7 -0
  156. package/dist/scripts/manage-agent-loop-settings.d.ts.map +1 -0
  157. package/dist/scripts/manage-agent-loop-settings.js +63 -0
  158. package/dist/scripts/manage-agent-loop-settings.js.map +1 -0
  159. package/dist/scripts/runner.d.ts.map +1 -1
  160. package/dist/scripts/runner.js +11 -0
  161. package/dist/scripts/runner.js.map +1 -1
  162. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  163. package/dist/server/agent-chat-plugin.js +60 -18
  164. package/dist/server/agent-chat-plugin.js.map +1 -1
  165. package/dist/server/app-url.d.ts +5 -4
  166. package/dist/server/app-url.d.ts.map +1 -1
  167. package/dist/server/app-url.js +8 -4
  168. package/dist/server/app-url.js.map +1 -1
  169. package/dist/server/auth.d.ts +8 -0
  170. package/dist/server/auth.d.ts.map +1 -1
  171. package/dist/server/auth.js +82 -29
  172. package/dist/server/auth.js.map +1 -1
  173. package/dist/server/better-auth-instance.d.ts.map +1 -1
  174. package/dist/server/better-auth-instance.js +16 -5
  175. package/dist/server/better-auth-instance.js.map +1 -1
  176. package/dist/server/builder-browser.d.ts +12 -0
  177. package/dist/server/builder-browser.d.ts.map +1 -1
  178. package/dist/server/builder-browser.js +36 -4
  179. package/dist/server/builder-browser.js.map +1 -1
  180. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  181. package/dist/server/core-routes-plugin.js +350 -53
  182. package/dist/server/core-routes-plugin.js.map +1 -1
  183. package/dist/server/credential-provider.d.ts +21 -3
  184. package/dist/server/credential-provider.d.ts.map +1 -1
  185. package/dist/server/credential-provider.js +51 -21
  186. package/dist/server/credential-provider.js.map +1 -1
  187. package/dist/server/google-oauth.d.ts +3 -0
  188. package/dist/server/google-oauth.d.ts.map +1 -1
  189. package/dist/server/google-oauth.js +27 -3
  190. package/dist/server/google-oauth.js.map +1 -1
  191. package/dist/server/index.d.ts +4 -3
  192. package/dist/server/index.d.ts.map +1 -1
  193. package/dist/server/index.js +4 -3
  194. package/dist/server/index.js.map +1 -1
  195. package/dist/server/schema-prompt.d.ts.map +1 -1
  196. package/dist/server/schema-prompt.js +2 -1
  197. package/dist/server/schema-prompt.js.map +1 -1
  198. package/dist/server/security-headers.d.ts +3 -0
  199. package/dist/server/security-headers.d.ts.map +1 -1
  200. package/dist/server/security-headers.js +7 -1
  201. package/dist/server/security-headers.js.map +1 -1
  202. package/dist/server/ssr-handler.d.ts.map +1 -1
  203. package/dist/server/ssr-handler.js +24 -4
  204. package/dist/server/ssr-handler.js.map +1 -1
  205. package/dist/templates/default/_gitignore +5 -1
  206. package/dist/templates/default/app/root.tsx +1 -0
  207. package/dist/templates/default/public/favicon.svg +3 -3
  208. package/dist/templates/default/public/icon-180.svg +3 -3
  209. package/dist/templates/default/public/icon-192.svg +3 -3
  210. package/dist/templates/default/public/icon-512.svg +3 -3
  211. package/dist/templates/workspace-core/AGENTS.md +23 -7
  212. package/dist/templates/workspace-core/package.json +2 -1
  213. package/dist/templates/workspace-core/src/credentials.ts +22 -11
  214. package/dist/templates/workspace-root/.env.example +7 -0
  215. package/dist/templates/workspace-root/README.md +6 -3
  216. package/dist/templates/workspace-root/_gitignore +3 -0
  217. package/dist/templates/workspace-root/package.json +3 -1
  218. package/dist/templates/workspace-root/scripts/workspace-dev.ts +410 -0
  219. package/dist/tools/actions.d.ts.map +1 -1
  220. package/dist/tools/actions.js +2 -0
  221. package/dist/tools/actions.js.map +1 -1
  222. package/dist/tools/html-shell.d.ts.map +1 -1
  223. package/dist/tools/html-shell.js +13 -1
  224. package/dist/tools/html-shell.js.map +1 -1
  225. package/dist/tools/store.d.ts.map +1 -1
  226. package/dist/tools/store.js +10 -10
  227. package/dist/tools/store.js.map +1 -1
  228. package/dist/tracking/providers.d.ts +1 -0
  229. package/dist/tracking/providers.d.ts.map +1 -1
  230. package/dist/tracking/providers.js +72 -0
  231. package/dist/tracking/providers.js.map +1 -1
  232. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  233. package/dist/vite/action-types-plugin.js +106 -9
  234. package/dist/vite/action-types-plugin.js.map +1 -1
  235. package/dist/vite/client.d.ts.map +1 -1
  236. package/dist/vite/client.js +67 -2
  237. package/dist/vite/client.js.map +1 -1
  238. package/docs/content/authentication.md +17 -13
  239. package/docs/content/deployment.md +11 -11
  240. package/docs/content/mcp-clients.md +2 -2
  241. package/docs/content/onboarding.md +32 -30
  242. package/docs/content/security.md +1 -1
  243. package/docs/content/tools.md +4 -0
  244. package/package.json +2 -2
  245. package/src/templates/default/_gitignore +5 -1
  246. package/src/templates/default/app/root.tsx +1 -0
  247. package/src/templates/default/public/favicon.svg +3 -3
  248. package/src/templates/default/public/icon-180.svg +3 -3
  249. package/src/templates/default/public/icon-192.svg +3 -3
  250. package/src/templates/default/public/icon-512.svg +3 -3
  251. package/src/templates/workspace-core/AGENTS.md +23 -7
  252. package/src/templates/workspace-core/package.json +2 -1
  253. package/src/templates/workspace-core/src/credentials.ts +22 -11
  254. package/src/templates/workspace-root/.env.example +7 -0
  255. package/src/templates/workspace-root/README.md +6 -3
  256. package/src/templates/workspace-root/_gitignore +3 -0
  257. package/src/templates/workspace-root/package.json +3 -1
  258. package/src/templates/workspace-root/scripts/workspace-dev.ts +410 -0
@@ -1,11 +1,11 @@
1
1
  import { getH3App, awaitBootstrap } from "./framework-request-handler.js";
2
- import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, } from "h3";
2
+ import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, } from "h3";
3
3
  import path from "node:path";
4
4
  import { createPollHandler } from "./poll.js";
5
5
  import { createSSEHandler } from "./sse.js";
6
6
  import { upsertEnvFile } from "./create-server.js";
7
7
  import { readBody } from "./h3-helpers.js";
8
- import { BUILDER_ENV_KEYS, BUILDER_STATE_PARAM, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, getBuilderBrowserStatusForEvent, resolveSafePreviewUrl, runBuilderAgent, signBuilderCallbackState, verifyBuilderCallbackState, } from "./builder-browser.js";
8
+ import { BUILDER_ENV_KEYS, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, getBuilderBranchProjectId, getBuilderBrowserStatusForEvent, isBuilderBranchingEnabled, resolveSafePreviewUrl, runBuilderAgent, } from "./builder-browser.js";
9
9
  import { getState, putState, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "../application-state/handlers.js";
10
10
  import { getSetting, putSetting, deleteSetting } from "../settings/store.js";
11
11
  import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
@@ -19,6 +19,7 @@ import { readMultipartFormData } from "h3";
19
19
  import { createListSecretsHandler, createWriteSecretHandler, createTestSecretHandler, createAdHocSecretHandler, } from "../secrets/routes.js";
20
20
  import { registerFrameworkSecrets } from "../secrets/register-framework-secrets.js";
21
21
  import { registerBuiltinProviders } from "../tracking/providers.js";
22
+ import { track } from "../tracking/index.js";
22
23
  import { registerBuiltinNotificationChannels } from "../notifications/channels.js";
23
24
  import { createNotificationsHandler } from "../notifications/routes.js";
24
25
  import { createProgressHandler } from "../progress/routes.js";
@@ -26,7 +27,9 @@ import { createTranscribeVoiceHandler } from "./transcribe-voice.js";
26
27
  import { runWithRequestContext } from "./request-context.js";
27
28
  import { createVoiceProvidersStatusHandler } from "./voice-providers-status.js";
28
29
  import { PROVIDER_ENV_META } from "../agent/engine/provider-env-vars.js";
30
+ import { canUpdateAgentLoopSettings, readAgentLoopSettings, resetAgentLoopSettings, validateMaxIterationsInput, writeAgentLoopSettings, } from "../agent/loop-settings.js";
29
31
  import { isAgentEngineSettingConfigured, getAgentEngineEntry, detectEngineFromEnv, detectEngineFromUserSecrets, isStoredEngineUsable, } from "../agent/engine/registry.js";
32
+ import { getOrgContext } from "../org/context.js";
30
33
  /**
31
34
  * The base path prefix for all framework-level routes.
32
35
  * All agent-native core routes live under this namespace to avoid
@@ -53,6 +56,14 @@ function isEnvVarWriteAllowed() {
53
56
  return true;
54
57
  return isDevEnvironment() && isLocalDatabase();
55
58
  }
59
+ function trackBuilderLifecycle(name, userEmail, properties = {}) {
60
+ if (!userEmail)
61
+ return;
62
+ track(name, {
63
+ feature: "builder",
64
+ ...properties,
65
+ }, { userId: userEmail });
66
+ }
56
67
  function normalizeAppBasePath(value) {
57
68
  if (!value || value === "/")
58
69
  return "";
@@ -295,15 +306,17 @@ export function createCoreRoutesPlugin(options = {}) {
295
306
  }
296
307
  getH3App(nitroApp).use(`${P}/builder/status`, defineEventHandler(async (event) => {
297
308
  const envStatus = getBuilderBrowserStatusForEvent(event);
298
- // Read session once so we can establish per-user request context for
299
- // credential resolution. Without this, resolveBuilderCredentials()
300
- // calls getRequestUserEmail() on an empty AsyncLocalStorage store and
301
- // falls through to process.env — causing the connection state to
302
- // flicker between requests depending on stale env values.
303
309
  const session = await getSession(event).catch(() => null);
304
310
  const userEmail = session?.email;
311
+ // Env-managed mode: BUILDER_PRIVATE_KEY is set at the deployment
312
+ // level, so every user shares the operator's Builder identity.
313
+ // Skip per-user lookups entirely — the env key is authoritative
314
+ // and the UI must hide the connect/disconnect controls.
315
+ if (envStatus.envManaged) {
316
+ return envStatus;
317
+ }
305
318
  return runWithRequestContext({ userEmail }, async () => {
306
- // Check per-user credentials first (stored in app_secrets).
319
+ // Per-user OAuth mode: read the user's app_secrets-stored creds.
307
320
  try {
308
321
  const { resolveBuilderCredentials } = await import("./credential-provider.js");
309
322
  const creds = await resolveBuilderCredentials();
@@ -352,7 +365,7 @@ export function createCoreRoutesPlugin(options = {}) {
352
365
  }
353
366
  }
354
367
  catch {
355
- // settings store unavailable — fall through to legacy/env status
368
+ // settings store unavailable — fall through
356
369
  }
357
370
  // Honor legacy disconnect flag for existing deployments.
358
371
  try {
@@ -370,49 +383,160 @@ export function createCoreRoutesPlugin(options = {}) {
370
383
  }
371
384
  }
372
385
  catch {
373
- // DB not reachable — fall back to env-only status.
386
+ // DB not reachable
374
387
  }
375
- // For authenticated non-local users who have no per-user credentials,
376
- // explicitly return not-configured rather than deploy-level env keys.
377
- // This is consistent with resolveBuilderCredential()'s design which
378
- // refuses the env fallback for authenticated users to prevent
379
- // cross-tenant credential leakage in shared-DB deployments.
380
- if (userEmail && userEmail !== DEV_MODE_USER_EMAIL) {
381
- return {
382
- ...envStatus,
383
- configured: false,
384
- privateKeyConfigured: false,
385
- publicKeyConfigured: false,
386
- userId: undefined,
387
- orgName: undefined,
388
- orgKind: undefined,
389
- };
390
- }
391
- return envStatus;
388
+ // No env, no per-user creds not configured. Both authenticated
389
+ // and unauthenticated callers see "not connected" so they can
390
+ // run through the OAuth flow.
391
+ return {
392
+ ...envStatus,
393
+ configured: false,
394
+ privateKeyConfigured: false,
395
+ publicKeyConfigured: false,
396
+ userId: undefined,
397
+ orgName: undefined,
398
+ orgKind: undefined,
399
+ };
392
400
  });
393
401
  }));
402
+ // How long a pending-connect row is valid. Must be long enough for
403
+ // the user to complete the Builder CLI-auth flow, but short enough
404
+ // that a stale row from an abandoned attempt doesn't accept a new
405
+ // callback minutes later.
406
+ const BUILDER_CONNECT_PENDING_TTL_MS = 10 * 60 * 1000; // 10 min
407
+ // Decide whether a /builder/connect navigation originated from this
408
+ // app's own UI (allowed) or from a foreign origin (cross-site CSRF
409
+ // attempt — rejected). Sec-Fetch-Site is the modern signal:
410
+ // - "same-origin": user clicked Connect from our own pages — allow
411
+ // - "none": typed in URL bar / bookmark / browser extension — allow
412
+ // - "same-site" / "cross-site" / missing-but-with-foreign-Origin
413
+ // all map to reject.
414
+ // For older browsers without Sec-Fetch-* we fall back to Origin and
415
+ // then Referer, comparing against the request's resolved origin.
416
+ function isSameOriginConnect(event) {
417
+ const fetchSite = getHeader(event, "sec-fetch-site");
418
+ if (fetchSite === "same-origin" || fetchSite === "none")
419
+ return true;
420
+ if (fetchSite)
421
+ return false; // browser told us it's cross-site/same-site
422
+ const expected = getOrigin(event).replace(/\/+$/, "");
423
+ const origin = getHeader(event, "origin");
424
+ if (origin)
425
+ return origin.replace(/\/+$/, "") === expected;
426
+ const referer = getHeader(event, "referer");
427
+ if (referer) {
428
+ try {
429
+ return new URL(referer).origin === expected;
430
+ }
431
+ catch {
432
+ return false;
433
+ }
434
+ }
435
+ // No Sec-Fetch-Site, no Origin, no Referer — pre-2020 browser
436
+ // making a top-level navigation. Allow; cookies are still
437
+ // session-bound so the worst case degrades to the prior behavior.
438
+ return true;
439
+ }
394
440
  // Lightweight 302 to the Builder CLI-auth URL. Lets clients do
395
441
  // `window.open('/_agent-native/builder/connect', '_blank')` synchronously
396
442
  // inside a click handler, avoiding the popup-blocker downgrade that
397
- // happens when an await sits before window.open. We mint a signed
398
- // CSRF state here and embed it in the callback URL we hand to
399
- // Builder; the callback handler verifies it before accepting any
400
- // keys. See `signBuilderCallbackState` for the threat model.
443
+ // happens when an await sits before window.open.
444
+ //
445
+ // CSRF protection here is two-layer because session cookies are
446
+ // SameSite=None;Secure (so the editor iframe can ride along) — that
447
+ // means a session cookie alone does NOT prevent cross-origin
448
+ // window.open from initiating a connect flow on the victim's behalf:
449
+ // 1. Sec-Fetch-Site header — modern browsers stamp every request
450
+ // with the navigation context. We only allow `same-origin` or
451
+ // `none` (typed/bookmark/extension); cross-site / same-site are
452
+ // rejected. The previous "URL-embedded signed state" was dropped
453
+ // because Builder's /cli-auth strips arbitrary query params; this
454
+ // replaces that defense with a browser-native one.
455
+ // 2. Pending row keyed by session email + bound nonce — the callback
456
+ // requires both a valid session and a one-time row that this
457
+ // handler wrote during the same flow. Without the same-origin
458
+ // check above, an attacker could prime the row from cross-site
459
+ // and then trick the victim into hitting a callback URL with
460
+ // attacker-controlled p-key/api-key, hijacking the victim's
461
+ // account. With the check, the attacker can't prime the row.
401
462
  getH3App(nitroApp).use(`${P}/builder/connect`, defineEventHandler(async (event) => {
402
463
  const session = await getSession(event).catch(() => null);
403
464
  if (!session?.email) {
404
465
  setResponseStatus(event, 401);
405
466
  return { error: "Authentication required" };
406
467
  }
407
- const state = signBuilderCallbackState(session.email);
408
- const cliAuthUrl = buildBuilderCliAuthUrl(getOrigin(event), state);
468
+ // Env-managed mode: per-user OAuth is disabled because the operator
469
+ // already provided a deploy-level Builder identity. Reject the
470
+ // connect attempt — any per-user keys we wrote would be ignored
471
+ // by the resolver, so completing the OAuth flow would be a no-op
472
+ // that misleads the user about the resulting connection state.
473
+ const { isBuilderEnvManaged } = await import("./credential-provider.js");
474
+ if (isBuilderEnvManaged()) {
475
+ setResponseStatus(event, 409);
476
+ return {
477
+ error: "Builder is managed by the deployment (BUILDER_PRIVATE_KEY is set). Per-user connect is disabled.",
478
+ envManaged: true,
479
+ };
480
+ }
481
+ // Same-origin gate. Sec-Fetch-Site is the primary signal; fall
482
+ // back to Origin/Referer for older browsers. Reject any context
483
+ // that isn't this exact app's origin — including same-site
484
+ // subdomains, since a compromised subdomain shouldn't be able
485
+ // to mint Builder credential writes for users of the main app.
486
+ if (!isSameOriginConnect(event)) {
487
+ trackBuilderLifecycle("builder connect failed", session.email, {
488
+ reason: "cross_origin",
489
+ stage: "connect",
490
+ });
491
+ setResponseStatus(event, 403);
492
+ return { error: "Cross-origin connect requests are not allowed" };
493
+ }
494
+ // Clear any prior failure row from a previous attempt — otherwise
495
+ // useBuilderStatus polling sees the stale error and aborts the
496
+ // new attempt before it can complete.
497
+ try {
498
+ await deleteSetting(`builder-connect-error:${session.email}`);
499
+ }
500
+ catch {
501
+ // No prior error row — fine
502
+ }
503
+ // Store a short-lived pending row. If the DB is unavailable we
504
+ // surface a popup-renderable error page that signals the parent
505
+ // via BroadcastChannel, rather than letting the popup show raw
506
+ // JSON and the parent poll for 5 minutes.
507
+ try {
508
+ await putSetting(`builder-pending-connect:${session.email}`, {
509
+ expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
510
+ });
511
+ }
512
+ catch (err) {
513
+ trackBuilderLifecycle("builder connect failed", session.email, {
514
+ reason: "pending_storage_unavailable",
515
+ stage: "connect",
516
+ });
517
+ const msg = "Could not initiate Builder connect — storage unavailable. Try again.";
518
+ console.error("[builder] Could not store pending-connect state:", err?.message ?? err);
519
+ // Best-effort: also write the error row so the parent's
520
+ // /builder/status poll picks it up if BroadcastChannel doesn't.
521
+ await putSetting(`builder-connect-error:${session.email}`, {
522
+ message: msg,
523
+ at: Date.now(),
524
+ }).catch(() => { });
525
+ setResponseStatus(event, 503);
526
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
527
+ return createBuilderBrowserCallbackErrorPage(msg);
528
+ }
529
+ trackBuilderLifecycle("builder connect started", session.email, {
530
+ stage: "connect",
531
+ });
532
+ // Build the cli-auth URL without embedding state in redirect_url:
533
+ // Builder's /cli-auth appends params directly to redirect_url and
534
+ // does not preserve any pre-existing query string we put there.
535
+ const cliAuthUrl = buildBuilderCliAuthUrl(getOrigin(event), null);
409
536
  setResponseStatus(event, 302);
410
537
  setResponseHeader(event, "Location", cliAuthUrl);
411
538
  return "";
412
539
  }));
413
- // Hardcoded for the early preview — later this will come from workspace/org
414
- // config so each team can point at its own Builder project.
415
- const DEFAULT_BUILDER_PROJECT_ID = "274d28fec94b48f2b2d68f2274d390eb";
416
540
  getH3App(nitroApp).use(`${P}/builder/run`, defineEventHandler(async (event) => {
417
541
  if (getMethod(event) !== "POST") {
418
542
  setResponseStatus(event, 405);
@@ -442,6 +566,12 @@ export function createCoreRoutesPlugin(options = {}) {
442
566
  return { error: "A signed-in user is required to run Builder" };
443
567
  }
444
568
  const userEmail = session.email;
569
+ if (!isBuilderBranchingEnabled()) {
570
+ setResponseStatus(event, 403);
571
+ return {
572
+ error: "Builder branch creation is not enabled for this deployment. Set ENABLE_BUILDER=true or BUILDER_BRANCH_PROJECT_ID.",
573
+ };
574
+ }
445
575
  // Wrap in runWithRequestContext so resolveBuilderCredential() inside
446
576
  // runBuilderAgent() resolves per-user app_secrets rather than falling
447
577
  // through to process.env — the same pattern the /builder/status endpoint
@@ -458,7 +588,7 @@ export function createCoreRoutesPlugin(options = {}) {
458
588
  try {
459
589
  const result = await runBuilderAgent({
460
590
  prompt,
461
- projectId: DEFAULT_BUILDER_PROJECT_ID,
591
+ projectId: getBuilderBranchProjectId(),
462
592
  branchName: typeof body?.branchName === "string"
463
593
  ? body.branchName
464
594
  : undefined,
@@ -480,27 +610,105 @@ export function createCoreRoutesPlugin(options = {}) {
480
610
  setResponseStatus(event, 405);
481
611
  return { error: "Method not allowed" };
482
612
  }
483
- // Session blocks anonymous callers; the signed state below blocks
484
- // CSRF (session cookies are SameSite=None;Secure for the iframe
485
- // editor, so they ride along on attacker-crafted top-level links).
613
+ // Session blocks anonymous callers; the pending-row check below
614
+ // (combined with the same-origin gate on /builder/connect) blocks
615
+ // CSRF. Session cookies are SameSite=None;Secure for the iframe
616
+ // editor, so they ride along on attacker-crafted top-level links —
617
+ // the SameSite=None concession is exactly why the connect flow
618
+ // can't rely on session-cookie identity alone.
486
619
  const session = await getSession(event).catch(() => null);
487
620
  if (!session?.email) {
488
621
  setResponseStatus(event, 401);
489
622
  return { error: "Authentication required" };
490
623
  }
491
624
  const requestUrl = new URL(`${event.url?.pathname || "/"}${event.url?.search || ""}`, getOrigin(event));
492
- const state = requestUrl.searchParams.get(BUILDER_STATE_PARAM);
493
- if (!verifyBuilderCallbackState(state, session.email)) {
625
+ // Verify and consume the server-side pending-connect row that the
626
+ // /builder/connect route stored. This replaces the old URL-embedded
627
+ // signed CSRF state (_an_state) which Builder's /cli-auth page was
628
+ // stripping from the redirect_url query string.
629
+ //
630
+ // The delete must succeed before we proceed — otherwise a DB blip
631
+ // leaves the row in place and the same callback URL can be
632
+ // replayed against the same session for up to 10 minutes (the
633
+ // TTL window). Treat a delete failure as a hard failure: the
634
+ // user retries, the next /builder/connect call rewrites the
635
+ // pending row.
636
+ let pendingValid = false;
637
+ let pendingError = null;
638
+ try {
639
+ const pending = (await getSetting(`builder-pending-connect:${session.email}`));
640
+ if (pending &&
641
+ typeof pending.expiresAt === "number" &&
642
+ Date.now() < pending.expiresAt) {
643
+ try {
644
+ await deleteSetting(`builder-pending-connect:${session.email}`);
645
+ pendingValid = true;
646
+ }
647
+ catch (err) {
648
+ pendingError =
649
+ "Could not consume pending-connect token (storage error). Please retry.";
650
+ console.error("[builder] deleteSetting failed for pending-connect — refusing to proceed (replay risk):", err?.message ?? err);
651
+ }
652
+ }
653
+ }
654
+ catch {
655
+ // DB temporarily unavailable — treat as missing.
656
+ }
657
+ if (pendingError) {
658
+ trackBuilderLifecycle("builder connect failed", session.email, {
659
+ reason: "pending_consume_storage_error",
660
+ stage: "callback",
661
+ });
662
+ // Best-effort signal to the parent's poll loop, then render the
663
+ // popup-friendly error page so the BroadcastChannel notify fires.
664
+ await putSetting(`builder-connect-error:${session.email}`, {
665
+ message: pendingError,
666
+ at: Date.now(),
667
+ }).catch(() => { });
668
+ setResponseStatus(event, 503);
669
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
670
+ return createBuilderBrowserCallbackErrorPage(pendingError);
671
+ }
672
+ if (!pendingValid) {
673
+ trackBuilderLifecycle("builder connect failed", session.email, {
674
+ reason: "missing_pending_connect",
675
+ stage: "callback",
676
+ });
677
+ const msg = "No active connect flow found. Restart the Builder connect flow from Settings.";
678
+ // Write an error signal so the polling loop in the parent tab
679
+ // terminates quickly instead of waiting 5 minutes for the timeout.
680
+ try {
681
+ await putSetting(`builder-connect-error:${session.email}`, {
682
+ message: msg,
683
+ at: Date.now(),
684
+ });
685
+ }
686
+ catch {
687
+ // DB unavailable — parent will time out naturally.
688
+ }
494
689
  setResponseStatus(event, 403);
495
- return {
496
- error: "Invalid or expired connect token. Restart the Builder connect flow from Settings.",
497
- };
690
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
691
+ return createBuilderBrowserCallbackErrorPage(msg);
498
692
  }
499
693
  const privateKey = requestUrl.searchParams.get("p-key");
500
694
  const publicKey = requestUrl.searchParams.get("api-key");
501
695
  if (!privateKey || !publicKey) {
696
+ trackBuilderLifecycle("builder connect failed", session.email, {
697
+ reason: "missing_credentials",
698
+ stage: "callback",
699
+ });
700
+ // Render the popup-friendly error page (and write a status row)
701
+ // instead of bare JSON, so the parent tab's poll loop terminates
702
+ // immediately via BroadcastChannel rather than hanging until the
703
+ // 5-minute timeout.
704
+ const msg = "Builder didn't return credentials. Restart the connect flow from settings.";
705
+ await putSetting(`builder-connect-error:${session.email}`, {
706
+ message: msg,
707
+ at: Date.now(),
708
+ }).catch(() => { });
502
709
  setResponseStatus(event, 400);
503
- return { error: "Missing Builder credentials in callback" };
710
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
711
+ return createBuilderBrowserCallbackErrorPage(msg);
504
712
  }
505
713
  const userId = requestUrl.searchParams.get("user-id");
506
714
  const orgName = requestUrl.searchParams.get("org-name");
@@ -532,6 +740,10 @@ export function createCoreRoutesPlugin(options = {}) {
532
740
  console.error("[builder] Failed to persist per-user credentials:", writeError);
533
741
  }
534
742
  if (writeError) {
743
+ trackBuilderLifecycle("builder connect failed", session.email, {
744
+ reason: "credential_write_failed",
745
+ stage: "callback",
746
+ });
535
747
  // Best-effort signal to /builder/status. If putSetting also fails
536
748
  // (entire DB unreachable) the popup's postMessage still notifies
537
749
  // the parent. If both fail the parent times out at 5min as today.
@@ -563,15 +775,19 @@ export function createCoreRoutesPlugin(options = {}) {
563
775
  // No prior error row — fine
564
776
  }
565
777
  const previewUrl = resolveSafePreviewUrl(requestUrl.searchParams.get("preview-url"), event);
778
+ trackBuilderLifecycle("builder connect succeeded", session.email, {
779
+ stage: "callback",
780
+ has_preview_url: Boolean(previewUrl),
781
+ org_kind: orgKind || undefined,
782
+ });
566
783
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
567
784
  return createBuilderBrowserCallbackPage(previewUrl);
568
785
  }));
569
- // POST /_agent-native/builder/disconnect — revoke the stored Builder
570
- // credentials so the next turn falls back to BYO / env detection. Mirrors
571
- // the callback handler's three write locations: the template `.env` file,
572
- // the in-process `process.env`, and the `persisted-env-vars` settings row
573
- // (rehydrated on serverless cold starts). All three must be cleared to
574
- // avoid a "still connected after disconnect" state on restart.
786
+ // POST /_agent-native/builder/disconnect — revoke the user's per-user
787
+ // Builder credentials in app_secrets. In env-managed mode (deploy-level
788
+ // BUILDER_PRIVATE_KEY set) disconnection is operator-controlled this
789
+ // endpoint refuses with 409 so a stale UI button can't pretend to
790
+ // disconnect a deploy-level identity it doesn't own.
575
791
  getH3App(nitroApp).use(`${P}/builder/disconnect`, defineEventHandler(async (event) => {
576
792
  if (getMethod(event) !== "POST") {
577
793
  setResponseStatus(event, 405);
@@ -582,12 +798,23 @@ export function createCoreRoutesPlugin(options = {}) {
582
798
  setResponseStatus(event, 401);
583
799
  return { error: "unauthorized" };
584
800
  }
801
+ const { isBuilderEnvManaged, deleteBuilderCredentials } = await import("./credential-provider.js");
802
+ if (isBuilderEnvManaged()) {
803
+ setResponseStatus(event, 409);
804
+ return {
805
+ ok: false,
806
+ error: "Builder is managed by deploy-level BUILDER_PRIVATE_KEY. To disconnect, the operator must remove the env var.",
807
+ envManaged: true,
808
+ };
809
+ }
585
810
  // Delete per-user Builder credentials from app_secrets.
586
811
  try {
587
- const { deleteBuilderCredentials } = await import("./credential-provider.js");
588
812
  await deleteBuilderCredentials(session.email);
589
813
  }
590
814
  catch (err) {
815
+ trackBuilderLifecycle("builder disconnect failed", session.email, {
816
+ reason: "credential_delete_failed",
817
+ });
591
818
  setResponseStatus(event, 500);
592
819
  return {
593
820
  ok: false,
@@ -595,6 +822,7 @@ export function createCoreRoutesPlugin(options = {}) {
595
822
  cause: err instanceof Error ? err.message : String(err),
596
823
  };
597
824
  }
825
+ trackBuilderLifecycle("builder disconnect succeeded", session.email);
598
826
  return { ok: true };
599
827
  }));
600
828
  // Proxy to Builder's agents-run API for background code changes.
@@ -847,6 +1075,75 @@ export function createCoreRoutesPlugin(options = {}) {
847
1075
  };
848
1076
  }
849
1077
  }));
1078
+ // GET/PUT/DELETE /_agent-native/agent-loop-settings — org/user-scoped
1079
+ // ceiling for tool-calling loop iterations before the agent asks whether
1080
+ // it should keep going.
1081
+ getH3App(nitroApp).use(`${P}/agent-loop-settings`, defineEventHandler(async (event) => {
1082
+ const session = await getSession(event).catch(() => null);
1083
+ if (!session?.email) {
1084
+ setResponseStatus(event, 401);
1085
+ return { error: "unauthorized" };
1086
+ }
1087
+ const orgCtx = await getOrgContext(event).catch(() => null);
1088
+ const orgId = orgCtx?.orgId ?? session.orgId ?? null;
1089
+ const ctx = { userEmail: session.email, orgId };
1090
+ const canUpdate = await canUpdateAgentLoopSettings(session.email, orgId);
1091
+ const withContext = async () => ({
1092
+ ...(await readAgentLoopSettings(ctx)),
1093
+ canUpdate,
1094
+ orgId,
1095
+ orgName: orgCtx?.orgName ?? null,
1096
+ role: orgCtx?.role ?? null,
1097
+ });
1098
+ const method = getMethod(event);
1099
+ if (method === "GET") {
1100
+ return withContext();
1101
+ }
1102
+ if (method === "PUT") {
1103
+ if (!canUpdate) {
1104
+ setResponseStatus(event, 403);
1105
+ return {
1106
+ error: orgId
1107
+ ? "Only organization owners and admins can change the agent step limit."
1108
+ : "You cannot change the agent step limit.",
1109
+ };
1110
+ }
1111
+ const body = await readBody(event).catch(() => ({}));
1112
+ const validation = validateMaxIterationsInput(body?.maxIterations);
1113
+ if (validation.ok === false) {
1114
+ setResponseStatus(event, 400);
1115
+ return { error: validation.error };
1116
+ }
1117
+ const updated = await writeAgentLoopSettings(ctx, validation.value);
1118
+ return {
1119
+ ...updated,
1120
+ canUpdate,
1121
+ orgId,
1122
+ orgName: orgCtx?.orgName ?? null,
1123
+ role: orgCtx?.role ?? null,
1124
+ };
1125
+ }
1126
+ if (method === "DELETE") {
1127
+ if (!canUpdate) {
1128
+ setResponseStatus(event, 403);
1129
+ return {
1130
+ error: orgId
1131
+ ? "Only organization owners and admins can reset the agent step limit."
1132
+ : "You cannot reset the agent step limit.",
1133
+ };
1134
+ }
1135
+ const updated = await resetAgentLoopSettings(ctx);
1136
+ return {
1137
+ ...updated,
1138
+ canUpdate,
1139
+ orgId,
1140
+ orgName: orgCtx?.orgName ?? null,
1141
+ role: orgCtx?.role ?? null,
1142
+ };
1143
+ }
1144
+ setResponseStatus(event, 405);
1145
+ return { error: "Method not allowed" };
1146
+ }));
850
1147
  // ─── Usage & cost summary ────────────────────────────────────────
851
1148
  // GET /_agent-native/usage?sinceDays=30
852
1149
  // Returns spend broken down by label, model, app, and day for the