@agent-native/dispatch 0.24.0 → 0.24.2

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 (161) hide show
  1. package/dist/actions/provider-api-register.d.ts +12 -12
  2. package/dist/components/admin-navigation.d.ts.map +1 -1
  3. package/dist/components/admin-navigation.js +21 -5
  4. package/dist/components/admin-navigation.js.map +1 -1
  5. package/dist/components/app-list-row.d.ts +1 -1
  6. package/dist/components/app-list-row.d.ts.map +1 -1
  7. package/dist/components/app-list-row.js +1 -1
  8. package/dist/components/app-list-row.js.map +1 -1
  9. package/dist/components/app-open-actions.d.ts +1 -17
  10. package/dist/components/app-open-actions.d.ts.map +1 -1
  11. package/dist/components/app-open-actions.js +1 -20
  12. package/dist/components/app-open-actions.js.map +1 -1
  13. package/dist/components/app-row-settings.d.ts +6 -0
  14. package/dist/components/app-row-settings.d.ts.map +1 -0
  15. package/dist/components/app-row-settings.js +11 -0
  16. package/dist/components/app-row-settings.js.map +1 -0
  17. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  18. package/dist/components/automation-run-history-dialog.js +3 -2
  19. package/dist/components/automation-run-history-dialog.js.map +1 -1
  20. package/dist/components/connected-app-card.d.ts.map +1 -1
  21. package/dist/components/connected-app-card.js +1 -1
  22. package/dist/components/connected-app-card.js.map +1 -1
  23. package/dist/components/create-app-popover.js +2 -2
  24. package/dist/components/create-app-popover.js.map +1 -1
  25. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  26. package/dist/components/dispatch-control-plane.js +23 -7
  27. package/dist/components/dispatch-control-plane.js.map +1 -1
  28. package/dist/components/dispatch-shell.d.ts.map +1 -1
  29. package/dist/components/dispatch-shell.js +5 -1
  30. package/dist/components/dispatch-shell.js.map +1 -1
  31. package/dist/components/docs-link.d.ts +16 -0
  32. package/dist/components/docs-link.d.ts.map +1 -0
  33. package/dist/components/docs-link.js +34 -0
  34. package/dist/components/docs-link.js.map +1 -0
  35. package/dist/components/layout/Layout.d.ts +1 -0
  36. package/dist/components/layout/Layout.d.ts.map +1 -1
  37. package/dist/components/layout/Layout.js +37 -28
  38. package/dist/components/layout/Layout.js.map +1 -1
  39. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  40. package/dist/components/messaging-setup-panel.js +3 -2
  41. package/dist/components/messaging-setup-panel.js.map +1 -1
  42. package/dist/components/usage-alerts-panel.d.ts +11 -0
  43. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  44. package/dist/components/usage-alerts-panel.js +125 -0
  45. package/dist/components/usage-alerts-panel.js.map +1 -0
  46. package/dist/components/workspace-app-card.d.ts +3 -1
  47. package/dist/components/workspace-app-card.d.ts.map +1 -1
  48. package/dist/components/workspace-app-card.js +70 -29
  49. package/dist/components/workspace-app-card.js.map +1 -1
  50. package/dist/components/workspace-app-search.d.ts +10 -0
  51. package/dist/components/workspace-app-search.d.ts.map +1 -0
  52. package/dist/components/workspace-app-search.js +27 -0
  53. package/dist/components/workspace-app-search.js.map +1 -0
  54. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  55. package/dist/components/workspace-resource-effective-stack.js +3 -2
  56. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  57. package/dist/components/workspace-template-card.d.ts.map +1 -1
  58. package/dist/components/workspace-template-card.js +2 -1
  59. package/dist/components/workspace-template-card.js.map +1 -1
  60. package/dist/lib/workspace-app-layout.d.ts +19 -0
  61. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  62. package/dist/lib/workspace-app-layout.js +75 -0
  63. package/dist/lib/workspace-app-layout.js.map +1 -0
  64. package/dist/lib/workspace-apps.d.ts +10 -0
  65. package/dist/lib/workspace-apps.d.ts.map +1 -1
  66. package/dist/lib/workspace-apps.js +59 -0
  67. package/dist/lib/workspace-apps.js.map +1 -1
  68. package/dist/lib/workspace-apps.test.js +18 -1
  69. package/dist/lib/workspace-apps.test.js.map +1 -1
  70. package/dist/routes/pages/$appId.d.ts.map +1 -1
  71. package/dist/routes/pages/$appId.js +4 -2
  72. package/dist/routes/pages/$appId.js.map +1 -1
  73. package/dist/routes/pages/agents.d.ts.map +1 -1
  74. package/dist/routes/pages/agents.js +2 -3
  75. package/dist/routes/pages/agents.js.map +1 -1
  76. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  77. package/dist/routes/pages/apps.$appId.js +8 -3
  78. package/dist/routes/pages/apps.$appId.js.map +1 -1
  79. package/dist/routes/pages/apps.d.ts.map +1 -1
  80. package/dist/routes/pages/apps.js +31 -16
  81. package/dist/routes/pages/apps.js.map +1 -1
  82. package/dist/routes/pages/audit.d.ts.map +1 -1
  83. package/dist/routes/pages/audit.js +2 -1
  84. package/dist/routes/pages/audit.js.map +1 -1
  85. package/dist/routes/pages/dreams.d.ts.map +1 -1
  86. package/dist/routes/pages/dreams.js +14 -18
  87. package/dist/routes/pages/dreams.js.map +1 -1
  88. package/dist/routes/pages/integrations.d.ts.map +1 -1
  89. package/dist/routes/pages/integrations.js +51 -14
  90. package/dist/routes/pages/integrations.js.map +1 -1
  91. package/dist/routes/pages/metrics.d.ts.map +1 -1
  92. package/dist/routes/pages/metrics.js +4 -1
  93. package/dist/routes/pages/metrics.js.map +1 -1
  94. package/dist/routes/pages/operations.js +1 -1
  95. package/dist/routes/pages/operations.js.map +1 -1
  96. package/dist/routes/pages/settings.d.ts.map +1 -1
  97. package/dist/routes/pages/settings.js +7 -2
  98. package/dist/routes/pages/settings.js.map +1 -1
  99. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  100. package/dist/routes/pages/thread-debug.js +5 -8
  101. package/dist/routes/pages/thread-debug.js.map +1 -1
  102. package/dist/routes/pages/vault.d.ts.map +1 -1
  103. package/dist/routes/pages/vault.js +1 -1
  104. package/dist/routes/pages/vault.js.map +1 -1
  105. package/dist/routes/pages/workspace.d.ts.map +1 -1
  106. package/dist/routes/pages/workspace.js +13 -10
  107. package/dist/routes/pages/workspace.js.map +1 -1
  108. package/dist/server/index.d.ts.map +1 -1
  109. package/dist/server/index.js +3 -0
  110. package/dist/server/index.js.map +1 -1
  111. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  112. package/dist/server/lib/app-creation-store.js +12 -52
  113. package/dist/server/lib/app-creation-store.js.map +1 -1
  114. package/dist/shared/app-roles.d.ts +9 -0
  115. package/dist/shared/app-roles.d.ts.map +1 -0
  116. package/dist/shared/app-roles.js +7 -0
  117. package/dist/shared/app-roles.js.map +1 -0
  118. package/package.json +4 -3
  119. package/src/components/admin-navigation.tsx +57 -11
  120. package/src/components/app-list-row.tsx +1 -2
  121. package/src/components/app-open-actions.tsx +5 -114
  122. package/src/components/app-row-settings.tsx +54 -0
  123. package/src/components/automation-run-history-dialog.tsx +8 -4
  124. package/src/components/connected-app-card.tsx +0 -1
  125. package/src/components/create-app-popover.spec.tsx +1 -1
  126. package/src/components/create-app-popover.tsx +2 -2
  127. package/src/components/dispatch-control-plane.spec.tsx +36 -2
  128. package/src/components/dispatch-control-plane.tsx +83 -27
  129. package/src/components/dispatch-shell.tsx +7 -1
  130. package/src/components/docs-link.tsx +52 -0
  131. package/src/components/layout/Layout.spec.tsx +10 -0
  132. package/src/components/layout/Layout.tsx +50 -23
  133. package/src/components/messaging-setup-panel.tsx +10 -3
  134. package/src/components/usage-alerts-panel.tsx +445 -0
  135. package/src/components/workspace-app-card.spec.tsx +130 -9
  136. package/src/components/workspace-app-card.tsx +128 -71
  137. package/src/components/workspace-app-search.tsx +96 -0
  138. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  139. package/src/components/workspace-template-card.spec.tsx +52 -23
  140. package/src/components/workspace-template-card.tsx +7 -10
  141. package/src/lib/workspace-app-layout.spec.ts +47 -0
  142. package/src/lib/workspace-app-layout.ts +121 -0
  143. package/src/lib/workspace-apps.test.ts +45 -1
  144. package/src/lib/workspace-apps.ts +72 -0
  145. package/src/routes/pages/$appId.tsx +3 -6
  146. package/src/routes/pages/agents.tsx +6 -3
  147. package/src/routes/pages/apps.$appId.tsx +6 -6
  148. package/src/routes/pages/apps.tsx +116 -62
  149. package/src/routes/pages/audit.tsx +28 -8
  150. package/src/routes/pages/dreams.tsx +92 -142
  151. package/src/routes/pages/integrations.tsx +147 -68
  152. package/src/routes/pages/metrics.tsx +8 -0
  153. package/src/routes/pages/operations.tsx +2 -2
  154. package/src/routes/pages/settings.tsx +14 -9
  155. package/src/routes/pages/thread-debug.tsx +18 -36
  156. package/src/routes/pages/vault.tsx +7 -2
  157. package/src/routes/pages/workspace.tsx +92 -56
  158. package/src/server/index.ts +4 -0
  159. package/src/server/lib/app-creation-store.spec.ts +90 -43
  160. package/src/server/lib/app-creation-store.ts +15 -61
  161. package/src/shared/app-roles.ts +8 -0
