@agent-native/core 0.207.0-nightly-20261011124220 → 0.207.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 (108) hide show
  1. package/dist/agent/production-agent.js +23 -6
  2. package/dist/agent/thread-data-builder.js +7 -4
  3. package/dist/cli/eval.js +20 -4
  4. package/dist/client/analytics/index.d.ts +1 -1
  5. package/dist/client/analytics.d.ts +12 -3
  6. package/dist/client/analytics.js +51 -17
  7. package/dist/client/onboarding/use-onboarding.d.ts +1 -0
  8. package/dist/client/onboarding/use-onboarding.js +19 -6
  9. package/dist/collab/struct-routes.d.ts +1 -1
  10. package/dist/eval/agent-runner.js +18 -8
  11. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  12. package/dist/mcp/connect-route.js +9 -4
  13. package/dist/observability/routes.d.ts +7 -7
  14. package/dist/onboarding/plugin.js +15 -45
  15. package/dist/progress/routes.d.ts +1 -1
  16. package/dist/resources/store.js +25 -30
  17. package/dist/server/agent-discovery.js +47 -53
  18. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  19. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  20. package/dist/server/builtin-agents.d.ts +2 -0
  21. package/dist/server/builtin-agents.js +50 -3
  22. package/dist/server/credential-provider.d.ts +6 -9
  23. package/dist/server/credential-provider.js +33 -21
  24. package/dist/templates/builder-code-starter/.agents/skills/adding-a-feature/SKILL.md.patch +1 -1
  25. package/dist/templates/builder-code-starter/AGENTS.md.patch +3 -3
  26. package/dist/templates/builder-code-starter/app/components/layout/Layout.tsx.patch +6 -7
  27. package/dist/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +21 -28
  28. package/dist/templates/builder-code-starter/app/root.tsx.patch +16 -5
  29. package/dist/templates/builder-code-starter/template-layer.json +4 -0
  30. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +6 -2
  31. package/dist/templates/chat/AGENTS.md +3 -3
  32. package/dist/templates/chat/CHANGELOG.md +6 -0
  33. package/dist/templates/chat/app/components/chat/ChatHomeRedirect.tsx +25 -0
  34. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +828 -15
  35. package/dist/templates/chat/app/components/home/HomePage.tsx +332 -0
  36. package/dist/templates/chat/app/components/layout/AgentInspector.tsx +2 -4
  37. package/dist/templates/chat/app/components/layout/Header.tsx +1 -1
  38. package/dist/templates/chat/app/components/layout/Layout.tsx +5 -6
  39. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -11
  40. package/dist/templates/chat/app/components/ui/item.tsx +1 -0
  41. package/dist/templates/chat/app/components/ui/separator.tsx +1 -0
  42. package/dist/templates/chat/app/components/ui/skeleton.tsx +1 -0
  43. package/dist/templates/chat/app/hooks/use-navigation-state.ts +16 -22
  44. package/dist/templates/chat/app/i18n/ar-SA.ts +25 -0
  45. package/dist/templates/chat/app/i18n/de-DE.ts +26 -0
  46. package/dist/templates/chat/app/i18n/en-US.ts +25 -0
  47. package/dist/templates/chat/app/i18n/es-ES.ts +26 -0
  48. package/dist/templates/chat/app/i18n/fr-FR.ts +26 -0
  49. package/dist/templates/chat/app/i18n/hi-IN.ts +26 -0
  50. package/dist/templates/chat/app/i18n/ja-JP.ts +26 -0
  51. package/dist/templates/chat/app/i18n/ko-KR.ts +26 -0
  52. package/dist/templates/chat/app/i18n/pt-BR.ts +26 -0
  53. package/dist/templates/chat/app/i18n/zh-CN.ts +25 -0
  54. package/dist/templates/chat/app/i18n/zh-TW.ts +25 -0
  55. package/dist/templates/chat/app/lib/chat-paths.ts +479 -0
  56. package/dist/templates/chat/app/root.tsx +14 -3
  57. package/dist/templates/chat/app/routes/chat._index.tsx +5 -0
  58. package/dist/templates/chat/app/routes/home.tsx +3 -22
  59. package/dist/templates/chat/changelog/2026-10-10-home-handoff-recovery-capacity.md +6 -0
  60. package/dist/templates/default/.agents/skills/secrets/SKILL.md +6 -2
  61. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +6 -2
  62. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +6 -2
  63. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  64. package/docs/content/deployment-environment-variables.mdx +10 -10
  65. package/docs/content/environment-variables.mdx +38 -6
  66. package/docs/content/getting-started.mdx +7 -7
  67. package/docs/content/multi-app-workspace.mdx +7 -3
  68. package/docs/content/template-chat-first-edits.mdx +46 -33
  69. package/package.json +4 -4
  70. package/src/templates/builder-code-starter/.agents/skills/adding-a-feature/SKILL.md.patch +1 -1
  71. package/src/templates/builder-code-starter/AGENTS.md.patch +3 -3
  72. package/src/templates/builder-code-starter/app/components/layout/Layout.tsx.patch +6 -7
  73. package/src/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +21 -28
  74. package/src/templates/builder-code-starter/app/root.tsx.patch +16 -5
  75. package/src/templates/builder-code-starter/template-layer.json +4 -0
  76. package/src/templates/chat/.agents/skills/secrets/SKILL.md +6 -2
  77. package/src/templates/chat/AGENTS.md +3 -3
  78. package/src/templates/chat/CHANGELOG.md +6 -0
  79. package/src/templates/chat/app/components/chat/ChatHomeRedirect.tsx +25 -0
  80. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +828 -15
  81. package/src/templates/chat/app/components/home/HomePage.tsx +332 -0
  82. package/src/templates/chat/app/components/layout/AgentInspector.tsx +2 -4
  83. package/src/templates/chat/app/components/layout/Header.tsx +1 -1
  84. package/src/templates/chat/app/components/layout/Layout.tsx +5 -6
  85. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -11
  86. package/src/templates/chat/app/components/ui/item.tsx +1 -0
  87. package/src/templates/chat/app/components/ui/separator.tsx +1 -0
  88. package/src/templates/chat/app/components/ui/skeleton.tsx +1 -0
  89. package/src/templates/chat/app/hooks/use-navigation-state.ts +16 -22
  90. package/src/templates/chat/app/i18n/ar-SA.ts +25 -0
  91. package/src/templates/chat/app/i18n/de-DE.ts +26 -0
  92. package/src/templates/chat/app/i18n/en-US.ts +25 -0
  93. package/src/templates/chat/app/i18n/es-ES.ts +26 -0
  94. package/src/templates/chat/app/i18n/fr-FR.ts +26 -0
  95. package/src/templates/chat/app/i18n/hi-IN.ts +26 -0
  96. package/src/templates/chat/app/i18n/ja-JP.ts +26 -0
  97. package/src/templates/chat/app/i18n/ko-KR.ts +26 -0
  98. package/src/templates/chat/app/i18n/pt-BR.ts +26 -0
  99. package/src/templates/chat/app/i18n/zh-CN.ts +25 -0
  100. package/src/templates/chat/app/i18n/zh-TW.ts +25 -0
  101. package/src/templates/chat/app/lib/chat-paths.ts +479 -0
  102. package/src/templates/chat/app/root.tsx +14 -3
  103. package/src/templates/chat/app/routes/chat._index.tsx +5 -0
  104. package/src/templates/chat/app/routes/home.tsx +3 -22
  105. package/src/templates/chat/changelog/2026-10-10-home-handoff-recovery-capacity.md +6 -0
  106. package/src/templates/default/.agents/skills/secrets/SKILL.md +6 -2
  107. package/src/templates/headless/.agents/skills/secrets/SKILL.md +6 -2
  108. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +6 -2
