@agent-native/core 0.122.0 → 0.122.2

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 (187) hide show
  1. package/README.md +5 -25
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +15 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +57 -4
  6. package/corpus/core/src/agent/production-agent.ts +65 -1
  7. package/corpus/core/src/agent/run-store.ts +61 -1
  8. package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
  9. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
  10. package/corpus/core/src/extensions/url-safety.ts +67 -4
  11. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  12. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  13. package/corpus/core/src/templates/default/app/root.tsx +1 -1
  14. package/corpus/core/src/templates/default/public/manifest.json +2 -2
  15. package/corpus/core/src/vite/client.ts +64 -3
  16. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
  17. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
  18. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
  19. package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
  20. package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
  21. package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  23. package/corpus/templates/analytics/package.json +1 -1
  24. package/corpus/templates/analytics/public/manifest.json +1 -1
  25. package/corpus/templates/analytics/server/db/schema.ts +12 -0
  26. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
  27. package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
  28. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
  29. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
  30. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
  31. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
  32. package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
  33. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  34. package/corpus/templates/analytics/server/plugins/db.ts +41 -1
  35. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
  36. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
  37. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  38. package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  40. package/corpus/templates/assets/server/plugins/auth.ts +1 -1
  41. package/corpus/templates/brain/app/lib/brain.ts +0 -7
  42. package/corpus/templates/brain/app/routes/_index.tsx +1 -2
  43. package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  44. package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  45. package/corpus/templates/brain/package.json +1 -1
  46. package/corpus/templates/brain/public/manifest.json +1 -1
  47. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  48. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
  49. package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
  50. package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  51. package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  52. package/corpus/templates/calendar/package.json +1 -1
  53. package/corpus/templates/calendar/public/manifest.json +1 -1
  54. package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
  55. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  56. package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  57. package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
  58. package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
  59. package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
  60. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
  61. package/corpus/templates/clips/app/routes/_index.tsx +1 -1
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
  63. package/corpus/templates/clips/app/routes/record.tsx +5 -0
  64. package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  66. package/corpus/templates/clips/server/db/schema.ts +2 -0
  67. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
  68. package/corpus/templates/clips/server/plugins/auth.ts +1 -1
  69. package/corpus/templates/clips/server/plugins/db.ts +8 -0
  70. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
  71. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
  72. package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
  73. package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  74. package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  75. package/corpus/templates/content/package.json +1 -1
  76. package/corpus/templates/content/public/manifest.json +1 -1
  77. package/corpus/templates/content/server/plugins/auth.ts +1 -1
  78. package/corpus/templates/design/actions/edit-design.ts +66 -27
  79. package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
  80. package/corpus/templates/design/app/routes/_index.tsx +1 -1
  81. package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  82. package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
  83. package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  84. package/corpus/templates/design/server/plugins/auth.ts +1 -1
  85. package/corpus/templates/design/server/source-workspace.ts +5 -1
  86. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
  87. package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
  88. package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  89. package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  90. package/corpus/templates/dispatch/package.json +1 -1
  91. package/corpus/templates/dispatch/public/manifest.json +1 -1
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
  93. package/corpus/templates/forms/actions/create-form.ts +5 -1
  94. package/corpus/templates/forms/actions/export-responses.ts +22 -11
  95. package/corpus/templates/forms/actions/update-form.ts +5 -1
  96. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
  97. package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
  99. package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
  100. package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  101. package/corpus/templates/forms/package.json +1 -1
  102. package/corpus/templates/forms/public/manifest.json +1 -1
  103. package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
  104. package/corpus/templates/forms/server/plugins/auth.ts +1 -1
  105. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
  106. package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  107. package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  108. package/corpus/templates/macros/package.json +1 -1
  109. package/corpus/templates/macros/server/plugins/auth.ts +2 -2
  110. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
  111. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
  112. package/corpus/templates/mail/app/routes/$view.tsx +1 -1
  113. package/corpus/templates/mail/app/routes/_index.tsx +1 -1
  114. package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  115. package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  116. package/corpus/templates/mail/package.json +1 -1
  117. package/corpus/templates/mail/public/manifest.json +1 -1
  118. package/corpus/templates/mail/server/plugins/auth.ts +1 -1
  119. package/corpus/templates/plan/agent-native.app-skill.json +1 -1
  120. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
  121. package/corpus/templates/plan/app/lib/app-config.ts +1 -1
  122. package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  123. package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  124. package/corpus/templates/plan/package.json +1 -1
  125. package/corpus/templates/plan/public/manifest.json +1 -1
  126. package/corpus/templates/plan/server/plugins/auth.ts +1 -1
  127. package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
  128. package/corpus/templates/slides/actions/import-file.ts +3 -4
  129. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  130. package/corpus/templates/slides/app/routes/_index.tsx +1 -1
  131. package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
  132. package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
  133. package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
  134. package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  135. package/corpus/templates/slides/package.json +1 -1
  136. package/corpus/templates/slides/public/manifest.json +1 -1
  137. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
  138. package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
  139. package/corpus/templates/slides/server/plugins/auth.ts +1 -1
  140. package/dist/a2a/client.d.ts.map +1 -1
  141. package/dist/a2a/client.js +58 -4
  142. package/dist/a2a/client.js.map +1 -1
  143. package/dist/agent/production-agent.d.ts.map +1 -1
  144. package/dist/agent/production-agent.js +63 -1
  145. package/dist/agent/production-agent.js.map +1 -1
  146. package/dist/agent/run-store.d.ts.map +1 -1
  147. package/dist/agent/run-store.js +49 -0
  148. package/dist/agent/run-store.js.map +1 -1
  149. package/dist/client/settings/SettingsPanel.js +9 -9
  150. package/dist/client/settings/SettingsPanel.js.map +1 -1
  151. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  152. package/dist/client/settings/SettingsTabsPage.js +7 -4
  153. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  154. package/dist/collab/awareness.d.ts +2 -2
  155. package/dist/collab/awareness.d.ts.map +1 -1
  156. package/dist/collab/routes.d.ts +1 -1
  157. package/dist/extensions/url-safety.d.ts +9 -16
  158. package/dist/extensions/url-safety.d.ts.map +1 -1
  159. package/dist/extensions/url-safety.js +48 -4
  160. package/dist/extensions/url-safety.js.map +1 -1
  161. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  162. package/dist/notifications/routes.d.ts +3 -3
  163. package/dist/observability/routes.d.ts +3 -3
  164. package/dist/secrets/routes.d.ts +9 -9
  165. package/dist/server/builder-design-systems.js +1 -1
  166. package/dist/server/builder-design-systems.js.map +1 -1
  167. package/dist/server/transcribe-voice.d.ts +1 -1
  168. package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  169. package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  170. package/dist/templates/default/app/root.tsx +1 -1
  171. package/dist/templates/default/public/manifest.json +2 -2
  172. package/dist/vite/client.d.ts.map +1 -1
  173. package/dist/vite/client.js +64 -1
  174. package/dist/vite/client.js.map +1 -1
  175. package/package.json +3 -3
  176. package/src/a2a/client.ts +57 -4
  177. package/src/agent/production-agent.ts +65 -1
  178. package/src/agent/run-store.ts +61 -1
  179. package/src/client/settings/SettingsPanel.tsx +9 -9
  180. package/src/client/settings/SettingsTabsPage.tsx +12 -2
  181. package/src/extensions/url-safety.ts +67 -4
  182. package/src/server/builder-design-systems.ts +1 -1
  183. package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
  184. package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
  185. package/src/templates/default/app/root.tsx +1 -1
  186. package/src/templates/default/public/manifest.json +2 -2
  187. 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
 
