@agent-native/dispatch 0.0.0-beta-20260819184657 → 0.0.0-beta-20260820150521

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.
@@ -15,7 +15,11 @@ import {
15
15
  runBuilderAgent,
16
16
  } from "@agent-native/core/server";
17
17
  import { getOrgSetting } from "@agent-native/core/settings";
18
- import { getSetting, putSetting } from "@agent-native/core/settings";
18
+ import {
19
+ getSetting,
20
+ mutateSetting,
21
+ putSetting,
22
+ } from "@agent-native/core/settings";
19
23
  import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
20
24
  import { resolveAccess } from "@agent-native/core/sharing";
21
25
 
@@ -167,6 +171,7 @@ interface PendingWorkspaceApp {
167
171
  contextId: string | null;
168
172
  contextLabel: string | null;
169
173
  audience?: WorkspaceAppAudience;
174
+ visibility?: WorkspaceAppVisibility;
170
175
  createdBy?: string | null;
171
176
  owner?: string | null;
172
177
  teams?: string[];
@@ -786,6 +791,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
786
791
  ...(record.audience === undefined
787
792
  ? {}
788
793
  : { audience: normalizeWorkspaceAppAudience(record.audience) }),
794
+ ...(record.visibility === "private" || record.visibility === "org"
795
+ ? { visibility: record.visibility }
796
+ : {}),
789
797
  createdAt,
790
798
  updatedAt:
791
799
  typeof record.updatedAt === "string" && record.updatedAt.trim()
@@ -805,6 +813,122 @@ async function listPendingWorkspaceApps(): Promise<PendingWorkspaceApp[]> {
805
813
  return parsePendingWorkspaceApps(raw.pendingApps);
806
814
  }
807
815
 
816
+ async function assertPendingWorkspaceAppCreationAvailable(
817
+ appId: string,
818
+ ): Promise<void> {
819
+ const existing = (await listPendingWorkspaceApps())
820
+ .filter((app) => !isPendingWorkspaceAppExpired(app))
821
+ .find((app) => app.id === appId);
822
+ if (!existing) return;
823
+
824
+ const viewerEmail = currentOwnerEmail().trim().toLowerCase();
825
+ const ownerEmail = (existing.createdBy ?? existing.owner)
826
+ ?.trim()
827
+ .toLowerCase();
828
+ if (ownerEmail && ownerEmail !== viewerEmail) {
829
+ throw new Error(
830
+ `Workspace app "${appId}" is already being created by another member.`,
831
+ );
832
+ }
833
+ }
834
+
835
+ async function assertWorkspaceAppIdRegisteredFree(
836
+ appId: string,
837
+ ): Promise<void> {
838
+ let existing: { id?: unknown }[];
839
+ try {
840
+ const result = await getDbExec().execute({
841
+ sql: "SELECT id FROM workspace_apps WHERE id = ? LIMIT 1",
842
+ args: [appId],
843
+ });
844
+ existing = result.rows as { id?: unknown }[];
845
+ } catch {
846
+ throw new Error(
847
+ "Could not verify the workspace app registry; refusing to reuse an app id.",
848
+ );
849
+ }
850
+ if (existing.length > 0) {
851
+ throw new Error(`Workspace app "${appId}" is already registered.`);
852
+ }
853
+ }
854
+
855
+ async function reservePendingWorkspaceApp(input: {
856
+ appId: string;
857
+ description: string;
858
+ projectId: string | null;
859
+ visibility: WorkspaceAppVisibility;
860
+ }): Promise<void> {
861
+ await assertWorkspaceAppIdRegisteredFree(input.appId);
862
+ const now = new Date().toISOString();
863
+ const context = pendingWorkspaceAppContext();
864
+ const creatorEmail = currentOwnerEmail();
865
+
866
+ await mutateSetting(scopedSettingsKey(), async (current) => {
867
+ const raw =
868
+ current && typeof current === "object" && !Array.isArray(current)
869
+ ? current
870
+ : {};
871
+ const pendingApps = parsePendingWorkspaceApps(raw.pendingApps);
872
+ const existing = pendingApps
873
+ .filter((app) => !isPendingWorkspaceAppExpired(app))
874
+ .find((app) => app.id === input.appId);
875
+ if (existing) {
876
+ throw new Error(
877
+ `Workspace app "${input.appId}" is already being created${
878
+ existing.createdBy || existing.owner
879
+ ? ` by ${existing.createdBy ?? existing.owner}`
880
+ : ""
881
+ }.`,
882
+ );
883
+ }
884
+
885
+ const reservation: PendingWorkspaceApp = {
886
+ id: input.appId,
887
+ name: titleCase(input.appId),
888
+ description:
889
+ input.description ||
890
+ "Builder is creating this app. The workspace path becomes live after the branch is merged and deployed.",
891
+ path: `/${input.appId}`,
892
+ builderUrl: null,
893
+ branchName: null,
894
+ projectId: input.projectId,
895
+ contextId: context?.id ?? null,
896
+ contextLabel: context?.label ?? null,
897
+ visibility: input.visibility,
898
+ createdBy: creatorEmail,
899
+ owner: creatorEmail,
900
+ createdAt: now,
901
+ updatedAt: now,
902
+ expiresAt: pendingWorkspaceAppExpiresAt(now),
903
+ };
904
+
905
+ return {
906
+ ...raw,
907
+ pendingApps: [reservation, ...pendingApps].slice(0, MAX_PENDING_APPS),
908
+ };
909
+ });
910
+ }
911
+
912
+ async function releasePendingWorkspaceAppReservation(appId: string) {
913
+ const contextId = pendingWorkspaceAppContext()?.id ?? null;
914
+ const creatorEmail = currentOwnerEmail().trim().toLowerCase();
915
+ await mutateSetting(scopedSettingsKey(), async (current) => {
916
+ const raw =
917
+ current && typeof current === "object" && !Array.isArray(current)
918
+ ? current
919
+ : {};
920
+ const pendingApps = parsePendingWorkspaceApps(raw.pendingApps);
921
+ return {
922
+ ...raw,
923
+ pendingApps: pendingApps.filter((app) => {
924
+ if (app.id !== appId || app.contextId !== contextId) return true;
925
+ const owner = (app.createdBy ?? app.owner)?.trim().toLowerCase();
926
+ return owner !== creatorEmail;
927
+ }),
928
+ };
929
+ });
930
+ }
931
+
808
932
  function parseArchivedAppIds(value: unknown): string[] {
809
933
  if (!Array.isArray(value)) return [];
810
934
  const ids = value
@@ -890,6 +1014,7 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
890
1014
  protectedPaths: [],
891
1015
  status: "pending",
892
1016
  statusLabel: "Pending Builder branch",
1017
+ visibility: app.visibility,
893
1018
  builderUrl: app.builderUrl,
894
1019
  branchName: app.branchName,
895
1020
  createdAt: app.createdAt,
@@ -1064,7 +1189,6 @@ async function ensureWorkspaceAppRecords(
1064
1189
 
1065
1190
  const orgId = currentOrgId();
1066
1191
  const metadata = await readWorkspaceAppMetadataSettings();
1067
- const defaultVisibility = await workspaceAppDefaultVisibility();
1068
1192
  const db = getDbExec();
1069
1193
  const records = new Map<
1070
1194
  string,
@@ -1107,14 +1231,13 @@ async function ensureWorkspaceAppRecords(
1107
1231
  cleanOptionalText(override?.createdBy) ??
1108
1232
  cleanOptionalText(app.createdBy) ??
1109
1233
  "";
1110
- const hasTrustedCreationMetadata = Boolean(ownerEmail);
1111
1234
  const visibility: WorkspaceAppVisibility =
1112
1235
  override?.visibility === "private"
1113
1236
  ? "private"
1114
1237
  : override?.visibility === "org"
1115
1238
  ? "org"
1116
- : hasTrustedCreationMetadata
1117
- ? defaultVisibility
1239
+ : app.visibility === "private"
1240
+ ? "private"
1118
1241
  : "org";
1119
1242
  const createdAt = appRecordTimestamp(app.createdAt);
1120
1243
  await db.execute({
@@ -1176,7 +1299,20 @@ async function filterWorkspaceAppsByAccess(
1176
1299
  const visibleIds = new Set<string>();
1177
1300
  const candidates: WorkspaceAppSummary[] = [];
1178
1301
  for (const app of apps) {
1179
- if (app.status === "pending" || app.isDispatch) {
1302
+ if (app.status === "pending") {
1303
+ const viewerEmail = userEmail.toLowerCase();
1304
+ const creatorEmail = app.createdBy?.trim().toLowerCase();
1305
+ const ownerEmail = app.owner?.trim().toLowerCase();
1306
+ if (
1307
+ app.visibility !== "private" ||
1308
+ creatorEmail === viewerEmail ||
1309
+ ownerEmail === viewerEmail
1310
+ ) {
1311
+ visibleIds.add(app.id);
1312
+ }
1313
+ continue;
1314
+ }
1315
+ if (app.isDispatch) {
1180
1316
  visibleIds.add(app.id);
1181
1317
  continue;
1182
1318
  }
@@ -1225,6 +1361,7 @@ async function recordPendingWorkspaceApp(input: {
1225
1361
  projectId: string | null;
1226
1362
  description: string;
1227
1363
  sourcePrompt: string;
1364
+ visibility: WorkspaceAppVisibility;
1228
1365
  branchName?: string | null;
1229
1366
  builderUrl?: string | null;
1230
1367
  }) {
@@ -1239,6 +1376,16 @@ async function recordPendingWorkspaceApp(input: {
1239
1376
  const existing = pendingApps
1240
1377
  .filter((app) => !isPendingWorkspaceAppExpired(app))
1241
1378
  .find(samePendingEntry);
1379
+ const creatorEmail = currentOwnerEmail();
1380
+ const existingOwnerEmail = existing?.createdBy ?? existing?.owner;
1381
+ if (
1382
+ existingOwnerEmail &&
1383
+ existingOwnerEmail.trim().toLowerCase() !== creatorEmail.toLowerCase()
1384
+ ) {
1385
+ throw new Error(
1386
+ `Workspace app "${input.appId}" is already being created by another member.`,
1387
+ );
1388
+ }
1242
1389
  const next: PendingWorkspaceApp = {
1243
1390
  id: input.appId,
1244
1391
  name: titleCase(input.appId),
@@ -1251,8 +1398,9 @@ async function recordPendingWorkspaceApp(input: {
1251
1398
  projectId: input.projectId,
1252
1399
  contextId: context?.id ?? null,
1253
1400
  contextLabel: context?.label ?? null,
1254
- createdBy: currentOwnerEmail(),
1255
- owner: currentOwnerEmail(),
1401
+ visibility: input.visibility,
1402
+ createdBy: existing?.createdBy ?? existing?.owner ?? creatorEmail,
1403
+ owner: existing?.owner ?? existing?.createdBy ?? creatorEmail,
1256
1404
  createdAt: existing?.createdAt || now,
1257
1405
  updatedAt: now,
1258
1406
  expiresAt: pendingWorkspaceAppExpiresAt(existing?.createdAt || now),
@@ -1271,8 +1419,9 @@ async function recordPendingWorkspaceApp(input: {
1271
1419
  description: input.description,
1272
1420
  generated: true,
1273
1421
  sourcePrompt: input.sourcePrompt,
1274
- updatedBy: currentOwnerEmail(),
1275
- createdBy: currentOwnerEmail(),
1422
+ updatedBy: creatorEmail,
1423
+ createdBy: next.createdBy,
1424
+ visibility: input.visibility,
1276
1425
  });
1277
1426
 
1278
1427
  await recordAudit({
@@ -1829,6 +1978,9 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
1829
1978
  const appId = (input.appId?.trim() || template).toLowerCase();
1830
1979
  assertValidWorkspaceAppId(appId);
1831
1980
 
1981
+ await assertPendingWorkspaceAppCreationAvailable(appId);
1982
+ await assertWorkspaceAppIdRegisteredFree(appId);
1983
+
1832
1984
  const workspaceRoot = findWorkspaceRoot();
1833
1985
  if (!workspaceRoot) {
1834
1986
  throw new Error("No agent-native workspace detected for scaffolding.");
@@ -1838,10 +1990,34 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
1838
1990
  throw new Error(`apps/${appId} already exists.`);
1839
1991
  }
1840
1992
 
1841
- const output = await runScaffoldCli({
1842
- cwd: workspaceRoot,
1843
- args: ["add-app", appId, "--template", template],
1844
- });
1993
+ const visibility = await workspaceAppDefaultVisibility();
1994
+ const creatorEmail = currentOwnerEmail();
1995
+ let output: string;
1996
+ try {
1997
+ output = await runScaffoldCli({
1998
+ cwd: workspaceRoot,
1999
+ args: ["add-app", appId, "--template", template],
2000
+ });
2001
+ } catch (error) {
2002
+ if (fs.existsSync(appDir)) {
2003
+ fs.rmSync(appDir, { recursive: true, force: true });
2004
+ }
2005
+ throw error;
2006
+ }
2007
+
2008
+ try {
2009
+ await writeWorkspaceAppMetadataOverride({
2010
+ appId,
2011
+ updatedBy: creatorEmail,
2012
+ createdBy: creatorEmail,
2013
+ visibility,
2014
+ });
2015
+ } catch (error) {
2016
+ if (fs.existsSync(appDir)) {
2017
+ fs.rmSync(appDir, { recursive: true, force: true });
2018
+ }
2019
+ throw error;
2020
+ }
1845
2021
 
1846
2022
  await recordAudit({
1847
2023
  action: "workspace-app.scaffolded",
@@ -1864,6 +2040,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
1864
2040
  protectedPaths: [],
1865
2041
  status: "ready",
1866
2042
  createdBy: currentOwnerEmail(),
2043
+ visibility,
1867
2044
  },
1868
2045
  ]);
1869
2046
 
@@ -2391,6 +2568,9 @@ export async function startWorkspaceAppCreation(input: {
2391
2568
  }
2392
2569
  }
2393
2570
 
2571
+ const creationVisibility = await workspaceAppDefaultVisibility();
2572
+ await assertPendingWorkspaceAppCreationAvailable(initial.appId);
2573
+
2394
2574
  const selectedKeys = input.secretIds?.length
2395
2575
  ? (await listSecretOptions())
2396
2576
  .filter((secret) => input.secretIds?.includes(secret.id))
@@ -2413,6 +2593,15 @@ export async function startWorkspaceAppCreation(input: {
2413
2593
  generateWorkspaceAppDescription(input.prompt, built.appId);
2414
2594
 
2415
2595
  if (isLocal) {
2596
+ await writeWorkspaceAppMetadataOverride({
2597
+ appId: built.appId,
2598
+ description: appDescription,
2599
+ generated: true,
2600
+ sourcePrompt: input.prompt,
2601
+ updatedBy: currentOwnerEmail(),
2602
+ createdBy: currentOwnerEmail(),
2603
+ visibility: creationVisibility,
2604
+ });
2416
2605
  await requestSelectedVaultKeys({
2417
2606
  appId: built.appId,
2418
2607
  selectedKeys,
@@ -2501,6 +2690,12 @@ export async function startWorkspaceAppCreation(input: {
2501
2690
  }
2502
2691
 
2503
2692
  const builderUserId = builderCreds.userId || undefined;
2693
+ await reservePendingWorkspaceApp({
2694
+ appId: built.appId,
2695
+ description: appDescription,
2696
+ projectId: builderProjectId,
2697
+ visibility: creationVisibility,
2698
+ });
2504
2699
 
2505
2700
  let result: {
2506
2701
  branchName: string;
@@ -2518,6 +2713,14 @@ export async function startWorkspaceAppCreation(input: {
2518
2713
  }),
2519
2714
  );
2520
2715
  } catch (err) {
2716
+ try {
2717
+ await releasePendingWorkspaceAppReservation(built.appId);
2718
+ } catch (cleanupError) {
2719
+ console.warn(
2720
+ "[dispatch] failed to release pending workspace app reservation",
2721
+ cleanupError,
2722
+ );
2723
+ }
2521
2724
  const detail =
2522
2725
  err instanceof Error && err.message
2523
2726
  ? err.message
@@ -2538,6 +2741,7 @@ export async function startWorkspaceAppCreation(input: {
2538
2741
  projectId: builderProjectId,
2539
2742
  description: appDescription,
2540
2743
  sourcePrompt: input.prompt,
2744
+ visibility: creationVisibility,
2541
2745
  branchName: result.branchName,
2542
2746
  builderUrl: result.url,
2543
2747
  });