@agent-native/dispatch 0.6.1 → 0.7.0

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 (146) hide show
  1. package/README.md +1 -1
  2. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  3. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  4. package/dist/actions/create-pylon-ticket.js +94 -0
  5. package/dist/actions/create-pylon-ticket.js.map +1 -0
  6. package/dist/actions/create-vault-grant.js +1 -1
  7. package/dist/actions/create-vault-grant.js.map +1 -1
  8. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  9. package/dist/actions/create-vault-secret.js +4 -3
  10. package/dist/actions/create-vault-secret.js.map +1 -1
  11. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  12. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  13. package/dist/actions/get-vault-access-settings.js +10 -0
  14. package/dist/actions/get-vault-access-settings.js.map +1 -0
  15. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  16. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  17. package/dist/actions/index.d.ts.map +1 -1
  18. package/dist/actions/index.js +8 -0
  19. package/dist/actions/index.js.map +1 -1
  20. package/dist/actions/list-integrations-catalog.js +1 -1
  21. package/dist/actions/list-integrations-catalog.js.map +1 -1
  22. package/dist/actions/list-vault-grants.js +1 -1
  23. package/dist/actions/list-vault-grants.js.map +1 -1
  24. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  25. package/dist/actions/list-workspace-apps.js +5 -1
  26. package/dist/actions/list-workspace-apps.js.map +1 -1
  27. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  28. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  29. package/dist/actions/set-vault-access-settings.js +13 -0
  30. package/dist/actions/set-vault-access-settings.js.map +1 -0
  31. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  32. package/dist/actions/start-workspace-app-creation.js +6 -0
  33. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  34. package/dist/actions/sync-vault-to-app.js +1 -1
  35. package/dist/actions/sync-vault-to-app.js.map +1 -1
  36. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  37. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  38. package/dist/actions/update-workspace-app-metadata.js +30 -0
  39. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  40. package/dist/actions/view-screen.d.ts.map +1 -1
  41. package/dist/actions/view-screen.js +4 -2
  42. package/dist/actions/view-screen.js.map +1 -1
  43. package/dist/components/app-keys-popover.js +16 -5
  44. package/dist/components/app-keys-popover.js.map +1 -1
  45. package/dist/components/create-app-popover.d.ts.map +1 -1
  46. package/dist/components/create-app-popover.js +38 -14
  47. package/dist/components/create-app-popover.js.map +1 -1
  48. package/dist/components/dispatch-shell.d.ts +4 -4
  49. package/dist/components/dispatch-shell.d.ts.map +1 -1
  50. package/dist/components/dispatch-shell.js +6 -6
  51. package/dist/components/dispatch-shell.js.map +1 -1
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +10 -3
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +2 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/workspace-app-card.d.ts.map +1 -1
  59. package/dist/components/workspace-app-card.js +41 -2
  60. package/dist/components/workspace-app-card.js.map +1 -1
  61. package/dist/hooks/use-navigation-state.js +12 -5
  62. package/dist/hooks/use-navigation-state.js.map +1 -1
  63. package/dist/lib/catch-all-target.d.ts +2 -0
  64. package/dist/lib/catch-all-target.d.ts.map +1 -0
  65. package/dist/lib/catch-all-target.js +95 -0
  66. package/dist/lib/catch-all-target.js.map +1 -0
  67. package/dist/lib/workspace-apps.d.ts +9 -0
  68. package/dist/lib/workspace-apps.d.ts.map +1 -1
  69. package/dist/lib/workspace-apps.js.map +1 -1
  70. package/dist/routes/pages/$appId.d.ts +2 -2
  71. package/dist/routes/pages/$appId.d.ts.map +1 -1
  72. package/dist/routes/pages/$appId.js +17 -8
  73. package/dist/routes/pages/$appId.js.map +1 -1
  74. package/dist/routes/pages/integrations.d.ts.map +1 -1
  75. package/dist/routes/pages/integrations.js +20 -15
  76. package/dist/routes/pages/integrations.js.map +1 -1
  77. package/dist/routes/pages/new-app.js +1 -1
  78. package/dist/routes/pages/new-app.js.map +1 -1
  79. package/dist/routes/pages/overview.d.ts.map +1 -1
  80. package/dist/routes/pages/overview.js +5 -1
  81. package/dist/routes/pages/overview.js.map +1 -1
  82. package/dist/routes/pages/vault.d.ts.map +1 -1
  83. package/dist/routes/pages/vault.js +23 -5
  84. package/dist/routes/pages/vault.js.map +1 -1
  85. package/dist/server/lib/app-creation-store.d.ts +13 -0
  86. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  87. package/dist/server/lib/app-creation-store.js +295 -9
  88. package/dist/server/lib/app-creation-store.js.map +1 -1
  89. package/dist/server/lib/env-config.d.ts.map +1 -1
  90. package/dist/server/lib/env-config.js +5 -0
  91. package/dist/server/lib/env-config.js.map +1 -1
  92. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  93. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  94. package/dist/server/lib/onboarding-steps.js +47 -0
  95. package/dist/server/lib/onboarding-steps.js.map +1 -0
  96. package/dist/server/lib/vault-store.d.ts +55 -0
  97. package/dist/server/lib/vault-store.d.ts.map +1 -1
  98. package/dist/server/lib/vault-store.js +210 -41
  99. package/dist/server/lib/vault-store.js.map +1 -1
  100. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  101. package/dist/server/plugins/agent-chat.js +2 -1
  102. package/dist/server/plugins/agent-chat.js.map +1 -1
  103. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  104. package/dist/server/plugins/core-routes.js +4 -0
  105. package/dist/server/plugins/core-routes.js.map +1 -1
  106. package/dist/server/plugins/integrations.js +2 -2
  107. package/dist/server/plugins/integrations.js.map +1 -1
  108. package/package.json +13 -11
  109. package/src/actions/create-pylon-ticket.ts +109 -0
  110. package/src/actions/create-vault-grant.ts +1 -1
  111. package/src/actions/create-vault-secret.ts +4 -3
  112. package/src/actions/get-vault-access-settings.ts +11 -0
  113. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  114. package/src/actions/index.ts +8 -0
  115. package/src/actions/list-integrations-catalog.ts +1 -1
  116. package/src/actions/list-vault-grants.ts +1 -1
  117. package/src/actions/list-workspace-apps.ts +5 -1
  118. package/src/actions/set-vault-access-settings.ts +16 -0
  119. package/src/actions/start-workspace-app-creation.ts +8 -0
  120. package/src/actions/sync-vault-to-app.ts +1 -1
  121. package/src/actions/update-workspace-app-metadata.ts +32 -0
  122. package/src/actions/view-screen.ts +4 -1
  123. package/src/components/app-keys-popover.tsx +23 -7
  124. package/src/components/create-app-popover.tsx +47 -14
  125. package/src/components/dispatch-shell.tsx +16 -15
  126. package/src/components/layout/Layout.tsx +11 -5
  127. package/src/components/messaging-setup-panel.tsx +54 -39
  128. package/src/components/workspace-app-card.tsx +102 -0
  129. package/src/hooks/use-navigation-state.ts +10 -4
  130. package/src/lib/catch-all-target.spec.ts +218 -0
  131. package/src/lib/catch-all-target.ts +99 -0
  132. package/src/lib/workspace-apps.ts +9 -0
  133. package/src/routes/pages/$appId.tsx +21 -8
  134. package/src/routes/pages/integrations.tsx +57 -18
  135. package/src/routes/pages/new-app.tsx +1 -1
  136. package/src/routes/pages/overview.tsx +11 -3
  137. package/src/routes/pages/vault.tsx +76 -9
  138. package/src/server/lib/app-creation-store.spec.ts +61 -2
  139. package/src/server/lib/app-creation-store.ts +386 -11
  140. package/src/server/lib/env-config.ts +5 -0
  141. package/src/server/lib/onboarding-steps.ts +49 -0
  142. package/src/server/lib/vault-store.spec.ts +69 -0
  143. package/src/server/lib/vault-store.ts +266 -49
  144. package/src/server/plugins/agent-chat.ts +2 -1
  145. package/src/server/plugins/core-routes.ts +5 -0
  146. package/src/server/plugins/integrations.ts +2 -2
