@agent-native/core 0.173.2-nightly-20260825163448 → 0.174.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/factory/AGENTS.md +1 -1
  3. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +23 -8
  4. package/corpus/templates/factory/actions/delete-factory.ts +174 -0
  5. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +28 -20
  6. package/corpus/templates/factory/actions/poll-github-sources.ts +6 -0
  7. package/corpus/templates/factory/actions/poll-sentry-errors.ts +16 -1
  8. package/corpus/templates/factory/actions/poll-slack-channel.ts +12 -0
  9. package/corpus/templates/factory/actions/start-builder-for-item.ts +74 -42
  10. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +117 -1
  11. package/corpus/templates/factory/app/i18n/en-US.ts +12 -0
  12. package/corpus/templates/factory/app/routes/factory.tsx +6 -1
  13. package/corpus/templates/factory/changelog/2026-08-24-slack-polling-accepts-the-agentnative-bot-handle.md +6 -0
  14. package/corpus/templates/factory/changelog/2026-08-24-you-can-delete-a-user-created-factory-from-settings.md +6 -0
  15. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +6 -0
  16. package/corpus/templates/factory/server/lib/factory-scope.ts +45 -1
  17. package/corpus/templates/factory/server/lib/require-factory-automation.ts +5 -0
  18. package/corpus/templates/factory/server/triage/slack-client.ts +10 -2
  19. package/corpus/templates/plan/server/plugins/core-routes.ts +6 -1
  20. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +4 -1
  21. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -2
  22. package/dist/app-config/app-identity.d.ts +20 -0
  23. package/dist/app-config/app-identity.js +41 -0
  24. package/dist/app-config/app.d.ts +2 -0
  25. package/dist/app-config/app.js +14 -0
  26. package/dist/app-config/observability.d.ts +27 -0
  27. package/dist/app-config/observability.js +76 -0
  28. package/dist/app-config/schema.d.ts +13 -0
  29. package/dist/app-config/schema.js +2 -0
  30. package/dist/app-config/store.d.ts +14 -4
  31. package/dist/app-config/store.js +18 -3
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/routes.d.ts +1 -1
  34. package/dist/email-catalog/actions/list-email-log.js +2 -2
  35. package/dist/email-catalog/actions/list-transactional-emails.js +2 -2
  36. package/dist/email-catalog/registry.js +3 -3
  37. package/dist/mcp/connect-route.js +15 -2
  38. package/dist/notifications/routes.d.ts +3 -3
  39. package/dist/observability/index.d.ts +1 -2
  40. package/dist/observability/index.js +0 -1
  41. package/dist/observability/posthog-ai.js +11 -3
  42. package/dist/observability/sentiment.d.ts +0 -1
  43. package/dist/observability/sentiment.js +8 -5
  44. package/dist/observability/traces.js +6 -12
  45. package/dist/observability/types.d.ts +0 -7
  46. package/dist/observability/types.js +1 -12
  47. package/dist/progress/routes.d.ts +1 -1
  48. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  49. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  50. package/dist/resources/handlers.d.ts +1 -1
  51. package/dist/secrets/routes.d.ts +3 -3
  52. package/dist/server/agent-chat/plugin-options.d.ts +17 -1
  53. package/dist/server/agent-chat-plugin.d.ts +10 -0
  54. package/dist/server/agent-chat-plugin.js +75 -67
  55. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  56. package/dist/server/core-routes/mcp-connect-options.d.ts +70 -0
  57. package/dist/server/core-routes/mcp-connect-options.js +77 -0
  58. package/dist/server/core-routes-plugin.d.ts +15 -11
  59. package/dist/server/core-routes-plugin.js +8 -7
  60. package/dist/server/edge.d.ts +1 -0
  61. package/dist/server/email-template.js +2 -2
  62. package/dist/server/email-templates.js +7 -7
  63. package/dist/server/email.js +4 -4
  64. package/dist/server/google-oauth.js +2 -2
  65. package/dist/server/http-response-telemetry.js +4 -4
  66. package/dist/server/identity-sso.js +1 -2
  67. package/dist/server/index.d.ts +1 -0
  68. package/dist/server/realtime-token.d.ts +1 -1
  69. package/dist/server/social-og-image.js +2 -2
  70. package/docs/content/observability.mdx +25 -31
  71. package/package.json +3 -3
  72. package/dist/server/app-name.d.ts +0 -33
  73. package/dist/server/app-name.js +0 -82
@@ -9,7 +9,7 @@ import { createA2AApproval, updateTaskStatusMessage, } from "../a2a/task-store.j
9
9
  import { clientAbortReason } from "../agent/abort-reasons.js";
10
10
  import { canUpdateAgentAppModelDefaultSettings, normalizeAgentAppModelDefaultAppId, readAgentAppModelDefaultSettings, resetAgentAppModelDefaultSettings, writeAgentAppModelDefaultSettings, } from "../agent/app-model-defaults.js";
11
11
  import { DEFAULT_ANTHROPIC_MODEL } from "../agent/default-model.js";
