@agent-native/dispatch 0.14.8 → 0.14.10

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 (37) hide show
  1. package/dist/actions/list-workspace-apps.js +1 -1
  2. package/dist/actions/list-workspace-apps.js.map +1 -1
  3. package/dist/actions/upsert-destination.d.ts +12 -12
  4. package/dist/components/layout/Layout.d.ts +2 -1
  5. package/dist/components/layout/Layout.d.ts.map +1 -1
  6. package/dist/components/layout/Layout.js +5 -5
  7. package/dist/components/layout/Layout.js.map +1 -1
  8. package/dist/server/lib/vault-boot-resync.d.ts +23 -0
  9. package/dist/server/lib/vault-boot-resync.d.ts.map +1 -0
  10. package/dist/server/lib/vault-boot-resync.js +44 -0
  11. package/dist/server/lib/vault-boot-resync.js.map +1 -0
  12. package/dist/server/lib/vault-store.d.ts +22 -0
  13. package/dist/server/lib/vault-store.d.ts.map +1 -1
  14. package/dist/server/lib/vault-store.js +49 -0
  15. package/dist/server/lib/vault-store.js.map +1 -1
  16. package/dist/server/plugins/agent-chat.js +2 -1
  17. package/dist/server/plugins/agent-chat.js.map +1 -1
  18. package/dist/server/plugins/db.d.ts +8 -1
  19. package/dist/server/plugins/db.d.ts.map +1 -1
  20. package/dist/server/plugins/db.js +13 -1
  21. package/dist/server/plugins/db.js.map +1 -1
  22. package/dist/server/plugins/integrations.d.ts.map +1 -1
  23. package/dist/server/plugins/integrations.js +2 -1
  24. package/dist/server/plugins/integrations.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/actions/index.spec.ts +12 -0
  27. package/src/actions/list-workspace-apps.ts +1 -1
  28. package/src/components/layout/Layout.spec.tsx +21 -0
  29. package/src/components/layout/Layout.tsx +14 -9
  30. package/src/components/workspace-app-card.spec.tsx +34 -3
  31. package/src/server/lib/vault-boot-resync.spec.ts +68 -0
  32. package/src/server/lib/vault-boot-resync.ts +50 -0
  33. package/src/server/lib/vault-store.spec.ts +132 -0
  34. package/src/server/lib/vault-store.ts +58 -0
  35. package/src/server/plugins/agent-chat.ts +2 -1
  36. package/src/server/plugins/db.ts +14 -1
  37. package/src/server/plugins/integrations.ts +2 -1
@@ -138,6 +138,27 @@ describe("Dispatch NavContent", () => {
138
138
  );
139
139
  });
140
140
 
