@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.
- package/README.md +5 -25
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +57 -4
- package/corpus/core/src/agent/production-agent.ts +65 -1
- package/corpus/core/src/agent/run-store.ts +61 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/corpus/core/src/extensions/url-safety.ts +67 -4
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/core/src/templates/default/app/root.tsx +1 -1
- package/corpus/core/src/templates/default/public/manifest.json +2 -2
- package/corpus/core/src/vite/client.ts +64 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
- package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/analytics/package.json +1 -1
- package/corpus/templates/analytics/public/manifest.json +1 -1
- package/corpus/templates/analytics/server/db/schema.ts +12 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
- package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
- package/corpus/templates/analytics/server/plugins/db.ts +41 -1
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/assets/app/routes/_index.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/assets/server/plugins/auth.ts +1 -1
- package/corpus/templates/brain/app/lib/brain.ts +0 -7
- package/corpus/templates/brain/app/routes/_index.tsx +1 -2
- package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/brain/package.json +1 -1
- package/corpus/templates/brain/public/manifest.json +1 -1
- package/corpus/templates/brain/server/plugins/auth.ts +1 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
- package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/calendar/package.json +1 -1
- package/corpus/templates/calendar/public/manifest.json +1 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_index.tsx +1 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
- package/corpus/templates/clips/app/routes/record.tsx +5 -0
- package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/server/db/schema.ts +2 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
- package/corpus/templates/clips/server/plugins/auth.ts +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +8 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
- package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
- package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/public/manifest.json +1 -1
- package/corpus/templates/content/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/actions/edit-design.ts +66 -27
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/design/app/routes/_index.tsx +1 -1
- package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/design/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/server/source-workspace.ts +5 -1
- package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
- package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
- package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/dispatch/package.json +1 -1
- package/corpus/templates/dispatch/public/manifest.json +1 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
- package/corpus/templates/forms/actions/create-form.ts +5 -1
- package/corpus/templates/forms/actions/export-responses.ts +22 -11
- package/corpus/templates/forms/actions/update-form.ts +5 -1
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
- package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/forms/package.json +1 -1
- package/corpus/templates/forms/public/manifest.json +1 -1
- package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
- package/corpus/templates/forms/server/plugins/auth.ts +1 -1
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
- package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/macros/package.json +1 -1
- package/corpus/templates/macros/server/plugins/auth.ts +2 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
- package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
- package/corpus/templates/mail/app/routes/$view.tsx +1 -1
- package/corpus/templates/mail/app/routes/_index.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/mail/package.json +1 -1
- package/corpus/templates/mail/public/manifest.json +1 -1
- package/corpus/templates/mail/server/plugins/auth.ts +1 -1
- package/corpus/templates/plan/agent-native.app-skill.json +1 -1
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
- package/corpus/templates/plan/app/lib/app-config.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/plan/package.json +1 -1
- package/corpus/templates/plan/public/manifest.json +1 -1
- package/corpus/templates/plan/server/plugins/auth.ts +1 -1
- package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
- package/corpus/templates/slides/actions/import-file.ts +3 -4
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/slides/app/routes/_index.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/slides/package.json +1 -1
- package/corpus/templates/slides/public/manifest.json +1 -1
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
- package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
- package/corpus/templates/slides/server/plugins/auth.ts +1 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +58 -4
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +63 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +49 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +9 -9
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +7 -4
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +9 -16
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +48 -4
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/dist/templates/default/app/root.tsx +1 -1
- package/dist/templates/default/public/manifest.json +2 -2
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +64 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
- package/src/a2a/client.ts +57 -4
- package/src/agent/production-agent.ts +65 -1
- package/src/agent/run-store.ts +61 -1
- package/src/client/settings/SettingsPanel.tsx +9 -9
- package/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/src/extensions/url-safety.ts +67 -4
- package/src/server/builder-design-systems.ts +1 -1
- package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/src/templates/default/app/root.tsx +1 -1
- package/src/templates/default/public/manifest.json +2 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.122.
|
|
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
|
-
|
|
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 && !
|
|
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: "
|
|
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(...
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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",
|