12
- import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRunMarkerExpectsBackgroundRuntime, isInBackgroundFunctionRuntime, prepareProcessRunRequest, } from "../agent/durable-background.js";
12
+ import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRunMarkerExpectsBackgroundRuntime, isAgentChatDurableBackgroundEnabled, isInBackgroundFunctionRuntime, prepareProcessRunRequest, } from "../agent/durable-background.js";
13
13
  import { resolveEngine, createAnthropicEngine, getStoredModelForEngine, normalizeModelForEngine, resolveDelegatedRunModel, getAgentEngineEntry, isAgentEnginePackageInstalled, isStoredEngineUsableForRequest, listAgentEngines, registerBuiltinEngines, } from "../agent/engine/index.js";
14
14
  import { SYSTEM_PROMPT_CACHE_SPLIT } from "../agent/engine/prompt-cache.js";
15
15
  import { PROVIDER_TO_ENV } from "../agent/engine/provider-env-vars.js";
@@ -153,6 +153,18 @@ export { resolveRecurringJobsBuildMarker };
153
153
  export { scheduledTriggerAvailability };
154
154
  export { shouldDisableRecurringJobsRuntime };
155
155
  export { finalizeClaimedAgentChatProcessRunFailure };
156
+ /**
157
+ * The model this mount runs with, when the caller does not pass one per request.
158
+ *
159
+ * The plugin option is the top layer because it is per-mount; `agent.model`
160
+ * (`AGENT_MODEL`) is the declared layer below it, so a deployment can pin a
161
+ * model without editing app source. Read through here rather than off
162
+ * `options` directly — every raw `options?.model` site is a place the declared
163
+ * field silently does nothing.
164
+ */
165
+ export function resolveConfiguredAgentModel(options) {
166
+ return options?.model ?? getAppConfig().agent.model;
167
+ }
156
168
  export function resolveInteractiveAgentRunOptions(options) {
157
169
  return {
158
170
  runSoftTimeoutMs: options?.runSoftTimeoutMs,
@@ -1427,7 +1439,7 @@ export function createAgentChatPlugin(options) {
1427
1439
  // below so it stays out of every identity/access path.
1428
1440
  const a2aCallerModelHint = correlation.callerModel;
1429
1441
  const model = resolveDelegatedRunModel(a2aEngine, {
1430
- explicitModel: options?.model,
1442
+ explicitModel: resolveConfiguredAgentModel(options),
1431
1443
  storedModel: a2aStoredModel,
1432
1444
  callerModelHint: a2aCallerModelHint,
1433
1445
  });
@@ -1444,7 +1456,7 @@ export function createAgentChatPlugin(options) {
1444
1456
  // label is derived from what actually won — reporting
1445
1457
  // "caller-hint" for a rejected hint would send the next person
1446
1458
  // debugging this in exactly the wrong direction.
1447
- `modelSource=${options?.model
1459
+ `modelSource=${resolveConfiguredAgentModel(options)
1448
1460
  ? "configured"
1449
1461
  : a2aStoredModel
1450
1462
  ? "stored"
@@ -1682,8 +1694,9 @@ export function createAgentChatPlugin(options) {
1682
1694
  // `runSoftTimeoutMs` resolves to 0 and the wrapper degrades to a
1683
1695
  // bare `runAgentLoop` — no resume at all on a delegated turn.
1684
1696
  useHostedDefault: true,
1685
- backgroundFunction: options?.durableBackgroundRuns === true &&
1686
- isInBackgroundFunctionRuntime(),
1697
+ backgroundFunction: isAgentChatDurableBackgroundEnabled({
1698
+ appOptIn: options?.durableBackgroundRuns,
1699
+ }) && isInBackgroundFunctionRuntime(),
1687
1700
  }, {
1688
1701
  telemetry: {
1689
1702
  runId: context.taskId,
@@ -1913,7 +1926,7 @@ export function createAgentChatPlugin(options) {
1913
1926
  apiKeyEnvVar: ownerApiKey.apiKeyEnvVar,
1914
1927
  appId: options?.appId,
1915
1928
  });
1916
- const mcpModelCandidate = options?.model ??
1929
+ const mcpModelCandidate = resolveConfiguredAgentModel(options) ??
1917
1930
  (await getStoredModelForEngine(mcpEngine, {
1918
1931
  appId: options?.appId,
1919
1932
  })) ??
@@ -2038,8 +2051,9 @@ export function createAgentChatPlugin(options) {
2038
2051
  // Same as the A2A call site: without this a hosted app that
2039
2052
  // never configured `runSoftTimeoutMs` gets no resume at all.
2040
2053
  useHostedDefault: true,
2041
- backgroundFunction: options?.durableBackgroundRuns === true &&
2042
- isInBackgroundFunctionRuntime(),
2054
+ backgroundFunction: isAgentChatDurableBackgroundEnabled({
2055
+ appOptIn: options?.durableBackgroundRuns,
2056
+ }) && isInBackgroundFunctionRuntime(),
2043
2057
  }, {
2044
2058
  telemetry: {
2045
2059
  runId: mcpRunId,
@@ -2141,62 +2155,56 @@ export function createAgentChatPlugin(options) {
2141
2155
  // queued-message save can clobber the assistant message we just
2142
2156
  // appended here, or vice versa.
2143
2157
  await withThreadDataLock(threadId, async () => {
2158
+ const thread = await getThread(threadId);
2159
+ if (!thread) {
2160
+ throw new Error(`Agent chat thread ${threadId} was not found while saving run ${run.runId}.`);
2161
+ }
2162
+ const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId, {
2163
+ suppressInternalContinuation: true,
2164
+ turnId: typeof run.turnId === "string" && run.turnId
2165
+ ? run.turnId
2166
+ : undefined,
2167
+ runDurationMs: typeof run.startedAt === "number" &&
2168
+ Number.isFinite(run.startedAt)
2169
+ ? Math.max(0, Date.now() - run.startedAt)
2170
+ : undefined,
2171
+ });
2172
+ if (!assistantMsg) {
2173
+ // No content produced — just bump timestamp
2174
+ await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
2175
+ return;
2176
+ }
2177
+ // Parse existing thread_data, append assistant message only if
2178
+ // the frontend hasn't already saved it (avoids duplicates when
2179
+ // the client is still connected during a normal flow).
2180
+ let repo;
2144
2181
  try {
2145
- const thread = await getThread(threadId);
2146
- if (!thread) {
2147
- throw new Error(`Agent chat thread ${threadId} was not found while saving run ${run.runId}.`);
2148
- }
2149
- const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId, {
2150
- suppressInternalContinuation: true,
2151
- turnId: typeof run.turnId === "string" && run.turnId
2152
- ? run.turnId
2153
- : undefined,
2154
- runDurationMs: typeof run.startedAt === "number" &&
2155
- Number.isFinite(run.startedAt)
2156
- ? Math.max(0, Date.now() - run.startedAt)
2157
- : undefined,
2158
- });
2159
- if (!assistantMsg) {
2160
- // No content produced — just bump timestamp
2161
- await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
2162
- return;
2163
- }
2164
- // Parse existing thread_data, append assistant message only if
2165
- // the frontend hasn't already saved it (avoids duplicates when
2166
- // the client is still connected during a normal flow).
2167
- let repo;
2168
- try {
2169
- repo = JSON.parse(thread.threadData || "{}");
2170
- }
2171
- catch {
2172
- repo = {};
2173
- }
2174
- if (!Array.isArray(repo.messages))
2175
- repo.messages = [];
2176
- repo = foldAssistantTurn(repo, assistantMsg, {
2177
- runId: run.runId,
2178
- turnId: typeof run.turnId === "string" && run.turnId
2179
- ? run.turnId
2180
- : undefined,
2181
- });
2182
- // Store debug metadata so we can inspect what the LLM actually
2183
- // received (system prompt, model, engine) when diagnosing issues.
2184
- const runCtx = getRequestRunContext();
2185
- const debug = {
2186
- runId: run.runId,
2187
- systemPrompt: runCtx?.systemPrompt,
2188
- model: runCtx?.model ?? resolvedModel,
2189
- engine: runCtx?.engine?.name ?? "unknown",
2190
- timestamp: Date.now(),
2191
- };
2192
- repo = appendThreadDebugHistory(repo, debug);
2193
- const meta = extractThreadMeta(repo);
2194
- await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
2182
+ repo = JSON.parse(thread.threadData || "{}");
2195
2183
  }
2196
- catch (err) {
2197
- // Run completion is only successful once thread_data is durable.
2198
- throw err;
2184
+ catch {
2185
+ repo = {};
2199
2186
  }
2187
+ if (!Array.isArray(repo.messages))
2188
+ repo.messages = [];
2189
+ repo = foldAssistantTurn(repo, assistantMsg, {
2190
+ runId: run.runId,
2191
+ turnId: typeof run.turnId === "string" && run.turnId
2192
+ ? run.turnId
2193
+ : undefined,
2194
+ });
2195
+ // Store debug metadata so we can inspect what the LLM actually
2196
+ // received (system prompt, model, engine) when diagnosing issues.
2197
+ const runCtx = getRequestRunContext();
2198
+ const debug = {
2199
+ runId: run.runId,
2200
+ systemPrompt: runCtx?.systemPrompt,
2201
+ model: runCtx?.model ?? resolvedModel,
2202
+ engine: runCtx?.engine?.name ?? "unknown",
2203
+ timestamp: Date.now(),
2204
+ };
2205
+ repo = appendThreadDebugHistory(repo, debug);
2206
+ const meta = extractThreadMeta(repo);
2207
+ await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
2200
2208
  });
2201
2209
  // Keep SQL run completion gated only on durable thread data. Follow-up
2202
2210
  // hooks are useful, but they should never leave agent_runs stuck
@@ -2382,7 +2390,7 @@ export function createAgentChatPlugin(options) {
2382
2390
  // Each run gets its own send function, keyed by threadId so concurrent
2383
2391
  // requests for different threads don't clobber each other.
2384
2392
  const _runSendByThread = new Map();
2385
- const resolvedModel = options?.model ?? DEFAULT_ANTHROPIC_MODEL;
2393
+ const resolvedModel = resolveConfiguredAgentModel(options) ?? DEFAULT_ANTHROPIC_MODEL;
2386
2394
  // The action set a sub-agent inherits. Shared between the spawn-time team
2387
2395
  // tool and the `_process-run` processor route below so the durable run
2388
2396
  // executes with exactly the tools the orchestrator intended.
@@ -2844,7 +2852,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
2844
2852
  modelOverlay +
2845
2853
  runtimeContext);
2846
2854
  },
2847
- model: options?.model,
2855
+ model: resolveConfiguredAgentModel(options),
2848
2856
  appId: options?.appId,
2849
2857
  hostedHarnessConfig,
2850
2858
  apiKey: options?.apiKey,
@@ -2947,7 +2955,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
2947
2955
  extra +
2948
2956
  runtimeContextForEvent(event));
2949
2957
  },
2950
- model: options?.model,
2958
+ model: resolveConfiguredAgentModel(options),
2951
2959
  appId: options?.appId,
2952
2960
  apiKey: options?.apiKey,
2953
2961
  ...resolveInteractiveAgentRunOptions(options),
@@ -3127,7 +3135,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3127
3135
  modelOverlay +
3128
3136
  runtimeContext);
3129
3137
  },
3130
- model: options?.model,
3138
+ model: resolveConfiguredAgentModel(options),
3131
3139
  appId: options?.appId,
3132
3140
  apiKey: options?.apiKey,
3133
3141
  ...resolveInteractiveAgentRunOptions(options),
@@ -5213,7 +5221,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5213
5221
  ...(job?.meta.mcpTools ?? []),
5214
5222
  ],
5215
5223
  apiKey: options?.apiKey,
5216
- model: options?.model,
5224
+ model: resolveConfiguredAgentModel(options),
5217
5225
  appId: options?.appId,
5218
5226
  };
5219
5227
  runNowSchedulerDeps = schedulerDeps;
@@ -5645,7 +5653,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5645
5653
  ...(automation?.meta.mcpTools ?? []),
5646
5654
  ],
