@agent-native/dispatch 0.40.30 → 0.40.33
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/CHANGELOG.md +28 -25
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +2 -9
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-dispatch-usage-metrics.js +1 -1
- package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
- package/dist/actions/list-workspace-connections.d.ts.map +1 -1
- package/dist/actions/list-workspace-connections.js +13 -18
- package/dist/actions/list-workspace-connections.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/actions/provider-api-register.js +2 -14
- package/dist/actions/provider-api-register.js.map +1 -1
- package/dist/components/action-query-error.d.ts +2 -1
- package/dist/components/action-query-error.d.ts.map +1 -1
- package/dist/components/action-query-error.js +25 -2
- package/dist/components/action-query-error.js.map +1 -1
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +2 -2
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.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 +7 -0
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +4 -9
- package/dist/db/index.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +1 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.js +1 -1
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/server/lib/admin-config.d.ts.map +1 -1
- package/dist/server/lib/admin-config.js +3 -1
- package/dist/server/lib/admin-config.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +124 -65
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
- package/dist/server/lib/mcp-access-store.js +2 -10
- package/dist/server/lib/mcp-access-store.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +2 -6
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +2 -11
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +2 -9
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/lib/workspace-app-action-auth.d.ts.map +1 -1
- package/dist/server/lib/workspace-app-action-auth.js +7 -2
- package/dist/server/lib/workspace-app-action-auth.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +1 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/connect-external-agent.spec.ts +25 -11
- package/src/actions/connect-external-agent.ts +2 -9
- package/src/actions/index.spec.ts +3 -0
- package/src/actions/list-connected-agents.spec.ts +17 -1
- package/src/actions/list-connected-agents.ts +7 -2
- package/src/actions/list-dispatch-usage-metrics.ts +1 -1
- package/src/actions/list-workspace-connections.spec.ts +14 -3
- package/src/actions/list-workspace-connections.ts +13 -17
- package/src/actions/provider-api-register.spec.ts +15 -9
- package/src/actions/provider-api-register.ts +5 -13
- package/src/components/action-query-error.spec.tsx +20 -0
- package/src/components/action-query-error.tsx +40 -0
- package/src/components/chat-home-apps.spec.tsx +32 -1
- package/src/components/chat-home-apps.tsx +6 -1
- package/src/components/dispatch-control-plane.tsx +1 -0
- package/src/components/layout/Layout.tsx +9 -0
- package/src/db/index.spec.ts +74 -0
- package/src/db/index.ts +4 -9
- package/src/routes/pages/$appId.tsx +1 -0
- package/src/routes/pages/apps.spec.tsx +26 -2
- package/src/routes/pages/apps.tsx +2 -0
- package/src/server/lib/admin-config.spec.ts +51 -0
- package/src/server/lib/admin-config.ts +2 -1
- package/src/server/lib/app-creation-store.spec.ts +308 -10
- package/src/server/lib/app-creation-store.ts +174 -70
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/mcp-access-store.spec.ts +39 -12
- package/src/server/lib/mcp-access-store.ts +3 -10
- package/src/server/lib/thread-debug-store.spec.ts +64 -0
- package/src/server/lib/thread-debug-store.ts +2 -9
- package/src/server/lib/usage-metrics-store.spec.ts +64 -0
- package/src/server/lib/usage-metrics-store.ts +2 -17
- package/src/server/lib/vault-store.spec.ts +26 -9
- package/src/server/lib/vault-store.ts +3 -10
- package/src/server/lib/workspace-app-action-auth.spec.ts +37 -6
- package/src/server/lib/workspace-app-action-auth.ts +12 -2
- package/src/server/plugins/agent-chat.spec.ts +14 -0
- package/src/server/plugins/agent-chat.ts +1 -1
|
@@ -80,8 +80,16 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
80
80
|
}));
|
|
81
81
|
|
|
82
82
|
vi.mock("../../components/action-query-error", () => ({
|
|
83
|
-
ActionQueryError: ({
|
|
84
|
-
|
|
83
|
+
ActionQueryError: ({
|
|
84
|
+
error,
|
|
85
|
+
queryName,
|
|
86
|
+
}: {
|
|
87
|
+
error: Error;
|
|
88
|
+
queryName?: string;
|
|
89
|
+
}) => (
|
|
90
|
+
<div role="alert">
|
|
91
|
+
{queryName}: {error.message}
|
|
92
|
+
</div>
|
|
85
93
|
),
|
|
86
94
|
}));
|
|
87
95
|
|
|
@@ -167,6 +175,22 @@ describe("Dispatch apps route", () => {
|
|
|
167
175
|
expect(container.textContent).toContain("No workspace apps");
|
|
168
176
|
});
|
|
169
177
|
|
|
178
|
+
it("identifies each app discovery query when both fail", async () => {
|
|
179
|
+
queryState.workspaceAppsError = new Error("Workspace app list failed");
|
|
180
|
+
queryState.connectedAppsError = new Error("Connected agent list failed");
|
|
181
|
+
|
|
182
|
+
await renderAppsRoute();
|
|
183
|
+
|
|
184
|
+
const errors = Array.from(
|
|
185
|
+
container.querySelectorAll('[role="alert"]'),
|
|
186
|
+
(alert) => alert.textContent,
|
|
187
|
+
);
|
|
188
|
+
expect(errors).toContain("list-workspace-apps: Workspace app list failed");
|
|
189
|
+
expect(errors).toContain(
|
|
190
|
+
"list-connected-agents: Connected agent list failed",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
170
194
|
it("hides community apps while the Lab is off", async () => {
|
|
171
195
|
await renderAppsRoute();
|
|
172
196
|
|
|
@@ -185,12 +185,14 @@ function AppsRoute() {
|
|
|
185
185
|
<ActionQueryError
|
|
186
186
|
error={appsQuery.error}
|
|
187
187
|
onRetry={() => void appsQuery.refetch()}
|
|
188
|
+
queryName="list-workspace-apps"
|
|
188
189
|
/>
|
|
189
190
|
) : null}
|
|
190
191
|
{connectedAppsQuery.isError ? (
|
|
191
192
|
<ActionQueryError
|
|
192
193
|
error={connectedAppsQuery.error}
|
|
193
194
|
onRetry={() => void connectedAppsQuery.refetch()}
|
|
195
|
+
queryName="list-connected-agents"
|
|
194
196
|
/>
|
|
195
197
|
) : null}
|
|
196
198
|
{showAppSkeletons ? (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const getAppConfigMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("@agent-native/core/server", async (importOriginal) => ({
|
|
6
|
+
...(await importOriginal<typeof import("@agent-native/core/server")>()),
|
|
7
|
+
getAppConfig: () => getAppConfigMock(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { runWithRequestContext } from "@agent-native/core/server";
|
|
11
|
+
|
|
12
|
+
import { isDispatchEnvironmentAdmin } from "./admin-config.js";
|
|
13
|
+
|
|
14
|
+
describe("isDispatchEnvironmentAdmin", () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("recognizes configured environment administrators outside delegated requests", () => {
|
|
20
|
+
getAppConfigMock.mockReturnValue({
|
|
21
|
+
dispatch: {
|
|
22
|
+
adminEmails: ["platform@example.test"],
|
|
23
|
+
workspaceOwnerEmails: [],
|
|
24
|
+
defaultOwnerEmail: null,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(isDispatchEnvironmentAdmin("PLATFORM@example.test")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not turn an org-member assertion for an environment admin into global access", async () => {
|
|
32
|
+
getAppConfigMock.mockReturnValue({
|
|
33
|
+
dispatch: {
|
|
34
|
+
adminEmails: ["platform@example.test"],
|
|
35
|
+
workspaceOwnerEmails: [],
|
|
36
|
+
defaultOwnerEmail: null,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await runWithRequestContext(
|
|
41
|
+
{
|
|
42
|
+
userEmail: "platform@example.test",
|
|
43
|
+
orgId: "org-1",
|
|
44
|
+
a2aIdentityAssurance: "org-member",
|
|
45
|
+
},
|
|
46
|
+
() => {
|
|
47
|
+
expect(isDispatchEnvironmentAdmin("platform@example.test")).toBe(false);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getAppConfig } from "@agent-native/core/server";
|
|
1
|
+
import { getAppConfig, getRequestContext } from "@agent-native/core/server";
|
|
2
2
|
|
|
3
3
|
export function isDispatchEnvironmentAdmin(email: string): boolean {
|
|
4
4
|
const normalized = email.trim().toLowerCase();
|
|
5
5
|
if (!normalized) return false;
|
|
6
|
+
if (getRequestContext()?.a2aIdentityAssurance === "org-member") return false;
|
|
6
7
|
const config = getAppConfig().dispatch;
|
|
7
8
|
return [
|
|
8
9
|
...config.adminEmails,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -29,6 +30,20 @@ import {
|
|
|
29
30
|
const originalFetch = globalThis.fetch;
|
|
30
31
|
const settingsKey = "dispatch-app-creation-settings:user:dev@example.test";
|
|
31
32
|
|
|
33
|
+
function verifyJwtWithSecret(
|
|
34
|
+
token: string,
|
|
35
|
+
secret: string,
|
|
36
|
+
): Record<string, unknown> {
|
|
37
|
+
const [header, payload, signature] = token.split(".");
|
|
38
|
+
const expected = createHmac("sha256", secret)
|
|
39
|
+
.update(`${header}.${payload}`)
|
|
40
|
+
.digest("base64url");
|
|
41
|
+
if (signature !== expected || !payload) {
|
|
42
|
+
throw new Error("JWT signature did not match the provided secret.");
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
const mocks = vi.hoisted(() => {
|
|
33
48
|
const settings = new Map<string, unknown>();
|
|
34
49
|
const state = {
|
|
@@ -55,6 +70,8 @@ const mocks = vi.hoisted(() => {
|
|
|
55
70
|
getOrgDomain: vi.fn(async (orgId: string) =>
|
|
56
71
|
orgId ? "example.test" : null,
|
|
57
72
|
),
|
|
73
|
+
getOrgA2ASecret: vi.fn(async () => null as string | null),
|
|
74
|
+
signA2AToken: vi.fn(),
|
|
58
75
|
isWorkspaceAppAccessAllowed: vi.fn(
|
|
59
76
|
async (): Promise<boolean | "unavailable"> => true,
|
|
60
77
|
),
|
|
@@ -62,6 +79,7 @@ const mocks = vi.hoisted(() => {
|
|
|
62
79
|
role: "viewer",
|
|
63
80
|
resource: {},
|
|
64
81
|
})),
|
|
82
|
+
readOrgMemberRole: vi.fn(async () => state.orgRole),
|
|
65
83
|
getDbExec: vi.fn(() => ({
|
|
66
84
|
execute: vi.fn(async (statement: unknown) => {
|
|
67
85
|
const sql =
|
|
@@ -131,6 +149,18 @@ const mocks = vi.hoisted(() => {
|
|
|
131
149
|
};
|
|
132
150
|
});
|
|
133
151
|
|
|
152
|
+
vi.mock("@agent-native/core/a2a", async (importOriginal) => {
|
|
153
|
+
const actual =
|
|
154
|
+
await importOriginal<typeof import("@agent-native/core/a2a")>();
|
|
155
|
+
mocks.signA2AToken.mockImplementation((...args: any[]) =>
|
|
156
|
+
actual.signA2AToken(...args),
|
|
157
|
+
);
|
|
158
|
+
return {
|
|
159
|
+
...actual,
|
|
160
|
+
signA2AToken: (...args: any[]) => mocks.signA2AToken(...args),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
134
164
|
vi.mock("../../db/index.js", async (importOriginal) => {
|
|
135
165
|
const actual = await importOriginal<typeof import("../../db/index.js")>();
|
|
136
166
|
return { ...actual, getDb: mocks.getDb };
|
|
@@ -166,6 +196,7 @@ vi.mock("@agent-native/core/org", async (importOriginal) => {
|
|
|
166
196
|
await importOriginal<typeof import("@agent-native/core/org")>();
|
|
167
197
|
return {
|
|
168
198
|
...actual,
|
|
199
|
+
getOrgA2ASecret: (...args: any[]) => mocks.getOrgA2ASecret(...args),
|
|
169
200
|
getOrgDomain: (...args: any[]) => mocks.getOrgDomain(...args),
|
|
170
201
|
isWorkspaceAppAccessAllowed: (...args: any[]) =>
|
|
171
202
|
mocks.isWorkspaceAppAccessAllowed(...args),
|
|
@@ -186,6 +217,7 @@ vi.mock("@agent-native/core/server", async (importOriginal) => {
|
|
|
186
217
|
await importOriginal<typeof import("@agent-native/core/server")>();
|
|
187
218
|
return {
|
|
188
219
|
...actual,
|
|
220
|
+
readOrgMemberRole: (...args: any[]) => mocks.readOrgMemberRole(...args),
|
|
189
221
|
resolveBuilderCredentialsDetailed: (...args: any[]) =>
|
|
190
222
|
mocks.resolveBuilderCredentialsDetailed(...args),
|
|
191
223
|
createBuilderProject: (...args: any[]) =>
|
|
@@ -218,6 +250,8 @@ afterEach(() => {
|
|
|
218
250
|
mocks.settings.clear();
|
|
219
251
|
mocks.getOrgSetting.mockReset();
|
|
220
252
|
mocks.getOrgSetting.mockResolvedValue(null);
|
|
253
|
+
mocks.getOrgA2ASecret.mockReset();
|
|
254
|
+
mocks.getOrgA2ASecret.mockResolvedValue(null);
|
|
221
255
|
mocks.isWorkspaceAppAccessAllowed.mockReset();
|
|
222
256
|
mocks.isWorkspaceAppAccessAllowed.mockResolvedValue(true);
|
|
223
257
|
mocks.mutateSetting.mockReset();
|
|
@@ -229,6 +263,8 @@ afterEach(() => {
|
|
|
229
263
|
},
|
|
230
264
|
);
|
|
231
265
|
mocks.state.orgRole = "admin";
|
|
266
|
+
mocks.readOrgMemberRole.mockReset();
|
|
267
|
+
mocks.readOrgMemberRole.mockImplementation(async () => mocks.state.orgRole);
|
|
232
268
|
mocks.state.workspaceApps = [];
|
|
233
269
|
orgAppsDirectoryCache.clear();
|
|
234
270
|
orgAppsDirectoryAccessCache.clear();
|
|
@@ -600,6 +636,130 @@ describe("listWorkspaceApps", () => {
|
|
|
600
636
|
expect(apps[0]?.url).toBe("https://agent-workspace.builder.io/atlas");
|
|
601
637
|
});
|
|
602
638
|
|
|
639
|
+
it("signs the hosted registry request with the caller's organization secret", async () => {
|
|
640
|
+
const fetchMock = vi.fn(
|
|
641
|
+
async () =>
|
|
642
|
+
new Response(JSON.stringify([]), {
|
|
643
|
+
headers: { "content-type": "application/json" },
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
646
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
647
|
+
vi.stubEnv("A2A_SECRET", "dispatch-deployment-secret");
|
|
648
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
649
|
+
mocks.getOrgA2ASecret.mockResolvedValueOnce("workspace-org-secret");
|
|
650
|
+
|
|
651
|
+
await runWithRequestContext(
|
|
652
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
653
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const authorization = fetchMock.mock.calls[0]?.[1]?.headers
|
|
657
|
+
?.Authorization as string;
|
|
658
|
+
const token = authorization.slice("Bearer ".length);
|
|
659
|
+
const payload = verifyJwtWithSecret(token, "workspace-org-secret");
|
|
660
|
+
expect(payload).toMatchObject({
|
|
661
|
+
sub: "dev@example.test",
|
|
662
|
+
org_domain: "example.test",
|
|
663
|
+
aud: "https://agent-workspace.builder.io",
|
|
664
|
+
});
|
|
665
|
+
expect(() =>
|
|
666
|
+
verifyJwtWithSecret(token, "dispatch-deployment-secret"),
|
|
667
|
+
).toThrow();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("reports missing hosted gateway signing credentials instead of sending anonymously", async () => {
|
|
671
|
+
const fetchMock = vi.fn();
|
|
672
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
673
|
+
vi.stubEnv("A2A_SECRET", "");
|
|
674
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
675
|
+
mocks.getOrgA2ASecret.mockResolvedValueOnce(null);
|
|
676
|
+
stubManifest([
|
|
677
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
678
|
+
{ id: "clips", name: "Clips", path: "/clips" },
|
|
679
|
+
]);
|
|
680
|
+
|
|
681
|
+
await expect(
|
|
682
|
+
runWithRequestContext(
|
|
683
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
684
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
685
|
+
),
|
|
686
|
+
).rejects.toMatchObject({
|
|
687
|
+
statusCode: 503,
|
|
688
|
+
errorCode: "workspace_gateway_a2a_secret_missing",
|
|
689
|
+
});
|
|
690
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("reports organization A2A secret lookup failures before using manifest apps", async () => {
|
|
694
|
+
const fetchMock = vi.fn();
|
|
695
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
696
|
+
vi.stubEnv("A2A_SECRET", "dispatch-deployment-secret");
|
|
697
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
698
|
+
stubManifest([
|
|
699
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
700
|
+
{ id: "clips", name: "Clips", path: "/clips" },
|
|
701
|
+
]);
|
|
702
|
+
mocks.getOrgA2ASecret.mockRejectedValueOnce(
|
|
703
|
+
new Error("database unavailable"),
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
await expect(
|
|
707
|
+
runWithRequestContext(
|
|
708
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
709
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
710
|
+
),
|
|
711
|
+
).rejects.toMatchObject({
|
|
712
|
+
statusCode: 503,
|
|
713
|
+
errorCode: "workspace_gateway_org_a2a_secret_unavailable",
|
|
714
|
+
});
|
|
715
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("reports token signing failures before using manifest apps", async () => {
|
|
719
|
+
const fetchMock = vi.fn();
|
|
720
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
721
|
+
vi.stubEnv("A2A_SECRET", "dispatch-deployment-secret");
|
|
722
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
723
|
+
stubManifest([
|
|
724
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
725
|
+
{ id: "clips", name: "Clips", path: "/clips" },
|
|
726
|
+
]);
|
|
727
|
+
mocks.signA2AToken.mockRejectedValueOnce(new Error("signing unavailable"));
|
|
728
|
+
|
|
729
|
+
await expect(
|
|
730
|
+
runWithRequestContext(
|
|
731
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
732
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
733
|
+
),
|
|
734
|
+
).rejects.toMatchObject({
|
|
735
|
+
statusCode: 503,
|
|
736
|
+
errorCode: "workspace_gateway_token_signing_failed",
|
|
737
|
+
});
|
|
738
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("does not hide an organization lookup failure behind manifest apps", async () => {
|
|
742
|
+
const fetchMock = vi.fn();
|
|
743
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
744
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
745
|
+
stubManifest([
|
|
746
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
747
|
+
{ id: "clips", name: "Clips", path: "/clips" },
|
|
748
|
+
]);
|
|
749
|
+
mocks.getOrgDomain.mockRejectedValueOnce(new Error("database unavailable"));
|
|
750
|
+
|
|
751
|
+
await expect(
|
|
752
|
+
runWithRequestContext(
|
|
753
|
+
{ userEmail: "dev@example.test", orgId: "org-1" },
|
|
754
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
755
|
+
),
|
|
756
|
+
).rejects.toMatchObject({
|
|
757
|
+
statusCode: 503,
|
|
758
|
+
errorCode: "workspace_gateway_org_domain_unavailable",
|
|
759
|
+
});
|
|
760
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
761
|
+
});
|
|
762
|
+
|
|
603
763
|
it("binds hosted registry tokens to the mounted Dispatch path", async () => {
|
|
604
764
|
const fetchMock = vi.fn(async () => {
|
|
605
765
|
return new Response(
|
|
@@ -793,6 +953,122 @@ describe("listWorkspaceApps", () => {
|
|
|
793
953
|
expect(apps.map((app) => app.id)).toEqual(["dispatch", "clips"]);
|
|
794
954
|
});
|
|
795
955
|
|
|
956
|
+
it("treats an empty hosted registry response as a successful empty list", async () => {
|
|
957
|
+
const fetchMock = vi.fn(
|
|
958
|
+
async () =>
|
|
959
|
+
new Response(JSON.stringify([]), {
|
|
960
|
+
headers: { "content-type": "application/json" },
|
|
961
|
+
}),
|
|
962
|
+
);
|
|
963
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
964
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
965
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
966
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
967
|
+
|
|
968
|
+
const apps = await runWithRequestContext(
|
|
969
|
+
{ userEmail: "dev@example.test" },
|
|
970
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
expect(apps).toEqual([]);
|
|
974
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it.each([206, 500, 502, 503])(
|
|
978
|
+
"surfaces hosted registry HTTP %i failures instead of using manifest apps",
|
|
979
|
+
async (status) => {
|
|
980
|
+
const fetchMock = vi.fn(
|
|
981
|
+
async () => new Response("gateway unavailable", { status }),
|
|
982
|
+
);
|
|
983
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
984
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
985
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
986
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
987
|
+
|
|
988
|
+
await expect(
|
|
989
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
990
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
991
|
+
),
|
|
992
|
+
).rejects.toMatchObject({
|
|
993
|
+
statusCode: 503,
|
|
994
|
+
errorCode: `workspace_gateway_http_${status}`,
|
|
995
|
+
});
|
|
996
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
it("surfaces hosted registry transport failures instead of using manifest apps", async () => {
|
|
1001
|
+
const fetchMock = vi.fn(async () => {
|
|
1002
|
+
throw new TypeError("Network connection failed");
|
|
1003
|
+
});
|
|
1004
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1005
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1006
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
1007
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
1008
|
+
|
|
1009
|
+
await expect(
|
|
1010
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
1011
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
1012
|
+
),
|
|
1013
|
+
).rejects.toMatchObject({
|
|
1014
|
+
statusCode: 503,
|
|
1015
|
+
errorCode: "workspace_gateway_unavailable",
|
|
1016
|
+
});
|
|
1017
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it("surfaces malformed hosted registry responses instead of using manifest apps", async () => {
|
|
1021
|
+
const fetchMock = vi.fn(
|
|
1022
|
+
async () =>
|
|
1023
|
+
new Response("not json", {
|
|
1024
|
+
headers: { "content-type": "application/json" },
|
|
1025
|
+
}),
|
|
1026
|
+
);
|
|
1027
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1028
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1029
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
1030
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
1031
|
+
|
|
1032
|
+
await expect(
|
|
1033
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
1034
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
1035
|
+
),
|
|
1036
|
+
).rejects.toMatchObject({
|
|
1037
|
+
statusCode: 503,
|
|
1038
|
+
errorCode: "workspace_gateway_invalid_response",
|
|
1039
|
+
});
|
|
1040
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it.each([
|
|
1044
|
+
[{ id: "dispatch", name: "Dispatch", path: "/dispatch" }, null],
|
|
1045
|
+
[
|
|
1046
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
1047
|
+
{ id: "DISPATCH", name: "Duplicate Dispatch", path: "/other" },
|
|
1048
|
+
],
|
|
1049
|
+
])("rejects incomplete hosted registry lists: %j", async (apps) => {
|
|
1050
|
+
const fetchMock = vi.fn(
|
|
1051
|
+
async () =>
|
|
1052
|
+
new Response(JSON.stringify(apps), {
|
|
1053
|
+
headers: { "content-type": "application/json" },
|
|
1054
|
+
}),
|
|
1055
|
+
);
|
|
1056
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1057
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1058
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
1059
|
+
stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
|
|
1060
|
+
|
|
1061
|
+
await expect(
|
|
1062
|
+
runWithRequestContext({ userEmail: "dev@example.test" }, () =>
|
|
1063
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
1064
|
+
),
|
|
1065
|
+
).rejects.toMatchObject({
|
|
1066
|
+
statusCode: 503,
|
|
1067
|
+
errorCode: "workspace_gateway_invalid_response",
|
|
1068
|
+
});
|
|
1069
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
796
1072
|
it("passes the Vercel protection bypass to the hosted workspace registry", async () => {
|
|
797
1073
|
const fetchMock = vi.fn(async () => {
|
|
798
1074
|
return new Response(
|
|
@@ -859,10 +1135,10 @@ describe("listWorkspaceApps", () => {
|
|
|
859
1135
|
);
|
|
860
1136
|
});
|
|
861
1137
|
|
|
862
|
-
it("
|
|
1138
|
+
it("skips a same-origin hosted registry without credentials", async () => {
|
|
863
1139
|
const fetchMock = vi.fn();
|
|
864
1140
|
vi.stubGlobal("fetch", fetchMock);
|
|
865
|
-
vi.stubEnv("A2A_SECRET", "
|
|
1141
|
+
vi.stubEnv("A2A_SECRET", "");
|
|
866
1142
|
vi.stubEnv("WORKSPACE_GATEWAY_URL", "https://agent-workspace.builder.io");
|
|
867
1143
|
|
|
868
1144
|
const apps = await runWithRequestContext(
|
|
@@ -891,9 +1167,13 @@ describe("listWorkspaceApps", () => {
|
|
|
891
1167
|
{ userEmail: "dev@example.test", orgId: "org-without-domain" },
|
|
892
1168
|
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
893
1169
|
),
|
|
894
|
-
).rejects.
|
|
895
|
-
|
|
896
|
-
|
|
1170
|
+
).rejects.toMatchObject({
|
|
1171
|
+
name: "WorkspaceAppsGatewayAuthorizationError",
|
|
1172
|
+
statusCode: 401,
|
|
1173
|
+
errorCode: "workspace_gateway_org_domain_missing",
|
|
1174
|
+
message:
|
|
1175
|
+
"Workspace apps gateway cannot authenticate without a resolved organization domain.",
|
|
1176
|
+
});
|
|
897
1177
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
898
1178
|
});
|
|
899
1179
|
|
|
@@ -2048,11 +2328,9 @@ describe("listWorkspaceApps", () => {
|
|
|
2048
2328
|
it("reports owner-role lookup failures as unavailable instead of unauthorized", async () => {
|
|
2049
2329
|
stubNoPendingContext();
|
|
2050
2330
|
stubManifest([{ id: "todo", name: "Todo", path: "/todo" }]);
|
|
2051
|
-
mocks.
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}),
|
|
2055
|
-
} as any);
|
|
2331
|
+
mocks.readOrgMemberRole.mockRejectedValue(
|
|
2332
|
+
new Error("database unavailable"),
|
|
2333
|
+
);
|
|
2056
2334
|
|
|
2057
2335
|
await expect(
|
|
2058
2336
|
runWithRequestContext(
|
|
@@ -2927,4 +3205,24 @@ describe("setAppCreationSettings", () => {
|
|
|
2927
3205
|
expect(mocks.writeAppSecret).not.toHaveBeenCalled();
|
|
2928
3206
|
expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
|
|
2929
3207
|
});
|
|
3208
|
+
|
|
3209
|
+
it("caps a delegated owner before changing the shared Builder project", async () => {
|
|
3210
|
+
mocks.readOrgMemberRole.mockResolvedValueOnce("member");
|
|
3211
|
+
|
|
3212
|
+
await expect(
|
|
3213
|
+
runWithRequestContext(
|
|
3214
|
+
{
|
|
3215
|
+
userEmail: "dev@example.test",
|
|
3216
|
+
orgId,
|
|
3217
|
+
a2aIdentityAssurance: "org-member",
|
|
3218
|
+
},
|
|
3219
|
+
() => setAppCreationSettings({ builderProjectId: projectId }),
|
|
3220
|
+
),
|
|
3221
|
+
).rejects.toMatchObject({ statusCode: 403 });
|
|
3222
|
+
expect(mocks.readOrgMemberRole).toHaveBeenCalledWith(
|
|
3223
|
+
orgId,
|
|
3224
|
+
"dev@example.test",
|
|
3225
|
+
);
|
|
3226
|
+
expect(mocks.putSetting).not.toHaveBeenCalled();
|
|
3227
|
+
});
|
|
2930
3228
|
});
|