@@ -982,36 +982,31 @@ async function _doEnsureTable() {
982
982
  now,
983
983
  ],
984
984
  });
985
- try {
986
- const { getBuiltinAgentsForSeeding } = await import("../server/agent-discovery.js");
987
- const builtins = getBuiltinAgentsForSeeding();
988
- for (const agent of builtins) {
989
- const agentJson = JSON.stringify({
990
- id: agent.id,
991
- name: agent.name,
992
- description: agent.description,
993
- url: agent.url, // always prod
994
- color: agent.color,
995
- }, null, 2);
996
- assertResourcePayloadIsSqlSafe(agentJson, "application/json");
997
- const agentSize = Buffer.byteLength(agentJson, "utf8");
998
- await client.execute({
999
- sql: seedSql,
1000
- args: [
1001
- crypto.randomUUID(),
1002
- `remote-agents/${agent.id}.json`,
1003
- SHARED_OWNER,
1004
- agentJson,
1005
- "application/json",
1006
- agentSize,
1007
- now,
1008
- now,
1009
- ],
1010
- });
1011
- }
1012
- }
1013
- catch {
1014
- // Agent discovery not available — skip seeding
985
+ const { getBuiltinAgentsForSeeding } = await import("../server/agent-discovery.js");
986
+ const builtins = getBuiltinAgentsForSeeding();
987
+ for (const agent of builtins) {
988
+ const agentJson = JSON.stringify({
989
+ id: agent.id,
990
+ name: agent.name,
991
+ description: agent.description,
992
+ url: agent.url, // always prod
993
+ color: agent.color,
994
+ }, null, 2);
995
+ assertResourcePayloadIsSqlSafe(agentJson, "application/json");
996
+ const agentSize = Buffer.byteLength(agentJson, "utf8");
997
+ await client.execute({
998
+ sql: seedSql,
999
+ args: [
1000
+ crypto.randomUUID(),
1001
+ `remote-agents/${agent.id}.json`,
1002
+ SHARED_OWNER,
1003
+ agentJson,
1004
+ "application/json",
1005
+ agentSize,
1006
+ now,
1007
+ now,
1008
+ ],
1009
+ });
1015
1010
  }
1016
1011
  try {
1017
1012
  const legacy = await client.execute({
@@ -6,7 +6,7 @@ import { HIDDEN_FIRST_PARTY_AGENT_IDS, isBuiltinAgentCatalogId, normalizeAgentId
6
6
  import { DEFAULT_WORKSPACE_APP_AUDIENCE, normalizeWorkspaceAppAudience, normalizeWorkspaceAppHomePath, normalizeWorkspaceAppPathList, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromPackageJson, } from "../shared/workspace-app-audience.js";
7
7
  import { inferWorkspaceAppRootHomePath, readConfiguredWorkspaceAppHomePath, } from "../workspace-app-config.js";
8
8
  import { resolveAppRuntimeUrl } from "./app-url.js";
9
- import { readBuiltinAgentsConfig } from "./builtin-agents.js";
9
+ import { readBuiltinAgentsConfig, } from "./builtin-agents.js";
10
10
  import { getRequestContext, getRequestOrgId, getRequestUserEmail, } from "./request-context.js";
11
11
  import { findWorkspaceRoot, readJson } from "./workspace-root.js";
12
12
  export { isBuiltinAgentCatalogId, normalizeAgentId };
@@ -199,23 +199,57 @@ export function shouldIncludeRemoteAgentManifest(manifest, selfAppId) {
199
199
  }
200
200
  return !HIDDEN_FIRST_PARTY_AGENT_IDS.has(normalizedId);
201
201
  }
202
- function builtinAgentsFor(ids, selfAppId, options) {
202
+ function builtinAgentsFor(config, selfAppId, options) {
203
203
  const normalizedSelfAppId = selfAppId ? normalizeAgentId(selfAppId) : "";
204
- return BUILTIN_AGENT_CATALOG.filter((app) => ids.includes(app.id) && app.id !== normalizedSelfAppId && app.url).map((app) => ({
204
+ const useLocalUrls = options?.preferLocalUrls === true || shouldUseLocalAgentUrls();
205
+ return BUILTIN_AGENT_CATALOG.filter((app) => config.include.includes(app.id) &&
206
+ app.id !== normalizedSelfAppId &&
207
+ app.url).map((app) => ({
205
208
  id: app.id,
206
209
  name: app.name,
207
210
  description: app.description,
208
- url: resolveAgentUrl(app, options?.preferLocalUrls),
211
+ url: !useLocalUrls && config.urls?.[app.id]
212
+ ? config.urls[app.id]
213
+ : resolveAgentUrl(app, options?.preferLocalUrls),
209
214
  color: app.color,
210
215
  }));
211
216
  }
212
217
  /** Built-ins the workspace builder offers (`agent-native.builtinAgents`). */
213
218
  export function getBuiltinAgents(selfAppId, options) {
214
- return builtinAgentsFor(readBuiltinAgentsConfig().include, selfAppId, options);
219
+ return builtinAgentsFor(readBuiltinAgentsConfig(), selfAppId, options);
215
220
  }
216
221
  function isUnofferedBuiltinManifest(manifestId, offeredBuiltinIds) {
217
222
  return (isBuiltinAgentCatalogId(manifestId) && !offeredBuiltinIds.has(manifestId));
218
223
  }
224
+ function resolveRemoteAgentManifestUrl(manifestId, rawManifestId, manifestUrl, builtin, options) {
225
+ const catalogBuiltin = BUILTIN_AGENT_CATALOG.find((candidate) => candidate.id === manifestId);
226
+ const hosted = isHostedRuntime();
227
+ let url = manifestUrl;
228
+ if (hosted && isLoopbackUrl(url)) {
229
+ if (builtin?.url)
230
+ url = builtin.url;
231
+ else
232
+ return undefined;
233
+ }
234
+ if (catalogBuiltin && builtin && manifestUrl === catalogBuiltin.url) {
235
+ url = builtin.url;
236
+ }
237
+ if (options?.preferLocalUrls && builtin && catalogBuiltin) {
238
+ url = builtin.url;
239
+ }
240
+ const isLegacyAssetsManifest = rawManifestId.trim().toLowerCase() !== manifestId;
241
+ if (isLegacyAssetsManifest && builtin?.url) {
242
+ try {
243
+ if (new URL(url).hostname === "images.agent-native.com") {
244
+ url = builtin.url;
245
+ }
246
+ }
247
+ catch {
248
+ url = builtin.url;
249
+ }
250
+ }
251
+ return { url, isLegacyAssetsManifest };
252
+ }
219
253
  export async function discoverAgents(selfAppId, options) {
220
254
  const builtins = getBuiltinAgents(selfAppId, options);
221
255
  const offeredBuiltinIds = new Set(builtins.map((agent) => agent.id));
@@ -281,32 +315,11 @@ export async function discoverAgents(selfAppId, options) {
281
315
  const manifestId = normalizeAgentId(manifest.id);
282
316
  if (isUnofferedBuiltinManifest(manifestId, offeredBuiltinIds))
283
317
  continue;
284
- let url = manifest.url;
285
- const isHosted = isHostedRuntime();
286
- if (isHosted && typeof url === "string" && isLoopbackUrl(url)) {
287
- const builtin = agentsById.get(manifestId);
288
- if (builtin?.url)
289
- url = builtin.url;
290
- else
291
- continue;
292
- }
293
318
  const builtin = agentsById.get(manifestId);
294
- if (options?.preferLocalUrls && builtin) {
295
- const isBuiltinAgent = BUILTIN_AGENT_CATALOG.some((candidate) => candidate.id === manifestId);
296
- if (isBuiltinAgent)
297
- url = builtin.url;
298
- }
299
- const isLegacyAssetsManifest = manifest.id.trim().toLowerCase() !== manifestId;
300
- if (isLegacyAssetsManifest && builtin?.url) {
301
- try {
302
- if (new URL(url).hostname === "images.agent-native.com") {
303
- url = builtin.url;
304
- }
305
- }
306
- catch {
307
- url = builtin.url;
308
- }
309
- }
319
+ const resolved = resolveRemoteAgentManifestUrl(manifestId, manifest.id, manifest.url, builtin, options);
320
+ if (!resolved)
321
+ continue;
322
+ const { url, isLegacyAssetsManifest } = resolved;
310
323
  agentsById.set(manifestId, {
311
324
  id: manifestId,
312
325
  name: isLegacyAssetsManifest && builtin?.name
@@ -414,30 +427,11 @@ async function overlayRemoteAgentResources(agentsById, offeredBuiltinIds, resour
414
427
  const manifestId = normalizeAgentId(manifest.id);
415
428
  if (isUnofferedBuiltinManifest(manifestId, offeredBuiltinIds))
416
429
  continue;
417
- let url = manifest.url;
418
430
  const builtin = agentsById.get(manifestId);
419
- if (isHostedRuntime() && isLoopbackUrl(url)) {
420
- if (builtin?.url)
421
- url = builtin.url;
422
- else
423
- continue;
424
- }
425
- if (options?.preferLocalUrls &&
426
- builtin &&
427
- BUILTIN_AGENT_CATALOG.some((candidate) => candidate.id === manifestId)) {
428
- url = builtin.url;
429
- }
430
- const isLegacyAssetsManifest = manifest.id.trim().toLowerCase() !== manifestId;
431
- if (isLegacyAssetsManifest && builtin?.url) {
432
- try {
433
- if (new URL(url).hostname === "images.agent-native.com") {
434
- url = builtin.url;
435
- }
436
- }
437
- catch {
438
- url = builtin.url;
439
- }
440
- }
431
+ const resolved = resolveRemoteAgentManifestUrl(manifestId, manifest.id, manifest.url, builtin, options);
432
+ if (!resolved)
433
+ continue;
434
+ const { url, isLegacyAssetsManifest } = resolved;
441
435
  agentsById.set(manifestId, {
442
436
  id: manifestId,
443
437
  name: isLegacyAssetsManifest && builtin?.name ? builtin.name : manifest.name,
@@ -66,6 +66,7 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
66
66
  error: any;
67
67
  code: any;
68
68
  } | {
69
+ error?: undefined;
69
70
  errorCode?: undefined;
70
71
  code?: undefined;
71
72
  ok: boolean;
@@ -73,6 +74,5 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
73
74
  baseUrlKey?: string;
74
75
  scope: AgentEngineApiKeyScope;
75
76
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
76
- error?: undefined;
77
77
  }>>;
78
78
  export {};
@@ -1,9 +1,9 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
2
  error: string;
3
- models?: undefined;
4
3
  ok?: undefined;
4
+ models?: undefined;
5
5
  } | {
6
+ error?: undefined;
6
7
  ok: boolean;
7
8
  models: string[];
8
- error?: undefined;
9
9
  }>>;
@@ -3,6 +3,8 @@ export interface BuiltinAgentsConfig {
3
3
  mode: BuiltinAgentsMode;
4
4
  /** Built-ins this workspace offers. Anything else is unknown to it. */
5
5
  include: string[];
6
+ /** Hosted URL overrides, applied only when a built-in is offered. */
7
+ urls?: Record<string, string>;
6
8
  }
7
9
  export declare const BUILTIN_AGENTS_ENV_KEY = "AGENT_NATIVE_BUILTIN_AGENTS_JSON";
8
10
  export declare function frameworkDefaultBuiltinAgentsConfig(): BuiltinAgentsConfig;
@@ -32,6 +32,48 @@ function parseIdList(value, field, warnings) {
32
32
  }
33
33
  return ids;
34
34
  }
35
+ function parseBuiltinAgentUrls(value) {
36
+ if (value === undefined)
37
+ return undefined;
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
39
+ throw new Error("builtinAgents.urls must be an object mapping built-in ids to URLs");
40
+ }
41
+ const urls = {};
42
+ const seenIds = new Set();
43
+ for (const [rawId, rawUrl] of Object.entries(value)) {
44
+ const id = normalizeAgentId(rawId);
45
+ if (!BUILTIN_AGENT_CATALOG_IDS.includes(id)) {
46
+ throw new Error("builtinAgents.urls contains an unknown built-in agent id");
47
+ }
48
+ if (seenIds.has(id)) {
49
+ throw new Error("builtinAgents.urls contains duplicate built-in agent ids");
50
+ }
51
+ seenIds.add(id);
52
+ if (typeof rawUrl !== "string" || rawUrl.trim() !== rawUrl) {
53
+ throw new Error("builtinAgents.urls values must be absolute HTTPS URLs without credentials, query, or fragment");
54
+ }
55
+ let url;
56
+ try {
57
+ url = new URL(rawUrl);
58
+ }
59
+ catch {
60
+ throw new Error("builtinAgents.urls values must be absolute HTTPS URLs without credentials, query, or fragment");
61
+ }
62
+ if (/[\s\p{Cc}]/u.test(rawUrl) ||
63
+ !/^https:\/\//i.test(rawUrl) ||
64
+ url.protocol !== "https:" ||
65
+ !url.hostname ||
66
+ url.username ||
67
+ url.password ||
68
+ url.search ||
69
+ url.hash ||
70
+ /[?#]/.test(rawUrl)) {
71
+ throw new Error("builtinAgents.urls values must be absolute HTTPS URLs without credentials, query, or fragment");
72
+ }
73
+ urls[id] = rawUrl;
74
+ }
75
+ return urls;
76
+ }
35
77
  export function parseBuiltinAgentsConfig(raw) {
36
78
  const warnings = [];
37
79
  if (raw === undefined || raw === null) {
@@ -42,13 +84,18 @@ export function parseBuiltinAgentsConfig(raw) {
42
84
  return { config: frameworkDefaultBuiltinAgentsConfig(), warnings };
43
85
  }
44
86
  const record = raw;
87
+ const urls = parseBuiltinAgentUrls(record.urls);
88
+ const urlsConfig = urls === undefined ? {} : { urls };
45
89
  const mode = record.mode ?? "all";
46
90
  if (mode !== "all" && mode !== "none" && mode !== "selected") {
47
91
  warnings.push(`builtinAgents.mode must be "all", "none", or "selected"; using the default`);
48
- return { config: frameworkDefaultBuiltinAgentsConfig(), warnings };
92
+ return {
93
+ config: { ...frameworkDefaultBuiltinAgentsConfig(), ...urlsConfig },
94
+ warnings,
95
+ };
49
96
  }
50
97
  if (mode === "none") {
51
- return { config: { mode, include: [] }, warnings };
98
+ return { config: { mode, include: [], ...urlsConfig }, warnings };
52
99
  }
53
100
  let include;
54
101
  if (mode === "all") {
@@ -64,7 +111,7 @@ export function parseBuiltinAgentsConfig(raw) {
64
111
  include =
65
112
  parseIdList(record.include, "builtinAgents.include", warnings) ?? [];
66
113
  }
67
- return { config: { mode, include }, warnings };
114
+ return { config: { mode, include, ...urlsConfig }, warnings };
68
115
  }
69
116
  function readRawBuiltinAgentsConfig() {
70
117
  const envJson = getAppConfig().workspace.builtinAgentsJson;
@@ -38,18 +38,15 @@ export declare function assertCredentialStoreReadable(result: {
38
38
  cause?: unknown;
39
39
  }): void;
40
40
  /**
41
- * Deployment-level credential fallback for single-tenant/local operation.
42
- * Multi-tenant call sites must gate this explicitly before calling.
41
+ * Read deployment-owned configuration. LLM provider keys have an app-wide
42
+ * fallback policy; user and workspace credentials must use scoped resolvers.
43
43
  */
44
44
  export declare function readDeployCredentialEnv(key: string): string | undefined;
45
45
  /**
46
- * Deployment-level credentials are safe as a runtime fallback only in local /
47
- * single-tenant contexts. In hosted production with a shared database, every
48
- * signed-in user needs their own user/org/workspace credential for provider
49
- * keys. Model-provider env keys are never shared with hosted users because
50
- * they bill the app owner. Other app-provided service credentials configure
51
- * the deployed app itself, such as email transport and OAuth client
52
- * credentials whose per-user identity remains in scoped OAuth tokens.
46
+ * Legacy deployment-level credential policy for non-provider call sites.
47
+ * Model-provider env keys use `canUseDeployCredentialFallbackForRequest()` so
48
+ * self-hosted apps and local PGlite development can share an app-owned
49
+ * inference key.
53
50
  *
54
51
  * @deprecated Use `canUseDeployCredentialFallbackForRequest()` for generic
55
52
  * provider secrets. This stricter helper remains for legacy call sites with
@@ -96,23 +96,27 @@ export function assertCredentialStoreReadable(result) {
96
96
  }
97
97
  }
98
98
  /**
99
- * Deployment-level credential fallback for single-tenant/local operation.
100
- * Multi-tenant call sites must gate this explicitly before calling.
99
+ * Read deployment-owned configuration. LLM provider keys have an app-wide
100
+ * fallback policy; user and workspace credentials must use scoped resolvers.
101
101
  */
102
102
  export function readDeployCredentialEnv(key) {
103
- if (HOSTED_MODEL_PROVIDER_ENV_KEYS.has(key) &&
103
+ if (key === "GOOGLE_APPLICATION_CREDENTIALS" &&
104
+ (getRequestContext()?.isSyntheticTraffic === true ||
105
+ isHostedWorkspaceRuntime() ||
106
+ !isDeployCredentialFallbackAllowed())) {
107
+ return undefined;
108
+ }
109
+ if ((DEPLOY_LLM_PROVIDER_ENV_KEYS.has(key) ||
110
+ BUILDER_CREDITS_DEPLOY_ENV_KEYS.has(key)) &&
104
111
  !canUseDeployCredentialFallbackForRequest(key)) {
105
112
  return undefined;
106
113
  }
107
114
  return process.env[key] || undefined;
108
115
  }
109
- const HOSTED_MODEL_PROVIDER_ENV_KEYS = new Set([
116
+ const DEPLOY_LLM_PROVIDER_ENV_KEYS = new Set([
110
117
  "ANTHROPIC_API_KEY",
111
- "BUILDER_GATEWAY_SPACE_ID",
112
- "BUILDER_GATEWAY_TOKEN",
113
118
  "COHERE_API_KEY",
114
119
  "GEMINI_API_KEY",
115
- "GOOGLE_APPLICATION_CREDENTIALS",
116
120
  "GOOGLE_GENERATIVE_AI_API_KEY",
117
121
  "GROQ_API_KEY",
118
122
  "JEV_API_KEY",
@@ -122,6 +126,10 @@ const HOSTED_MODEL_PROVIDER_ENV_KEYS = new Set([
122
126
  "TYPESAFE_API_KEY",
123
127
  "VOYAGE_API_KEY",
124
128
  ]);
129
+ const BUILDER_CREDITS_DEPLOY_ENV_KEYS = new Set([
130
+ "BUILDER_GATEWAY_SPACE_ID",
131
+ "BUILDER_GATEWAY_TOKEN",
132
+ ]);
125
133
  const APP_PROVIDED_DEPLOY_CREDENTIAL_KEYS = new Set([
126
134
  "EMAIL_FROM",
127
135
  "EMAIL_INBOUND_WEBHOOK_SECRET",
@@ -142,13 +150,10 @@ function isAppProvidedDeployCredentialKey(key) {
142
150
  return !!key && APP_PROVIDED_DEPLOY_CREDENTIAL_KEYS.has(key);
143
151
  }
144
152
  /**
145
- * Deployment-level credentials are safe as a runtime fallback only in local /
146
- * single-tenant contexts. In hosted production with a shared database, every
147
- * signed-in user needs their own user/org/workspace credential for provider
148
- * keys. Model-provider env keys are never shared with hosted users because
149
- * they bill the app owner. Other app-provided service credentials configure
150
- * the deployed app itself, such as email transport and OAuth client
151
- * credentials whose per-user identity remains in scoped OAuth tokens.
153
+ * Legacy deployment-level credential policy for non-provider call sites.
154
+ * Model-provider env keys use `canUseDeployCredentialFallbackForRequest()` so
155
+ * self-hosted apps and local PGlite development can share an app-owned
156
+ * inference key.
152
157
  *
153
158
  * @deprecated Use `canUseDeployCredentialFallbackForRequest()` for generic
154
159
  * provider secrets. This stricter helper remains for legacy call sites with
@@ -160,16 +165,23 @@ export function isDeployCredentialFallbackAllowed() {
160
165
  return isLocalDatabase();
161
166
  }
162
167
  export function canUseDeployCredentialFallbackForRequest(key) {
168
+ const requestContext = getRequestContext();
163
169
  // Synthetic checks must never fall through to a deploy-wide provider key.
164
- // If the dedicated test credential is rejected, using the site's shared key
165
- // would make a green retry both misleading and billable to real traffic.
166
- if (getRequestContext()?.isSyntheticTraffic === true)
170
+ // Anonymous public runs must not consume the deployment owner's key either.
171
+ if (requestContext?.isSyntheticTraffic === true ||
172
+ requestContext?.agentRunAnonymous === true) {
167
173
  return false;
168
- if (key && HOSTED_MODEL_PROVIDER_ENV_KEYS.has(key)) {
169
- if (isHostedWorkspaceRuntime())
170
- return false;
171
- if (isProductionLikeRuntime() && !isLocalDatabase())
174
+ }
175
+ if (key && BUILDER_CREDITS_DEPLOY_ENV_KEYS.has(key)) {
176
+ return isTrustedSelfHostedRuntime();
177
+ }
178
+ if (key && DEPLOY_LLM_PROVIDER_ENV_KEYS.has(key)) {
179
+ const hostedWorkspace = isHostedWorkspaceRuntime();
180
+ if (hostedWorkspace && isProductionLikeRuntime())
172
181
  return false;
182
+ if (isLocalDatabase())
183
+ return true;
184
+ return !hostedWorkspace;
173
185
  }
174
186
  const email = getRequestUserEmail();
175
187
  if (!email)
@@ -1,7 +1,7 @@
1
1
  ===================================================================
2
2
  --- a/.agents/skills/adding-a-feature/SKILL.md
3
3
  +++ b/.agents/skills/adding-a-feature/SKILL.md
4
- @@ -129,11 +129,12 @@
4
+ @@ -131,11 +131,12 @@
5
5
  Instruction examples may name secret keys like `SLACK_WEBHOOK`, but must use
6
6
  placeholders such as `${keys.SLACK_WEBHOOK}` or `<SLACK_WEBHOOK>`. Do not paste
7
7
  real keys, internal data, or customer data into instructions as examples.
@@ -5,7 +5,7 @@
5
5
  -# Chat — Agent Guide
6
6
  +# App — Agent Guide
7
7
 
8
- -Chat is the minimal chat-first agent-native app. `/` redirects to shared sign-in; authenticated chat starts at `/home`. Actions carry capabilities; screens exist only for durable workflows.
8
+ -Chat is the minimal chat-first agent-native app. `/` redirects to shared sign-in, then `/home`, the editable Get started page. Chat lives at `/chat`; new chats start there. Actions carry capabilities; screens exist only for durable workflows.
9
9
  +This starter ships as a blank Agent-Native app canvas — that describes its
10
10
  +initial state, not necessarily its current one. See `DEVELOPING.md` before
11
11
  +making any source code change.
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## Application state
32
32
 
33
- -- `navigation` describes the view and selected ids. Chat is `chat` at `/home`; `/` opens shared sign-in/signup.
33
+ -- `navigation` describes the view and selected ids. Chat is `chat` at `/chat` and `/chat/<threadId>`; the Get started page is `home` at `/home`; `/` opens shared sign-in/signup.
34
34
  +- `navigation` describes the view and selected ids. The default home view is
35
35
  + `home` at `/` (blank app canvas). No agent rail or chat is mounted by
36
36
  + default; add one only when the user asks.
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## Building a domain app
40
40
 
41
- -Choose the primary workflow and route before editing. Add a domain route under `app/routes/` and set `app.homePath` in `server/plugins/agent-native-email-branding.ts` with `defineAppConfig`. Keep `/home` as Chat. Add a static link in `app/components/layout/Sidebar.tsx` before `ChatThreadsSection`; `Header.tsx` maps `/home` to Chat and uses `APP_TITLE` elsewhere. Shared sidebar and AgentInspector remain.
41
+ -Choose the primary workflow and route before editing. Add a domain route under `app/routes/` and set `app.homePath` in `server/plugins/agent-native-email-branding.ts` with `defineAppConfig`. The default Get started page lives at `/home`, and Chat stays at `/chat`; edit `app/routes/home.tsx` when replacing that page with a domain-specific home. Add a static link in `app/components/layout/Sidebar.tsx` before `ChatThreadsSection`; `Header.tsx` uses `APP_TITLE` elsewhere. Shared sidebar and AgentInspector remain.
42
42
  +Choose the primary workflow and route before editing. Build on the blank canvas in `app/routes/_index.tsx` or add domain routes under `app/routes/`. This starter has no Chat route, sidebar, header, or agent rail; add only the navigation the product needs. Set `app.homePath` in the existing `defineAppConfig` in `server/plugins/config.ts` as `DEVELOPING.md` describes.
43
43
 
44
44
  -Use `adding-a-feature` for functional parity and `frontend-design` for a user-facing screen. Keep feature guidance here; do not rewrite `README.md` or `DESIGN.md`. After all edits, run one typecheck, one doctor check, and one browser smoke of the primary workflow, including overlap rejection, cancellation freeing the slot, and authenticated landing. Skip production build and extra test suites.
@@ -1,7 +1,7 @@
1
1
  ===================================================================
2
2
  --- a/app/components/layout/Layout.tsx
3
3
  +++ b/app/components/layout/Layout.tsx
4
- @@ -1,189 +1,28 @@
4
+ @@ -1,188 +1,28 @@
5
5
  -import { useT } from "@agent-native/core/client/i18n";
6
6
  -import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell/header-actions";
7
7
  -import {
@@ -23,6 +23,7 @@
23
23
  - SheetTitle,
24
24
  -} from "@/components/ui/sheet";
25
25
  -import { APP_TITLE } from "@/lib/app-config";
26
+ -import { HOME_PATH, isChatPathname } from "@/lib/chat-paths";
26
27
  -
27
28
  -import { Sidebar } from "./Sidebar";
28
29
  -
@@ -43,8 +44,8 @@
43
44
  -
44
45
  -function routeOwnsToolbar(pathname: string): boolean {
45
46
  - return (
46
- - pathname === "/home" ||
47
- - pathname.startsWith("/chat/") ||
47
+ - pathname === HOME_PATH ||
48
+ - isChatPathname(pathname) ||
48
49
  - pathname === "/database" ||
49
50
  - pathname.startsWith("/extensions")
50
51
  - );
@@ -66,8 +67,7 @@
66
67
  - const t = useT();
67
68
  - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
68
69
  - const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
69
- - const isChatRoute =
70
- - location.pathname === "/home" || location.pathname.startsWith("/chat/");
70
+ - const isChatRoute = isChatPathname(location.pathname);
71
71
  - // Settings brings its own navigation, header, and agent toggle, so it
72
72
  - // renders full width.
73
73
  - const settingsOwnsChrome = isSettingsPathname(location.pathname);
@@ -79,8 +79,7 @@
79
79
  - const chatHomeHandoffPending = isAgentChatHomeHandoffActive("chat");
80
80
  - useAgentChatHomeHandoffLinks({
81
81
  - storageKey: "chat",
82
- - isChatPath: (pathname) =>
83
- - pathname === "/home" || pathname.startsWith("/chat/"),
82
+ - isChatPath: isChatPathname,
84
83
  - requireActiveHandoff: true,
85
84
  - });
86
85
  -
@@ -1,7 +1,18 @@
1
1
  ===================================================================
2
2
  --- a/app/hooks/use-navigation-state.ts
3
3
  +++ b/app/hooks/use-navigation-state.ts
4
- @@ -5,83 +5,37 @@
4
+ @@ -1,85 +1,41 @@
5
+ import { appBasePath, appPath } from "@agent-native/core/client/api-path";
6
+ import { useAgentRouteState } from "@agent-native/core/client/navigation";
7
+
8
+ -import {
9
+ - chatThreadPath,
10
+ - HOME_PATH,
11
+ - isChatPathname,
12
+ - NEW_CHAT_PATH,
13
+ - threadIdFromPath,
14
+ -} from "@/lib/chat-paths";
15
+ import { TAB_ID } from "@/lib/tab-id";
5
16
 
6
17
  export interface NavigationState {
7
18
  view: string;
@@ -31,19 +42,9 @@
31
42
  });
32
43
  }
33
44
 
34
- -function threadIdFromPath(pathname: string): string | null {
35
- - const match = pathname.match(/^\/chat\/([^/]+)/);
36
- - if (!match) return null;
37
- - try {
38
- - const value = decodeURIComponent(match[1]).trim();
39
- - return value || null;
40
- - } catch {
41
- - return null;
42
- - }
43
- -}
44
- -
45
45
  function viewForPath(pathname: string): string {
46
- - if (isChatPath(pathname)) return "chat";
46
+ - if (isChatPathname(pathname)) return "chat";
47
+ - if (pathname === HOME_PATH) return "home";
47
48
  - if (pathname.startsWith("/database")) return "database";
48
49
  - if (pathname.startsWith("/extensions")) return "extensions";
49
50
  - if (pathname.startsWith("/observability")) return "observability";
@@ -60,9 +61,10 @@
60
61
  function pathForView(view?: string): string {
61
62
  switch (view) {
62
63
  - case "chat":
63
- case "home":
64
64
  - case "ask":
65
- - return "/home";
65
+ - return NEW_CHAT_PATH;
66
+ case "home":
67
+ - return HOME_PATH;
66
68
  - case "database":
67
69
  - return "/database";
68
70
  - case "extensions":
@@ -77,29 +79,20 @@
77
79
  - return "/settings/organization";
78
80
  + return "/";
79
81
  default:
80
- - return "/home";
82
+ - return HOME_PATH;
81
83
  + return "/";
82
84
  }
83
85
  }
84
86
 
85
- -function pathForCommand(command: any): string {
87
+ -export function pathForCommand(command: any): string {
86
88
  - const path = pathForView(command?.view);
87
- - if (path !== "/home") return path;
89
+ - if (command?.view !== "chat" && command?.view !== "ask") return path;
88
90
  - const threadId =
89
91
  - typeof command?.threadId === "string" ? command.threadId.trim() : "";
90
- - return threadId ? `/chat/${encodeURIComponent(threadId)}` : path;
92
+ - return threadId ? chatThreadPath(threadId) : path;
91
93
  -}
92
94
  -
93
95
  function routerPath(path: string): string {
94
96
  const basePath = appBasePath();
95
97
  if (!basePath) return path;
96
98
  if (path === basePath) return "/";
97
- @@ -89,8 +43,4 @@
98
- return path.slice(basePath.length) || "/";
99
- }
100
- return path;
101
- }
102
- -
103
- -function isChatPath(pathname: string): boolean {
104
- - return pathname === "/home" || pathname.startsWith("/chat/");
105
- -}