5647
5655
  apiKey: options?.apiKey,
5648
- model: options?.model,
5656
+ model: resolveConfiguredAgentModel(options),
5649
5657
  appId: options?.appId,
5650
5658
  });
5651
5659
  if (process.env.DEBUG)
@@ -34,10 +34,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
34
34
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
35
35
  error: any;
36
36
  } | {
37
- error?: undefined;
38
37
  ok: boolean;
39
38
  key: string;
40
39
  baseUrlKey?: string;
41
40
  scope: AgentEngineApiKeyScope;
41
+ error?: undefined;
42
42
  }>>;
43
43
  export {};
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Everything the core-routes plugin can configure about its MCP *connect*
3
+ * surface, in one object.
4
+ *
5
+ * This is deliberately not the same object as `AgentChatMcpOptions`. That one
6
+ * owns the protocol mount — which tools an external caller sees. This one owns
7
+ * how a caller gets a token in the first place: the browser Connect page, the
8
+ * CLI device-code flow, and the OAuth endpoints. They live in different plugins
9
+ * because core-routes is the light one; an app can serve MCP from it without
10
+ * paying for chat plugin initialization, which is what `mcp.enabled: false` on
11
+ * the chat side exists for.
12
+ *
13
+ * Before this existed the settings were four flat `mcpConnect*` keys, two of
14
+ * which only restated app identity the config schema already declares.
15
+ */
16
+ export interface CoreRoutesMcpOptions {
17
+ /**
18
+ * Mount the `/mcp/connect` routes (browser Connect page + CLI device-code
19
+ * flow that mints per-user, revocable MCP tokens) and the standard
20
+ * remote-MCP OAuth endpoints under `/mcp/oauth`. The legacy
21
+ * `/_agent-native/mcp` aliases follow this same switch. Defaults to `true` —
22
+ * the routes are session-gated where they approve user access, and token
23
+ * endpoints are protected by single-use codes / refresh tokens.
24
+ */
25
+ connect?: boolean;
26
+ /**
27
+ * Explicit MCP server id returned in copyable config and device-flow grants.
28
+ *
29
+ * Only needed when the id a client should key this server by differs from
30
+ * this app's own identity. Otherwise it derives from `app.id` /
31
+ * `app.template` / `app.slug`, which covers every first-party template with
32
+ * no configuration at all.
33
+ */
34
+ serverName?: string;
35
+ }
36
+ /** The legacy top-level keys `mcp` replaces. */
37
+ export interface CoreRoutesMcpLegacyInput {
38
+ /** @deprecated Use `mcp.connect: false`. */
39
+ disableMcpConnect?: boolean;
40
+ /** @deprecated Use `mcp.serverName`. */
41
+ mcpConnectServerName?: string;
42
+ /**
43
+ * @deprecated Set `app.id` in `defineAppConfig()` (env alias
44
+ * `AGENT_NATIVE_APP_ID`). App identity is one declared field, not a
45
+ * per-surface option.
46
+ */
47
+ mcpConnectAppId?: string;
48
+ /**
49
+ * @deprecated Set `app.name` in `defineAppConfig()` (env alias `APP_NAME`).
50
+ * Unset, it resolves from this app's package.json.
51
+ */
52
+ mcpConnectAppName?: string;
53
+ mcp?: CoreRoutesMcpOptions;
54
+ }
55
+ export interface ResolvedCoreRoutesMcp {
56
+ connect: boolean;
57
+ serverName: string | undefined;
58
+ /** Passed through to the connect/OAuth handlers as their app identity. */
59
+ appId: string | undefined;
60
+ appName: string | undefined;
61
+ }
62
+ /**
63
+ * Collapse the nested `mcp` option and the legacy top-level keys into the one
64
+ * shape the plugin threads into the connect and OAuth handlers.
65
+ *
66
+ * `appId` and `appName` fall through to the declared config fields rather than
67
+ * defaulting here, so the connect page, the OAuth consent screen, and the
68
+ * runtime config report cannot disagree about what this app is called.
69
+ */
70
+ export declare function resolveCoreRoutesMcpOptions(input: CoreRoutesMcpLegacyInput | undefined): ResolvedCoreRoutesMcp;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Everything the core-routes plugin can configure about its MCP *connect*
3
+ * surface, in one object.
4
+ *
5
+ * This is deliberately not the same object as `AgentChatMcpOptions`. That one
6
+ * owns the protocol mount — which tools an external caller sees. This one owns
7
+ * how a caller gets a token in the first place: the browser Connect page, the
8
+ * CLI device-code flow, and the OAuth endpoints. They live in different plugins
9
+ * because core-routes is the light one; an app can serve MCP from it without
10
+ * paying for chat plugin initialization, which is what `mcp.enabled: false` on
11
+ * the chat side exists for.
12
+ *
13
+ * Before this existed the settings were four flat `mcpConnect*` keys, two of
14
+ * which only restated app identity the config schema already declares.
15
+ */
16
+ import { getAppConfig } from "../../app-config/index.js";
17
+ const warnedLegacyKeys = new Set();
18
+ /**
19
+ * Nested value wins, but only when the two forms agree.
20
+ *
21
+ * Disagreement throws at plugin init rather than picking a side, the same
22
+ * contract as `resolveAgentChatMcpOptions` and `resolveFrameworkTools`: an app
23
+ * that boots with a connect surface nobody chose is how a "why does my client
24
+ * see the wrong server" report ends up unexplainable.
25
+ */
26
+ function pick(key, legacyKey, legacyValue, nestedValue) {
27
+ if (legacyValue === undefined)
28
+ return nestedValue;
29
+ if (nestedValue !== undefined && legacyValue !== nestedValue) {
30
+ throw new Error(`[agent-native] Conflicting core-routes options: \`${legacyKey}: ${JSON.stringify(legacyValue)}\` ` +
31
+ `and \`mcp.${key}: ${JSON.stringify(nestedValue)}\` disagree. ` +
32
+ `Remove the deprecated \`${legacyKey}\` and keep \`mcp.${key}\`.`);
33
+ }
34
+ if (!warnedLegacyKeys.has(legacyKey)) {
35
+ warnedLegacyKeys.add(legacyKey);
36
+ console.warn(`[agent-native] \`${legacyKey}\` is deprecated — use \`mcp: { ${key}: … }\`.`);
37
+ }
38
+ return nestedValue ?? legacyValue;
39
+ }
40
+ const warnedIdentityKeys = new Set();
41
+ function warnIdentityOption(legacyKey, field) {
42
+ if (warnedIdentityKeys.has(legacyKey))
43
+ return;
44
+ warnedIdentityKeys.add(legacyKey);
45
+ console.warn(`[agent-native] \`${legacyKey}\` is deprecated — set \`${field}\` in ` +
46
+ `defineAppConfig() so every surface reads one app identity.`);
47
+ }
48
+ /**
49
+ * Collapse the nested `mcp` option and the legacy top-level keys into the one
50
+ * shape the plugin threads into the connect and OAuth handlers.
51
+ *
52
+ * `appId` and `appName` fall through to the declared config fields rather than
53
+ * defaulting here, so the connect page, the OAuth consent screen, and the
54
+ * runtime config report cannot disagree about what this app is called.
55
+ */
56
+ export function resolveCoreRoutesMcpOptions(input) {
57
+ const mcp = input?.mcp ?? {};
58
+ // `disableMcpConnect` is the inverse of `connect`, so normalize before
59
+ // comparing — otherwise `disableMcpConnect: true` + `connect: false` would
60
+ // read as a conflict.
61
+ const legacyConnect = input?.disableMcpConnect === undefined
62
+ ? undefined
63
+ : !input.disableMcpConnect;
64
+ if (input?.mcpConnectAppId !== undefined) {
65
+ warnIdentityOption("mcpConnectAppId", "app.id");
66
+ }
67
+ if (input?.mcpConnectAppName !== undefined) {
68
+ warnIdentityOption("mcpConnectAppName", "app.name");
69
+ }
70
+ const app = getAppConfig().app;
71
+ return {
72
+ connect: pick("connect", "disableMcpConnect", legacyConnect, mcp.connect) ?? true,
73
+ serverName: pick("serverName", "mcpConnectServerName", input?.mcpConnectServerName, mcp.serverName),
74
+ appId: input?.mcpConnectAppId ?? app.id,
75
+ appName: input?.mcpConnectAppName ?? app.name,
76
+ };
77
+ }
@@ -3,6 +3,7 @@ import { type DatabaseSchemaHealthResult } from "../db/runtime-diagnostics.js";
3
3
  import { getSetting, deleteSetting, mutateSetting, listSettingsByPrefix } from "../settings/store.js";
