@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +15 -60
  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-connected-agents.js +7 -3
  26. package/dist/actions/list-connected-agents.js.map +1 -1
  27. package/dist/actions/list-curated-community-apps.d.ts +3 -0
  28. package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
  29. package/dist/actions/list-curated-community-apps.js +10 -0
  30. package/dist/actions/list-curated-community-apps.js.map +1 -0
  31. package/dist/actions/list-workspace-connections.d.ts.map +1 -1
  32. package/dist/actions/list-workspace-connections.js +13 -18
  33. package/dist/actions/list-workspace-connections.js.map +1 -1
  34. package/dist/actions/provider-api-register.d.ts +14 -14
  35. package/dist/actions/remix-workspace-template.d.ts +6 -0
  36. package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
  37. package/dist/actions/update-workspace-app-metadata.js +26 -5
  38. package/dist/actions/update-workspace-app-metadata.js.map +1 -1
  39. package/dist/components/action-query-error.d.ts +2 -1
  40. package/dist/components/action-query-error.d.ts.map +1 -1
  41. package/dist/components/action-query-error.js +25 -2
  42. package/dist/components/action-query-error.js.map +1 -1
  43. package/dist/components/chat-home-apps.d.ts.map +1 -1
  44. package/dist/components/chat-home-apps.js +2 -2
  45. package/dist/components/chat-home-apps.js.map +1 -1
  46. package/dist/components/community-apps-section.d.ts +2 -0
  47. package/dist/components/community-apps-section.d.ts.map +1 -0
  48. package/dist/components/community-apps-section.js +90 -0
  49. package/dist/components/community-apps-section.js.map +1 -0
  50. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  51. package/dist/components/dispatch-control-plane.js +1 -1
  52. package/dist/components/dispatch-control-plane.js.map +1 -1
  53. package/dist/components/layout/Layout.d.ts +1 -0
  54. package/dist/components/layout/Layout.d.ts.map +1 -1
  55. package/dist/components/layout/Layout.js +7 -0
  56. package/dist/components/layout/Layout.js.map +1 -1
  57. package/dist/components/workspace-app-card.d.ts.map +1 -1
  58. package/dist/components/workspace-app-card.js +63 -9
  59. package/dist/components/workspace-app-card.js.map +1 -1
  60. package/dist/lib/connect-apps.d.ts +1 -2
  61. package/dist/lib/connect-apps.d.ts.map +1 -1
  62. package/dist/lib/connect-apps.js +41 -65
  63. package/dist/lib/connect-apps.js.map +1 -1
  64. package/dist/lib/workspace-apps.d.ts +1 -0
  65. package/dist/lib/workspace-apps.d.ts.map +1 -1
  66. package/dist/lib/workspace-apps.js.map +1 -1
  67. package/dist/routes/pages/$appId.d.ts.map +1 -1
  68. package/dist/routes/pages/$appId.js +1 -1
  69. package/dist/routes/pages/$appId.js.map +1 -1
  70. package/dist/routes/pages/apps.d.ts.map +1 -1
  71. package/dist/routes/pages/apps.js +3 -2
  72. package/dist/routes/pages/apps.js.map +1 -1
  73. package/dist/routes/pages/connect.d.ts.map +1 -1
  74. package/dist/routes/pages/connect.js +5 -7
  75. package/dist/routes/pages/connect.js.map +1 -1
  76. package/dist/server/index.d.ts +2 -0
  77. package/dist/server/index.d.ts.map +1 -1
  78. package/dist/server/index.js +2 -0
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/lib/app-creation-store.d.ts +11 -1
  81. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  82. package/dist/server/lib/app-creation-store.js +379 -92
  83. package/dist/server/lib/app-creation-store.js.map +1 -1
  84. package/dist/server/lib/community-apps.d.ts +45 -0
  85. package/dist/server/lib/community-apps.d.ts.map +1 -0
  86. package/dist/server/lib/community-apps.js +267 -0
  87. package/dist/server/lib/community-apps.js.map +1 -0
  88. package/dist/server/lib/community-source.d.ts +19 -0
  89. package/dist/server/lib/community-source.d.ts.map +1 -0
  90. package/dist/server/lib/community-source.js +118 -0
  91. package/dist/server/lib/community-source.js.map +1 -0
  92. package/dist/server/lib/connect-apps.d.ts +4 -0
  93. package/dist/server/lib/connect-apps.d.ts.map +1 -0
  94. package/dist/server/lib/connect-apps.js +99 -0
  95. package/dist/server/lib/connect-apps.js.map +1 -0
  96. package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
  97. package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
  98. package/dist/server/lib/org-apps-directory-cache.js +44 -0
  99. package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
  100. package/dist/server/plugins/integrations.js +3 -3
  101. package/dist/server/plugins/integrations.js.map +1 -1
  102. package/dist/shared/labs.d.ts +2 -0
  103. package/dist/shared/labs.d.ts.map +1 -0
  104. package/dist/shared/labs.js +8 -0
  105. package/dist/shared/labs.js.map +1 -0
  106. package/package.json +5 -4
  107. package/src/actions/connect-external-agent.spec.ts +101 -3
  108. package/src/actions/connect-external-agent.ts +12 -11
  109. package/src/actions/index.spec.ts +13 -0
  110. package/src/actions/index.ts +8 -0
  111. package/src/actions/inspect-connect-agent-card.ts +17 -0
  112. package/src/actions/install-curated-community-app.ts +32 -0
  113. package/src/actions/list-connect-marketplace-apps.ts +17 -0
  114. package/src/actions/list-connected-agents.spec.ts +17 -1
  115. package/src/actions/list-connected-agents.ts +7 -2
  116. package/src/actions/list-curated-community-apps.ts +12 -0
  117. package/src/actions/list-workspace-connections.spec.ts +14 -3
  118. package/src/actions/list-workspace-connections.ts +13 -17
  119. package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
  120. package/src/actions/update-workspace-app-metadata.ts +48 -24
  121. package/src/components/action-query-error.spec.tsx +20 -0
  122. package/src/components/action-query-error.tsx +40 -0
  123. package/src/components/chat-home-apps.spec.tsx +32 -1
  124. package/src/components/chat-home-apps.tsx +6 -1
  125. package/src/components/community-apps-section.tsx +224 -0
  126. package/src/components/dispatch-control-plane.tsx +1 -0
  127. package/src/components/layout/Layout.tsx +9 -0
  128. package/src/components/workspace-app-card.spec.tsx +226 -1
  129. package/src/components/workspace-app-card.tsx +115 -12
  130. package/src/lib/connect-apps.spec.ts +118 -66
  131. package/src/lib/connect-apps.ts +47 -82
  132. package/src/lib/workspace-apps.ts +1 -0
  133. package/src/routes/pages/$appId.tsx +1 -0
  134. package/src/routes/pages/apps.spec.tsx +227 -3
  135. package/src/routes/pages/apps.tsx +5 -0
  136. package/src/routes/pages/connect.spec.tsx +53 -6
  137. package/src/routes/pages/connect.tsx +14 -11
  138. package/src/server/index.ts +9 -0
  139. package/src/server/lib/app-creation-store.spec.ts +820 -16
  140. package/src/server/lib/app-creation-store.ts +504 -101
  141. package/src/server/lib/community-apps.spec.ts +367 -0
  142. package/src/server/lib/community-apps.ts +372 -0
  143. package/src/server/lib/community-source.spec.ts +151 -0
  144. package/src/server/lib/community-source.ts +186 -0
  145. package/src/server/lib/connect-apps.ts +149 -0
  146. package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
  147. package/src/server/lib/org-apps-directory-cache.ts +66 -0
  148. package/src/server/plugins/integrations.spec.ts +60 -0
  149. package/src/server/plugins/integrations.ts +3 -3
  150. package/src/shared/labs.ts +9 -0