@@ -47,8 +39,6 @@ Fork a working app and let the agent evolve it. **You can customize everything.*
47
39
 
48
40
  **Agent-Native Loom**
49
41
 
50
- Record your screen with auto-transcripts and captured browser debug logs, share a link, and let an agent read the transcript, see timestamped frames, and fix the bug.
51
-
52
42
  </td>
53
43
  <td width="33%" align="center" valign="top">
54
44
 
@@ -58,8 +48,6 @@ Record your screen with auto-transcripts and captured browser debug logs, share
58
48
 
59
49
  **Visual plan mode for coding agents**
60
50
 
61
- Install `/visual-plan` and `/visual-recap` so your coding agent can plan before it builds and recap changes after they land. High-level code reviews with diagrams, wireframes, annotations, and review links.
62
-
63
51
  </td>
64
52
  <td width="33%" align="center" valign="top">
65
53
 
@@ -69,8 +57,6 @@ Install `/visual-plan` and `/visual-recap` so your coding agent can plan before
69
57
 
70
58
  **Agent-Native Figma**
71
59
 
72
- Generate interactive HTML prototypes, compare variants, refine controls, and export the result.
73
-
74
60
  </td>
75
61
  </tr>
76
62
  <tr>
@@ -82,8 +68,6 @@ Generate interactive HTML prototypes, compare variants, refine controls, and exp
82
68
 