4
4
  import { type AuthSession } from "./auth.js";
5
5
  import { type BuilderConnectTrackingParams, type BuilderRelayCredentials } from "./builder-browser.js";
6
+ import { type CoreRoutesMcpOptions } from "./core-routes/mcp-connect-options.js";
6
7
  import type { EnvKeyConfig } from "./create-server.js";
7
8
  import { type DbPressure } from "./db-pressure.js";
8
9
  /**
@@ -261,20 +262,23 @@ export interface CoreRoutesPluginOptions {
261
262
  /** Disable the /_agent-native/embed/start iframe session launcher. */
262
263
  disableEmbedRoute?: boolean;
263
264
  /**
264
- * Disable the /mcp/connect routes (browser Connect page + CLI device-code
265
- * flow that mints per-user, revocable MCP tokens) and the standard remote-MCP
266
- * OAuth endpoints under /mcp/oauth. The legacy /_agent-native/mcp aliases
267
- * are disabled at the same time.
268
- * Enabled by default — the routes are session-gated where they approve user
269
- * access; token endpoints are protected by single-use codes / refresh
270
- * tokens.
265
+ * Everything about this app's MCP connect surface — whether the Connect page
266
+ * and OAuth endpoints are mounted, and the server id clients key it by.
267
+ * See `CoreRoutesMcpOptions`.
268
+ *
269
+ * Replaces the top-level `disableMcpConnect`, `mcpConnectServerName`,
270
+ * `mcpConnectAppId`, and `mcpConnectAppName`, which stay accepted for one
271
+ * minor. Setting both forms to disagreeing values throws at plugin init
272
+ * rather than silently picking one.
271
273
  */
