@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.
- package/README.md +1 -1
- package/dist/actions/create-pylon-ticket.d.ts +3 -0
- package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
- package/dist/actions/create-pylon-ticket.js +94 -0
- package/dist/actions/create-pylon-ticket.js.map +1 -0
- package/dist/actions/create-vault-grant.js +1 -1
- package/dist/actions/create-vault-grant.js.map +1 -1
- package/dist/actions/create-vault-secret.d.ts.map +1 -1
- package/dist/actions/create-vault-secret.js +4 -3
- package/dist/actions/create-vault-secret.js.map +1 -1
- package/dist/actions/get-vault-access-settings.d.ts +3 -0
- package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/get-vault-access-settings.js +10 -0
- package/dist/actions/get-vault-access-settings.js.map +1 -0
- package/dist/actions/grant-vault-secrets-to-app.js +1 -1
- package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-integrations-catalog.js +1 -1
- package/dist/actions/list-integrations-catalog.js.map +1 -1
- package/dist/actions/list-vault-grants.js +1 -1
- package/dist/actions/list-vault-grants.js.map +1 -1
- package/dist/actions/list-workspace-apps.d.ts.map +1 -1
- package/dist/actions/list-workspace-apps.js +5 -1
- package/dist/actions/list-workspace-apps.js.map +1 -1
- package/dist/actions/set-vault-access-settings.d.ts +3 -0
- package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
- package/dist/actions/set-vault-access-settings.js +13 -0
- package/dist/actions/set-vault-access-settings.js.map +1 -0
- package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +6 -0
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/sync-vault-to-app.js +1 -1
- package/dist/actions/sync-vault-to-app.js.map +1 -1
- package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
- package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
- package/dist/actions/update-workspace-app-metadata.js +30 -0
- package/dist/actions/update-workspace-app-metadata.js.map +1 -0
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +4 -2
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/app-keys-popover.js +16 -5
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +38 -14
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts +4 -4
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +6 -6
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +10 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +2 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +41 -2
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/hooks/use-navigation-state.js +12 -5
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/catch-all-target.d.ts +2 -0
- package/dist/lib/catch-all-target.d.ts.map +1 -0
- package/dist/lib/catch-all-target.js +95 -0
- package/dist/lib/catch-all-target.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +9 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts +2 -2
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +17 -8
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +20 -15
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/new-app.js +1 -1
- package/dist/routes/pages/new-app.js.map +1 -1
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +5 -1
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +23 -5
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +13 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +295 -9
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/env-config.d.ts.map +1 -1
- package/dist/server/lib/env-config.js +5 -0
- package/dist/server/lib/env-config.js.map +1 -1
- package/dist/server/lib/onboarding-steps.d.ts +12 -0
- package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
- package/dist/server/lib/onboarding-steps.js +47 -0
- package/dist/server/lib/onboarding-steps.js.map +1 -0
- package/dist/server/lib/vault-store.d.ts +55 -0
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +210 -41
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/core-routes.d.ts.map +1 -1
- package/dist/server/plugins/core-routes.js +4 -0
- package/dist/server/plugins/core-routes.js.map +1 -1
- package/dist/server/plugins/integrations.js +2 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +13 -11
- package/src/actions/create-pylon-ticket.ts +109 -0
- package/src/actions/create-vault-grant.ts +1 -1
- package/src/actions/create-vault-secret.ts +4 -3
- package/src/actions/get-vault-access-settings.ts +11 -0
- package/src/actions/grant-vault-secrets-to-app.ts +1 -1
- package/src/actions/index.ts +8 -0
- package/src/actions/list-integrations-catalog.ts +1 -1
- package/src/actions/list-vault-grants.ts +1 -1
- package/src/actions/list-workspace-apps.ts +5 -1
- package/src/actions/set-vault-access-settings.ts +16 -0
- package/src/actions/start-workspace-app-creation.ts +8 -0
- package/src/actions/sync-vault-to-app.ts +1 -1
- package/src/actions/update-workspace-app-metadata.ts +32 -0
- package/src/actions/view-screen.ts +4 -1
- package/src/components/app-keys-popover.tsx +23 -7
- package/src/components/create-app-popover.tsx +47 -14
- package/src/components/dispatch-shell.tsx +16 -15
- package/src/components/layout/Layout.tsx +11 -5
- package/src/components/messaging-setup-panel.tsx +54 -39
- package/src/components/workspace-app-card.tsx +102 -0
- package/src/hooks/use-navigation-state.ts +10 -4
- package/src/lib/catch-all-target.spec.ts +218 -0
- package/src/lib/catch-all-target.ts +99 -0
- package/src/lib/workspace-apps.ts +9 -0
- package/src/routes/pages/$appId.tsx +21 -8
- package/src/routes/pages/integrations.tsx +57 -18
- package/src/routes/pages/new-app.tsx +1 -1
- package/src/routes/pages/overview.tsx +11 -3
- package/src/routes/pages/vault.tsx +76 -9
- package/src/server/lib/app-creation-store.spec.ts +61 -2
- package/src/server/lib/app-creation-store.ts +386 -11
- package/src/server/lib/env-config.ts +5 -0
- package/src/server/lib/onboarding-steps.ts +49 -0
- package/src/server/lib/vault-store.spec.ts +69 -0
- package/src/server/lib/vault-store.ts +266 -49
- package/src/server/plugins/agent-chat.ts +2 -1
- package/src/server/plugins/core-routes.ts +5 -0
- 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
|
-
|
|
44
|
-
eq(table.ownerEmail, ctx.ownerEmail),
|
|
45
|
-
|
|
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
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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}" (${
|
|
173
|
-
metadata: { credentialKey
|
|
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}" (${
|
|
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 = [
|
|
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
|
|
418
|
-
const activeGrants =
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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 (
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
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: {
|
|
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 {
|
|
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 = [
|
|
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 &&
|
|
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:
|
|
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).
|