@agent-native/core 0.164.1 → 0.164.3
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
- package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
- package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
- package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
- package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
- package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
- package/corpus/templates/assets/app/routes/picker.tsx +4 -1
- package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
- package/corpus/templates/clips/app/routes/_index.tsx +7 -11
- package/corpus/templates/design/app/routes/examples.tsx +2 -1
- package/corpus/templates/forms/app/routes/_index.tsx +3 -2
- package/corpus/templates/mail/app/routes/_index.tsx +3 -2
- package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
- package/dist/cli/code-agent-executor.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/deploy/build.js +10 -4
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/ssr-handler.js +13 -4
- package/dist/shared/cache-control.d.ts +22 -0
- package/dist/shared/cache-control.js +25 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@agent-native/core/client/hooks";
|
|
10
10
|
import { useT } from "@agent-native/core/client/i18n";
|
|
11
11
|
import { IconRefresh } from "@tabler/icons-react";
|
|
12
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
|
13
13
|
|
|
14
14
|
import { Badge } from "@/components/ui/badge";
|
|
15
15
|
import { Button } from "@/components/ui/button";
|
|
@@ -40,6 +40,51 @@ export interface FlagDirectory {
|
|
|
40
40
|
apps?: FlagApp[];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
interface VerifiedFlagMutation {
|
|
44
|
+
contractVersion: 3;
|
|
45
|
+
status: "verified";
|
|
46
|
+
key: string;
|
|
47
|
+
rules: FeatureFlagMetadata["rules"];
|
|
48
|
+
enabledForCurrentUser: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function reconcileVerifiedFleetMutation(
|
|
52
|
+
value: unknown,
|
|
53
|
+
targetAppId: string,
|
|
54
|
+
verified: VerifiedFlagMutation,
|
|
55
|
+
): FlagDirectory {
|
|
56
|
+
const data = asDirectory(value);
|
|
57
|
+
return {
|
|
58
|
+
...data,
|
|
59
|
+
apps: data.apps?.map((app) =>
|
|
60
|
+
appId(app) !== targetAppId
|
|
61
|
+
? app
|
|
62
|
+
: {
|
|
63
|
+
...app,
|
|
64
|
+
flags: app.flags?.map((flag) =>
|
|
65
|
+
flag.key === verified.key
|
|
66
|
+
? {
|
|
67
|
+
...flag,
|
|
68
|
+
rules: verified.rules,
|
|
69
|
+
enabledForCurrentUser: verified.enabledForCurrentUser,
|
|
70
|
+
}
|
|
71
|
+
: flag,
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function recoverFleetMutationFailure(
|
|
79
|
+
client: QueryClient,
|
|
80
|
+
context?: { previous: unknown; key: readonly unknown[] },
|
|
81
|
+
) {
|
|
82
|
+
if (context) client.setQueryData(context.key, context.previous);
|
|
83
|
+
void client.invalidateQueries({
|
|
84
|
+
queryKey: ["action", "list-workspace-feature-flags"],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
function asDirectory(value: unknown): FlagDirectory {
|
|
44
89
|
return value && typeof value === "object" ? (value as FlagDirectory) : {};
|
|
45
90
|
}
|
|
@@ -76,9 +121,10 @@ export function FeatureFlagsFleetPanel() {
|
|
|
76
121
|
{ retry: false },
|
|
77
122
|
);
|
|
78
123
|
const mutation = useActionMutation<
|
|
79
|
-
unknown,
|
|
124
|
+
VerifiedFlagMutation | unknown,
|
|
80
125
|
{ appId: string } & SetFeatureFlagInput
|
|
81
126
|
>("set-workspace-feature-flag", {
|
|
127
|
+
skipActionQueryInvalidation: true,
|
|
82
128
|
onMutate: async (input) => {
|
|
83
129
|
await client.cancelQueries({
|
|
84
130
|
queryKey: ["action", "list-workspace-feature-flags"],
|
|
@@ -119,13 +165,25 @@ export function FeatureFlagsFleetPanel() {
|
|
|
119
165
|
});
|
|
120
166
|
return { previous, key };
|
|
121
167
|
},
|
|
122
|
-
onError: (_error, _input, context: any) =>
|
|
123
|
-
|
|
168
|
+
onError: (_error, _input, context: any) =>
|
|
169
|
+
recoverFleetMutationFailure(client, context),
|
|
170
|
+
onSuccess: (result, input, context: any) => {
|
|
171
|
+
if (
|
|
172
|
+
!result ||
|
|
173
|
+
typeof result !== "object" ||
|
|
174
|
+
(result as Partial<VerifiedFlagMutation>).contractVersion !== 3 ||
|
|
175
|
+
(result as Partial<VerifiedFlagMutation>).status !== "verified"
|
|
176
|
+
) {
|
|
177
|
+
void client.invalidateQueries({
|
|
178
|
+
queryKey: ["action", "list-workspace-feature-flags"],
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const verified = result as VerifiedFlagMutation;
|
|
183
|
+
client.setQueryData(context.key, (old: unknown) =>
|
|
184
|
+
reconcileVerifiedFleetMutation(old, input.appId, verified),
|
|
185
|
+
);
|
|
124
186
|
},
|
|
125
|
-
onSettled: () =>
|
|
126
|
-
void client.invalidateQueries({
|
|
127
|
-
queryKey: ["action", "list-workspace-feature-flags"],
|
|
128
|
-
}),
|
|
129
187
|
});
|
|
130
188
|
const directory = asDirectory(flags.data);
|
|
131
189
|
const apps = directory.apps ?? [];
|
|
@@ -134,7 +192,11 @@ export function FeatureFlagsFleetPanel() {
|
|
|
134
192
|
return (
|
|
135
193
|
<StatusState
|
|
136
194
|
title={t("agents.flagsUnavailable")}
|
|
137
|
-
detail={
|
|
195
|
+
detail={
|
|
196
|
+
mutation.error?.message ||
|
|
197
|
+
flags.error?.message ||
|
|
198
|
+
t("agents.flagsUnreachable")
|
|
199
|
+
}
|
|
138
200
|
onRetry={() => void flags.refetch()}
|
|
139
201
|
/>
|
|
140
202
|
);
|
|
@@ -179,22 +241,19 @@ export function FeatureFlagsFleetPanel() {
|
|
|
179
241
|
</div>
|
|
180
242
|
{isReady(app) ? (
|
|
181
243
|
<div className="p-4">
|
|
244
|
+
{mutation.variables?.appId === appId(app) && mutation.error ? (
|
|
245
|
+
<p className="mb-3 text-sm text-destructive">
|
|
246
|
+
{mutation.error.message}
|
|
247
|
+
</p>
|
|
248
|
+
) : null}
|
|
182
249
|
<FeatureFlagsEditor
|
|
183
250
|
flags={(app.flags ?? []).filter(
|
|
184
251
|
(flag): flag is FeatureFlagMetadata =>
|
|
185
252
|
!!flag.rules && typeof flag.defaultValue === "boolean",
|
|
186
253
|
)}
|
|
187
254
|
isPending={mutation.isPending}
|
|
188
|
-
error={
|
|
189
|
-
|
|
190
|
-
? mutation.error
|
|
191
|
-
: null
|
|
192
|
-
}
|
|
193
|
-
errorFlagKey={
|
|
194
|
-
mutation.variables?.appId === appId(app)
|
|
195
|
-
? mutation.variables.key
|
|
196
|
-
: null
|
|
197
|
-
}
|
|
255
|
+
error={null}
|
|
256
|
+
errorFlagKey={null}
|
|
198
257
|
showHeader={false}
|
|
199
258
|
onMutate={(input) =>
|
|
200
259
|
mutation.mutate(qualifyFleetMutation(appId(app), input))
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useEffect, useRef, type RefObject } from "react";
|
|
2
2
|
|
|
3
|
-
/** Focus an input and select its existing value after the next paint. */
|
|
4
|
-
export function useAutoFocusSelect<
|
|
5
|
-
|
|
6
|
-
): RefObject<T | null> {
|
|
3
|
+
/** Focus an input/textarea and select its existing value after the next paint. */
|
|
4
|
+
export function useAutoFocusSelect<
|
|
5
|
+
T extends HTMLInputElement | HTMLTextAreaElement,
|
|
6
|
+
>(enabled: boolean): RefObject<T | null> {
|
|
7
7
|
const inputRef = useRef<T | null>(null);
|
|
8
8
|
|
|
9
9
|
useEffect(() => {
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
resourceCanManage,
|
|
94
94
|
type ResourceAccess,
|
|
95
95
|
} from "@/lib/resource-access";
|
|
96
|
+
import { useAutoFocusSelect } from "@/lib/use-auto-focus-select";
|
|
96
97
|
|
|
97
98
|
import { DashboardSkeleton } from "../DashboardSkeleton";
|
|
98
99
|
import { DashboardChartCard } from "./ChartCard";
|
|
@@ -206,6 +207,7 @@ export default function ExplorerDashboardPage() {
|
|
|
206
207
|
);
|
|
207
208
|
const [editingName, setEditingName] = useState(false);
|
|
208
209
|
const [nameInput, setNameInput] = useState("");
|
|
210
|
+
const nameInputRef = useAutoFocusSelect<HTMLInputElement>(editingName);
|
|
209
211
|
const [addChartOpen, setAddChartOpen] = useState(false);
|
|
210
212
|
const [loaded, setLoaded] = useState(false);
|
|
211
213
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
@@ -635,6 +637,7 @@ export default function ExplorerDashboardPage() {
|
|
|
635
637
|
) : null}
|
|
636
638
|
{editingName && canEdit ? (
|
|
637
639
|
<Input
|
|
640
|
+
ref={nameInputRef}
|
|
638
641
|
value={nameInput}
|
|
639
642
|
onChange={(e) => setNameInput(e.target.value)}
|
|
640
643
|
onBlur={handleSaveName}
|
|
@@ -129,6 +129,7 @@ import {
|
|
|
129
129
|
resourceCanManage,
|
|
130
130
|
type ResourceAccess,
|
|
131
131
|
} from "@/lib/resource-access";
|
|
132
|
+
import { useAutoFocusSelect } from "@/lib/use-auto-focus-select";
|
|
132
133
|
|
|
133
134
|
import BlankDashboard from "../BlankDashboard";
|
|
134
135
|
import { DashboardSkeleton } from "../DashboardSkeleton";
|
|
@@ -598,8 +599,11 @@ function SqlDashboardPageContent({
|
|
|
598
599
|
);
|
|
599
600
|
const [editingName, setEditingName] = useState(false);
|
|
600
601
|
const [nameInput, setNameInput] = useState("");
|
|
602
|
+
const nameInputRef = useAutoFocusSelect<HTMLInputElement>(editingName);
|
|
601
603
|
const [editingDescription, setEditingDescription] = useState(false);
|
|
602
604
|
const [descriptionInput, setDescriptionInput] = useState("");
|
|
605
|
+
const descriptionInputRef =
|
|
606
|
+
useAutoFocusSelect<HTMLTextAreaElement>(editingDescription);
|
|
603
607
|
const [loaded, setLoaded] = useState(false);
|
|
604
608
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
605
609
|
const [openDeleteAfterMenuClose, setOpenDeleteAfterMenuClose] =
|
|
@@ -1734,6 +1738,7 @@ function SqlDashboardPageContent({
|
|
|
1734
1738
|
<div className="flex min-w-0 items-center gap-2">
|
|
1735
1739
|
{editingName && canEdit ? (
|
|
1736
1740
|
<Input
|
|
1741
|
+
ref={nameInputRef}
|
|
1737
1742
|
value={nameInput}
|
|
1738
1743
|
onChange={(e) => setNameInput(e.target.value)}
|
|
1739
1744
|
onBlur={handleSaveName}
|
|
@@ -2159,6 +2164,7 @@ function SqlDashboardPageContent({
|
|
|
2159
2164
|
{/* Description (click to edit) */}
|
|
2160
2165
|
{editingDescription && canEdit ? (
|
|
2161
2166
|
<Textarea
|
|
2167
|
+
ref={descriptionInputRef}
|
|
2162
2168
|
value={descriptionInput}
|
|
2163
2169
|
onChange={(e) => setDescriptionInput(e.target.value)}
|
|
2164
2170
|
onBlur={handleSaveDescription}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
const SEO_TITLE =
|
|
@@ -25,11 +26,11 @@ function target(url: URL): string {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
28
|
-
throw redirect(target(url));
|
|
29
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
32
|
-
throw redirect(target(url));
|
|
33
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export default function IndexRoute() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
// `/adhoc/:id` is the legacy dashboard URL. The canonical, user-facing URL is
|
|
@@ -9,11 +10,15 @@ function target({ params, url }: LoaderFunctionArgs): string {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function loader(args: LoaderFunctionArgs) {
|
|
12
|
-
throw redirect(target(args))
|
|
13
|
+
throw withSsrHtmlContentType(redirect(target(args)), {
|
|
14
|
+
varyByQuery: true,
|
|
15
|
+
});
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function clientLoader(args: LoaderFunctionArgs) {
|
|
16
|
-
throw redirect(target(args))
|
|
19
|
+
throw withSsrHtmlContentType(redirect(target(args)), {
|
|
20
|
+
varyByQuery: true,
|
|
21
|
+
});
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export default function AdhocRedirectRoute() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
function target(url: URL): string {
|
|
@@ -5,11 +6,11 @@ function target(url: URL): string {
|
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
8
|
-
throw redirect(target(url));
|
|
9
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
12
|
-
throw redirect(target(url));
|
|
13
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export default function DashboardAliasRoute() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
function target(url: URL): string {
|
|
@@ -5,11 +6,11 @@ function target(url: URL): string {
|
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
8
|
-
throw redirect(target(url));
|
|
9
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
12
|
-
throw redirect(target(url));
|
|
13
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export default function OverviewAliasRoute() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
const TRAFFIC_DASHBOARD_PATH = "/dashboards/agent-native-templates-first-party";
|
|
@@ -7,11 +8,11 @@ function target(url: URL): string {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
10
|
-
throw redirect(target(url));
|
|
11
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
14
|
-
throw redirect(target(url));
|
|
15
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export default function TrafficDashboardAliasRoute() {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { signA2AToken } from "@agent-native/core/a2a";
|
|
4
|
+
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
4
5
|
import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
|
|
5
6
|
import { getOrgDomain } from "@agent-native/core/org";
|
|
6
7
|
|
|
8
|
+
import { VERIFIED_FLEET_FLAG_MUTATIONS } from "../../shared/feature-flags.js";
|
|
7
9
|
import type { AnalyticsAdminContext } from "./db-admin-connections.js";
|
|
8
10
|
|
|
9
11
|
const TARGET_TIMEOUT_MS = 3_000;
|
|
@@ -31,7 +33,16 @@ export interface WorkspaceFeatureFlagsResult {
|
|
|
31
33
|
apps: FleetFlagApp[];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
export interface
|
|
36
|
+
export interface VerifiedWorkspaceFeatureFlagMutationResult {
|
|
37
|
+
contractVersion: 3;
|
|
38
|
+
status: "verified";
|
|
39
|
+
key: string;
|
|
40
|
+
rules: Record<string, unknown>;
|
|
41
|
+
scope: { orgId: string | null; orgDomain: string | null };
|
|
42
|
+
enabledForCurrentUser: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TargetFeatureFlagMutationResult {
|
|
35
46
|
contractVersion: 2;
|
|
36
47
|
status: "ready";
|
|
37
48
|
key: string;
|
|
@@ -39,6 +50,10 @@ export interface WorkspaceFeatureFlagMutationResult {
|
|
|
39
50
|
scope: { orgId: string | null; orgDomain: string | null };
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
export type WorkspaceFeatureFlagMutationResult =
|
|
54
|
+
| TargetFeatureFlagMutationResult
|
|
55
|
+
| VerifiedWorkspaceFeatureFlagMutationResult;
|
|
56
|
+
|
|
42
57
|
export interface WorkspaceFeatureFlagMutationInput {
|
|
43
58
|
appId: string;
|
|
44
59
|
key: string;
|
|
@@ -72,8 +87,8 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
72
87
|
rules?: Record<string, unknown>;
|
|
73
88
|
enabledForEmail?: string;
|
|
74
89
|
},
|
|
75
|
-
):
|
|
76
|
-
const payload = body as Partial<
|
|
90
|
+
): TargetFeatureFlagMutationResult {
|
|
91
|
+
const payload = body as Partial<TargetFeatureFlagMutationResult> | null;
|
|
77
92
|
const valid =
|
|
78
93
|
payload?.contractVersion === 2 &&
|
|
79
94
|
payload.status === "ready" &&
|
|
@@ -144,7 +159,7 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
144
159
|
"The target app did not enable the feature flag for the delegated operator.",
|
|
145
160
|
);
|
|
146
161
|
}
|
|
147
|
-
return payload as
|
|
162
|
+
return payload as TargetFeatureFlagMutationResult;
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
function targetOrigin(app: OrgApp): string {
|
|
@@ -173,6 +188,39 @@ async function delegatedToken(
|
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
|
|
191
|
+
export type WorkspaceFeatureFlagFailurePhase =
|
|
192
|
+
| "directory"
|
|
193
|
+
| "token-generation"
|
|
194
|
+
| "timeout"
|
|
195
|
+
| "network"
|
|
196
|
+
| "authorization"
|
|
197
|
+
| "unsupported-target"
|
|
198
|
+
| "target-action"
|
|
199
|
+
| "persistence"
|
|
200
|
+
| "verification";
|
|
201
|
+
|
|
202
|
+
const FAILURE_MESSAGES: Record<WorkspaceFeatureFlagFailurePhase, string> = {
|
|
203
|
+
directory: "The organization app directory could not resolve this target.",
|
|
204
|
+
"token-generation":
|
|
205
|
+
"Analytics could not create delegated feature flag authority.",
|
|
206
|
+
timeout: "The target app timed out before the feature flag change completed.",
|
|
207
|
+
network: "Analytics could not reach the target app.",
|
|
208
|
+
authorization: "The target app denied this delegated flag operation.",
|
|
209
|
+
"unsupported-target":
|
|
210
|
+
"The target app does not support feature flag management.",
|
|
211
|
+
"target-action": "The target app could not complete the feature flag action.",
|
|
212
|
+
persistence:
|
|
213
|
+
"The target app did not persist the requested feature flag rules.",
|
|
214
|
+
verification: "Analytics could not verify the persisted feature flag change.",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export class WorkspaceFeatureFlagFailure extends Error {
|
|
218
|
+
constructor(readonly phase: WorkspaceFeatureFlagFailurePhase) {
|
|
219
|
+
super(`[${phase}] ${FAILURE_MESSAGES[phase]}`);
|
|
220
|
+
this.name = "WorkspaceFeatureFlagFailure";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
176
224
|
type TargetFailureReason = "token-generation" | "timeout" | "network";
|
|
177
225
|
|
|
178
226
|
class TargetCallFailure extends Error {
|
|
@@ -194,6 +242,30 @@ export function classifyWorkspaceFeatureFlagTargetFailure(
|
|
|
194
242
|
return "network";
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
246
|
+
const reason = classifyWorkspaceFeatureFlagTargetFailure(error);
|
|
247
|
+
return new WorkspaceFeatureFlagFailure(reason);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function resolveTargetApp(
|
|
251
|
+
admin: AnalyticsAdminContext,
|
|
252
|
+
appId: string,
|
|
253
|
+
): Promise<OrgApp> {
|
|
254
|
+
let apps: OrgApp[];
|
|
255
|
+
try {
|
|
256
|
+
apps = await fetchOrgApps({
|
|
257
|
+
selfId: "analytics",
|
|
258
|
+
includeDirectoryApp: true,
|
|
259
|
+
serviceOrgId: admin.orgId,
|
|
260
|
+
});
|
|
261
|
+
} catch {
|
|
262
|
+
throw new WorkspaceFeatureFlagFailure("directory");
|
|
263
|
+
}
|
|
264
|
+
const app = apps.find((candidate) => candidate.id === appId);
|
|
265
|
+
if (!app) throw new WorkspaceFeatureFlagFailure("directory");
|
|
266
|
+
return app;
|
|
267
|
+
}
|
|
268
|
+
|
|
197
269
|
async function callTarget(
|
|
198
270
|
app: OrgApp,
|
|
199
271
|
admin: AnalyticsAdminContext,
|
|
@@ -326,63 +398,139 @@ export async function setWorkspaceFeatureFlag(
|
|
|
326
398
|
admin: AnalyticsAdminContext,
|
|
327
399
|
input: WorkspaceFeatureFlagMutationInput,
|
|
328
400
|
): Promise<WorkspaceFeatureFlagMutationResult> {
|
|
329
|
-
const
|
|
330
|
-
selfId: "analytics",
|
|
331
|
-
includeDirectoryApp: true,
|
|
332
|
-
serviceOrgId: admin.orgId,
|
|
333
|
-
});
|
|
334
|
-
const app = apps.find((candidate) => candidate.id === input.appId);
|
|
335
|
-
if (!app)
|
|
336
|
-
throw new Error(
|
|
337
|
-
"The requested app is not available in this organization directory.",
|
|
338
|
-
);
|
|
401
|
+
const app = await resolveTargetApp(admin, input.appId);
|
|
339
402
|
const targetInput = workspaceFeatureFlagTargetInput(input);
|
|
340
403
|
let orgDomain: string | undefined;
|
|
341
404
|
try {
|
|
342
405
|
orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
|
|
343
406
|
} catch {
|
|
344
|
-
throw new
|
|
407
|
+
throw new WorkspaceFeatureFlagFailure("token-generation");
|
|
408
|
+
}
|
|
409
|
+
if (!orgDomain) throw new WorkspaceFeatureFlagFailure("token-generation");
|
|
410
|
+
let result: Awaited<ReturnType<typeof callTarget>>;
|
|
411
|
+
try {
|
|
412
|
+
result = await callTarget(
|
|
413
|
+
app,
|
|
414
|
+
admin,
|
|
415
|
+
"set-feature-flag",
|
|
416
|
+
targetInput,
|
|
417
|
+
orgDomain,
|
|
418
|
+
);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw targetFailure(error);
|
|
345
421
|
}
|
|
346
|
-
if (!orgDomain) throw new TargetCallFailure("token-generation");
|
|
347
|
-
const result = await callTarget(
|
|
348
|
-
app,
|
|
349
|
-
admin,
|
|
350
|
-
"set-feature-flag",
|
|
351
|
-
targetInput,
|
|
352
|
-
orgDomain,
|
|
353
|
-
);
|
|
354
422
|
if (result.status === 401 || result.status === 403)
|
|
355
|
-
throw new
|
|
423
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
356
424
|
if (result.status === 404 || result.status === 405)
|
|
357
|
-
throw new
|
|
425
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
358
426
|
if (result.status < 200 || result.status >= 300)
|
|
359
|
-
throw new
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
? {
|
|
368
|
-
rules: {
|
|
369
|
-
mode: input.rules.mode,
|
|
370
|
-
emails: input.rules.emails ?? [],
|
|
371
|
-
orgIds: input.rules.orgIds ?? [],
|
|
372
|
-
percentage: input.rules.percentage ?? 0,
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
: input.operation === "off"
|
|
427
|
+
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
428
|
+
let mutation: TargetFeatureFlagMutationResult;
|
|
429
|
+
try {
|
|
430
|
+
mutation = validateWorkspaceFeatureFlagMutation(result.body, {
|
|
431
|
+
key: input.key,
|
|
432
|
+
orgDomain,
|
|
433
|
+
allowExplicitNoOrgTarget: true,
|
|
434
|
+
...(input.operation === "replace-rules" && input.rules
|
|
376
435
|
? {
|
|
377
436
|
rules: {
|
|
378
|
-
mode:
|
|
379
|
-
emails: [],
|
|
380
|
-
orgIds: [],
|
|
381
|
-
percentage: 0,
|
|
437
|
+
mode: input.rules.mode,
|
|
438
|
+
emails: input.rules.emails ?? [],
|
|
439
|
+
orgIds: input.rules.orgIds ?? [],
|
|
440
|
+
percentage: input.rules.percentage ?? 0,
|
|
382
441
|
},
|
|
383
442
|
}
|
|
384
|
-
:
|
|
385
|
-
|
|
443
|
+
: input.operation === "off"
|
|
444
|
+
? {
|
|
445
|
+
rules: {
|
|
446
|
+
mode: "off",
|
|
447
|
+
emails: [],
|
|
448
|
+
orgIds: [],
|
|
449
|
+
percentage: 0,
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
: { enabledForEmail: admin.userEmail }),
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
!(await isFeatureFlagEnabled(VERIFIED_FLEET_FLAG_MUTATIONS, {
|
|
460
|
+
userEmail: admin.userEmail,
|
|
461
|
+
userKey: admin.userEmail,
|
|
462
|
+
orgId: admin.orgId,
|
|
463
|
+
}))
|
|
464
|
+
) {
|
|
465
|
+
return mutation;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let readBack: Awaited<ReturnType<typeof callTarget>>;
|
|
469
|
+
try {
|
|
470
|
+
readBack = await callTarget(
|
|
471
|
+
app,
|
|
472
|
+
admin,
|
|
473
|
+
"list-feature-flags",
|
|
474
|
+
{},
|
|
475
|
+
orgDomain,
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw targetFailure(error);
|
|
479
|
+
}
|
|
480
|
+
if (readBack.status === 401 || readBack.status === 403)
|
|
481
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
482
|
+
if (readBack.status === 404 || readBack.status === 405)
|
|
483
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
484
|
+
if (readBack.status < 200 || readBack.status >= 300)
|
|
485
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
486
|
+
const verifiedApp = classifyWorkspaceFeatureFlagList(app, readBack);
|
|
487
|
+
if (verifiedApp.state === "forbidden")
|
|
488
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
489
|
+
if (verifiedApp.state === "unsupported")
|
|
490
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
491
|
+
const verifiedFlag = verifiedApp.flags.find((flag) => flag.key === input.key);
|
|
492
|
+
const expectedEnabled =
|
|
493
|
+
input.operation === "enable-for-current-user"
|
|
494
|
+
? true
|
|
495
|
+
: input.operation === "off"
|
|
496
|
+
? false
|
|
497
|
+
: null;
|
|
498
|
+
if (
|
|
499
|
+
verifiedApp.state !== "ready" ||
|
|
500
|
+
!verifiedFlag ||
|
|
501
|
+
typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
|
|
502
|
+
(expectedEnabled !== null &&
|
|
503
|
+
verifiedFlag.enabledForCurrentUser !== expectedEnabled)
|
|
504
|
+
) {
|
|
505
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
validateWorkspaceFeatureFlagMutation(
|
|
509
|
+
{
|
|
510
|
+
contractVersion: 2,
|
|
511
|
+
status: "ready",
|
|
512
|
+
key: input.key,
|
|
513
|
+
rules: verifiedFlag.rules,
|
|
514
|
+
scope: mutation.scope,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
key: input.key,
|
|
518
|
+
orgDomain,
|
|
519
|
+
allowExplicitNoOrgTarget: true,
|
|
520
|
+
rules: mutation.rules,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
} catch {
|
|
524
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
contractVersion: 3,
|
|
528
|
+
status: "verified",
|
|
529
|
+
key: mutation.key,
|
|
530
|
+
rules: verifiedFlag.rules as Record<string, unknown>,
|
|
531
|
+
scope: mutation.scope,
|
|
532
|
+
enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
|
|
533
|
+
};
|
|
386
534
|
}
|
|
387
535
|
|
|
388
536
|
export async function getWorkspaceFlagTarget(
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
|
|
2
|
+
|
|
3
|
+
export const VERIFIED_FLEET_FLAG_MUTATIONS = defineFeatureFlag({
|
|
4
|
+
key: "analytics.verified-fleet-flag-mutations",
|
|
5
|
+
displayName: "Verified fleet flag mutations",
|
|
6
|
+
description:
|
|
7
|
+
"Verify cross-app feature flag changes with an independent target read-back.",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const ANALYTICS_FEATURE_FLAGS = [VERIFIED_FLEET_FLAG_MUTATIONS];
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@agent-native/core/client/hooks";
|
|
12
12
|
import { useT } from "@agent-native/core/client/i18n";
|
|
13
13
|
import { ShareButton } from "@agent-native/core/client/sharing";
|
|
14
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
14
15
|
import { CreativeContextShareSheet } from "@agent-native/creative-context/client";
|
|
15
16
|
import {
|
|
16
17
|
IconCheck,
|
|
@@ -311,7 +312,10 @@ function assetUpdatedTime(asset: any): number {
|
|
|
311
312
|
|
|
312
313
|
export function loader({ params, request }: LoaderFunctionArgs) {
|
|
313
314
|
const url = new URL(request.url);
|
|
314
|
-
return
|
|
315
|
+
return withSsrHtmlContentType(
|
|
316
|
+
redirect(`/library/${params.id}${url.search}`),
|
|
317
|
+
{ varyByQuery: true },
|
|
318
|
+
);
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
export default function BrandKitDetailRedirect() {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
export function loader({ request }: LoaderFunctionArgs) {
|
|
4
5
|
const url = new URL(request.url);
|
|
5
|
-
return redirect(`/library${url.search}`)
|
|
6
|
+
return withSsrHtmlContentType(redirect(`/library${url.search}`), {
|
|
7
|
+
varyByQuery: true,
|
|
8
|
+
});
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export default function BrandKitsIndexRedirect() {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
// Legacy redirect: brand containers now live in the unified Library workspace.
|
|
4
5
|
export function loader({ request }: LoaderFunctionArgs) {
|
|
5
6
|
const url = new URL(request.url);
|
|
6
|
-
return redirect(`/library${url.search}`)
|
|
7
|
+
return withSsrHtmlContentType(redirect(`/library${url.search}`), {
|
|
8
|
+
varyByQuery: true,
|
|
9
|
+
});
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export default function LibrariesRedirect() {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
3
|
|
|
3
4
|
// Legacy redirect: the image browser moved from "Picker" (/picker) to
|
|
4
5
|
// "Library" (/library). Preserve any query string so deep links keep working.
|
|
5
6
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
6
|
-
return redirect(`/library${url.search}`)
|
|
7
|
+
return withSsrHtmlContentType(redirect(`/library${url.search}`), {
|
|
8
|
+
varyByQuery: true,
|
|
9
|
+
});
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export default function PickerRedirect() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect } from "react-router";
|
|
2
3
|
|
|
3
4
|
export function loader() {
|
|
4
|
-
return redirect("/dictate");
|
|
5
|
+
return withSsrHtmlContentType(redirect("/dictate"));
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export default function LegacyWisprRedirect() {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { DefaultSpinner } from "@agent-native/core/client/ui";
|
|
2
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
2
3
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
3
4
|
|
|
4
5
|
const SEO_TITLE = "Clips - Open Source screen recorder";
|
|
5
6
|
const SEO_DESCRIPTION =
|
|
6
7
|
"Open Source screen recorder and meeting-notes app with AI transcripts, summaries, search, dictation, and agent-readable share links.";
|
|
7
|
-
const SSR_REDIRECT_CONTENT_TYPE = "text/html; charset=utf-8";
|
|
8
|
-
|
|
9
8
|
export function meta() {
|
|
10
9
|
return [
|
|
11
10
|
{ title: SEO_TITLE },
|
|
@@ -39,18 +38,15 @@ function buildTarget(url: URL): string {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// even though the /library shell is already cached at the edge.
|
|
46
|
-
response.headers.set("content-type", SSR_REDIRECT_CONTENT_TYPE);
|
|
47
|
-
throw response;
|
|
41
|
+
throw withSsrHtmlContentType(redirect(buildTarget(url)), {
|
|
42
|
+
varyByQuery: true,
|
|
43
|
+
});
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
throw withSsrHtmlContentType(redirect(buildTarget(url)), {
|
|
48
|
+
varyByQuery: true,
|
|
49
|
+
});
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
export function HydrateFallback() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect } from "react-router";
|
|
2
3
|
|
|
3
4
|
export function loader() {
|
|
4
|
-
return redirect("/", 302);
|
|
5
|
+
return withSsrHtmlContentType(redirect("/", 302));
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export default function ExamplesRedirect() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DefaultSpinner } from "@agent-native/core/client/ui";
|
|
2
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
2
3
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
3
4
|
|
|
4
5
|
import messages from "@/i18n/en-US";
|
|
@@ -11,11 +12,11 @@ function target(url: URL): string {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function loader({ url }: LoaderFunctionArgs) {
|
|
14
|
-
throw redirect(target(url));
|
|
15
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function clientLoader({ url }: LoaderFunctionArgs) {
|
|
18
|
-
throw redirect(target(url));
|
|
19
|
+
throw withSsrHtmlContentType(redirect(target(url)), { varyByQuery: true });
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function meta() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect } from "react-router";
|
|
2
3
|
|
|
3
4
|
import { Spinner } from "@/components/ui/spinner";
|
|
@@ -31,11 +32,11 @@ export function meta() {
|
|
|
31
32
|
* hydrates. The app opens to the Important triage tab by default.
|
|
32
33
|
*/
|
|
33
34
|
export function loader() {
|
|
34
|
-
throw redirect("/inbox?label=important");
|
|
35
|
+
throw withSsrHtmlContentType(redirect("/inbox?label=important"));
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function clientLoader() {
|
|
38
|
-
throw redirect("/inbox?label=important");
|
|
39
|
+
throw withSsrHtmlContentType(redirect("/inbox?label=important"));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function HydrateFallback() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSsrHtmlContentType } from "@agent-native/core/shared";
|
|
1
2
|
import { redirect } from "react-router";
|
|
2
3
|
|
|
3
4
|
import { APP_TITLE } from "@/lib/app-config";
|
|
@@ -14,7 +15,7 @@ export function meta() {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function loader() {
|
|
17
|
-
return redirect("/tasks");
|
|
18
|
+
return withSsrHtmlContentType(redirect("/tasks"));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export default function IndexRedirect() {
|
|
@@ -14,9 +14,9 @@ import { buildMergedConfig, McpClientManager, mcpToolsToActionEntries, } from ".
|
|
|
14
14
|
import { readAgentsBundleFromFs, generateDevelopmentSkillsPromptBlock, } from "../server/agents-bundle.js";
|
|
15
15
|
import { getAmbientOrgId, getAmbientUserEmail, runWithRequestContext, } from "../server/request-context.js";
|
|
16
16
|
import { isReasoningEffort, } from "../shared/reasoning-effort.js";
|
|
17
|
-
import { createCodeAgentAgentTools } from "./code-agent-agent-tools.js";
|
|
18
17
|
import { createAgentNativeConfigContext, loadResolvedAgentNativeConfig, } from "../vite/agent-native-config-loader.js";
|
|
19
18
|
import { runClaudeCodeParticipant, } from "./claude-code-participant.js";
|
|
19
|
+
import { createCodeAgentAgentTools } from "./code-agent-agent-tools.js";
|
|
20
20
|
import { codexMcpConfigArgs, mergeCodeAgentMcpConfig, restrictCodeAgentMcpConfig, } from "./code-agent-mcp-config.js";
|
|
21
21
|
import { createCodeAgentOutputSmoother } from "./code-agent-output-smoother.js";
|
|
22
22
|
import { addCodeAgentCommandToAllowlist, appendCodeAgentTranscriptEvent, dequeueCodeAgentFollowUp, getCodeAgentRunRecord, isCodeAgentCommandAllowed, listCodeAgentTranscriptEvents, updateCodeAgentRunRecord, } from "./code-agent-runs.js";
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
package/dist/deploy/build.js
CHANGED
|
@@ -23,7 +23,7 @@ import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTE
|
|
|
23
23
|
import { isValidCron } from "../jobs/cron.js";
|
|
24
24
|
import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
|
|
25
25
|
import { normalizeAppBasePath } from "../server/app-base-path.js";
|
|
26
|
-
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, } from "../shared/cache-control.js";
|
|
26
|
+
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, SSR_QUERY_CACHE_KEY_HEADER, } from "../shared/cache-control.js";
|
|
27
27
|
import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
|
|
28
28
|
import { isTruthyRuntimeValue } from "../shared/runtime-config.js";
|
|
29
29
|
import { AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, } from "../shared/social-meta.js";
|
|
@@ -1107,6 +1107,7 @@ function injectHeadScript(html, script) {
|
|
|
1107
1107
|
// Resolved from AGENT_NATIVE_SSR_CACHE at build time.
|
|
1108
1108
|
const SSR_CACHE_HEADERS = ${JSON.stringify(ssrCacheHeaders)};
|
|
1109
1109
|
const SSR_CACHE_KEY_HEADERS = ${JSON.stringify(ssrCacheKeyHeaders)};
|
|
1110
|
+
const SSR_QUERY_CACHE_KEY_HEADER = ${JSON.stringify(SSR_QUERY_CACHE_KEY_HEADER)};
|
|
1110
1111
|
const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
|
|
1111
1112
|
const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
|
|
1112
1113
|
const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify([...new Set(immutableAssetPaths)].sort())});
|
|
@@ -1174,6 +1175,9 @@ function isSsrHtmlOrDataResponse(headers, status, pathname) {
|
|
|
1174
1175
|
* from the canonical Nitro/Netlify handler or send normal pages to origin.
|
|
1175
1176
|
*/
|
|
1176
1177
|
function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
1178
|
+
const varyByQuery =
|
|
1179
|
+
(headers.get(SSR_QUERY_CACHE_KEY_HEADER) || "").trim().toLowerCase() === "query";
|
|
1180
|
+
headers.delete(SSR_QUERY_CACHE_KEY_HEADER);
|
|
1177
1181
|
if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
|
|
1178
1182
|
|
|
1179
1183
|
headers.delete("set-cookie");
|
|
@@ -1193,9 +1197,11 @@ function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
|
1193
1197
|
for (const [name, value] of Object.entries(SSR_CACHE_HEADERS)) {
|
|
1194
1198
|
headers.set(name, value);
|
|
1195
1199
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1200
|
+
const netlifyVary = varyByQuery
|
|
1201
|
+
? SSR_CACHE_KEY_HEADERS["netlify-vary"] ? "query" : undefined
|
|
1202
|
+
: SSR_CACHE_KEY_HEADERS["netlify-vary"];
|
|
1203
|
+
if (netlifyVary) headers.set("netlify-vary", netlifyVary);
|
|
1204
|
+
else headers.delete("netlify-vary");
|
|
1199
1205
|
}
|
|
1200
1206
|
|
|
1201
1207
|
function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* DELETE /_agent-native/notifications/:id — delete
|
|
12
12
|
*/
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
|
-
error?: undefined;
|
|
15
14
|
count: number;
|
|
16
15
|
updated?: undefined;
|
|
16
|
+
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
error?: undefined;
|
|
20
19
|
count?: undefined;
|
|
21
20
|
updated: number;
|
|
21
|
+
error?: undefined;
|
|
22
22
|
ok?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
error: string;
|
|
27
27
|
ok?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
count?: undefined;
|
|
31
30
|
updated?: undefined;
|
|
31
|
+
error?: undefined;
|
|
32
32
|
ok: boolean;
|
|
33
33
|
}>>;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
|
@@ -18,7 +18,7 @@ import { defineEventHandler } from "h3";
|
|
|
18
18
|
*/
|
|
19
19
|
import { createRequestHandler } from "react-router";
|
|
20
20
|
import { isMcpPublicPath } from "../mcp/route-paths.js";
|
|
21
|
-
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, } from "../shared/cache-control.js";
|
|
21
|
+
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, resolveSsrCacheKeyHeaders, SSR_QUERY_CACHE_KEY_HEADER, } from "../shared/cache-control.js";
|
|
22
22
|
import { AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, withAgentNativeSocialImageCacheBuster, } from "../shared/social-meta.js";
|
|
23
23
|
import { getAppBasePathFromViteEnv, stripAppBasePath as canonicalStripAppBasePath, } from "./app-base-path.js";
|
|
24
24
|
import { getAppOriginClientConfigScript } from "./app-origin-config.js";
|
|
@@ -204,6 +204,8 @@ function isSsrHtmlOrDataResponse(headers, status, pathname) {
|
|
|
204
204
|
* that contradicts that.
|
|
205
205
|
*/
|
|
206
206
|
function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
207
|
+
const varyByQuery = headers.get(SSR_QUERY_CACHE_KEY_HEADER)?.trim().toLowerCase() === "query";
|
|
208
|
+
headers.delete(SSR_QUERY_CACHE_KEY_HEADER);
|
|
207
209
|
if (!isSsrHtmlOrDataResponse(headers, status, pathname))
|
|
208
210
|
return;
|
|
209
211
|
// A public shell must never set a viewer cookie or vary by credentials.
|
|
@@ -234,9 +236,16 @@ function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
|
234
236
|
for (const [name, value] of Object.entries(resolveSsrCacheHeaders())) {
|
|
235
237
|
headers.set(name, value);
|
|
236
238
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
const cacheKeyHeaders = resolveSsrCacheKeyHeaders();
|
|
240
|
+
const netlifyVary = varyByQuery
|
|
241
|
+
? cacheKeyHeaders["netlify-vary"]
|
|
242
|
+
? "query"
|
|
243
|
+
: undefined
|
|
244
|
+
: cacheKeyHeaders["netlify-vary"];
|
|
245
|
+
if (netlifyVary)
|
|
246
|
+
headers.set("netlify-vary", netlifyVary);
|
|
247
|
+
else
|
|
248
|
+
headers.delete("netlify-vary");
|
|
240
249
|
}
|
|
241
250
|
function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
|
|
242
251
|
if (status < 200 || status >= 400)
|
|
@@ -7,6 +7,28 @@ export declare const DEFAULT_SSR_CACHE_HEADERS: {
|
|
|
7
7
|
readonly "cdn-cache-control": "public, max-age=600, stale-while-revalidate=604800, stale-if-error=3600";
|
|
8
8
|
readonly "netlify-cdn-cache-control": "public, durable, s-maxage=31536000, stale-while-revalidate=604800, stale-if-error=3600";
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Content type used by impersonal HTML redirects that should enter the
|
|
12
|
+
* deployment adapter's shared SSR cache path.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SSR_HTML_CONTENT_TYPE = "text/html; charset=utf-8";
|
|
15
|
+
/**
|
|
16
|
+
* Internal response marker for public HTML whose redirect target preserves
|
|
17
|
+
* request query parameters. The SSR adapters consume this marker and emit a
|
|
18
|
+
* provider-specific full-query cache key only where the provider supports it.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SSR_QUERY_CACHE_KEY_HEADER = "x-agent-native-ssr-key";
|
|
21
|
+
export type SsrHtmlContentTypeOptions = {
|
|
22
|
+
varyByQuery?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Mark an already-built HTML response as HTML without changing its redirect
|
|
26
|
+
* status, location, or other headers. Only use this for redirects whose
|
|
27
|
+
* target is independent of the viewer and request credentials. Set
|
|
28
|
+
* `varyByQuery` when the target preserves request query parameters so the SSR
|
|
29
|
+
* adapter can request a full-query cache key on providers that support it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withSsrHtmlContentType<T extends Response>(response: T, options?: SsrHtmlContentTypeOptions): T;
|
|
10
32
|
/**
|
|
11
33
|
* Deployment-wide override for the SSR shell cache policy.
|
|
12
34
|
*
|
|
@@ -7,6 +7,31 @@ export const DEFAULT_SSR_CACHE_HEADERS = {
|
|
|
7
7
|
"cdn-cache-control": DEFAULT_SSR_CDN_CACHE_CONTROL,
|
|
8
8
|
"netlify-cdn-cache-control": DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL,
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Content type used by impersonal HTML redirects that should enter the
|
|
12
|
+
* deployment adapter's shared SSR cache path.
|
|
13
|
+
*/
|
|
14
|
+
export const SSR_HTML_CONTENT_TYPE = "text/html; charset=utf-8";
|
|
15
|
+
/**
|
|
16
|
+
* Internal response marker for public HTML whose redirect target preserves
|
|
17
|
+
* request query parameters. The SSR adapters consume this marker and emit a
|
|
18
|
+
* provider-specific full-query cache key only where the provider supports it.
|
|
19
|
+
*/
|
|
20
|
+
export const SSR_QUERY_CACHE_KEY_HEADER = "x-agent-native-ssr-key";
|
|
21
|
+
/**
|
|
22
|
+
* Mark an already-built HTML response as HTML without changing its redirect
|
|
23
|
+
* status, location, or other headers. Only use this for redirects whose
|
|
24
|
+
* target is independent of the viewer and request credentials. Set
|
|
25
|
+
* `varyByQuery` when the target preserves request query parameters so the SSR
|
|
26
|
+
* adapter can request a full-query cache key on providers that support it.
|
|
27
|
+
*/
|
|
28
|
+
export function withSsrHtmlContentType(response, options = {}) {
|
|
29
|
+
response.headers.set("content-type", SSR_HTML_CONTENT_TYPE);
|
|
30
|
+
if (options.varyByQuery) {
|
|
31
|
+
response.headers.set(SSR_QUERY_CACHE_KEY_HEADER, "query");
|
|
32
|
+
}
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
10
35
|
/**
|
|
11
36
|
* Deployment-wide override for the SSR shell cache policy.
|
|
12
37
|
*
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { truncate } from "./truncate.js";
|
|
|
6
6
|
export { isHumanReadableDocumentTitle, normalizeDocumentTitle, } from "./document-title.js";
|
|
7
7
|
export { injectDocumentMarkup } from "./html-document.js";
|
|
8
8
|
export { withBuilderUtmTrackingParams } from "./builder-link-tracking.js";
|
|
9
|
+
export { SSR_HTML_CONTENT_TYPE, SSR_QUERY_CACHE_KEY_HEADER, type SsrHtmlContentTypeOptions, withSsrHtmlContentType, } from "./cache-control.js";
|
|
9
10
|
export { AGENT_NATIVE_DOCS_ORIGIN, docsUrl, type DocsUrlOptions, } from "./docs-url.js";
|
|
10
11
|
export { buildRuntimeConfigPrompt, formatRuntimeConfigReport, getRuntimeConfigReport, parseRuntimeConfigReport, runtimeConfigRequirementsFromSearchParams, type RuntimeConfigEnvironment, type RuntimeConfigIssue, type RuntimeConfigIssueCode, type RuntimeConfigIssueSeverity, type RuntimeConfigPhase, type RuntimeConfigReport, type RuntimeConfigRequirements, } from "./runtime-config.js";
|
|
11
12
|
export { llmConnectionTrackingProperties, normalizeLlmConnection, type LlmConnectionStatus, } from "./llm-connection.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { truncate } from "./truncate.js";
|
|
|
6
6
|
export { isHumanReadableDocumentTitle, normalizeDocumentTitle, } from "./document-title.js";
|
|
7
7
|
export { injectDocumentMarkup } from "./html-document.js";
|
|
8
8
|
export { withBuilderUtmTrackingParams } from "./builder-link-tracking.js";
|
|
9
|
+
export { SSR_HTML_CONTENT_TYPE, SSR_QUERY_CACHE_KEY_HEADER, withSsrHtmlContentType, } from "./cache-control.js";
|
|
9
10
|
export { AGENT_NATIVE_DOCS_ORIGIN, docsUrl, } from "./docs-url.js";
|
|
10
11
|
export { buildRuntimeConfigPrompt, formatRuntimeConfigReport, getRuntimeConfigReport, parseRuntimeConfigReport, runtimeConfigRequirementsFromSearchParams, } from "./runtime-config.js";
|
|
11
12
|
export { llmConnectionTrackingProperties, normalizeLlmConnection, } from "./llm-connection.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.164.
|
|
3
|
+
"version": "0.164.3",
|
|
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": {
|