274
+ mcp?: CoreRoutesMcpOptions;
275
+ /** @deprecated Use `mcp.connect: false`. */
272
276
  disableMcpConnect?: boolean;
273
- /** Canonical app id (e.g. `mail`) for the MCP connect server name. */
274
- mcpConnectAppId?: string;
275
- /** Explicit MCP server id for copyable config/device-flow grants. */
277
+ /** @deprecated Use `mcp.serverName`. */
276
278
  mcpConnectServerName?: string;
277
- /** Human app name shown on the MCP connect page. */
279
+ /** @deprecated Set `app.id` in `defineAppConfig()`. */
280
+ mcpConnectAppId?: string;
281
+ /** @deprecated Set `app.name` in `defineAppConfig()`. */
278
282
  mcpConnectAppName?: string;
279
283
  /** Per-template override mapping deep-link params → client SPA path.
280
284
  * See `createOpenRouteHandler`. */
@@ -40,11 +40,11 @@ import { createAutomationsHandler } from "../triggers/routes.js";
40
40
  import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js";
41
41
  import { readAnalyticsClientPlatformHeader, readBrowserSessionIdHeader, } from "./agent-run-context.js";
42
42
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
43
- import { getAppName } from "./app-name.js";
44
43
  import { getSession } from "./auth.js";
