@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.30

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +6 -47
  2. package/dist/actions/connect-external-agent.d.ts.map +1 -1
  3. package/dist/actions/connect-external-agent.js +12 -13
  4. package/dist/actions/connect-external-agent.js.map +1 -1
  5. package/dist/actions/create-workspace-resource.d.ts +13 -13
  6. package/dist/actions/delete-destination.d.ts +12 -12
  7. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  8. package/dist/actions/import-agent.d.ts +13 -13
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +8 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
  13. package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
  14. package/dist/actions/inspect-connect-agent-card.js +15 -0
  15. package/dist/actions/inspect-connect-agent-card.js.map +1 -0
  16. package/dist/actions/install-curated-community-app.d.ts +36 -0
  17. package/dist/actions/install-curated-community-app.d.ts.map +1 -0
  18. package/dist/actions/install-curated-community-app.js +26 -0
  19. package/dist/actions/install-curated-community-app.js.map +1 -0
  20. package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
  21. package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
  22. package/dist/actions/list-connect-marketplace-apps.js +15 -0
  23. package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
  24. package/dist/actions/list-connected-agents.d.ts +2 -0
  25. package/dist/actions/list-curated-community-apps.d.ts +3 -0
  26. package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
  27. package/dist/actions/list-curated-community-apps.js +10 -0
  28. package/dist/actions/list-curated-community-apps.js.map +1 -0
  29. package/dist/actions/provider-api-register.d.ts +14 -14
  30. package/dist/actions/remix-workspace-template.d.ts +6 -0
  31. package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
  32. package/dist/actions/update-workspace-app-metadata.js +26 -5
  33. package/dist/actions/update-workspace-app-metadata.js.map +1 -1
  34. package/dist/components/community-apps-section.d.ts +2 -0
  35. package/dist/components/community-apps-section.d.ts.map +1 -0
  36. package/dist/components/community-apps-section.js +90 -0
  37. package/dist/components/community-apps-section.js.map +1 -0
  38. package/dist/components/workspace-app-card.d.ts.map +1 -1
  39. package/dist/components/workspace-app-card.js +63 -9
  40. package/dist/components/workspace-app-card.js.map +1 -1
  41. package/dist/lib/connect-apps.d.ts +1 -2
  42. package/dist/lib/connect-apps.d.ts.map +1 -1
  43. package/dist/lib/connect-apps.js +41 -65
  44. package/dist/lib/connect-apps.js.map +1 -1
  45. package/dist/lib/workspace-apps.d.ts +1 -0
  46. package/dist/lib/workspace-apps.d.ts.map +1 -1
  47. package/dist/lib/workspace-apps.js.map +1 -1
  48. package/dist/routes/pages/apps.d.ts.map +1 -1
  49. package/dist/routes/pages/apps.js +2 -1
  50. package/dist/routes/pages/apps.js.map +1 -1
  51. package/dist/routes/pages/connect.d.ts.map +1 -1
  52. package/dist/routes/pages/connect.js +5 -7
  53. package/dist/routes/pages/connect.js.map +1 -1
  54. package/dist/server/index.d.ts +2 -0
  55. package/dist/server/index.d.ts.map +1 -1
  56. package/dist/server/index.js +2 -0
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/app-creation-store.d.ts +11 -1
  59. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  60. package/dist/server/lib/app-creation-store.js +258 -39
  61. package/dist/server/lib/app-creation-store.js.map +1 -1
  62. package/dist/server/lib/community-apps.d.ts +45 -0
  63. package/dist/server/lib/community-apps.d.ts.map +1 -0
  64. package/dist/server/lib/community-apps.js +267 -0
  65. package/dist/server/lib/community-apps.js.map +1 -0
  66. package/dist/server/lib/community-source.d.ts +19 -0
  67. package/dist/server/lib/community-source.d.ts.map +1 -0
  68. package/dist/server/lib/community-source.js +118 -0
  69. package/dist/server/lib/community-source.js.map +1 -0
  70. package/dist/server/lib/connect-apps.d.ts +4 -0
  71. package/dist/server/lib/connect-apps.d.ts.map +1 -0
  72. package/dist/server/lib/connect-apps.js +99 -0
  73. package/dist/server/lib/connect-apps.js.map +1 -0
  74. package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
  75. package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
  76. package/dist/server/lib/org-apps-directory-cache.js +44 -0
  77. package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
  78. package/dist/server/plugins/integrations.js +3 -3
  79. package/dist/server/plugins/integrations.js.map +1 -1
  80. package/dist/shared/labs.d.ts +2 -0
  81. package/dist/shared/labs.d.ts.map +1 -0
  82. package/dist/shared/labs.js +8 -0
  83. package/dist/shared/labs.js.map +1 -0
  84. package/package.json +5 -4
  85. package/src/actions/connect-external-agent.spec.ts +101 -3
  86. package/src/actions/connect-external-agent.ts +12 -11
  87. package/src/actions/index.spec.ts +13 -0
  88. package/src/actions/index.ts +8 -0
  89. package/src/actions/inspect-connect-agent-card.ts +17 -0
  90. package/src/actions/install-curated-community-app.ts +32 -0
  91. package/src/actions/list-connect-marketplace-apps.ts +17 -0
  92. package/src/actions/list-curated-community-apps.ts +12 -0
  93. package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
  94. package/src/actions/update-workspace-app-metadata.ts +48 -24
  95. package/src/components/community-apps-section.tsx +224 -0
  96. package/src/components/workspace-app-card.spec.tsx +226 -1
  97. package/src/components/workspace-app-card.tsx +115 -12
  98. package/src/lib/connect-apps.spec.ts +118 -66
  99. package/src/lib/connect-apps.ts +47 -82
  100. package/src/lib/workspace-apps.ts +1 -0
  101. package/src/routes/pages/apps.spec.tsx +201 -1
  102. package/src/routes/pages/apps.tsx +3 -0
  103. package/src/routes/pages/connect.spec.tsx +53 -6
  104. package/src/routes/pages/connect.tsx +14 -11
  105. package/src/server/index.ts +9 -0
  106. package/src/server/lib/app-creation-store.spec.ts +539 -11
  107. package/src/server/lib/app-creation-store.ts +332 -41
  108. package/src/server/lib/community-apps.spec.ts +367 -0
  109. package/src/server/lib/community-apps.ts +372 -0
  110. package/src/server/lib/community-source.spec.ts +151 -0
  111. package/src/server/lib/community-source.ts +186 -0
  112. package/src/server/lib/connect-apps.ts +149 -0
  113. package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
  114. package/src/server/lib/org-apps-directory-cache.ts +66 -0
  115. package/src/server/plugins/integrations.spec.ts +60 -0
  116. package/src/server/plugins/integrations.ts +3 -3
  117. package/src/shared/labs.ts +9 -0
