@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/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": {
|
|
@@ -390,8 +390,8 @@
|
|
|
390
390
|
"y-protocols": "^1.0.7",
|
|
391
391
|
"yjs": "^13.6.31",
|
|
392
392
|
"zod": "^4.3.6",
|
|
393
|
-
"@agent-native/
|
|
394
|
-
"@agent-native/
|
|
393
|
+
"@agent-native/recap-cli": "0.5.1",
|
|
394
|
+
"@agent-native/toolkit": "^0.10.6"
|
|
395
395
|
},
|
|
396
396
|
"devDependencies": {
|
|
397
397
|
"@ai-sdk/anthropic": "^3.0.71",
|
package/src/a2a/client.ts
CHANGED
|
@@ -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,
|
package/src/agent/run-store.ts
CHANGED
|
@@ -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",
|
package/src/vite/client.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|