@agent-native/core 0.122.0 → 0.122.1

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 (143) hide show
  1. package/README.md +4 -12
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +9 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +57 -4
  6. package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
  7. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
  8. package/corpus/core/src/extensions/url-safety.ts +67 -4
  9. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  10. package/corpus/core/src/templates/default/app/root.tsx +1 -1
  11. package/corpus/core/src/templates/default/public/manifest.json +2 -2
  12. package/corpus/core/src/vite/client.ts +64 -3
  13. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
  14. package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
  15. package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  17. package/corpus/templates/analytics/package.json +1 -1
  18. package/corpus/templates/analytics/public/manifest.json +1 -1
  19. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  20. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
  21. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  22. package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  23. package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  24. package/corpus/templates/assets/server/plugins/auth.ts +1 -1
  25. package/corpus/templates/brain/app/lib/brain.ts +0 -7
  26. package/corpus/templates/brain/app/routes/_index.tsx +1 -2
  27. package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  28. package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  29. package/corpus/templates/brain/package.json +1 -1
  30. package/corpus/templates/brain/public/manifest.json +1 -1
  31. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  32. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
  33. package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
  34. package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  35. package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  36. package/corpus/templates/calendar/package.json +1 -1
  37. package/corpus/templates/calendar/public/manifest.json +1 -1
  38. package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
  39. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  40. package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  41. package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
  42. package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
  43. package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
  44. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
  45. package/corpus/templates/clips/app/routes/_index.tsx +1 -1
  46. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
  47. package/corpus/templates/clips/app/routes/record.tsx +5 -0
  48. package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  50. package/corpus/templates/clips/server/db/schema.ts +2 -0
  51. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
  52. package/corpus/templates/clips/server/plugins/auth.ts +1 -1
  53. package/corpus/templates/clips/server/plugins/db.ts +8 -0
  54. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
  55. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
  56. package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
  57. package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  59. package/corpus/templates/content/package.json +1 -1
  60. package/corpus/templates/content/public/manifest.json +1 -1
  61. package/corpus/templates/content/server/plugins/auth.ts +1 -1
  62. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  63. package/corpus/templates/design/app/routes/_index.tsx +1 -1
  64. package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  65. package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  66. package/corpus/templates/design/server/plugins/auth.ts +1 -1
  67. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
  68. package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
  69. package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  70. package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  71. package/corpus/templates/dispatch/package.json +1 -1
  72. package/corpus/templates/dispatch/public/manifest.json +1 -1
  73. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
  74. package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  75. package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  76. package/corpus/templates/forms/package.json +1 -1
  77. package/corpus/templates/forms/public/manifest.json +1 -1
  78. package/corpus/templates/forms/server/plugins/auth.ts +1 -1
  79. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
  80. package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  81. package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  82. package/corpus/templates/macros/package.json +1 -1
  83. package/corpus/templates/macros/server/plugins/auth.ts +2 -2
  84. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
  85. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
  86. package/corpus/templates/mail/app/routes/$view.tsx +1 -1
  87. package/corpus/templates/mail/app/routes/_index.tsx +1 -1
  88. package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  89. package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  90. package/corpus/templates/mail/package.json +1 -1
  91. package/corpus/templates/mail/public/manifest.json +1 -1
  92. package/corpus/templates/mail/server/plugins/auth.ts +1 -1
  93. package/corpus/templates/plan/agent-native.app-skill.json +1 -1
  94. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
  95. package/corpus/templates/plan/app/lib/app-config.ts +1 -1
  96. package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  97. package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  98. package/corpus/templates/plan/package.json +1 -1
  99. package/corpus/templates/plan/public/manifest.json +1 -1
  100. package/corpus/templates/plan/server/plugins/auth.ts +1 -1
  101. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  102. package/corpus/templates/slides/app/routes/_index.tsx +1 -1
  103. package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  104. package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  105. package/corpus/templates/slides/package.json +1 -1
  106. package/corpus/templates/slides/public/manifest.json +1 -1
  107. package/corpus/templates/slides/server/plugins/auth.ts +1 -1
  108. package/dist/a2a/client.d.ts.map +1 -1
  109. package/dist/a2a/client.js +58 -4
  110. package/dist/a2a/client.js.map +1 -1
  111. package/dist/client/settings/SettingsPanel.js +9 -9
  112. package/dist/client/settings/SettingsPanel.js.map +1 -1
  113. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  114. package/dist/client/settings/SettingsTabsPage.js +7 -4
  115. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  116. package/dist/collab/struct-routes.d.ts +1 -1
  117. package/dist/extensions/url-safety.d.ts +9 -16
  118. package/dist/extensions/url-safety.d.ts.map +1 -1
  119. package/dist/extensions/url-safety.js +48 -4
  120. package/dist/extensions/url-safety.js.map +1 -1
  121. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  122. package/dist/notifications/routes.d.ts +2 -2
  123. package/dist/observability/routes.d.ts +3 -3
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  126. package/dist/server/transcribe-voice.d.ts +1 -1
  127. package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  128. package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  129. package/dist/templates/default/app/root.tsx +1 -1
  130. package/dist/templates/default/public/manifest.json +2 -2
  131. package/dist/vite/client.d.ts.map +1 -1
  132. package/dist/vite/client.js +64 -1
  133. package/dist/vite/client.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/a2a/client.ts +57 -4
  136. package/src/client/settings/SettingsPanel.tsx +9 -9
  137. package/src/client/settings/SettingsTabsPage.tsx +12 -2
  138. package/src/extensions/url-safety.ts +67 -4
  139. package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  140. package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  141. package/src/templates/default/app/root.tsx +1 -1
  142. package/src/templates/default/public/manifest.json +2 -2
  143. package/src/vite/client.ts +64 -3
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Agent-Native
2
2
 