83
69
  **Agent-Native Notion/Obsidian**
84
70
 
85
- Edit local Markdown/MDX files, generate rich interactive custom blocks, and draft, rewrite, or publish with an agent.
86
-
87
71
  </td>
88
72
  <td width="33%" align="center" valign="top">
89
73
 
@@ -93,8 +77,6 @@ Edit local Markdown/MDX files, generate rich interactive custom blocks, and draf
93
77
 
94
78
  **Open-Source Alternative to Amplitude and FullStory**
95
79
 
96
- Connect analytics data sources, prompt for real charts, and build reusable dashboards.
97
-
98
80
  </td>
99
81
  <td width="33%" align="center" valign="top">
100
82
 
@@ -104,13 +86,11 @@ Connect analytics data sources, prompt for real charts, and build reusable dashb
104
86
 
105
87
  **A minimal ChatGPT-style app for your own agent**
106
88
 
107
- Chat-first app scaffold with durable threads, actions, auth, live sync, and a clean path to add screens or plug in your own agent backend.
108
-
109
89
  </td>
110
90
  </tr>
111
91
  </table>
112
92
 
113
- View the full app gallery at **[agent-native.com/apps](https://agent-native.com/apps)**, or build from scratch with the **[framework guide](https://agent-native.com/docs/getting-started)**.
93
+ View the [full gallery](https://agent-native.com/apps), or build from scratch with the **[framework guide](https://agent-native.com/docs/getting-started)**.
114
94
 
115
95
  ## Quick Start
116
96
 
@@ -123,7 +103,7 @@ pnpm install
123
103
  pnpm dev
124
104
  ```
125
105
 
126
- Prefer flags? `create my-app --template mail`, `--headless`, or `--standalone` skip the prompt.
106
+ Prefer flags? `create my-app --template chat`, `--headless`, or `--standalone` skip the prompt.
127
107
 
128
108
  See the full [getting started docs](https://agent-native.com/docs).
129
109
 
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: 6470
@@ -1,5 +1,20 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.122.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 905bdba: Improve tool-input recovery, stale-run handling, and design-system routing reliability.
8
+
9
+ ## 0.122.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 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".
14
+ - 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).
15
+ - c8a0bcf: Bound Nitro dev-server startup recovery so a failed restart stops refreshing forever.
16
+ - c8a0bcf: Keep Manage agent as a dedicated linked destination at the bottom of settings navigation.
17
+
3
18
  ## 0.122.0
4
19
 
5
20
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.122.0",
3
+ "version": "0.122.2",
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 {
@@ -3369,6 +3369,59 @@ function normalizeOptionalToolPlaceholders(
3369
3369
  return { input: normalized, changed: true };
3370
3370
  }
3371
3371
 
3372
+ /**
3373
+ * Models routinely JSON-encode a field's value into a string when the tool
3374
+ * schema wants an object/array — e.g. `config: "{\"name\":...}"` instead of
3375
+ * `config: {name:...}`, or `operations: "[...]"` instead of `operations: [...]`.
3376
+ * Seen across every app profiled in the 2026-07-25 reliability sweep
3377
+ * (brain's `update-source` config, analytics' `mutate-dashboard`/
3378
+ * `update-dashboard` operations/ops/config) and the model does NOT
3379
+ * self-correct across repeated identical retries — it burns the full
3380
+ * `MAX_IDENTICAL_TOOL_ERRORS` retry budget on the same wrong shape every
3381
+ * time. Evidence-gated exactly like `normalizeOptionalToolPlaceholders`:
3382
+ * only coerce a field whose CURRENT value fails schema validation (so a
3383
+ * legitimate string value is never touched — a field schema-valid as a
3384
+ * string is never also schema-valid as object/array) and whose parsed form
3385
+ * passes. Never touches values that are already the right shape.
3386
+ */
3387
+ function coerceStringifiedJsonToolValues(
3388
+ schema: RawJsonSchema | undefined,
3389
+ input: unknown,
3390
+ ): { input: unknown; changed: boolean } {
3391
+ if (!schema?.properties || !input || typeof input !== "object") {
3392
+ return { input, changed: false };
3393
+ }
3394
+ if (Array.isArray(input)) return { input, changed: false };
3395
+
3396
+ let normalized: Record<string, unknown> | null = null;
3397
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
3398
+ if (typeof value !== "string" || value.trim().length === 0) continue;
3399
+ const propertySchema = schema.properties[key];
3400
+ if (!propertySchema || typeof propertySchema !== "object") continue;
3401
+ const expectedType = (propertySchema as { type?: unknown }).type;
3402
+ const expectsObjectOrArray =
3403
+ expectedType === "object" ||
3404
+ expectedType === "array" ||
3405
+ (Array.isArray(expectedType) &&
3406
+ (expectedType.includes("object") || expectedType.includes("array")));
3407
+ if (!expectsObjectOrArray) continue;
3408
+ if (schemaAcceptsToolValue(propertySchema, value)) continue;
3409
+ let parsed: unknown;
3410
+ try {
3411
+ parsed = JSON.parse(value);
3412
+ } catch {
3413
+ continue;
3414
+ }
3415
+ if (typeof parsed !== "object" || parsed === null) continue;
3416
+ if (!schemaAcceptsToolValue(propertySchema, parsed)) continue;
3417
+ normalized ??= { ...(input as Record<string, unknown>) };
3418
+ normalized[key] = parsed;
3419
+ }
3420
+ return normalized
3421
+ ? { input: normalized, changed: true }
3422
+ : { input, changed: false };
3423
+ }
3424
+
3372
3425
  function getRawToolInputValidator(schema: RawJsonSchema): ValidateFunction {
3373
3426
  const cached = rawToolInputValidatorCache.get(schema);
3374
3427
  if (cached) return cached;
@@ -4354,6 +4407,17 @@ export async function runAgentLoop(opts: {
4354
4407
  if (placeholderNormalization.changed) {
4355
4408
  toolCall = { ...toolCall, input: placeholderNormalization.input };
4356
4409
  }
4410
+ const jsonStringCoercion = actionEntry
4411
+ ? coerceStringifiedJsonToolValues(
4412
+ actionEntry.tool.parameters,
4413
+ toolCall.input,
4414
+ )
4415
+ : { input: toolCall.input, changed: false };
4416
+ if (jsonStringCoercion.changed) {
4417
+ toolCall = { ...toolCall, input: jsonStringCoercion.input };
4418
+ }
4419
+ const toolInputNormalized =
4420
+ placeholderNormalization.changed || jsonStringCoercion.changed;
4357
4421
  const wireToolInput = JSON.stringify(toolCall.input ?? {});
4358
4422
  const normalizedToolInput = normalizeToolCallInputForHistory(
4359
4423
  toolCall.input,
@@ -4776,7 +4840,7 @@ export async function runAgentLoop(opts: {
4776
4840
  });
4777
4841
 
4778
4842
  const toolCallSchemaError = toolCallErrors.get(toolCall.id);
4779
- if (toolCallSchemaError && !placeholderNormalization.changed) {
4843
+ if (toolCallSchemaError && !toolInputNormalized) {
4780
4844
  const result = finalizeToolErrorResult(
4781
4845
  toolInputSchemaErrorResult(
4782
4846
  toolCall.name,
@@ -173,6 +173,34 @@ export const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20_000;
173
173
  */
174
174
  const STALE_RUN_RECOVERY_MAX_TURN_RUNS = 25;
175
175
 
176
+ /**
177
+ * Circuit breaker for a DETERMINISTIC dead-on-arrival loop: some request
178
+ * shapes make the worker hang almost immediately every single time (e.g. an
179
+ * un-timed-out provider fetch that blocks the event loop) rather than merely
180
+ * hitting a transient blip. Because `attemptStaleRunRecovery` replays the
181
+ * SAME captured `dispatch_payload` on every successor (never a fresh
182
+ * request), such a turn was retrying an unwinnable request up to
183
+ * `STALE_RUN_RECOVERY_MAX_TURN_RUNS` (25) times — ~25 * 53s ≈ 22 minutes,
184
+ * each cycle re-billing the full input context — before finally giving up.
185
+ * Confirmed live in prod (assets: one turn cycled 24x, each attempt an
186
+ * identical ~32K-token request that made a token of real progress around
187
+ * ~8s in then went completely silent for the rest of its life until the 45s
188
+ * reap). Stop recovering after this many CONSECUTIVE stale_run reaps that
189
+ * each made near-zero real progress — a single blip never trips it (needs
190
+ * 3 in a row), and a run that's genuinely grinding through long work right
191
+ * up to its heartbeat window is untouched (see `hasNoForwardProgress`).
192
+ */
193
+ const STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT = 3;
194
+
195
+ /**
196
+ * A reaped run counts as having made no forward progress if it never
197
+ * emitted a real event (`last_progress_at` unset) or died within this many
198
+ * ms of starting — well short of the 45s background reap window, so a run
199
+ * that was legitimately working almost up to the reap boundary is not
200
+ * penalized. See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`.
201
+ */
202
+ const STALE_RUN_RECOVERY_NO_PROGRESS_WINDOW_MS = 20_000;
203
+
176
204
  /**
177
205
  * Maximum time the stale reapers (`reapIfStale`, `reapAllStaleRuns`,
178
206
  * `cleanupOldRuns`'s heartbeat-stale pass) will suspend reaping a "running"
@@ -1361,7 +1389,8 @@ type StaleRunRecoveryOutcome =
1361
1389
  | { outcome: "not_background" }
1362
1390
  | { outcome: "payload_missing" }
1363
1391
  | { outcome: "newer_run_exists" }
1364
- | { outcome: "budget_exhausted" };
1392
+ | { outcome: "budget_exhausted" }
1393
+ | { outcome: "repeated_no_progress" };
1365
1394
 
1366
1395
  /**
1367
1396
  * Mirrors `production-agent.ts`'s `generateRunId` — duplicated (not
@@ -1477,6 +1506,37 @@ async function attemptStaleRunRecovery(
1477
1506
  return { outcome: "budget_exhausted" };
1478
1507
  }
1479
1508
 
1509
+ // See `STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT`: a run whose last
1510
+ // N attempts (including the one just reaped, already written by the
1511
+ // caller's UPDATE earlier in this same transaction) all died as stale_run
1512
+ // having made essentially no real progress is retrying an unwinnable
1513
+ // request, not recovering from a blip. Stop far short of the 25-run/~22min
1514
+ // budget above instead of grinding through it.
1515
+ const { rows: recentRows } = await db.execute({
1516
+ sql: `SELECT error_code, started_at, last_progress_at FROM agent_runs WHERE turn_id = ? ORDER BY started_at DESC LIMIT ?`,
1517
+ args: [turnId, STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT],
1518
+ });
1519
+ const recent = (recentRows ?? []) as Array<{
1520
+ error_code?: string | null;
1521
+ started_at: number | string;
1522
+ last_progress_at: number | string | null;
1523
+ }>;
1524
+ const allDeadOnArrival =
1525
+ recent.length === STALE_RUN_RECOVERY_CONSECUTIVE_NO_PROGRESS_LIMIT &&
1526
+ recent.every((r) => {
1527
+ if (r.error_code !== STALE_RUN_ERROR_EVENT.errorCode) return false;
1528
+ const started = Number(r.started_at) || 0;
1529
+ const progress =
1530
+ r.last_progress_at == null ? null : Number(r.last_progress_at);
1531
+ return (
1532
+ progress === null ||
1533
+ progress - started < STALE_RUN_RECOVERY_NO_PROGRESS_WINDOW_MS
1534
+ );
1535
+ });
1536
+ if (allDeadOnArrival) {
1537
+ return { outcome: "repeated_no_progress" };
1538
+ }
1539
+
1480
1540
  const successorRunId = generateRecoveryRunId();
1481
1541
  const now = Date.now();
1482
1542
  await db.execute({
@@ -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
  );
@@ -140,7 +140,7 @@ function trimTrailingSlash(value: string): string {
140
140
  export function getBuilderDesignSystemsBaseUrl(): string {
141
141
  return (
142
142
  process.env.BUILDER_DESIGN_SYSTEMS_BASE_URL ||
143
- `${trimTrailingSlash(getBuilderProxyOrigin())}/design-systems/v1`
143
+ `${trimTrailingSlash(getBuilderProxyOrigin())}/agent-native/design-systems/v1`
144
144
  );
145
145
  }
146
146
 
@@ -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",