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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +7 -47
  2. package/dist/actions/connect-external-agent.d.ts.map +1 -1
  3. package/dist/actions/connect-external-agent.js +12 -13
  4. package/dist/actions/connect-external-agent.js.map +1 -1
  5. package/dist/actions/create-workspace-resource.d.ts +13 -13
  6. package/dist/actions/delete-destination.d.ts +12 -12
  7. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  8. package/dist/actions/import-agent.d.ts +13 -13
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +8 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
  13. package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
  14. package/dist/actions/inspect-connect-agent-card.js +15 -0
  15. package/dist/actions/inspect-connect-agent-card.js.map +1 -0
  16. package/dist/actions/install-curated-community-app.d.ts +36 -0
  17. package/dist/actions/install-curated-community-app.d.ts.map +1 -0
  18. package/dist/actions/install-curated-community-app.js +26 -0
  19. package/dist/actions/install-curated-community-app.js.map +1 -0
  20. package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
  21. package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
  22. package/dist/actions/list-connect-marketplace-apps.js +15 -0
  23. package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
  24. package/dist/actions/list-connected-agents.d.ts +2 -0
  25. package/dist/actions/list-curated-community-apps.d.ts +3 -0
  26. package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
  27. package/dist/actions/list-curated-community-apps.js +10 -0
  28. package/dist/actions/list-curated-community-apps.js.map +1 -0
  29. package/dist/actions/provider-api-register.d.ts +14 -14
  30. package/dist/actions/remix-workspace-template.d.ts +6 -0
  31. package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
  32. package/dist/actions/update-workspace-app-metadata.js +26 -5
  33. package/dist/actions/update-workspace-app-metadata.js.map +1 -1
  34. package/dist/components/community-apps-section.d.ts +2 -0
  35. package/dist/components/community-apps-section.d.ts.map +1 -0
  36. package/dist/components/community-apps-section.js +90 -0
  37. package/dist/components/community-apps-section.js.map +1 -0
  38. package/dist/components/workspace-app-card.d.ts.map +1 -1
  39. package/dist/components/workspace-app-card.js +63 -9
  40. package/dist/components/workspace-app-card.js.map +1 -1
  41. package/dist/lib/connect-apps.d.ts +1 -2
  42. package/dist/lib/connect-apps.d.ts.map +1 -1
  43. package/dist/lib/connect-apps.js +41 -65
  44. package/dist/lib/connect-apps.js.map +1 -1
  45. package/dist/lib/workspace-apps.d.ts +1 -0
  46. package/dist/lib/workspace-apps.d.ts.map +1 -1
  47. package/dist/lib/workspace-apps.js.map +1 -1
  48. package/dist/routes/pages/apps.d.ts.map +1 -1
  49. package/dist/routes/pages/apps.js +2 -1
  50. package/dist/routes/pages/apps.js.map +1 -1
  51. package/dist/routes/pages/connect.d.ts.map +1 -1
  52. package/dist/routes/pages/connect.js +5 -7
  53. package/dist/routes/pages/connect.js.map +1 -1
  54. package/dist/server/index.d.ts +2 -0
  55. package/dist/server/index.d.ts.map +1 -1
  56. package/dist/server/index.js +2 -0
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/app-creation-store.d.ts +11 -1
  59. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  60. package/dist/server/lib/app-creation-store.js +258 -39
  61. package/dist/server/lib/app-creation-store.js.map +1 -1
  62. package/dist/server/lib/community-apps.d.ts +45 -0
  63. package/dist/server/lib/community-apps.d.ts.map +1 -0
  64. package/dist/server/lib/community-apps.js +267 -0
  65. package/dist/server/lib/community-apps.js.map +1 -0
  66. package/dist/server/lib/community-source.d.ts +19 -0
  67. package/dist/server/lib/community-source.d.ts.map +1 -0
  68. package/dist/server/lib/community-source.js +118 -0
  69. package/dist/server/lib/community-source.js.map +1 -0
  70. package/dist/server/lib/connect-apps.d.ts +4 -0
  71. package/dist/server/lib/connect-apps.d.ts.map +1 -0
  72. package/dist/server/lib/connect-apps.js +99 -0
  73. package/dist/server/lib/connect-apps.js.map +1 -0
  74. package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
  75. package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
  76. package/dist/server/lib/org-apps-directory-cache.js +44 -0
  77. package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
  78. package/dist/server/plugins/integrations.js +3 -3
  79. package/dist/server/plugins/integrations.js.map +1 -1
  80. package/dist/shared/labs.d.ts +2 -0
  81. package/dist/shared/labs.d.ts.map +1 -0
  82. package/dist/shared/labs.js +8 -0
  83. package/dist/shared/labs.js.map +1 -0
  84. package/package.json +5 -4
  85. package/src/actions/connect-external-agent.spec.ts +101 -3
  86. package/src/actions/connect-external-agent.ts +12 -11
  87. package/src/actions/index.spec.ts +13 -0
  88. package/src/actions/index.ts +8 -0
  89. package/src/actions/inspect-connect-agent-card.ts +17 -0
  90. package/src/actions/install-curated-community-app.ts +32 -0
  91. package/src/actions/list-connect-marketplace-apps.ts +17 -0
  92. package/src/actions/list-curated-community-apps.ts +12 -0
  93. package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
  94. package/src/actions/update-workspace-app-metadata.ts +48 -24
  95. package/src/components/community-apps-section.tsx +224 -0
  96. package/src/components/workspace-app-card.spec.tsx +226 -1
  97. package/src/components/workspace-app-card.tsx +115 -12
  98. package/src/lib/connect-apps.spec.ts +118 -66
  99. package/src/lib/connect-apps.ts +47 -82
  100. package/src/lib/workspace-apps.ts +1 -0
  101. package/src/routes/pages/apps.spec.tsx +201 -1
  102. package/src/routes/pages/apps.tsx +3 -0
  103. package/src/routes/pages/connect.spec.tsx +53 -6
  104. package/src/routes/pages/connect.tsx +14 -11
  105. package/src/server/index.ts +9 -0
  106. package/src/server/lib/app-creation-store.spec.ts +539 -11
  107. package/src/server/lib/app-creation-store.ts +332 -41
  108. package/src/server/lib/community-apps.spec.ts +367 -0
  109. package/src/server/lib/community-apps.ts +372 -0
  110. package/src/server/lib/community-source.spec.ts +151 -0
  111. package/src/server/lib/community-source.ts +186 -0
  112. package/src/server/lib/connect-apps.ts +149 -0
  113. package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
  114. package/src/server/lib/org-apps-directory-cache.ts +66 -0
  115. package/src/server/plugins/integrations.spec.ts +60 -0
  116. package/src/server/plugins/integrations.ts +3 -3
  117. package/src/shared/labs.ts +9 -0