3
+
3
4
  ## The framework for agentic apps
4
5
 
5
- Agent-Native is an open-source framework for rapidly building robust applications with agents at their core.
6
+ Don't pick between apps or agents. Agent-Native apps are both.
6
7
 
7
8
  ```ts
8
9
  // One action powers every app surface: UI, agent, HTTP, MCP, A2A, and CLI.
@@ -22,16 +23,7 @@ export default defineAction({
22
23
  - **[Backend agnostic](https://agent-native.com/docs/database)**: Plug in any Drizzle-supported SQL database and Nitro-compatible host.
23
24
  - **[Toolkits](https://agent-native.com/docs/agent-native-toolkit)**: Reusable building blocks for collaboration, sharing, settings, teams, and observability.
24
25
 
25
- ## Don't pick between apps or agents.
26
-
27
- Agent-native apps are both
28
-
29
- | | SaaS Tools | Raw AI Agents | Internal Tools | Agent-Native App |
30
- | ----------------- | ------------------ | ----------------------- | -------------------------- | ----------------------- |
31
- | **UI** | Polished but rigid | None | Mixed quality | Full UI, fork & go |
32
- | **AI** | Bolted on | Powerful | Shallowly connected | Agent-first, integrated |
33
- | **Customization** | Can't | Instructions and skills | Full, but high maintenance | Agent modifies the app |
34
- | **Ownership** | Rented | Somewhat yours | You own the code | You own the code |
26
+ https://github.com/user-attachments/assets/ef51644b-6506-46d8-8083-0af7b7e5b65c
35
27
 
36
28
  ## Try an Agent-Native app
37
29
 
@@ -123,7 +115,7 @@ pnpm install
123
115
  pnpm dev
124
116
  ```
125
117
 
126
- Prefer flags? `create my-app --template mail`, `--headless`, or `--standalone` skip the prompt.
118
+ Prefer flags? `create my-app --template chat`, `--headless`, or `--standalone` skip the prompt.
127
119
 