@@ -0,0 +1,69 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ writeAppSecret: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@agent-native/core/secrets", () => ({
8
+ writeAppSecret: mocks.writeAppSecret,
9
+ }));
10
+
11
+ import {
12
+ credentialStoreScopeForVaultCtx,
13
+ syncSecretsToCredentialStore,
14
+ } from "./vault-store.js";
15
+
16
+ afterEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ describe("credentialStoreScopeForVaultCtx", () => {
21
+ it("uses org scope when vault sync runs inside an org", () => {
22
+ expect(
23
+ credentialStoreScopeForVaultCtx({
24
+ ownerEmail: "admin@example.test",
25
+ orgId: "org_123",
26
+ }),
27
+ ).toEqual({ scope: "org", scopeId: "org_123" });
28
+ });
29
+
30
+ it("uses workspace solo scope when no org is active", () => {
31
+ expect(
32
+ credentialStoreScopeForVaultCtx({
33
+ ownerEmail: "owner@example.test",
34
+ orgId: null,
35
+ }),
36
+ ).toEqual({
37
+ scope: "workspace",
38
+ scopeId: "solo:owner@example.test",
39
+ });
40
+ });
41
+ });
42
+
43
+ describe("syncSecretsToCredentialStore", () => {
44
+ it("writes vault secrets into app_secrets without returning values", async () => {
45
+ const result = await syncSecretsToCredentialStore(
46
+ [
47
+ {
48
+ name: "OpenAI API Key",
49
+ credentialKey: "OPENAI_API_KEY",
50
+ value: "sk-test-key",
51
+ } as any,
52
+ ],
53
+ { ownerEmail: "admin@example.test", orgId: "org_123" },
54
+ );
55
+
56
+ expect(mocks.writeAppSecret).toHaveBeenCalledWith({
57
+ key: "OPENAI_API_KEY",
58
+ value: "sk-test-key",
59
+ scope: "org",
60
+ scopeId: "org_123",
61
+ description: "Synced from Dispatch vault: OpenAI API Key",
62
+ });
63
+ expect(result).toEqual({
64
+ scope: "org",
65
+ scopeId: "org_123",
66
+ keys: ["OPENAI_API_KEY"],
67
+ });
68
+ });
69
+ });
@@ -1,6 +1,13 @@
1
1
  import crypto from "node:crypto";