@@ -2,28 +2,31 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { canonicalA2AAudience, signA2AToken } from "@agent-native/core/a2a";
6
- import { isActionContractError } from "@agent-native/core/action";
5
+ import { canonicalA2AAudience, getGlobalA2ASecret, signA2AToken, } from "@agent-native/core/a2a";
6
+ import { ActionContractError, 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 { getOrgA2ASecret, 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() {
@@ -70,13 +89,11 @@ function requireWorkspaceAppOrgId() {
70
89
  throw new WorkspaceAppOrgRequiredError();
71
90
  return orgId;
72
91
  }
73
- class WorkspaceAppsGatewayAuthorizationError extends Error {
74
- constructor(statusCode, message = `Workspace apps gateway rejected the request with HTTP ${statusCode}.`) {
75
- super(message);
92
+ class WorkspaceAppsGatewayAuthorizationError extends ActionContractError {
93
+ constructor(statusCode, errorCode = "workspace_gateway_rejected", message = `Workspace apps gateway rejected the request with HTTP ${statusCode}.`) {
94
+ super(message, { statusCode, errorCode });
76
95
  this.name = "WorkspaceAppsGatewayAuthorizationError";
77
- this.statusCode = statusCode;
78
96
  }
79
- statusCode;
80
97
  }
81
98
  function warnWorkspaceAppsGatewayDenial(denial, source) {
82
99
  if (!denial)
@@ -189,7 +206,7 @@ function pendingWorkspaceAppContext() {
189
206
  }
190
207
  }
191
208
  async function readSettingsRecord() {
192
- const raw = await getSetting(scopedSettingsKey()).catch(() => null);
209
+ const raw = await getSetting(scopedSettingsKey());
193
210
  return raw && typeof raw === "object" && !Array.isArray(raw)
194
211
  ? raw
195
212
  : {};
@@ -197,6 +214,14 @@ async function readSettingsRecord() {
197
214
  function cleanOptionalText(value) {
198
215
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
199
216
  }
217
+ function normalizeCommunitySourceId(value) {
218
+ if (typeof value !== "string" ||
219
+ value.length > 100 ||
220
+ !/^community-[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(value)) {
221
+ return undefined;
222
+ }
223
+ return value.toLowerCase();
224
+ }
200
225
  function parseWorkspaceAppMetadataSettings(raw) {
201
226
  const record = raw && typeof raw === "object" && !Array.isArray(raw)
202
227
  ? raw
@@ -214,14 +239,23 @@ function parseWorkspaceAppMetadataSettings(raw) {
214
239
  const override = {};
215
240
  const name = cleanOptionalText(item.name);
216
241
  const description = cleanOptionalText(item.description);
242
+ const profile = parseAgentProfileMetadata({
243
+ whenToUse: item.whenToUse,
244
+ });
217
245
  const sourcePrompt = cleanOptionalText(item.sourcePrompt);
218
246
  const updatedAt = cleanOptionalText(item.updatedAt);
219
247
  const updatedBy = cleanOptionalText(item.updatedBy);
220
248
  const createdBy = cleanOptionalText(item.createdBy);
249
+ const communitySourceId = normalizeCommunitySourceId(item.communitySourceId);
221
250
  if (name)
222
251
  override.name = name;
223
252
  if (description)
224
253
  override.description = description;
254
+ if (profile?.whenToUse)
255
+ override.whenToUse = profile.whenToUse;
256
+ else if (Array.isArray(item.whenToUse) && item.whenToUse.length === 0) {
257
+ override.whenToUse = [];
258
+ }
225
259
  if (item.generated === true)
226
260
  override.generated = true;
227
261
  if (sourcePrompt)
@@ -232,6 +266,8 @@ function parseWorkspaceAppMetadataSettings(raw) {
232
266
  override.updatedBy = updatedBy;
233
267
  if (createdBy)
234
268
  override.createdBy = createdBy;
269
+ if (communitySourceId)
270
+ override.communitySourceId = communitySourceId;
235
271
  if (item.visibility === "private" || item.visibility === "org") {
236
272
  override.visibility = item.visibility;
237
273
  }
@@ -241,31 +277,54 @@ function parseWorkspaceAppMetadataSettings(raw) {
241
277
  return { apps };
242
278
  }
243
279
  async function readWorkspaceAppMetadataSettings() {
244
- const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(() => null);
280
+ const raw = await getSetting(workspaceAppMetadataSettingsKey());
245
281
  return parseWorkspaceAppMetadataSettings(raw);
246
282
  }
247
283
  async function writeWorkspaceAppMetadataOverride(input) {
248
284
  const key = workspaceAppMetadataSettingsKey();
249
- const current = parseWorkspaceAppMetadataSettings(await getSetting(key).catch(() => null));
285
+ const current = parseWorkspaceAppMetadataSettings(await getSetting(key));
250
286
  const appId = input.appId.trim();
251
287
  const existing = current.apps[appId] ?? {};
252
288
  const next = {
253
289
  ...existing,
254
290
  updatedAt: new Date().toISOString(),
255
291
  };
292
+ const hasName = Object.hasOwn(input, "name") && input.name !== undefined;
293
+ const hasDescription = Object.hasOwn(input, "description") && input.description !== undefined;
294
+ const hasWhenToUse = Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
256
295
  const name = cleanOptionalText(input.name);
257
296
  const description = cleanOptionalText(input.description);
297
+ const whenToUse = input.whenToUse;
258
298
  const sourcePrompt = cleanOptionalText(input.sourcePrompt);
259
299
  const updatedBy = cleanOptionalText(input.updatedBy);
260
300
  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;
301
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
302
+ if (hasName) {
303
+ if (name)
304
+ next.name = name;
305
+ else
306
+ delete next.name;
307
+ }
308
+ if (hasDescription) {
309
+ if (description)
310
+ next.description = description;
311
+ else
312
+ delete next.description;
313
+ }
314
+ if (hasWhenToUse) {
315
+ const examples = whenToUse ?? [];
316
+ const profile = parseAgentProfileMetadata({
317
+ description: description ?? next.description,
318
+ whenToUse: examples,
319
+ });
320
+ if (!profile) {
321
+ throw new Error("Workspace app when-to-use entries must contain up to 6 non-empty examples of 140 characters or fewer.");
322
+ }
323
+ next.whenToUse = profile.whenToUse ?? [];
324
+ }
325
+ if (description && description.length > 1000) {
326
+ throw new Error("Workspace app descriptions must be 1,000 characters or fewer.");
327
+ }
269
328
  if (input.generated === true)
270
329
  next.generated = true;
271
330
  else if (input.generated === false)
@@ -276,6 +335,14 @@ async function writeWorkspaceAppMetadataOverride(input) {
276
335
  next.updatedBy = updatedBy;
277
336
  if (createdBy)
278
337
  next.createdBy = createdBy;
338
+ if (input.communitySourceId !== undefined) {
339
+ if (input.communitySourceId === null || !communitySourceId) {
340
+ delete next.communitySourceId;
341
+ }
342
+ else {
343
+ next.communitySourceId = communitySourceId;
344
+ }
345
+ }
279
346
  if (input.visibility)
280
347
  next.visibility = input.visibility;
281
348
  current.apps[appId] = next;
@@ -288,17 +355,26 @@ function applyWorkspaceAppMetadataOverride(app, settings) {
288
355
  return app;
289
356
  const name = cleanOptionalText(override.name);
290
357
  const description = cleanOptionalText(override.description);
358
+ const whenToUse = override.whenToUse;
291
359
  const generated = override.generated === true;
292
360
  const shouldApplyName = !!name && !generated;
293
361
  const shouldApplyDescription = !!description && (!generated || !cleanOptionalText(app.description));
294
362
  const visibility = override.visibility;
295
- if (!shouldApplyName && !shouldApplyDescription && !visibility)
363
+ const communitySourceId = override.communitySourceId;
364
+ if (!shouldApplyName &&
365
+ !shouldApplyDescription &&
366
+ whenToUse === undefined &&
367
+ !visibility &&
368
+ !communitySourceId) {
296
369
  return app;
370
+ }
297
371
  return {
298
372
  ...app,
299
373
  ...(shouldApplyName ? { name } : {}),
300
374
  ...(shouldApplyDescription ? { description } : {}),
375
+ ...(whenToUse !== undefined ? { whenToUse } : {}),
301
376
  ...(visibility ? { visibility } : {}),
377
+ ...(communitySourceId ? { communitySourceId } : {}),
302
378
  ...(app.status === "pending" &&
303
379
  !app.createdBy &&
304
380
  (override.createdBy || override.updatedBy)
@@ -385,6 +461,11 @@ function normalizeWorkspaceAppTeams(value) {
385
461
  }
386
462
  function workspaceAppMetadataFromRecord(record) {
387
463
  const config = record["agent-native"] ?? record.agentNative;
464
+ const profile = parseAgentProfileMetadata({
465
+ whenToUse: config && typeof config === "object" && !Array.isArray(config)
466
+ ? config.whenToUse
467
+ : undefined,
468
+ });
388
469
  const workspaceConfig = config && typeof config === "object" && !Array.isArray(config)
389
470
  ? (config.workspaceApp ?? config.workspace ?? config)
390
471
  : {};
@@ -405,6 +486,7 @@ function workspaceAppMetadataFromRecord(record) {
405
486
  createdBy: pick(record.createdBy, record.createdByEmail, record.created_by, metadata.createdBy, metadata.createdByEmail, workspaceConfig.createdBy, workspaceConfig.createdByEmail),
406
487
  owner: pick(record.owner, record.ownerEmail, record.owner_email, metadata.owner, metadata.ownerEmail, workspaceConfig.owner, workspaceConfig.ownerEmail),
407
488
  teams,
489
+ ...(profile?.whenToUse ? { whenToUse: profile.whenToUse } : {}),
408
490
  };
409
491
  }
410
492
  function workspaceAppAudienceFromPackageJson(pkg) {
@@ -462,6 +544,11 @@ function parseWorkspaceAppsManifest(parsed) {
462
544
  const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
463
545
  if (!id || !pathValue.startsWith("/"))
464
546
  return null;
547
+ const profile = parseAgentProfileMetadata({
548
+ whenToUse: entry.whenToUse,
549
+ });
550
+ if (!profile)
551
+ return null;
465
552
  const metadata = workspaceAppMetadataFromRecord(entry);
466
553
  return {
467
554
  id,
@@ -469,6 +556,7 @@ function parseWorkspaceAppsManifest(parsed) {
469
556
  ? entry.name.trim()
470
557
  : titleCase(id),
471
558
  description: typeof entry.description === "string" ? entry.description : "",
559
+ ...(profile.whenToUse ? { whenToUse: profile.whenToUse } : {}),
472
560
  path: pathValue,
473
561
  homePath: normalizeWorkspaceAppHomePath(entry.homePath),
474
562
  url: workspaceAppLink(pathValue, entry.url),
@@ -491,6 +579,20 @@ function parseWorkspaceAppsManifest(parsed) {
491
579
  .sort(sortWorkspaceApps);
492
580
  return apps.length ? apps : null;
493
581
  }
582
+ function parseWorkspaceAppsGatewayResponse(parsed) {
583
+ const rawApps = parsed && typeof parsed === "object" && !Array.isArray(parsed)
584
+ ? parsed.apps
585
+ : parsed;
586
+ if (!Array.isArray(rawApps))
587
+ return null;
588
+ if (rawApps.length === 0)
589
+ return [];
590
+ const apps = parseWorkspaceAppsManifest(parsed);
591
+ if (!apps || apps.length !== rawApps.length)
592
+ return null;
593
+ const ids = new Set(apps.map((app) => app.id.toLowerCase()));
594
+ return ids.size === apps.length ? apps : null;
595
+ }
494
596
  function parseDateMs(value) {
495
597
  if (!value)
496
598
  return null;
@@ -571,6 +673,7 @@ function parsePendingWorkspaceApps(value) {
571
673
  return null;
572
674
  const now = new Date().toISOString();
573
675
  const createdAt = cleanOptionalString(record.createdAt) ?? now;
676
+ const communitySourceId = normalizeCommunitySourceId(record.communitySourceId);
574
677
  return {
575
678
  id,
576
679
  name: typeof record.name === "string" && record.name.trim()
@@ -600,6 +703,7 @@ function parsePendingWorkspaceApps(value) {
600
703
  ...(record.visibility === "private" || record.visibility === "org"
601
704
  ? { visibility: record.visibility }
602
705
  : {}),
706
+ ...(communitySourceId ? { communitySourceId } : {}),
603
707
  createdAt,
604
708
  updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim()
605
709
  ? record.updatedAt.trim()
@@ -680,6 +784,7 @@ async function reservePendingWorkspaceApp(input) {
680
784
  const now = new Date().toISOString();
681
785
  const context = pendingWorkspaceAppContext();
682
786
  const creatorEmail = currentOwnerEmail();
787
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
683
788
  await mutateSetting(scopedSettingsKey(), async (current) => {
684
789
  const raw = current && typeof current === "object" && !Array.isArray(current)
685
790
  ? current
@@ -709,6 +814,7 @@ async function reservePendingWorkspaceApp(input) {
709
814
  contextId: context?.id ?? null,
710
815
  contextLabel: context?.label ?? null,
711
816
  visibility: input.visibility,
817
+ ...(communitySourceId ? { communitySourceId } : {}),
712
818
  createdBy: creatorEmail,
713
819
  owner: creatorEmail,
714
820
  createdAt: now,
@@ -761,6 +867,7 @@ export async function archiveWorkspaceApp(input) {
761
867
  if (!current.includes(appId))
762
868
  current.push(appId);
763
869
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
870
+ invalidateOrgAppsDirectoryCache();
764
871
  await recordAudit({
765
872
  action: "workspace-app.archived",
766
873
  targetType: "workspace-app",
@@ -776,6 +883,7 @@ export async function unarchiveWorkspaceApp(input) {
776
883
  const raw = await readSettingsRecord();
777
884
  const current = parseArchivedAppIds(raw.archivedAppIds).filter((id) => id !== appId);
778
885
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
886
+ invalidateOrgAppsDirectoryCache();
779
887
  await recordAudit({
780
888
  action: "workspace-app.unarchived",
781
889
  targetType: "workspace-app",
@@ -818,6 +926,7 @@ function pendingAppToSummary(app) {
818
926
  status: "pending",
819
927
  statusLabel: "Pending Builder branch",
820
928
  visibility: app.visibility,
929
+ communitySourceId: app.communitySourceId,
821
930
  builderUrl: app.builderUrl,
822
931
  branchName: app.branchName,
823
932
  createdAt: app.createdAt,
@@ -1161,6 +1270,21 @@ async function filterWorkspaceAppsByAccess(apps) {
1161
1270
  }
1162
1271
  return apps.filter((app) => visibleIds.has(app.id));
1163
1272
  }
1273
+ async function filterOrganizationVisibleWorkspaceApps(apps, orgId) {
1274
+ const readyApps = apps.filter((app) => app.status === "ready" && !app.archived);
1275
+ const visibleIds = new Set();
1276
+ const db = getDb();
1277
+ for (let start = 0; start < readyApps.length; start += 500) {
1278
+ const batch = readyApps.slice(start, start + 500);
1279
+ const rows = await db
1280
+ .select({ id: workspaceApps.id })
1281
+ .from(workspaceApps)
1282
+ .where(and(eq(workspaceApps.orgId, orgId), eq(workspaceApps.visibility, "org"), eq(workspaceApps.orgEnabled, true), inArray(workspaceApps.id, batch.map((app) => app.id))));
1283
+ for (const row of rows)
1284
+ visibleIds.add(row.id);
1285
+ }
1286
+ return readyApps.filter((app) => visibleIds.has(app.id));
1287
+ }
1164
1288
  async function recordPendingWorkspaceApp(input) {
1165
1289
  const now = new Date().toISOString();
1166
1290
  const context = pendingWorkspaceAppContext();
@@ -1174,6 +1298,8 @@ async function recordPendingWorkspaceApp(input) {
1174
1298
  .find(samePendingEntry);
1175
1299
  const creatorEmail = currentOwnerEmail();
1176
1300
  const existingOwnerEmail = existing?.createdBy ?? existing?.owner;
1301
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId) ??
1302
+ normalizeCommunitySourceId(existing?.communitySourceId);
1177
1303
  if (existingOwnerEmail &&
1178
1304
  existingOwnerEmail.trim().toLowerCase() !== creatorEmail.toLowerCase()) {
1179
1305
  throw new Error(`Workspace app "${input.appId}" is already being created by another member.`);
@@ -1190,6 +1316,7 @@ async function recordPendingWorkspaceApp(input) {
1190
1316
  contextId: context?.id ?? null,
1191
1317
  contextLabel: context?.label ?? null,
1192
1318
  visibility: input.visibility,
1319
+ ...(communitySourceId ? { communitySourceId } : {}),
1193
1320
  createdBy: existing?.createdBy ?? existing?.owner ?? creatorEmail,
1194
1321
  owner: existing?.owner ?? existing?.createdBy ?? creatorEmail,
1195
1322
  createdAt: existing?.createdAt || now,
@@ -1205,12 +1332,14 @@ async function recordPendingWorkspaceApp(input) {
1205
1332
  });
1206
1333
  await writeWorkspaceAppMetadataOverride({
1207
1334
  appId: input.appId,
1335
+ name: null,
1208
1336
  description: input.description,
1209
1337
  generated: true,
1210
1338
  sourcePrompt: input.sourcePrompt,
1211
1339
  updatedBy: creatorEmail,
1212
1340
  createdBy: next.createdBy,
1213
1341
  visibility: input.visibility,
1342
+ communitySourceId,
1214
1343
  });
1215
1344
  await recordAudit({
1216
1345
  action: "workspace-app.pending",
@@ -1254,27 +1383,64 @@ async function readWorkspaceAppsFromGateway() {
1254
1383
  return null;
1255
1384
  }
1256
1385
  const requestContext = getRequestContext();
1386
+ const isLocalGateway = isLocalWorkspaceGateway(baseUrl);
1387
+ const requestOrigin = requestContext?.requestOrigin;
1388
+ const isSameOriginGateway = requestOrigin
1389
+ ? (() => {
1390
+ try {
1391
+ return new URL(requestOrigin).origin === baseUrl.origin;
1392
+ }
1393
+ catch {
1394
+ // coercion-ok: an invalid request origin cannot prove a self-fetch.
1395
+ return false;
1396
+ }
1397
+ })()
1398
+ : false;
1399
+ if (isSameOriginGateway && !isLocalGateway)
1400
+ return null;
1257
1401
  const authHeaders = {};
1258
1402
  if (requestContext?.userEmail) {
1259
- const orgDomain = requestContext.orgId
1260
- ? (await getOrgDomain(requestContext.orgId))?.trim().toLowerCase()
1261
- : undefined;
1403
+ let orgDomain;
1404
+ let orgSecret;
1405
+ if (requestContext.orgId) {
1406
+ try {
1407
+ orgDomain =
1408
+ (await getOrgDomain(requestContext.orgId))?.trim().toLowerCase() ||
1409
+ undefined;
1410
+ }
1411
+ catch {
1412
+ throw new WorkspaceAppsGatewayAuthorizationError(503, "workspace_gateway_org_domain_unavailable", "Workspace apps gateway organization domain could not be read.");
1413
+ }
1414
+ }
1262
1415
  if (requestContext.orgId && !orgDomain) {
1263
- throw new WorkspaceAppsGatewayAuthorizationError(401, "Workspace apps gateway cannot authenticate without a resolved organization domain.");
1416
+ throw new WorkspaceAppsGatewayAuthorizationError(401, "workspace_gateway_org_domain_missing", "Workspace apps gateway cannot authenticate without a resolved organization domain.");
1264
1417
  }
1265
- try {
1266
- const token = await signA2AToken(requestContext.userEmail, orgDomain, undefined, {
1267
- expiresIn: "1m",
1268
- preferGlobalSecret: true,
1269
- audience: canonicalA2AAudience(baseUrl.toString(), getConfiguredAppBasePath() || undefined),
1270
- });
1271
- authHeaders.Authorization = `Bearer ${token}`;
1418
+ if (requestContext.orgId) {
1419
+ try {
1420
+ orgSecret =
1421
+ (await getOrgA2ASecret(requestContext.orgId))?.trim() || undefined;
1422
+ }
1423
+ catch {
1424
+ throw new WorkspaceAppsGatewayAuthorizationError(503, "workspace_gateway_org_a2a_secret_unavailable", "Workspace apps gateway organization A2A secret could not be read.");
1425
+ }
1272
1426
  }
1273
- catch {
1274
- // coercion-ok: absent signing credentials keep local unauthenticated
1275
- // gateway discovery available and make hosted discovery fail closed.
1276
- // Keep the unauthenticated local-dev gateway path available. A hosted
1277
- // gateway will fail closed below when its action route needs identity.
1427
+ const globalSecret = getGlobalA2ASecret();
1428
+ const allowLocalUnauthenticatedRequest = isLocalGateway && !orgSecret && !globalSecret;
1429
+ if (!orgSecret && !globalSecret && !allowLocalUnauthenticatedRequest) {
1430
+ throw new WorkspaceAppsGatewayAuthorizationError(503, "workspace_gateway_a2a_secret_missing", "Workspace apps gateway cannot authenticate because no A2A secret is configured.");
1431
+ }
1432
+ if (!allowLocalUnauthenticatedRequest) {
1433
+ try {
1434
+ const token = await signA2AToken(requestContext.userEmail, orgDomain, orgSecret, {
1435
+ expiresIn: "1m",
1436
+ preferGlobalSecret: false,
1437
+ audience: canonicalA2AAudience(baseUrl.toString(), getConfiguredAppBasePath() || undefined),
1438
+ });
1439
+ authHeaders.Authorization = `Bearer ${token}`;
1440
+ }
1441
+ catch {
1442
+ throw new WorkspaceAppsGatewayAuthorizationError(503, "workspace_gateway_token_signing_failed", "Workspace apps gateway could not sign the authenticated request.");
1443
+ }
1278
1444
  }
1279
1445
  }
1280
1446
  const controller = new AbortController();
@@ -1296,36 +1462,46 @@ async function readWorkspaceAppsFromGateway() {
1296
1462
  const protectedRedirect = protectionHeaders["x-vercel-protection-bypass"]
1297
1463
  ? { redirect: "manual" }
1298
1464
  : {};
1465
+ const readAppsResponse = async (response) => {
1466
+ let payload;
1467
+ try {
1468
+ payload = await response.json();
1469
+ }
1470
+ catch {
1471
+ throw new ActionContractError("Workspace apps gateway returned an unreadable response.", {
1472
+ statusCode: 503,
1473
+ errorCode: "workspace_gateway_invalid_response",
1474
+ });
1475
+ }
1476
+ const apps = parseWorkspaceAppsGatewayResponse(payload);
1477
+ if (!apps) {
1478
+ throw new ActionContractError("Workspace apps gateway returned an invalid response.", {
1479
+ statusCode: 503,
1480
+ errorCode: "workspace_gateway_invalid_response",
1481
+ });
1482
+ }
1483
+ return { apps };
1484
+ };
1299
1485
  try {
1300
- if (isLocalWorkspaceGateway(baseUrl)) {
1486
+ if (isLocalGateway) {
1301
1487
  const localResponse = await fetch(gatewayUrl(WORKSPACE_APPS_GATEWAY_PATH), {
1302
1488
  headers,
1303
1489
  ...protectedRedirect,
1304
1490
  signal: controller.signal,
1305
1491
  });
1306
1492
  if (localResponse.status === 401 || localResponse.status === 403) {
1307
- throw new WorkspaceAppsGatewayAuthorizationError(localResponse.status);
1493
+ throw new WorkspaceAppsGatewayAuthorizationError(localResponse.status, "workspace_gateway_rejected");
1308
1494
  }
1309
- if (localResponse.ok) {
1310
- const apps = parseWorkspaceAppsManifest(
1311
- // coercion-ok: malformed gateway JSON is an unavailable registry and
1312
- // must fall through to the local manifest sources.
1313
- await localResponse.json().catch(() => null));
1314
- return apps ? { apps } : null;
1495
+ if (localResponse.status !== 404 && localResponse.status !== 200) {
1496
+ throw new ActionContractError(`Workspace apps gateway returned HTTP ${localResponse.status}.`, {
1497
+ statusCode: 503,
1498
+ errorCode: `workspace_gateway_http_${localResponse.status}`,
1499
+ });
1500
+ }
1501
+ if (localResponse.status === 200) {
1502
+ return await readAppsResponse(localResponse);
1315
1503
  }
1316
1504
  }
1317
- const requestOrigin = requestContext?.requestOrigin;
1318
- const isSameOriginGateway = requestOrigin
1319
- ? (() => {
1320
- try {
1321
- return new URL(requestOrigin).origin === baseUrl.origin;
1322
- }
1323
- catch {
1324
- // coercion-ok: an invalid request origin cannot prove a self-fetch.
1325
- return false;
1326
- }
1327
- })()
1328
- : false;
1329
1505
  if (isSameOriginGateway || !authHeaders.Authorization)
1330
1506
  return null;
1331
1507
  const actionUrl = gatewayUrl(WORKSPACE_APPS_ACTION_PATH);
@@ -1337,20 +1513,25 @@ async function readWorkspaceAppsFromGateway() {
1337
1513
  signal: controller.signal,
1338
1514
  });
1339
1515
  if (actionResponse.status === 401 || actionResponse.status === 403) {
1340
- throw new WorkspaceAppsGatewayAuthorizationError(actionResponse.status);
1516
+ throw new WorkspaceAppsGatewayAuthorizationError(actionResponse.status, "workspace_gateway_rejected");
1341
1517
  }
1342
- if (!actionResponse.ok)
1518
+ if (actionResponse.status === 404)
1343
1519
  return null;
1344
- const apps = parseWorkspaceAppsManifest(
1345
- // coercion-ok: malformed gateway JSON is an unavailable registry and
1346
- // must fall through to the local manifest sources.
1347
- await actionResponse.json().catch(() => null));
1348
- return apps ? { apps } : null;
1520
+ if (actionResponse.status !== 200) {
1521
+ throw new ActionContractError(`Workspace apps gateway returned HTTP ${actionResponse.status}.`, {
1522
+ statusCode: 503,
1523
+ errorCode: `workspace_gateway_http_${actionResponse.status}`,
1524
+ });
1525
+ }
1526
+ return await readAppsResponse(actionResponse);
1349
1527
  }
1350
1528
  catch (error) {
1351
- if (error instanceof WorkspaceAppsGatewayAuthorizationError)
1529
+ if (error instanceof ActionContractError)
1352
1530
  throw error;
1353
- return null;
1531
+ throw new ActionContractError("Workspace apps gateway could not be reached.", {
1532
+ statusCode: 503,
1533
+ errorCode: "workspace_gateway_unavailable",
1534
+ });
1354
1535
  }
1355
1536
  finally {
1356
1537
  clearTimeout(timeout);
@@ -1495,6 +1676,10 @@ export async function updateWorkspaceAppMetadata(input) {
1495
1676
  const appId = input.appId.trim();
1496
1677
  assertValidWorkspaceAppId(appId);
1497
1678
  requireWorkspaceAppOrgId();
1679
+ const role = await requestOwnerRole();
1680
+ if (role !== "owner" && role !== "admin") {
1681
+ throw new WorkspaceAppMetadataAuthorizationError();
1682
+ }
1498
1683
  const apps = await listWorkspaceApps({
1499
1684
  includeAgentCards: false,
1500
1685
  includeArchived: true,
@@ -1502,34 +1687,54 @@ export async function updateWorkspaceAppMetadata(input) {
1502
1687
  const app = apps.find((candidate) => candidate.id === appId);
1503
1688
  if (!app)
1504
1689
  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({
1690
+ const nameProvided = Object.hasOwn(input, "name") && input.name !== undefined;
1691
+ const descriptionProvided = Object.hasOwn(input, "description") && input.description !== undefined;
1692
+ const whenToUseProvided = Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
1693
+ const name = nameProvided ? (input.name?.trim() ?? null) : app.name;
1694
+ const description = descriptionProvided
1695
+ ? (input.description?.trim() ?? null)
1696
+ : (app.description ?? undefined);
1697
+ const whenToUse = whenToUseProvided ? (input.whenToUse ?? []) : app.whenToUse;
1698
+ const metadataOverride = {
1510
1699
  appId,
1511
- name,
1512
- description,
1513
- generated: false,
1514
1700
  updatedBy: currentOwnerEmail(),
1701
+ };
1702
+ if (nameProvided)
1703
+ metadataOverride.name = input.name?.trim() ?? null;
1704
+ if (descriptionProvided) {
1705
+ metadataOverride.description = input.description?.trim() ?? null;
1706
+ }
1707
+ if (whenToUseProvided)
1708
+ metadataOverride.whenToUse = input.whenToUse ?? [];
1709
+ if (nameProvided || descriptionProvided)
1710
+ metadataOverride.generated = false;
1711
+ await writeWorkspaceAppMetadataOverride({
1712
+ ...metadataOverride,
1515
1713
  });
1714
+ invalidateOrgAppsDirectoryCache();
1516
1715
  await recordAudit({
1517
1716
  action: "workspace-app.metadata-updated",
1518
1717
  targetType: "workspace-app",
1519
1718
  targetId: appId,
1520
- summary: `Updated workspace app details for ${name}`,
1719
+ summary: `Updated workspace app details for ${name ?? app.name}`,
1521
1720
  metadata: {
1522
- name,
1721
+ name: name ?? app.name,
1523
1722
  descriptionConfigured: !!description,
1723
+ whenToUseCount: whenToUse?.length ?? 0,
1524
1724
  },
1525
1725
  });
1526
1726
  const updated = (await listWorkspaceApps({
1527
1727
  includeAgentCards: false,
1528
1728
  includeArchived: true,
1529
1729
  })).find((candidate) => candidate.id === appId);
1530
- return updated ?? { ...app, name, description };
1730
+ return (updated ?? {
1731
+ ...app,
1732
+ name: name ?? app.name,
1733
+ description: description ?? app.description,
1734
+ whenToUse,
1735
+ });
1531
1736
  }
1532
- export async function listWorkspaceApps(options = {}) {
1737
+ async function listWorkspaceAppsWithPrincipal(options = {}, principal = "user") {
1533
1738
  const finalize = async (apps, { persist = true } = {}) => {
1534
1739
  const annotated = await applyArchivedAndPending(apps);
1535
1740
  const recorded = await ensureWorkspaceAppRecords(annotated, {
@@ -1538,7 +1743,10 @@ export async function listWorkspaceApps(options = {}) {
1538
1743
  const listed = options.includeArchived
1539
1744
  ? recorded
1540
1745
  : recorded.filter((app) => !app.archived);
1541
- const visible = await filterWorkspaceAppsByAccess(filterAppsByAudience(listed, options.audience));
1746
+ const candidates = filterAppsByAudience(listed, options.audience);
1747
+ const visible = principal === "organization"
1748
+ ? await filterOrganizationVisibleWorkspaceApps(candidates, requireWorkspaceAppOrgId())
1749
+ : await filterWorkspaceAppsByAccess(candidates);
1542
1750
  return maybeIncludeAgentCards(visible, options);
1543
1751
  };
1544
1752
  let gatewayDenial = null;
@@ -1554,6 +1762,9 @@ export async function listWorkspaceApps(options = {}) {
1554
1762
  if (gatewayApps) {
1555
1763
  return finalize(gatewayApps.apps);
1556
1764
  }
1765
+ if (gatewayDenial && gatewayDenial.statusCode >= 500) {
1766
+ throw gatewayDenial;
1767
+ }
1557
1768
  const unverified = gatewayDenial !== null;
1558
1769
  const workspaceRoot = findWorkspaceRoot();
1559
1770
  const localFilesystemApps = workspaceRoot && isLocalAppCreationRuntime()
@@ -1573,8 +1784,9 @@ export async function listWorkspaceApps(options = {}) {
1573
1784
  // means the registry will not show this reader its apps, so they get the
1574
1785
  // deployment's own list instead, and none of it is recorded. (A manifest
1575
1786
  // answers earlier for both, with the warning above.)
1576
- if (gatewayDenial?.statusCode === 401)
1787
+ if (gatewayDenial && gatewayDenial.statusCode !== 403) {
1577
1788
  throw gatewayDenial;
1789
+ }
1578
1790
  warnWorkspaceAppsGatewayDenial(gatewayDenial, "deployment's own app list");
1579
1791
  if (!workspaceRoot) {
1580
1792
  return finalize([
@@ -1598,6 +1810,13 @@ export async function listWorkspaceApps(options = {}) {
1598
1810
  return finalize(apps, { persist: !unverified });
1599
1811
  return finalize([], { persist: !unverified });
1600
1812
  }
1813
+ export function listWorkspaceApps(options = {}) {
1814
+ return listWorkspaceAppsWithPrincipal(options, "user");
1815
+ }
1816
+ export async function listOrganizationVisibleWorkspaceApps() {
1817
+ requireWorkspaceAppOrgId();
1818
+ return listWorkspaceAppsWithPrincipal({ includeAgentCards: false, includeArchived: false, audience: "all" }, "organization");
1819
+ }
1601
1820
  const ADDABLE_TEMPLATES = [
1602
1821
  {
1603
1822
  name: "mail",
@@ -1748,6 +1967,8 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
1748
1967
  try {
1749
1968
  await writeWorkspaceAppMetadataOverride({
1750
1969
  appId,
1970
+ name: null,
1971
+ description: null,
1751
1972
  updatedBy: creatorEmail,
1752
1973
  createdBy: creatorEmail,
1753
1974
  visibility,
@@ -1783,6 +2004,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
1783
2004
  visibility,
1784
2005
  },
1785
2006
  ]);
2007
+ invalidateOrgAppsDirectoryCache();
1786
2008
  return { appId, template, output };
1787
2009
  }
1788
2010
  function runScaffoldCli(input) {
@@ -1945,8 +2167,8 @@ async function requestOwnerRole() {
1945
2167
  const role = rows[0]?.role;
1946
2168
  return typeof role === "string" ? role : null;
1947
2169
  }
1948
- catch {
1949
- return null;
2170
+ catch (error) {
2171
+ throw new AppCreationOwnerRoleUnavailableError(error);
1950
2172
  }
1951
2173
  }
1952
2174
  async function assertCanManageAppCreationSettings() {
@@ -2198,6 +2420,10 @@ export async function startWorkspaceAppCreation(input) {
2198
2420
  description: input.description,
2199
2421
  template: input.template,
2200
2422
  });
2423
+ if (input.communitySourceId !== undefined &&
2424
+ !normalizeCommunitySourceId(input.communitySourceId)) {
2425
+ throw new Error("Community source ids must be valid bounded identifiers.");
2426
+ }
2201
2427
  assertValidWorkspaceAppId(initial.appId);
2202
2428
  requireWorkspaceAppOrgId();
2203
2429
  normalizeBuilderAgentAttachments(input.attachments);
@@ -2244,15 +2470,41 @@ export async function startWorkspaceAppCreation(input) {
2244
2470
  const appDescription = input.description?.trim() ||
2245
2471
  generateWorkspaceAppDescription(input.prompt, built.appId);
2246
2472
  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
- });
2473
+ if (input.communitySourceId) {
2474
+ try {
2475
+ await reservePendingWorkspaceApp({
2476
+ appId: built.appId,
2477
+ description: appDescription,
2478
+ projectId: null,
2479
+ visibility: creationVisibility,
2480
+ communitySourceId: input.communitySourceId,
2481
+ });
2482
+ }
2483
+ catch (err) {
2484
+ if (err instanceof WorkspaceAppIdTakenError)
2485
+ return appIdTakenResult(err);
2486
+ throw err;
2487
+ }
2488
+ }
2489
+ try {
2490
+ await writeWorkspaceAppMetadataOverride({
2491
+ appId: built.appId,
2492
+ name: null,
2493
+ description: appDescription,
2494
+ generated: true,
2495
+ sourcePrompt: input.prompt,
2496
+ updatedBy: currentOwnerEmail(),
2497
+ createdBy: currentOwnerEmail(),
2498
+ visibility: creationVisibility,
2499
+ communitySourceId: input.communitySourceId,
2500
+ });
2501
+ }
2502
+ catch (error) {
2503
+ if (input.communitySourceId) {
2504
+ await releasePendingWorkspaceAppReservation(built.appId);
2505
+ }
2506
+ throw error;
2507
+ }
2256
2508
  await requestSelectedVaultKeys({
2257
2509
  appId: built.appId,
2258
2510
  selectedKeys,
@@ -2265,6 +2517,20 @@ export async function startWorkspaceAppCreation(input) {
2265
2517
  mode: "local-agent",
2266
2518
  appId: built.appId,
2267
2519
  prompt,
2520
+ ...(input.communitySourceId && input.attachments?.length
2521
+ ? {
2522
+ attachments: input.attachments.flatMap((attachment) => attachment.type === "upload" && attachment.text !== undefined
2523
+ ? [
2524
+ {
2525
+ type: "file",
2526
+ name: attachment.name,
2527
+ contentType: attachment.contentType,
2528
+ text: attachment.text,
2529
+ },
2530
+ ]
2531
+ : []),
2532
+ }
2533
+ : {}),
2268
2534
  message: "Use the local code agent to create this app in the workspace, then open it from /dispatch/apps.",
2269
2535
  };
2270
2536
  }
@@ -2300,6 +2566,9 @@ export async function startWorkspaceAppCreation(input) {
2300
2566
  description: appDescription,
2301
2567
  projectId: builderProjectId,
2302
2568
  visibility: creationVisibility,
2569
+ ...(normalizeCommunitySourceId(input.communitySourceId)
2570
+ ? { communitySourceId: input.communitySourceId }
2571
+ : {}),
2303
2572
  });
2304
2573
  }
2305
2574
  catch (err) {
@@ -2307,6 +2576,23 @@ export async function startWorkspaceAppCreation(input) {
2307
2576
  return appIdTakenResult(err);
2308
2577
  throw err;
2309
2578
  }
2579
+ if (input.communitySourceId) {
2580
+ try {
2581
+ await writeWorkspaceAppMetadataOverride({
2582
+ appId: built.appId,
2583
+ communitySourceId: input.communitySourceId,
2584
+ });
2585
+ }
2586
+ catch (error) {
2587
+ try {
2588
+ await releasePendingWorkspaceAppReservation(built.appId);
2589
+ }
2590
+ catch (cleanupError) {
2591
+ console.warn("[dispatch] failed to release pending workspace app reservation", cleanupError);
2592
+ }
2593
+ throw error;
2594
+ }
2595
+ }
2310
2596
  let result;
2311
2597
  try {
2312
2598
  result = normalizeBuilderRunResult(await runBuilderAgent({
@@ -2337,6 +2623,7 @@ export async function startWorkspaceAppCreation(input) {
2337
2623
  description: appDescription,
2338
2624
  sourcePrompt: input.prompt,
2339
2625
  visibility: creationVisibility,
2626
+ communitySourceId: input.communitySourceId,
2340
2627
  branchName: result.branchName,
2341
2628
  builderUrl: result.url,
2342
2629
  });