128
120
  See the full [getting started docs](https://agent-native.com/docs).
129
121
 
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1671
32
32
  - toolkit files: 160
33
- - template files: 6433
33
+ - template files: 6461
@@ -1,5 +1,14 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.122.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c8a0bcf: Sign the A2A identity token's audience as the target's origin. Receivers derive their expected audience from `APP_URL`, which carries no app path, so a path-qualified audience could never match — every direct `actions/invoke` between workspace apps failed with "Invalid or expired A2A token".
8
+ - c8a0bcf: Allow agent-to-agent calls between apps in a locally running workspace. The gateway serves every app on loopback, so sibling A2A targets are private addresses by construction and `ssrfSafeFetch` rejected them as SSRF — making cross-app delegation fail locally with "refusing to fetch private/internal address". `ssrfSafeFetch` now accepts an `allowedPrivateOrigins` list and the A2A client passes the deployment's own gateway origin (never a request-supplied value).
9
+ - c8a0bcf: Bound Nitro dev-server startup recovery so a failed restart stops refreshing forever.
10
+ - c8a0bcf: Keep Manage agent as a dedicated linked destination at the bottom of settings navigation.
11
+
3
12
  ## 0.122.0
4
13
 
5
14
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.122.0",
3
+ "version": "0.122.1",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,6 +1,50 @@
1
1
  import * as jose from "jose";
2
2
 
3
3
  import { ssrfSafeFetch } from "../extensions/url-safety.js";
4
+
5
+ /**
6
+ * A workspace serves every app from one gateway on loopback, so sibling A2A
7
+ * targets are private addresses by construction and the SSRF guard cannot tell
8
+ * them apart from an attack. Trust only origins this deployment configured for
9
+ * itself — never a value that arrived on a request.
10
+ */
11
+ function workspacePrivateOrigins(): string[] {
12
+ const origins = [
13
+ process.env.WORKSPACE_GATEWAY_URL,
14
+ process.env.APP_URL,
15
+ process.env.BETTER_AUTH_URL,
16
+ // Escape hatch for contexts that never receive the gateway manifest (the
17
+ // action CLI, one-off scripts). Comma-separated origins.
18
+ ...(process.env.AGENT_NATIVE_A2A_ALLOWED_ORIGINS ?? "").split(","),
19
+ ]
20
+ .map((value) => value?.trim())
21
+ .filter((value): value is string => !!value);
22
+
23
+ // The gateway also hands each child the sibling manifest, and siblings are
24
+ // reached on their own loopback ports rather than through the gateway.
25
+ const raw = process.env.AGENT_NATIVE_WORKSPACE_APPS_JSON;
26
+ if (raw) {
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ const apps = Array.isArray(parsed?.apps)
30
+ ? parsed.apps
31
+ : Array.isArray(parsed)
32
+ ? parsed
33
+ : [];
34
+ for (const app of apps) {
35
+ const url = app?.url ?? app?.origin ?? app?.baseUrl;
36
+ if (typeof url === "string" && url) origins.push(url);
37
+ const port = app?.port;
38
+ if (typeof port === "number" && Number.isFinite(port)) {
39
+ origins.push(`http://127.0.0.1:${port}`);
40
+ }
41
+ }
42
+ } catch {
43
+ // A malformed manifest must not disable the SSRF guard.
44
+ }
45
+ }
46
+ return origins;
47
+ }
4
48
  import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
