@agent-native/dispatch 0.40.30-nightly-20261021064625 → 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 +7 -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
@@ -6,24 +6,27 @@ import { canonicalA2AAudience, signA2AToken } from "@agent-native/core/a2a";
6
6
  import { isActionContractError } from "@agent-native/core/action";
7
7
  import { describeErrorWithCauses } from "@agent-native/core/agent/engine/error-detail";
8
8
  import { getDbExec } from "@agent-native/core/db";
9
- import { getOrgDomain, isWorkspaceAppAccessAllowed, } from "@agent-native/core/org";
9
+ import { and, eq, inArray } from "@agent-native/core/db/schema";
10
+ import { getOrgDomain, isWorkspaceAppAccessAllowed, workspaceApps, } from "@agent-native/core/org";
10
11
  import { CredentialStoreUnavailableError, FeatureNotConfiguredError, createBuilderProject, getBuilderBranchProjectId, getConfiguredAppBasePath, getRequestContext, inferWorkspaceAppRootHomePath, isIntegrationCallerRequest, readConfiguredWorkspaceAppHomePath, resolveAppRuntimeUrl, resolveVercelDeploymentProtectionHeaders, normalizeBuilderAgentAttachments, runBuilderAgent, } from "@agent-native/core/server";
11
12
  import { getOrgSetting } from "@agent-native/core/settings";
12
13
  import { getSetting, mutateSetting, putSetting, } from "@agent-native/core/settings";
13
- import { BUILDER_CONNECT_PROVIDER, BUILDER_CONNECT_PROVIDER_LABEL, assertValidWorkspaceAppId, connectRequiredResult, normalizeWorkspaceAppHomePath, } from "@agent-native/core/shared";
14
+ import { BUILDER_CONNECT_PROVIDER, BUILDER_CONNECT_PROVIDER_LABEL, assertValidWorkspaceAppId, connectRequiredResult, normalizeWorkspaceAppHomePath, parseAgentProfileMetadata, } from "@agent-native/core/shared";
14
15
  import { resolveAccess } from "@agent-native/core/sharing";
15
- import "../../db/index.js";
16
+ import { getDb } from "../../db/index.js";
16
17
  import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
17
18
  import { getDispatchDefaultOwnerEmail } from "./admin-config.js";
18
19
  import { identityKeyForIncoming } from "./dispatch-integrations.js";
19
20
  import { currentOrgId, currentOwnerEmail, recordAudit, resolveLinkedOwner, } from "./dispatch-store.js";
20
21
  import { projectEnvironmentUrl, requestEnvironmentLane, } from "./environment-lane.js";
22
+ import { invalidateOrgAppsDirectoryCache } from "./org-apps-directory-cache.js";
21
23
  import { createRequest, listSecretOptions } from "./vault-store.js";
22
24
  import { WORKSPACE_APPS_ACTION_PATH } from "./workspace-app-action-auth.js";
23
25
  import { grantWorkspaceResourcesToApp, listWorkspaceResourceOptions, } from "./workspace-resources-store.js";
24
26
  const SETTINGS_KEY = "dispatch-app-creation-settings";
25
27
  const DEFAULT_BUILDER_WORKSPACE_PROJECT_NAME = "Agent-Native Workspace";
26
28
  const APP_CREATION_SETTINGS_AUTHORIZATION_MESSAGE = "Only organization owners and admins can update app creation settings.";
29
+ const WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE = "Only organization owners and admins can update workspace app details.";
27
30
  const APP_CREATION_SETTINGS_REQUIRED_MESSAGE = "An organization owner or admin must configure the Builder workspace project before members can create apps.";
28
31
  const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
29
32
  const WORKSPACE_APP_DEFAULT_VISIBILITY_KEY = "workspace-app-default-visibility";
@@ -57,6 +60,22 @@ class AppCreationSettingsAuthorizationError extends Error {
57
60
  this.name = "AppCreationSettingsAuthorizationError";
58
61
  }
59
62
  }