@@ -67,7 +67,7 @@ import {
67
67
  } from "../../components/workspace-resource-impact-preview";
68
68
 
69
69
  export function meta() {
70
- return [{ title: "Agent Resources — Dispatch" }];
70
+ return [{ title: "Resources — Dispatch" }];
71
71
  }
72
72
 
73
73
  const KIND_CONFIG = {
@@ -600,7 +600,11 @@ function EffectiveContextPreview({ resource }: { resource: any }) {
600
600
  organization/app override, then personal override.
601
601
  </p>
602
602
  </div>
603
- <Badge variant="outline">{availabilityLabel(availability)}</Badge>
603
+ {isLoading ? (
604
+ <Skeleton className="h-6 w-24 rounded-full" />
605
+ ) : (
606
+ <Badge variant="outline">{availabilityLabel(availability)}</Badge>
607
+ )}
604
608
  </div>
605
609
 
606
610
  <div className="mt-3 grid gap-3 md:grid-cols-2">
@@ -881,14 +885,13 @@ function ResourceRow({ resource, grants }: { resource: any; grants: any[] }) {
881
885
  );
882
886
  }
883
887
 
884
- function GlobalContextSection({ resources }: { resources: any[] }) {
885
- const byPath = new Map(
886
- resources.map((resource) => [resource.path, resource]),
887
- );
888
- const missingPaths = STARTER_GLOBAL_CONTEXT.filter(
889
- (item) => !byPath.has(item.path),
890
- ).map((item) => item.path);
891
- const presentCount = STARTER_GLOBAL_CONTEXT.length - missingPaths.length;
888
+ function GlobalContextSection({
889
+ resources,
890
+ isLoading,
891
+ }: {
892
+ resources: any[];
893
+ isLoading: boolean;
894
+ }) {
892
895
  const restoreStarter = useActionMutation(
893
896
  "restore-starter-workspace-resources",
894
897
  {
@@ -905,6 +908,44 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
905
908
  },
906
909
  );
907
910
 
911
+ if (isLoading && resources.length === 0) {
912
+ return (
913
+ <section className="space-y-3">
914
+ <div>
915
+ <Skeleton className="h-4 w-28" />
916
+ <Skeleton className="mt-2 h-3 w-full max-w-xl" />
917
+ <Skeleton className="mt-1.5 h-3 w-2/3 max-w-lg" />
918
+ </div>
919
+ <div className="overflow-hidden rounded-lg bg-card">
920
+ {Array.from({ length: STARTER_GLOBAL_CONTEXT.length }).map(
921
+ (_, index) => (
922
+ <div
923
+ key={index}
924
+ className="flex items-center gap-3 border-t px-4 py-3 first:border-t-0"
925
+ >
926
+ <Skeleton className="size-4 rounded-full" />
927
+ <div className="min-w-0 flex-1 space-y-1.5">
928
+ <Skeleton className="h-4 w-28" />
929
+ <Skeleton className="h-3 w-64 max-w-full" />
930
+ <Skeleton className="h-3 w-40 max-w-full" />
931
+ </div>
932
+ <Skeleton className="h-6 w-16 rounded-full" />
933
+ </div>
934
+ ),
935
+ )}
936
+ </div>
937
+ </section>
938
+ );
939
+ }
940
+
941
+ const byPath = new Map(
942
+ resources.map((resource) => [resource.path, resource]),
943
+ );
944
+ const missingPaths = STARTER_GLOBAL_CONTEXT.filter(
945
+ (item) => !byPath.has(item.path),
946
+ ).map((item) => item.path);
947
+ const presentCount = STARTER_GLOBAL_CONTEXT.length - missingPaths.length;
948
+
908
949
  return (
909
950
  <section className="space-y-3">
910
951
  <div className="flex items-center justify-between gap-3">
@@ -938,35 +979,44 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
938
979
  </Badge>
939
980
  </div>
940
981
  </div>
941
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
982
+ <div className="overflow-hidden rounded-lg bg-card">
942
983
  {STARTER_GLOBAL_CONTEXT.map((item) => {
943
984
  const resource = byPath.get(item.path);
944
985
  const exists = !!resource;
945
986
  const global = resource?.scope === "all";
946
987
  return (
947
- <div key={item.path} className="rounded-lg bg-card p-4">
948
- <div className="flex items-start justify-between gap-3">
949
- <div className="min-w-0">
950
- <div className="flex items-center gap-2">
951
- {exists ? (
952
- <IconCircleCheck
953
- size={15}
954
- className="shrink-0 text-green-600 dark:text-green-400"
955
- />
956
- ) : (
957
- <IconAlertCircle
958
- size={15}
959
- className="shrink-0 text-amber-600 dark:text-amber-400"
960
- />
961
- )}
962
- <h3 className="truncate text-sm font-medium text-foreground">
963
- {item.label}
964
- </h3>
965
- </div>
966
- <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
967
- {item.description}
968
- </p>
988
+ <div
989
+ key={item.path}
990
+ className="flex flex-wrap items-center gap-3 border-t px-4 py-3 first:border-t-0"
991
+ >
992
+ {exists ? (
993
+ <IconCircleCheck size={16} className="shrink-0 text-primary" />
994
+ ) : (
995
+ <IconAlertCircle
996
+ size={16}
997
+ className="shrink-0 text-amber-600 dark:text-amber-400"
998
+ />
999
+ )}
1000
+ <div className="min-w-0 flex-1">
1001
+ <div className="truncate text-sm font-medium text-foreground">
1002
+ {item.label}
1003
+ </div>
1004
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">
1005
+ {item.description}
1006
+ </div>
1007
+ <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground/80">
1008
+ {item.path}
969
1009
  </div>
1010
+ </div>
1011
+ <div className="flex items-center gap-1.5">
1012
+ {exists ? (
1013
+ <Badge variant="outline">
1014
+ {global ? "All apps" : "Selected"}
1015
+ </Badge>
1016
+ ) : null}
1017
+ {resource && isAutoLoadedInstruction(resource) ? (
1018
+ <Badge variant="outline">Auto-loaded</Badge>
1019
+ ) : null}
970
1020
  {resource ? (
971
1021
  <EditResourceDialog
972
1022
  resource={resource}
@@ -992,24 +1042,6 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
992
1042
  </Button>
993
1043
  )}
994
1044
  </div>
995
- <div className="mt-3 space-y-2">
996
- <div className="truncate font-mono text-[11px] text-muted-foreground">
997
- {item.path}
998
- </div>
999
- <div className="flex flex-wrap gap-1.5">
1000
- <Badge variant={exists ? "secondary" : "outline"}>
1001
- {exists ? "Present" : "Missing"}
1002
- </Badge>
1003
- {exists ? (
1004
- <Badge variant="outline">
1005
- {global ? "All apps" : "Selected"}
1006
- </Badge>
1007
- ) : null}
1008
- {resource && isAutoLoadedInstruction(resource) ? (
1009
- <Badge variant="outline">Auto-loaded</Badge>
1010
- ) : null}
1011
- </div>
1012
- </div>
1013
1045
  </div>
1014
1046
  );
1015
1047
  })}
@@ -1086,7 +1118,7 @@ export default function WorkspaceRoute() {
1086
1118
 
1087
1119
  return (
1088
1120
  <DispatchShell
1089
- title="Agent Resources"
1121
+ title="Resources"
1090
1122
  description="Manage inherited skills, guardrail instructions, agent profiles, reference resources, and MCP servers. All-app resources are available to every app without syncing."
1091
1123
  >
1092
1124
  {resourcesQuery.isError || grantsQuery.isError ? (
@@ -1111,13 +1143,10 @@ export default function WorkspaceRoute() {
1111
1143
  </div>
1112
1144
  </div>
1113
1145
 
1114
- {!resourcesQuery.isError ? (
1115
- <GlobalContextSection resources={resources || []} />
1116
- ) : null}
1117
-
1118
1146
  {!resourcesQuery.isError && !grantsQuery.isError ? (
1119
1147
  <Tabs defaultValue="skills">
1120
1148
  <TabsList>
1149
+ <TabsTrigger value="context">Global context</TabsTrigger>
1121
1150
  <TabsTrigger value="skills">
1122
1151
  Skills {skills.length > 0 && `(${skills.length})`}
1123
1152
  </TabsTrigger>
@@ -1136,6 +1165,13 @@ export default function WorkspaceRoute() {
1136
1165
  </TabsTrigger>
1137
1166
  </TabsList>
1138
1167
 
1168
+ <TabsContent value="context" className="mt-4">
1169
+ <GlobalContextSection
1170
+ resources={resources || []}
1171
+ isLoading={isLoading}
1172
+ />
1173
+ </TabsContent>
1174
+
1139
1175
  <TabsContent value="skills" className="mt-4">
1140
1176
  <ResourceList
1141
1177
  items={skills}
@@ -1,3 +1,4 @@
1
+ import { defineAppRoles } from "@agent-native/core/org";
1
2
  import {
2
3
  registerPackageActions,
3
4
  type NitroPluginDef,
@@ -5,6 +6,9 @@ import {
5
6
 
6
7
  import { dispatchActions } from "../actions/index.js";
7
8
  import type { DispatchConfig } from "../config.js";
9
+ import { dispatchAccessDescriptor } from "../shared/app-roles.js";
10
+
11
+ defineAppRoles(dispatchAccessDescriptor);
8
12
 
9
13
  /**
10
14
  * Register dispatch's package-contributed actions on import. The framework's
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import {
5
5
  generateWorkspaceAppDescription,
6
+ getAppCreationSettings,
6
7
  listAvailableWorkspaceTemplates,
7
8
  listWorkspaceApps,
8
9
  setAppCreationSettings,
@@ -46,7 +47,6 @@ const mocks = vi.hoisted(() => {
46
47
  })),
47
48
  ensureBuilderProject: vi.fn(),
48
49
  runBuilderAgent: vi.fn(),
49
- resolveBuilderBranchProjectId: vi.fn(async () => ""),
50
50
  getBuilderBranchProjectId: vi.fn(() => ""),
51
51
  writeAppSecret: vi.fn(async () => "secret-id"),
52
52
  deleteAppSecret: vi.fn(async () => true),
@@ -86,8 +86,6 @@ vi.mock("@agent-native/core/server", async (importOriginal) => {
86
86
  ensureBuilderProject: (...args: any[]) =>
87
87
  mocks.ensureBuilderProject(...args),
88
88
  runBuilderAgent: (...args: any[]) => mocks.runBuilderAgent(...args),
89
- resolveBuilderBranchProjectId: (...args: any[]) =>
90
- mocks.resolveBuilderBranchProjectId(...args),
91
89
  getBuilderBranchProjectId: (...args: any[]) =>
92
90
  mocks.getBuilderBranchProjectId(...args),
93
91
  };
@@ -121,12 +119,50 @@ afterEach(() => {
121
119
  source: null,
122
120
  lookupFailed: false,
123
121
  });
124
- mocks.resolveBuilderBranchProjectId.mockResolvedValue("");
125
122
  mocks.getBuilderBranchProjectId.mockReturnValue("");
126
123
  mocks.ensureBuilderProject.mockReset();
127
124
  globalThis.fetch = originalFetch;
128
125
  });
129
126
 
127
+ describe("getAppCreationSettings", () => {
128
+ it("treats the Dispatch project as authoritative over the environment", async () => {
129
+ mocks.settings.set("dispatch-app-creation-settings:org:builder_io", {
130
+ builderProjectId: "dispatch-project",
131
+ });
132
+ vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", "stale-env-project");
133
+
134
+ const result = await runWithRequestContext(
135
+ { userEmail: "dev@example.test", orgId: "builder_io" },
136
+ () => getAppCreationSettings(),
137
+ );
138
+
139
+ expect(result).toMatchObject({
140
+ builderProjectId: "dispatch-project",
141
+ builderProjectIdSource: "dispatch",
142
+ envBuilderProjectId: "stale-env-project",
143
+ });
144
+ });
145
+
146
+ it("treats an explicit Dispatch null as disabled over the environment", async () => {
147
+ mocks.settings.set("dispatch-app-creation-settings:org:builder_io", {
148
+ builderProjectId: null,
149
+ });
150
+ vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", "stale-env-project");
151
+
152
+ const result = await runWithRequestContext(
153
+ { userEmail: "dev@example.test", orgId: "builder_io" },
154
+ () => getAppCreationSettings(),
155
+ );
156
+
157
+ expect(result).toMatchObject({
158
+ builderProjectId: null,
159
+ builderProjectIdSource: "dispatch",
160
+ envBuilderProjectId: "stale-env-project",
161
+ builderBranchingEnabled: false,
162
+ });
163
+ });
164
+ });
165
+
130
166
  describe("listWorkspaceApps", () => {
131
167
  function stubNoPendingContext() {
132
168
  for (const key of [
@@ -399,9 +435,11 @@ describe("listWorkspaceApps", () => {
399
435
  name: "Todo Board",
400
436
  }),
401
437
  ),
402
- ).rejects.toThrow(
403
- "Only organization owners and admins can update app creation settings.",
404
- );
438
+ ).rejects.toMatchObject({
439
+ message:
440
+ "Only organization owners and admins can update app creation settings.",
441
+ statusCode: 403,
442
+ });
405
443
  expect(
406
444
  mocks.settings.get("workspace-app-metadata:org:org-123"),
407
445
  ).toBeUndefined();
@@ -603,12 +641,11 @@ describe("startWorkspaceAppCreation", () => {
603
641
  expect(mocks.runBuilderAgent).toHaveBeenCalledWith(
604
642
  expect.objectContaining({ projectId: "project-provisioned" }),
605
643
  );
606
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
607
- expect.objectContaining({
608
- key: "BUILDER_BRANCH_PROJECT_ID",
609
- value: "project-provisioned",
610
- }),
611
- );
644
+ expect(mocks.settings.get(settingsKey)).toMatchObject({
645
+ builderProjectId: "project-provisioned",
646
+ });
647
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
648
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
612
649
  });
613
650
 
614
651
  it("does not let an organization member persist an auto-provisioned project", async () => {
@@ -653,58 +690,68 @@ describe("setAppCreationSettings", () => {
653
690
  );
654
691
  }
655
692
 
656
- it("stores the project id as an org-scoped credential so member apps resolve it", async () => {
693
+ it("stores the project id in the org-scoped Dispatch settings row", async () => {
657
694
  await save(projectId);
658
695
 
659
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
660
- expect.objectContaining({
661
- key: "BUILDER_BRANCH_PROJECT_ID",
662
- value: projectId,
663
- scope: "org",
664
- scopeId: orgId,
665
- }),
696
+ expect(
697
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
698
+ ).toEqual({ builderProjectId: projectId });
699
+ expect(mocks.putSetting).toHaveBeenCalledWith(
700
+ "dispatch-app-creation-settings:org:builder_io",
701
+ { builderProjectId: projectId },
666
702
  );
667
703
  expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
704
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
668
705
  });
669
706
 
670
- it("scopes the credential to one organization rather than every tenant", async () => {
707
+ it("scopes the settings row to one organization rather than every tenant", async () => {
671
708
  await save(projectId);
672
709
 
673
- const [args] = mocks.writeAppSecret.mock.calls.at(-1) as [
674
- { scope: string; scopeId: string },
675
- ];
676
- expect(args.scope).not.toBe("user");
677
- expect(args.scopeId).toBe(orgId);
710
+ expect(mocks.putSetting.mock.calls.at(-1)?.[0]).toBe(
711
+ "dispatch-app-creation-settings:org:builder_io",
712
+ );
713
+ expect(mocks.putSetting.mock.calls.at(-1)?.[0]).not.toContain(":user:");
714
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
678
715
  });
679
716
 
680
- it("falls back to a solo workspace scope when there is no active org", async () => {
717
+ it("uses a user-scoped settings row when there is no active org", async () => {
681
718
  await save(projectId, { userEmail: "dev@example.test" });
682
719
 
683
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
684
- expect.objectContaining({
685
- scope: "workspace",
686
- scopeId: "solo:dev@example.test",
687
- }),
688
- );
720
+ expect(mocks.settings.get(settingsKey)).toEqual({
721
+ builderProjectId: projectId,
722
+ });
723
+ expect(mocks.putSetting).toHaveBeenCalledWith(settingsKey, {
724
+ builderProjectId: projectId,
725
+ });
726
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
689
727
  });
690
728
 
691
- it("removes the credential when the project id is cleared", async () => {
729
+ it("persists an explicit null when the project id is cleared", async () => {
692
730
  await save(null);
693
731
 
694
- expect(mocks.deleteAppSecret).toHaveBeenCalledWith({
695
- key: "BUILDER_BRANCH_PROJECT_ID",
696
- scope: "org",
697
- scopeId: orgId,
698
- });
732
+ expect(
733
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
734
+ ).toEqual({ builderProjectId: null });
735
+ expect(mocks.putSetting).toHaveBeenCalledWith(
736
+ "dispatch-app-creation-settings:org:builder_io",
737
+ { builderProjectId: null },
738
+ );
739
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
699
740
  expect(mocks.writeAppSecret).not.toHaveBeenCalled();
700
741
  });
701
742
 
702
- it("does not save the setting when the credential write fails", async () => {
743
+ it("never consults the project secret store when saving settings", async () => {
703
744
  mocks.writeAppSecret.mockRejectedValueOnce(
704
745
  new Error("credential store down"),
705
746
  );
706
747
 
707
- await expect(save(projectId)).rejects.toThrow("credential store down");
708
- expect(mocks.putSetting).not.toHaveBeenCalled();
748
+ await expect(save(projectId)).resolves.toMatchObject({
749
+ builderProjectId: projectId,
750
+ });
751
+ expect(
752
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
753
+ ).toEqual({ builderProjectId: projectId });
754
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
755
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
709
756
  });
710
757
  });
@@ -4,17 +4,11 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import { getDbExec } from "@agent-native/core/db";
7
- import {
8
- deleteAppSecret,
9
- writeAppSecret,
10
- type SecretScope,
11
- } from "@agent-native/core/secrets";
12
7
  import {
13
8
  ensureBuilderProject,
14
9
  getBuilderBranchProjectId,
15
10
  getRequestContext,
16
11
  isIntegrationCallerRequest,
17
- resolveBuilderBranchProjectId,
18
12
  resolveBuilderCredentialsDetailed,
19
13
  runBuilderAgent,
20
14
  } from "@agent-native/core/server";
@@ -36,9 +30,6 @@ import {
36
30
  } from "./workspace-resources-store.js";
37
31
 
38
32
  const SETTINGS_KEY = "dispatch-app-creation-settings";
39
- const BUILDER_BRANCH_PROJECT_SECRET_KEY = "BUILDER_BRANCH_PROJECT_ID";
40
- const BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION =
41
- "Builder project for cloud code-change branches (set in Dispatch)";
42
33
  const BUILDER_WORKSPACE_REPO_URL_ENV = "AGENT_NATIVE_WORKSPACE_REPO_URL";
43
34
  const DEFAULT_BUILDER_WORKSPACE_REPO_URL =
44
35
  "https://github.com/BuilderIO/builder-agent-native-workspace";
@@ -61,6 +52,7 @@ const AUTONOMOUS_WORKSPACE_APP_CREATION_CONTRACT = [
61
52
  "Autonomous Builder handoff contract:",
62
53
  "- This is a background implementation run launched by the turn-into-app workflow. Treat the source brief and latest user request as authorization to build the app now; do not return a proposal or wait for another turn.",
63
54
  "- Do not ask the user questions during the initial build and do not invoke a clarification, guided-question, or choice flow for a non-blocking decision.",
55
+ "- If the confirmed source brief describes a spreadsheet source-review or input/output confirmation surface, implement that review UI as part of the first-run app experience and seed it with the bounded candidates and mapping; keep the background build autonomous and do not send a question back from the Builder run.",
64
56
  "- When the source or a tool presents a recommended option, choose it and continue. When no recommendation is present, choose the most direct, conservative default supported by the source and normal Agent-Native conventions.",
65
57
  "- Resolve product, visual, copy, layout, route, data-model, dependency, and integration choices yourself. If an input is missing, use an empty state or clearly labeled representative sample so the workflow is demonstrable; never invent private facts or credentials.",
66
58
  "- Treat the source brief's unknowns and follow-up items as assumptions to record in the app README or a visible Assumptions / Review section, not as questions to send back to the user.",
@@ -74,6 +66,8 @@ const pendingBuilderProjectProvisioning = new Map<
74
66
  >();
75
67
 
76
68
  class AppCreationSettingsAuthorizationError extends Error {
69
+ statusCode = 403;
70
+
77
71
  constructor() {
78
72
  super(APP_CREATION_SETTINGS_AUTHORIZATION_MESSAGE);
79
73
  this.name = "AppCreationSettingsAuthorizationError";
@@ -254,16 +248,6 @@ function scopedSettingsKey(): string {
254
248
  return `${SETTINGS_KEY}:user:${currentOwnerEmail()}`;
255
249
  }
256
250
 
257
- function builderProjectSecretTarget(): {
258
- scope: Extract<SecretScope, "org" | "workspace">;
259
- scopeId: string;
260
- } | null {
261
- const orgId = currentOrgId();
262
- if (orgId) return { scope: "org", scopeId: orgId };
263
- const email = currentOwnerEmail();
264
- return email ? { scope: "workspace", scopeId: `solo:${email}` } : null;
265
- }
266
-
267
251
  function workspaceAppMetadataSettingsKey(): string {
268
252
  const orgId = currentOrgId();
269
253
  if (orgId) return `${WORKSPACE_APP_METADATA_SETTINGS_KEY}:org:${orgId}`;
@@ -1607,26 +1591,28 @@ function runScaffoldCli(input: {
1607
1591
 
1608
1592
  export async function getAppCreationSettings(): Promise<AppCreationSettings> {
1609
1593
  const envBuilderProjectId = getEnvBuilderProjectId();
1610
- const resolvedBuilderProjectId = await resolveBuilderBranchProjectId();
1611
1594
  const raw = await readSettingsRecord();
1595
+ const hasSavedBuilderProjectId = Object.prototype.hasOwnProperty.call(
1596
+ raw,
1597
+ "builderProjectId",
1598
+ );
1612
1599
  const savedBuilderProjectId =
1613
1600
  typeof raw?.builderProjectId === "string" && raw.builderProjectId.trim()
1614
1601
  ? raw.builderProjectId.trim()
1615
1602
  : null;
1616
- const builderProjectId = envBuilderProjectId || savedBuilderProjectId;
1617
1603
  const enableBuilder =
1618
1604
  process.env.ENABLE_BUILDER === "true" || process.env.ENABLE_BUILDER === "1";
1619
- const effectiveBuilderProjectId =
1620
- builderProjectId ||
1621
- resolvedBuilderProjectId ||
1622
- (enableBuilder ? getBuilderBranchProjectId() : null);
1605
+ const effectiveBuilderProjectId = hasSavedBuilderProjectId
1606
+ ? savedBuilderProjectId
1607
+ : envBuilderProjectId ||
1608
+ (enableBuilder ? getBuilderBranchProjectId() : null);
1623
1609
 
1624
1610
  return {
1625
1611
  builderProjectId: effectiveBuilderProjectId,
1626
- builderProjectIdSource: envBuilderProjectId
1627
- ? "env"
1628
- : savedBuilderProjectId
1629
- ? "dispatch"
1612
+ builderProjectIdSource: hasSavedBuilderProjectId
1613
+ ? "dispatch"
1614
+ : envBuilderProjectId
1615
+ ? "env"
1630
1616
  : effectiveBuilderProjectId
1631
1617
  ? "default"
1632
1618
  : "unset",
@@ -1647,20 +1633,10 @@ async function persistProvisionedBuilderProjectId(
1647
1633
  builderProjectId: string,
1648
1634
  ): Promise<void> {
1649
1635
  const raw = await readSettingsRecord();
1650
- const secretTarget = builderProjectSecretTarget();
1651
1636
 
1652
1637
  // This is an internal consequence of an authorized app-creation request,
1653
1638
  // not a user-controlled settings update. Persist the project before the
1654
1639
  // branch run so later members reuse the same Builder project.
1655
- if (secretTarget) {
1656
- await writeAppSecret({
1657
- key: BUILDER_BRANCH_PROJECT_SECRET_KEY,
1658
- scope: secretTarget.scope,
1659
- scopeId: secretTarget.scopeId,
1660
- value: builderProjectId,
1661
- description: BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION,
1662
- });
1663
- }
1664
1640
  await putSetting(scopedSettingsKey(), { ...raw, builderProjectId });
1665
1641
  await recordAudit({
1666
1642
  action: "settings.updated",
@@ -1712,28 +1688,6 @@ export async function setAppCreationSettings(input: {
1712
1688
  const builderProjectId = input.builderProjectId?.trim() || null;
1713
1689
  const raw = await readSettingsRecord();
1714
1690
 
1715
- // The credential store, not this settings row, is what
1716
- // `resolveBuilderBranchProjectId()` reads. Write it first: a saved setting
1717
- // whose secret never landed reports the project as configured while cloud
1718
- // code changes stay silently disabled.
1719
- const secretTarget = builderProjectSecretTarget();
1720
- if (secretTarget) {
1721
- const ref = {
1722
- key: BUILDER_BRANCH_PROJECT_SECRET_KEY,
1723
- scope: secretTarget.scope,
1724
- scopeId: secretTarget.scopeId,
1725
- };
1726
- if (builderProjectId) {
1727
- await writeAppSecret({
1728
- ...ref,
1729
- value: builderProjectId,
1730
- description: BUILDER_BRANCH_PROJECT_SECRET_DESCRIPTION,
1731
- });
1732
- } else {
1733
- await deleteAppSecret(ref);
1734
- }
1735
- }
1736
-
1737
1691
  await putSetting(scopedSettingsKey(), { ...raw, builderProjectId });
1738
1692
  await recordAudit({
1739
1693
  action: "settings.updated",
@@ -0,0 +1,8 @@
1
+ import type { AppRolesDescriptor } from "@agent-native/core/client/org";
2
+
3
+ export const dispatchAccessDescriptor = {
4
+ appId: "dispatch",
5
+ roles: ["admin"] as const,
6
+ roleLabels: { admin: "Dispatch admin" },
7
+ label: "Dispatch access",
8
+ } satisfies AppRolesDescriptor<"admin">;