@agent-native/dispatch 0.14.3 → 0.14.4
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 +114 -128
- package/dist/actions/list_apps.d.ts +1 -0
- package/dist/actions/list_apps.js +16 -8
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/set-mcp-app-access.js +1 -4
- package/dist/actions/set-mcp-app-access.js.map +1 -1
- package/dist/components/action-query-error.d.ts +8 -0
- package/dist/components/action-query-error.d.ts.map +1 -0
- package/dist/components/action-query-error.js +10 -0
- package/dist/components/action-query-error.js.map +1 -0
- package/dist/components/app-keys-popover.d.ts +4 -0
- package/dist/components/app-keys-popover.d.ts.map +1 -1
- package/dist/components/app-keys-popover.js +26 -14
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +31 -27
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +19 -21
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/sheet.js +1 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/toast.js +1 -1
- package/dist/components/ui/toast.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +8 -5
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
- package/dist/components/workspace-resource-effective-stack.js +6 -1
- package/dist/components/workspace-resource-effective-stack.js.map +1 -1
- package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
- package/dist/components/workspace-resource-impact-preview.js +6 -1
- package/dist/components/workspace-resource-impact-preview.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +6 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +5 -8
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +6 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/approvals.d.ts.map +1 -1
- package/dist/routes/pages/approvals.js +8 -5
- package/dist/routes/pages/approvals.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +4 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +9 -3
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/audit.d.ts.map +1 -1
- package/dist/routes/pages/audit.js +4 -2
- package/dist/routes/pages/audit.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +16 -2
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/destinations.d.ts.map +1 -1
- package/dist/routes/pages/destinations.js +5 -3
- package/dist/routes/pages/destinations.js.map +1 -1
- package/dist/routes/pages/identities.d.ts.map +1 -1
- package/dist/routes/pages/identities.js +5 -3
- package/dist/routes/pages/identities.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +9 -3
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +7 -6
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +23 -7
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +9 -3
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +2 -2
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
- package/dist/server/lib/mcp-access-store.js +30 -2
- package/dist/server/lib/mcp-access-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts +1 -1
- package/package.json +2 -2
- package/src/actions/list_apps.spec.ts +52 -0
- package/src/actions/list_apps.ts +16 -8
- package/src/actions/set-mcp-app-access.spec.ts +73 -0
- package/src/actions/set-mcp-app-access.ts +1 -5
- package/src/components/action-query-error.spec.tsx +56 -0
- package/src/components/action-query-error.tsx +32 -0
- package/src/components/app-keys-popover.spec.tsx +85 -0
- package/src/components/app-keys-popover.tsx +76 -50
- package/src/components/dispatch-control-plane.spec.tsx +106 -0
- package/src/components/dispatch-control-plane.tsx +71 -115
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.spec.tsx +42 -2
- package/src/components/layout/Layout.tsx +22 -27
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/workspace-app-card.spec.tsx +80 -0
- package/src/components/workspace-app-card.tsx +36 -25
- package/src/components/workspace-resource-effective-stack.tsx +13 -1
- package/src/components/workspace-resource-impact-preview.tsx +12 -1
- package/src/routes/pages/$appId.tsx +17 -1
- package/src/routes/pages/agents.tsx +24 -11
- package/src/routes/pages/approval.tsx +16 -4
- package/src/routes/pages/approvals.tsx +108 -89
- package/src/routes/pages/apps.$appId.tsx +9 -2
- package/src/routes/pages/apps.tsx +20 -7
- package/src/routes/pages/audit.tsx +29 -20
- package/src/routes/pages/chat.spec.tsx +94 -0
- package/src/routes/pages/chat.tsx +30 -10
- package/src/routes/pages/destinations.tsx +75 -62
- package/src/routes/pages/identities.tsx +74 -63
- package/src/routes/pages/integrations.tsx +17 -10
- package/src/routes/pages/thread-debug.tsx +18 -12
- package/src/routes/pages/vault.tsx +59 -26
- package/src/routes/pages/workspace.tsx +69 -55
- package/src/server/lib/approval-fencing.spec.ts +113 -2
- package/src/server/lib/dispatch-store.ts +2 -0
- package/src/server/lib/mcp-access-store.spec.ts +116 -4
- package/src/server/lib/mcp-access-store.ts +37 -2
- package/src/server/lib/mcp-gateway.spec.ts +79 -3
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
execute: vi.fn(),
|
|
5
|
+
getOrgSetting: vi.fn(),
|
|
6
|
+
getUserSetting: vi.fn(),
|
|
7
|
+
putOrgSetting: vi.fn(),
|
|
8
|
+
putUserSetting: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@agent-native/core/db", () => ({
|
|
12
|
+
getDbExec: () => ({ execute: mocks.execute }),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/settings", () => ({
|
|
16
|
+
getOrgSetting: mocks.getOrgSetting,
|
|
17
|
+
getUserSetting: mocks.getUserSetting,
|
|
18
|
+
putOrgSetting: mocks.putOrgSetting,
|
|
19
|
+
putUserSetting: mocks.putUserSetting,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { runWithRequestContext } from "@agent-native/core/server";
|
|
2
23
|
|
|
3
24
|
import {
|
|
4
25
|
isAppAllowedByMcpAccess,
|
|
5
26
|
normalizeMcpAppAccessSettings,
|
|
27
|
+
setDispatchMcpAppAccessSettings,
|
|
6
28
|
} from "./mcp-access-store.js";
|
|
7
29
|
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
mocks.execute.mockResolvedValue({ rows: [] });
|
|
33
|
+
});
|
|
34
|
+
|
|
8
35
|
describe("normalizeMcpAppAccessSettings", () => {
|
|
9
|
-
it("defaults to
|
|
36
|
+
it("defaults to Dispatch only", () => {
|
|
10
37
|
expect(normalizeMcpAppAccessSettings(null)).toEqual({
|
|
11
|
-
mode: "
|
|
12
|
-
selectedAppIds: [],
|
|
38
|
+
mode: "selected-apps",
|
|
39
|
+
selectedAppIds: ["dispatch"],
|
|
13
40
|
updatedAt: undefined,
|
|
14
41
|
updatedBy: undefined,
|
|
15
42
|
});
|
|
@@ -32,6 +59,91 @@ describe("normalizeMcpAppAccessSettings", () => {
|
|
|
32
59
|
});
|
|
33
60
|
});
|
|
34
61
|
|
|
62
|
+
describe("setDispatchMcpAppAccessSettings", () => {
|
|
63
|
+
it.each(["owner", "admin"])(
|
|
64
|
+
"lets an organization %s change the app grants",
|
|
65
|
+
async (role) => {
|
|
66
|
+
mocks.execute.mockResolvedValue({ rows: [{ role }] });
|
|
67
|
+
|
|
68
|
+
const result = await runWithRequestContext(
|
|
69
|
+
{ userEmail: "manager@example.test", orgId: "org-1" },
|
|
70
|
+
() =>
|
|
71
|
+
setDispatchMcpAppAccessSettings({
|
|
72
|
+
mode: "selected-apps",
|
|
73
|
+
selectedAppIds: ["dispatch", "analytics"],
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(mocks.execute).toHaveBeenCalledWith({
|
|
78
|
+
sql: expect.stringContaining("FROM org_members"),
|
|
79
|
+
args: ["org-1", "manager@example.test"],
|
|
80
|
+
});
|
|
81
|
+
expect(mocks.putOrgSetting).toHaveBeenCalledWith(
|
|
82
|
+
"org-1",
|
|
83
|
+
"dispatch-mcp-app-access",
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
mode: "selected-apps",
|
|
86
|
+
selectedAppIds: ["dispatch", "analytics"],
|
|
87
|
+
updatedBy: "manager@example.test",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(result.mode).toBe("selected-apps");
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it("rejects an organization member without changing grants", async () => {
|
|
95
|
+
mocks.execute.mockResolvedValue({ rows: [{ role: "member" }] });
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
runWithRequestContext(
|
|
99
|
+
{ userEmail: "member@example.test", orgId: "org-1" },
|
|
100
|
+
() =>
|
|
101
|
+
setDispatchMcpAppAccessSettings({
|
|
102
|
+
mode: "all-apps",
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
).rejects.toMatchObject({
|
|
106
|
+
message: expect.stringMatching(/Only organization owners and admins/),
|
|
107
|
+
statusCode: 403,
|
|
108
|
+
});
|
|
109
|
+
expect(mocks.putOrgSetting).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("fails closed when organization membership cannot be verified", async () => {
|
|
113
|
+
mocks.execute.mockRejectedValue(new Error("org lookup unavailable"));
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
runWithRequestContext(
|
|
117
|
+
{ userEmail: "owner@example.test", orgId: "org-1" },
|
|
118
|
+
() =>
|
|
119
|
+
setDispatchMcpAppAccessSettings({
|
|
120
|
+
mode: "all-apps",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
).rejects.toThrow(/Only organization owners and admins/);
|
|
124
|
+
expect(mocks.putOrgSetting).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("lets an authenticated solo user manage only their own grants", async () => {
|
|
128
|
+
await runWithRequestContext({ userEmail: "solo@example.test" }, () =>
|
|
129
|
+
setDispatchMcpAppAccessSettings({
|
|
130
|
+
mode: "selected-apps",
|
|
131
|
+
selectedAppIds: [],
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(mocks.execute).not.toHaveBeenCalled();
|
|
136
|
+
expect(mocks.putUserSetting).toHaveBeenCalledWith(
|
|
137
|
+
"solo@example.test",
|
|
138
|
+
"dispatch-mcp-app-access",
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
mode: "selected-apps",
|
|
141
|
+
selectedAppIds: [],
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
35
147
|
describe("isAppAllowedByMcpAccess", () => {
|
|
36
148
|
it("allows every app in all-apps mode", () => {
|
|
37
149
|
expect(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
1
2
|
import {
|
|
2
3
|
getRequestOrgId,
|
|
3
4
|
getRequestUserEmail,
|
|
@@ -20,6 +21,16 @@ export interface DispatchMcpAppAccessSettings {
|
|
|
20
21
|
updatedBy?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
class McpAppAccessError extends Error {
|
|
25
|
+
statusCode: number;
|
|
26
|
+
|
|
27
|
+
constructor(message: string, statusCode: number) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "McpAppAccessError";
|
|
30
|
+
this.statusCode = statusCode;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
interface AccessScope {
|
|
24
35
|
kind: "org" | "user";
|
|
25
36
|
id: string;
|
|
@@ -45,10 +56,11 @@ export function normalizeMcpAppAccessSettings(
|
|
|
45
56
|
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
46
57
|
? (raw as Record<string, unknown>)
|
|
47
58
|
: {};
|
|
48
|
-
const mode = record.mode === "
|
|
59
|
+
const mode = record.mode === "all-apps" ? "all-apps" : "selected-apps";
|
|
49
60
|
return {
|
|
50
61
|
mode,
|
|
51
|
-
selectedAppIds:
|
|
62
|
+
selectedAppIds:
|
|
63
|
+
raw == null ? ["dispatch"] : uniqueAppIds(record.selectedAppIds),
|
|
52
64
|
updatedAt:
|
|
53
65
|
typeof record.updatedAt === "string" ? record.updatedAt : undefined,
|
|
54
66
|
updatedBy:
|
|
@@ -64,6 +76,28 @@ function currentAccessScope(): AccessScope {
|
|
|
64
76
|
return { kind: "user", id: actor, actor };
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
async function assertCanManageMcpAppAccess(scope: AccessScope): Promise<void> {
|
|
80
|
+
if (scope.kind === "user") return;
|
|
81
|
+
|
|
82
|
+
let role: unknown = null;
|
|
83
|
+
try {
|
|
84
|
+
const { rows } = await getDbExec().execute({
|
|
85
|
+
sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
|
|
86
|
+
args: [scope.id, scope.actor.toLowerCase()],
|
|
87
|
+
});
|
|
88
|
+
role = rows[0]?.role;
|
|
89
|
+
} catch {
|
|
90
|
+
// Fail closed when org membership cannot be verified.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (role !== "owner" && role !== "admin") {
|
|
94
|
+
throw new McpAppAccessError(
|
|
95
|
+
"Only organization owners and admins can change Dispatch MCP app access.",
|
|
96
|
+
403,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
67
101
|
export async function getDispatchMcpAppAccessSettings(): Promise<DispatchMcpAppAccessSettings> {
|
|
68
102
|
const scope = currentAccessScope();
|
|
69
103
|
const raw =
|
|
@@ -78,6 +112,7 @@ export async function setDispatchMcpAppAccessSettings(input: {
|
|
|
78
112
|
selectedAppIds?: string[];
|
|
79
113
|
}): Promise<DispatchMcpAppAccessSettings> {
|
|
80
114
|
const scope = currentAccessScope();
|
|
115
|
+
await assertCanManageMcpAppAccess(scope);
|
|
81
116
|
const next: DispatchMcpAppAccessSettings = {
|
|
82
117
|
mode: input.mode,
|
|
83
118
|
selectedAppIds: uniqueAppIds(input.selectedAppIds),
|
|
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
12
12
|
managerStop: vi.fn(),
|
|
13
13
|
managerCallTool: vi.fn(),
|
|
14
14
|
managerConstructor: vi.fn(),
|
|
15
|
+
callAgent: vi.fn(),
|
|
15
16
|
signA2AToken: vi.fn(),
|
|
16
17
|
getOrgA2ASecret: vi.fn(),
|
|
17
18
|
getOrgDomain: vi.fn(),
|
|
@@ -39,7 +40,7 @@ vi.mock("@agent-native/core/server", async (importOriginal) => {
|
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
vi.mock("@agent-native/core/a2a", () => ({
|
|
42
|
-
callAgent:
|
|
43
|
+
callAgent: mocks.callAgent,
|
|
43
44
|
signA2AToken: mocks.signA2AToken,
|
|
44
45
|
}));
|
|
45
46
|
|
|
@@ -74,6 +75,7 @@ import { runWithRequestContext } from "@agent-native/core/server";
|
|
|
74
75
|
|
|
75
76
|
import {
|
|
76
77
|
createGrantedDispatchMcpEmbedSession,
|
|
78
|
+
askGrantedDispatchMcpApp,
|
|
77
79
|
listGrantedDispatchMcpApps,
|
|
78
80
|
listGrantedDispatchMcpAppOrigins,
|
|
79
81
|
openGrantedDispatchMcpApp,
|
|
@@ -90,8 +92,8 @@ const analyticsAgent = {
|
|
|
90
92
|
|
|
91
93
|
beforeEach(() => {
|
|
92
94
|
mocks.discoverAgents.mockResolvedValue([analyticsAgent]);
|
|
93
|
-
mocks.getUserSetting.mockResolvedValue(
|
|
94
|
-
mocks.getOrgSetting.mockResolvedValue(
|
|
95
|
+
mocks.getUserSetting.mockResolvedValue({ mode: "all-apps" });
|
|
96
|
+
mocks.getOrgSetting.mockResolvedValue({ mode: "all-apps" });
|
|
95
97
|
mocks.createEmbedSessionTicket.mockResolvedValue({
|
|
96
98
|
ticket: "ticket-123",
|
|
97
99
|
ticketHash: "hash-123",
|
|
@@ -104,6 +106,7 @@ beforeEach(() => {
|
|
|
104
106
|
startUrl: "http://localhost:8086/_agent-native/embed/start?ticket=remote",
|
|
105
107
|
},
|
|
106
108
|
});
|
|
109
|
+
mocks.callAgent.mockResolvedValue("Created the requested dashboard.");
|
|
107
110
|
mocks.signA2AToken.mockResolvedValue("signed-token");
|
|
108
111
|
mocks.getOrgA2ASecret.mockResolvedValue(null);
|
|
109
112
|
mocks.getOrgDomain.mockResolvedValue(null);
|
|
@@ -115,7 +118,21 @@ afterEach(() => {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
describe("Dispatch MCP gateway app discovery", () => {
|
|
121
|
+
it("defaults to exposing Dispatch only until an owner or admin grants more apps", async () => {
|
|
122
|
+
mocks.getUserSetting.mockResolvedValue(null);
|
|
123
|
+
const apps = await runWithRequestContext(
|
|
124
|
+
{
|
|
125
|
+
userEmail: "owner@example.test",
|
|
126
|
+
requestOrigin: "http://localhost:8092",
|
|
127
|
+
},
|
|
128
|
+
() => listGrantedDispatchMcpApps(),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
118
134
|
it("includes Dispatch itself so agents can target extension routes", async () => {
|
|
135
|
+
mocks.getUserSetting.mockResolvedValue({ mode: "all-apps" });
|
|
119
136
|
const apps = await runWithRequestContext(
|
|
120
137
|
{
|
|
121
138
|
userEmail: "owner@example.test",
|
|
@@ -222,6 +239,7 @@ describe("Dispatch MCP gateway app discovery", () => {
|
|
|
222
239
|
color: "#111827",
|
|
223
240
|
},
|
|
224
241
|
]);
|
|
242
|
+
mocks.getUserSetting.mockResolvedValue({ mode: "all-apps" });
|
|
225
243
|
|
|
226
244
|
await expect(
|
|
227
245
|
runWithRequestContext(
|
|
@@ -235,6 +253,64 @@ describe("Dispatch MCP gateway app discovery", () => {
|
|
|
235
253
|
});
|
|
236
254
|
});
|
|
237
255
|
|
|
256
|
+
describe("askGrantedDispatchMcpApp", () => {
|
|
257
|
+
it("routes the authenticated user and active org identity to the granted app", async () => {
|
|
258
|
+
mocks.getOrgSetting.mockResolvedValue({
|
|
259
|
+
mode: "selected-apps",
|
|
260
|
+
selectedAppIds: ["analytics"],
|
|
261
|
+
});
|
|
262
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
263
|
+
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
264
|
+
|
|
265
|
+
const result = await runWithRequestContext(
|
|
266
|
+
{
|
|
267
|
+
userEmail: "owner@example.test",
|
|
268
|
+
orgId: "org-1",
|
|
269
|
+
requestOrigin: "http://localhost:8092",
|
|
270
|
+
},
|
|
271
|
+
() =>
|
|
272
|
+
askGrantedDispatchMcpApp(
|
|
273
|
+
"analytics",
|
|
274
|
+
"Build a weekly active users dashboard.",
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(mocks.callAgent).toHaveBeenCalledWith(
|
|
279
|
+
"http://localhost:8086",
|
|
280
|
+
"Build a weekly active users dashboard.",
|
|
281
|
+
{
|
|
282
|
+
userEmail: "owner@example.test",
|
|
283
|
+
orgDomain: "builder.io",
|
|
284
|
+
orgSecret: "org-specific-secret",
|
|
285
|
+
timeoutMs: 5 * 60_000,
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
expect(result).toEqual({
|
|
289
|
+
app: "analytics",
|
|
290
|
+
routedVia: "a2a",
|
|
291
|
+
response: "Created the requested dashboard.",
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("rejects delegation to an app outside the grant", async () => {
|
|
296
|
+
mocks.getUserSetting.mockResolvedValue({
|
|
297
|
+
mode: "selected-apps",
|
|
298
|
+
selectedAppIds: ["dispatch"],
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await expect(
|
|
302
|
+
runWithRequestContext(
|
|
303
|
+
{
|
|
304
|
+
userEmail: "owner@example.test",
|
|
305
|
+
requestOrigin: "http://localhost:8092",
|
|
306
|
+
},
|
|
307
|
+
() => askGrantedDispatchMcpApp("analytics", "Show signups."),
|
|
308
|
+
),
|
|
309
|
+
).rejects.toThrow(/not granted/);
|
|
310
|
+
expect(mocks.callAgent).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
238
314
|
describe("openGrantedDispatchMcpApp", () => {
|
|
239
315
|
it("opens Dispatch extension routes through the Dispatch app id", async () => {
|
|
240
316
|
const result = await runWithRequestContext(
|