63
+ class WorkspaceAppMetadataAuthorizationError extends Error {
64
+ statusCode = 403;
65
+ constructor() {
66
+ super(WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE);
67
+ this.name = "WorkspaceAppMetadataAuthorizationError";
68
+ }
69
+ }
70
+ class AppCreationOwnerRoleUnavailableError extends Error {
71
+ statusCode = 503;
72
+ constructor(cause) {
73
+ super("Could not verify the organization role. Try again shortly.", {
74
+ cause,
75
+ });
76
+ this.name = "AppCreationOwnerRoleUnavailableError";
77
+ }
78
+ }
60
79
  class WorkspaceAppOrgRequiredError extends Error {
61
80
  statusCode = 400;
62
81
  constructor() {
@@ -189,7 +208,7 @@ function pendingWorkspaceAppContext() {
189
208
  }
190
209
  }
191
210
  async function readSettingsRecord() {
192
- const raw = await getSetting(scopedSettingsKey()).catch(() => null);
211
+ const raw = await getSetting(scopedSettingsKey());
193
212
  return raw && typeof raw === "object" && !Array.isArray(raw)
194
213
  ? raw
195
214
  : {};
@@ -197,6 +216,14 @@ async function readSettingsRecord() {
197
216
  function cleanOptionalText(value) {
198
217
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
199
218
  }
219
+ function normalizeCommunitySourceId(value) {
220
+ if (typeof value !== "string" ||
221
+ value.length > 100 ||
222
+ !/^community-[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(value)) {
223
+ return undefined;
224
+ }
225
+ return value.toLowerCase();
226
+ }
200
227
  function parseWorkspaceAppMetadataSettings(raw) {
201
228
  const record = raw && typeof raw === "object" && !Array.isArray(raw)
202
229
  ? raw
@@ -214,14 +241,23 @@ function parseWorkspaceAppMetadataSettings(raw) {
214
241
  const override = {};
215
242
  const name = cleanOptionalText(item.name);
216
243
  const description = cleanOptionalText(item.description);
244
+ const profile = parseAgentProfileMetadata({
245
+ whenToUse: item.whenToUse,
246
+ });
217
247
  const sourcePrompt = cleanOptionalText(item.sourcePrompt);
218
248
  const updatedAt = cleanOptionalText(item.updatedAt);
219
249
  const updatedBy = cleanOptionalText(item.updatedBy);
220
250
  const createdBy = cleanOptionalText(item.createdBy);
251
+ const communitySourceId = normalizeCommunitySourceId(item.communitySourceId);
221
252
  if (name)
222
253
  override.name = name;
223
254
  if (description)
224
255
  override.description = description;
256
+ if (profile?.whenToUse)
257
+ override.whenToUse = profile.whenToUse;
258
+ else if (Array.isArray(item.whenToUse) && item.whenToUse.length === 0) {
259
+ override.whenToUse = [];
260
+ }
225
261
  if (item.generated === true)
226
262
  override.generated = true;
227
263
  if (sourcePrompt)
@@ -232,6 +268,8 @@ function parseWorkspaceAppMetadataSettings(raw) {
232
268
  override.updatedBy = updatedBy;
233
269
  if (createdBy)
234
270
  override.createdBy = createdBy;
271
+ if (communitySourceId)
272
+ override.communitySourceId = communitySourceId;
235
273
  if (item.visibility === "private" || item.visibility === "org") {
236
274
  override.visibility = item.visibility;
237
275
  }
@@ -241,31 +279,54 @@ function parseWorkspaceAppMetadataSettings(raw) {
241
279
  return { apps };
242
280
  }
243
281
  async function readWorkspaceAppMetadataSettings() {
244
- const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(() => null);
282
+ const raw = await getSetting(workspaceAppMetadataSettingsKey());
245
283
  return parseWorkspaceAppMetadataSettings(raw);
246
284
  }
247
285
  async function writeWorkspaceAppMetadataOverride(input) {
248
286
  const key = workspaceAppMetadataSettingsKey();
249
- const current = parseWorkspaceAppMetadataSettings(await getSetting(key).catch(() => null));
287
+ const current = parseWorkspaceAppMetadataSettings(await getSetting(key));
250
288
  const appId = input.appId.trim();
251
289
  const existing = current.apps[appId] ?? {};
252
290
  const next = {
253
291
  ...existing,
254
292
  updatedAt: new Date().toISOString(),
255
293
  };
294
+ const hasName = Object.hasOwn(input, "name") && input.name !== undefined;
295
+ const hasDescription = Object.hasOwn(input, "description") && input.description !== undefined;
296
+ const hasWhenToUse = Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
256
297
  const name = cleanOptionalText(input.name);
257
298
  const description = cleanOptionalText(input.description);
299
+ const whenToUse = input.whenToUse;
258
300
  const sourcePrompt = cleanOptionalText(input.sourcePrompt);
259
301
  const updatedBy = cleanOptionalText(input.updatedBy);
260
302
  const createdBy = cleanOptionalText(input.createdBy);
261
- if (name)
262
- next.name = name;
263
- else
264
- delete next.name;
265
- if (description)
266
- next.description = description;
267
- else
268
- delete next.description;
303
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
304
+ if (hasName) {
305
+ if (name)
306
+ next.name = name;
307
+ else
308
+ delete next.name;
309
+ }
310
+ if (hasDescription) {
311
+ if (description)
312
+ next.description = description;
313
+ else
314
+ delete next.description;
315
+ }
316
+ if (hasWhenToUse) {
317
+ const examples = whenToUse ?? [];
318
+ const profile = parseAgentProfileMetadata({
319
+ description: description ?? next.description,
320
+ whenToUse: examples,
321
+ });
322
+ if (!profile) {
323
+ throw new Error("Workspace app when-to-use entries must contain up to 6 non-empty examples of 140 characters or fewer.");
324
+ }
325
+ next.whenToUse = profile.whenToUse ?? [];
326
+ }
327
+ if (description && description.length > 1000) {
328
+ throw new Error("Workspace app descriptions must be 1,000 characters or fewer.");
329
+ }
269
330
  if (input.generated === true)
270
331
  next.generated = true;
271
332
  else if (input.generated === false)
@@ -276,6 +337,14 @@ async function writeWorkspaceAppMetadataOverride(input) {
276
337
  next.updatedBy = updatedBy;
277
338
  if (createdBy)
278
339
  next.createdBy = createdBy;
340
+ if (input.communitySourceId !== undefined) {
341
+ if (input.communitySourceId === null || !communitySourceId) {
342
+ delete next.communitySourceId;
343
+ }
344
+ else {
345
+ next.communitySourceId = communitySourceId;
346
+ }
347
+ }
279
348
  if (input.visibility)
280
349
  next.visibility = input.visibility;
281
350
  current.apps[appId] = next;
@@ -288,17 +357,26 @@ function applyWorkspaceAppMetadataOverride(app, settings) {
288
357
  return app;
289
358
  const name = cleanOptionalText(override.name);
290
359
  const description = cleanOptionalText(override.description);
360
+ const whenToUse = override.whenToUse;
291
361
  const generated = override.generated === true;
292
362
  const shouldApplyName = !!name && !generated;
293
363
  const shouldApplyDescription = !!description && (!generated || !cleanOptionalText(app.description));
294
364
  const visibility = override.visibility;
295
- if (!shouldApplyName && !shouldApplyDescription && !visibility)
365
+ const communitySourceId = override.communitySourceId;
366
+ if (!shouldApplyName &&
367
+ !shouldApplyDescription &&
368
+ whenToUse === undefined &&
369
+ !visibility &&
370
+ !communitySourceId) {
296
371
  return app;
372
+ }
297
373
  return {
298
374
  ...app,
299
375
  ...(shouldApplyName ? { name } : {}),
300
376
  ...(shouldApplyDescription ? { description } : {}),
377
+ ...(whenToUse !== undefined ? { whenToUse } : {}),
301
378
  ...(visibility ? { visibility } : {}),
379
+ ...(communitySourceId ? { communitySourceId } : {}),
302
380
  ...(app.status === "pending" &&
303
381
  !app.createdBy &&
304
382
  (override.createdBy || override.updatedBy)
@@ -385,6 +463,11 @@ function normalizeWorkspaceAppTeams(value) {
385
463
  }
386
464
  function workspaceAppMetadataFromRecord(record) {
387
465
  const config = record["agent-native"] ?? record.agentNative;
466
+ const profile = parseAgentProfileMetadata({
467
+ whenToUse: config && typeof config === "object" && !Array.isArray(config)
468
+ ? config.whenToUse
469
+ : undefined,
470
+ });
388
471
  const workspaceConfig = config && typeof config === "object" && !Array.isArray(config)
389
472
  ? (config.workspaceApp ?? config.workspace ?? config)
390
473
  : {};
@@ -405,6 +488,7 @@ function workspaceAppMetadataFromRecord(record) {
405
488
  createdBy: pick(record.createdBy, record.createdByEmail, record.created_by, metadata.createdBy, metadata.createdByEmail, workspaceConfig.createdBy, workspaceConfig.createdByEmail),
406
489
  owner: pick(record.owner, record.ownerEmail, record.owner_email, metadata.owner, metadata.ownerEmail, workspaceConfig.owner, workspaceConfig.ownerEmail),
407
490
  teams,
491
+ ...(profile?.whenToUse ? { whenToUse: profile.whenToUse } : {}),
408
492
  };
409
493
  }
410
494
  function workspaceAppAudienceFromPackageJson(pkg) {
@@ -462,6 +546,11 @@ function parseWorkspaceAppsManifest(parsed) {
462
546
  const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
463
547
  if (!id || !pathValue.startsWith("/"))
464
548
  return null;
549
+ const profile = parseAgentProfileMetadata({
550
+ whenToUse: entry.whenToUse,
551
+ });
552
+ if (!profile)
553
+ return null;
465
554
  const metadata = workspaceAppMetadataFromRecord(entry);
466
555
  return {
467
556
  id,
@@ -469,6 +558,7 @@ function parseWorkspaceAppsManifest(parsed) {
469
558
  ? entry.name.trim()
470
559
  : titleCase(id),
471
560
  description: typeof entry.description === "string" ? entry.description : "",
561
+ ...(profile.whenToUse ? { whenToUse: profile.whenToUse } : {}),
472
562
  path: pathValue,
473
563
  homePath: normalizeWorkspaceAppHomePath(entry.homePath),
474
564
  url: workspaceAppLink(pathValue, entry.url),
@@ -571,6 +661,7 @@ function parsePendingWorkspaceApps(value) {
571
661
  return null;
572
662
  const now = new Date().toISOString();
573
663
  const createdAt = cleanOptionalString(record.createdAt) ?? now;
664
+ const communitySourceId = normalizeCommunitySourceId(record.communitySourceId);
574
665
  return {
575
666
  id,
576
667
  name: typeof record.name === "string" && record.name.trim()
@@ -600,6 +691,7 @@ function parsePendingWorkspaceApps(value) {
600
691
  ...(record.visibility === "private" || record.visibility === "org"
601
692
  ? { visibility: record.visibility }
602
693
  : {}),
694
+ ...(communitySourceId ? { communitySourceId } : {}),
603
695
  createdAt,
604
696
  updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim()
605
697
  ? record.updatedAt.trim()
@@ -680,6 +772,7 @@ async function reservePendingWorkspaceApp(input) {
680
772
  const now = new Date().toISOString();
681
773
  const context = pendingWorkspaceAppContext();
682
774
  const creatorEmail = currentOwnerEmail();
775
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
683
776
  await mutateSetting(scopedSettingsKey(), async (current) => {
684
777
  const raw = current && typeof current === "object" && !Array.isArray(current)
685
778
  ? current
@@ -709,6 +802,7 @@ async function reservePendingWorkspaceApp(input) {
709
802
  contextId: context?.id ?? null,
710
803
  contextLabel: context?.label ?? null,
711
804
  visibility: input.visibility,
805
+ ...(communitySourceId ? { communitySourceId } : {}),
712
806
  createdBy: creatorEmail,
713
807
  owner: creatorEmail,
714
808
  createdAt: now,
@@ -761,6 +855,7 @@ export async function archiveWorkspaceApp(input) {
761
855
  if (!current.includes(appId))
762
856
  current.push(appId);
763
857
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
858
+ invalidateOrgAppsDirectoryCache();
764
859
  await recordAudit({
765
860
  action: "workspace-app.archived",
766
861
  targetType: "workspace-app",
@@ -776,6 +871,7 @@ export async function unarchiveWorkspaceApp(input) {
776
871
  const raw = await readSettingsRecord();
777
872
  const current = parseArchivedAppIds(raw.archivedAppIds).filter((id) => id !== appId);
778
873
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
874
+ invalidateOrgAppsDirectoryCache();
779
875
  await recordAudit({
780
876
  action: "workspace-app.unarchived",
781
877
  targetType: "workspace-app",
@@ -818,6 +914,7 @@ function pendingAppToSummary(app) {
818
914
  status: "pending",
819
915
  statusLabel: "Pending Builder branch",
820
916
  visibility: app.visibility,
917
+ communitySourceId: app.communitySourceId,
821
918
  builderUrl: app.builderUrl,
822
919
  branchName: app.branchName,
823
920
  createdAt: app.createdAt,
@@ -1161,6 +1258,21 @@ async function filterWorkspaceAppsByAccess(apps) {
1161
1258
  }
1162
1259
  return apps.filter((app) => visibleIds.has(app.id));
1163
1260
  }
1261
+ async function filterOrganizationVisibleWorkspaceApps(apps, orgId) {
1262
+ const readyApps = apps.filter((app) => app.status === "ready" && !app.archived);
1263
+ const visibleIds = new Set();
1264
+ const db = getDb();
1265
+ for (let start = 0; start < readyApps.length; start += 500) {
1266
+ const batch = readyApps.slice(start, start + 500);
1267
+ const rows = await db
1268
+ .select({ id: workspaceApps.id })
1269
+ .from(workspaceApps)
1270
+ .where(and(eq(workspaceApps.orgId, orgId), eq(workspaceApps.visibility, "org"), eq(workspaceApps.orgEnabled, true), inArray(workspaceApps.id, batch.map((app) => app.id))));
1271
+ for (const row of rows)
1272
+ visibleIds.add(row.id);
1273
+ }
1274
+ return readyApps.filter((app) => visibleIds.has(app.id));
1275
+ }
1164
1276
  async function recordPendingWorkspaceApp(input) {
1165
1277
  const now = new Date().toISOString();
1166
1278
  const context = pendingWorkspaceAppContext();
@@ -1174,6 +1286,8 @@ async function recordPendingWorkspaceApp(input) {
1174
1286
  .find(samePendingEntry);
1175
1287
  const creatorEmail = currentOwnerEmail();
1176
1288
  const existingOwnerEmail = existing?.createdBy ?? existing?.owner;
1289
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId) ??
1290
+ normalizeCommunitySourceId(existing?.communitySourceId);
1177
1291
  if (existingOwnerEmail &&
1178
1292
  existingOwnerEmail.trim().toLowerCase() !== creatorEmail.toLowerCase()) {
1179
1293
  throw new Error(`Workspace app "${input.appId}" is already being created by another member.`);
@@ -1190,6 +1304,7 @@ async function recordPendingWorkspaceApp(input) {
1190
1304
  contextId: context?.id ?? null,
1191
1305
  contextLabel: context?.label ?? null,
1192
1306
  visibility: input.visibility,
1307
+ ...(communitySourceId ? { communitySourceId } : {}),
1193
1308
  createdBy: existing?.createdBy ?? existing?.owner ?? creatorEmail,
1194
1309
  owner: existing?.owner ?? existing?.createdBy ?? creatorEmail,
1195
1310
  createdAt: existing?.createdAt || now,
@@ -1205,12 +1320,14 @@ async function recordPendingWorkspaceApp(input) {
1205
1320
  });
1206
1321
  await writeWorkspaceAppMetadataOverride({
1207
1322
  appId: input.appId,
1323
+ name: null,
1208
1324
  description: input.description,
1209
1325
  generated: true,
1210
1326
  sourcePrompt: input.sourcePrompt,
1211
1327
  updatedBy: creatorEmail,
1212
1328
  createdBy: next.createdBy,
1213
1329
  visibility: input.visibility,
1330
+ communitySourceId,
1214
1331
  });
1215
1332
  await recordAudit({
1216
1333
  action: "workspace-app.pending",
@@ -1495,6 +1612,10 @@ export async function updateWorkspaceAppMetadata(input) {
1495
1612
  const appId = input.appId.trim();
1496
1613
  assertValidWorkspaceAppId(appId);
1497
1614
  requireWorkspaceAppOrgId();
1615
+ const role = await requestOwnerRole();
1616
+ if (role !== "owner" && role !== "admin") {
1617
+ throw new WorkspaceAppMetadataAuthorizationError();
1618
+ }
1498
1619
  const apps = await listWorkspaceApps({
1499
1620
  includeAgentCards: false,
1500
1621
  includeArchived: true,
@@ -1502,34 +1623,54 @@ export async function updateWorkspaceAppMetadata(input) {
1502
1623
  const app = apps.find((candidate) => candidate.id === appId);
1503
1624
  if (!app)
1504
1625
  throw new Error(`Workspace app "${appId}" was not found.`);
1505
- const name = input.name == null ? app.name : input.name.trim();
1506
- const description = input.description == null
1507
- ? (app.description ?? undefined)
1508
- : input.description.trim();
1509
- await writeWorkspaceAppMetadataOverride({
1626
+ const nameProvided = Object.hasOwn(input, "name") && input.name !== undefined;
1627
+ const descriptionProvided = Object.hasOwn(input, "description") && input.description !== undefined;
1628
+ const whenToUseProvided = Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
1629
+ const name = nameProvided ? (input.name?.trim() ?? null) : app.name;
1630
+ const description = descriptionProvided
1631
+ ? (input.description?.trim() ?? null)
1632
+ : (app.description ?? undefined);
1633
+ const whenToUse = whenToUseProvided ? (input.whenToUse ?? []) : app.whenToUse;
1634
+ const metadataOverride = {
1510
1635
  appId,
1511
- name,
1512
- description,
1513
- generated: false,
1514
1636
  updatedBy: currentOwnerEmail(),
1637
+ };
1638
+ if (nameProvided)
1639
+ metadataOverride.name = input.name?.trim() ?? null;
1640
+ if (descriptionProvided) {
1641
+ metadataOverride.description = input.description?.trim() ?? null;
1642
+ }
1643
+ if (whenToUseProvided)
1644
+ metadataOverride.whenToUse = input.whenToUse ?? [];
1645
+ if (nameProvided || descriptionProvided)
1646
+ metadataOverride.generated = false;
1647
+ await writeWorkspaceAppMetadataOverride({
1648
+ ...metadataOverride,
1515
1649
  });
1650
+ invalidateOrgAppsDirectoryCache();
1516
1651
  await recordAudit({
1517
1652
  action: "workspace-app.metadata-updated",
1518
1653
  targetType: "workspace-app",
1519
1654
  targetId: appId,
1520
- summary: `Updated workspace app details for ${name}`,
1655
+ summary: `Updated workspace app details for ${name ?? app.name}`,
1521
1656
  metadata: {
1522
- name,
1657
+ name: name ?? app.name,
1523
1658
  descriptionConfigured: !!description,
1659
+ whenToUseCount: whenToUse?.length ?? 0,
1524
1660
  },
1525
1661
  });
1526
1662
  const updated = (await listWorkspaceApps({
1527
1663
  includeAgentCards: false,
1528
1664
  includeArchived: true,
1529
1665
  })).find((candidate) => candidate.id === appId);
1530
- return updated ?? { ...app, name, description };
1666
+ return (updated ?? {
1667
+ ...app,
1668
+ name: name ?? app.name,
1669
+ description: description ?? app.description,
1670
+ whenToUse,
1671
+ });
1531
1672
  }
1532
- export async function listWorkspaceApps(options = {}) {
1673
+ async function listWorkspaceAppsWithPrincipal(options = {}, principal = "user") {
1533
1674
  const finalize = async (apps, { persist = true } = {}) => {
1534
1675
  const annotated = await applyArchivedAndPending(apps);
1535
1676
  const recorded = await ensureWorkspaceAppRecords(annotated, {
@@ -1538,7 +1679,10 @@ export async function listWorkspaceApps(options = {}) {
1538
1679
  const listed = options.includeArchived
1539
1680
  ? recorded
1540
1681
  : recorded.filter((app) => !app.archived);
1541
- const visible = await filterWorkspaceAppsByAccess(filterAppsByAudience(listed, options.audience));
1682
+ const candidates = filterAppsByAudience(listed, options.audience);
1683
+ const visible = principal === "organization"
1684
+ ? await filterOrganizationVisibleWorkspaceApps(candidates, requireWorkspaceAppOrgId())
1685
+ : await filterWorkspaceAppsByAccess(candidates);
1542
1686
  return maybeIncludeAgentCards(visible, options);
1543
1687
  };
1544
1688
  let gatewayDenial = null;
@@ -1598,6 +1742,13 @@ export async function listWorkspaceApps(options = {}) {
1598
1742
  return finalize(apps, { persist: !unverified });
1599
1743
  return finalize([], { persist: !unverified });
1600
1744
  }
1745
+ export function listWorkspaceApps(options = {}) {
1746
+ return listWorkspaceAppsWithPrincipal(options, "user");
1747
+ }
1748
+ export async function listOrganizationVisibleWorkspaceApps() {
1749
+ requireWorkspaceAppOrgId();
1750
+ return listWorkspaceAppsWithPrincipal({ includeAgentCards: false, includeArchived: false, audience: "all" }, "organization");
1751
+ }
1601
1752
  const ADDABLE_TEMPLATES = [
1602
1753
  {
1603
1754
  name: "mail",
@@ -1748,6 +1899,8 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
1748
1899
  try {
1749
1900
  await writeWorkspaceAppMetadataOverride({
1750
1901
  appId,
1902
+ name: null,
1903
+ description: null,
1751
1904
  updatedBy: creatorEmail,
1752
1905
  createdBy: creatorEmail,
1753
1906
  visibility,
@@ -1783,6 +1936,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
1783
1936
  visibility,
1784
1937
  },
1785
1938
  ]);
1939
+ invalidateOrgAppsDirectoryCache();
1786
1940
  return { appId, template, output };
1787
1941
  }
1788
1942
  function runScaffoldCli(input) {
@@ -1945,8 +2099,8 @@ async function requestOwnerRole() {
1945
2099
  const role = rows[0]?.role;
1946
2100
  return typeof role === "string" ? role : null;
1947
2101
  }
1948
- catch {
1949
- return null;
2102
+ catch (error) {
2103
+ throw new AppCreationOwnerRoleUnavailableError(error);
1950
2104
  }
1951
2105
  }
1952
2106
  async function assertCanManageAppCreationSettings() {
@@ -2198,6 +2352,10 @@ export async function startWorkspaceAppCreation(input) {
2198
2352
  description: input.description,
2199
2353
  template: input.template,
2200
2354
  });
2355
+ if (input.communitySourceId !== undefined &&
2356
+ !normalizeCommunitySourceId(input.communitySourceId)) {
2357
+ throw new Error("Community source ids must be valid bounded identifiers.");
2358
+ }
2201
2359
  assertValidWorkspaceAppId(initial.appId);
2202
2360
  requireWorkspaceAppOrgId();
2203
2361
  normalizeBuilderAgentAttachments(input.attachments);
@@ -2244,15 +2402,41 @@ export async function startWorkspaceAppCreation(input) {
2244
2402
  const appDescription = input.description?.trim() ||
2245
2403
  generateWorkspaceAppDescription(input.prompt, built.appId);
2246
2404
  if (isLocal) {
2247
- await writeWorkspaceAppMetadataOverride({
2248
- appId: built.appId,
2249
- description: appDescription,
2250
- generated: true,
2251
- sourcePrompt: input.prompt,
2252
- updatedBy: currentOwnerEmail(),
2253
- createdBy: currentOwnerEmail(),
2254
- visibility: creationVisibility,
2255
- });
2405
+ if (input.communitySourceId) {
2406
+ try {
2407
+ await reservePendingWorkspaceApp({
2408
+ appId: built.appId,
2409
+ description: appDescription,
2410
+ projectId: null,
2411
+ visibility: creationVisibility,
2412
+ communitySourceId: input.communitySourceId,
2413
+ });
2414
+ }
2415
+ catch (err) {
2416
+ if (err instanceof WorkspaceAppIdTakenError)
2417
+ return appIdTakenResult(err);
2418
+ throw err;
2419
+ }
2420
+ }
2421
+ try {
2422
+ await writeWorkspaceAppMetadataOverride({
2423
+ appId: built.appId,
2424
+ name: null,
2425
+ description: appDescription,
2426
+ generated: true,
2427
+ sourcePrompt: input.prompt,
2428
+ updatedBy: currentOwnerEmail(),
2429
+ createdBy: currentOwnerEmail(),
2430
+ visibility: creationVisibility,
2431
+ communitySourceId: input.communitySourceId,
2432
+ });
2433
+ }
2434
+ catch (error) {
2435
+ if (input.communitySourceId) {
2436
+ await releasePendingWorkspaceAppReservation(built.appId);
2437
+ }
2438
+ throw error;
2439
+ }
2256
2440
  await requestSelectedVaultKeys({
2257
2441
  appId: built.appId,
2258
2442
  selectedKeys,
@@ -2265,6 +2449,20 @@ export async function startWorkspaceAppCreation(input) {
2265
2449
  mode: "local-agent",
2266
2450
  appId: built.appId,
2267
2451
  prompt,
2452
+ ...(input.communitySourceId && input.attachments?.length
2453
+ ? {
2454
+ attachments: input.attachments.flatMap((attachment) => attachment.type === "upload" && attachment.text !== undefined
2455
+ ? [
2456
+ {
2457
+ type: "file",
2458
+ name: attachment.name,
2459
+ contentType: attachment.contentType,
2460
+ text: attachment.text,
2461
+ },
2462
+ ]
2463
+ : []),
2464
+ }
2465
+ : {}),
2268
2466
  message: "Use the local code agent to create this app in the workspace, then open it from /dispatch/apps.",
2269
2467
  };
2270
2468
  }
@@ -2300,6 +2498,9 @@ export async function startWorkspaceAppCreation(input) {
2300
2498
  description: appDescription,
2301
2499
  projectId: builderProjectId,
2302
2500
  visibility: creationVisibility,
2501
+ ...(normalizeCommunitySourceId(input.communitySourceId)
2502
+ ? { communitySourceId: input.communitySourceId }
2503
+ : {}),
2303
2504
  });
2304
2505
  }
2305
2506
  catch (err) {
@@ -2307,6 +2508,23 @@ export async function startWorkspaceAppCreation(input) {
2307
2508
  return appIdTakenResult(err);
2308
2509
  throw err;
2309
2510
  }
2511
+ if (input.communitySourceId) {
2512
+ try {
2513
+ await writeWorkspaceAppMetadataOverride({
2514
+ appId: built.appId,
2515
+ communitySourceId: input.communitySourceId,
2516
+ });
2517
+ }
2518
+ catch (error) {
2519
+ try {
2520
+ await releasePendingWorkspaceAppReservation(built.appId);
2521
+ }
2522
+ catch (cleanupError) {
2523
+ console.warn("[dispatch] failed to release pending workspace app reservation", cleanupError);
2524
+ }
2525
+ throw error;
2526
+ }
2527
+ }
2310
2528
  let result;
2311
2529
  try {
2312
2530
  result = normalizeBuilderRunResult(await runBuilderAgent({
@@ -2337,6 +2555,7 @@ export async function startWorkspaceAppCreation(input) {
2337
2555
  description: appDescription,
2338
2556
  sourcePrompt: input.prompt,
2339
2557
  visibility: creationVisibility,
2558
+ communitySourceId: input.communitySourceId,
2340
2559
  branchName: result.branchName,
2341
2560
  builderUrl: result.url,
2342
2561
  });