5
49
  import type {
6
50
  A2AApprovedAction,
@@ -138,7 +182,7 @@ export class A2AClient {
138
182
  const res = await ssrfSafeFetch(
139
183
  endpoint,
140
184
  { method: "OPTIONS" },
141
- { maxRedirects: 3 },
185
+ { maxRedirects: 3, allowedPrivateOrigins: workspacePrivateOrigins() },
142
186
  );
143
187
  if (res.status !== 404 && res.status !== 405) {
144
188
  this.endpointCandidates = [endpoint];
@@ -237,7 +281,7 @@ export class A2AClient {
237
281
  const res = await ssrfSafeFetch(
238
282
  `${this.baseUrl}/.well-known/agent-card.json`,
239
283
  {},
240
- { maxRedirects: 3 },
284
+ { maxRedirects: 3, allowedPrivateOrigins: workspacePrivateOrigins() },
241
285
  );
242
286
  if (!res.ok) {
243
287
  throw new Error(`Failed to fetch agent card (${res.status})`);
@@ -548,7 +592,7 @@ export class A2AClient {
548
592
  body: JSON.stringify(body),
549
593
  signal: controller?.signal,
550
594
  },
551
- { maxRedirects: 3 },
595
+ { maxRedirects: 3, allowedPrivateOrigins: workspacePrivateOrigins() },
552
596
  );
553
597
  } finally {
554
598
  if (timer) clearTimeout(timer);
@@ -883,7 +927,16 @@ async function buildA2AApiKeyAttempts(
883
927
 
884
928
  function normalizeA2AAudience(url: string): string {
885
929
  const explicit = splitExplicitA2AEndpoint(url.replace(/\/$/, ""));
886
- return (explicit?.baseUrl ?? url).replace(/\/$/, "");
930
+ const base = (explicit?.baseUrl ?? url).replace(/\/$/, "");
931
+ // Receivers derive their expected audience from APP_URL, which carries no
932
+ // app path. In a workspace every app is mounted under one gateway origin, so
933
+ // signing the path-qualified URL yields an audience the receiver can never
934
+ // match. The origin is the identifier both sides agree on.
935
+ try {
936
+ return new URL(base).origin;
937
+ } catch {
938
+ return base;
939
+ }
887
940
  }
888
941
 
889
942
  function isA2AAuthRejection(err: unknown): boolean {
@@ -3001,18 +3001,10 @@ export function useAgentSettingsTabs(): SettingsTabItem[] {
3001
3001
  const organization = searchTab("organization");
3002
3002
  const workspace = searchTab("workspace");
3003
3003
  return [
3004
- {
3005
- ...agent,
3006
- icon: IconHierarchy2,
3007
- group: "agent",
3008
- href: "/agent#settings",
3009
- searchEntries: undefined,
3010
- content: <Navigate to="/agent#settings" replace />,
3011
- },
3012
3004
  {
3013
3005
  ...connections,
3014
3006
  icon: IconPlugConnected,
3015
- group: "agent",
3007
+ group: "workspace",
3016
3008
  content: <ConnectionsSettingsContent settingsPanelProps={baseProps} />,
3017
3009
  },
3018
3010
  {
@@ -3047,6 +3039,14 @@ export function useAgentSettingsTabs(): SettingsTabItem[] {
3047
3039
  keywords: "extensions widgets mini apps tools sandboxed apps",
3048
3040
  content: <ExtensionsSettingsContent />,
3049
3041
  },
3042
+ {
3043
+ ...agent,
3044
+ icon: IconHierarchy2,
3045
+ group: "manage-agent",
3046
+ href: "/agent#settings",
3047
+ searchEntries: undefined,
3048
+ content: <Navigate to="/agent#settings" replace />,
3049
+ },
3050
3050
  ];
3051
3051
  }, [baseProps]);
3052
3052
  }
@@ -1,5 +1,6 @@
1
1
  import { Tabs, useDesignSystem } from "@agent-native/toolkit/design-system";
2
2
  import {
3
+ IconArrowUpRight,
3
4
  IconHistory,
4
5
  IconSearch,
5
6
  IconSettings,
@@ -216,6 +217,8 @@ export function SettingsTabsPage({
216
217
  const hasOrganizationTab = extraTabs.some(
217
218
  (tab) => tab.id === "organization",
218
219
  );
220
+ const inlineTabs = extraTabs.filter((tab) => !tab.href);
221
+ const linkedTabs = extraTabs.filter((tab) => tab.href);
219
222
  const next: SettingsTabItem[] = [
220
223
  {
221
224
  id: "general",
@@ -234,7 +237,7 @@ export function SettingsTabsPage({
234
237
  keywords: "profile photo avatar identity signed in email name",
235
238
  });
236
239
  }
237
- next.push(...extraTabs);
240
+ next.push(...inlineTabs);
238
241
  if (team && !hasOrganizationTab) {
239
242
  next.push({
240
243
  id: "team",
@@ -249,10 +252,11 @@ export function SettingsTabsPage({
249
252
  id: "whats-new",
250
253
  label: whatsNewLabel,
251
254
  icon: IconHistory,
252
- group: "updates",
255
+ group: next.at(-1)?.group ?? "app",
253
256
  content: whatsNew,
254
257
  });
255
258
  }
259
+ next.push(...linkedTabs);
256
260
  return next;
257
261
  }, [
258
262
  account,
@@ -615,6 +619,12 @@ export function SettingsTabsPage({
615
619
  />
616
620
  ) : null}
617
621
  <span className="truncate">{tab.label}</span>
622
+ {tab.href ? (
623
+ <IconArrowUpRight
624
+ aria-hidden="true"
625
+ className="size-3.5 shrink-0 text-muted-foreground/80"
626
+ />
627
+ ) : null}
618
628
  </>
619
629
  );
620
630
  if (tab.href) {
@@ -163,7 +163,32 @@ export async function isBlockedExtensionUrlWithDns(
163
163
  * runtimes); the caller should fall back to the regular `fetch` path —
164
164
  * `isBlockedExtensionUrlWithDns` will still have caught most rebinding cases.
165
165
  */
166
- export async function createSsrfSafeDispatcher(): Promise<unknown | null> {
166
+ function normalizeLookupHostname(hostname: string): string {
167
+ return hostname.toLowerCase().replace(/^\[|\]$/g, "");
168
+ }
169
+
170
+ function normalizeAllowedPrivateOriginKeys(
171
+ origins: readonly string[],
172
+ ): Set<string> {
173
+ const keys = new Set<string>();
174
+ for (const origin of origins) {
175
+ try {
176
+ const parsed = new URL(origin);
177
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
178
+ continue;
179
+ }
180
+ const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
181
+ keys.add(`${normalizeLookupHostname(parsed.hostname)}:${port}`);
182
+ } catch {
183
+ // Ignore malformed deployment configuration and retain the private-IP guard.
184
+ }
185
+ }
186
+ return keys;
187
+ }
188
+
189
+ export async function createSsrfSafeDispatcher(
190
+ allowedPrivateOrigins: readonly string[] = [],
191
+ ): Promise<unknown | null> {
167
192
  // Keep the optional undici import opaque to Vite/Rolldown. A static
168
193
  // `import("undici")` makes browser builds try to resolve and bundle undici
169
194
  // even though this dispatcher is only useful in Node server runtimes.
@@ -182,6 +207,9 @@ export async function createSsrfSafeDispatcher(): Promise<unknown | null> {
182
207
 
183
208
  const { Agent } = undici;
184
209
  const { lookup } = dnsModule;
210
+ const allowedPrivateOriginKeys = normalizeAllowedPrivateOriginKeys(
211
+ allowedPrivateOrigins,
212
+ );
185
213
  if (!Agent || !lookup) return null;
186
214
 
187
215
  return new Agent({
@@ -209,7 +237,10 @@ export async function createSsrfSafeDispatcher(): Promise<unknown | null> {
209
237
  ? addresses
210
238
  : [{ address: addresses, family: 4 }];
211
239
  for (const record of list) {
212
- if (isPrivateHost(record.address)) {
240
+ const allowedOrigin = allowedPrivateOriginKeys.has(
241
+ `${normalizeLookupHostname(hostname)}:${String(options?.port ?? "")}`,
242
+ );
243
+ if (isPrivateHost(record.address) && !allowedOrigin) {
213
244
  const e = new Error(
214
245
  `Connect blocked: ${hostname} resolved to private address ${record.address}`,
215
246
  ) as NodeJS.ErrnoException;
@@ -264,10 +295,39 @@ export async function ssrfSafeFetch(
264
295
  maxRedirects?: number;
265
296
  httpsOnly?: boolean;
266
297
  assertUrlAllowed?: (url: string) => void | Promise<void>;
298
+ /**
299
+ * Exact origins that may resolve to a private address. A workspace runs
300
+ * every app on loopback behind one gateway, so sibling A2A calls are
301
+ * private by construction; without this they are indistinguishable from an
302
+ * SSRF attempt and get blocked. Only ever pass origins the deployment
303
+ * itself configured (never a request-supplied value).
304
+ */
305
+ allowedPrivateOrigins?: readonly string[];
267
306
  } = {},
268
307
  ): Promise<Response> {
269
308
  const maxRedirects = options.maxRedirects ?? 3;
270
- const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
309
+ const dispatcher =
310
+ (await createSsrfSafeDispatcher(options.allowedPrivateOrigins)) ??
311
+ undefined;
312
+ const allowedPrivateOrigins = new Set(
313
+ (options.allowedPrivateOrigins ?? [])
314
+ .map((origin) => {
315
+ try {
316
+ return new URL(origin).origin;
317
+ } catch {
318
+ return "";
319
+ }
320
+ })
321
+ .filter(Boolean),
322
+ );
323
+ const isAllowedPrivateOrigin = (candidate: string): boolean => {
324
+ if (allowedPrivateOrigins.size === 0) return false;
325
+ try {
326
+ return allowedPrivateOrigins.has(new URL(candidate).origin);
327
+ } catch {
328
+ return false;
329
+ }
330
+ };
271
331
 
272
332
  let currentUrl = url;
273
333
  for (let hop = 0; hop <= maxRedirects; hop++) {
@@ -277,7 +337,10 @@ export async function ssrfSafeFetch(
277
337
  `SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`,
278
338
  );
279
339
  }
280
- if (await isBlockedExtensionUrlWithDns(currentUrl)) {
340
+ if (
341
+ !isAllowedPrivateOrigin(currentUrl) &&
342
+ (await isBlockedExtensionUrlWithDns(currentUrl))
343
+ ) {
281
344
  throw new Error(
282
345
  `SSRF blocked: refusing to fetch private/internal address (${currentUrl})`,
283
346
  );
@@ -14,7 +14,6 @@ import {
14
14
  type ChatHistoryItem,
15
15
  } from "@agent-native/toolkit/chat-history";
16
16
  import {
17
- IconHierarchy2,
18
17
  IconLayoutSidebarLeftCollapse,
19
18
  IconLayoutSidebarLeftExpand,
20
19
  IconMessageCircle,
@@ -43,12 +42,6 @@ const navItems = [
43
42
  ];
44
43
 
45
44
  const bottomNavItems = [
46
- {
47
- icon: IconHierarchy2,
48
- labelKey: "settings.agentTitle",
49
- href: "/agent",
50
- view: "agent",
51
- },
52
45
  {
53
46
  icon: IconSettings,
54
47
  labelKey: "navigation.settings",
@@ -327,7 +327,7 @@ export function ErrorBoundary() {
327
327
  {copy.goHome}
328
328
  </Link>
329
329
  <ErrorReportActions
330
- appName="Agent Native"
330
+ appName="{{APP_TITLE}}"
331
331
  title={title}
332
332
  details={details}
333
333
  status={status}
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "Agent Native App",
3
- "short_name": "App",
2
+ "name": "{{APP_TITLE}}",
3
+ "short_name": "{{APP_TITLE}}",
4
4
  "description": "Agent-native application",
5
5
  "start_url": "/",
6
6
  "display": "standalone",
@@ -2414,6 +2414,10 @@ type NitroModuleGraph = {
2414
2414
 
2415
2415
  const NITRO_STARTUP_SETTLE_MS = 1_000;
2416
2416
  const NITRO_STARTUP_TIMEOUT_MS = 30_000;
2417
+ const NITRO_STARTUP_RETRY_KEY = "__agent_native_nitro_startup_retry";
2418
+ const NITRO_STARTUP_RETRY_MAX = 5;
2419
+ const NITRO_STARTUP_RETRY_DELAY_MS = 1_000;
2420
+ const NITRO_STARTUP_RETRY_RESET_MS = 15_000;
2417
2421
 
2418
2422
  function nitroModuleGraphSignature(environment: unknown): string | null {
2419
2423
  const graph = (environment as { moduleGraph?: NitroModuleGraph } | undefined)
@@ -2456,9 +2460,66 @@ function sendNitroStartingResponse(
2456
2460
  res.end();
2457
2461
  return;
2458
2462
  }
2459
- res.end(
2460
- '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0.25"><title>Starting…</title></head><body></body></html>',
2461
- );
2463
+ res.end(`<!doctype html>
2464
+ <html>
2465
+ <head>
2466
+ <meta charset="utf-8">
2467
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2468
+ <title>Dev server restarting…</title>
2469
+ <style>
2470
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; font: 16px/1.5 system-ui, sans-serif; color: #171717; background: #fafafa; }
2471
+ main { width: min(560px, calc(100vw - 48px)); }
2472
+ h1 { margin: 0 0 8px; font-size: 1.25rem; }
2473
+ p { margin: 0; color: #737373; }
2474
+ </style>
2475
+ </head>
2476
+ <body>
2477
+ <main>
2478
+ <h1>Dev server is restarting…</h1>
2479
+ <p id="agent-native-nitro-retry-status">Checking again shortly.</p>
2480
+ </main>
2481
+ <script>
2482
+ (() => {
2483
+ const key = ${JSON.stringify(NITRO_STARTUP_RETRY_KEY)};
2484
+ const maxRetries = ${NITRO_STARTUP_RETRY_MAX};
2485
+ const resetAfterMs = ${NITRO_STARTUP_RETRY_RESET_MS};
2486
+ const retryDelayMs = ${NITRO_STARTUP_RETRY_DELAY_MS};
2487
+ const status = document.getElementById("agent-native-nitro-retry-status");
2488
+ const now = Date.now();
2489
+ let count = 0;
2490
+ let lastAttemptAt = 0;
2491
+
2492
+ try {
2493
+ const stored = JSON.parse(sessionStorage.getItem(key) || "null");
2494
+ if (stored && typeof stored === "object") {
2495
+ count = Number.isFinite(stored.count) ? stored.count : 0;
2496
+ lastAttemptAt = Number.isFinite(stored.at) ? stored.at : 0;
2497
+ }
2498
+ } catch (error) {
2499
+ // A blocked session store is handled below by showing a manual retry.
2500
+ }
2501
+
2502
+ if (now - lastAttemptAt > resetAfterMs) count = 0;
2503
+ if (count >= maxRetries) {
2504
+ if (status) status.textContent = "The server is still unavailable. Refresh when it is ready.";
2505
+ return;
2506
+ }
2507
+
2508
+ const nextState = JSON.stringify({ count: count + 1, at: now });
2509
+ try {
2510
+ sessionStorage.setItem(key, nextState);
2511
+ if (sessionStorage.getItem(key) !== nextState) throw new Error("unavailable");
2512
+ } catch (error) {
2513
+ if (status) status.textContent = "Refresh manually when the server is ready.";
2514
+ return;
2515
+ }
2516
+
2517
+ if (status) status.textContent = "Retrying in one second…";
2518
+ setTimeout(() => window.location.reload(), retryDelayMs);
2519
+ })();
2520
+ </script>
2521
+ </body>
2522
+ </html>`);
2462
2523
  }
2463
2524
 
2464
2525
  function nitroStartupGate(
@@ -19,14 +19,12 @@ import {
19
19
  IconLock,
20
20
  IconLink,
21
21
  IconMessageCircle,
22
- IconHierarchy2,
23
22
  IconUsersGroup,
24
23
  IconEye,
25
24
  IconEyeOff,
26
25
  IconPlayerPlay,
27
26
  IconLayoutSidebarLeftCollapse,
28
27
  IconLayoutSidebarLeftExpand,
29
- IconArrowUpRight,
30
28
  } from "@tabler/icons-react";
31
29
  import {
32
30
  useQuery,
@@ -183,12 +181,6 @@ import {
183
181
  import { CSS } from "@dnd-kit/utilities";
184
182
 
185
183
  const bottomItems = [
186
- {
187
- icon: IconHierarchy2,
188
- labelKey: "settings.agentTitle",
189
- href: "/agent",
190
- showExternalLinkIcon: true,
191
- },
192
184
  { icon: IconSettings, labelKey: "navigation.settings", href: "/settings" },
193
185
  ];
194
186
 
@@ -2098,12 +2090,6 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2098
2090
  href: "/data-dictionary",
2099
2091
  active: location.pathname.startsWith("/data-dictionary"),
2100
2092
  },
2101
- {
2102
- icon: IconHierarchy2,
2103
- label: t("settings.agentTitle"),
2104
- href: "/agent",
2105
- active: location.pathname === "/agent",
2106
- },
2107
2093
  {
2108
2094
  icon: IconSettings,
2109
2095
  label: t("navigation.settings"),
@@ -2572,15 +2558,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2572
2558
  )}
2573
2559
  >
2574
2560
  <Icon className="h-4 w-4" />
2575
- <span className="flex min-w-0 items-center gap-1">
2576
- <span className="truncate">{t(item.labelKey)}</span>
2577
- {item.showExternalLinkIcon && (
2578
- <IconArrowUpRight
2579
- aria-hidden="true"
2580
- className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70"
2581
- />
2582
- )}
2583
- </span>
2561
+ <span className="truncate">{t(item.labelKey)}</span>
2584
2562
  </Link>
2585
2563
  );
2586
2564
  })}
@@ -1,7 +1,7 @@
1
1
  import { redirect, type LoaderFunctionArgs } from "react-router";
2
2
 
3
3
  const SEO_TITLE =
4
- "Agent-Native Analytics - Open Source Alternative to Amplitude & FullStory";
4
+ "Analytics - Open Source Alternative to Amplitude & FullStory";
5
5
  const SEO_DESCRIPTION =
6
6
  "Open Source analytics app and alternative to Amplitude and FullStory where AI agents connect to warehouses, product analytics, and CRM data to answer questions and build dashboards.";
7
7
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ App branding now uses the product name without the Agent-Native prefix.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "analytics",
3
- "displayName": "Agent-Native Analytics",
3
+ "displayName": "Analytics",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "Agent Native Analytics",
2
+ "name": "Analytics",
3
3
  "short_name": "Analytics",
4
4
  "description": "Analytics dashboard",
5
5
  "start_url": ".",
@@ -13,7 +13,7 @@ export default createAuthPlugin({
13
13
  "/_agent-native/actions/get-public-status-page",
14
14
  ],
15
15
  marketing: {
16
- appName: "Agent-Native Analytics",
16
+ appName: "Analytics",
17
17
  tagline:
18
18
  "Your AI agent queries your data sources, builds dashboards, and answers business questions alongside you.",
19
19
  features: [
@@ -17,7 +17,6 @@ import {
17
17
  type ChatHistoryItem,
18
18
  } from "@agent-native/toolkit/chat-history";
19
19
  import {
20
- IconHierarchy2,
21
20
  IconClipboardList,
22
21
  IconLayoutSidebarLeftCollapse,
23
22
  IconLayoutSidebarLeftExpand,
@@ -45,7 +44,6 @@ const baseNavItems = [
45
44
  ];
46
45
 
47
46
  const bottomNavItems = [
48
- { icon: IconHierarchy2, labelKey: "settings.agentTitle", href: "/agent" },
49
47
  { icon: IconSettings, labelKey: "navigation.settings", href: "/settings" },
50
48
  ];
51
49
 
@@ -35,7 +35,7 @@ const CHAT_STARTERS = [
35
35
  ] as const;
36
36
 
37
37
  const SEO_TITLE =
38
- "Agent-Native Assets - Open Source AI asset library for brand-safe images and video";
38
+ "Assets - Open Source AI asset library for brand-safe images and video";
39
39
  const SEO_DESCRIPTION =
40
40
  "Open Source asset manager for AI teams to organize brand libraries, search creative work, and generate on-brand images and videos.";
41
41
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ App branding now uses the product name without the Agent-Native prefix.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-25
4
+ ---
5
+
6
+ Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.
@@ -2,7 +2,7 @@ import { createAuthPlugin } from "@agent-native/core/server";
2
2
 
3
3
  export default createAuthPlugin({
4
4
  marketing: {
5
- appName: "Agent-Native Assets",
5
+ appName: "Assets",
6
6
  tagline:
7
7
  "Your AI agent creates, refines, and organizes on-brand assets alongside you.",
8
8
  features: [
@@ -1,6 +1,5 @@
1
1
  import type { Icon } from "@tabler/icons-react";
2
2
  import {
3
- IconHierarchy2,
4
3
  IconBook2,
5
4
  IconChecks,
6
5
  IconDatabase,
@@ -1007,12 +1006,6 @@ export const navItems: Array<{
1007
1006
  href: "/settings",
1008
1007
  icon: IconSettings,
1009
1008
  },
1010
- {
1011
- view: "agent",
1012
- label: "Manage agent",
1013
- href: "/agent",
1014
- icon: IconHierarchy2,
1015
- },
1016
1009
  ];
1017
1010
 
1018
1011
  export const defaultSettings: BrainSettings = {