@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.
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +178 -14
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/package.json +4 -4
- package/src/server/lib/app-creation-store.spec.ts +296 -19
- package/src/server/lib/app-creation-store.ts +218 -14
|
@@ -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 {
|
|
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
|
-
:
|
|
1117
|
-
?
|
|
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"
|
|
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
|
-
|
|
1255
|
-
|
|
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:
|
|
1275
|
-
createdBy:
|
|
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
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
});
|