@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
@@ -3,13 +3,23 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- import { canonicalA2AAudience, signA2AToken } from "@agent-native/core/a2a";
7
- import { isActionContractError } from "@agent-native/core/action";
6
+ import {
7
+ canonicalA2AAudience,
8
+ getGlobalA2ASecret,
9
+ signA2AToken,
10
+ } from "@agent-native/core/a2a";
11
+ import {
12
+ ActionContractError,
13
+ isActionContractError,
14
+ } from "@agent-native/core/action";
8
15
  import { describeErrorWithCauses } from "@agent-native/core/agent/engine/error-detail";
9
16
  import { getDbExec } from "@agent-native/core/db";
17
+ import { and, eq, inArray } from "@agent-native/core/db/schema";
10
18
  import {
19
+ getOrgA2ASecret,
11
20
  getOrgDomain,
12
21
  isWorkspaceAppAccessAllowed,
22
+ workspaceApps,
13
23
  } from "@agent-native/core/org";
14
24
  import {
15
25
  CredentialStoreUnavailableError,
@@ -39,11 +49,12 @@ import {
39
49
  assertValidWorkspaceAppId,
40
50
  connectRequiredResult,
41
51
  normalizeWorkspaceAppHomePath,
52
+ parseAgentProfileMetadata,
42
53
  type ConnectRequiredCard,
43
54
  } from "@agent-native/core/shared";
44
55
  import { resolveAccess } from "@agent-native/core/sharing";
45
56
 
46
- import "../../db/index.js";
57
+ import { getDb } from "../../db/index.js";
47
58
  import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
48
59
  import { getDispatchDefaultOwnerEmail } from "./admin-config.js";
49
60
  import { identityKeyForIncoming } from "./dispatch-integrations.js";
@@ -57,6 +68,7 @@ import {
57
68
  projectEnvironmentUrl,
58
69
  requestEnvironmentLane,
59
70
  } from "./environment-lane.js";
71
+ import { invalidateOrgAppsDirectoryCache } from "./org-apps-directory-cache.js";
60
72
  import { createRequest, listSecretOptions } from "./vault-store.js";
61
73
  import { WORKSPACE_APPS_ACTION_PATH } from "./workspace-app-action-auth.js";
62
74
  import {
@@ -69,6 +81,8 @@ const SETTINGS_KEY = "dispatch-app-creation-settings";
69
81
  const DEFAULT_BUILDER_WORKSPACE_PROJECT_NAME = "Agent-Native Workspace";
70
82
  const APP_CREATION_SETTINGS_AUTHORIZATION_MESSAGE =
71
83
  "Only organization owners and admins can update app creation settings.";
84
+ const WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE =
85
+ "Only organization owners and admins can update workspace app details.";
72
86
  const APP_CREATION_SETTINGS_REQUIRED_MESSAGE =
73
87
  "An organization owner or admin must configure the Builder workspace project before members can create apps.";
74
88
  const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
@@ -109,6 +123,26 @@ class AppCreationSettingsAuthorizationError extends Error {
109
123
  }
110
124
  }
111
125
 
126
+ class WorkspaceAppMetadataAuthorizationError extends Error {
127
+ statusCode = 403;
128
+
129
+ constructor() {
130
+ super(WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE);
131
+ this.name = "WorkspaceAppMetadataAuthorizationError";
132
+ }
133
+ }
134
+
135
+ class AppCreationOwnerRoleUnavailableError extends Error {
136
+ statusCode = 503;
137
+
138
+ constructor(cause: unknown) {
139
+ super("Could not verify the organization role. Try again shortly.", {
140
+ cause,
141
+ });
142
+ this.name = "AppCreationOwnerRoleUnavailableError";
143
+ }
144
+ }
145
+
112
146
  class WorkspaceAppOrgRequiredError extends Error {
113
147
  statusCode = 400;
114
148
 
@@ -126,17 +160,15 @@ function requireWorkspaceAppOrgId(): string {
126
160
  return orgId;
127
161
  }
128
162
 
129
- class WorkspaceAppsGatewayAuthorizationError extends Error {
163
+ class WorkspaceAppsGatewayAuthorizationError extends ActionContractError {
130
164
  constructor(
131
- statusCode: 401 | 403,
165
+ statusCode: 401 | 403 | 503,
166
+ errorCode = "workspace_gateway_rejected",
132
167
  message = `Workspace apps gateway rejected the request with HTTP ${statusCode}.`,
133
168
  ) {
134
- super(message);
169
+ super(message, { statusCode, errorCode });
135
170
  this.name = "WorkspaceAppsGatewayAuthorizationError";
136
- this.statusCode = statusCode;
137
171
  }
138
-
139
- statusCode: 401 | 403;
140
172
  }
141
173
 
142
174
  function warnWorkspaceAppsGatewayDenial(
@@ -156,6 +188,8 @@ export interface WorkspaceAppSummary {
156
188
  id: string;
157
189
  name: string;
158
190
  description: string;
191
+ whenToUse?: string[];
192
+ communitySourceId?: string;
159
193
  path: string;
160
194
  homePath?: string;
161
195
  url: string | null;
@@ -233,6 +267,7 @@ interface PendingWorkspaceApp {
233
267
  contextLabel: string | null;
234
268
  audience?: WorkspaceAppAudience;
235
269
  visibility?: WorkspaceAppVisibility;
270
+ communitySourceId?: string;
236
271
  createdBy?: string | null;
237
272
  owner?: string | null;
238
273
  teams?: string[];
@@ -244,12 +279,14 @@ interface PendingWorkspaceApp {
244
279
  interface WorkspaceAppMetadataOverride {
245
280
  name?: string;
246
281
  description?: string;
282
+ whenToUse?: string[];
247
283
  generated?: boolean;
248
284
  sourcePrompt?: string;
249
285
  updatedAt?: string;
250
286
  updatedBy?: string;
251
287
  createdBy?: string;
252
288
  visibility?: WorkspaceAppVisibility;
289
+ communitySourceId?: string;
253
290
  }
254
291
 
255
292
  interface WorkspaceAppMetadataSettings {
@@ -379,7 +416,7 @@ function pendingWorkspaceAppContext(): { id: string; label: string } | null {
379
416
  }
380
417
 
381
418
  async function readSettingsRecord(): Promise<Record<string, any>> {
382
- const raw = await getSetting(scopedSettingsKey()).catch(() => null);
419
+ const raw = await getSetting(scopedSettingsKey());
383
420
  return raw && typeof raw === "object" && !Array.isArray(raw)
384
421
  ? (raw as Record<string, any>)
385
422
  : {};
@@ -389,6 +426,17 @@ function cleanOptionalText(value: unknown): string | undefined {
389
426
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
390
427
  }
391
428
 
429
+ function normalizeCommunitySourceId(value: unknown): string | undefined {
430
+ if (
431
+ typeof value !== "string" ||
432
+ value.length > 100 ||
433
+ !/^community-[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(value)
434
+ ) {
435
+ return undefined;
436
+ }
437
+ return value.toLowerCase();
438
+ }
439
+
392
440
  function parseWorkspaceAppMetadataSettings(
393
441
  raw: unknown,
394
442
  ): WorkspaceAppMetadataSettings {
@@ -410,18 +458,29 @@ function parseWorkspaceAppMetadataSettings(
410
458
  const override: WorkspaceAppMetadataOverride = {};
411
459
  const name = cleanOptionalText(item.name);
412
460
  const description = cleanOptionalText(item.description);
461
+ const profile = parseAgentProfileMetadata({
462
+ whenToUse: item.whenToUse,
463
+ });
413
464
  const sourcePrompt = cleanOptionalText(item.sourcePrompt);
414
465
  const updatedAt = cleanOptionalText(item.updatedAt);
415
466
  const updatedBy = cleanOptionalText(item.updatedBy);
416
467
  const createdBy = cleanOptionalText(item.createdBy);
468
+ const communitySourceId = normalizeCommunitySourceId(
469
+ item.communitySourceId,
470
+ );
417
471
 
418
472
  if (name) override.name = name;
419
473
  if (description) override.description = description;
474
+ if (profile?.whenToUse) override.whenToUse = profile.whenToUse;
475
+ else if (Array.isArray(item.whenToUse) && item.whenToUse.length === 0) {
476
+ override.whenToUse = [];
477
+ }
420
478
  if (item.generated === true) override.generated = true;
421
479
  if (sourcePrompt) override.sourcePrompt = sourcePrompt;
422
480
  if (updatedAt) override.updatedAt = updatedAt;
423
481
  if (updatedBy) override.updatedBy = updatedBy;
424
482
  if (createdBy) override.createdBy = createdBy;
483
+ if (communitySourceId) override.communitySourceId = communitySourceId;
425
484
  if (item.visibility === "private" || item.visibility === "org") {
426
485
  override.visibility = item.visibility;
427
486
  }
@@ -433,9 +492,7 @@ function parseWorkspaceAppMetadataSettings(
433
492
  }
434
493
 
435
494
  async function readWorkspaceAppMetadataSettings(): Promise<WorkspaceAppMetadataSettings> {
436
- const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(
437
- () => null,
438
- );
495
+ const raw = await getSetting(workspaceAppMetadataSettingsKey());
439
496
  return parseWorkspaceAppMetadataSettings(raw);
440
497
  }
441
498
 
@@ -443,37 +500,73 @@ async function writeWorkspaceAppMetadataOverride(input: {
443
500
  appId: string;
444
501
  name?: string | null;
445
502
  description?: string | null;
503
+ whenToUse?: string[] | null;
446
504
  generated?: boolean;
447
505
  sourcePrompt?: string | null;
448
506
  updatedBy?: string | null;
449
507
  createdBy?: string | null;
450
508
  visibility?: WorkspaceAppVisibility | null;
509
+ communitySourceId?: string | null;
451
510
  }): Promise<WorkspaceAppMetadataSettings> {
452
511
  const key = workspaceAppMetadataSettingsKey();
453
- const current = parseWorkspaceAppMetadataSettings(
454
- await getSetting(key).catch(() => null),
455
- );
512
+ const current = parseWorkspaceAppMetadataSettings(await getSetting(key));
456
513
  const appId = input.appId.trim();
457
514
  const existing = current.apps[appId] ?? {};
458
515
  const next: WorkspaceAppMetadataOverride = {
459
516
  ...existing,
460
517
  updatedAt: new Date().toISOString(),
461
518
  };
519
+ const hasName = Object.hasOwn(input, "name") && input.name !== undefined;
520
+ const hasDescription =
521
+ Object.hasOwn(input, "description") && input.description !== undefined;
522
+ const hasWhenToUse =
523
+ Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
462
524
  const name = cleanOptionalText(input.name);
463
525
  const description = cleanOptionalText(input.description);
526
+ const whenToUse = input.whenToUse;
464
527
  const sourcePrompt = cleanOptionalText(input.sourcePrompt);
465
528
  const updatedBy = cleanOptionalText(input.updatedBy);
466
529
  const createdBy = cleanOptionalText(input.createdBy);
530
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
467
531
 
468
- if (name) next.name = name;
469
- else delete next.name;
470
- if (description) next.description = description;
471
- else delete next.description;
532
+ if (hasName) {
533
+ if (name) next.name = name;
534
+ else delete next.name;
535
+ }
536
+ if (hasDescription) {
537
+ if (description) next.description = description;
538
+ else delete next.description;
539
+ }
540
+ if (hasWhenToUse) {
541
+ const examples = whenToUse ?? [];
542
+ const profile = parseAgentProfileMetadata({
543
+ description: description ?? next.description,
544
+ whenToUse: examples,
545
+ });
546
+ if (!profile) {
547
+ throw new Error(
548
+ "Workspace app when-to-use entries must contain up to 6 non-empty examples of 140 characters or fewer.",
549
+ );
550
+ }
551
+ next.whenToUse = profile.whenToUse ?? [];
552
+ }
553
+ if (description && description.length > 1000) {
554
+ throw new Error(
555
+ "Workspace app descriptions must be 1,000 characters or fewer.",
556
+ );
557
+ }
472
558
  if (input.generated === true) next.generated = true;
473
559
  else if (input.generated === false) delete next.generated;
474
560
  if (sourcePrompt) next.sourcePrompt = sourcePrompt;
475
561
  if (updatedBy) next.updatedBy = updatedBy;
476
562
  if (createdBy) next.createdBy = createdBy;
563
+ if (input.communitySourceId !== undefined) {
564
+ if (input.communitySourceId === null || !communitySourceId) {
565
+ delete next.communitySourceId;
566
+ } else {
567
+ next.communitySourceId = communitySourceId;
568
+ }
569
+ }
477
570
  if (input.visibility) next.visibility = input.visibility;
478
571
 
479
572
  current.apps[appId] = next;
@@ -490,18 +583,30 @@ function applyWorkspaceAppMetadataOverride(
490
583
 
491
584
  const name = cleanOptionalText(override.name);
492
585
  const description = cleanOptionalText(override.description);
586
+ const whenToUse = override.whenToUse;
493
587
  const generated = override.generated === true;
494
588
  const shouldApplyName = !!name && !generated;
495
589
  const shouldApplyDescription =
496
590
  !!description && (!generated || !cleanOptionalText(app.description));
497
591
  const visibility = override.visibility;
498
- if (!shouldApplyName && !shouldApplyDescription && !visibility) return app;
592
+ const communitySourceId = override.communitySourceId;
593
+ if (
594
+ !shouldApplyName &&
595
+ !shouldApplyDescription &&
596
+ whenToUse === undefined &&
597
+ !visibility &&
598
+ !communitySourceId
599
+ ) {
600
+ return app;
601
+ }
499
602
 
500
603
  return {
501
604
  ...app,
502
605
  ...(shouldApplyName ? { name } : {}),
503
606
  ...(shouldApplyDescription ? { description } : {}),
607
+ ...(whenToUse !== undefined ? { whenToUse } : {}),
504
608
  ...(visibility ? { visibility } : {}),
609
+ ...(communitySourceId ? { communitySourceId } : {}),
505
610
  ...(app.status === "pending" &&
506
611
  !app.createdBy &&
507
612
  (override.createdBy || override.updatedBy)
@@ -602,8 +707,17 @@ function normalizeWorkspaceAppTeams(value: unknown): string[] {
602
707
 
603
708
  function workspaceAppMetadataFromRecord(
604
709
  record: Record<string, any>,
605
- ): Pick<WorkspaceAppSummary, "createdAt" | "createdBy" | "owner" | "teams"> {
710
+ ): Pick<
711
+ WorkspaceAppSummary,
712
+ "createdAt" | "createdBy" | "owner" | "teams" | "whenToUse"
713
+ > {
606
714
  const config = record["agent-native"] ?? record.agentNative;
715
+ const profile = parseAgentProfileMetadata({
716
+ whenToUse:
717
+ config && typeof config === "object" && !Array.isArray(config)
718
+ ? config.whenToUse
719
+ : undefined,
720
+ });
607
721
  const workspaceConfig =
608
722
  config && typeof config === "object" && !Array.isArray(config)
609
723
  ? (config.workspaceApp ?? config.workspace ?? config)
@@ -651,6 +765,7 @@ function workspaceAppMetadataFromRecord(
651
765
  workspaceConfig.ownerEmail,
652
766
  ),
653
767
  teams,
768
+ ...(profile?.whenToUse ? { whenToUse: profile.whenToUse } : {}),
654
769
  };
655
770
  }
656
771
 
@@ -719,6 +834,10 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
719
834
  const id = typeof entry.id === "string" ? entry.id.trim() : "";
720
835
  const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
721
836
  if (!id || !pathValue.startsWith("/")) return null;
837
+ const profile = parseAgentProfileMetadata({
838
+ whenToUse: entry.whenToUse,
839
+ });
840
+ if (!profile) return null;
722
841
  const metadata = workspaceAppMetadataFromRecord(entry);
723
842
  return {
724
843
  id,
@@ -728,6 +847,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
728
847
  : titleCase(id),
729
848
  description:
730
849
  typeof entry.description === "string" ? entry.description : "",
850
+ ...(profile.whenToUse ? { whenToUse: profile.whenToUse } : {}),
731
851
  path: pathValue,
732
852
  homePath: normalizeWorkspaceAppHomePath(entry.homePath),
733
853
  url: workspaceAppLink(pathValue, entry.url),
@@ -757,6 +877,21 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
757
877
  return apps.length ? apps : null;
758
878
  }
759
879
 
880
+ function parseWorkspaceAppsGatewayResponse(
881
+ parsed: unknown,
882
+ ): WorkspaceAppSummary[] | null {
883
+ const rawApps =
884
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
885
+ ? (parsed as { apps?: unknown }).apps
886
+ : parsed;
887
+ if (!Array.isArray(rawApps)) return null;
888
+ if (rawApps.length === 0) return [];
889
+ const apps = parseWorkspaceAppsManifest(parsed);
890
+ if (!apps || apps.length !== rawApps.length) return null;
891
+ const ids = new Set(apps.map((app) => app.id.toLowerCase()));
892
+ return ids.size === apps.length ? apps : null;
893
+ }
894
+
760
895
  function parseDateMs(value: string | null | undefined): number | null {
761
896
  if (!value) return null;
762
897
  const ms = Date.parse(value);
@@ -850,6 +985,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
850
985
  if (!id || !pathValue.startsWith("/")) return null;
851
986
  const now = new Date().toISOString();
852
987
  const createdAt = cleanOptionalString(record.createdAt) ?? now;
988
+ const communitySourceId = normalizeCommunitySourceId(
989
+ record.communitySourceId,
990
+ );
853
991
  return {
854
992
  id,
855
993
  name:
@@ -884,6 +1022,7 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
884
1022
  ...(record.visibility === "private" || record.visibility === "org"
885
1023
  ? { visibility: record.visibility }
886
1024
  : {}),
1025
+ ...(communitySourceId ? { communitySourceId } : {}),
887
1026
  createdAt,
888
1027
  updatedAt:
889
1028
  typeof record.updatedAt === "string" && record.updatedAt.trim()
@@ -987,11 +1126,13 @@ async function reservePendingWorkspaceApp(input: {
987
1126
  description: string;
988
1127
  projectId: string | null;
989
1128
  visibility: WorkspaceAppVisibility;
1129
+ communitySourceId?: string;
990
1130
  }): Promise<void> {
991
1131
  await assertWorkspaceAppIdRegisteredFree(input.appId);
992
1132
  const now = new Date().toISOString();
993
1133
  const context = pendingWorkspaceAppContext();
994
1134
  const creatorEmail = currentOwnerEmail();
1135
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
995
1136
 
996
1137
  await mutateSetting(scopedSettingsKey(), async (current) => {
997
1138
  const raw =
@@ -1027,6 +1168,7 @@ async function reservePendingWorkspaceApp(input: {
1027
1168
  contextId: context?.id ?? null,
1028
1169
  contextLabel: context?.label ?? null,
1029
1170
  visibility: input.visibility,
1171
+ ...(communitySourceId ? { communitySourceId } : {}),
1030
1172
  createdBy: creatorEmail,
1031
1173
  owner: creatorEmail,
1032
1174
  createdAt: now,
@@ -1083,6 +1225,7 @@ export async function archiveWorkspaceApp(input: {
1083
1225
  const current = parseArchivedAppIds(raw.archivedAppIds);
1084
1226
  if (!current.includes(appId)) current.push(appId);
1085
1227
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
1228
+ invalidateOrgAppsDirectoryCache();
1086
1229
  await recordAudit({
1087
1230
  action: "workspace-app.archived",
1088
1231
  targetType: "workspace-app",
@@ -1102,6 +1245,7 @@ export async function unarchiveWorkspaceApp(input: {
1102
1245
  (id) => id !== appId,
1103
1246
  );
1104
1247
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
1248
+ invalidateOrgAppsDirectoryCache();
1105
1249
  await recordAudit({
1106
1250
  action: "workspace-app.unarchived",
1107
1251
  targetType: "workspace-app",
@@ -1148,6 +1292,7 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
1148
1292
  status: "pending",
1149
1293
  statusLabel: "Pending Builder branch",
1150
1294
  visibility: app.visibility,
1295
+ communitySourceId: app.communitySourceId,
1151
1296
  builderUrl: app.builderUrl,
1152
1297
  branchName: app.branchName,
1153
1298
  createdAt: app.createdAt,
@@ -1590,12 +1735,45 @@ async function filterWorkspaceAppsByAccess(
1590
1735
  return apps.filter((app) => visibleIds.has(app.id));
1591
1736
  }
1592
1737
 
1738
+ async function filterOrganizationVisibleWorkspaceApps(
1739
+ apps: WorkspaceAppSummary[],
1740
+ orgId: string,
1741
+ ): Promise<WorkspaceAppSummary[]> {
1742
+ const readyApps = apps.filter(
1743
+ (app) => app.status === "ready" && !app.archived,
1744
+ );
1745
+ const visibleIds = new Set<string>();
1746
+ const db = getDb();
1747
+
1748
+ for (let start = 0; start < readyApps.length; start += 500) {
1749
+ const batch = readyApps.slice(start, start + 500);
1750
+ const rows = await db
1751
+ .select({ id: workspaceApps.id })
1752
+ .from(workspaceApps)
1753
+ .where(
1754
+ and(
1755
+ eq(workspaceApps.orgId, orgId),
1756
+ eq(workspaceApps.visibility, "org"),
1757
+ eq(workspaceApps.orgEnabled, true),
1758
+ inArray(
1759
+ workspaceApps.id,
1760
+ batch.map((app) => app.id),
1761
+ ),
1762
+ ),
1763
+ );
1764
+ for (const row of rows) visibleIds.add(row.id);
1765
+ }
1766
+
1767
+ return readyApps.filter((app) => visibleIds.has(app.id));
1768
+ }
1769
+
1593
1770
  async function recordPendingWorkspaceApp(input: {
1594
1771
  appId: string;
1595
1772
  projectId: string | null;
1596
1773
  description: string;
1597
1774
  sourcePrompt: string;
1598
1775
  visibility: WorkspaceAppVisibility;
1776
+ communitySourceId?: string;
1599
1777
  branchName?: string | null;
1600
1778
  builderUrl?: string | null;
1601
1779
  }) {
@@ -1612,6 +1790,9 @@ async function recordPendingWorkspaceApp(input: {
1612
1790
  .find(samePendingEntry);
1613
1791
  const creatorEmail = currentOwnerEmail();
1614
1792
  const existingOwnerEmail = existing?.createdBy ?? existing?.owner;
1793
+ const communitySourceId =
1794
+ normalizeCommunitySourceId(input.communitySourceId) ??
1795
+ normalizeCommunitySourceId(existing?.communitySourceId);
1615
1796
  if (
1616
1797
  existingOwnerEmail &&
1617
1798
  existingOwnerEmail.trim().toLowerCase() !== creatorEmail.toLowerCase()
@@ -1633,6 +1814,7 @@ async function recordPendingWorkspaceApp(input: {
1633
1814
  contextId: context?.id ?? null,
1634
1815
  contextLabel: context?.label ?? null,
1635
1816
  visibility: input.visibility,
1817
+ ...(communitySourceId ? { communitySourceId } : {}),
1636
1818
  createdBy: existing?.createdBy ?? existing?.owner ?? creatorEmail,
1637
1819
  owner: existing?.owner ?? existing?.createdBy ?? creatorEmail,
1638
1820
  createdAt: existing?.createdAt || now,
@@ -1650,12 +1832,14 @@ async function recordPendingWorkspaceApp(input: {
1650
1832
 
1651
1833
  await writeWorkspaceAppMetadataOverride({
1652
1834
  appId: input.appId,
1835
+ name: null,
1653
1836
  description: input.description,
1654
1837
  generated: true,
1655
1838
  sourcePrompt: input.sourcePrompt,
1656
1839
  updatedBy: creatorEmail,
1657
1840
  createdBy: next.createdBy,
1658
1841
  visibility: input.visibility,
1842
+ communitySourceId,
1659
1843
  });
1660
1844
 
1661
1845
  await recordAudit({
@@ -1700,37 +1884,89 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
1700
1884
  }
1701
1885
 
1702
1886
  const requestContext = getRequestContext();
1887
+ const isLocalGateway = isLocalWorkspaceGateway(baseUrl);
1888
+ const requestOrigin = requestContext?.requestOrigin;
1889
+ const isSameOriginGateway = requestOrigin
1890
+ ? (() => {
1891
+ try {
1892
+ return new URL(requestOrigin).origin === baseUrl.origin;
1893
+ } catch {
1894
+ // coercion-ok: an invalid request origin cannot prove a self-fetch.
1895
+ return false;
1896
+ }
1897
+ })()
1898
+ : false;
1899
+ if (isSameOriginGateway && !isLocalGateway) return null;
1900
+
1703
1901
  const authHeaders: Record<string, string> = {};
1704
1902
  if (requestContext?.userEmail) {
1705
- const orgDomain = requestContext.orgId
1706
- ? (await getOrgDomain(requestContext.orgId))?.trim().toLowerCase()
1707
- : undefined;
1903
+ let orgDomain: string | undefined;
1904
+ let orgSecret: string | undefined;
1905
+ if (requestContext.orgId) {
1906
+ try {
1907
+ orgDomain =
1908
+ (await getOrgDomain(requestContext.orgId))?.trim().toLowerCase() ||
1909
+ undefined;
1910
+ } catch {
1911
+ throw new WorkspaceAppsGatewayAuthorizationError(
1912
+ 503,
1913
+ "workspace_gateway_org_domain_unavailable",
1914
+ "Workspace apps gateway organization domain could not be read.",
1915
+ );
1916
+ }
1917
+ }
1708
1918
  if (requestContext.orgId && !orgDomain) {
1709
1919
  throw new WorkspaceAppsGatewayAuthorizationError(
1710
1920
  401,
1921
+ "workspace_gateway_org_domain_missing",
1711
1922
  "Workspace apps gateway cannot authenticate without a resolved organization domain.",
1712
1923
  );
1713
1924
  }
1714
- try {
1715
- const token = await signA2AToken(
1716
- requestContext.userEmail,
1717
- orgDomain,
1718
- undefined,
1719
- {
1720
- expiresIn: "1m",
1721
- preferGlobalSecret: true,
1722
- audience: canonicalA2AAudience(
1723
- baseUrl.toString(),
1724
- getConfiguredAppBasePath() || undefined,
1725
- ),
1726
- },
1925
+ if (requestContext.orgId) {
1926
+ try {
1927
+ orgSecret =
1928
+ (await getOrgA2ASecret(requestContext.orgId))?.trim() || undefined;
1929
+ } catch {
1930
+ throw new WorkspaceAppsGatewayAuthorizationError(
1931
+ 503,
1932
+ "workspace_gateway_org_a2a_secret_unavailable",
1933
+ "Workspace apps gateway organization A2A secret could not be read.",
1934
+ );
1935
+ }
1936
+ }
1937
+ const globalSecret = getGlobalA2ASecret();
1938
+ const allowLocalUnauthenticatedRequest =
1939
+ isLocalGateway && !orgSecret && !globalSecret;
1940
+ if (!orgSecret && !globalSecret && !allowLocalUnauthenticatedRequest) {
1941
+ throw new WorkspaceAppsGatewayAuthorizationError(
1942
+ 503,
1943
+ "workspace_gateway_a2a_secret_missing",
1944
+ "Workspace apps gateway cannot authenticate because no A2A secret is configured.",
1727
1945
  );
1728
- authHeaders.Authorization = `Bearer ${token}`;
1729
- } catch {
1730
- // coercion-ok: absent signing credentials keep local unauthenticated
1731
- // gateway discovery available and make hosted discovery fail closed.
1732
- // Keep the unauthenticated local-dev gateway path available. A hosted
1733
- // gateway will fail closed below when its action route needs identity.
1946
+ }
1947
+ if (!allowLocalUnauthenticatedRequest) {
1948
+ try {
1949
+ const token = await signA2AToken(
1950
+ requestContext.userEmail,
1951
+ orgDomain,
1952
+ orgSecret,
1953
+ {
1954
+ expiresIn: "1m",
1955
+ preferGlobalSecret: false,
1956
+ audience: canonicalA2AAudience(
1957
+ baseUrl.toString(),
1958
+ getConfiguredAppBasePath() || undefined,
1959
+ ),
1960
+ },
1961
+ );
1962
+ authHeaders.Authorization = `Bearer ${token}`;
1963
+ } catch {
1964
+ throw new WorkspaceAppsGatewayAuthorizationError(
1965
+ 503,
1966
+ "workspace_gateway_token_signing_failed",
1967
+ "Workspace apps gateway could not sign the authenticated request.",
1968
+ );
1969
+ }
1734
1970
  }
1735
1971
  }
1736
1972
 
@@ -1760,9 +1996,34 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
1760
1996
  const protectedRedirect = protectionHeaders["x-vercel-protection-bypass"]
1761
1997
  ? { redirect: "manual" as const }
1762
1998
  : {};
1999
+ const readAppsResponse = async (response: Response) => {
2000
+ let payload: unknown;
2001
+ try {
2002
+ payload = await response.json();
2003
+ } catch {
2004
+ throw new ActionContractError(
2005
+ "Workspace apps gateway returned an unreadable response.",
2006
+ {
2007
+ statusCode: 503,
2008
+ errorCode: "workspace_gateway_invalid_response",
2009
+ },
2010
+ );
2011
+ }
2012
+ const apps = parseWorkspaceAppsGatewayResponse(payload);
2013
+ if (!apps) {
2014
+ throw new ActionContractError(
2015
+ "Workspace apps gateway returned an invalid response.",
2016
+ {
2017
+ statusCode: 503,
2018
+ errorCode: "workspace_gateway_invalid_response",
2019
+ },
2020
+ );
2021
+ }
2022
+ return { apps };
2023
+ };
1763
2024
 
1764
2025
  try {
1765
- if (isLocalWorkspaceGateway(baseUrl)) {
2026
+ if (isLocalGateway) {
1766
2027
  const localResponse = await fetch(
1767
2028
  gatewayUrl(WORKSPACE_APPS_GATEWAY_PATH),
1768
2029
  {
@@ -1772,29 +2033,25 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
1772
2033
  },
1773
2034
  );
1774
2035
  if (localResponse.status === 401 || localResponse.status === 403) {
1775
- throw new WorkspaceAppsGatewayAuthorizationError(localResponse.status);
2036
+ throw new WorkspaceAppsGatewayAuthorizationError(
2037
+ localResponse.status,
2038
+ "workspace_gateway_rejected",
2039
+ );
1776
2040
  }
1777
- if (localResponse.ok) {
1778
- const apps = parseWorkspaceAppsManifest(
1779
- // coercion-ok: malformed gateway JSON is an unavailable registry and
1780
- // must fall through to the local manifest sources.
1781
- await localResponse.json().catch(() => null),
2041
+ if (localResponse.status !== 404 && localResponse.status !== 200) {
2042
+ throw new ActionContractError(
2043
+ `Workspace apps gateway returned HTTP ${localResponse.status}.`,
2044
+ {
2045
+ statusCode: 503,
2046
+ errorCode: `workspace_gateway_http_${localResponse.status}`,
2047
+ },
1782
2048
  );
1783
- return apps ? { apps } : null;
2049
+ }
2050
+ if (localResponse.status === 200) {
2051
+ return await readAppsResponse(localResponse);
1784
2052
  }
1785
2053
  }
1786
2054
 
1787
- const requestOrigin = requestContext?.requestOrigin;
1788
- const isSameOriginGateway = requestOrigin
1789
- ? (() => {
1790
- try {
1791
- return new URL(requestOrigin).origin === baseUrl.origin;
1792
- } catch {
1793
- // coercion-ok: an invalid request origin cannot prove a self-fetch.
1794
- return false;
1795
- }
1796
- })()
1797
- : false;
1798
2055
  if (isSameOriginGateway || !authHeaders.Authorization) return null;
1799
2056
  const actionUrl = gatewayUrl(WORKSPACE_APPS_ACTION_PATH);
1800
2057
  actionUrl.searchParams.set("includeAgentCards", "false");
@@ -1805,18 +2062,31 @@ async function readWorkspaceAppsFromGateway(): Promise<WorkspaceAppDiscovery | n
1805
2062
  signal: controller.signal,
1806
2063
  });
1807
2064
  if (actionResponse.status === 401 || actionResponse.status === 403) {
1808
- throw new WorkspaceAppsGatewayAuthorizationError(actionResponse.status);
2065
+ throw new WorkspaceAppsGatewayAuthorizationError(
2066
+ actionResponse.status,
2067
+ "workspace_gateway_rejected",
2068
+ );
1809
2069
  }
1810
- if (!actionResponse.ok) return null;
1811
- const apps = parseWorkspaceAppsManifest(
1812
- // coercion-ok: malformed gateway JSON is an unavailable registry and
1813
- // must fall through to the local manifest sources.
1814
- await actionResponse.json().catch(() => null),
1815
- );
1816
- return apps ? { apps } : null;
2070
+ if (actionResponse.status === 404) return null;
2071
+ if (actionResponse.status !== 200) {
2072
+ throw new ActionContractError(
2073
+ `Workspace apps gateway returned HTTP ${actionResponse.status}.`,
2074
+ {
2075
+ statusCode: 503,
2076
+ errorCode: `workspace_gateway_http_${actionResponse.status}`,
2077
+ },
2078
+ );
2079
+ }
2080
+ return await readAppsResponse(actionResponse);
1817
2081
  } catch (error) {
1818
- if (error instanceof WorkspaceAppsGatewayAuthorizationError) throw error;
1819
- return null;
2082
+ if (error instanceof ActionContractError) throw error;
2083
+ throw new ActionContractError(
2084
+ "Workspace apps gateway could not be reached.",
2085
+ {
2086
+ statusCode: 503,
2087
+ errorCode: "workspace_gateway_unavailable",
2088
+ },
2089
+ );
1820
2090
  } finally {
1821
2091
  clearTimeout(timeout);
1822
2092
  }
@@ -1989,10 +2259,15 @@ export async function updateWorkspaceAppMetadata(input: {
1989
2259
  appId: string;
1990
2260
  name?: string | null;
1991
2261
  description?: string | null;
2262
+ whenToUse?: string[] | null;
1992
2263
  }): Promise<WorkspaceAppSummary> {
1993
2264
  const appId = input.appId.trim();
1994
2265
  assertValidWorkspaceAppId(appId);
1995
2266
  requireWorkspaceAppOrgId();
2267
+ const role = await requestOwnerRole();
2268
+ if (role !== "owner" && role !== "admin") {
2269
+ throw new WorkspaceAppMetadataAuthorizationError();
2270
+ }
1996
2271
 
1997
2272
  const apps = await listWorkspaceApps({
1998
2273
  includeAgentCards: false,
@@ -2001,27 +2276,42 @@ export async function updateWorkspaceAppMetadata(input: {
2001
2276
  const app = apps.find((candidate) => candidate.id === appId);
2002
2277
  if (!app) throw new Error(`Workspace app "${appId}" was not found.`);
2003
2278
 
2004
- const name = input.name == null ? app.name : input.name.trim();
2005
- const description =
2006
- input.description == null
2007
- ? (app.description ?? undefined)
2008
- : input.description.trim();
2009
- await writeWorkspaceAppMetadataOverride({
2279
+ const nameProvided = Object.hasOwn(input, "name") && input.name !== undefined;
2280
+ const descriptionProvided =
2281
+ Object.hasOwn(input, "description") && input.description !== undefined;
2282
+ const whenToUseProvided =
2283
+ Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
2284
+ const name = nameProvided ? (input.name?.trim() ?? null) : app.name;
2285
+ const description = descriptionProvided
2286
+ ? (input.description?.trim() ?? null)
2287
+ : (app.description ?? undefined);
2288
+ const whenToUse = whenToUseProvided ? (input.whenToUse ?? []) : app.whenToUse;
2289
+ const metadataOverride: Parameters<
2290
+ typeof writeWorkspaceAppMetadataOverride
2291
+ >[0] = {
2010
2292
  appId,
2011
- name,
2012
- description,
2013
- generated: false,
2014
2293
  updatedBy: currentOwnerEmail(),
2294
+ };
2295
+ if (nameProvided) metadataOverride.name = input.name?.trim() ?? null;
2296
+ if (descriptionProvided) {
2297
+ metadataOverride.description = input.description?.trim() ?? null;
2298
+ }
2299
+ if (whenToUseProvided) metadataOverride.whenToUse = input.whenToUse ?? [];
2300
+ if (nameProvided || descriptionProvided) metadataOverride.generated = false;
2301
+ await writeWorkspaceAppMetadataOverride({
2302
+ ...metadataOverride,
2015
2303
  });
2304
+ invalidateOrgAppsDirectoryCache();
2016
2305
 
2017
2306
  await recordAudit({
2018
2307
  action: "workspace-app.metadata-updated",
2019
2308
  targetType: "workspace-app",
2020
2309
  targetId: appId,
2021
- summary: `Updated workspace app details for ${name}`,
2310
+ summary: `Updated workspace app details for ${name ?? app.name}`,
2022
2311
  metadata: {
2023
- name,
2312
+ name: name ?? app.name,
2024
2313
  descriptionConfigured: !!description,
2314
+ whenToUseCount: whenToUse?.length ?? 0,
2025
2315
  },
2026
2316
  });
2027
2317
 
@@ -2031,11 +2321,19 @@ export async function updateWorkspaceAppMetadata(input: {
2031
2321
  includeArchived: true,
2032
2322
  })
2033
2323
  ).find((candidate) => candidate.id === appId);
2034
- return updated ?? { ...app, name, description };
2324
+ return (
2325
+ updated ?? {
2326
+ ...app,
2327
+ name: name ?? app.name,
2328
+ description: description ?? app.description,
2329
+ whenToUse,
2330
+ }
2331
+ );
2035
2332
  }
2036
2333
 
2037
- export async function listWorkspaceApps(
2334
+ async function listWorkspaceAppsWithPrincipal(
2038
2335
  options: ListWorkspaceAppsOptions = {},
2336
+ principal: "user" | "organization" = "user",
2039
2337
  ): Promise<WorkspaceAppSummary[]> {
2040
2338
  const finalize = async (
2041
2339
  apps: WorkspaceAppSummary[],
@@ -2048,9 +2346,14 @@ export async function listWorkspaceApps(
2048
2346
  const listed = options.includeArchived
2049
2347
  ? recorded
2050
2348
  : recorded.filter((app) => !app.archived);
2051
- const visible = await filterWorkspaceAppsByAccess(
2052
- filterAppsByAudience(listed, options.audience),
2053
- );
2349
+ const candidates = filterAppsByAudience(listed, options.audience);
2350
+ const visible =
2351
+ principal === "organization"
2352
+ ? await filterOrganizationVisibleWorkspaceApps(
2353
+ candidates,
2354
+ requireWorkspaceAppOrgId(),
2355
+ )
2356
+ : await filterWorkspaceAppsByAccess(candidates);
2054
2357
  return maybeIncludeAgentCards(visible, options);
2055
2358
  };
2056
2359
  let gatewayDenial: WorkspaceAppsGatewayAuthorizationError | null = null;
@@ -2064,6 +2367,9 @@ export async function listWorkspaceApps(
2064
2367
  if (gatewayApps) {
2065
2368
  return finalize(gatewayApps.apps);
2066
2369
  }
2370
+ if (gatewayDenial && gatewayDenial.statusCode >= 500) {
2371
+ throw gatewayDenial;
2372
+ }
2067
2373
  const unverified = gatewayDenial !== null;
2068
2374
 
2069
2375
  const workspaceRoot = findWorkspaceRoot();
@@ -2088,7 +2394,9 @@ export async function listWorkspaceApps(
2088
2394
  // means the registry will not show this reader its apps, so they get the
2089
2395
  // deployment's own list instead, and none of it is recorded. (A manifest
2090
2396
  // answers earlier for both, with the warning above.)
2091
- if (gatewayDenial?.statusCode === 401) throw gatewayDenial;
2397
+ if (gatewayDenial && gatewayDenial.statusCode !== 403) {
2398
+ throw gatewayDenial;
2399
+ }
2092
2400
  warnWorkspaceAppsGatewayDenial(gatewayDenial, "deployment's own app list");
2093
2401
 
2094
2402
  if (!workspaceRoot) {
@@ -2117,6 +2425,22 @@ export async function listWorkspaceApps(
2117
2425
  return finalize([], { persist: !unverified });
2118
2426
  }
2119
2427
 
2428
+ export function listWorkspaceApps(
2429
+ options: ListWorkspaceAppsOptions = {},
2430
+ ): Promise<WorkspaceAppSummary[]> {
2431
+ return listWorkspaceAppsWithPrincipal(options, "user");
2432
+ }
2433
+
2434
+ export async function listOrganizationVisibleWorkspaceApps(): Promise<
2435
+ WorkspaceAppSummary[]
2436
+ > {
2437
+ requireWorkspaceAppOrgId();
2438
+ return listWorkspaceAppsWithPrincipal(
2439
+ { includeAgentCards: false, includeArchived: false, audience: "all" },
2440
+ "organization",
2441
+ );
2442
+ }
2443
+
2120
2444
  const ADDABLE_TEMPLATES: AvailableWorkspaceTemplate[] = [
2121
2445
  {
2122
2446
  name: "mail",
@@ -2282,6 +2606,8 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
2282
2606
  try {
2283
2607
  await writeWorkspaceAppMetadataOverride({
2284
2608
  appId,
2609
+ name: null,
2610
+ description: null,
2285
2611
  updatedBy: creatorEmail,
2286
2612
  createdBy: creatorEmail,
2287
2613
  visibility,
@@ -2318,6 +2644,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
2318
2644
  visibility,
2319
2645
  },
2320
2646
  ]);
2647
+ invalidateOrgAppsDirectoryCache();
2321
2648
 
2322
2649
  return { appId, template, output };
2323
2650
  }
@@ -2512,8 +2839,8 @@ async function requestOwnerRole(): Promise<string | null> {
2512
2839
  });
2513
2840
  const role = (rows[0] as any)?.role;
2514
2841
  return typeof role === "string" ? role : null;
2515
- } catch {
2516
- return null;
2842
+ } catch (error) {
2843
+ throw new AppCreationOwnerRoleUnavailableError(error);
2517
2844
  }
2518
2845
  }
2519
2846
 
@@ -2853,6 +3180,12 @@ export interface AppCreationLocalAgentResult {
2853
3180
  appId: string;
2854
3181
  prompt: string;
2855
3182
  message: string;
3183
+ attachments?: Array<{
3184
+ type: "file";
3185
+ name: string;
3186
+ contentType: string;
3187
+ text: string;
3188
+ }>;
2856
3189
  }
2857
3190
 
2858
3191
  export interface AppCreationComingSoonResult {
@@ -2897,6 +3230,7 @@ export async function startWorkspaceAppCreation(input: {
2897
3230
  secretIds?: string[];
2898
3231
  resourceIds?: string[];
2899
3232
  attachments?: BuilderAgentAttachment[];
3233
+ communitySourceId?: string;
2900
3234
  }): Promise<StartWorkspaceAppCreationResult> {
2901
3235
  const initial = buildWorkspaceAppPrompt({
2902
3236
  prompt: input.prompt,
@@ -2904,6 +3238,12 @@ export async function startWorkspaceAppCreation(input: {
2904
3238
  description: input.description,
2905
3239
  template: input.template,
2906
3240
  });
3241
+ if (
3242
+ input.communitySourceId !== undefined &&
3243
+ !normalizeCommunitySourceId(input.communitySourceId)
3244
+ ) {
3245
+ throw new Error("Community source ids must be valid bounded identifiers.");
3246
+ }
2907
3247
  assertValidWorkspaceAppId(initial.appId);
2908
3248
  requireWorkspaceAppOrgId();
2909
3249
  normalizeBuilderAgentAttachments(input.attachments);
@@ -2955,15 +3295,39 @@ export async function startWorkspaceAppCreation(input: {
2955
3295
  generateWorkspaceAppDescription(input.prompt, built.appId);
2956
3296
 
2957
3297
  if (isLocal) {
2958
- await writeWorkspaceAppMetadataOverride({
2959
- appId: built.appId,
2960
- description: appDescription,
2961
- generated: true,
2962
- sourcePrompt: input.prompt,
2963
- updatedBy: currentOwnerEmail(),
2964
- createdBy: currentOwnerEmail(),
2965
- visibility: creationVisibility,
2966
- });
3298
+ if (input.communitySourceId) {
3299
+ try {
3300
+ await reservePendingWorkspaceApp({
3301
+ appId: built.appId,
3302
+ description: appDescription,
3303
+ projectId: null,
3304
+ visibility: creationVisibility,
3305
+ communitySourceId: input.communitySourceId,
3306
+ });
3307
+ } catch (err) {
3308
+ if (err instanceof WorkspaceAppIdTakenError)
3309
+ return appIdTakenResult(err);
3310
+ throw err;
3311
+ }
3312
+ }
3313
+ try {
3314
+ await writeWorkspaceAppMetadataOverride({
3315
+ appId: built.appId,
3316
+ name: null,
3317
+ description: appDescription,
3318
+ generated: true,
3319
+ sourcePrompt: input.prompt,
3320
+ updatedBy: currentOwnerEmail(),
3321
+ createdBy: currentOwnerEmail(),
3322
+ visibility: creationVisibility,
3323
+ communitySourceId: input.communitySourceId,
3324
+ });
3325
+ } catch (error) {
3326
+ if (input.communitySourceId) {
3327
+ await releasePendingWorkspaceAppReservation(built.appId);
3328
+ }
3329
+ throw error;
3330
+ }
2967
3331
  await requestSelectedVaultKeys({
2968
3332
  appId: built.appId,
2969
3333
  selectedKeys,
@@ -2976,6 +3340,22 @@ export async function startWorkspaceAppCreation(input: {
2976
3340
  mode: "local-agent",
2977
3341
  appId: built.appId,
2978
3342
  prompt,
3343
+ ...(input.communitySourceId && input.attachments?.length
3344
+ ? {
3345
+ attachments: input.attachments.flatMap((attachment) =>
3346
+ attachment.type === "upload" && attachment.text !== undefined
3347
+ ? [
3348
+ {
3349
+ type: "file" as const,
3350
+ name: attachment.name,
3351
+ contentType: attachment.contentType,
3352
+ text: attachment.text,
3353
+ },
3354
+ ]
3355
+ : [],
3356
+ ),
3357
+ }
3358
+ : {}),
2979
3359
  message:
2980
3360
  "Use the local code agent to create this app in the workspace, then open it from /dispatch/apps.",
2981
3361
  };
@@ -3018,12 +3398,34 @@ export async function startWorkspaceAppCreation(input: {
3018
3398
  description: appDescription,
3019
3399
  projectId: builderProjectId,
3020
3400
  visibility: creationVisibility,
3401
+ ...(normalizeCommunitySourceId(input.communitySourceId)
3402
+ ? { communitySourceId: input.communitySourceId }
3403
+ : {}),
3021
3404
  });
3022
3405
  } catch (err) {
3023
3406
  if (err instanceof WorkspaceAppIdTakenError) return appIdTakenResult(err);
3024
3407
  throw err;
3025
3408
  }
3026
3409
 
3410
+ if (input.communitySourceId) {
3411
+ try {
3412
+ await writeWorkspaceAppMetadataOverride({
3413
+ appId: built.appId,
3414
+ communitySourceId: input.communitySourceId,
3415
+ });
3416
+ } catch (error) {
3417
+ try {
3418
+ await releasePendingWorkspaceAppReservation(built.appId);
3419
+ } catch (cleanupError) {
3420
+ console.warn(
3421
+ "[dispatch] failed to release pending workspace app reservation",
3422
+ cleanupError,
3423
+ );
3424
+ }
3425
+ throw error;
3426
+ }
3427
+ }
3428
+
3027
3429
  let result: {
3028
3430
  branchName: string;
3029
3431
  url: string;
@@ -3063,6 +3465,7 @@ export async function startWorkspaceAppCreation(input: {
3063
3465
  description: appDescription,
3064
3466
  sourcePrompt: input.prompt,
3065
3467
  visibility: creationVisibility,
3468
+ communitySourceId: input.communitySourceId,
3066
3469
  branchName: result.branchName,
3067
3470
  builderUrl: result.url,
3068
3471
  });