@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.31
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/CHANGELOG.md +15 -60
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +12 -13
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/import-agent.d.ts +13 -13
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
- package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
- package/dist/actions/inspect-connect-agent-card.js +15 -0
- package/dist/actions/inspect-connect-agent-card.js.map +1 -0
- package/dist/actions/install-curated-community-app.d.ts +36 -0
- package/dist/actions/install-curated-community-app.d.ts.map +1 -0
- package/dist/actions/install-curated-community-app.js +26 -0
- package/dist/actions/install-curated-community-app.js.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.js +15 -0
- package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
- package/dist/actions/list-connected-agents.d.ts +2 -0
- package/dist/actions/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-curated-community-apps.d.ts +3 -0
- package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
- package/dist/actions/list-curated-community-apps.js +10 -0
- package/dist/actions/list-curated-community-apps.js.map +1 -0
- package/dist/actions/list-workspace-connections.d.ts.map +1 -1
- package/dist/actions/list-workspace-connections.js +13 -18
- package/dist/actions/list-workspace-connections.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +6 -0
- package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
- package/dist/actions/update-workspace-app-metadata.js +26 -5
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/action-query-error.d.ts +2 -1
- package/dist/components/action-query-error.d.ts.map +1 -1
- package/dist/components/action-query-error.js +25 -2
- package/dist/components/action-query-error.js.map +1 -1
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +2 -2
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/community-apps-section.d.ts +2 -0
- package/dist/components/community-apps-section.d.ts.map +1 -0
- package/dist/components/community-apps-section.js +90 -0
- package/dist/components/community-apps-section.js.map +1 -0
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +7 -0
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +63 -9
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/connect-apps.d.ts +1 -2
- package/dist/lib/connect-apps.d.ts.map +1 -1
- package/dist/lib/connect-apps.js +41 -65
- package/dist/lib/connect-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +1 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +3 -2
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/connect.d.ts.map +1 -1
- package/dist/routes/pages/connect.js +5 -7
- package/dist/routes/pages/connect.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +11 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +379 -92
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.d.ts +45 -0
- package/dist/server/lib/community-apps.d.ts.map +1 -0
- package/dist/server/lib/community-apps.js +267 -0
- package/dist/server/lib/community-apps.js.map +1 -0
- package/dist/server/lib/community-source.d.ts +19 -0
- package/dist/server/lib/community-source.d.ts.map +1 -0
- package/dist/server/lib/community-source.js +118 -0
- package/dist/server/lib/community-source.js.map +1 -0
- package/dist/server/lib/connect-apps.d.ts +4 -0
- package/dist/server/lib/connect-apps.d.ts.map +1 -0
- package/dist/server/lib/connect-apps.js +99 -0
- package/dist/server/lib/connect-apps.js.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.js +44 -0
- package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/labs.d.ts +2 -0
- package/dist/shared/labs.d.ts.map +1 -0
- package/dist/shared/labs.js +8 -0
- package/dist/shared/labs.js.map +1 -0
- package/package.json +5 -4
- package/src/actions/connect-external-agent.spec.ts +101 -3
- package/src/actions/connect-external-agent.ts +12 -11
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +8 -0
- package/src/actions/inspect-connect-agent-card.ts +17 -0
- package/src/actions/install-curated-community-app.ts +32 -0
- package/src/actions/list-connect-marketplace-apps.ts +17 -0
- package/src/actions/list-connected-agents.spec.ts +17 -1
- package/src/actions/list-connected-agents.ts +7 -2
- package/src/actions/list-curated-community-apps.ts +12 -0
- package/src/actions/list-workspace-connections.spec.ts +14 -3
- package/src/actions/list-workspace-connections.ts +13 -17
- package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
- package/src/actions/update-workspace-app-metadata.ts +48 -24
- package/src/components/action-query-error.spec.tsx +20 -0
- package/src/components/action-query-error.tsx +40 -0
- package/src/components/chat-home-apps.spec.tsx +32 -1
- package/src/components/chat-home-apps.tsx +6 -1
- package/src/components/community-apps-section.tsx +224 -0
- package/src/components/dispatch-control-plane.tsx +1 -0
- package/src/components/layout/Layout.tsx +9 -0
- package/src/components/workspace-app-card.spec.tsx +226 -1
- package/src/components/workspace-app-card.tsx +115 -12
- package/src/lib/connect-apps.spec.ts +118 -66
- package/src/lib/connect-apps.ts +47 -82
- package/src/lib/workspace-apps.ts +1 -0
- package/src/routes/pages/$appId.tsx +1 -0
- package/src/routes/pages/apps.spec.tsx +227 -3
- package/src/routes/pages/apps.tsx +5 -0
- package/src/routes/pages/connect.spec.tsx +53 -6
- package/src/routes/pages/connect.tsx +14 -11
- package/src/server/index.ts +9 -0
- package/src/server/lib/app-creation-store.spec.ts +820 -16
- package/src/server/lib/app-creation-store.ts +504 -101
- package/src/server/lib/community-apps.spec.ts +367 -0
- package/src/server/lib/community-apps.ts +372 -0
- package/src/server/lib/community-source.spec.ts +151 -0
- package/src/server/lib/community-source.ts +186 -0
- package/src/server/lib/connect-apps.ts +149 -0
- package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
- package/src/server/lib/org-apps-directory-cache.ts +66 -0
- package/src/server/plugins/integrations.spec.ts +60 -0
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/labs.ts +9 -0
package/src/lib/connect-apps.ts
CHANGED
|
@@ -14,28 +14,58 @@ export interface MarketplaceApp {
|
|
|
14
14
|
source?: "first-party" | "community";
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function isPrivateIpv4(hostname: string): boolean {
|
|
18
|
+
const octets = hostname.split(".").map(Number);
|
|
19
|
+
if (
|
|
20
|
+
octets.length !== 4 ||
|
|
21
|
+
octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)
|
|
22
|
+
) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const [first, second] = octets;
|
|
26
|
+
return (
|
|
27
|
+
first === 0 ||
|
|
28
|
+
first === 10 ||
|
|
29
|
+
first === 127 ||
|
|
30
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
31
|
+
(first === 169 && second === 254) ||
|
|
32
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
33
|
+
(first === 192 && second === 168)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mappedIpv4(hostname: string): string | null {
|
|
38
|
+
const match = /^::ffff:(.*)$/i.exec(hostname);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
if (match[1].includes(".")) return match[1];
|
|
41
|
+
const words = match[1].split(":");
|
|
42
|
+
if (
|
|
43
|
+
words.length !== 2 ||
|
|
44
|
+
words.some((word) => !/^[\da-f]{1,4}$/i.test(word))
|
|
45
|
+
) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const high = Number.parseInt(words[0], 16);
|
|
49
|
+
const low = Number.parseInt(words[1], 16);
|
|
50
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
17
53
|
export function normalizeConnectUrl(value: string): URL | null {
|
|
18
54
|
try {
|
|
19
55
|
const url = new URL(value.trim());
|
|
20
56
|
const hostname = url.hostname.toLowerCase();
|
|
21
57
|
const ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
22
|
-
const privateIpv4 =
|
|
23
|
-
|
|
24
|
-
hostname,
|
|
25
|
-
) ||
|
|
26
|
-
hostname === "0.0.0.0" ||
|
|
27
|
-
/^100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(hostname);
|
|
58
|
+
const privateIpv4 = isPrivateIpv4(hostname);
|
|
59
|
+
const mappedAddress = mappedIpv4(ipv6);
|
|
28
60
|
const privateIpv6 =
|
|
29
|
-
ipv6
|
|
30
|
-
ipv6 === "::
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
61
|
+
ipv6.includes(":") &&
|
|
62
|
+
(ipv6 === "::" ||
|
|
63
|
+
ipv6 === "::1" ||
|
|
64
|
+
/^(?:fc|fd)/.test(ipv6) ||
|
|
65
|
+
/^fe[89ab]/.test(ipv6) ||
|
|
66
|
+
Boolean(mappedAddress && isPrivateIpv4(mappedAddress)));
|
|
36
67
|
if (
|
|
37
|
-
|
|
38
|
-
!(url.protocol === "http:" && hostname === "localhost")) ||
|
|
68
|
+
url.protocol !== "https:" ||
|
|
39
69
|
(hostname !== "localhost" && (privateIpv4 || privateIpv6)) ||
|
|
40
70
|
url.username ||
|
|
41
71
|
url.password
|
|
@@ -103,73 +133,8 @@ export function buildIdentityConnectUrl(appUrl: string): string {
|
|
|
103
133
|
return url.toString();
|
|
104
134
|
}
|
|
105
135
|
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
): Promise<MarketplaceApp[]> {
|
|
109
|
-
const response = await fetch(feedUrl, {
|
|
110
|
-
headers: { accept: "application/json" },
|
|
111
|
-
signal: AbortSignal.timeout(5_000),
|
|
112
|
-
});
|
|
113
|
-
if (!response.ok)
|
|
114
|
-
throw new Error(`Marketplace feed returned ${response.status}`);
|
|
115
|
-
const length = Number(response.headers.get("content-length") ?? 0);
|
|
116
|
-
if (length > 1_000_000) throw new Error("Marketplace feed is too large");
|
|
117
|
-
const body = await response.text();
|
|
118
|
-
if (body.length > 1_000_000) throw new Error("Marketplace feed is too large");
|
|
119
|
-
const payload = JSON.parse(body) as { apps?: unknown };
|
|
120
|
-
if (!Array.isArray(payload.apps))
|
|
121
|
-
throw new Error("Marketplace feed is invalid");
|
|
122
|
-
const candidates = payload.apps.filter((entry): entry is MarketplaceApp => {
|
|
123
|
-
if (!entry || typeof entry !== "object") return false;
|
|
124
|
-
const app = entry as Record<string, unknown>;
|
|
125
|
-
const url =
|
|
126
|
-
typeof app.url === "string" ? normalizeConnectUrl(app.url) : null;
|
|
127
|
-
return (
|
|
128
|
-
typeof app.id === "string" &&
|
|
129
|
-
typeof app.name === "string" &&
|
|
130
|
-
typeof app.description === "string" &&
|
|
131
|
-
typeof app.url === "string" &&
|
|
132
|
-
Boolean(url) &&
|
|
133
|
-
Array.isArray(app.capabilities) &&
|
|
134
|
-
app.capabilities.every((capability) => typeof capability === "string")
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
const verified = await Promise.allSettled(
|
|
138
|
-
candidates.map(async (app) => {
|
|
139
|
-
const card = await fetchConnectAgentCard(app.url);
|
|
140
|
-
return card.connect ? { ...app, capabilities: ["connect"] } : null;
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
return verified.flatMap((result) =>
|
|
144
|
-
result.status === "fulfilled" && result.value ? [result.value] : [],
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function fetchConnectAgentCard(
|
|
149
|
-
appUrl: string,
|
|
150
|
-
): Promise<ConnectAgentCard> {
|
|
151
|
-
const target = normalizeConnectUrl(appUrl);
|
|
152
|
-
if (!target) throw new Error("Enter a secure app URL.");
|
|
153
|
-
const response = await fetch(
|
|
154
|
-
appPathUrl(target.toString(), "/.well-known/agent-card.json").toString(),
|
|
155
|
-
{
|
|
156
|
-
headers: { accept: "application/json" },
|
|
157
|
-
signal: AbortSignal.timeout(5_000),
|
|
158
|
-
},
|
|
159
|
-
);
|
|
160
|
-
if (!response.ok) throw new Error(`Agent card returned ${response.status}`);
|
|
161
|
-
const length = Number(response.headers.get("content-length") ?? 0);
|
|
162
|
-
if (length > 256_000) throw new Error("Agent card is too large");
|
|
163
|
-
const body = await response.text();
|
|
164
|
-
if (body.length > 256_000) throw new Error("Agent card is too large");
|
|
165
|
-
const card = parseConnectAgentCard(
|
|
166
|
-
JSON.parse(body),
|
|
167
|
-
target.origin,
|
|
168
|
-
target.pathname,
|
|
169
|
-
);
|
|
170
|
-
if (!card)
|
|
171
|
-
throw new Error("This app has an invalid or incompatible agent card.");
|
|
172
|
-
return card;
|
|
136
|
+
export function buildAgentCardUrl(appUrl: string): string {
|
|
137
|
+
return appPathUrl(appUrl, "/.well-known/agent-card.json").toString();
|
|
173
138
|
}
|
|
174
139
|
|
|
175
140
|
function appPathUrl(appUrl: string, suffix: string): URL {
|
|
@@ -11,12 +11,18 @@ const queryState = vi.hoisted(() => ({
|
|
|
11
11
|
connectedApps: [] as Array<Record<string, unknown>>,
|
|
12
12
|
connectedAppsError: null as Error | null,
|
|
13
13
|
connectedAppsLoading: false,
|
|
14
|
+
communityAppsEnabled: false,
|
|
15
|
+
communityApps: [] as Array<Record<string, unknown>>,
|
|
16
|
+
installCommunityApp: vi.fn(),
|
|
17
|
+
installCommunityAppResult: null as Record<string, unknown> | null,
|
|
18
|
+
sendToAgentChat: vi.fn(),
|
|
14
19
|
}));
|
|
15
20
|
|
|
16
21
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
17
22
|
useActionQuery: (action: string) => {
|
|
18
23
|
const workspaceApps = action === "list-workspace-apps";
|
|
19
24
|
const connectedApps = action === "list-connected-agents";
|
|
25
|
+
const communityApps = action === "list-curated-community-apps";
|
|
20
26
|
const error = workspaceApps
|
|
21
27
|
? queryState.workspaceAppsError
|
|
22
28
|
: connectedApps
|
|
@@ -27,7 +33,9 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
27
33
|
? queryState.workspaceApps
|
|
28
34
|
: connectedApps
|
|
29
35
|
? queryState.connectedApps
|
|
30
|
-
:
|
|
36
|
+
: communityApps
|
|
37
|
+
? queryState.communityApps
|
|
38
|
+
: { name: null, displayName: null, appCount: 0 },
|
|
31
39
|
error,
|
|
32
40
|
isError: Boolean(error),
|
|
33
41
|
isLoading: workspaceApps
|
|
@@ -38,6 +46,30 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
38
46
|
refetch: vi.fn(),
|
|
39
47
|
};
|
|
40
48
|
},
|
|
49
|
+
useActionMutation: (
|
|
50
|
+
_action: string,
|
|
51
|
+
options?: { onSuccess?: (result: Record<string, unknown>) => void },
|
|
52
|
+
) => ({
|
|
53
|
+
mutate: (input: unknown) => {
|
|
54
|
+
queryState.installCommunityApp(input);
|
|
55
|
+
if (queryState.installCommunityAppResult) {
|
|
56
|
+
options?.onSuccess?.(queryState.installCommunityAppResult);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
isPending: false,
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
64
|
+
sendToAgentChat: queryState.sendToAgentChat,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
vi.mock("@agent-native/core/client/labs", () => ({
|
|
68
|
+
useLab: () => queryState.communityAppsEnabled,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
vi.mock("../../shared/labs.js", () => ({
|
|
72
|
+
COMMUNITY_APPS_LAB: { key: "dispatch.community-apps", defaultEnabled: false },
|
|
41
73
|
}));
|
|
42
74
|
|
|
43
75
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
@@ -48,8 +80,16 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
48
80
|
}));
|
|
49
81
|
|
|
50
82
|
vi.mock("../../components/action-query-error", () => ({
|
|
51
|
-
ActionQueryError: ({
|
|
52
|
-
|
|
83
|
+
ActionQueryError: ({
|
|
84
|
+
error,
|
|
85
|
+
queryName,
|
|
86
|
+
}: {
|
|
87
|
+
error: Error;
|
|
88
|
+
queryName?: string;
|
|
89
|
+
}) => (
|
|
90
|
+
<div role="alert">
|
|
91
|
+
{queryName}: {error.message}
|
|
92
|
+
</div>
|
|
53
93
|
),
|
|
54
94
|
}));
|
|
55
95
|
|
|
@@ -92,6 +132,11 @@ describe("Dispatch apps route", () => {
|
|
|
92
132
|
queryState.connectedApps = [];
|
|
93
133
|
queryState.connectedAppsError = null;
|
|
94
134
|
queryState.connectedAppsLoading = false;
|
|
135
|
+
queryState.communityAppsEnabled = false;
|
|
136
|
+
queryState.communityApps = [];
|
|
137
|
+
queryState.installCommunityApp.mockReset();
|
|
138
|
+
queryState.installCommunityAppResult = null;
|
|
139
|
+
queryState.sendToAgentChat.mockReset();
|
|
95
140
|
container = document.createElement("div");
|
|
96
141
|
document.body.appendChild(container);
|
|
97
142
|
root = createRoot(container);
|
|
@@ -129,4 +174,183 @@ describe("Dispatch apps route", () => {
|
|
|
129
174
|
|
|
130
175
|
expect(container.textContent).toContain("No workspace apps");
|
|
131
176
|
});
|
|
177
|
+
|
|
178
|
+
it("identifies each app discovery query when both fail", async () => {
|
|
179
|
+
queryState.workspaceAppsError = new Error("Workspace app list failed");
|
|
180
|
+
queryState.connectedAppsError = new Error("Connected agent list failed");
|
|
181
|
+
|
|
182
|
+
await renderAppsRoute();
|
|
183
|
+
|
|
184
|
+
const errors = Array.from(
|
|
185
|
+
container.querySelectorAll('[role="alert"]'),
|
|
186
|
+
(alert) => alert.textContent,
|
|
187
|
+
);
|
|
188
|
+
expect(errors).toContain("list-workspace-apps: Workspace app list failed");
|
|
189
|
+
expect(errors).toContain(
|
|
190
|
+
"list-connected-agents: Connected agent list failed",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("hides community apps while the Lab is off", async () => {
|
|
195
|
+
await renderAppsRoute();
|
|
196
|
+
|
|
197
|
+
expect(container.textContent).not.toContain("dispatch.pages.communityApps");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("lists the reviewed catalog and starts the selected install", async () => {
|
|
201
|
+
queryState.communityAppsEnabled = true;
|
|
202
|
+
queryState.communityApps = [
|
|
203
|
+
{
|
|
204
|
+
id: "community-qr",
|
|
205
|
+
name: "QR",
|
|
206
|
+
description: "Generate QR images.",
|
|
207
|
+
appId: "community-qr",
|
|
208
|
+
repo: "BuilderIO/agent-native",
|
|
209
|
+
ref: "main",
|
|
210
|
+
license: "MIT",
|
|
211
|
+
provenance: "Reviewed source",
|
|
212
|
+
templatePath: "templates/community/qr",
|
|
213
|
+
installed: false,
|
|
214
|
+
pending: false,
|
|
215
|
+
installable: true,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
await renderAppsRoute();
|
|
220
|
+
|
|
221
|
+
expect(container.textContent).toContain("QR");
|
|
222
|
+
expect(
|
|
223
|
+
container.querySelector<HTMLAnchorElement>("a[href*='github.com']")?.href,
|
|
224
|
+
).toContain("/tree/main/templates/community/qr");
|
|
225
|
+
const installButton = Array.from(
|
|
226
|
+
container.querySelectorAll<HTMLButtonElement>("button"),
|
|
227
|
+
).find((button) =>
|
|
228
|
+
button.textContent?.includes("dispatch.pages.communityAppsInstall"),
|
|
229
|
+
);
|
|
230
|
+
expect(installButton).toBeDefined();
|
|
231
|
+
await act(async () => installButton?.click());
|
|
232
|
+
expect(queryState.installCommunityApp).toHaveBeenCalledWith({
|
|
233
|
+
sourceId: "community-qr",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("sends verified source files to the local code agent", async () => {
|
|
238
|
+
queryState.communityAppsEnabled = true;
|
|
239
|
+
queryState.communityApps = [
|
|
240
|
+
{
|
|
241
|
+
id: "community-qr",
|
|
242
|
+
name: "QR",
|
|
243
|
+
description: "Generate QR images.",
|
|
244
|
+
appId: "community-qr",
|
|
245
|
+
repo: "BuilderIO/agent-native",
|
|
246
|
+
ref: "main",
|
|
247
|
+
license: "MIT",
|
|
248
|
+
provenance: "Reviewed source",
|
|
249
|
+
templatePath: "templates/community/qr",
|
|
250
|
+
installed: false,
|
|
251
|
+
pending: false,
|
|
252
|
+
installable: true,
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
queryState.installCommunityAppResult = {
|
|
256
|
+
mode: "local-agent",
|
|
257
|
+
prompt: "Create this app from the pinned files.",
|
|
258
|
+
attachments: [
|
|
259
|
+
{
|
|
260
|
+
type: "file",
|
|
261
|
+
name: "community-qr/README.md",
|
|
262
|
+
contentType: "text/plain",
|
|
263
|
+
text: "Verified source",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await renderAppsRoute();
|
|
269
|
+
const installButton = Array.from(
|
|
270
|
+
container.querySelectorAll<HTMLButtonElement>("button"),
|
|
271
|
+
).find((button) =>
|
|
272
|
+
button.textContent?.includes("dispatch.pages.communityAppsInstall"),
|
|
273
|
+
);
|
|
274
|
+
await act(async () => installButton?.click());
|
|
275
|
+
|
|
276
|
+
expect(queryState.sendToAgentChat).toHaveBeenCalledWith({
|
|
277
|
+
message: "Create this app from the pinned files.",
|
|
278
|
+
attachments: [
|
|
279
|
+
{
|
|
280
|
+
type: "file",
|
|
281
|
+
name: "community-qr/README.md",
|
|
282
|
+
contentType: "text/plain",
|
|
283
|
+
text: "Verified source",
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
submit: true,
|
|
287
|
+
type: "code",
|
|
288
|
+
newTab: true,
|
|
289
|
+
reuseEmptyTab: true,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("explains when a community app ID is already in use", async () => {
|
|
294
|
+
queryState.communityAppsEnabled = true;
|
|
295
|
+
queryState.communityApps = [
|
|
296
|
+
{
|
|
297
|
+
id: "community-qr",
|
|
298
|
+
name: "QR",
|
|
299
|
+
description: "Generate QR images.",
|
|
300
|
+
appId: "qr",
|
|
301
|
+
repo: "BuilderIO/agent-native",
|
|
302
|
+
ref: "main",
|
|
303
|
+
license: "MIT",
|
|
304
|
+
provenance: "Reviewed source",
|
|
305
|
+
templatePath: "templates/community/qr",
|
|
306
|
+
installed: false,
|
|
307
|
+
pending: false,
|
|
308
|
+
installable: false,
|
|
309
|
+
installUnavailableReason: "app-id-conflict",
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
await renderAppsRoute();
|
|
314
|
+
|
|
315
|
+
expect(container.textContent).toContain(
|
|
316
|
+
"dispatch.pages.communityAppsIdConflict",
|
|
317
|
+
);
|
|
318
|
+
expect(
|
|
319
|
+
Array.from(container.querySelectorAll("button")).some((button) =>
|
|
320
|
+
button.textContent?.includes("dispatch.pages.communityAppsInstall"),
|
|
321
|
+
),
|
|
322
|
+
).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("explains when Builder-mode community installs cannot be verified", async () => {
|
|
326
|
+
queryState.communityAppsEnabled = true;
|
|
327
|
+
queryState.communityApps = [
|
|
328
|
+
{
|
|
329
|
+
id: "community-qr",
|
|
330
|
+
name: "QR",
|
|
331
|
+
description: "Generate QR images.",
|
|
332
|
+
appId: "community-qr",
|
|
333
|
+
repo: "BuilderIO/agent-native",
|
|
334
|
+
ref: "main",
|
|
335
|
+
license: "MIT",
|
|
336
|
+
provenance: "Reviewed source",
|
|
337
|
+
templatePath: "templates/community/qr",
|
|
338
|
+
installed: false,
|
|
339
|
+
pending: false,
|
|
340
|
+
installable: false,
|
|
341
|
+
installUnavailableReason: "builder-install-unverified",
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
await renderAppsRoute();
|
|
346
|
+
|
|
347
|
+
expect(container.textContent).toContain(
|
|
348
|
+
"dispatch.pages.communityAppsBuilderInstallUnavailable",
|
|
349
|
+
);
|
|
350
|
+
expect(
|
|
351
|
+
Array.from(container.querySelectorAll("button")).some((button) =>
|
|
352
|
+
button.textContent?.includes("dispatch.pages.communityAppsInstall"),
|
|
353
|
+
),
|
|
354
|
+
).toBe(false);
|
|
355
|
+
});
|
|
132
356
|
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
APP_LIST_GRID_ROW_CLASS,
|
|
17
17
|
AppList,
|
|
18
18
|
} from "../../components/app-list-row";
|
|
19
|
+
import { CommunityAppsSection } from "../../components/community-apps-section";
|
|
19
20
|
import { ConnectedAppCard } from "../../components/connected-app-card";
|
|
20
21
|
import { CreateAppPopover } from "../../components/create-app-popover";
|
|
21
22
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
@@ -184,12 +185,14 @@ function AppsRoute() {
|
|
|
184
185
|
<ActionQueryError
|
|
185
186
|
error={appsQuery.error}
|
|
186
187
|
onRetry={() => void appsQuery.refetch()}
|
|
188
|
+
queryName="list-workspace-apps"
|
|
187
189
|
/>
|
|
188
190
|
) : null}
|
|
189
191
|
{connectedAppsQuery.isError ? (
|
|
190
192
|
<ActionQueryError
|
|
191
193
|
error={connectedAppsQuery.error}
|
|
192
194
|
onRetry={() => void connectedAppsQuery.refetch()}
|
|
195
|
+
queryName="list-connected-agents"
|
|
193
196
|
/>
|
|
194
197
|
) : null}
|
|
195
198
|
{showAppSkeletons ? (
|
|
@@ -346,6 +349,8 @@ function AppsRoute() {
|
|
|
346
349
|
</section>
|
|
347
350
|
</Collapsible>
|
|
348
351
|
) : null}
|
|
352
|
+
|
|
353
|
+
<CommunityAppsSection />
|
|
349
354
|
</div>
|
|
350
355
|
</DispatchShell>
|
|
351
356
|
);
|
|
@@ -8,7 +8,8 @@ import ConnectRoute from "./connect";
|
|
|
8
8
|
const connectState = vi.hoisted(() => ({
|
|
9
9
|
enabled: false,
|
|
10
10
|
status: "ready" as "loading" | "ready",
|
|
11
|
-
|
|
11
|
+
useActionQuery: vi.fn(() => ({ data: [], isLoading: false, isError: false })),
|
|
12
|
+
useActionMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
15
|
vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
@@ -23,8 +24,9 @@ vi.mock("../../components/dispatch-shell", () => ({
|
|
|
23
24
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
24
25
|
useT: () => (key: string) => key,
|
|
25
26
|
}));
|
|
26
|
-
vi.mock("@
|
|
27
|
-
|
|
27
|
+
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
28
|
+
useActionQuery: connectState.useActionQuery,
|
|
29
|
+
useActionMutation: connectState.useActionMutation,
|
|
28
30
|
}));
|
|
29
31
|
|
|
30
32
|
describe("ConnectRoute", () => {
|
|
@@ -34,7 +36,8 @@ describe("ConnectRoute", () => {
|
|
|
34
36
|
beforeEach(() => {
|
|
35
37
|
connectState.enabled = false;
|
|
36
38
|
connectState.status = "ready";
|
|
37
|
-
connectState.
|
|
39
|
+
connectState.useActionQuery.mockClear();
|
|
40
|
+
connectState.useActionMutation.mockClear();
|
|
38
41
|
container = document.createElement("div");
|
|
39
42
|
document.body.appendChild(container);
|
|
40
43
|
root = createRoot(container);
|
|
@@ -48,13 +51,57 @@ describe("ConnectRoute", () => {
|
|
|
48
51
|
it("renders nothing and does not fetch when the labs flag is off", async () => {
|
|
49
52
|
await act(async () => root.render(<ConnectRoute />));
|
|
50
53
|
expect(container.innerHTML).toBe("");
|
|
51
|
-
expect(connectState.
|
|
54
|
+
expect(connectState.useActionQuery).not.toHaveBeenCalled();
|
|
55
|
+
expect(connectState.useActionMutation).not.toHaveBeenCalled();
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
it("shows the page skeleton while the labs flag is loading", async () => {
|
|
55
59
|
connectState.status = "loading";
|
|
56
60
|
await act(async () => root.render(<ConnectRoute />));
|
|
57
61
|
expect(container.querySelector('[aria-busy="true"]')).not.toBeNull();
|
|
58
|
-
expect(connectState.
|
|
62
|
+
expect(connectState.useActionQuery).not.toHaveBeenCalled();
|
|
63
|
+
expect(connectState.useActionMutation).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("inspects a URL through the server action and renders its card", async () => {
|
|
67
|
+
connectState.enabled = true;
|
|
68
|
+
const mutateAsync = vi.fn().mockResolvedValue({
|
|
69
|
+
name: "Slides",
|
|
70
|
+
description: "A slide workspace",
|
|
71
|
+
url: "https://slides.example.com/_agent-native/a2a",
|
|
72
|
+
connect: true,
|
|
73
|
+
});
|
|
74
|
+
connectState.useActionMutation.mockReturnValue({ mutateAsync } as never);
|
|
75
|
+
|
|
76
|
+
await act(async () => root.render(<ConnectRoute />));
|
|
77
|
+
expect(connectState.useActionQuery).toHaveBeenCalledWith(
|
|
78
|
+
"list-connect-marketplace-apps",
|
|
79
|
+
{},
|
|
80
|
+
{ staleTime: 5 * 60 * 1000, retry: 1 },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const input = container.querySelector("input");
|
|
84
|
+
expect(input).not.toBeNull();
|
|
85
|
+
await act(async () => {
|
|
86
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
87
|
+
HTMLInputElement.prototype,
|
|
88
|
+
"value",
|
|
89
|
+
)?.set;
|
|
90
|
+
setter?.call(input, "https://slides.example.com");
|
|
91
|
+
input?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const inspectButton = [...container.querySelectorAll("button")].find(
|
|
95
|
+
(button) => button.textContent?.includes("dispatch.pages.inspectApp"),
|
|
96
|
+
);
|
|
97
|
+
expect(inspectButton).toBeDefined();
|
|
98
|
+
await act(async () => inspectButton?.click());
|
|
99
|
+
|
|
100
|
+
expect(mutateAsync).toHaveBeenCalledWith({
|
|
101
|
+
url: "https://slides.example.com/",
|
|
102
|
+
});
|
|
103
|
+
expect(container.querySelector("[data-connect-card] h3")?.textContent).toBe(
|
|
104
|
+
"Slides",
|
|
105
|
+
);
|
|
59
106
|
});
|
|
60
107
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useFeatureFlagState } from "@agent-native/core/client/feature-flags";
|
|
2
|
+
import {
|
|
3
|
+
useActionMutation,
|
|
4
|
+
useActionQuery,
|
|
5
|
+
} from "@agent-native/core/client/hooks";
|
|
2
6
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
7
|
import { IconExternalLink, IconPlugConnected } from "@tabler/icons-react";
|
|
4
|
-
import { useQuery } from "@tanstack/react-query";
|
|
5
8
|
import { useState } from "react";
|
|
6
9
|
|
|
7
10
|
import { AppIcon } from "../../components/app-icon";
|
|
@@ -11,8 +14,6 @@ import { Input } from "../../components/ui/input";
|
|
|
11
14
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
12
15
|
import {
|
|
13
16
|
buildIdentityConnectUrl,
|
|
14
|
-
fetchConnectAgentCard,
|
|
15
|
-
fetchMarketplaceApps,
|
|
16
17
|
normalizeConnectUrl,
|
|
17
18
|
type ConnectAgentCard,
|
|
18
19
|
type MarketplaceApp,
|
|
@@ -143,13 +144,15 @@ function ConnectAppsContent() {
|
|
|
143
144
|
const [card, setCard] = useState<ConnectAgentCard | null>(null);
|
|
144
145
|
const [cardError, setCardError] = useState(false);
|
|
145
146
|
const [cardLoading, setCardLoading] = useState(false);
|
|
146
|
-
const feedQuery =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const feedQuery = useActionQuery(
|
|
148
|
+
"list-connect-marketplace-apps",
|
|
149
|
+
{},
|
|
150
|
+
{
|
|
151
|
+
staleTime: 5 * 60 * 1000,
|
|
152
|
+
retry: 1,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
const inspectCard = useActionMutation("inspect-connect-agent-card");
|
|
153
156
|
|
|
154
157
|
const target = normalizeConnectUrl(url);
|
|
155
158
|
|
|
@@ -159,7 +162,7 @@ function ConnectAppsContent() {
|
|
|
159
162
|
setCardError(false);
|
|
160
163
|
setCard(null);
|
|
161
164
|
try {
|
|
162
|
-
setCard(await
|
|
165
|
+
setCard(await inspectCard.mutateAsync({ url: target.toString() }));
|
|
163
166
|
} catch {
|
|
164
167
|
setCardError(true);
|
|
165
168
|
} finally {
|
package/src/server/index.ts
CHANGED
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
import { dispatchActions } from "../actions/index.js";
|
|
8
8
|
import type { DispatchConfig } from "../config.js";
|
|
9
9
|
import { dispatchAccessDescriptor } from "../shared/app-roles.js";
|
|
10
|
+
export {
|
|
11
|
+
listOrganizationVisibleWorkspaceApps,
|
|
12
|
+
listWorkspaceApps,
|
|
13
|
+
} from "./lib/app-creation-store.js";
|
|
14
|
+
export {
|
|
15
|
+
invalidateOrgAppsDirectoryCache,
|
|
16
|
+
orgAppsDirectoryAccessCache,
|
|
17
|
+
orgAppsDirectoryCache,
|
|
18
|
+
} from "./lib/org-apps-directory-cache.js";
|
|
10
19
|
|
|
11
20
|
defineAppRoles(dispatchAccessDescriptor, { allowOrgAdmins: true });
|
|
12
21
|
|