@@ -7,9 +7,11 @@ import { canonicalA2AAudience, signA2AToken } from "@agent-native/core/a2a";
7
7
  import { isActionContractError } from "@agent-native/core/action";
8
8
  import { describeErrorWithCauses } from "@agent-native/core/agent/engine/error-detail";
9
9
  import { getDbExec } from "@agent-native/core/db";
10
+ import { and, eq, inArray } from "@agent-native/core/db/schema";
10
11
  import {
11
12
  getOrgDomain,
12
13
  isWorkspaceAppAccessAllowed,
14
+ workspaceApps,
13
15
  } from "@agent-native/core/org";
14
16
  import {
15
17
  CredentialStoreUnavailableError,
@@ -39,11 +41,12 @@ import {
39
41
  assertValidWorkspaceAppId,
40
42
  connectRequiredResult,
41
43
  normalizeWorkspaceAppHomePath,
44
+ parseAgentProfileMetadata,
42
45
  type ConnectRequiredCard,
43
46
  } from "@agent-native/core/shared";
44
47
  import { resolveAccess } from "@agent-native/core/sharing";
45
48
 
46
- import "../../db/index.js";
49
+ import { getDb } from "../../db/index.js";
47
50
  import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
48
51
  import { getDispatchDefaultOwnerEmail } from "./admin-config.js";
49
52
  import { identityKeyForIncoming } from "./dispatch-integrations.js";
@@ -57,6 +60,7 @@ import {
57
60
  projectEnvironmentUrl,
58
61
  requestEnvironmentLane,
59
62
  } from "./environment-lane.js";
63
+ import { invalidateOrgAppsDirectoryCache } from "./org-apps-directory-cache.js";
60
64
  import { createRequest, listSecretOptions } from "./vault-store.js";
61
65
  import { WORKSPACE_APPS_ACTION_PATH } from "./workspace-app-action-auth.js";
62
66
  import {
@@ -69,6 +73,8 @@ const SETTINGS_KEY = "dispatch-app-creation-settings";
69
73
  const DEFAULT_BUILDER_WORKSPACE_PROJECT_NAME = "Agent-Native Workspace";
70
74
  const APP_CREATION_SETTINGS_AUTHORIZATION_MESSAGE =
71
75
  "Only organization owners and admins can update app creation settings.";
76
+ const WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE =
77
+ "Only organization owners and admins can update workspace app details.";
72
78
  const APP_CREATION_SETTINGS_REQUIRED_MESSAGE =
73
79
  "An organization owner or admin must configure the Builder workspace project before members can create apps.";
74
80
  const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
@@ -109,6 +115,26 @@ class AppCreationSettingsAuthorizationError extends Error {
109
115
  }
110
116
  }
111
117
 
118
+ class WorkspaceAppMetadataAuthorizationError extends Error {
119
+ statusCode = 403;
120
+
121
+ constructor() {
122
+ super(WORKSPACE_APP_METADATA_AUTHORIZATION_MESSAGE);
123
+ this.name = "WorkspaceAppMetadataAuthorizationError";
124
+ }
125
+ }
126
+
127
+ class AppCreationOwnerRoleUnavailableError extends Error {
128
+ statusCode = 503;
129
+
130
+ constructor(cause: unknown) {
131
+ super("Could not verify the organization role. Try again shortly.", {
132
+ cause,
133
+ });
134
+ this.name = "AppCreationOwnerRoleUnavailableError";
135
+ }
136
+ }
137
+
112
138
  class WorkspaceAppOrgRequiredError extends Error {
113
139
  statusCode = 400;
114
140
 
@@ -156,6 +182,8 @@ export interface WorkspaceAppSummary {
156
182
  id: string;
157
183
  name: string;
158
184
  description: string;
185
+ whenToUse?: string[];
186
+ communitySourceId?: string;
159
187
  path: string;
160
188
  homePath?: string;
161
189
  url: string | null;
@@ -233,6 +261,7 @@ interface PendingWorkspaceApp {
233
261
  contextLabel: string | null;
234
262
  audience?: WorkspaceAppAudience;
235
263
  visibility?: WorkspaceAppVisibility;
264
+ communitySourceId?: string;
236
265
  createdBy?: string | null;
237
266
  owner?: string | null;
238
267
  teams?: string[];
@@ -244,12 +273,14 @@ interface PendingWorkspaceApp {
244
273
  interface WorkspaceAppMetadataOverride {
245
274
  name?: string;
246
275
  description?: string;
276
+ whenToUse?: string[];
247
277
  generated?: boolean;
248
278
  sourcePrompt?: string;
249
279
  updatedAt?: string;
250
280
  updatedBy?: string;
251
281
  createdBy?: string;
252
282
  visibility?: WorkspaceAppVisibility;
283
+ communitySourceId?: string;
253
284
  }
254
285
 
255
286
  interface WorkspaceAppMetadataSettings {
@@ -379,7 +410,7 @@ function pendingWorkspaceAppContext(): { id: string; label: string } | null {
379
410
  }
380
411
 
381
412
  async function readSettingsRecord(): Promise<Record<string, any>> {
382
- const raw = await getSetting(scopedSettingsKey()).catch(() => null);
413
+ const raw = await getSetting(scopedSettingsKey());
383
414
  return raw && typeof raw === "object" && !Array.isArray(raw)
384
415
  ? (raw as Record<string, any>)
385
416
  : {};
@@ -389,6 +420,17 @@ function cleanOptionalText(value: unknown): string | undefined {
389
420
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
390
421
  }
391
422
 
423
+ function normalizeCommunitySourceId(value: unknown): string | undefined {
424
+ if (
425
+ typeof value !== "string" ||
426
+ value.length > 100 ||
427
+ !/^community-[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(value)
428
+ ) {
429
+ return undefined;
430
+ }
431
+ return value.toLowerCase();
432
+ }
433
+
392
434
  function parseWorkspaceAppMetadataSettings(
393
435
  raw: unknown,
394
436
  ): WorkspaceAppMetadataSettings {
@@ -410,18 +452,29 @@ function parseWorkspaceAppMetadataSettings(
410
452
  const override: WorkspaceAppMetadataOverride = {};
411
453
  const name = cleanOptionalText(item.name);
412
454
  const description = cleanOptionalText(item.description);
455
+ const profile = parseAgentProfileMetadata({
456
+ whenToUse: item.whenToUse,
457
+ });
413
458
  const sourcePrompt = cleanOptionalText(item.sourcePrompt);
414
459
  const updatedAt = cleanOptionalText(item.updatedAt);
415
460
  const updatedBy = cleanOptionalText(item.updatedBy);
416
461
  const createdBy = cleanOptionalText(item.createdBy);
462
+ const communitySourceId = normalizeCommunitySourceId(
463
+ item.communitySourceId,
464
+ );
417
465
 
418
466
  if (name) override.name = name;
419
467
  if (description) override.description = description;
468
+ if (profile?.whenToUse) override.whenToUse = profile.whenToUse;
469
+ else if (Array.isArray(item.whenToUse) && item.whenToUse.length === 0) {
470
+ override.whenToUse = [];
471
+ }
420
472
  if (item.generated === true) override.generated = true;
421
473
  if (sourcePrompt) override.sourcePrompt = sourcePrompt;
422
474
  if (updatedAt) override.updatedAt = updatedAt;
423
475
  if (updatedBy) override.updatedBy = updatedBy;
424
476
  if (createdBy) override.createdBy = createdBy;
477
+ if (communitySourceId) override.communitySourceId = communitySourceId;
425
478
  if (item.visibility === "private" || item.visibility === "org") {
426
479
  override.visibility = item.visibility;
427
480
  }
@@ -433,9 +486,7 @@ function parseWorkspaceAppMetadataSettings(
433
486
  }
434
487
 
435
488
  async function readWorkspaceAppMetadataSettings(): Promise<WorkspaceAppMetadataSettings> {
436
- const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(
437
- () => null,
438
- );
489
+ const raw = await getSetting(workspaceAppMetadataSettingsKey());
439
490
  return parseWorkspaceAppMetadataSettings(raw);
440
491
  }
441
492
 
@@ -443,37 +494,73 @@ async function writeWorkspaceAppMetadataOverride(input: {
443
494
  appId: string;
444
495
  name?: string | null;
445
496
  description?: string | null;
497
+ whenToUse?: string[] | null;
446
498
  generated?: boolean;
447
499
  sourcePrompt?: string | null;
448
500
  updatedBy?: string | null;
449
501
  createdBy?: string | null;
450
502
  visibility?: WorkspaceAppVisibility | null;
503
+ communitySourceId?: string | null;
451
504
  }): Promise<WorkspaceAppMetadataSettings> {
452
505
  const key = workspaceAppMetadataSettingsKey();
453
- const current = parseWorkspaceAppMetadataSettings(
454
- await getSetting(key).catch(() => null),
455
- );
506
+ const current = parseWorkspaceAppMetadataSettings(await getSetting(key));
456
507
  const appId = input.appId.trim();
457
508
  const existing = current.apps[appId] ?? {};
458
509
  const next: WorkspaceAppMetadataOverride = {
459
510
  ...existing,
460
511
  updatedAt: new Date().toISOString(),
461
512
  };
513
+ const hasName = Object.hasOwn(input, "name") && input.name !== undefined;
514
+ const hasDescription =
515
+ Object.hasOwn(input, "description") && input.description !== undefined;
516
+ const hasWhenToUse =
517
+ Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
462
518
  const name = cleanOptionalText(input.name);
463
519
  const description = cleanOptionalText(input.description);
520
+ const whenToUse = input.whenToUse;
464
521
  const sourcePrompt = cleanOptionalText(input.sourcePrompt);
465
522
  const updatedBy = cleanOptionalText(input.updatedBy);
466
523
  const createdBy = cleanOptionalText(input.createdBy);
524
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
467
525
 
468
- if (name) next.name = name;
469
- else delete next.name;
470
- if (description) next.description = description;
471
- else delete next.description;
526
+ if (hasName) {
527
+ if (name) next.name = name;
528
+ else delete next.name;
529
+ }
530
+ if (hasDescription) {
531
+ if (description) next.description = description;
532
+ else delete next.description;
533
+ }
534
+ if (hasWhenToUse) {
535
+ const examples = whenToUse ?? [];
536
+ const profile = parseAgentProfileMetadata({
537
+ description: description ?? next.description,
538
+ whenToUse: examples,
539
+ });
540
+ if (!profile) {
541
+ throw new Error(
542
+ "Workspace app when-to-use entries must contain up to 6 non-empty examples of 140 characters or fewer.",
543
+ );
544
+ }
545
+ next.whenToUse = profile.whenToUse ?? [];
546
+ }
547
+ if (description && description.length > 1000) {
548
+ throw new Error(
549
+ "Workspace app descriptions must be 1,000 characters or fewer.",
550
+ );
551
+ }
472
552
  if (input.generated === true) next.generated = true;
473
553
  else if (input.generated === false) delete next.generated;
474
554
  if (sourcePrompt) next.sourcePrompt = sourcePrompt;
475
555
  if (updatedBy) next.updatedBy = updatedBy;
476
556
  if (createdBy) next.createdBy = createdBy;
557
+ if (input.communitySourceId !== undefined) {
558
+ if (input.communitySourceId === null || !communitySourceId) {
559
+ delete next.communitySourceId;
560
+ } else {
561
+ next.communitySourceId = communitySourceId;
562
+ }
563
+ }
477
564
  if (input.visibility) next.visibility = input.visibility;
478
565
 
479
566
  current.apps[appId] = next;
@@ -490,18 +577,30 @@ function applyWorkspaceAppMetadataOverride(
490
577
 
491
578
  const name = cleanOptionalText(override.name);
492
579
  const description = cleanOptionalText(override.description);
580
+ const whenToUse = override.whenToUse;
493
581
  const generated = override.generated === true;
494
582
  const shouldApplyName = !!name && !generated;
495
583
  const shouldApplyDescription =
496
584
  !!description && (!generated || !cleanOptionalText(app.description));
497
585
  const visibility = override.visibility;
498
- if (!shouldApplyName && !shouldApplyDescription && !visibility) return app;
586
+ const communitySourceId = override.communitySourceId;
587
+ if (
588
+ !shouldApplyName &&
589
+ !shouldApplyDescription &&
590
+ whenToUse === undefined &&
591
+ !visibility &&
592
+ !communitySourceId
593
+ ) {
594
+ return app;
595
+ }
499
596
 
500
597
  return {
501
598
  ...app,
502
599
  ...(shouldApplyName ? { name } : {}),
503
600
  ...(shouldApplyDescription ? { description } : {}),
601
+ ...(whenToUse !== undefined ? { whenToUse } : {}),
504
602
  ...(visibility ? { visibility } : {}),
603
+ ...(communitySourceId ? { communitySourceId } : {}),
505
604
  ...(app.status === "pending" &&
506
605
  !app.createdBy &&
507
606
  (override.createdBy || override.updatedBy)
@@ -602,8 +701,17 @@ function normalizeWorkspaceAppTeams(value: unknown): string[] {
602
701
 
603
702
  function workspaceAppMetadataFromRecord(
604
703
  record: Record<string, any>,
605
- ): Pick<WorkspaceAppSummary, "createdAt" | "createdBy" | "owner" | "teams"> {
704
+ ): Pick<
705
+ WorkspaceAppSummary,
706
+ "createdAt" | "createdBy" | "owner" | "teams" | "whenToUse"
707
+ > {
606
708
  const config = record["agent-native"] ?? record.agentNative;
709
+ const profile = parseAgentProfileMetadata({
710
+ whenToUse:
711
+ config && typeof config === "object" && !Array.isArray(config)
712
+ ? config.whenToUse
713
+ : undefined,
714
+ });
607
715
  const workspaceConfig =
608
716
  config && typeof config === "object" && !Array.isArray(config)
609
717
  ? (config.workspaceApp ?? config.workspace ?? config)
@@ -651,6 +759,7 @@ function workspaceAppMetadataFromRecord(
651
759
  workspaceConfig.ownerEmail,
652
760
  ),
653
761
  teams,
762
+ ...(profile?.whenToUse ? { whenToUse: profile.whenToUse } : {}),
654
763
  };
655
764
  }
656
765
 
@@ -719,6 +828,10 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
719
828
  const id = typeof entry.id === "string" ? entry.id.trim() : "";
720
829
  const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
721
830
  if (!id || !pathValue.startsWith("/")) return null;
831
+ const profile = parseAgentProfileMetadata({
832
+ whenToUse: entry.whenToUse,
833
+ });
834
+ if (!profile) return null;
722
835
  const metadata = workspaceAppMetadataFromRecord(entry);
723
836
  return {
724
837
  id,
@@ -728,6 +841,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
728
841
  : titleCase(id),
729
842
  description:
730
843
  typeof entry.description === "string" ? entry.description : "",
844
+ ...(profile.whenToUse ? { whenToUse: profile.whenToUse } : {}),
731
845
  path: pathValue,
732
846
  homePath: normalizeWorkspaceAppHomePath(entry.homePath),
733
847
  url: workspaceAppLink(pathValue, entry.url),
@@ -850,6 +964,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
850
964
  if (!id || !pathValue.startsWith("/")) return null;
851
965
  const now = new Date().toISOString();
852
966
  const createdAt = cleanOptionalString(record.createdAt) ?? now;
967
+ const communitySourceId = normalizeCommunitySourceId(
968
+ record.communitySourceId,
969
+ );
853
970
  return {
854
971
  id,
855
972
  name:
@@ -884,6 +1001,7 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
884
1001
  ...(record.visibility === "private" || record.visibility === "org"
885
1002
  ? { visibility: record.visibility }
886
1003
  : {}),
1004
+ ...(communitySourceId ? { communitySourceId } : {}),
887
1005
  createdAt,
888
1006
  updatedAt:
889
1007
  typeof record.updatedAt === "string" && record.updatedAt.trim()
@@ -987,11 +1105,13 @@ async function reservePendingWorkspaceApp(input: {
987
1105
  description: string;
988
1106
  projectId: string | null;
989
1107
  visibility: WorkspaceAppVisibility;
1108
+ communitySourceId?: string;
990
1109
  }): Promise<void> {
991
1110
  await assertWorkspaceAppIdRegisteredFree(input.appId);
992
1111
  const now = new Date().toISOString();
993
1112
  const context = pendingWorkspaceAppContext();
994
1113
  const creatorEmail = currentOwnerEmail();
1114
+ const communitySourceId = normalizeCommunitySourceId(input.communitySourceId);
995
1115
 
996
1116
  await mutateSetting(scopedSettingsKey(), async (current) => {
997
1117
  const raw =
@@ -1027,6 +1147,7 @@ async function reservePendingWorkspaceApp(input: {
1027
1147
  contextId: context?.id ?? null,
1028
1148
  contextLabel: context?.label ?? null,
1029
1149
  visibility: input.visibility,
1150
+ ...(communitySourceId ? { communitySourceId } : {}),
1030
1151
  createdBy: creatorEmail,
1031
1152
  owner: creatorEmail,
1032
1153
  createdAt: now,
@@ -1083,6 +1204,7 @@ export async function archiveWorkspaceApp(input: {
1083
1204
  const current = parseArchivedAppIds(raw.archivedAppIds);
1084
1205
  if (!current.includes(appId)) current.push(appId);
1085
1206
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
1207
+ invalidateOrgAppsDirectoryCache();
1086
1208
  await recordAudit({
1087
1209
  action: "workspace-app.archived",
1088
1210
  targetType: "workspace-app",
@@ -1102,6 +1224,7 @@ export async function unarchiveWorkspaceApp(input: {
1102
1224
  (id) => id !== appId,
1103
1225
  );
1104
1226
  await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
1227
+ invalidateOrgAppsDirectoryCache();
1105
1228
  await recordAudit({
1106
1229
  action: "workspace-app.unarchived",
1107
1230
  targetType: "workspace-app",
@@ -1148,6 +1271,7 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
1148
1271
  status: "pending",
1149
1272
  statusLabel: "Pending Builder branch",
1150
1273
  visibility: app.visibility,
1274
+ communitySourceId: app.communitySourceId,
1151
1275
  builderUrl: app.builderUrl,
1152
1276
  branchName: app.branchName,
1153
1277
  createdAt: app.createdAt,
@@ -1590,12 +1714,45 @@ async function filterWorkspaceAppsByAccess(
1590
1714
  return apps.filter((app) => visibleIds.has(app.id));
1591
1715
  }
1592
1716
 
1717
+ async function filterOrganizationVisibleWorkspaceApps(
1718
+ apps: WorkspaceAppSummary[],
1719
+ orgId: string,
1720
+ ): Promise<WorkspaceAppSummary[]> {
1721
+ const readyApps = apps.filter(
1722
+ (app) => app.status === "ready" && !app.archived,
1723
+ );
1724
+ const visibleIds = new Set<string>();
1725
+ const db = getDb();
1726
+
1727
+ for (let start = 0; start < readyApps.length; start += 500) {
1728
+ const batch = readyApps.slice(start, start + 500);
1729
+ const rows = await db
1730
+ .select({ id: workspaceApps.id })
1731
+ .from(workspaceApps)
1732
+ .where(
1733
+ and(
1734
+ eq(workspaceApps.orgId, orgId),
1735
+ eq(workspaceApps.visibility, "org"),
1736
+ eq(workspaceApps.orgEnabled, true),
1737
+ inArray(
1738
+ workspaceApps.id,
1739
+ batch.map((app) => app.id),
1740
+ ),
1741
+ ),
1742
+ );
1743
+ for (const row of rows) visibleIds.add(row.id);
1744
+ }
1745
+
1746
+ return readyApps.filter((app) => visibleIds.has(app.id));
1747
+ }
1748
+
1593
1749
  async function recordPendingWorkspaceApp(input: {
1594
1750
  appId: string;
1595
1751
  projectId: string | null;
1596
1752
  description: string;
1597
1753
  sourcePrompt: string;
1598
1754
  visibility: WorkspaceAppVisibility;
1755
+ communitySourceId?: string;
1599
1756
  branchName?: string | null;
1600
1757
  builderUrl?: string | null;
1601
1758
  }) {
@@ -1612,6 +1769,9 @@ async function recordPendingWorkspaceApp(input: {
1612
1769
  .find(samePendingEntry);
1613
1770
  const creatorEmail = currentOwnerEmail();
1614
1771
  const existingOwnerEmail = existing?.createdBy ?? existing?.owner;
1772
+ const communitySourceId =
1773
+ normalizeCommunitySourceId(input.communitySourceId) ??
1774
+ normalizeCommunitySourceId(existing?.communitySourceId);
1615
1775
  if (
1616
1776
  existingOwnerEmail &&
1617
1777
  existingOwnerEmail.trim().toLowerCase() !== creatorEmail.toLowerCase()
@@ -1633,6 +1793,7 @@ async function recordPendingWorkspaceApp(input: {
1633
1793
  contextId: context?.id ?? null,
1634
1794
  contextLabel: context?.label ?? null,
1635
1795
  visibility: input.visibility,
1796
+ ...(communitySourceId ? { communitySourceId } : {}),
1636
1797
  createdBy: existing?.createdBy ?? existing?.owner ?? creatorEmail,
1637
1798
  owner: existing?.owner ?? existing?.createdBy ?? creatorEmail,
1638
1799
  createdAt: existing?.createdAt || now,
@@ -1650,12 +1811,14 @@ async function recordPendingWorkspaceApp(input: {
1650
1811
 
1651
1812
  await writeWorkspaceAppMetadataOverride({
1652
1813
  appId: input.appId,
1814
+ name: null,
1653
1815
  description: input.description,
1654
1816
  generated: true,
1655
1817
  sourcePrompt: input.sourcePrompt,
1656
1818
  updatedBy: creatorEmail,
1657
1819
  createdBy: next.createdBy,
1658
1820
  visibility: input.visibility,
1821
+ communitySourceId,
1659
1822
  });
1660
1823
 
1661
1824
  await recordAudit({
@@ -1989,10 +2152,15 @@ export async function updateWorkspaceAppMetadata(input: {
1989
2152
  appId: string;
1990
2153
  name?: string | null;
1991
2154
  description?: string | null;
2155
+ whenToUse?: string[] | null;
1992
2156
  }): Promise<WorkspaceAppSummary> {
1993
2157
  const appId = input.appId.trim();
1994
2158
  assertValidWorkspaceAppId(appId);
1995
2159
  requireWorkspaceAppOrgId();
2160
+ const role = await requestOwnerRole();
2161
+ if (role !== "owner" && role !== "admin") {
2162
+ throw new WorkspaceAppMetadataAuthorizationError();
2163
+ }
1996
2164
 
1997
2165
  const apps = await listWorkspaceApps({
1998
2166
  includeAgentCards: false,
@@ -2001,27 +2169,42 @@ export async function updateWorkspaceAppMetadata(input: {
2001
2169
  const app = apps.find((candidate) => candidate.id === appId);
2002
2170
  if (!app) throw new Error(`Workspace app "${appId}" was not found.`);
2003
2171
 
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({
2172
+ const nameProvided = Object.hasOwn(input, "name") && input.name !== undefined;
2173
+ const descriptionProvided =
2174
+ Object.hasOwn(input, "description") && input.description !== undefined;
2175
+ const whenToUseProvided =
2176
+ Object.hasOwn(input, "whenToUse") && input.whenToUse !== undefined;
2177
+ const name = nameProvided ? (input.name?.trim() ?? null) : app.name;
2178
+ const description = descriptionProvided
2179
+ ? (input.description?.trim() ?? null)
2180
+ : (app.description ?? undefined);
2181
+ const whenToUse = whenToUseProvided ? (input.whenToUse ?? []) : app.whenToUse;
2182
+ const metadataOverride: Parameters<
2183
+ typeof writeWorkspaceAppMetadataOverride
2184
+ >[0] = {
2010
2185
  appId,
2011
- name,
2012
- description,
2013
- generated: false,
2014
2186
  updatedBy: currentOwnerEmail(),
2187
+ };
2188
+ if (nameProvided) metadataOverride.name = input.name?.trim() ?? null;
2189
+ if (descriptionProvided) {
2190
+ metadataOverride.description = input.description?.trim() ?? null;
2191
+ }
2192
+ if (whenToUseProvided) metadataOverride.whenToUse = input.whenToUse ?? [];
2193
+ if (nameProvided || descriptionProvided) metadataOverride.generated = false;
2194
+ await writeWorkspaceAppMetadataOverride({
2195
+ ...metadataOverride,
2015
2196
  });
2197
+ invalidateOrgAppsDirectoryCache();
2016
2198
 
2017
2199
  await recordAudit({
2018
2200
  action: "workspace-app.metadata-updated",
2019
2201
  targetType: "workspace-app",
2020
2202
  targetId: appId,
2021
- summary: `Updated workspace app details for ${name}`,
2203
+ summary: `Updated workspace app details for ${name ?? app.name}`,
2022
2204
  metadata: {
2023
- name,
2205
+ name: name ?? app.name,
2024
2206
  descriptionConfigured: !!description,
2207
+ whenToUseCount: whenToUse?.length ?? 0,
2025
2208
  },
2026
2209
  });
2027
2210
 
@@ -2031,11 +2214,19 @@ export async function updateWorkspaceAppMetadata(input: {
2031
2214
  includeArchived: true,
2032
2215
  })
2033
2216
  ).find((candidate) => candidate.id === appId);
2034
- return updated ?? { ...app, name, description };
2217
+ return (
2218
+ updated ?? {
2219
+ ...app,
2220
+ name: name ?? app.name,
2221
+ description: description ?? app.description,
2222
+ whenToUse,
2223
+ }
2224
+ );
2035
2225
  }
2036
2226
 
2037
- export async function listWorkspaceApps(
2227
+ async function listWorkspaceAppsWithPrincipal(
2038
2228
  options: ListWorkspaceAppsOptions = {},
2229
+ principal: "user" | "organization" = "user",
2039
2230
  ): Promise<WorkspaceAppSummary[]> {
2040
2231
  const finalize = async (
2041
2232
  apps: WorkspaceAppSummary[],
@@ -2048,9 +2239,14 @@ export async function listWorkspaceApps(
2048
2239
  const listed = options.includeArchived
2049
2240
  ? recorded
2050
2241
  : recorded.filter((app) => !app.archived);
2051
- const visible = await filterWorkspaceAppsByAccess(
2052
- filterAppsByAudience(listed, options.audience),
2053
- );
2242
+ const candidates = filterAppsByAudience(listed, options.audience);
2243
+ const visible =
2244
+ principal === "organization"
2245
+ ? await filterOrganizationVisibleWorkspaceApps(
2246
+ candidates,
2247
+ requireWorkspaceAppOrgId(),
2248
+ )
2249
+ : await filterWorkspaceAppsByAccess(candidates);
2054
2250
  return maybeIncludeAgentCards(visible, options);
2055
2251
  };
2056
2252
  let gatewayDenial: WorkspaceAppsGatewayAuthorizationError | null = null;
@@ -2117,6 +2313,22 @@ export async function listWorkspaceApps(
2117
2313
  return finalize([], { persist: !unverified });
2118
2314
  }
2119
2315
 
2316
+ export function listWorkspaceApps(
2317
+ options: ListWorkspaceAppsOptions = {},
2318
+ ): Promise<WorkspaceAppSummary[]> {
2319
+ return listWorkspaceAppsWithPrincipal(options, "user");
2320
+ }
2321
+
2322
+ export async function listOrganizationVisibleWorkspaceApps(): Promise<
2323
+ WorkspaceAppSummary[]
2324
+ > {
2325
+ requireWorkspaceAppOrgId();
2326
+ return listWorkspaceAppsWithPrincipal(
2327
+ { includeAgentCards: false, includeArchived: false, audience: "all" },
2328
+ "organization",
2329
+ );
2330
+ }
2331
+
2120
2332
  const ADDABLE_TEMPLATES: AvailableWorkspaceTemplate[] = [
2121
2333
  {
2122
2334
  name: "mail",
@@ -2282,6 +2494,8 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
2282
2494
  try {
2283
2495
  await writeWorkspaceAppMetadataOverride({
2284
2496
  appId,
2497
+ name: null,
2498
+ description: null,
2285
2499
  updatedBy: creatorEmail,
2286
2500
  createdBy: creatorEmail,
2287
2501
  visibility,
@@ -2318,6 +2532,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
2318
2532
  visibility,
2319
2533
  },
2320
2534
  ]);
2535
+ invalidateOrgAppsDirectoryCache();
2321
2536
 
2322
2537
  return { appId, template, output };
2323
2538
  }
@@ -2512,8 +2727,8 @@ async function requestOwnerRole(): Promise<string | null> {
2512
2727
  });
2513
2728
  const role = (rows[0] as any)?.role;
2514
2729
  return typeof role === "string" ? role : null;
2515
- } catch {
2516
- return null;
2730
+ } catch (error) {
2731
+ throw new AppCreationOwnerRoleUnavailableError(error);
2517
2732
  }
2518
2733
  }
2519
2734
 
@@ -2853,6 +3068,12 @@ export interface AppCreationLocalAgentResult {
2853
3068
  appId: string;
2854
3069
  prompt: string;
2855
3070
  message: string;
3071
+ attachments?: Array<{
3072
+ type: "file";
3073
+ name: string;
3074
+ contentType: string;
3075
+ text: string;
3076
+ }>;
2856
3077
  }
2857
3078
 
2858
3079
  export interface AppCreationComingSoonResult {
@@ -2897,6 +3118,7 @@ export async function startWorkspaceAppCreation(input: {
2897
3118
  secretIds?: string[];
2898
3119
  resourceIds?: string[];
2899
3120
  attachments?: BuilderAgentAttachment[];
3121
+ communitySourceId?: string;
2900
3122
  }): Promise<StartWorkspaceAppCreationResult> {
2901
3123
  const initial = buildWorkspaceAppPrompt({
2902
3124
  prompt: input.prompt,
@@ -2904,6 +3126,12 @@ export async function startWorkspaceAppCreation(input: {
2904
3126
  description: input.description,
2905
3127
  template: input.template,
2906
3128
  });
3129
+ if (
3130
+ input.communitySourceId !== undefined &&
3131
+ !normalizeCommunitySourceId(input.communitySourceId)
3132
+ ) {
3133
+ throw new Error("Community source ids must be valid bounded identifiers.");
3134
+ }
2907
3135
  assertValidWorkspaceAppId(initial.appId);
2908
3136
  requireWorkspaceAppOrgId();
2909
3137
  normalizeBuilderAgentAttachments(input.attachments);
@@ -2955,15 +3183,39 @@ export async function startWorkspaceAppCreation(input: {
2955
3183
  generateWorkspaceAppDescription(input.prompt, built.appId);
2956
3184
 
2957
3185
  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
- });
3186
+ if (input.communitySourceId) {
3187
+ try {
3188
+ await reservePendingWorkspaceApp({
3189
+ appId: built.appId,
3190
+ description: appDescription,
3191
+ projectId: null,
3192
+ visibility: creationVisibility,
3193
+ communitySourceId: input.communitySourceId,
3194
+ });
3195
+ } catch (err) {
3196
+ if (err instanceof WorkspaceAppIdTakenError)
3197
+ return appIdTakenResult(err);
3198
+ throw err;
3199
+ }
3200
+ }
3201
+ try {
3202
+ await writeWorkspaceAppMetadataOverride({
3203
+ appId: built.appId,
3204
+ name: null,
3205
+ description: appDescription,
3206
+ generated: true,
3207
+ sourcePrompt: input.prompt,
3208
+ updatedBy: currentOwnerEmail(),
3209
+ createdBy: currentOwnerEmail(),
3210
+ visibility: creationVisibility,
3211
+ communitySourceId: input.communitySourceId,
3212
+ });
3213
+ } catch (error) {
3214
+ if (input.communitySourceId) {
3215
+ await releasePendingWorkspaceAppReservation(built.appId);
3216
+ }
3217
+ throw error;
3218
+ }
2967
3219
  await requestSelectedVaultKeys({
2968
3220
  appId: built.appId,
2969
3221
  selectedKeys,
@@ -2976,6 +3228,22 @@ export async function startWorkspaceAppCreation(input: {
2976
3228
  mode: "local-agent",
2977
3229
  appId: built.appId,
2978
3230
  prompt,
3231
+ ...(input.communitySourceId && input.attachments?.length
3232
+ ? {
3233
+ attachments: input.attachments.flatMap((attachment) =>
3234
+ attachment.type === "upload" && attachment.text !== undefined
3235
+ ? [
3236
+ {
3237
+ type: "file" as const,
3238
+ name: attachment.name,
3239
+ contentType: attachment.contentType,
3240
+ text: attachment.text,
3241
+ },
3242
+ ]
3243
+ : [],
3244
+ ),
3245
+ }
3246
+ : {}),
2979
3247
  message:
2980
3248
  "Use the local code agent to create this app in the workspace, then open it from /dispatch/apps.",
2981
3249
  };
@@ -3018,12 +3286,34 @@ export async function startWorkspaceAppCreation(input: {
3018
3286
  description: appDescription,
3019
3287
  projectId: builderProjectId,
3020
3288
  visibility: creationVisibility,
3289
+ ...(normalizeCommunitySourceId(input.communitySourceId)
3290
+ ? { communitySourceId: input.communitySourceId }
3291
+ : {}),
3021
3292
  });
3022
3293
  } catch (err) {
3023
3294
  if (err instanceof WorkspaceAppIdTakenError) return appIdTakenResult(err);
3024
3295
  throw err;
3025
3296
  }
3026
3297
 
3298
+ if (input.communitySourceId) {
3299
+ try {
3300
+ await writeWorkspaceAppMetadataOverride({
3301
+ appId: built.appId,
3302
+ communitySourceId: input.communitySourceId,
3303
+ });
3304
+ } catch (error) {
3305
+ try {
3306
+ await releasePendingWorkspaceAppReservation(built.appId);
3307
+ } catch (cleanupError) {
3308
+ console.warn(
3309
+ "[dispatch] failed to release pending workspace app reservation",
3310
+ cleanupError,
3311
+ );
3312
+ }
3313
+ throw error;
3314
+ }
3315
+ }
3316
+
3027
3317
  let result: {
3028
3318
  branchName: string;
3029
3319
  url: string;
@@ -3063,6 +3353,7 @@ export async function startWorkspaceAppCreation(input: {
3063
3353
  description: appDescription,
3064
3354
  sourcePrompt: input.prompt,
3065
3355
  visibility: creationVisibility,
3356
+ communitySourceId: input.communitySourceId,
3066
3357
  branchName: result.branchName,
3067
3358
  builderUrl: result.url,
3068
3359
  });