@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.0.0-beta-20260819184657",
3
+ "version": "0.0.0-beta-20260820150521",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -87,7 +87,7 @@
87
87
  "tailwind-merge": "^3.5.0",
88
88
  "vaul": "^1.1.2",
89
89
  "zod": "^4.3.6",
90
- "@agent-native/toolkit": "0.16.7"
90
+ "@agent-native/toolkit": "0.0.0-beta-20260820150521"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@react-router/dev": "^8.1.0",
@@ -101,10 +101,10 @@
101
101
  "typescript-7": "npm:typescript@^7.0.2",
102
102
  "vite": "8.1.0",
103
103
  "vitest": "^4.1.5",
104
- "@agent-native/core": "0.0.0-beta-20260819184657"
104
+ "@agent-native/core": "0.0.0-beta-20260820150521"
105
105
  },
106
106
  "peerDependencies": {
107
- "@agent-native/core": "0.0.0-beta-20260819184657",
107
+ "@agent-native/core": "0.0.0-beta-20260820150521",
108
108
  "react": ">=19.2.7",
109
109
  "react-dom": ">=19.2.7",
110
110
  "react-router": ">=8"
@@ -6,6 +6,7 @@ import {
6
6
  getAppCreationSettings,
7
7
  listAvailableWorkspaceTemplates,
8
8
  listWorkspaceApps,
9
+ scaffoldWorkspaceAppFromTemplate,
9
10
  setAppCreationSettings,
10
11
  startWorkspaceAppCreation,
11
12
  updateWorkspaceAppMetadata,
@@ -23,6 +24,13 @@ const mocks = vi.hoisted(() => {
23
24
  settings,
24
25
  state,
25
26
  getSetting: vi.fn(async (key: string) => settings.get(key) ?? null),
27
+ mutateSetting: vi.fn(
28
+ async (key: string, updater: (current: any) => any) => {
29
+ const next = await updater(settings.get(key) ?? null);
30
+ settings.set(key, next);
31
+ return next;
32
+ },
33
+ ),
26
34
  putSetting: vi.fn(async (key: string, value: unknown) => {
27
35
  settings.set(key, value);
28
36
  }),
@@ -32,9 +40,19 @@ const mocks = vi.hoisted(() => {
32
40
  resource: {},
33
41
  })),
34
42
  getDbExec: vi.fn(() => ({
35
- execute: vi.fn(async () => ({
36
- rows: state.orgRole ? [{ role: state.orgRole }] : [],
37
- })),
43
+ execute: vi.fn(async (statement: unknown) => {
44
+ const sql =
45
+ typeof statement === "string"
46
+ ? statement
47
+ : String((statement as { sql?: unknown })?.sql ?? "");
48
+ if (sql.includes("SELECT id FROM workspace_apps")) {
49
+ return { rows: [], rowsAffected: 0 };
50
+ }
51
+ return {
52
+ rows: state.orgRole ? [{ role: state.orgRole }] : [],
53
+ rowsAffected: 0,
54
+ };
55
+ }),
38
56
  })),
39
57
  resolveBuilderCredentialsDetailed: vi.fn(async () => ({
40
58
  privateKey: null as string | null,
@@ -78,6 +96,7 @@ vi.mock("@agent-native/core/db", async (importOriginal) => {
78
96
 
79
97
  vi.mock("@agent-native/core/settings", () => ({
80
98
  getSetting: (...args: any[]) => mocks.getSetting(...args),
99
+ mutateSetting: (...args: any[]) => mocks.mutateSetting(...args),
81
100
  putSetting: (...args: any[]) => mocks.putSetting(...args),
82
101
  getOrgSetting: (...args: any[]) => mocks.getOrgSetting(...args),
83
102
  }));
@@ -119,12 +138,32 @@ afterEach(() => {
119
138
  vi.unstubAllGlobals();
120
139
  vi.clearAllMocks();
121
140
  mocks.settings.clear();
141
+ mocks.getOrgSetting.mockReset();
142
+ mocks.getOrgSetting.mockResolvedValue(null);
143
+ mocks.mutateSetting.mockReset();
144
+ mocks.mutateSetting.mockImplementation(
145
+ async (key: string, updater: (current: any) => any) => {
146
+ const next = await updater(mocks.settings.get(key) ?? null);
147
+ mocks.settings.set(key, next);
148
+ return next;
149
+ },
150
+ );
122
151
  mocks.state.orgRole = "admin";
123
152
  mocks.getDbExec.mockReset();
124
153
  mocks.getDbExec.mockImplementation(() => ({
125
- execute: vi.fn(async () => ({
126
- rows: mocks.state.orgRole ? [{ role: mocks.state.orgRole }] : [],
127
- })),
154
+ execute: vi.fn(async (statement: unknown) => {
155
+ const sql =
156
+ typeof statement === "string"
157
+ ? statement
158
+ : String((statement as { sql?: unknown })?.sql ?? "");
159
+ if (sql.includes("SELECT id FROM workspace_apps")) {
160
+ return { rows: [], rowsAffected: 0 };
161
+ }
162
+ return {
163
+ rows: mocks.state.orgRole ? [{ role: mocks.state.orgRole }] : [],
164
+ rowsAffected: 0,
165
+ };
166
+ }),
128
167
  }));
129
168
  mocks.resolveAccess.mockReset();
130
169
  mocks.resolveAccess.mockResolvedValue({ role: "viewer", resource: {} });
@@ -368,7 +407,7 @@ describe("listWorkspaceApps", () => {
368
407
  expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
369
408
  });
370
409
 
371
- it("does not create or expose apps when the org visibility setting cannot be read", async () => {
410
+ it("keeps legacy apps organization-visible when the org default cannot be read", async () => {
372
411
  stubManifest([
373
412
  { id: "dispatch", name: "Dispatch", path: "/dispatch" },
374
413
  { id: "private-app", name: "Private app", path: "/private-app" },
@@ -377,12 +416,12 @@ describe("listWorkspaceApps", () => {
377
416
  new Error("settings store unavailable"),
378
417
  );
379
418
 
380
- await expect(
381
- runWithRequestContext(
382
- { userEmail: "dev@example.test", orgId: "org-123" },
383
- () => listWorkspaceApps({ includeAgentCards: false }),
384
- ),
385
- ).rejects.toThrow("settings store unavailable");
419
+ const apps = await runWithRequestContext(
420
+ { userEmail: "dev@example.test", orgId: "org-123" },
421
+ () => listWorkspaceApps({ includeAgentCards: false }),
422
+ );
423
+
424
+ expect(apps.map((app) => app.id)).toEqual(["dispatch", "private-app"]);
386
425
  });
387
426
 
388
427
  it("fails closed when the workspace-app access schema is unavailable", async () => {
@@ -417,7 +456,7 @@ describe("listWorkspaceApps", () => {
417
456
  expect(mocks.resolveAccess).not.toHaveBeenCalled();
418
457
  });
419
458
 
420
- it("uses the organization default only for apps with trusted creation metadata", async () => {
459
+ it("does not apply the current default retroactively to legacy apps", async () => {
421
460
  stubNoPendingContext();
422
461
  stubManifest([
423
462
  {
@@ -451,9 +490,8 @@ describe("listWorkspaceApps", () => {
451
490
  );
452
491
 
453
492
  expect(apps.find((app) => app.id === "legacy-app")?.visibility).toBe("org");
454
- expect(apps.find((app) => app.id === "new-app")?.visibility).toBe(
455
- "private",
456
- );
493
+ expect(apps.find((app) => app.id === "new-app")?.visibility).toBe("org");
494
+ expect(mocks.getOrgSetting).not.toHaveBeenCalled();
457
495
  const inserts = execute.mock.calls.filter(([statement]) =>
458
496
  String((statement as { sql?: unknown })?.sql ?? "").includes(
459
497
  "INSERT INTO workspace_apps",
@@ -461,7 +499,7 @@ describe("listWorkspaceApps", () => {
461
499
  );
462
500
  expect(inserts.map(([statement]) => (statement as any).args?.[3])).toEqual([
463
501
  "org",
464
- "private",
502
+ "org",
465
503
  ]);
466
504
  });
467
505
 
@@ -592,6 +630,61 @@ describe("listWorkspaceApps", () => {
592
630
  expect(apps.map((app) => app.id)).toEqual(["dispatch", "legacy"]);
593
631
  });
594
632
 
633
+ it("hides private pending apps from non-creators", async () => {
634
+ stubNoPendingContext();
635
+ stubManifest();
636
+ mocks.settings.set(settingsKey, {
637
+ pendingApps: [
638
+ pendingApp("private-pending", {
639
+ visibility: "private",
640
+ createdBy: "creator@example.test",
641
+ owner: "creator@example.test",
642
+ }),
643
+ ],
644
+ });
645
+
646
+ const apps = await runWithRequestContext(
647
+ { userEmail: "viewer@example.test", orgId: "org-123" },
648
+ () => listWorkspaceApps({ includeAgentCards: false }),
649
+ );
650
+
651
+ expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
652
+ });
653
+
654
+ it("fails closed when a private app access record cannot be inserted", async () => {
655
+ stubNoPendingContext();
656
+ stubManifest([
657
+ { id: "scaffolded", name: "Scaffolded", path: "/scaffolded" },
658
+ ]);
659
+ mocks.settings.set("workspace-app-metadata:org:org-123", {
660
+ apps: {
661
+ scaffolded: {
662
+ visibility: "private",
663
+ createdBy: "creator@example.test",
664
+ },
665
+ },
666
+ });
667
+ const execute = vi.fn(async (statement: unknown) => {
668
+ const sql =
669
+ typeof statement === "string"
670
+ ? statement
671
+ : String((statement as { sql?: unknown })?.sql ?? "");
672
+ if (sql.includes("SELECT id, owner_email, org_id, visibility")) {
673
+ return { rows: [], rowsAffected: 0 };
674
+ }
675
+ throw new Error("workspace app access insert failed");
676
+ });
677
+ mocks.getDbExec.mockReturnValue({ execute });
678
+ mocks.resolveAccess.mockResolvedValue(null);
679
+
680
+ const apps = await runWithRequestContext(
681
+ { userEmail: "viewer@example.test", orgId: "org-123" },
682
+ () => listWorkspaceApps({ includeAgentCards: false }),
683
+ );
684
+
685
+ expect(apps).toEqual([]);
686
+ });
687
+
595
688
  it("hides expired pending Builder app rows", async () => {
596
689
  stubManifest();
597
690
  vi.stubEnv("BRANCH", "feature-a");
@@ -783,6 +876,179 @@ describe("startWorkspaceAppCreation", () => {
783
876
  );
784
877
  }
785
878
 
879
+ it("persists the private default for local-agent app creation", async () => {
880
+ mocks.getOrgSetting.mockResolvedValueOnce({ visibility: "private" });
881
+
882
+ const result = (await create("onboarding", {
883
+ userEmail: "dev@example.test",
884
+ orgId: "org-123",
885
+ })) as any;
886
+
887
+ expect(result.mode).toBe("local-agent");
888
+ expect(
889
+ mocks.settings.get("workspace-app-metadata:org:org-123"),
890
+ ).toMatchObject({
891
+ apps: {
892
+ onboarding: { visibility: "private" },
893
+ },
894
+ });
895
+ });
896
+
897
+ it("rejects a cross-member collision with an active pending app id", async () => {
898
+ stubHostedRuntime();
899
+ stubBuilderProjectConfigured();
900
+ mocks.getOrgSetting.mockResolvedValueOnce({ visibility: "private" });
901
+ mocks.settings.set("dispatch-app-creation-settings:org:org-123", {
902
+ pendingApps: [
903
+ {
904
+ id: "onboarding",
905
+ name: "Onboarding",
906
+ description: "Already being created",
907
+ path: "/onboarding",
908
+ builderUrl:
909
+ "https://builder.io/app/projects/project-1/branch/onboarding",
910
+ branchName: "onboarding",
911
+ projectId: "project-1",
912
+ createdBy: "creator@example.test",
913
+ owner: "creator@example.test",
914
+ createdAt: "2026-08-19T21:00:00.000Z",
915
+ updatedAt: "2026-08-19T21:00:00.000Z",
916
+ expiresAt: "2999-01-01T00:00:00.000Z",
917
+ },
918
+ ],
919
+ });
920
+
921
+ await expect(
922
+ create("onboarding", {
923
+ userEmail: "other@example.test",
924
+ orgId: "org-123",
925
+ }),
926
+ ).rejects.toThrow("already being created by another member");
927
+ expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
928
+ expect(
929
+ mocks.settings.get("dispatch-app-creation-settings:org:org-123"),
930
+ ).toMatchObject({
931
+ pendingApps: [
932
+ expect.objectContaining({ createdBy: "creator@example.test" }),
933
+ ],
934
+ });
935
+ });
936
+
937
+ it("rejects a collision from another deployment context", async () => {
938
+ stubHostedRuntime();
939
+ stubBuilderProjectConfigured();
940
+ mocks.settings.set("dispatch-app-creation-settings:org:org-123", {
941
+ pendingApps: [
942
+ {
943
+ id: "onboarding",
944
+ name: "Onboarding",
945
+ description: "Already being created",
946
+ path: "/onboarding",
947
+ contextId: "branch:other-context",
948
+ createdBy: "creator@example.test",
949
+ owner: "creator@example.test",
950
+ createdAt: "2026-08-19T21:00:00.000Z",
951
+ updatedAt: "2026-08-19T21:00:00.000Z",
952
+ expiresAt: "2999-01-01T00:00:00.000Z",
953
+ },
954
+ ],
955
+ });
956
+
957
+ await expect(
958
+ create("onboarding", {
959
+ userEmail: "other@example.test",
960
+ orgId: "org-123",
961
+ }),
962
+ ).rejects.toThrow("already being created by another member");
963
+ expect(mocks.runBuilderAgent).not.toHaveBeenCalled();
964
+ });
965
+
966
+ it("atomically reserves an app id before starting Builder", async () => {
967
+ stubHostedRuntime();
968
+ stubBuilderProjectConfigured();
969
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
970
+ credentials({
971
+ privateKey: "priv",
972
+ publicKey: "pub",
973
+ userId: "builder-user-42",
974
+ }),
975
+ );
976
+ mocks.runBuilderAgent.mockImplementation(async () => {
977
+ expect(
978
+ mocks.settings.get("dispatch-app-creation-settings:org:org-123"),
979
+ ).toMatchObject({
980
+ pendingApps: [
981
+ expect.objectContaining({
982
+ id: "onboarding",
983
+ createdBy: "dev@example.test",
984
+ }),
985
+ ],
986
+ });
987
+ return {
988
+ branchName: "onboarding1",
989
+ url: "https://builder.io/app/projects/project-1/branch/onboarding1",
990
+ status: "processing",
991
+ };
992
+ });
993
+
994
+ const result = (await create("onboarding", {
995
+ userEmail: "dev@example.test",
996
+ orgId: "org-123",
997
+ })) as any;
998
+
999
+ expect(result.mode).toBe("builder");
1000
+ expect(mocks.mutateSetting).toHaveBeenCalled();
1001
+ });
1002
+
1003
+ it("releases the reservation when Builder handoff fails", async () => {
1004
+ stubHostedRuntime();
1005
+ stubBuilderProjectConfigured();
1006
+ mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
1007
+ credentials({
1008
+ privateKey: "priv",
1009
+ publicKey: "pub",
1010
+ userId: "builder-user-42",
1011
+ }),
1012
+ );
1013
+ mocks.runBuilderAgent.mockRejectedValue(new Error("Builder unavailable"));
1014
+
1015
+ const result = (await create("onboarding", {
1016
+ userEmail: "dev@example.test",
1017
+ orgId: "org-123",
1018
+ })) as any;
1019
+
1020
+ expect(result).toMatchObject({ mode: "builder-unavailable" });
1021
+ expect(
1022
+ mocks.settings.get("dispatch-app-creation-settings:org:org-123"),
1023
+ ).toMatchObject({ pendingApps: [] });
1024
+ expect(mocks.settings.get("workspace-app-metadata:org:org-123")).toBe(
1025
+ undefined,
1026
+ );
1027
+ });
1028
+
1029
+ it("rejects a scaffold id already registered in the shared app registry", async () => {
1030
+ mocks.getDbExec.mockReturnValue({
1031
+ execute: vi.fn(async (statement: unknown) => {
1032
+ const sql = String((statement as { sql?: unknown })?.sql ?? "");
1033
+ if (sql.includes("SELECT id FROM workspace_apps")) {
1034
+ return { rows: [{ id: "mail" }], rowsAffected: 0 };
1035
+ }
1036
+ return { rows: [], rowsAffected: 0 };
1037
+ }),
1038
+ });
1039
+
1040
+ await expect(
1041
+ runWithRequestContext(
1042
+ { userEmail: "dev@example.test", orgId: "org-123" },
1043
+ () =>
1044
+ scaffoldWorkspaceAppFromTemplate({
1045
+ template: "mail",
1046
+ appId: "mail",
1047
+ }),
1048
+ ),
1049
+ ).rejects.toThrow("already registered");
1050
+ });
1051
+
786
1052
  it("returns builder-not-connected without leaking the project id when no Builder credentials are configured", async () => {
787
1053
  stubHostedRuntime();
788
1054
  stubBuilderProjectConfigured();
@@ -835,6 +1101,7 @@ describe("startWorkspaceAppCreation", () => {
835
1101
  it("starts the Builder branch and passes the resolved userId through", async () => {
836
1102
  stubHostedRuntime();
837
1103
  stubBuilderProjectConfigured();
1104
+ mocks.getOrgSetting.mockResolvedValueOnce({ visibility: "private" });
838
1105
  mocks.resolveBuilderCredentialsDetailed.mockResolvedValue(
839
1106
  credentials({
840
1107
  privateKey: "priv",
@@ -848,7 +1115,10 @@ describe("startWorkspaceAppCreation", () => {
848
1115
  status: "processing",
849
1116
  });
850
1117
 
851
- const result = (await create()) as any;
1118
+ const result = (await create("onboarding", {
1119
+ userEmail: "dev@example.test",
1120
+ orgId: "org-123",
1121
+ })) as any;
852
1122
 
853
1123
  expect(result.mode).toBe("builder");
854
1124
  expect(mocks.runBuilderAgent).toHaveBeenCalledWith(
@@ -867,6 +1137,13 @@ describe("startWorkspaceAppCreation", () => {
867
1137
  expect(builderPrompt).toContain(
868
1138
  "Treat the source brief's unknowns and follow-up items as assumptions",
869
1139
  );
1140
+ expect(
1141
+ mocks.settings.get("workspace-app-metadata:org:org-123"),
1142
+ ).toMatchObject({
1143
+ apps: {
1144
+ onboarding: { visibility: "private" },
1145
+ },
1146
+ });
870
1147
  });
871
1148
 
872
1149
  it("provisions and remembers the workspace Builder project when none is configured", async () => {