45
44
  import { BUILDER_CONNECT_PARAM, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_RELAY_FLOW_HEADER, BUILDER_RELAY_SIGNATURE_HEADER, BUILDER_RELAY_STATE_PARAM, BUILDER_RELAY_TIMESTAMP_HEADER, appendBuilderConnectToken, builderConnectTrackingProperties, createBuilderConnectState, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, createBuilderRelayRequest, getBuilderConnectTrackingParams, getBuilderBrowserOriginForEvent, getBuilderBrowserStatusForEvent, isBuilderConnectCallbackUrlAllowed, isSignedBuilderConnectState, normalizeBuilderAgentContext, resolveBuilderBranchProjectId, resolveBuilderConnectCallbackUrl, resolveBuilderPreviewRelayParentOrigin, runBuilderAgent, verifyBuilderRelayRequest, verifyBuilderPreviewRelayStateForCallback, verifyBuilderConnectTokenAndGetOwner, } from "./builder-browser.js";
46
45
  import { BUILDER_OAUTH_SCOPE, deleteBuilderOAuthSession, finishBuilderOAuthAuthorization, hasBuilderOAuthSession, resolveBuilderOAuthRequestAccess, startBuilderOAuthAuthorization, } from "./builder-oauth.js";
47
46
  import { captureError, registerErrorCaptureProvider } from "./capture-error.js";
47
+ import { resolveCoreRoutesMcpOptions, } from "./core-routes/mcp-connect-options.js";
48
48
  import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
49
49
  import { canUseDeployCredentialFallbackForRequest, prefetchSecrets, readDeployCredentialEnv, resolveSecret, } from "./credential-provider.js";
50
50
  import { probeDbPressure } from "./db-pressure.js";
@@ -3038,7 +3038,8 @@ export function createCoreRoutesPlugin(options = {}) {
3038
3038
  setResponseStatus(event, 405);
3039
3039
  return { error: "Method not allowed" };
3040
3040
  }));