141
+ it("keeps collapsed navigation compact and preserves section spacing", async () => {
142
+ await act(async () => {
143
+ root.render(
144
+ <MemoryRouter initialEntries={["/overview"]}>
145
+ <TooltipProvider>
146
+ <NavContent collapsed />
147
+ </TooltipProvider>
148
+ </MemoryRouter>,
149
+ );
150
+ });
151
+
152
+ const lists = [...container.querySelectorAll("nav > ul")];
153
+ expect(lists).toHaveLength(3);
154
+ expect(lists[0].className).toContain("gap-1");
155
+ expect(lists[1].className).toContain("mt-5");
156
+ expect(lists[1].className).toContain("gap-1");
157
+ expect(lists[2].className).toContain("mt-3");
158
+ expect(lists[2].className).toContain("gap-1");
159
+ expect(lists[0].querySelector("a")?.className).toContain("h-8 w-8");
160
+ });
161
+
141
162
  it("uses the quieter Analytics-style chat history and retains thread actions", async () => {
142
163
  await act(async () => {
143
164
  root.render(
@@ -648,7 +648,7 @@ export function NavContent({
648
648
  className={({ isActive }) => {
649
649
  const active = isActive || itemMatchesLocalPath;
650
650
  return cn(
651
- "flex h-10 w-10 items-center justify-center rounded-md text-sm",
651
+ "flex h-8 w-8 items-center justify-center rounded-md text-sm",
652
652
  active
653
653
  ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
654
654
  : "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
@@ -775,19 +775,21 @@ export function NavContent({
775
775
  <nav className={cn("py-3", collapsed ? "px-1" : "px-2")}>
776
776
  <ul
777
777
  className={cn(
778
- "space-y-0.5",
779
- collapsed && "flex flex-col items-center",
778
+ collapsed ? "flex flex-col items-center gap-1" : "space-y-0.5",
780
779
  )}
781
780
  >
782
781
  {primaryNavItems.map(renderNavItem)}
783
782
  </ul>
784
783
 
785
784
  {collapsed ? (
786
- <ul className="mt-2 flex flex-col items-center space-y-0.5">
787
- {[...operationsNavItems, ...ADVANCED_NAV_ITEMS].map(
788
- renderNavItem,
789
- )}
790
- </ul>
785
+ <>
786
+ <ul className="mt-5 flex flex-col items-center gap-1">
787
+ {operationsNavItems.map(renderNavItem)}
788
+ </ul>
789
+ <ul className="mt-3 flex flex-col items-center gap-1">
790
+ {ADVANCED_NAV_ITEMS.map(renderNavItem)}
791
+ </ul>
792
+ </>
791
793
  ) : (
792
794
  <div className="mt-5">
793
795
  <p className="px-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-sidebar-foreground/45">
@@ -838,9 +840,11 @@ export function NavContent({
838
840
  export function Layout({
839
841
  children,
840
842
  extensions,
843
+ agentPageHref,
841
844
  }: {
842
845
  children: ReactNode;
843
846
  extensions?: DispatchExtensionConfig;
847
+ agentPageHref?: string;
844
848
  }) {
845
849
  const t = useT();
846
850
  const location = useLocation();
@@ -926,6 +930,7 @@ export function Layout({
926
930
  <AgentSidebar
927
931
  position="right"
928
932
  defaultOpen={false}
933
+ agentPageHref={agentPageHref}
929
934
  chatViewTransition
930
935
  storageKey="dispatch"
931
936
  openOnChatRunning={chatHomeHandoffActive}
@@ -944,7 +949,7 @@ export function Layout({
944
949
  data-collapsed={sidebarCollapsed ? "true" : "false"}
945
950
  className={cn(
946
951
  "agent-layout-left-drawer hidden shrink-0 flex-col border-e bg-sidebar text-sidebar-foreground transition-[width] duration-200 ease-out lg:flex",
947
- sidebarCollapsed ? "w-12" : "w-64",
952
+ sidebarCollapsed ? "w-14" : "w-56",
948
953
  )}
949
954
  >
950
955
  <NavContent
@@ -51,9 +51,13 @@ describe("WorkspaceAppCard", () => {
51
51
  );
52
52
  });
53
53
 
54
- expect(
55
- container.querySelector('a[aria-label="Open Analytics"]')?.className,
56
- ).toContain("focus-visible:ring-2");
54
+ const appLink = container.querySelector<HTMLAnchorElement>(
55
+ 'a[aria-label="Open Analytics"]',
56
+ );
57
+ expect(appLink?.getAttribute("href")).toBe("/analytics");
58
+ expect(appLink?.getAttribute("target")).toBeNull();
59
+ expect(appLink?.getAttribute("rel")).toBeNull();
60
+ expect(appLink?.className).toContain("focus-visible:ring-2");
57
61
 
58
62
  const actions = [
59
63
  container.querySelector<HTMLButtonElement>(
@@ -77,4 +81,31 @@ describe("WorkspaceAppCard", () => {
77
81
  expect(action?.className).not.toContain("opacity-0");
78
82
  }
79
83
  });
84
+
85
+ it("opens pending Builder apps in a new tab", async () => {
86
+ await act(async () => {
87
+ root.render(
88
+ <TooltipProvider>
89
+ <WorkspaceAppCard
90
+ app={{
91
+ id: "new-app",
92
+ name: "New app",
93
+ path: "/new-app",
94
+ builderUrl: "https://builder.example.com/projects/new-app",
95
+ status: "pending",
96
+ }}
97
+ />
98
+ </TooltipProvider>,
99
+ );
100
+ });
101
+
102
+ const appLink = container.querySelector<HTMLAnchorElement>(
103
+ 'a[aria-label="Open New app"]',
104
+ );
105
+ expect(appLink?.getAttribute("href")).toBe(
106
+ "https://builder.example.com/projects/new-app",
107
+ );
108
+ expect(appLink?.getAttribute("target")).toBe("_blank");
109
+ expect(appLink?.getAttribute("rel")).toBe("noreferrer");
110
+ });
80
111
  });
@@ -0,0 +1,68 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ resyncAllVaultSecretsToCredentialStore: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("./vault-store.js", () => ({
8
+ resyncAllVaultSecretsToCredentialStore:
9
+ mocks.resyncAllVaultSecretsToCredentialStore,
10
+ }));
11
+
12
+ import {
13
+ __resetVaultBootResyncGuardForTests,
14
+ scheduleVaultBootResync,
15
+ } from "./vault-boot-resync.js";
16
+
17
+ describe("scheduleVaultBootResync", () => {
18
+ beforeEach(() => {
19
+ vi.useFakeTimers();
20
+ __resetVaultBootResyncGuardForTests();
21
+ mocks.resyncAllVaultSecretsToCredentialStore.mockResolvedValue({
22
+ groups: 0,
23
+ failedGroups: 0,
24
+ syncedKeys: 0,
25
+ });
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.useRealTimers();
30
+ mocks.resyncAllVaultSecretsToCredentialStore.mockReset();
31
+ });
32
+
33
+ it("does not run the resync synchronously — it waits for the boot delay", () => {
34
+ scheduleVaultBootResync();
35
+ expect(mocks.resyncAllVaultSecretsToCredentialStore).not.toHaveBeenCalled();
36
+ });
37
+
38
+ it("runs the resync exactly once after the boot delay elapses", async () => {
39
+ scheduleVaultBootResync();
40
+ await vi.runAllTimersAsync();
41
+ expect(mocks.resyncAllVaultSecretsToCredentialStore).toHaveBeenCalledTimes(
42
+ 1,
43
+ );
44
+ });
45
+
46
+ it("only schedules one timer per process even if called repeatedly", async () => {
47
+ scheduleVaultBootResync();
48
+ scheduleVaultBootResync();
49
+ scheduleVaultBootResync();
50
+ await vi.runAllTimersAsync();
51
+ expect(mocks.resyncAllVaultSecretsToCredentialStore).toHaveBeenCalledTimes(
52
+ 1,
53
+ );
54
+ });
55
+
56
+ it("swallows a resync failure instead of throwing", async () => {
57
+ mocks.resyncAllVaultSecretsToCredentialStore.mockRejectedValue(
58
+ new Error("boom"),
59
+ );
60
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
61
+
62
+ scheduleVaultBootResync();
63
+ await vi.runAllTimersAsync();
64
+
65
+ expect(warnSpy).toHaveBeenCalled();
66
+ warnSpy.mockRestore();
67
+ });
68
+ });
@@ -0,0 +1,50 @@
1
+ import { resyncAllVaultSecretsToCredentialStore } from "./vault-store.js";
2
+
3
+ /**
4
+ * Give the process a moment to finish booting (migrations, DB pool warmup,
5
+ * etc.) before touching every vault secret. This is a self-heal, not a
6
+ * hot-path dependency, so there is no urgency to run it immediately.
7
+ */
8
+ const BOOT_RESYNC_DELAY_MS = 8000;
9
+
10
+ /** Guards against scheduling more than one resync per process — see
11
+ * `scheduleVaultBootResync` below. */
12
+ let scheduled = false;
13
+
14
+ /**
15
+ * Schedule a one-time, best-effort re-sync of every vault secret (across
16
+ * every tenant) into the shared credential store, a few seconds after boot.
17
+ *
18
+ * Why this exists: `syncSecretsToCredentialStore` only runs when a vault
19
+ * secret is created or updated, so it only re-encrypts rows a user happens
20
+ * to touch. When the shared `app_secrets` encryption format changes under
21
+ * it — e.g. the `shared_encrypted_value` dual-write, or hosted workspaces
22
+ * deriving shared key material from `A2A_SECRET` — existing rows are stuck
23
+ * on the old format until someone manually re-saves each vault secret,
24
+ * which breaks sibling apps reading them. Re-running the sync for every row
25
+ * at boot self-heals that without any manual step.
26
+ *
27
+ * Fires at most once per process (`scheduled` guard) so calling this again
28
+ * — e.g. if the owning plugin module re-runs under dev's `*-plugin.ts` HMR
29
+ * reload — doesn't stack duplicate timers. Runs fully non-blocking: it
30
+ * never delays startup, and any failure is caught and logged rather than
31
+ * thrown, since a stale-encryption row is a degraded state, not a crash.
32
+ */
33
+ export function scheduleVaultBootResync(): void {
34
+ if (scheduled) return;
35
+ scheduled = true;
36
+
37
+ setTimeout(() => {
38
+ void resyncAllVaultSecretsToCredentialStore().catch((error) => {
39
+ console.warn(
40
+ "[dispatch] vault boot resync failed to run",
41
+ error instanceof Error ? error.message : error,
42
+ );
43
+ });
44
+ }, BOOT_RESYNC_DELAY_MS);
45
+ }
46
+
47
+ /** Test-only: reset the once-per-process guard between spec runs. */
48
+ export function __resetVaultBootResyncGuardForTests(): void {
49
+ scheduled = false;
50
+ }
@@ -4,12 +4,14 @@ const mocks = vi.hoisted(() => ({
4
4
  deleteAppSecret: vi.fn(),
5
5
  getDb: vi.fn(),
6
6
  listAppSecretsForScope: vi.fn(),
7
+ readAppSecret: vi.fn(),
7
8
  writeAppSecret: vi.fn(),
8
9
  }));
9
10
 
10
11
  vi.mock("@agent-native/core/secrets", () => ({
11
12
  deleteAppSecret: mocks.deleteAppSecret,
12
13
  listAppSecretsForScope: mocks.listAppSecretsForScope,
14
+ readAppSecret: mocks.readAppSecret,
13
15
  writeAppSecret: mocks.writeAppSecret,
14
16
  }));
15
17
 
@@ -21,10 +23,13 @@ vi.mock("../../db/index.js", async (importOriginal) => {
21
23
  };
22
24
  });
23
25
 
26
+ import { readAppSecret } from "@agent-native/core/secrets";
27
+
24
28
  import {
25
29
  cleanupSyncedCredentialKeysIfUnused,
26
30
  credentialStoreScopeForVaultCtx,
27
31
  isTrustedEnvVarSyncAgentUrl,
32
+ resyncAllVaultSecretsToCredentialStore,
28
33
  syncSecretsToCredentialStore,
29
34
  } from "./vault-store.js";
30
35
 
@@ -187,3 +192,130 @@ describe("cleanupSyncedCredentialKeysIfUnused", () => {
187
192
  });
188
193
  });
189
194
  });
195
+
196
+ describe("resyncAllVaultSecretsToCredentialStore", () => {
197
+ function mockVaultSecretsRows(rows: Array<Record<string, unknown>>) {
198
+ mocks.getDb.mockReturnValue({
199
+ select: () => ({
200
+ from: () => Promise.resolve(rows),
201
+ }),
202
+ });
203
+ }
204
+
205
+ /** In-memory stand-in for the shared credential store, keyed the same way
206
+ * the real app_secrets table is: scope + scopeId + key. */
207
+ function fakeCredentialStore() {
208
+ const store = new Map<string, string>();
209
+ mocks.writeAppSecret.mockImplementation(async (args: any) => {
210
+ store.set(`${args.scope}:${args.scopeId}:${args.key}`, args.value);
211
+ return "app-secret-id";
212
+ });
213
+ mocks.readAppSecret.mockImplementation(async (ref: any) => {
214
+ const value = store.get(`${ref.scope}:${ref.scopeId}:${ref.key}`);
215
+ return value === undefined ? null : { value, updatedAt: Date.now() };
216
+ });
217
+ return store;
218
+ }
219
+
220
+ afterEach(() => {
221
+ mocks.writeAppSecret.mockReset();
222
+ mocks.readAppSecret.mockReset();
223
+ });
224
+
225
+ it("syncs vault secrets from different tenants into their own credential-store scopes", async () => {
226
+ fakeCredentialStore();
227
+ mockVaultSecretsRows([
228
+ {
229
+ id: "secret_org",
230
+ ownerEmail: "admin@example.test",
231
+ orgId: "org_123",
232
+ name: "OpenAI API Key",
233
+ credentialKey: "OPENAI_API_KEY",
234
+ value: "sk-org-key",
235
+ },
236
+ {
237
+ id: "secret_solo",
238
+ ownerEmail: "owner@example.test",
239
+ orgId: null,
240
+ name: "Personal API Key",
241
+ credentialKey: "PERSONAL_API_KEY",
242
+ value: "sk-personal-key",
243
+ },
244
+ ]);
245
+
246
+ const result = await resyncAllVaultSecretsToCredentialStore();
247
+
248
+ expect(result).toEqual({ groups: 2, failedGroups: 0, syncedKeys: 2 });
249
+
250
+ const orgScope = credentialStoreScopeForVaultCtx({
251
+ ownerEmail: "admin@example.test",
252
+ orgId: "org_123",
253
+ });
254
+ const soloScope = credentialStoreScopeForVaultCtx({
255
+ ownerEmail: "owner@example.test",
256
+ orgId: null,
257
+ });
258
+
259
+ await expect(
260
+ readAppSecret({ key: "OPENAI_API_KEY", ...orgScope }),
261
+ ).resolves.toMatchObject({ value: "sk-org-key" });
262
+ await expect(
263
+ readAppSecret({ key: "PERSONAL_API_KEY", ...soloScope }),
264
+ ).resolves.toMatchObject({ value: "sk-personal-key" });
265
+ });
266
+
267
+ it("logs and skips a group that fails without blocking the other groups", async () => {
268
+ const store = fakeCredentialStore();
269
+ const writeImpl = mocks.writeAppSecret.getMockImplementation();
270
+ mocks.writeAppSecret.mockImplementation(async (args: any) => {
271
+ if (args.key === "BROKEN_KEY") {
272
+ throw new Error("simulated credential-store write failure");
273
+ }
274
+ return writeImpl!(args);
275
+ });
276
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
277
+
278
+ mockVaultSecretsRows([
279
+ {
280
+ id: "secret_broken",
281
+ ownerEmail: "admin@broken.test",
282
+ orgId: "org_broken",
283
+ name: "Broken Key",
284
+ credentialKey: "BROKEN_KEY",
285
+ value: "sk-broken-value",
286
+ },
287
+ {
288
+ id: "secret_solo",
289
+ ownerEmail: "owner@example.test",
290
+ orgId: null,
291
+ name: "Personal API Key",
292
+ credentialKey: "PERSONAL_API_KEY",
293
+ value: "sk-personal-key",
294
+ },
295
+ ]);
296
+
297
+ const result = await resyncAllVaultSecretsToCredentialStore();
298
+
299
+ expect(result).toEqual({ groups: 2, failedGroups: 1, syncedKeys: 1 });
300
+
301
+ // The failed org's key never landed in the credential store.
302
+ expect(store.get("org:org_broken:BROKEN_KEY")).toBeUndefined();
303
+
304
+ // The other tenant's group still synced successfully.
305
+ const soloScope = credentialStoreScopeForVaultCtx({
306
+ ownerEmail: "owner@example.test",
307
+ orgId: null,
308
+ });
309
+ await expect(
310
+ readAppSecret({ key: "PERSONAL_API_KEY", ...soloScope }),
311
+ ).resolves.toMatchObject({ value: "sk-personal-key" });
312
+
313
+ // Exactly one warning, naming the key but never the plaintext value.
314
+ expect(warnSpy).toHaveBeenCalledTimes(1);
315
+ const [warnMessage] = warnSpy.mock.calls[0]!;
316
+ expect(String(warnMessage)).toContain("BROKEN_KEY");
317
+ expect(String(warnMessage)).not.toContain("sk-broken-value");
318
+
319
+ warnSpy.mockRestore();
320
+ });
321
+ });
@@ -763,6 +763,64 @@ export async function syncSecretsToCredentialStore(
763
763
  return { ...target, keys: syncedKeys };
764
764
  }
765
765
 
766
+ /**
767
+ * Re-sync every vault secret across every tenant into the shared credential
768
+ * store, regardless of which request/ctx is currently active.
769
+ *
770
+ * `syncSecretsToCredentialStore` normally only runs on `createSecret` /
771
+ * `updateSecret`, so it only re-encrypts the rows a user happens to touch.
772
+ * When the shared `app_secrets` encryption format changes underneath it
773
+ * (e.g. a new dual-write format, or a change to how key material is
774
+ * derived), existing rows are stuck on the old format until someone
775
+ * manually re-saves each vault secret. This walks every `vault_secrets`
776
+ * row directly — bypassing the ctx-scoped `listSecrets()` — groups them by
777
+ * their (orgId, ownerEmail) tenant, and re-runs the sync per group so every
778
+ * row regains fresh ciphertext.
779
+ *
780
+ * A failure syncing one tenant's group is caught and logged (key NAMES
781
+ * only, never values) so it can't block the rest of the resync.
782
+ */
783
+ export async function resyncAllVaultSecretsToCredentialStore(): Promise<{
784
+ groups: number;
785
+ failedGroups: number;
786
+ syncedKeys: number;
787
+ }> {
788
+ const db = getDb();
789
+ const rows = await db.select().from(schema.vaultSecrets);
790
+
791
+ const groups = new Map<string, { ctx: VaultCtx; rows: VaultSecretRow[] }>();
792
+ for (const row of rows) {
793
+ if (!row.credentialKey || !row.value) continue;
794
+ const ctx: VaultCtx = { ownerEmail: row.ownerEmail, orgId: row.orgId };
795
+ const groupKey = `${ctx.orgId ?? ""}${ctx.ownerEmail}`;
796
+ const group = groups.get(groupKey);
797
+ if (group) {
798
+ group.rows.push(row);
799
+ } else {
800
+ groups.set(groupKey, { ctx, rows: [row] });
801
+ }
802
+ }
803
+
804
+ let failedGroups = 0;
805
+ let syncedKeys = 0;
806
+
807
+ for (const { ctx, rows: groupRows } of groups.values()) {
808
+ try {
809
+ const result = await syncSecretsToCredentialStore(groupRows, ctx);
810
+ syncedKeys += result.keys.length;
811
+ } catch (error) {
812
+ failedGroups++;
813
+ const keyNames = groupRows.map((row) => row.credentialKey).join(", ");
814
+ console.warn(
815
+ `[dispatch] vault boot resync failed for org=${ctx.orgId ?? "(solo)"} owner=${ctx.ownerEmail}; affected keys: ${keyNames}`,
816
+ error instanceof Error ? error.message : error,
817
+ );
818
+ }
819
+ }
820
+
821
+ return { groups: groups.size, failedGroups, syncedKeys };
822
+ }
823
+
766
824
  export async function cleanupSyncedCredentialKeysIfUnused(
767
825
  ctx: VaultCtx,
768
826
  candidateKeys?: string[],
@@ -55,7 +55,8 @@ Use the standard workspace primitives:
55
55
  - Use recurring jobs for scheduled behavior.
56
56
  - Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.
57
57
  - You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
58
- - When answering whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
58
+ - Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
59
+ - When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
59
60
  - When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.
60
61
  - If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
61
62
  - Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
@@ -1,7 +1,20 @@
1
1
  import { runMigrations } from "@agent-native/core/db";
2
2
 
3
3
  import { dispatchMigrations } from "../../db/migrations.js";
4
+ import { scheduleVaultBootResync } from "../lib/vault-boot-resync.js";
4
5
 
5
- export default runMigrations(dispatchMigrations, {
6
+ const runDispatchMigrations = runMigrations(dispatchMigrations, {
6
7
  table: "dispatch_migrations",
7
8
  });
9
+
10
+ /**
11
+ * Run dispatch's own migrations first (this is what guarantees
12
+ * `vault_secrets` exists), then kick off the vault boot resync. The resync
13
+ * itself waits several more seconds before touching the DB — see
14
+ * vault-boot-resync.ts — so this ordering is a belt-and-suspenders
15
+ * guarantee, not a hard dependency.
16
+ */
17
+ export default async (nitroApp: any) => {
18
+ await runDispatchMigrations(nitroApp);
19
+ scheduleVaultBootResync();
20
+ };
@@ -24,7 +24,8 @@ Default posture:
24
24
  - Treat Slack, Telegram, and email as shared entrypoints into the workspace.
25
25
  - Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).
26
26
  - Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.
27
- - When asked whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
27
+ - Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
28
+ - When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
28
29
  - Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
29
30
  - Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
30
31
  - Keep durable memory and operating instructions in resources rather than ephemeral chat.