@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
|
@@ -34,7 +34,13 @@ vi.mock("../../../core/dist/client/i18n.js", () => ({
|
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
36
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
37
|
-
useActionMutation: () => ({
|
|
37
|
+
useActionMutation: (action: string) => ({
|
|
38
|
+
mutate:
|
|
39
|
+
action === "update-workspace-app-metadata"
|
|
40
|
+
? metadataMutationState.mutate
|
|
41
|
+
: vi.fn(),
|
|
42
|
+
isPending: false,
|
|
43
|
+
}),
|
|
38
44
|
useActionQuery: () => ({
|
|
39
45
|
data: { resources: [], counts: {} },
|
|
40
46
|
isLoading: false,
|
|
@@ -53,6 +59,8 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
53
59
|
}));
|
|
54
60
|
|
|
55
61
|
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
62
|
+
const orgRoleState = vi.hoisted(() => ({ role: "owner" as string }));
|
|
63
|
+
const metadataMutationState = vi.hoisted(() => ({ mutate: vi.fn() }));
|
|
56
64
|
|
|
57
65
|
function LocationProbe() {
|
|
58
66
|
const location = useLocation();
|
|
@@ -64,6 +72,24 @@ vi.mock("@agent-native/core/client/host", () => ({
|
|
|
64
72
|
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
65
73
|
}));
|
|
66
74
|
|
|
75
|
+
vi.mock("@agent-native/core/client/org", () => ({
|
|
76
|
+
useOrgRole: () => ({
|
|
77
|
+
org: { orgId: "org-123" },
|
|
78
|
+
role: orgRoleState.role,
|
|
79
|
+
isLoading: false,
|
|
80
|
+
error: null,
|
|
81
|
+
}),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
|
|
85
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
86
|
+
HTMLTextAreaElement.prototype,
|
|
87
|
+
"value",
|
|
88
|
+
)?.set;
|
|
89
|
+
setter?.call(textarea, value);
|
|
90
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
describe("WorkspaceAppCard", () => {
|
|
68
94
|
let container: HTMLDivElement;
|
|
69
95
|
let root: Root;
|
|
@@ -71,6 +97,8 @@ describe("WorkspaceAppCard", () => {
|
|
|
71
97
|
|
|
72
98
|
beforeEach(() => {
|
|
73
99
|
frameState.inBuilderFrame = false;
|
|
100
|
+
orgRoleState.role = "owner";
|
|
101
|
+
metadataMutationState.mutate.mockClear();
|
|
74
102
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
75
103
|
container = document.createElement("div");
|
|
76
104
|
document.body.appendChild(container);
|
|
@@ -90,6 +118,203 @@ describe("WorkspaceAppCard", () => {
|
|
|
90
118
|
vi.unstubAllGlobals();
|
|
91
119
|
});
|
|
92
120
|
|
|
121
|
+
it("edits the app's when-to-use examples with the existing details form", async () => {
|
|
122
|
+
await act(async () => {
|
|
123
|
+
root.render(
|
|
124
|
+
<MemoryRouter>
|
|
125
|
+
<TooltipProvider>
|
|
126
|
+
<WorkspaceAppCard
|
|
127
|
+
app={{
|
|
128
|
+
id: "analytics",
|
|
129
|
+
name: "Analytics",
|
|
130
|
+
path: "/analytics",
|
|
131
|
+
status: "ready",
|
|
132
|
+
whenToUse: [
|
|
133
|
+
"Explain this week's activation trend",
|
|
134
|
+
"Compare signup sources",
|
|
135
|
+
],
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
</TooltipProvider>
|
|
139
|
+
</MemoryRouter>,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
144
|
+
'button[aria-label="Settings for Analytics"]',
|
|
145
|
+
);
|
|
146
|
+
await act(async () => {
|
|
147
|
+
settingsButton?.dispatchEvent(
|
|
148
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
const editItem = Array.from(
|
|
152
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
153
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
154
|
+
expect(editItem).not.toBeUndefined();
|
|
155
|
+
|
|
156
|
+
await act(async () => editItem?.click());
|
|
157
|
+
|
|
158
|
+
const whenToUse = document.querySelector<HTMLTextAreaElement>(
|
|
159
|
+
"#app-when-to-use-analytics",
|
|
160
|
+
);
|
|
161
|
+
expect(whenToUse?.value).toBe(
|
|
162
|
+
"Explain this week's activation trend\nCompare signup sources",
|
|
163
|
+
);
|
|
164
|
+
expect(document.body.textContent).toContain("When to use");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("validates profile examples and normalizes description line breaks", async () => {
|
|
168
|
+
await act(async () => {
|
|
169
|
+
root.render(
|
|
170
|
+
<MemoryRouter>
|
|
171
|
+
<TooltipProvider>
|
|
172
|
+
<WorkspaceAppCard
|
|
173
|
+
app={{
|
|
174
|
+
id: "analytics",
|
|
175
|
+
name: "Analytics",
|
|
176
|
+
path: "/analytics",
|
|
177
|
+
status: "ready",
|
|
178
|
+
whenToUse: ["Compare signup sources"],
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
</TooltipProvider>
|
|
182
|
+
</MemoryRouter>,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
187
|
+
'button[aria-label="Settings for Analytics"]',
|
|
188
|
+
);
|
|
189
|
+
await act(async () => {
|
|
190
|
+
settingsButton?.dispatchEvent(
|
|
191
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
const editItem = Array.from(
|
|
195
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
196
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
197
|
+
await act(async () => editItem?.click());
|
|
198
|
+
|
|
199
|
+
const examples = document.querySelector<HTMLTextAreaElement>(
|
|
200
|
+
"#app-when-to-use-analytics",
|
|
201
|
+
);
|
|
202
|
+
const description = document.querySelector<HTMLTextAreaElement>(
|
|
203
|
+
"#app-description-analytics",
|
|
204
|
+
);
|
|
205
|
+
const save = Array.from(
|
|
206
|
+
document.querySelectorAll<HTMLButtonElement>('button[type="submit"]'),
|
|
207
|
+
).find((button) => button.textContent?.includes("Save"));
|
|
208
|
+
expect(examples).not.toBeNull();
|
|
209
|
+
expect(description).not.toBeNull();
|
|
210
|
+
expect(save).not.toBeNull();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
setTextareaValue(
|
|
214
|
+
examples!,
|
|
215
|
+
Array.from({ length: 7 }, (_, i) => `Example ${i + 1}`).join("\n"),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
expect(save?.disabled).toBe(true);
|
|
219
|
+
|
|
220
|
+
await act(async () => {
|
|
221
|
+
setTextareaValue(examples!, `${"x".repeat(141)}\nValid example`);
|
|
222
|
+
});
|
|
223
|
+
expect(save?.disabled).toBe(true);
|
|
224
|
+
|
|
225
|
+
await act(async () => {
|
|
226
|
+
setTextareaValue(examples!, "Safe\u0001 example");
|
|
227
|
+
setTextareaValue(description!, "First line\nSecond line");
|
|
228
|
+
});
|
|
229
|
+
expect(examples?.value).toBe("Safe example");
|
|
230
|
+
expect(description?.value).toBe("First line Second line");
|
|
231
|
+
expect(save?.disabled).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("submits only the profile field that changed", async () => {
|
|
235
|
+
await act(async () => {
|
|
236
|
+
root.render(
|
|
237
|
+
<MemoryRouter>
|
|
238
|
+
<TooltipProvider>
|
|
239
|
+
<WorkspaceAppCard
|
|
240
|
+
app={{
|
|
241
|
+
id: "analytics",
|
|
242
|
+
name: "Analytics",
|
|
243
|
+
description: "Original description",
|
|
244
|
+
path: "/analytics",
|
|
245
|
+
status: "ready",
|
|
246
|
+
whenToUse: ["Compare signup sources"],
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</TooltipProvider>
|
|
250
|
+
</MemoryRouter>,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
255
|
+
'button[aria-label="Settings for Analytics"]',
|
|
256
|
+
);
|
|
257
|
+
await act(async () => {
|
|
258
|
+
settingsButton?.dispatchEvent(
|
|
259
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
const editItem = Array.from(
|
|
263
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
264
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
265
|
+
await act(async () => editItem?.click());
|
|
266
|
+
|
|
267
|
+
const examples = document.querySelector<HTMLTextAreaElement>(
|
|
268
|
+
"#app-when-to-use-analytics",
|
|
269
|
+
);
|
|
270
|
+
const form = examples?.closest("form");
|
|
271
|
+
await act(async () => {
|
|
272
|
+
setTextareaValue(examples!, "Track acquisition by channel");
|
|
273
|
+
form?.dispatchEvent(
|
|
274
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(metadataMutationState.mutate).toHaveBeenCalledWith({
|
|
279
|
+
appId: "analytics",
|
|
280
|
+
whenToUse: ["Track acquisition by channel"],
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("hides Edit details from members", async () => {
|
|
285
|
+
orgRoleState.role = "member";
|
|
286
|
+
await act(async () => {
|
|
287
|
+
root.render(
|
|
288
|
+
<MemoryRouter>
|
|
289
|
+
<TooltipProvider>
|
|
290
|
+
<WorkspaceAppCard
|
|
291
|
+
app={{
|
|
292
|
+
id: "analytics",
|
|
293
|
+
name: "Analytics",
|
|
294
|
+
path: "/analytics",
|
|
295
|
+
status: "ready",
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
</TooltipProvider>
|
|
299
|
+
</MemoryRouter>,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
304
|
+
'button[aria-label="Settings for Analytics"]',
|
|
305
|
+
);
|
|
306
|
+
await act(async () => {
|
|
307
|
+
settingsButton?.dispatchEvent(
|
|
308
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
expect(
|
|
312
|
+
Array.from(
|
|
313
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
314
|
+
).some((item) => item.textContent?.includes("Edit details")),
|
|
315
|
+
).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
93
318
|
it("opens ready apps inline and keeps new-tab opening in the chevron menu", async () => {
|
|
94
319
|
await act(async () => {
|
|
95
320
|
root.render(
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
useActionQuery,
|
|
4
4
|
} from "@agent-native/core/client/hooks";
|
|
5
5
|
import { useT } from "@agent-native/core/client/i18n";
|
|
6
|
+
import { useOrgRole } from "@agent-native/core/client/org";
|
|
7
|
+
import { parseAgentProfileMetadata } from "@agent-native/core/shared";
|
|
6
8
|
import { APP_ACTION_MENU_CONTENT_CLASS } from "@agent-native/toolkit/app/chat/chat-first/app-open-actions";
|
|
7
9
|
import { ShareButton } from "@agent-native/toolkit/app/sharing";
|
|
8
10
|
import {
|
|
@@ -70,6 +72,23 @@ import { AppResourceEffectiveStack } from "./workspace-resource-effective-stack"
|
|
|
70
72
|
const APP_CARD_ACTION_CLASS =
|
|
71
73
|
"size-7 rounded-md p-0 text-muted-foreground transition-[background-color,color] hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-foreground";
|
|
72
74
|
|
|
75
|
+
function normalizeProfileDescription(value: string): string {
|
|
76
|
+
return value.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeWhenToUseText(value: string): string {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/\r\n?/g, "\n")
|
|
82
|
+
.replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseWhenToUseText(value: string): string[] {
|
|
86
|
+
return value
|
|
87
|
+
.split("\n")
|
|
88
|
+
.map((item) => item.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
function deferWorkspaceAppOverlayOpen(
|
|
74
93
|
event: { preventDefault: () => void },
|
|
75
94
|
closeMenu: () => void,
|
|
@@ -108,6 +127,12 @@ export function WorkspaceAppCard({
|
|
|
108
127
|
onTogglePinned?: () => void;
|
|
109
128
|
}) {
|
|
110
129
|
const t = useT();
|
|
130
|
+
const { org, role, isLoading: orgLoading, error: orgError } = useOrgRole();
|
|
131
|
+
const canEditMetadata =
|
|
132
|
+
!orgLoading &&
|
|
133
|
+
!orgError &&
|
|
134
|
+
!!org?.orgId &&
|
|
135
|
+
(role === "owner" || role === "admin");
|
|
111
136
|
const href = workspaceAppHref(app);
|
|
112
137
|
const directHref =
|
|
113
138
|
app.status !== "pending" &&
|
|
@@ -130,14 +155,27 @@ export function WorkspaceAppCard({
|
|
|
130
155
|
const [resourcesOpen, setResourcesOpen] = useState(false);
|
|
131
156
|
const [draftName, setDraftName] = useState(app.name);
|
|
132
157
|
const [draftDescription, setDraftDescription] = useState(
|
|
133
|
-
app.description || "",
|
|
158
|
+
normalizeProfileDescription(app.description || ""),
|
|
159
|
+
);
|
|
160
|
+
const [draftWhenToUse, setDraftWhenToUse] = useState(
|
|
161
|
+
app.whenToUse?.join("\n") ?? "",
|
|
162
|
+
);
|
|
163
|
+
const draftWhenToUseValues = parseWhenToUseText(draftWhenToUse);
|
|
164
|
+
const validProfile = Boolean(
|
|
165
|
+
parseAgentProfileMetadata({
|
|
166
|
+
...(draftDescription.trim()
|
|
167
|
+
? { description: draftDescription.trim() }
|
|
168
|
+
: {}),
|
|
169
|
+
whenToUse: draftWhenToUseValues,
|
|
170
|
+
}),
|
|
134
171
|
);
|
|
135
172
|
|
|
136
173
|
useEffect(() => {
|
|
137
174
|
if (editOpen) return;
|
|
138
175
|
setDraftName(app.name);
|
|
139
|
-
setDraftDescription(app.description || "");
|
|
140
|
-
|
|
176
|
+
setDraftDescription(normalizeProfileDescription(app.description || ""));
|
|
177
|
+
setDraftWhenToUse(app.whenToUse?.join("\n") ?? "");
|
|
178
|
+
}, [app.description, app.name, app.whenToUse, editOpen]);
|
|
141
179
|
|
|
142
180
|
const archive = useActionMutation("archive-workspace-app", {
|
|
143
181
|
onError: (err) =>
|
|
@@ -179,10 +217,35 @@ export function WorkspaceAppCard({
|
|
|
179
217
|
toast.error("App name is required.");
|
|
180
218
|
return;
|
|
181
219
|
}
|
|
220
|
+
const description = draftDescription.trim();
|
|
221
|
+
const currentDescription = normalizeProfileDescription(
|
|
222
|
+
app.description || "",
|
|
223
|
+
).trim();
|
|
224
|
+
const currentWhenToUse = app.whenToUse ?? [];
|
|
225
|
+
const nameChanged = name !== app.name.trim();
|
|
226
|
+
const descriptionChanged = description !== currentDescription;
|
|
227
|
+
const whenToUseChanged =
|
|
228
|
+
draftWhenToUseValues.length !== currentWhenToUse.length ||
|
|
229
|
+
draftWhenToUseValues.some(
|
|
230
|
+
(example, index) => example !== currentWhenToUse[index],
|
|
231
|
+
);
|
|
232
|
+
if (
|
|
233
|
+
!parseAgentProfileMetadata({
|
|
234
|
+
...(description ? { description } : {}),
|
|
235
|
+
whenToUse: draftWhenToUseValues,
|
|
236
|
+
})
|
|
237
|
+
) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!nameChanged && !descriptionChanged && !whenToUseChanged) {
|
|
241
|
+
setEditOpen(false);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
182
244
|
updateMetadata.mutate({
|
|
183
245
|
appId: app.id,
|
|
184
|
-
name,
|
|
185
|
-
description:
|
|
246
|
+
...(nameChanged ? { name } : {}),
|
|
247
|
+
...(descriptionChanged ? { description } : {}),
|
|
248
|
+
...(whenToUseChanged ? { whenToUse: draftWhenToUseValues } : {}),
|
|
186
249
|
});
|
|
187
250
|
};
|
|
188
251
|
const pinLabel = t(
|
|
@@ -247,6 +310,7 @@ export function WorkspaceAppCard({
|
|
|
247
310
|
/>
|
|
248
311
|
<WorkspaceAppSettings
|
|
249
312
|
app={app}
|
|
313
|
+
canEditMetadata={canEditMetadata}
|
|
250
314
|
isArchived={isArchived}
|
|
251
315
|
isPending={isPending}
|
|
252
316
|
onEdit={() => setEditOpen(true)}
|
|
@@ -284,10 +348,42 @@ export function WorkspaceAppCard({
|
|
|
284
348
|
<Textarea
|
|
285
349
|
id={`app-description-${app.id}`}
|
|
286
350
|
value={draftDescription}
|
|
287
|
-
maxLength={
|
|
351
|
+
maxLength={1000}
|
|
288
352
|
rows={4}
|
|
289
|
-
onChange={(event) =>
|
|
353
|
+
onChange={(event) =>
|
|
354
|
+
setDraftDescription(
|
|
355
|
+
normalizeProfileDescription(event.target.value),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="space-y-2">
|
|
361
|
+
<Label htmlFor={`app-when-to-use-${app.id}`}>
|
|
362
|
+
{t("dispatch.pages.appProfileWhenToUse", {
|
|
363
|
+
defaultValue: "When to use",
|
|
364
|
+
})}
|
|
365
|
+
</Label>
|
|
366
|
+
<Textarea
|
|
367
|
+
id={`app-when-to-use-${app.id}`}
|
|
368
|
+
value={draftWhenToUse}
|
|
369
|
+
aria-invalid={!validProfile}
|
|
370
|
+
rows={3}
|
|
371
|
+
placeholder={t(
|
|
372
|
+
"dispatch.pages.appProfileWhenToUsePlaceholder",
|
|
373
|
+
{
|
|
374
|
+
defaultValue: "One example request per line",
|
|
375
|
+
},
|
|
376
|
+
)}
|
|
377
|
+
onChange={(event) =>
|
|
378
|
+
setDraftWhenToUse(normalizeWhenToUseText(event.target.value))
|
|
379
|
+
}
|
|
290
380
|
/>
|
|
381
|
+
<p className="text-xs text-muted-foreground">
|
|
382
|
+
{t("dispatch.pages.appProfileWhenToUseHint", {
|
|
383
|
+
defaultValue:
|
|
384
|
+
"Add up to 6 examples, with no more than 140 characters each.",
|
|
385
|
+
})}
|
|
386
|
+
</p>
|
|
291
387
|
</div>
|
|
292
388
|
<DialogFooter>
|
|
293
389
|
<Button
|
|
@@ -297,7 +393,10 @@ export function WorkspaceAppCard({
|
|
|
297
393
|
>
|
|
298
394
|
Cancel
|
|
299
395
|
</Button>
|
|
300
|
-
<Button
|
|
396
|
+
<Button
|
|
397
|
+
type="submit"
|
|
398
|
+
disabled={updateMetadata.isPending || !validProfile}
|
|
399
|
+
>
|
|
301
400
|
{updateMetadata.isPending ? "Saving..." : "Save"}
|
|
302
401
|
</Button>
|
|
303
402
|
</DialogFooter>
|
|
@@ -406,6 +505,7 @@ type WorkspaceAppMetadataLabels = {
|
|
|
406
505
|
|
|
407
506
|
function WorkspaceAppSettings({
|
|
408
507
|
app,
|
|
508
|
+
canEditMetadata,
|
|
409
509
|
isArchived,
|
|
410
510
|
isPending,
|
|
411
511
|
onEdit,
|
|
@@ -417,6 +517,7 @@ function WorkspaceAppSettings({
|
|
|
417
517
|
labels,
|
|
418
518
|
}: {
|
|
419
519
|
app: WorkspaceAppSummary;
|
|
520
|
+
canEditMetadata: boolean;
|
|
420
521
|
isArchived: boolean;
|
|
421
522
|
isPending: boolean;
|
|
422
523
|
onEdit: () => void;
|
|
@@ -477,10 +578,12 @@ function WorkspaceAppSettings({
|
|
|
477
578
|
)
|
|
478
579
|
}
|
|
479
580
|
>
|
|
480
|
-
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
|
|
581
|
+
{canEditMetadata ? (
|
|
582
|
+
<DropdownMenuItem onSelect={onEdit}>
|
|
583
|
+
<IconEdit size={14} aria-hidden="true" />
|
|
584
|
+
Edit details
|
|
585
|
+
</DropdownMenuItem>
|
|
586
|
+
) : null}
|
|
484
587
|
{!isPending ? (
|
|
485
588
|
<DropdownMenuItem
|
|
486
589
|
onSelect={(event) =>
|
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { ssrfSafeFetchMock } = vi.hoisted(() => ({
|
|
4
|
+
ssrfSafeFetchMock: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@agent-native/core/extensions/url-safety", () => ({
|
|
8
|
+
ssrfSafeFetch: (...args: unknown[]) => ssrfSafeFetchMock(...args),
|
|
9
|
+
}));
|
|
2
10
|
|
|
3
11
|
import {
|
|
4
|
-
buildIdentityConnectUrl,
|
|
5
12
|
fetchConnectAgentCard,
|
|
6
13
|
fetchMarketplaceApps,
|
|
14
|
+
} from "../server/lib/connect-apps";
|
|
15
|
+
import {
|
|
16
|
+
buildAgentCardUrl,
|
|
17
|
+
buildIdentityConnectUrl,
|
|
7
18
|
normalizeConnectUrl,
|
|
8
19
|
parseConnectAgentCard,
|
|
9
20
|
} from "./connect-apps";
|
|
10
21
|
|
|
11
22
|
describe("connect apps helpers", () => {
|
|
23
|
+
beforeEach(() => ssrfSafeFetchMock.mockReset());
|
|
24
|
+
|
|
12
25
|
it("rejects unsafe or credential-bearing URLs", () => {
|
|
13
26
|
expect(normalizeConnectUrl("javascript:alert(1)")).toBeNull();
|
|
14
27
|
expect(normalizeConnectUrl("https://user:pass@example.com")).toBeNull();
|
|
15
28
|
expect(normalizeConnectUrl("https://127.0.0.1")).toBeNull();
|
|
16
29
|
expect(normalizeConnectUrl("https://169.254.169.254")).toBeNull();
|
|
17
30
|
expect(normalizeConnectUrl("https://[::1]")).toBeNull();
|
|
18
|
-
expect(normalizeConnectUrl("
|
|
31
|
+
expect(normalizeConnectUrl("https://[::ffff:127.0.0.1]")).toBeNull();
|
|
32
|
+
expect(normalizeConnectUrl("https://[::ffff:7f00:1]")).toBeNull();
|
|
33
|
+
expect(normalizeConnectUrl("https://[::ffff:6440:1]")).toBeNull();
|
|
34
|
+
expect(normalizeConnectUrl("https://[::ffff:0:0]")).toBeNull();
|
|
35
|
+
expect(normalizeConnectUrl("https://[fc00::1]")).toBeNull();
|
|
36
|
+
expect(normalizeConnectUrl("https://[fe80::1]")).toBeNull();
|
|
37
|
+
expect(normalizeConnectUrl("https://fdroid.org")).not.toBeNull();
|
|
38
|
+
expect(normalizeConnectUrl("http://localhost:3000")).toBeNull();
|
|
19
39
|
});
|
|
20
40
|
|
|
21
41
|
it("accepts a connect-capable card only from the requested origin", () => {
|
|
@@ -84,24 +104,17 @@ describe("connect apps helpers", () => {
|
|
|
84
104
|
expect(url.searchParams.get("return")).toBe("/_agent-native/open");
|
|
85
105
|
});
|
|
86
106
|
|
|
87
|
-
it("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
expect(identity.pathname).toBe(
|
|
94
|
-
"/account-tiering/_agent-native/identity/login",
|
|
95
|
-
);
|
|
96
|
-
expect(identity.searchParams.get("return")).toBe(
|
|
97
|
-
"/account-tiering/_agent-native/open",
|
|
107
|
+
it("builds a path-mounted agent card URL", () => {
|
|
108
|
+
expect(
|
|
109
|
+
buildAgentCardUrl("https://community.agent-native.com/account-tiering"),
|
|
110
|
+
).toBe(
|
|
111
|
+
"https://community.agent-native.com/account-tiering/.well-known/agent-card.json",
|
|
98
112
|
);
|
|
113
|
+
});
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
requestedUrl = String(input);
|
|
104
|
-
return new Response(
|
|
115
|
+
it("fetches agent cards through the server SSRF boundary", async () => {
|
|
116
|
+
ssrfSafeFetchMock.mockResolvedValue(
|
|
117
|
+
new Response(
|
|
105
118
|
JSON.stringify({
|
|
106
119
|
name: "Account Tiering",
|
|
107
120
|
description: "Prioritize accounts",
|
|
@@ -109,57 +122,96 @@ describe("connect apps helpers", () => {
|
|
|
109
122
|
capabilities: { connect: true },
|
|
110
123
|
}),
|
|
111
124
|
{ status: 200 },
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await expect(
|
|
129
|
+
fetchConnectAgentCard(
|
|
116
130
|
"https://community.agent-native.com/account-tiering",
|
|
117
|
-
)
|
|
118
|
-
|
|
131
|
+
),
|
|
132
|
+
).resolves.toMatchObject({ name: "Account Tiering", connect: true });
|
|
133
|
+
expect(ssrfSafeFetchMock).toHaveBeenCalledWith(
|
|
134
|
+
"https://community.agent-native.com/account-tiering/.well-known/agent-card.json",
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
headers: { accept: "application/json" },
|
|
137
|
+
signal: expect.any(AbortSignal),
|
|
138
|
+
}),
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
maxRedirects: 3,
|
|
141
|
+
requireDispatcher: true,
|
|
142
|
+
httpsOnly: true,
|
|
143
|
+
assertUrlAllowed: expect.any(Function),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
const fetchPolicy = ssrfSafeFetchMock.mock.calls[0]?.[2] as {
|
|
147
|
+
assertUrlAllowed: (url: string) => void;
|
|
148
|
+
};
|
|
149
|
+
expect(() =>
|
|
150
|
+
fetchPolicy.assertUrlAllowed(
|
|
151
|
+
"https://evil.example.test/account-tiering/.well-known/agent-card.json",
|
|
152
|
+
),
|
|
153
|
+
).toThrow(/requested URL/);
|
|
154
|
+
expect(() =>
|
|
155
|
+
fetchPolicy.assertUrlAllowed(
|
|
119
156
|
"https://community.agent-native.com/account-tiering/.well-known/agent-card.json",
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
),
|
|
158
|
+
).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects unsafe addresses before calling the server fetch boundary", async () => {
|
|
162
|
+
await expect(fetchConnectAgentCard("https://127.0.0.1")).rejects.toThrow(
|
|
163
|
+
/secure app URL/,
|
|
164
|
+
);
|
|
165
|
+
await expect(
|
|
166
|
+
fetchConnectAgentCard("http://localhost:3000"),
|
|
167
|
+
).rejects.toThrow(/secure app URL/);
|
|
168
|
+
expect(ssrfSafeFetchMock).not.toHaveBeenCalled();
|
|
124
169
|
});
|
|
125
170
|
|
|
126
171
|
it("rejects unsafe marketplace entries before rendering links", async () => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
ssrfSafeFetchMock
|
|
173
|
+
.mockResolvedValueOnce(
|
|
174
|
+
new Response(
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
apps: [
|
|
177
|
+
{
|
|
178
|
+
id: "unsafe",
|
|
179
|
+
name: "Unsafe",
|
|
180
|
+
description: "bad",
|
|
181
|
+
url: "javascript:alert(1)",
|
|
182
|
+
capabilities: ["connect"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "safe",
|
|
186
|
+
name: "Safe",
|
|
187
|
+
description: "ok",
|
|
188
|
+
url: "https://safe.example.test",
|
|
189
|
+
capabilities: ["connect"],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
}),
|
|
193
|
+
{ status: 200 },
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
.mockResolvedValueOnce(
|
|
197
|
+
new Response(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
name: "Safe",
|
|
200
|
+
description: "ok",
|
|
201
|
+
url: "https://safe.example.test/_agent-native/a2a",
|
|
202
|
+
capabilities: { connect: true },
|
|
203
|
+
}),
|
|
204
|
+
{ status: 200 },
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await expect(fetchMarketplaceApps("https://feed.test")).resolves.toEqual([
|
|
209
|
+
expect.objectContaining({ id: "safe" }),
|
|
210
|
+
]);
|
|
211
|
+
expect(ssrfSafeFetchMock).toHaveBeenCalledTimes(2);
|
|
212
|
+
expect(ssrfSafeFetchMock.mock.calls.map(([url]) => url)).toEqual([
|
|
213
|
+
"https://feed.test/",
|
|
214
|
+
"https://safe.example.test/.well-known/agent-card.json",
|
|
215
|
+
]);
|
|
164
216
|
});
|
|
165
217
|
});
|