3041
- if (!options.disableMcpConnect) {
3041
+ const mcpConnect = resolveCoreRoutesMcpOptions(options);
3042
+ if (mcpConnect.connect) {
3042
3043
  getH3App(nitroApp).use("/.well-known/oauth-protected-resource", defineEventHandler((event) => handleMcpOAuthProtectedResourceMetadata(event)));
3043
3044
  getH3App(nitroApp).use("/.well-known/oauth-authorization-server", defineEventHandler((event) => handleMcpOAuthAuthorizationServerMetadata(event)));
3044
3045
  getH3App(nitroApp).use("/.well-known/openid-configuration", defineEventHandler((event) => handleMcpOAuthAuthorizationServerMetadata(event)));
@@ -3046,8 +3047,8 @@ export function createCoreRoutesPlugin(options = {}) {
3046
3047
  getH3App(nitroApp).use(`${mcpRoutePrefix}/oauth`, defineEventHandler(async (event) => {
3047
3048
  const subpath = event.url?.pathname || "";
3048
3049
  return handleMcpOAuth(event, subpath, {
3049
- appId: options.mcpConnectAppId,
3050
- appName: options.mcpConnectAppName ?? getAppName(),
3050
+ appId: mcpConnect.appId,
3051
+ appName: mcpConnect.appName,
3051
3052
  });
3052
3053
  }));
3053
3054
  }
@@ -3061,9 +3062,9 @@ export function createCoreRoutesPlugin(options = {}) {
3061
3062
  // The auth guard bypasses ONLY the page + device/start + device/poll
3062
3063
  // (see createAuthGuardFn in auth.ts).
3063
3064
  const mcpConnectOpts = {
3064
- appId: options.mcpConnectAppId,
3065
- appName: options.mcpConnectAppName ?? getAppName(),
3066
- serverName: options.mcpConnectServerName,
3065
+ appId: mcpConnect.appId,
3066
+ appName: mcpConnect.appName,
3067
+ serverName: mcpConnect.serverName,
3067
3068
  };
3068
3069
  for (const mcpRoutePrefix of MCP_ROUTE_PREFIXES) {
3069
3070
  getH3App(nitroApp).use(`${mcpRoutePrefix}/connect`, defineEventHandler(async (event) => {
@@ -5,6 +5,7 @@ export { runFrameworkReleaseMigrations } from "./release-migrations.js";
5
5
  export { createAgentChatPlugin, defaultAgentChatPlugin, refreshGlobalMcpManager, type AgentChatPluginOptions, } from "./agent-chat-plugin.js";
6
6
  export { createContextXrayPlugin, defaultContextXrayPlugin, } from "../agent/context-xray/plugin.js";
7
7
  export { createCoreRoutesPlugin, defaultCoreRoutesPlugin, FRAMEWORK_ROUTE_PREFIX, type CoreRoutesPluginOptions, } from "./core-routes-plugin.js";
8
+ export type { CoreRoutesMcpOptions } from "./core-routes/mcp-connect-options.js";
8
9
  export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, type PlatformAdapter, type IncomingMessage, type OutgoingMessage, type PlatformAdapterCapabilities, type ImmediateWebhookResponse, type IntegrationStatus, type IntegrationsPluginOptions, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, type BuiltInChannelId, type ChannelCapabilities, type IntegrationAvailability, type IntegrationCatalogEntry, type IntegrationCategory, type IntegrationCredentialRequirement, type IntegrationIconKey, type IntegrationSupportMaturity, } from "../integrations/index.js";
9
10
  export { createObservationalMemoryPlugin, defaultObservationalMemoryPlugin, } from "../agent/observational-memory/plugin.js";
10
11
  export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/plugin.js";
@@ -1,3 +1,4 @@
1
+ import { getAppConfig } from "../app-config/index.js";
1
2
  /**
2
3
  * Reusable dark-themed HTML email template.
3
4
  *
@@ -17,7 +18,6 @@
17
18
  * footer: "If you weren't expecting this, ignore this email.",
18
19
  * });
19
20
  */
20
- import { getAppName } from "./app-name.js";
21
21
  export const AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID = "agent-native-logo";
22
22
  function escapeHtml(s) {
23
23
  return s
@@ -57,7 +57,7 @@ function sanitizeLogoUrl(input) {
57
57
  export function renderEmail(args) {
58
58
  const preheader = args.preheader || "";
59
59
  const brand = sanitizeHexColor(args.brandColor);
60
- const brandName = args.brandName?.trim() || getAppName() || "Agent Native";
60
+ const brandName = args.brandName?.trim() || getAppConfig().app.name || "Agent Native";
61
61
  const logoSrc = sanitizeLogoUrl(args.brandLogoUrl) ??
62
62
  `cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`;
63
63
  // Monochrome default: near-white button with dark text. Brand override:
@@ -10,7 +10,7 @@
10
10
  * confirmation), add it here rather than inlining `renderEmail` at the call
11
11
  * site — keeps the transactional look-and-feel consistent.
12
12
  */
13
- import { getAppName, getAppSlug, getAppDescription } from "./app-name.js";
13
+ import { getAppConfig } from "../app-config/index.js";
14
14
  import { renderEmail, emailStrong } from "./email-template.js";
15
15
  /** Shared reply-to for the framework's transactional emails. */
16
16
  export const AGENT_NATIVE_REPLY_TO = "agent-native@builder.io";
@@ -23,7 +23,7 @@ function stripCrlf(s) {
23
23
  return s.replace(/[\r\n]+/g, " ").trim();
24
24
  }
25
25
  function resolveAppName() {
26
- return stripCrlf(getAppName() || "Agent Native");
26
+ return stripCrlf(getAppConfig().app.name || "Agent Native");
27
27
  }
28
28
  /**
29
29
  * Recipient-facing brand for auth emails. Only a recognized first-party
@@ -31,7 +31,7 @@ function resolveAppName() {
31
31
  * own name so its users aren't told they signed up for an Agent Native app.
32
32
  */
33
33
  function resolveBrand(slug) {
34
- const appName = getAppName();
34
+ const appName = getAppConfig().app.name;
35
35
  if (!appName)
36
36
  return "Agent Native";
37
37
  return slug ? `Agent-Native ${stripCrlf(appName)}` : stripCrlf(appName);
@@ -74,10 +74,10 @@ const VERIFY_EMAIL_DESCRIPTIONS = {
74
74
  };
75
75
  export function renderVerifySignupEmail(args) {
76
76
  const email = stripCrlf(args.email);
77
- const slug = getAppSlug();
77
+ const slug = getAppConfig().app.slug;
78
78
  const brand = resolveBrand(slug);
79
79
  const description = slug
80
- ? (VERIFY_EMAIL_DESCRIPTIONS[slug] ?? getAppDescription())
80
+ ? (VERIFY_EMAIL_DESCRIPTIONS[slug] ?? getAppConfig().app.description)
81
81
  : undefined;
82
82
  const paragraphs = [
83
83
  `Thanks for signing up for ${emailStrong(brand)}. To finish creating your account, confirm that ${emailStrong(email)} is your email address.`,
@@ -105,7 +105,7 @@ export function renderVerifySignupEmail(args) {
105
105
  }
106
106
  export function renderMagicLinkEmail(args) {
107
107
  const email = stripCrlf(args.email);
108
- const slug = getAppSlug();
108
+ const slug = getAppConfig().app.slug;
109
109
  const brand = resolveBrand(slug);
110
110
  const { html, text } = renderEmail({
111
111
  brandName: brand,
@@ -131,7 +131,7 @@ export function renderResetPasswordEmail(args) {
131
131
  const email = stripCrlf(args.email);
132
132
  // Match the verification email branding so password resets are clearly tied
133
133
  // to the specific app. No value pitch here — it's a security email.
134
- const slug = getAppSlug();
134
+ const slug = getAppConfig().app.slug;
135
135
  const brand = resolveBrand(slug);
136
136
  const { html, text } = renderEmail({
137
137
  brandName: brand,
@@ -9,9 +9,9 @@
9
9
  * With neither provider configured, `sendEmail` logs the message to the console
10
10
  * so the reset-password flow still works end-to-end for local development.
11
11
  */
12
+ import { getAppConfig } from "../app-config/index.js";
12
13
  import { FAVICON_PNG_BASE64 } from "../assets/branding/favicon-base64.js";
13
14
  import { getScopedEmailProviderCategory, recordEmailSend, } from "../email-catalog/log.js";
14
- import { getAppSlug } from "./app-name.js";
15
15
  import { resolveSecret } from "./credential-provider.js";
16
16
  import { AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID } from "./email-template.js";
17
17
  import { getRequestOrgId } from "./request-context.js";
@@ -236,7 +236,7 @@ async function deliverEmail(args, signal) {
236
236
  const orgId = args.orgId ?? getRequestOrgId();
237
237
  const categories = [
238
238
  args.templateId,
239
- args.app ?? getAppSlug(),
239
+ args.app ?? getAppConfig().app.slug,
240
240
  args.templateId && orgId
241
241
  ? getScopedEmailProviderCategory(args.templateId, orgId)
242
242
  : undefined,
@@ -300,7 +300,7 @@ async function sendEmailWithSignal(args, signal) {
300
300
  catch (error) {
301
301
  await recordEmailSend({
302
302
  templateId: args.templateId,
303
- app: args.app ?? getAppSlug() ?? "unknown",
303
+ app: args.app ?? getAppConfig().app.slug ?? "unknown",
304
304
  orgId: args.orgId ?? getRequestOrgId(),
305
305
  recipient: args.to,
306
306
  sender: outcome?.from ?? args.from ?? "unknown",
@@ -313,7 +313,7 @@ async function sendEmailWithSignal(args, signal) {
313
313
  }
314
314
  await recordEmailSend({
315
315
  templateId: args.templateId,
316
- app: args.app ?? getAppSlug() ?? "unknown",
316
+ app: args.app ?? getAppConfig().app.slug ?? "unknown",
317
317
  orgId: args.orgId ?? getRequestOrgId(),
318
318
  recipient: args.to,
319
319
  sender: outcome.from,
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import crypto from "node:crypto";
10
10
  import { getHeader, getQuery, setResponseStatus, setResponseHeader, } from "h3";
11
+ import { getAppConfig } from "../app-config/index.js";
11
12
  import { normalizeAnalyticsAnonymousId } from "../shared/analytics-anonymous-id.js";
12
13
  import { getAppBasePathFromViteEnv } from "./app-base-path.js";
13
- import { getAppName } from "./app-name.js";
14
14
  import { readAnalyticsAnonymousId, signupAttributionFromCookieHeader, } from "./attribution.js";
15
15
  import { addSession, getSession, getSessionMaxAge, hasLegacySessionForEmail, safeReturnPath, setFrameworkSessionCookie, } from "./auth.js";
16
16
  import { hasBetterAuthUserEmail, trackSignupEvent, } from "./better-auth-instance.js";
@@ -731,7 +731,7 @@ export function oauthDesktopExchangePage(message = "Returning to the app...", cl
731
731
  }
732
732
  // ─── Internal ────────────────────────────────────────────────────────────────
733
733
  function resolveOAuthAppName(explicit) {
734
- const raw = explicit || getAppName() || "Agent Native";
734
+ const raw = explicit || getAppConfig().app.name || "Agent Native";
735
735
  if (!/^[a-z0-9_-]+$/.test(raw))
736
736
  return raw;
737
737
  return raw