2
2
  import { and, desc, eq, isNull, or } from "drizzle-orm";
3
3
  import { discoverAgents } from "@agent-native/core/server/agent-discovery";
4
+ import { writeAppSecret, type SecretScope } from "@agent-native/core/secrets";
5
+ import {
6
+ getOrgSetting,
7
+ getUserSetting,
8
+ putOrgSetting,
9
+ putUserSetting,
10
+ } from "@agent-native/core/settings";
4
11
  import { getDb, schema } from "../../db/index.js";
5
12
  import {
6
13
  currentOwnerEmail,
@@ -8,6 +15,16 @@ import {
8
15
  recordAudit,
9
16
  } from "./dispatch-store.js";
10
17
 
18
+ const VAULT_ACCESS_SETTINGS_KEY = "dispatch-vault-access-settings";
19
+
20
+ export type VaultAccessMode = "all-apps" | "manual";
21
+
22
+ export interface VaultAccessSettings {
23
+ mode: VaultAccessMode;
24
+ scope: "org" | "user";
25
+ scopeId: string;
26
+ }
27
+
11
28
  /**
12
29
  * Caller-supplied access context for vault operations.
13
30
  *
@@ -40,10 +57,10 @@ function ctxScope<T extends { ownerEmail: any; orgId: any }>(
40
57
  table: T,
41
58
  ctx: VaultCtx,
42
59
  ) {
43
- return or(
44
- eq(table.ownerEmail, ctx.ownerEmail),
45
- ctx.orgId ? eq(table.orgId, ctx.orgId) : isNull(table.orgId),
46
- );
60
+ if (!ctx.orgId) {
61
+ return and(eq(table.ownerEmail, ctx.ownerEmail), isNull(table.orgId));
62
+ }
63
+ return or(eq(table.ownerEmail, ctx.ownerEmail), eq(table.orgId, ctx.orgId));
47
64
  }
48
65
 
49
66
  /** Build a ctx that scopes to a specific row's owner/org (used when a
@@ -68,12 +85,57 @@ function safeJson(value: unknown) {
68
85
  return JSON.stringify(value ?? null);
69
86
  }
70
87
 
71
- function orgFilter<T extends { ownerEmail: any; orgId: any }>(table: T) {
88
+ function scopedFilter<T extends { ownerEmail: any; orgId: any }>(table: T) {
89
+ return ctxScope(table, requireVaultCtx());
90
+ }
91
+
92
+ function normalizeCredentialKey(value: string) {
93
+ return value.trim();
94
+ }
95
+
96
+ function vaultAccessScope() {
72
97
  const orgId = currentOrgId();
73
- return and(
74
- eq(table.ownerEmail, currentOwnerEmail()),
75
- orgId ? eq(table.orgId, orgId) : isNull(table.orgId),
76
- );
98
+ if (orgId) return { scope: "org" as const, scopeId: orgId };
99
+ return { scope: "user" as const, scopeId: currentOwnerEmail() };
100
+ }
101
+
102
+ function parseVaultAccessMode(value: unknown): VaultAccessMode {
103
+ return value === "manual" ? "manual" : "all-apps";
104
+ }
105
+
106
+ export async function getVaultAccessSettings(): Promise<VaultAccessSettings> {
107
+ const scope = vaultAccessScope();
108
+ const raw =
109
+ scope.scope === "org"
110
+ ? await getOrgSetting(scope.scopeId, VAULT_ACCESS_SETTINGS_KEY)
111
+ : await getUserSetting(scope.scopeId, VAULT_ACCESS_SETTINGS_KEY);
112
+ return {
113
+ ...scope,
114
+ mode: parseVaultAccessMode(raw?.mode),
115
+ };
116
+ }
117
+
118
+ export async function setVaultAccessSettings(input: {
119
+ mode: VaultAccessMode;
120
+ }): Promise<VaultAccessSettings> {
121
+ const scope = vaultAccessScope();
122
+ const next = { mode: parseVaultAccessMode(input.mode) };
123
+ if (scope.scope === "org") {
124
+ await putOrgSetting(scope.scopeId, VAULT_ACCESS_SETTINGS_KEY, next);
125
+ } else {
126
+ await putUserSetting(scope.scopeId, VAULT_ACCESS_SETTINGS_KEY, next);
127
+ }
128
+ await recordAudit({
129
+ action: "vault.access-settings.updated",
130
+ targetType: "vault-settings",
131
+ targetId: VAULT_ACCESS_SETTINGS_KEY,
132
+ summary:
133
+ next.mode === "all-apps"
134
+ ? "Set vault access to all workspace apps"
135
+ : "Set vault access to manual per-app grants",
136
+ metadata: next,
137
+ });
138
+ return getVaultAccessSettings();
77
139
  }
78
140
 
79
141
  // ─── Vault Audit ──────────────────────────────────────────────────
@@ -106,7 +168,7 @@ export async function listVaultAudit(limit = 50) {
106
168
  return db
107
169
  .select()
108
170
  .from(schema.vaultAuditLog)
109
- .where(orgFilter(schema.vaultAuditLog))
171
+ .where(scopedFilter(schema.vaultAuditLog))
110
172
  .orderBy(desc(schema.vaultAuditLog.createdAt))
111
173
  .limit(limit);
112
174
  }
@@ -118,7 +180,7 @@ export async function listSecrets() {
118
180
  return db
119
181
  .select()
120
182
  .from(schema.vaultSecrets)
121
- .where(orgFilter(schema.vaultSecrets))
183
+ .where(scopedFilter(schema.vaultSecrets))
122
184
  .orderBy(desc(schema.vaultSecrets.updatedAt));
123
185
  }
124
186
 
@@ -149,6 +211,55 @@ export async function createSecret(
149
211
  ) {
150
212
  const db = getDb();
151
213
  const timestamp = now();
214
+ const credentialKey = normalizeCredentialKey(input.credentialKey);
215
+ if (!credentialKey) throw new Error("Credential key is required");
216
+ const existing = await db
217
+ .select()
218
+ .from(schema.vaultSecrets)
219
+ .where(
220
+ and(
221
+ eq(schema.vaultSecrets.credentialKey, credentialKey),
222
+ ctxScope(schema.vaultSecrets, ctx),
223
+ ),
224
+ )
225
+ .orderBy(desc(schema.vaultSecrets.updatedAt))
226
+ .limit(1);
227
+
228
+ if (existing[0]) {
229
+ await db
230
+ .update(schema.vaultSecrets)
231
+ .set({
232
+ name: input.name,
233
+ credentialKey,
234
+ value: input.value,
235
+ provider: input.provider || null,
236
+ description: input.description || null,
237
+ updatedAt: timestamp,
238
+ })
239
+ .where(
240
+ and(
241
+ eq(schema.vaultSecrets.id, existing[0].id),
242
+ ctxScope(schema.vaultSecrets, ctx),
243
+ ),
244
+ );
245
+
246
+ await recordVaultAudit({
247
+ action: "secret.updated",
248
+ secretId: existing[0].id,
249
+ summary: `Updated secret "${input.name}" (${credentialKey})`,
250
+ metadata: { credentialKey, provider: input.provider },
251
+ });
252
+
253
+ await recordAudit({
254
+ action: "vault.secret.updated",
255
+ targetType: "vault-secret",
256
+ targetId: existing[0].id,
257
+ summary: `Updated vault secret "${input.name}" (${credentialKey})`,
258
+ });
259
+
260
+ return getSecret(existing[0].id, ctx);
261
+ }
262
+
152
263
  const secretId = id();
153
264
  const actor = ctx.ownerEmail;
154
265
 
@@ -157,7 +268,7 @@ export async function createSecret(
157
268
  ownerEmail: actor,
158
269
  orgId: ctx.orgId,
159
270
  name: input.name,
160
- credentialKey: input.credentialKey,
271
+ credentialKey,
161
272
  value: input.value,
162
273
  provider: input.provider || null,
163
274
  description: input.description || null,
@@ -169,15 +280,15 @@ export async function createSecret(
169
280
  await recordVaultAudit({
170
281
  action: "secret.created",
171
282
  secretId,
172
- summary: `Created secret "${input.name}" (${input.credentialKey})`,
173
- metadata: { credentialKey: input.credentialKey, provider: input.provider },
283
+ summary: `Created secret "${input.name}" (${credentialKey})`,
284
+ metadata: { credentialKey, provider: input.provider },
174
285
  });
175
286
 
176
287
  await recordAudit({
177
288
  action: "vault.secret.created",
178
289
  targetType: "vault-secret",
179
290
  targetId: secretId,
180
- summary: `Created vault secret "${input.name}" (${input.credentialKey})`,
291
+ summary: `Created vault secret "${input.name}" (${credentialKey})`,
181
292
  });
182
293
 
183
294
  return getSecret(secretId, ctx);
@@ -259,7 +370,7 @@ export async function listGrants(filter?: {
259
370
  appId?: string;
260
371
  }) {
261
372
  const db = getDb();
262
- const conditions = [orgFilter(schema.vaultGrants)];
373
+ const conditions = [scopedFilter(schema.vaultGrants)];
263
374
  if (filter?.secretId) {
264
375
  conditions.push(eq(schema.vaultGrants.secretId, filter.secretId) as any);
265
376
  }
@@ -340,7 +451,16 @@ export async function grantSecretsToApp(
340
451
  appId: string,
341
452
  ctx: VaultCtx = requireVaultCtx(),
342
453
  ) {
454
+ const access = await getVaultAccessSettings();
343
455
  const uniqueSecretIds = Array.from(new Set(secretIds));
456
+ if (access.mode === "all-apps") {
457
+ return {
458
+ appId,
459
+ accessMode: access.mode,
460
+ created: [],
461
+ skipped: uniqueSecretIds,
462
+ };
463
+ }
344
464
  const existingActive = (await listGrants({ appId })).filter(
345
465
  (grant) => grant.status === "active",
346
466
  );
@@ -362,7 +482,7 @@ export async function grantSecretsToApp(
362
482
  }
363
483
  }
364
484
 
365
- return { appId, created, skipped };
485
+ return { appId, accessMode: access.mode, created, skipped };
366
486
  }
367
487
 
368
488
  export async function revokeGrant(
@@ -403,6 +523,40 @@ export async function revokeGrant(
403
523
  return getGrant(grantId, ctx);
404
524
  }
405
525
 
526
+ // ─── Shared Credential Store Sync ─────────────────────────────────
527
+
528
+ type VaultSecretRow = typeof schema.vaultSecrets.$inferSelect;
529
+
530
+ export function credentialStoreScopeForVaultCtx(ctx: VaultCtx): {
531
+ scope: Extract<SecretScope, "org" | "workspace">;
532
+ scopeId: string;
533
+ } {
534
+ if (ctx.orgId) return { scope: "org", scopeId: ctx.orgId };
535
+ return { scope: "workspace", scopeId: `solo:${ctx.ownerEmail}` };
536
+ }
537
+
538
+ export async function syncSecretsToCredentialStore(
539
+ secrets: VaultSecretRow[],
540
+ ctx: VaultCtx,
541
+ ) {
542
+ const target = credentialStoreScopeForVaultCtx(ctx);
543
+ const syncedKeys: string[] = [];
544
+
545
+ for (const secret of secrets) {
546
+ if (!secret.credentialKey || !secret.value) continue;
547
+ await writeAppSecret({
548
+ key: secret.credentialKey,
549
+ value: secret.value,
550
+ scope: target.scope,
551
+ scopeId: target.scopeId,
552
+ description: `Synced from Dispatch vault: ${secret.name}`,
553
+ });
554
+ syncedKeys.push(secret.credentialKey);
555
+ }
556
+
557
+ return { ...target, keys: syncedKeys };
558
+ }
559
+
406
560
  // ─── Sync ──────────────────────────────────────────────────────
407
561
 
408
562
  export async function syncGrantsToApp(
@@ -410,46 +564,78 @@ export async function syncGrantsToApp(
410
564
  ctx: VaultCtx = requireVaultCtx(),
411
565
  ) {
412
566
  const db = getDb();
567
+ const access = await getVaultAccessSettings();
413
568
  const agents = await discoverAgents("dispatch");
414
569
  const agent = agents.find((a) => a.id === appId);
415
570
  if (!agent) throw new Error(`App "${appId}" not found in agent registry`);
416
571
 
417
- const grants = await listGrants({ appId });
418
- const activeGrants = grants.filter((g) => g.status === "active");
419
- if (activeGrants.length === 0) {
420
- return { appId, synced: 0, keys: [] };
421
- }
572
+ const secretsToSync: VaultSecretRow[] = [];
573
+ const activeGrants =
574
+ access.mode === "manual"
575
+ ? (await listGrants({ appId })).filter((g) => g.status === "active")
576
+ : [];
422
577
 
423
- // Resolve secret values for each grant
424
- const vars: Array<{ key: string; value: string }> = [];
425
- for (const grant of activeGrants) {
426
- const secret = await getSecret(grant.secretId, ctx);
427
- if (secret) {
428
- vars.push({ key: secret.credentialKey, value: secret.value });
578
+ if (access.mode === "all-apps") {
579
+ const secrets = await listSecrets();
580
+ for (const secret of secrets) {
581
+ secretsToSync.push(secret);
582
+ }
583
+ } else {
584
+ for (const grant of activeGrants) {
585
+ const secret = await getSecret(grant.secretId, ctx);
586
+ if (secret) {
587
+ secretsToSync.push(secret);
588
+ }
429
589
  }
430
590
  }
431
591
 
432
- if (vars.length === 0) {
433
- return { appId, synced: 0, keys: [] };
592
+ if (secretsToSync.length === 0) {
593
+ return { appId, accessMode: access.mode, synced: 0, keys: [] };
434
594
  }
435
595
 
436
- // Push to the app's env-vars endpoint
437
- const res = await fetch(`${agent.url}/_agent-native/env-vars`, {
438
- method: "POST",
439
- headers: { "Content-Type": "application/json" },
440
- body: JSON.stringify({ vars }),
441
- });
442
-
443
- if (!res.ok) {
444
- const err = await res.text().catch(() => "Unknown error");
445
- throw new Error(`Failed to sync to ${appId}: ${err}`);
596
+ const credentialStoreSync = await syncSecretsToCredentialStore(
597
+ secretsToSync,
598
+ ctx,
599
+ );
600
+ const vars = secretsToSync.map((secret) => ({
601
+ key: secret.credentialKey,
602
+ value: secret.value,
603
+ }));
604
+ let envVarSync:
605
+ | { status: "synced"; keys: string[] }
606
+ | { status: "skipped"; reason: string }
607
+ | { status: "failed"; reason: string };
608
+
609
+ // Best-effort push to the app's env-vars endpoint for local/dev apps that
610
+ // still read process.env directly. Production/shared-DB apps intentionally
611
+ // reject env writes; the encrypted app_secrets sync above is the canonical
612
+ // path for request-scoped credentials.
613
+ try {
614
+ const res = await fetch(`${agent.url}/_agent-native/env-vars`, {
615
+ method: "POST",
616
+ headers: { "Content-Type": "application/json" },
617
+ body: JSON.stringify({ vars }),
618
+ });
619
+
620
+ if (res.ok) {
621
+ const result = await res.json();
622
+ envVarSync = { status: "synced", keys: result.saved || [] };
623
+ } else {
624
+ const err = await res.text().catch(() => "Unknown error");
625
+ envVarSync = { status: "skipped", reason: err };
626
+ }
627
+ } catch (err) {
628
+ envVarSync = {
629
+ status: "failed",
630
+ reason: err instanceof Error ? err.message : String(err),
631
+ };
446
632
  }
447
633
 
448
- const result = await res.json();
449
- const syncedKeys: string[] = result.saved || [];
634
+ const syncedKeys = credentialStoreSync.keys;
450
635
  const timestamp = now();
451
636
 
452
- // Update syncedAt on grants that were successfully pushed
637
+ // Update syncedAt on grants that were successfully pushed to the shared
638
+ // credential store. All-apps mode has no explicit grant rows to update.
453
639
  for (const grant of activeGrants) {
454
640
  const secret = await getSecret(grant.secretId, ctx);
455
641
  if (secret && syncedKeys.includes(secret.credentialKey)) {
@@ -464,17 +650,36 @@ export async function syncGrantsToApp(
464
650
  action: "secret.synced",
465
651
  appId,
466
652
  summary: `Synced ${syncedKeys.length} secret(s) to ${appId}: ${syncedKeys.join(", ")}`,
467
- metadata: { syncedKeys },
653
+ metadata: {
654
+ syncedKeys,
655
+ accessMode: access.mode,
656
+ credentialStore: {
657
+ scope: credentialStoreSync.scope,
658
+ scopeId: credentialStoreSync.scopeId,
659
+ },
660
+ envVars: envVarSync,
661
+ },
468
662
  });
469
663
 
470
- return { appId, synced: syncedKeys.length, keys: syncedKeys };
664
+ return {
665
+ appId,
666
+ accessMode: access.mode,
667
+ synced: syncedKeys.length,
668
+ keys: syncedKeys,
669
+ credentialStore: {
670
+ scope: credentialStoreSync.scope,
671
+ scopeId: credentialStoreSync.scopeId,
672
+ synced: credentialStoreSync.keys.length,
673
+ },
674
+ envVars: envVarSync,
675
+ };
471
676
  }
472
677
 
473
678
  // ─── Requests ──────────────────────────────────────────────────────
474
679
 
475
680
  export async function listRequests(filter?: { status?: string }) {
476
681
  const db = getDb();
477
- const conditions = [orgFilter(schema.vaultRequests)];
682
+ const conditions = [scopedFilter(schema.vaultRequests)];
478
683
  if (filter?.status) {
479
684
  conditions.push(eq(schema.vaultRequests.status, filter.status) as any);
480
685
  }
@@ -671,10 +876,12 @@ export interface AppIntegrations {
671
876
  url: string;
672
877
  color: string;
673
878
  integrations: IntegrationEntry[];
879
+ vaultAccessMode: VaultAccessMode;
674
880
  reachable: boolean;
675
881
  }
676
882
 
677
883
  export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
884
+ const access = await getVaultAccessSettings();
678
885
  const agents = await discoverAgents("dispatch");
679
886
  const grants = await listGrants();
680
887
  const secrets = await listSecrets();
@@ -695,6 +902,7 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
695
902
  url: agent.url,
696
903
  color: agent.color,
697
904
  integrations: [],
905
+ vaultAccessMode: access.mode,
698
906
  reachable: false,
699
907
  });
700
908
  continue;
@@ -720,7 +928,9 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
720
928
  required: env.required,
721
929
  configured: env.configured,
722
930
  vaultGranted:
723
- !!matchingSecret && grantedSecretIds.has(matchingSecret.id),
931
+ !!matchingSecret &&
932
+ (access.mode === "all-apps" ||
933
+ grantedSecretIds.has(matchingSecret.id)),
724
934
  vaultSecretId: matchingSecret?.id,
725
935
  };
726
936
  });
@@ -731,6 +941,7 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
731
941
  url: agent.url,
732
942
  color: agent.color,
733
943
  integrations,
944
+ vaultAccessMode: access.mode,
734
945
  reachable: true,
735
946
  });
736
947
  } catch {
@@ -740,6 +951,7 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
740
951
  url: agent.url,
741
952
  color: agent.color,
742
953
  integrations: [],
954
+ vaultAccessMode: access.mode,
743
955
  reachable: false,
744
956
  });
745
957
  }
@@ -751,15 +963,20 @@ export async function listIntegrationsCatalog(): Promise<AppIntegrations[]> {
751
963
  // ─── Vault Overview (for dashboard) ──────────────────────────────
752
964
 
753
965
  export async function listVaultOverview() {
754
- const [secrets, grants, requests] = await Promise.all([
966
+ const [secrets, grants, requests, access] = await Promise.all([
755
967
  listSecrets(),
756
968
  listGrants(),
757
969
  listRequests(),
970
+ getVaultAccessSettings(),
758
971
  ]);
972
+ const manualGrantCount = grants.filter((g) => g.status === "active").length;
759
973
 
760
974
  return {
975
+ accessMode: access.mode,
761
976
  secretCount: secrets.length,
762
- activeGrantCount: grants.filter((g) => g.status === "active").length,
977
+ activeGrantCount:
978
+ access.mode === "all-apps" ? secrets.length : manualGrantCount,
979
+ manualGrantCount,
763
980
  pendingRequestCount: requests.filter((r) => r.status === "pending").length,
764
981
  };
765
982
  }
@@ -27,8 +27,9 @@ Use the standard workspace primitives:
27
27
  - Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.
28
28
  - Use recurring jobs for scheduled behavior.
29
29
  - Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.
30
+ - 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.
30
31
  - 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.
31
- - When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json, 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.
32
+ - 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.
32
33
  - Treat first-party apps such as Mail, Calendar, Analytics, 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.
33
34
 
34
35
  When a user asks for something like a digest, reminder, routing rule, or saved behavior:
@@ -1,5 +1,10 @@
1
1
  import { createCoreRoutesPlugin } from "@agent-native/core/server";
2
2
  import { envKeys } from "../lib/env-config.js";
3
+ import { registerDispatchOnboardingSteps } from "../lib/onboarding-steps.js";
4
+
5
+ // Register before the core plugin so "create your first app" (order 5) appears
6
+ // above the auto-generated Slack/Telegram steps (order 60). Idempotent.
7
+ registerDispatchOnboardingSteps();
3
8
 
4
9
  export default createCoreRoutesPlugin({
5
10
  envKeys,
@@ -11,7 +11,7 @@ const DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for thi
11
11
  Default posture:
12
12
  - Treat Slack, Telegram, and email as shared entrypoints into the workspace.
13
13
  - 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), videos (Remotion compositions), forms (form builder), clips (screen recordings), design (visual designs), and images (brand image libraries and generated raster imagery).
14
- - Use list-connected-agents to see what agents are available before assuming a request must be handled locally.
14
+ - 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.
15
15
  - 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.
16
16
  - Treat first-party apps such as Mail, Calendar, Analytics, 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.
17
17
  - Keep durable memory and operating instructions in resources rather than ephemeral chat.
@@ -23,7 +23,7 @@ When a user asks for something:
23
23
  - Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
24
24
  - If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
25
25
  - If a new-app prompt asks for access to Mail, Calendar, Analytics, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.
26
- - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
26
+ - If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description when possible. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon" or "builder-unavailable", explain the missing Builder setup and ask them to connect/configure Builder.
27
27
  - For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
28
28
  - Keep responses concise and operational — messaging platforms have character limits.
29
29
  - Use markdown sparingly (bold and lists are fine, avoid complex formatting).