@agent-native/dispatch 0.6.0 → 0.7.0

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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  3. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  4. package/dist/actions/create-pylon-ticket.js +94 -0
  5. package/dist/actions/create-pylon-ticket.js.map +1 -0
  6. package/dist/actions/create-vault-grant.js +1 -1
  7. package/dist/actions/create-vault-grant.js.map +1 -1
  8. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  9. package/dist/actions/create-vault-secret.js +4 -3
  10. package/dist/actions/create-vault-secret.js.map +1 -1
  11. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  12. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  13. package/dist/actions/get-vault-access-settings.js +10 -0
  14. package/dist/actions/get-vault-access-settings.js.map +1 -0
  15. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  16. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  17. package/dist/actions/index.d.ts.map +1 -1
  18. package/dist/actions/index.js +8 -0
  19. package/dist/actions/index.js.map +1 -1
  20. package/dist/actions/list-integrations-catalog.js +1 -1
  21. package/dist/actions/list-integrations-catalog.js.map +1 -1
  22. package/dist/actions/list-vault-grants.js +1 -1
  23. package/dist/actions/list-vault-grants.js.map +1 -1
  24. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  25. package/dist/actions/list-workspace-apps.js +5 -1
  26. package/dist/actions/list-workspace-apps.js.map +1 -1
  27. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  28. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  29. package/dist/actions/set-vault-access-settings.js +13 -0
  30. package/dist/actions/set-vault-access-settings.js.map +1 -0
  31. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  32. package/dist/actions/start-workspace-app-creation.js +6 -0
  33. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  34. package/dist/actions/sync-vault-to-app.js +1 -1
  35. package/dist/actions/sync-vault-to-app.js.map +1 -1
  36. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  37. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  38. package/dist/actions/update-workspace-app-metadata.js +30 -0
  39. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  40. package/dist/actions/view-screen.d.ts.map +1 -1
  41. package/dist/actions/view-screen.js +4 -2
  42. package/dist/actions/view-screen.js.map +1 -1
  43. package/dist/components/app-keys-popover.d.ts.map +1 -1
  44. package/dist/components/app-keys-popover.js +17 -5
  45. package/dist/components/app-keys-popover.js.map +1 -1
  46. package/dist/components/create-app-popover.d.ts.map +1 -1
  47. package/dist/components/create-app-popover.js +38 -14
  48. package/dist/components/create-app-popover.js.map +1 -1
  49. package/dist/components/dispatch-shell.d.ts +4 -4
  50. package/dist/components/dispatch-shell.d.ts.map +1 -1
  51. package/dist/components/dispatch-shell.js +6 -6
  52. package/dist/components/dispatch-shell.js.map +1 -1
  53. package/dist/components/layout/Layout.d.ts.map +1 -1
  54. package/dist/components/layout/Layout.js +10 -3
  55. package/dist/components/layout/Layout.js.map +1 -1
  56. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  57. package/dist/components/messaging-setup-panel.js +2 -2
  58. package/dist/components/messaging-setup-panel.js.map +1 -1
  59. package/dist/components/workspace-app-card.d.ts.map +1 -1
  60. package/dist/components/workspace-app-card.js +41 -2
  61. package/dist/components/workspace-app-card.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.js +12 -5
  63. package/dist/hooks/use-navigation-state.js.map +1 -1
  64. package/dist/lib/catch-all-target.d.ts +2 -0
  65. package/dist/lib/catch-all-target.d.ts.map +1 -0
  66. package/dist/lib/catch-all-target.js +95 -0
  67. package/dist/lib/catch-all-target.js.map +1 -0
  68. package/dist/lib/workspace-apps.d.ts +9 -0
  69. package/dist/lib/workspace-apps.d.ts.map +1 -1
  70. package/dist/lib/workspace-apps.js.map +1 -1
  71. package/dist/routes/pages/$appId.d.ts +2 -24
  72. package/dist/routes/pages/$appId.d.ts.map +1 -1
  73. package/dist/routes/pages/$appId.js +42 -8
  74. package/dist/routes/pages/$appId.js.map +1 -1
  75. package/dist/routes/pages/approval.d.ts.map +1 -1
  76. package/dist/routes/pages/approval.js +2 -1
  77. package/dist/routes/pages/approval.js.map +1 -1
  78. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  79. package/dist/routes/pages/apps.$appId.js +2 -1
  80. package/dist/routes/pages/apps.$appId.js.map +1 -1
  81. package/dist/routes/pages/integrations.d.ts.map +1 -1
  82. package/dist/routes/pages/integrations.js +20 -15
  83. package/dist/routes/pages/integrations.js.map +1 -1
  84. package/dist/routes/pages/new-app.js +1 -1
  85. package/dist/routes/pages/new-app.js.map +1 -1
  86. package/dist/routes/pages/overview.d.ts.map +1 -1
  87. package/dist/routes/pages/overview.js +14 -1
  88. package/dist/routes/pages/overview.js.map +1 -1
  89. package/dist/routes/pages/vault.d.ts.map +1 -1
  90. package/dist/routes/pages/vault.js +25 -6
  91. package/dist/routes/pages/vault.js.map +1 -1
  92. package/dist/routes/pages/workspace.d.ts.map +1 -1
  93. package/dist/routes/pages/workspace.js +5 -3
  94. package/dist/routes/pages/workspace.js.map +1 -1
  95. package/dist/server/lib/app-creation-store.d.ts +13 -0
  96. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  97. package/dist/server/lib/app-creation-store.js +295 -9
  98. package/dist/server/lib/app-creation-store.js.map +1 -1
  99. package/dist/server/lib/env-config.d.ts.map +1 -1
  100. package/dist/server/lib/env-config.js +5 -0
  101. package/dist/server/lib/env-config.js.map +1 -1
  102. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  103. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  104. package/dist/server/lib/onboarding-steps.js +47 -0
  105. package/dist/server/lib/onboarding-steps.js.map +1 -0
  106. package/dist/server/lib/vault-store.d.ts +55 -0
  107. package/dist/server/lib/vault-store.d.ts.map +1 -1
  108. package/dist/server/lib/vault-store.js +210 -41
  109. package/dist/server/lib/vault-store.js.map +1 -1
  110. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  111. package/dist/server/plugins/agent-chat.js +2 -1
  112. package/dist/server/plugins/agent-chat.js.map +1 -1
  113. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  114. package/dist/server/plugins/core-routes.js +4 -0
  115. package/dist/server/plugins/core-routes.js.map +1 -1
  116. package/dist/server/plugins/integrations.js +2 -2
  117. package/dist/server/plugins/integrations.js.map +1 -1
  118. package/package.json +13 -11
  119. package/src/actions/create-pylon-ticket.ts +109 -0
  120. package/src/actions/create-vault-grant.ts +1 -1
  121. package/src/actions/create-vault-secret.ts +4 -3
  122. package/src/actions/get-vault-access-settings.ts +11 -0
  123. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  124. package/src/actions/index.ts +8 -0
  125. package/src/actions/list-integrations-catalog.ts +1 -1
  126. package/src/actions/list-vault-grants.ts +1 -1
  127. package/src/actions/list-workspace-apps.ts +5 -1
  128. package/src/actions/set-vault-access-settings.ts +16 -0
  129. package/src/actions/start-workspace-app-creation.ts +8 -0
  130. package/src/actions/sync-vault-to-app.ts +1 -1
  131. package/src/actions/update-workspace-app-metadata.ts +32 -0
  132. package/src/actions/view-screen.ts +4 -1
  133. package/src/components/app-keys-popover.tsx +38 -8
  134. package/src/components/create-app-popover.tsx +47 -14
  135. package/src/components/dispatch-shell.tsx +16 -15
  136. package/src/components/layout/Layout.tsx +11 -5
  137. package/src/components/messaging-setup-panel.tsx +54 -39
  138. package/src/components/workspace-app-card.tsx +102 -0
  139. package/src/hooks/use-navigation-state.ts +10 -4
  140. package/src/lib/catch-all-target.spec.ts +218 -0
  141. package/src/lib/catch-all-target.ts +99 -0
  142. package/src/lib/workspace-apps.ts +9 -0
  143. package/src/routes/pages/$appId.tsx +45 -7
  144. package/src/routes/pages/approval.tsx +33 -3
  145. package/src/routes/pages/apps.$appId.tsx +6 -1
  146. package/src/routes/pages/integrations.tsx +57 -18
  147. package/src/routes/pages/new-app.tsx +1 -1
  148. package/src/routes/pages/overview.tsx +69 -29
  149. package/src/routes/pages/vault.tsx +101 -21
  150. package/src/routes/pages/workspace.tsx +21 -3
  151. package/src/server/lib/app-creation-store.spec.ts +61 -2
  152. package/src/server/lib/app-creation-store.ts +386 -11
  153. package/src/server/lib/env-config.ts +5 -0
  154. package/src/server/lib/onboarding-steps.ts +49 -0
  155. package/src/server/lib/vault-store.spec.ts +69 -0
  156. package/src/server/lib/vault-store.ts +266 -49
  157. package/src/server/plugins/agent-chat.ts +2 -1
  158. package/src/server/plugins/core-routes.ts +5 -0
  159. package/src/server/plugins/integrations.ts +2 -2
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getSetting, putSetting } from "@agent-native/core/settings";
6
+ import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
6
7
  import {
7
8
  getBuilderBranchProjectId,
8
9
  getRequestContext,
@@ -12,7 +13,6 @@ import {
12
13
  runBuilderAgent,
13
14
  } from "@agent-native/core/server";
14
15
  import { getDbExec } from "@agent-native/core/db";
15
- import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
16
16
  import {
17
17
  currentOrgId,
18
18
  currentOwnerEmail,
@@ -28,6 +28,7 @@ import {
28
28
  } from "./workspace-resources-store.js";
29
29
 
30
30
  const SETTINGS_KEY = "dispatch-app-creation-settings";
31
+ const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
31
32
  const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
32
33
  const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
33
34
  const WORKSPACE_APPS_GATEWAY_PATH = "/_workspace/apps";
@@ -35,6 +36,9 @@ const WORKSPACE_APPS_GATEWAY_TIMEOUT_MS = 1_000;
35
36
  const MAX_PENDING_APPS = 50;
36
37
  const AGENT_CARD_PATH = "/.well-known/agent-card.json";
37
38
  const AGENT_CARD_FETCH_TIMEOUT_MS = 1_500;
39
+ const DEFAULT_WORKSPACE_APP_AUDIENCE = "internal";
40
+
41
+ type WorkspaceAppAudience = "internal" | "public";
38
42
 
39
43
  export interface WorkspaceAppSummary {
40
44
  id: string;
@@ -43,6 +47,9 @@ export interface WorkspaceAppSummary {
43
47
  path: string;
44
48
  url: string | null;
45
49
  isDispatch: boolean;
50
+ audience: WorkspaceAppAudience;
51
+ publicPaths: string[];
52
+ protectedPaths: string[];
46
53
  status?: "ready" | "pending";
47
54
  statusLabel?: string;
48
55
  builderUrl?: string | null;
@@ -64,6 +71,7 @@ export interface ListWorkspaceAppsOptions {
64
71
  * when rendering the "Hidden apps" expander.
65
72
  */
66
73
  includeArchived?: boolean;
74
+ audience?: WorkspaceAppAudience | "all";
67
75
  }
68
76
 
69
77
  export interface AvailableWorkspaceTemplate {
@@ -103,10 +111,24 @@ interface PendingWorkspaceApp {
103
111
  builderUrl: string | null;
104
112
  branchName: string | null;
105
113
  projectId: string | null;
114
+ audience?: WorkspaceAppAudience;
106
115
  createdAt: string;
107
116
  updatedAt: string;
108
117
  }
109
118
 
119
+ interface WorkspaceAppMetadataOverride {
120
+ name?: string;
121
+ description?: string;
122
+ generated?: boolean;
123
+ sourcePrompt?: string;
124
+ updatedAt?: string;
125
+ updatedBy?: string;
126
+ }
127
+
128
+ interface WorkspaceAppMetadataSettings {
129
+ apps: Record<string, WorkspaceAppMetadataOverride>;
130
+ }
131
+
110
132
  function readJson(file: string): any {
111
133
  try {
112
134
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -137,12 +159,56 @@ function titleCase(value: string): string {
137
159
  .join(" ");
138
160
  }
139
161
 
162
+ function normalizeWhitespace(value: string): string {
163
+ return value.replace(/\s+/g, " ").trim();
164
+ }
165
+
166
+ function ensureSentence(value: string): string {
167
+ if (!value) return value;
168
+ const capitalized = value.charAt(0).toUpperCase() + value.slice(1);
169
+ return /[.!?]$/.test(capitalized) ? capitalized : `${capitalized}.`;
170
+ }
171
+
172
+ function clipSentence(value: string, max = 180): string {
173
+ if (value.length <= max) return value;
174
+ const clipped = value
175
+ .slice(0, max - 1)
176
+ .replace(/\s+\S*$/, "")
177
+ .trim();
178
+ return `${clipped || value.slice(0, max - 1).trim()}…`;
179
+ }
180
+
181
+ export function generateWorkspaceAppDescription(
182
+ prompt: string,
183
+ appId: string,
184
+ ): string {
185
+ const cleaned = normalizeWhitespace(prompt)
186
+ .replace(
187
+ /^(please\s+)?(build|create|make|generate|scaffold)\s+(me\s+|us\s+)?/i,
188
+ "",
189
+ )
190
+ .replace(
191
+ /^(an?\s+)?(workspace\s+)?(agent-native\s+)?(app|tool)\s+(that|to|for)\s+/i,
192
+ "",
193
+ )
194
+ .replace(/^(an?\s+)?(dashboard|workspace|agent)\s+(that|to|for)\s+/i, "");
195
+
196
+ if (!cleaned) return `Workspace app for ${titleCase(appId)}.`;
197
+ return clipSentence(ensureSentence(cleaned));
198
+ }
199
+
140
200
  function scopedSettingsKey(): string {
141
201
  const orgId = currentOrgId();
142
202
  if (orgId) return `${SETTINGS_KEY}:org:${orgId}`;
143
203
  return `${SETTINGS_KEY}:user:${currentOwnerEmail()}`;
144
204
  }
145
205
 
206
+ function workspaceAppMetadataSettingsKey(): string {
207
+ const orgId = currentOrgId();
208
+ if (orgId) return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:org:${orgId}`;
209
+ return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:user:${currentOwnerEmail()}`;
210
+ }
211
+
146
212
  async function readSettingsRecord(): Promise<Record<string, any>> {
147
213
  const raw = await getSetting(scopedSettingsKey()).catch(() => null);
148
214
  return raw && typeof raw === "object" && !Array.isArray(raw)
@@ -150,6 +216,114 @@ async function readSettingsRecord(): Promise<Record<string, any>> {
150
216
  : {};
151
217
  }
152
218
 
219
+ function cleanOptionalText(value: unknown): string | undefined {
220
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
221
+ }
222
+
223
+ function parseWorkspaceAppMetadataSettings(
224
+ raw: unknown,
225
+ ): WorkspaceAppMetadataSettings {
226
+ const record =
227
+ raw && typeof raw === "object" && !Array.isArray(raw)
228
+ ? (raw as Record<string, unknown>)
229
+ : {};
230
+ const rawApps =
231
+ record.apps &&
232
+ typeof record.apps === "object" &&
233
+ !Array.isArray(record.apps)
234
+ ? (record.apps as Record<string, unknown>)
235
+ : {};
236
+ const apps: Record<string, WorkspaceAppMetadataOverride> = {};
237
+
238
+ for (const [id, value] of Object.entries(rawApps)) {
239
+ if (!id.trim() || !value || typeof value !== "object") continue;
240
+ const item = value as Record<string, unknown>;
241
+ const override: WorkspaceAppMetadataOverride = {};
242
+ const name = cleanOptionalText(item.name);
243
+ const description = cleanOptionalText(item.description);
244
+ const sourcePrompt = cleanOptionalText(item.sourcePrompt);
245
+ const updatedAt = cleanOptionalText(item.updatedAt);
246
+ const updatedBy = cleanOptionalText(item.updatedBy);
247
+
248
+ if (name) override.name = name;
249
+ if (description) override.description = description;
250
+ if (item.generated === true) override.generated = true;
251
+ if (sourcePrompt) override.sourcePrompt = sourcePrompt;
252
+ if (updatedAt) override.updatedAt = updatedAt;
253
+ if (updatedBy) override.updatedBy = updatedBy;
254
+
255
+ if (Object.keys(override).length > 0) apps[id.trim()] = override;
256
+ }
257
+
258
+ return { apps };
259
+ }
260
+
261
+ async function readWorkspaceAppMetadataSettings(): Promise<WorkspaceAppMetadataSettings> {
262
+ const raw = await getSetting(workspaceAppMetadataSettingsKey()).catch(
263
+ () => null,
264
+ );
265
+ return parseWorkspaceAppMetadataSettings(raw);
266
+ }
267
+
268
+ async function writeWorkspaceAppMetadataOverride(input: {
269
+ appId: string;
270
+ name?: string | null;
271
+ description?: string | null;
272
+ generated?: boolean;
273
+ sourcePrompt?: string | null;
274
+ updatedBy?: string | null;
275
+ }): Promise<WorkspaceAppMetadataSettings> {
276
+ const key = workspaceAppMetadataSettingsKey();
277
+ const current = parseWorkspaceAppMetadataSettings(
278
+ await getSetting(key).catch(() => null),
279
+ );
280
+ const appId = input.appId.trim();
281
+ const existing = current.apps[appId] ?? {};
282
+ const next: WorkspaceAppMetadataOverride = {
283
+ ...existing,
284
+ updatedAt: new Date().toISOString(),
285
+ };
286
+ const name = cleanOptionalText(input.name);
287
+ const description = cleanOptionalText(input.description);
288
+ const sourcePrompt = cleanOptionalText(input.sourcePrompt);
289
+ const updatedBy = cleanOptionalText(input.updatedBy);
290
+
291
+ if (name) next.name = name;
292
+ else delete next.name;
293
+ if (description) next.description = description;
294
+ else delete next.description;
295
+ if (input.generated === true) next.generated = true;
296
+ else if (input.generated === false) delete next.generated;
297
+ if (sourcePrompt) next.sourcePrompt = sourcePrompt;
298
+ if (updatedBy) next.updatedBy = updatedBy;
299
+
300
+ current.apps[appId] = next;
301
+ await putSetting(key, { apps: current.apps });
302
+ return current;
303
+ }
304
+
305
+ function applyWorkspaceAppMetadataOverride(
306
+ app: WorkspaceAppSummary,
307
+ settings: WorkspaceAppMetadataSettings,
308
+ ): WorkspaceAppSummary {
309
+ const override = settings.apps[app.id];
310
+ if (!override) return app;
311
+
312
+ const name = cleanOptionalText(override.name);
313
+ const description = cleanOptionalText(override.description);
314
+ const generated = override.generated === true;
315
+ const shouldApplyName = !!name && !generated;
316
+ const shouldApplyDescription =
317
+ !!description && (!generated || !cleanOptionalText(app.description));
318
+ if (!shouldApplyName && !shouldApplyDescription) return app;
319
+
320
+ return {
321
+ ...app,
322
+ ...(shouldApplyName ? { name } : {}),
323
+ ...(shouldApplyDescription ? { description } : {}),
324
+ };
325
+ }
326
+
153
327
  function workspaceAppUrl(appPath: string): string | null {
154
328
  const base =
155
329
  process.env.WORKSPACE_GATEWAY_URL ||
@@ -180,6 +354,85 @@ function workspaceAppLink(
180
354
  }
181
355
  }
182
356
 
357
+ function normalizeWorkspaceAppAudience(value: unknown): WorkspaceAppAudience {
358
+ return value === "public" ? "public" : DEFAULT_WORKSPACE_APP_AUDIENCE;
359
+ }
360
+
361
+ function normalizeWorkspaceAppPathList(value: unknown): string[] {
362
+ let rawPaths: unknown[] = [];
363
+ if (Array.isArray(value)) {
364
+ rawPaths = value;
365
+ } else if (typeof value === "string") {
366
+ const trimmed = value.trim();
367
+ if (!trimmed) return [];
368
+ try {
369
+ const parsed = JSON.parse(trimmed);
370
+ rawPaths = Array.isArray(parsed) ? parsed : [trimmed];
371
+ } catch {
372
+ rawPaths = trimmed.split(",");
373
+ }
374
+ }
375
+
376
+ const paths = rawPaths
377
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
378
+ .filter((entry) => entry.startsWith("/"))
379
+ .map((entry) =>
380
+ entry.length > 1 && entry.endsWith("/") ? entry.slice(0, -1) : entry,
381
+ );
382
+ return Array.from(new Set(paths));
383
+ }
384
+
385
+ function workspaceAppAudienceFromPackageJson(
386
+ pkg: unknown,
387
+ ): WorkspaceAppAudience | undefined {
388
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return undefined;
389
+ const record = pkg as Record<string, any>;
390
+ const config = record["agent-native"] ?? record.agentNative;
391
+ const nested =
392
+ config && typeof config === "object" && !Array.isArray(config)
393
+ ? (config as Record<string, any>)
394
+ : {};
395
+ const raw =
396
+ nested.workspaceApp?.audience ??
397
+ nested.workspace?.audience ??
398
+ nested.audience ??
399
+ record.workspaceAppAudience;
400
+ if (raw === undefined) return undefined;
401
+ return normalizeWorkspaceAppAudience(raw);
402
+ }
403
+
404
+ function workspaceAppRouteAccessFromPackageJson(pkg: unknown): {
405
+ publicPaths: string[];
406
+ protectedPaths: string[];
407
+ } {
408
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
409
+ return { publicPaths: [], protectedPaths: [] };
410
+ }
411
+ const record = pkg as Record<string, any>;
412
+ const config = record["agent-native"] ?? record.agentNative;
413
+ const nested =
414
+ config && typeof config === "object" && !Array.isArray(config)
415
+ ? (config as Record<string, any>)
416
+ : {};
417
+ return {
418
+ publicPaths: normalizeWorkspaceAppPathList(
419
+ nested.workspaceApp?.publicPaths ??
420
+ nested.workspaceApp?.publicPagePaths ??
421
+ nested.workspace?.publicPaths ??
422
+ nested.publicPaths ??
423
+ record.workspaceAppPublicPaths,
424
+ ),
425
+ protectedPaths: normalizeWorkspaceAppPathList(
426
+ nested.workspaceApp?.protectedPaths ??
427
+ nested.workspaceApp?.privatePaths ??
428
+ nested.workspaceApp?.authRequiredPaths ??
429
+ nested.workspace?.protectedPaths ??
430
+ nested.protectedPaths ??
431
+ record.workspaceAppProtectedPaths,
432
+ ),
433
+ };
434
+ }
435
+
183
436
  function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
184
437
  const rawApps = Array.isArray(parsed?.apps)
185
438
  ? parsed.apps
@@ -208,6 +461,12 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
208
461
  typeof entry.isDispatch === "boolean"
209
462
  ? entry.isDispatch
210
463
  : id === "dispatch",
464
+ audience:
465
+ entry.audience === undefined
466
+ ? DEFAULT_WORKSPACE_APP_AUDIENCE
467
+ : normalizeWorkspaceAppAudience(entry.audience),
468
+ publicPaths: normalizeWorkspaceAppPathList(entry.publicPaths),
469
+ protectedPaths: normalizeWorkspaceAppPathList(entry.protectedPaths),
211
470
  status: "ready",
212
471
  } satisfies WorkspaceAppSummary;
213
472
  })
@@ -228,7 +487,7 @@ function sortWorkspaceApps(a: WorkspaceAppSummary, b: WorkspaceAppSummary) {
228
487
  function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
229
488
  if (!Array.isArray(value)) return [];
230
489
  return value
231
- .map((entry) => {
490
+ .map((entry): PendingWorkspaceApp | null => {
232
491
  if (!entry || typeof entry !== "object") return null;
233
492
  const record = entry as Record<string, unknown>;
234
493
  const id = typeof record.id === "string" ? record.id.trim() : "";
@@ -259,6 +518,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
259
518
  typeof record.projectId === "string" && record.projectId.trim()
260
519
  ? record.projectId.trim()
261
520
  : null,
521
+ ...(record.audience === undefined
522
+ ? {}
523
+ : { audience: normalizeWorkspaceAppAudience(record.audience) }),
262
524
  createdAt:
263
525
  typeof record.createdAt === "string" && record.createdAt.trim()
264
526
  ? record.createdAt.trim()
@@ -356,6 +618,9 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
356
618
  path: app.path,
357
619
  url: app.builderUrl,
358
620
  isDispatch: false,
621
+ audience: app.audience ?? DEFAULT_WORKSPACE_APP_AUDIENCE,
622
+ publicPaths: [],
623
+ protectedPaths: [],
359
624
  status: "pending",
360
625
  statusLabel: "Building in Builder",
361
626
  builderUrl: app.builderUrl,
@@ -494,6 +759,8 @@ async function maybeIncludeAgentCards(
494
759
  async function recordPendingWorkspaceApp(input: {
495
760
  appId: string;
496
761
  projectId: string | null;
762
+ description: string;
763
+ sourcePrompt: string;
497
764
  branchName?: string | null;
498
765
  builderUrl?: string | null;
499
766
  }) {
@@ -505,6 +772,7 @@ async function recordPendingWorkspaceApp(input: {
505
772
  id: input.appId,
506
773
  name: titleCase(input.appId),
507
774
  description:
775
+ input.description ||
508
776
  "Builder is creating this app. The workspace path becomes live after the branch is merged and deployed.",
509
777
  path: `/${input.appId}`,
510
778
  builderUrl: input.builderUrl?.trim() || null,
@@ -522,6 +790,14 @@ async function recordPendingWorkspaceApp(input: {
522
790
  ].slice(0, MAX_PENDING_APPS),
523
791
  });
524
792
 
793
+ await writeWorkspaceAppMetadataOverride({
794
+ appId: input.appId,
795
+ description: input.description,
796
+ generated: true,
797
+ sourcePrompt: input.sourcePrompt,
798
+ updatedBy: currentOwnerEmail(),
799
+ });
800
+
525
801
  await recordAudit({
526
802
  action: "workspace-app.pending",
527
803
  targetType: "workspace-app",
@@ -614,11 +890,12 @@ function readWorkspaceAppsFromFilesystem(
614
890
 
615
891
  const apps = fs
616
892
  .readdirSync(appsDir, { withFileTypes: true })
617
- .filter((entry) => entry.isDirectory())
893
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
618
894
  .map((entry): WorkspaceAppSummary | null => {
619
895
  const appDir = path.join(appsDir, entry.name);
620
896
  const pkg = readJson(path.join(appDir, "package.json"));
621
897
  if (!pkg) return null;
898
+ const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
622
899
  return {
623
900
  id: entry.name,
624
901
  name: pkg.displayName || titleCase(entry.name),
@@ -626,6 +903,11 @@ function readWorkspaceAppsFromFilesystem(
626
903
  path: `/${entry.name}`,
627
904
  url: workspaceAppUrl(`/${entry.name}`),
628
905
  isDispatch: entry.name === "dispatch",
906
+ audience:
907
+ workspaceAppAudienceFromPackageJson(pkg) ??
908
+ DEFAULT_WORKSPACE_APP_AUDIENCE,
909
+ publicPaths: routeAccess.publicPaths,
910
+ protectedPaths: routeAccess.protectedPaths,
629
911
  status: "ready",
630
912
  } satisfies WorkspaceAppSummary;
631
913
  })
@@ -689,17 +971,92 @@ async function applyArchivedAndPending(
689
971
  apps: WorkspaceAppSummary[],
690
972
  options: ListWorkspaceAppsOptions,
691
973
  ): Promise<WorkspaceAppSummary[]> {
692
- const [withPending, archivedIds] = await Promise.all([
974
+ const [withPending, archivedIds, metadataSettings] = await Promise.all([
693
975
  appendPendingWorkspaceApps(apps),
694
976
  listArchivedAppIds(),
977
+ readWorkspaceAppMetadataSettings(),
695
978
  ]);
696
979
  const archivedSet = new Set(archivedIds);
697
- const annotated = withPending.map((app) =>
698
- archivedSet.has(app.id) ? { ...app, archived: true } : app,
699
- );
980
+ const annotated = withPending.map((app) => {
981
+ const withMetadata = applyWorkspaceAppMetadataOverride(
982
+ app,
983
+ metadataSettings,
984
+ );
985
+ return archivedSet.has(app.id)
986
+ ? { ...withMetadata, archived: true }
987
+ : withMetadata;
988
+ });
700
989
  return options.includeArchived
701
- ? annotated
702
- : annotated.filter((app) => !app.archived);
990
+ ? filterAppsByAudience(annotated, options.audience)
991
+ : filterAppsByAudience(
992
+ annotated.filter((app) => !app.archived),
993
+ options.audience,
994
+ );
995
+ }
996
+
997
+ function filterAppsByAudience(
998
+ apps: WorkspaceAppSummary[],
999
+ audience: ListWorkspaceAppsOptions["audience"],
1000
+ ): WorkspaceAppSummary[] {
1001
+ if (!audience || audience === "all") return apps;
1002
+ return apps.filter(
1003
+ (app) =>
1004
+ (app.audience ?? DEFAULT_WORKSPACE_APP_AUDIENCE) ===
1005
+ normalizeWorkspaceAppAudience(audience),
1006
+ );
1007
+ }
1008
+
1009
+ export async function updateWorkspaceAppMetadata(input: {
1010
+ appId: string;
1011
+ name?: string | null;
1012
+ description?: string | null;
1013
+ }): Promise<WorkspaceAppSummary> {
1014
+ await assertCanManageAppCreationSettings();
1015
+ const appId = input.appId.trim();
1016
+ assertValidWorkspaceAppId(appId);
1017
+
1018
+ const apps = await listWorkspaceApps({
1019
+ includeAgentCards: false,
1020
+ includeArchived: true,
1021
+ });
1022
+ const app = apps.find((candidate) => candidate.id === appId);
1023
+ if (!app) throw new Error(`Workspace app "${appId}" was not found.`);
1024
+
1025
+ // Treat undefined/null as "field omitted, leave existing value alone"; an
1026
+ // explicit empty string clears the override (the app reverts to its
1027
+ // built-in name / no description). Without this, a partial update that
1028
+ // only touches one field silently wipes the other.
1029
+ const name = input.name == null ? app.name : input.name.trim();
1030
+ const description =
1031
+ input.description == null
1032
+ ? (app.description ?? undefined)
1033
+ : input.description.trim();
1034
+ await writeWorkspaceAppMetadataOverride({
1035
+ appId,
1036
+ name,
1037
+ description,
1038
+ generated: false,
1039
+ updatedBy: currentOwnerEmail(),
1040
+ });
1041
+
1042
+ await recordAudit({
1043
+ action: "workspace-app.metadata-updated",
1044
+ targetType: "workspace-app",
1045
+ targetId: appId,
1046
+ summary: `Updated workspace app details for ${name}`,
1047
+ metadata: {
1048
+ name,
1049
+ descriptionConfigured: !!description,
1050
+ },
1051
+ });
1052
+
1053
+ const updated = (
1054
+ await listWorkspaceApps({
1055
+ includeAgentCards: false,
1056
+ includeArchived: true,
1057
+ })
1058
+ ).find((candidate) => candidate.id === appId);
1059
+ return updated ?? { ...app, name, description };
703
1060
  }
704
1061
 
705
1062
  export async function listWorkspaceApps(
@@ -745,6 +1102,9 @@ export async function listWorkspaceApps(
745
1102
  path: "/dispatch",
746
1103
  url: workspaceAppUrl("/dispatch"),
747
1104
  isDispatch: true,
1105
+ audience: DEFAULT_WORKSPACE_APP_AUDIENCE,
1106
+ publicPaths: [],
1107
+ protectedPaths: [],
748
1108
  status: "ready",
749
1109
  },
750
1110
  ],
@@ -1166,6 +1526,7 @@ async function remoteAppCreationAuthorization(): Promise<
1166
1526
  function buildWorkspaceAppPrompt(input: {
1167
1527
  prompt: string;
1168
1528
  appId?: string | null;
1529
+ description?: string | null;
1169
1530
  template?: string | null;
1170
1531
  selectedKeys?: string[];
1171
1532
  selectedResources?: WorkspaceResourceOption[];
@@ -1176,6 +1537,9 @@ function buildWorkspaceAppPrompt(input: {
1176
1537
  input.prompt.replace(/\b(build|create|make|an?|the|app|tool)\b/gi, " "),
1177
1538
  ) ||
1178
1539
  "new-app";
1540
+ const appDescription =
1541
+ input.description?.trim() ||
1542
+ generateWorkspaceAppDescription(input.prompt, appId);
1179
1543
  const selectedKeys = input.selectedKeys || [];
1180
1544
  const selectedResources = input.selectedResources || [];
1181
1545
  const resourceList = selectedResources.length
@@ -1192,6 +1556,7 @@ function buildWorkspaceAppPrompt(input: {
1192
1556
  "Create a new agent-native app in this workspace.",
1193
1557
  "",
1194
1558
  `App name: ${appId}`,
1559
+ `App description: ${appDescription}`,
1195
1560
  `Template to start from: ${input.template || "starter"}`,
1196
1561
  `User prompt: ${input.prompt.trim()}`,
1197
1562
  "If the user mentions a product or company such as Granola, Loom, Superhuman, Linear, or Notion, treat it as product inspiration unless they explicitly ask to connect to that service. Do not invent or require third-party API keys like GRANOLA_API_KEY just because a product is named.",
@@ -1222,11 +1587,13 @@ function buildWorkspaceAppPrompt(input: {
1222
1587
  "",
1223
1588
  "Branch readiness requirements before handing off:",
1224
1589
  "- The CLI auto-fills package.json name and displayName from the app id; only edit the description / scripts / dependencies if the app actually needs more than the template provides.",
1590
+ `- Save a concise, human-readable app description in apps/${appId}/package.json "description" so Dispatch, A2A discovery, and connected agents can describe what this app does. Use the description above or improve it based on the prompt.`,
1225
1591
  "- Do not add or update workspace-apps.json or .agent-native/workspace-apps.json unless the app needs an explicit external URL override; the root deploy generates the workspace app registry from apps/* and deploy metadata.",
1226
1592
  "- Update pnpm-lock.yaml when adding or changing dependencies so Netlify can install the branch reliably.",
1227
1593
  "- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model; do not leave the branch relying only on uncommitted local state.",
1228
- "- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment.",
1229
- "- Include a final verification note covering the registry entry, manifest/deploy metadata, and agent-card readiness.",
1594
+ "- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment. Every sibling workspace app should be usable over A2A by default through call-agent.",
1595
+ "- Give the app agent context that sibling workspace apps are available over A2A with names and descriptions from the workspace app registry; do not hardcode a stale app list.",
1596
+ "- Include a final verification note covering the registry entry, manifest/deploy metadata, relative same-origin routing, and agent-card readiness.",
1230
1597
  `When it is ready, start or update the workspace dev server and navigate the user to /${appId}.`,
1231
1598
  ].join("\n"),
1232
1599
  };
@@ -1270,6 +1637,7 @@ async function grantSelectedWorkspaceResources(input: {
1270
1637
  export async function startWorkspaceAppCreation(input: {
1271
1638
  prompt: string;
1272
1639
  appId?: string | null;
1640
+ description?: string | null;
1273
1641
  template?: string | null;
1274
1642
  secretIds?: string[];
1275
1643
  resourceIds?: string[];
@@ -1277,6 +1645,7 @@ export async function startWorkspaceAppCreation(input: {
1277
1645
  const initial = buildWorkspaceAppPrompt({
1278
1646
  prompt: input.prompt,
1279
1647
  appId: input.appId,
1648
+ description: input.description,
1280
1649
  template: input.template,
1281
1650
  });
1282
1651
  assertValidWorkspaceAppId(initial.appId);
@@ -1304,11 +1673,15 @@ export async function startWorkspaceAppCreation(input: {
1304
1673
  const built = buildWorkspaceAppPrompt({
1305
1674
  prompt: input.prompt,
1306
1675
  appId: input.appId,
1676
+ description: input.description,
1307
1677
  template: input.template,
1308
1678
  selectedKeys,
1309
1679
  selectedResources,
1310
1680
  });
1311
1681
  const prompt = built.prompt;
1682
+ const appDescription =
1683
+ input.description?.trim() ||
1684
+ generateWorkspaceAppDescription(input.prompt, built.appId);
1312
1685
 
1313
1686
  if (isLocal) {
1314
1687
  await requestSelectedVaultKeys({
@@ -1375,6 +1748,8 @@ export async function startWorkspaceAppCreation(input: {
1375
1748
  await recordPendingWorkspaceApp({
1376
1749
  appId: built.appId,
1377
1750
  projectId: settings.builderProjectId,
1751
+ description: appDescription,
1752
+ sourcePrompt: input.prompt,
1378
1753
  branchName: result.branchName,
1379
1754
  builderUrl: result.url,
1380
1755
  });
@@ -41,4 +41,9 @@ export const envKeys: EnvKeyConfig[] = [
41
41
  label: "WhatsApp phone number ID",
42
42
  required: false,
43
43
  },
44
+ {
45
+ key: "PYLON_API_KEY",
46
+ label: "Pylon API key",
47
+ required: false,
48
+ },
44
49
  ];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Dispatch-specific onboarding steps.
3
+ *
4
+ * Slack/Telegram/etc. are auto-registered at order 60 by the framework when
5
+ * their env keys are declared `required: true` in `env-config.ts`. Without
6
+ * any earlier dispatch-specific step, a brand-new workspace lands on
7
+ * "Connect Slack" as the first visible to-do — which is intimidating before
8
+ * the user has even created a real app. This step nudges them at adding
9
+ * their first workspace app first.
10
+ */
11
+
12
+ import { registerOnboardingStep } from "@agent-native/core/onboarding";
13
+ import { listWorkspaceApps } from "./app-creation-store.js";
14
+
15
+ let registered = false;
16
+
17
+ export function registerDispatchOnboardingSteps(): void {
18
+ if (registered) return;
19
+ registered = true;
20
+
21
+ registerOnboardingStep({
22
+ id: "dispatch:create-first-app",
23
+ title: "Create your first app",
24
+ description:
25
+ "Add a workspace app like Mail, Calendar, or Slides — or describe a custom app from the Apps page.",
26
+ order: 5,
27
+ required: false,
28
+ methods: [
29
+ {
30
+ id: "open-apps",
31
+ kind: "link",
32
+ primary: true,
33
+ label: "Browse apps",
34
+ payload: { url: "/dispatch/apps", external: false },
35
+ },
36
+ ],
37
+ isComplete: async () => {
38
+ try {
39
+ const apps = await listWorkspaceApps({
40
+ includeAgentCards: false,
41
+ includeArchived: true,
42
+ });
43
+ return apps.some((app) => !app.isDispatch);
44
+ } catch {
45
+ return false;
46
+ }
47
+ },
48
+ });
49
+ }