@@ -0,0 +1,367 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ getUserLabEnabled: vi.fn(async () => true),
9
+ normalizeWorkspaceAppId: vi.fn((value: string) =>
10
+ value
11
+ .trim()
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9-]+/g, "-"),
14
+ ),
15
+ getWorkspaceAppIdValidationError: vi.fn((value: string) =>
16
+ value === "invalid" ? "invalid app id" : null,
17
+ ),
18
+ getAppCreationSettings: vi.fn(async () => ({
19
+ builderProjectId: "project-1",
20
+ })),
21
+ getWorkspaceInfo: vi.fn(() => ({ rootPath: "/workspace" })),
22
+ isLocalAppCreationRuntime: vi.fn(() => false),
23
+ listWorkspaceApps: vi.fn(async () => [] as any[]),
24
+ startWorkspaceAppCreation: vi.fn(),
25
+ loadPinnedCommunitySourceAttachments: vi.fn(),
26
+ currentOrgId: vi.fn(() => "org-1"),
27
+ currentOwnerEmail: vi.fn(() => "owner@example.com"),
28
+ recordAudit: vi.fn(async () => undefined),
29
+ fail: vi.fn((message: string, options?: Record<string, unknown>) => {
30
+ throw Object.assign(new Error(message), options);
31
+ }),
32
+ }));
33
+
34
+ vi.mock("@agent-native/core/action", () => ({ fail: mocks.fail }));
35
+ vi.mock("@agent-native/core/labs/server", () => ({
36
+ getUserLabEnabled: mocks.getUserLabEnabled,
37
+ }));
38
+ vi.mock("@agent-native/core/shared", () => ({
39
+ normalizeWorkspaceAppId: mocks.normalizeWorkspaceAppId,
40
+ getWorkspaceAppIdValidationError: mocks.getWorkspaceAppIdValidationError,
41
+ }));
42
+ vi.mock("@agent-native/core/shared/first-party-agents", () => ({
43
+ BUILTIN_AGENT_CATALOG_IDS: ["plan", "chat", "tasks", "crm", "factory"],
44
+ HIDDEN_FIRST_PARTY_AGENT_IDS: new Set(["removed-agent"]),
45
+ }));
46
+ vi.mock("../../shared/labs.js", () => ({
47
+ COMMUNITY_APPS_LAB: { key: "dispatch.community-apps", defaultEnabled: false },
48
+ }));
49
+ vi.mock("./app-creation-store.js", () => ({
50
+ getAppCreationSettings: mocks.getAppCreationSettings,
51
+ getWorkspaceInfo: mocks.getWorkspaceInfo,
52
+ isLocalAppCreationRuntime: mocks.isLocalAppCreationRuntime,
53
+ listWorkspaceApps: mocks.listWorkspaceApps,
54
+ startWorkspaceAppCreation: mocks.startWorkspaceAppCreation,
55
+ }));
56
+ vi.mock("./community-source.js", () => ({
57
+ loadPinnedCommunitySourceAttachments:
58
+ mocks.loadPinnedCommunitySourceAttachments,
59
+ }));
60
+ vi.mock("./dispatch-store.js", () => ({
61
+ currentOrgId: mocks.currentOrgId,
62
+ currentOwnerEmail: mocks.currentOwnerEmail,
63
+ recordAudit: mocks.recordAudit,
64
+ }));
65
+ vi.mock("./curated-workspace-templates.js", () => ({
66
+ CURATED_WORKSPACE_TEMPLATES: [{ id: "mail" }, { id: "calendar" }],
67
+ }));
68
+
69
+ import {
70
+ CURATED_COMMUNITY_APP_SOURCES,
71
+ installCuratedCommunityApp,
72
+ listCuratedCommunityApps,
73
+ } from "./community-apps.js";
74
+
75
+ describe("curated community apps", () => {
76
+ beforeEach(() => {
77
+ vi.clearAllMocks();
78
+ mocks.getUserLabEnabled.mockResolvedValue(true);
79
+ mocks.getAppCreationSettings.mockResolvedValue({
80
+ builderProjectId: "project-1",
81
+ });
82
+ mocks.isLocalAppCreationRuntime.mockReturnValue(false);
83
+ mocks.listWorkspaceApps.mockResolvedValue([]);
84
+ mocks.recordAudit.mockResolvedValue(undefined);
85
+ mocks.loadPinnedCommunitySourceAttachments.mockResolvedValue([
86
+ {
87
+ type: "upload",
88
+ contentType: "text/plain",
89
+ name: "community-qr/README.md",
90
+ text: "pinned README",
91
+ dataUrl: "",
92
+ id: "community-qr:README.md",
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it("pins the exact tracked QR template files by Git blob id", () => {
98
+ expect(CURATED_COMMUNITY_APP_SOURCES.length).toBeGreaterThan(0);
99
+ expect(
100
+ CURATED_COMMUNITY_APP_SOURCES.every((source) =>
101
+ source.id.startsWith("community-"),
102
+ ),
103
+ ).toBe(true);
104
+ expect(
105
+ CURATED_COMMUNITY_APP_SOURCES.every(
106
+ (source) =>
107
+ source.ref === "main" &&
108
+ source.files.every((file) => /^[a-f0-9]{40}$/i.test(file.blobSha)),
109
+ ),
110
+ ).toBe(true);
111
+ expect(
112
+ CURATED_COMMUNITY_APP_SOURCES.some((source) =>
113
+ ["mail", "calendar"].includes(source.id),
114
+ ),
115
+ ).toBe(false);
116
+
117
+ const source = CURATED_COMMUNITY_APP_SOURCES.find(
118
+ (entry) => entry.id === "community-qr",
119
+ )!;
120
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
121
+ cwd: path.dirname(fileURLToPath(import.meta.url)),
122
+ encoding: "utf8",
123
+ }).trim();
124
+ const templatePath = "templates/community/qr";
125
+ const trackedFiles = execFileSync(
126
+ "git",
127
+ ["ls-files", "-z", "--", templatePath],
128
+ { cwd: repoRoot, encoding: "utf8" },
129
+ )
130
+ .split("\0")
131
+ .filter((file) => file.startsWith(templatePath + "/"))
132
+ .map((file) => file.slice(templatePath.length + 1))
133
+ .sort();
134
+
135
+ expect(source.ref).toBe("main");
136
+ expect(source.templatePath).toBe(templatePath);
137
+ expect(source.files.map((file) => file.path).sort()).toEqual(trackedFiles);
138
+ for (const file of source.files) {
139
+ const relativePath = templatePath + "/" + file.path;
140
+ const blobSha = execFileSync("git", ["hash-object", relativePath], {
141
+ cwd: repoRoot,
142
+ encoding: "utf8",
143
+ }).trim();
144
+ expect(blobSha, relativePath).toBe(file.blobSha);
145
+ }
146
+ });
147
+
148
+ it("refuses the catalog and installs while the Lab is off", async () => {
149
+ mocks.getUserLabEnabled.mockResolvedValue(false);
150
+
151
+ await expect(listCuratedCommunityApps()).rejects.toMatchObject({
152
+ errorCode: "community_apps_lab_disabled",
153
+ statusCode: 403,
154
+ });
155
+ await expect(
156
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
157
+ ).rejects.toMatchObject({
158
+ errorCode: "community_apps_lab_disabled",
159
+ statusCode: 403,
160
+ });
161
+ expect(mocks.listWorkspaceApps).not.toHaveBeenCalled();
162
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it("marks existing pending installs and workspace availability", async () => {
166
+ mocks.listWorkspaceApps.mockResolvedValue([
167
+ {
168
+ id: "community-qr",
169
+ communitySourceId: "community-qr",
170
+ name: "QR",
171
+ path: "/community-qr",
172
+ url: null,
173
+ status: "pending",
174
+ },
175
+ ]);
176
+
177
+ await expect(listCuratedCommunityApps()).resolves.toEqual([
178
+ expect.objectContaining({
179
+ id: "community-qr",
180
+ installed: false,
181
+ pending: true,
182
+ installable: false,
183
+ }),
184
+ ]);
185
+ });
186
+
187
+ it("does not offer unverified Builder-mode community installs", async () => {
188
+ mocks.isLocalAppCreationRuntime.mockReturnValue(false);
189
+
190
+ await expect(listCuratedCommunityApps()).resolves.toEqual([
191
+ expect.objectContaining({
192
+ id: "community-qr",
193
+ installed: false,
194
+ pending: false,
195
+ installable: false,
196
+ installUnavailableReason: "builder-install-unverified",
197
+ }),
198
+ ]);
199
+ });
200
+
201
+ it("returns existing progress when the same install is requested twice", async () => {
202
+ const pending = {
203
+ id: "community-qr",
204
+ communitySourceId: "community-qr",
205
+ name: "QR",
206
+ path: "/community-qr",
207
+ url: null,
208
+ status: "pending" as const,
209
+ statusLabel: "Creating",
210
+ builderUrl: "https://builder.io/project/branch",
211
+ branchName: "community-qr",
212
+ };
213
+ mocks.listWorkspaceApps.mockResolvedValue([pending]);
214
+
215
+ await expect(
216
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
217
+ ).resolves.toMatchObject({ mode: "pending", app: { id: "community-qr" } });
218
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it("passes only the catalog's pinned source into app creation", async () => {
222
+ mocks.isLocalAppCreationRuntime.mockReturnValue(true);
223
+ mocks.startWorkspaceAppCreation.mockResolvedValue({
224
+ mode: "local-agent",
225
+ appId: "qr-team",
226
+ prompt: "Create this app from the pinned source.",
227
+ attachments: [],
228
+ message: "Use the local code agent to create this app.",
229
+ });
230
+
231
+ await installCuratedCommunityApp({
232
+ sourceId: "community-qr",
233
+ appId: "QR Team",
234
+ repo: "attacker/arbitrary" as never,
235
+ });
236
+
237
+ const [request] = mocks.startWorkspaceAppCreation.mock.calls[0] ?? [];
238
+ const source = CURATED_COMMUNITY_APP_SOURCES[0]!;
239
+ expect(request).toMatchObject({
240
+ appId: "qr-team",
241
+ template: "community-qr",
242
+ communitySourceId: "community-qr",
243
+ prompt: expect.stringContaining(
244
+ `https://github.com/${source.repo}/tree/${source.ref}/${source.templatePath}`,
245
+ ),
246
+ attachments: [
247
+ expect.objectContaining({
248
+ name: "community-qr/README.md",
249
+ text: "pinned README",
250
+ }),
251
+ ],
252
+ });
253
+ expect(mocks.loadPinnedCommunitySourceAttachments).toHaveBeenCalledWith(
254
+ source,
255
+ "qr-team",
256
+ );
257
+ expect(request.prompt).not.toContain("attacker/arbitrary");
258
+ expect(mocks.recordAudit).toHaveBeenCalledWith(
259
+ expect.objectContaining({
260
+ action: "workspace-app.community-install-requested",
261
+ metadata: expect.objectContaining({
262
+ ref: source.ref,
263
+ blobShas: source.files.map((file) => file.blobSha),
264
+ }),
265
+ }),
266
+ );
267
+ });
268
+
269
+ it("requires an associated workspace and prevents first-party id shadowing", async () => {
270
+ mocks.isLocalAppCreationRuntime.mockReturnValue(false);
271
+ mocks.getAppCreationSettings.mockResolvedValue({ builderProjectId: null });
272
+ await expect(
273
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
274
+ ).rejects.toMatchObject({ errorCode: "community_workspace_required" });
275
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
276
+
277
+ mocks.getAppCreationSettings.mockResolvedValue({
278
+ builderProjectId: "project-1",
279
+ });
280
+ await expect(
281
+ installCuratedCommunityApp({ sourceId: "community-qr", appId: "mail" }),
282
+ ).rejects.toMatchObject({
283
+ errorCode: "community_app_id_shadows_first_party",
284
+ statusCode: 409,
285
+ });
286
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
287
+ });
288
+
289
+ it.each(["plan", "chat", "tasks", "crm", "factory", "removed-agent"])(
290
+ "prevents shadowing first-party agent id %s",
291
+ async (appId) => {
292
+ await expect(
293
+ installCuratedCommunityApp({ sourceId: "community-qr", appId }),
294
+ ).rejects.toMatchObject({
295
+ errorCode: "community_app_id_shadows_first_party",
296
+ statusCode: 409,
297
+ });
298
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
299
+ },
300
+ );
301
+
302
+ it.each([
303
+ ["integrity", "community_source_integrity_failed"],
304
+ ["unavailable", "community_source_unavailable"],
305
+ ] as const)(
306
+ "fails closed and preserves pinned source %s failures",
307
+ async (failureKind, errorCode) => {
308
+ mocks.isLocalAppCreationRuntime.mockReturnValue(true);
309
+ mocks.loadPinnedCommunitySourceAttachments.mockRejectedValueOnce(
310
+ Object.assign(new Error("Pinned source failure"), { failureKind }),
311
+ );
312
+
313
+ await expect(
314
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
315
+ ).rejects.toMatchObject({
316
+ errorCode,
317
+ statusCode: 503,
318
+ });
319
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
320
+ },
321
+ );
322
+
323
+ it("fails closed before starting an unverified Builder-mode install", async () => {
324
+ mocks.isLocalAppCreationRuntime.mockReturnValue(false);
325
+
326
+ await expect(
327
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
328
+ ).rejects.toMatchObject({
329
+ errorCode: "community_builder_install_unverified",
330
+ statusCode: 503,
331
+ message: expect.stringContaining(
332
+ "generated branch files can be read back and verified",
333
+ ),
334
+ });
335
+ expect(mocks.loadPinnedCommunitySourceAttachments).not.toHaveBeenCalled();
336
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
337
+ });
338
+
339
+ it("does not mistake an unrelated app with the same id for a curated install", async () => {
340
+ mocks.listWorkspaceApps.mockResolvedValue([
341
+ {
342
+ id: "community-qr",
343
+ name: "Unrelated QR App",
344
+ path: "/community-qr",
345
+ url: null,
346
+ status: "ready",
347
+ },
348
+ ]);
349
+
350
+ await expect(listCuratedCommunityApps()).resolves.toEqual([
351
+ expect.objectContaining({
352
+ id: "community-qr",
353
+ installed: false,
354
+ pending: false,
355
+ installable: false,
356
+ installUnavailableReason: "app-id-conflict",
357
+ }),
358
+ ]);
359
+ await expect(
360
+ installCuratedCommunityApp({ sourceId: "community-qr" }),
361
+ ).rejects.toMatchObject({
362
+ errorCode: "community_app_id_conflict",
363
+ statusCode: 409,
364
+ });
365
+ expect(mocks.startWorkspaceAppCreation).not.toHaveBeenCalled();
366
+ });
367
+ });