@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
|
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
9
9
|
listWorkspaceApps: vi.fn(),
|
|
10
10
|
currentOrgId: vi.fn((): string | null => null),
|
|
11
11
|
currentOwnerEmail: vi.fn(() => "owner@example.test"),
|
|
12
|
+
delegatedOrgMember: false,
|
|
12
13
|
registerBuiltinEngines: vi.fn(),
|
|
13
14
|
dispatchConfig: {
|
|
14
15
|
adminEmails: [] as string[],
|
|
@@ -40,6 +41,25 @@ vi.mock("@agent-native/core/settings", () => ({
|
|
|
40
41
|
|
|
41
42
|
vi.mock("@agent-native/core/server", () => ({
|
|
42
43
|
getAppConfig: () => ({ dispatch: mocks.dispatchConfig }),
|
|
44
|
+
getRequestContext: () =>
|
|
45
|
+
mocks.delegatedOrgMember
|
|
46
|
+
? {
|
|
47
|
+
a2aIdentityAssurance: "org-member",
|
|
48
|
+
userEmail: mocks.currentOwnerEmail(),
|
|
49
|
+
orgId: mocks.currentOrgId(),
|
|
50
|
+
}
|
|
51
|
+
: undefined,
|
|
52
|
+
readOrgMemberRole: async (orgId: string, email: string) => {
|
|
53
|
+
const result = await mocks.execute({
|
|
54
|
+
sql: "SELECT role FROM org_members",
|
|
55
|
+
args: [orgId, email.toLowerCase()],
|
|
56
|
+
});
|
|
57
|
+
const role = result.rows[0]?.role;
|
|
58
|
+
if (mocks.delegatedOrgMember && (role === "owner" || role === "admin")) {
|
|
59
|
+
return "member";
|
|
60
|
+
}
|
|
61
|
+
return typeof role === "string" ? role : null;
|
|
62
|
+
},
|
|
43
63
|
}));
|
|
44
64
|
|
|
45
65
|
vi.mock("@agent-native/core/usage", () => ({
|
|
@@ -104,6 +124,7 @@ afterEach(() => {
|
|
|
104
124
|
vi.clearAllMocks();
|
|
105
125
|
mocks.currentOrgId.mockReturnValue(null);
|
|
106
126
|
mocks.currentOwnerEmail.mockReturnValue("owner@example.test");
|
|
127
|
+
mocks.delegatedOrgMember = false;
|
|
107
128
|
mocks.dispatchConfig.adminEmails = [];
|
|
108
129
|
mocks.dispatchConfig.workspaceOwnerEmails = [];
|
|
109
130
|
mocks.dispatchConfig.defaultOwnerEmail = undefined;
|
|
@@ -127,6 +148,49 @@ describe("listDispatchUsageMetrics", () => {
|
|
|
127
148
|
expect(mocks.listWorkspaceApps).not.toHaveBeenCalled();
|
|
128
149
|
});
|
|
129
150
|
|
|
151
|
+
it("does not let a delegated org member read workspace usage as an owner", async () => {
|
|
152
|
+
mocks.currentOrgId.mockReturnValue("org-a");
|
|
153
|
+
mocks.currentOwnerEmail.mockReturnValue("member@example.test");
|
|
154
|
+
mocks.delegatedOrgMember = true;
|
|
155
|
+
mocks.execute.mockResolvedValue({ rows: [{ role: "owner" }] });
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
listDispatchUsageMetrics({ scope: "workspace" }),
|
|
159
|
+
).rejects.toMatchObject({
|
|
160
|
+
name: "ForbiddenError",
|
|
161
|
+
statusCode: 403,
|
|
162
|
+
message:
|
|
163
|
+
"Only organization owners and admins can view workspace usage metrics.",
|
|
164
|
+
});
|
|
165
|
+
expect(
|
|
166
|
+
mocks.execute.mock.calls.some(([query]) =>
|
|
167
|
+
String((query as { sql?: string }).sql).includes(
|
|
168
|
+
"SELECT email, role, joined_at",
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
).toBe(false);
|
|
172
|
+
expect(mocks.getUsageSummary).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does not let an environment admin assertion read workspace usage as an org member", async () => {
|
|
176
|
+
mocks.currentOrgId.mockReturnValue("org-a");
|
|
177
|
+
mocks.currentOwnerEmail.mockReturnValue("platform-admin@example.test");
|
|
178
|
+
mocks.delegatedOrgMember = true;
|
|
179
|
+
mocks.dispatchConfig.adminEmails = ["platform-admin@example.test"];
|
|
180
|
+
mocks.execute.mockResolvedValue({ rows: [{ role: "owner" }] });
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
listDispatchUsageMetrics({ scope: "workspace" }),
|
|
184
|
+
).rejects.toMatchObject({
|
|
185
|
+
name: "ForbiddenError",
|
|
186
|
+
statusCode: 403,
|
|
187
|
+
message:
|
|
188
|
+
"Only organization owners and admins can view workspace usage metrics.",
|
|
189
|
+
});
|
|
190
|
+
expect(mocks.getUsageSummary).not.toHaveBeenCalled();
|
|
191
|
+
expect(mocks.listWorkspaceApps).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
130
194
|
it("allows a configured deployment admin to view workspace metrics", async () => {
|
|
131
195
|
mocks.currentOrgId.mockReturnValue("org-a");
|
|
132
196
|
mocks.currentOwnerEmail.mockReturnValue("owner@example.test");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
registerBuiltinEngines,
|
|
9
9
|
} from "@agent-native/core/agent/engine";
|
|
10
10
|
import { getDbExec } from "@agent-native/core/db";
|
|
11
|
+
import { readOrgMemberRole } from "@agent-native/core/server";
|
|
11
12
|
import { ForbiddenError } from "@agent-native/core/sharing";
|
|
12
13
|
import {
|
|
13
14
|
builderCreditsFromCostCents,
|
|
@@ -428,22 +429,6 @@ async function initializeUsageMetricsTable(sinceMs: number): Promise<void> {
|
|
|
428
429
|
}
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
async function getViewerOrgRole(
|
|
432
|
-
orgId: string | null,
|
|
433
|
-
email: string,
|
|
434
|
-
): Promise<string | null> {
|
|
435
|
-
if (!orgId) return null;
|
|
436
|
-
const rows = await queryRows<{ role?: string }>(
|
|
437
|
-
`SELECT role FROM org_members
|
|
438
|
-
WHERE org_id = ? AND LOWER(email) = ?
|
|
439
|
-
AND federation_removal_pending_at IS NULL
|
|
440
|
-
LIMIT 1`,
|
|
441
|
-
[orgId, email.toLowerCase()],
|
|
442
|
-
);
|
|
443
|
-
const role = rows[0]?.role;
|
|
444
|
-
return typeof role === "string" ? role : null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
432
|
async function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {
|
|
448
433
|
if (!orgId) return [];
|
|
449
434
|
const result = await getDbExec().execute({
|
|
@@ -950,7 +935,7 @@ async function assertCanViewMetrics(viewScope: UsageMetricsScope): Promise<{
|
|
|
950
935
|
}> {
|
|
951
936
|
const viewerEmail = currentOwnerEmail();
|
|
952
937
|
const orgId = currentOrgId();
|
|
953
|
-
const role = await
|
|
938
|
+
const role = orgId ? await readOrgMemberRole(orgId, viewerEmail) : null;
|
|
954
939
|
if (
|
|
955
940
|
viewScope === "me" ||
|
|
956
941
|
isDispatchEnvironmentAdmin(viewerEmail) ||
|
|
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
12
12
|
listAppSecretsForScope: vi.fn(),
|
|
13
13
|
readAppSecret: vi.fn(),
|
|
14
14
|
recordAudit: vi.fn(),
|
|
15
|
+
readOrgMemberRole: vi.fn(),
|
|
15
16
|
writeAppSecret: vi.fn(),
|
|
16
17
|
}));
|
|
17
18
|
|
|
@@ -28,6 +29,11 @@ vi.mock("@agent-native/core/db", async (importOriginal) => ({
|
|
|
28
29
|
getDbExec: mocks.getDbExec,
|
|
29
30
|
}));
|
|
30
31
|
|
|
32
|
+
vi.mock("@agent-native/core/server", async (importOriginal) => ({
|
|
33
|
+
...(await importOriginal<typeof import("@agent-native/core/server")>()),
|
|
34
|
+
readOrgMemberRole: (...args: unknown[]) => mocks.readOrgMemberRole(...args),
|
|
35
|
+
}));
|
|
36
|
+
|
|
31
37
|
vi.mock("@agent-native/core/server/agent-discovery", () => ({
|
|
32
38
|
discoverAgents: mocks.discoverAgents,
|
|
33
39
|
}));
|
|
@@ -100,9 +106,7 @@ describe("vault authorization", () => {
|
|
|
100
106
|
it("rejects an organization member before reading shared vault values", async () => {
|
|
101
107
|
mocks.currentOrgId.mockReturnValue("org_123");
|
|
102
108
|
mocks.currentOwnerEmail.mockReturnValue("member@example.test");
|
|
103
|
-
mocks.
|
|
104
|
-
execute: vi.fn().mockResolvedValue({ rows: [{ role: "member" }] }),
|
|
105
|
-
});
|
|
109
|
+
mocks.readOrgMemberRole.mockResolvedValue("member");
|
|
106
110
|
|
|
107
111
|
await expect(assertCanManageVault()).rejects.toMatchObject({
|
|
108
112
|
message:
|
|
@@ -114,9 +118,7 @@ describe("vault authorization", () => {
|
|
|
114
118
|
it("rejects organization members before listing or reading grants", async () => {
|
|
115
119
|
mocks.currentOrgId.mockReturnValue("org_123");
|
|
116
120
|
mocks.currentOwnerEmail.mockReturnValue("member@example.test");
|
|
117
|
-
mocks.
|
|
118
|
-
execute: vi.fn().mockResolvedValue({ rows: [{ role: "member" }] }),
|
|
119
|
-
});
|
|
121
|
+
mocks.readOrgMemberRole.mockResolvedValue("member");
|
|
120
122
|
|
|
121
123
|
await expect(listGrants()).rejects.toThrow(
|
|
122
124
|
"Only organization owners and admins can manage the workspace vault.",
|
|
@@ -132,14 +134,28 @@ describe("vault authorization", () => {
|
|
|
132
134
|
async (role) => {
|
|
133
135
|
mocks.currentOrgId.mockReturnValue("org_123");
|
|
134
136
|
mocks.currentOwnerEmail.mockReturnValue(`${role}@example.test`);
|
|
135
|
-
mocks.
|
|
136
|
-
execute: vi.fn().mockResolvedValue({ rows: [{ role }] }),
|
|
137
|
-
});
|
|
137
|
+
mocks.readOrgMemberRole.mockResolvedValue(role);
|
|
138
138
|
|
|
139
139
|
await expect(assertCanManageVault()).resolves.toBeUndefined();
|
|
140
140
|
},
|
|
141
141
|
);
|
|
142
142
|
|
|
143
|
+
it("caps a delegated owner before vault management", async () => {
|
|
144
|
+
mocks.currentOrgId.mockReturnValue("org_123");
|
|
145
|
+
mocks.currentOwnerEmail.mockReturnValue("owner@example.test");
|
|
146
|
+
mocks.readOrgMemberRole.mockResolvedValue("member");
|
|
147
|
+
|
|
148
|
+
await expect(assertCanManageVault()).rejects.toMatchObject({
|
|
149
|
+
statusCode: 403,
|
|
150
|
+
message:
|
|
151
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
152
|
+
});
|
|
153
|
+
expect(mocks.readOrgMemberRole).toHaveBeenCalledWith(
|
|
154
|
+
"org_123",
|
|
155
|
+
"owner@example.test",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
143
159
|
it("allows a solo workspace owner to manage their own vault", async () => {
|
|
144
160
|
mocks.currentOrgId.mockReturnValue(null);
|
|
145
161
|
|
|
@@ -587,6 +603,7 @@ describe("syncGrantsToApp", () => {
|
|
|
587
603
|
) {
|
|
588
604
|
mocks.currentOwnerEmail.mockReturnValue(caller.ownerEmail);
|
|
589
605
|
mocks.currentOrgId.mockReturnValue(caller.orgId);
|
|
606
|
+
mocks.readOrgMemberRole.mockResolvedValue("owner");
|
|
590
607
|
mocks.getOrgSetting.mockResolvedValue(null);
|
|
591
608
|
mocks.getUserSetting.mockResolvedValue(null);
|
|
592
609
|
mocks.discoverAgents.mockResolvedValue([
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import { getDbExec } from "@agent-native/core/db";
|
|
4
3
|
import { and, desc, eq, isNull, or, sql } from "@agent-native/core/db/schema";
|
|
5
4
|
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
6
5
|
import {
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
writeAppSecret,
|
|
12
11
|
type SecretScope,
|
|
13
12
|
} from "@agent-native/core/secrets";
|
|
13
|
+
import { readOrgMemberRole } from "@agent-native/core/server";
|
|
14
14
|
import { discoverAgents } from "@agent-native/core/server/agent-discovery";
|
|
15
15
|
import {
|
|
16
16
|
getOrgSetting,
|
|
@@ -148,16 +148,9 @@ export async function assertCanManageVault(): Promise<void> {
|
|
|
148
148
|
if (!orgId) return;
|
|
149
149
|
|
|
150
150
|
const email = currentOwnerEmail().trim().toLowerCase();
|
|
151
|
-
let role:
|
|
151
|
+
let role: string | null = null;
|
|
152
152
|
try {
|
|
153
|
-
|
|
154
|
-
sql: `SELECT role FROM org_members
|
|
155
|
-
WHERE org_id = ? AND LOWER(email) = ?
|
|
156
|
-
AND federation_removal_pending_at IS NULL
|
|
157
|
-
LIMIT 1`,
|
|
158
|
-
args: [orgId, email],
|
|
159
|
-
});
|
|
160
|
-
role = result.rows[0]?.role;
|
|
153
|
+
role = await readOrgMemberRole(orgId, email);
|
|
161
154
|
} catch {
|
|
162
155
|
// coercion-ok: unknown membership is treated as unauthorized below.
|
|
163
156
|
// A failed membership check must not become an authorization bypass.
|
|
@@ -212,6 +212,7 @@ describe("workspace app action auth", () => {
|
|
|
212
212
|
owner: "steve@builder.io",
|
|
213
213
|
anonymous: false,
|
|
214
214
|
orgId: "org-builder",
|
|
215
|
+
identityAssurance: "user",
|
|
215
216
|
});
|
|
216
217
|
expect(mocks.verifyA2AToken).toHaveBeenCalledWith(
|
|
217
218
|
"verified",
|
|
@@ -221,11 +222,41 @@ describe("workspace app action auth", () => {
|
|
|
221
222
|
expect(mocks.isOrgMember).toHaveBeenCalledWith(
|
|
222
223
|
"org-builder",
|
|
223
224
|
"steve@builder.io",
|
|
225
|
+
{ requestEvent: expect.anything() },
|
|
224
226
|
);
|
|
225
227
|
expect(mocks.resolveOrgIdForEmail).not.toHaveBeenCalled();
|
|
226
228
|
},
|
|
227
229
|
);
|
|
228
230
|
|
|
231
|
+
it("preserves delegated member assurance through the registry action resolver", async () => {
|
|
232
|
+
mocks.verifyA2AToken.mockResolvedValue({
|
|
233
|
+
email: "owner@builder.io",
|
|
234
|
+
orgDomain: "builder.io",
|
|
235
|
+
orgId: "org-builder",
|
|
236
|
+
identityAssurance: "org-member",
|
|
237
|
+
});
|
|
238
|
+
mocks.resolveOrgByDomain.mockResolvedValue({
|
|
239
|
+
orgId: "org-builder",
|
|
240
|
+
orgName: "Builder.io",
|
|
241
|
+
});
|
|
242
|
+
mocks.isOrgMember.mockResolvedValue(true);
|
|
243
|
+
const event = eventFor(WORKSPACE_APP_CLAIM_ACTION_PATH, "Bearer delegated");
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
workspaceAppActionRouteAuth.resolveCaller?.(event),
|
|
247
|
+
).resolves.toEqual({
|
|
248
|
+
owner: "owner@builder.io",
|
|
249
|
+
anonymous: false,
|
|
250
|
+
orgId: "org-builder",
|
|
251
|
+
identityAssurance: "org-member",
|
|
252
|
+
});
|
|
253
|
+
expect(mocks.isOrgMember).toHaveBeenCalledWith(
|
|
254
|
+
"org-builder",
|
|
255
|
+
"owner@builder.io",
|
|
256
|
+
{ requestEvent: event },
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
229
260
|
it("uses the original mounted pathname after the route prefix is stripped", async () => {
|
|
230
261
|
mocks.verifyA2AToken.mockResolvedValue({
|
|
231
262
|
email: "steve@builder.io",
|
|
@@ -276,14 +307,14 @@ describe("workspace app action auth", () => {
|
|
|
276
307
|
});
|
|
277
308
|
mocks.isOrgMember.mockResolvedValue(true);
|
|
278
309
|
|
|
310
|
+
const event = eventFor(WORKSPACE_APPS_ACTION_PATH, "Bearer scoped");
|
|
279
311
|
await expect(
|
|
280
|
-
workspaceAppActionRouteAuth.resolveCaller?.(
|
|
281
|
-
eventFor(WORKSPACE_APPS_ACTION_PATH, "Bearer scoped"),
|
|
282
|
-
),
|
|
312
|
+
workspaceAppActionRouteAuth.resolveCaller?.(event),
|
|
283
313
|
).resolves.toMatchObject({ orgId: "org-builder" });
|
|
284
314
|
expect(mocks.isOrgMember).toHaveBeenCalledWith(
|
|
285
315
|
"org-builder",
|
|
286
316
|
"steve@builder.io",
|
|
317
|
+
{ requestEvent: event },
|
|
287
318
|
);
|
|
288
319
|
expect(mocks.resolveOrgIdForEmail).not.toHaveBeenCalled();
|
|
289
320
|
});
|
|
@@ -332,14 +363,14 @@ describe("workspace app action auth", () => {
|
|
|
332
363
|
});
|
|
333
364
|
mocks.isOrgMember.mockResolvedValue(false);
|
|
334
365
|
|
|
366
|
+
const event = eventFor(WORKSPACE_APPS_ACTION_PATH, "Bearer global-token");
|
|
335
367
|
await expect(
|
|
336
|
-
workspaceAppActionRouteAuth.resolveCaller?.(
|
|
337
|
-
eventFor(WORKSPACE_APPS_ACTION_PATH, "Bearer global-token"),
|
|
338
|
-
),
|
|
368
|
+
workspaceAppActionRouteAuth.resolveCaller?.(event),
|
|
339
369
|
).rejects.toThrow("Invalid workspace registry authorization");
|
|
340
370
|
expect(mocks.isOrgMember).toHaveBeenCalledWith(
|
|
341
371
|
"org-builder",
|
|
342
372
|
"outsider@example.com",
|
|
373
|
+
{ requestEvent: event },
|
|
343
374
|
);
|
|
344
375
|
});
|
|
345
376
|
});
|
|
@@ -119,14 +119,22 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
|
|
|
119
119
|
refuseRegistryCaller("org-mismatch");
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
!(await isOrgMember(claimedOrgId, identity.email, {
|
|
124
|
+
requestEvent: event,
|
|
125
|
+
}))
|
|
126
|
+
) {
|
|
123
127
|
refuseRegistryCaller("not-org-member");
|
|
124
128
|
}
|
|
125
129
|
orgId = claimedOrgId;
|
|
126
130
|
} else if (orgDomain) {
|
|
127
131
|
const org = await resolveOrgByDomain(orgDomain);
|
|
128
132
|
if (!org) refuseRegistryCaller("org-unknown");
|
|
129
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
!(await isOrgMember(org.orgId, identity.email, {
|
|
135
|
+
requestEvent: event,
|
|
136
|
+
}))
|
|
137
|
+
) {
|
|
130
138
|
refuseRegistryCaller("not-org-member");
|
|
131
139
|
}
|
|
132
140
|
orgId = org.orgId;
|
|
@@ -138,6 +146,8 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
|
|
|
138
146
|
owner: identity.email,
|
|
139
147
|
anonymous: false,
|
|
140
148
|
orgId,
|
|
149
|
+
identityAssurance:
|
|
150
|
+
identity.identityAssurance === "org-member" ? "org-member" : "user",
|
|
141
151
|
};
|
|
142
152
|
},
|
|
143
153
|
};
|
|
@@ -45,4 +45,18 @@ describe("Dispatch agent chat plugin", () => {
|
|
|
45
45
|
}),
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("routes customer credit questions to the owning workspace app", async () => {
|
|
50
|
+
await import("./agent-chat.js");
|
|
51
|
+
|
|
52
|
+
const options = mocks.createAgentChatPlugin.mock.calls[0]?.[0] as {
|
|
53
|
+
systemPrompt?: string;
|
|
54
|
+
};
|
|
55
|
+
expect(options.systemPrompt).toContain(
|
|
56
|
+
"External customer, account, or company usage, credits, activity, or product data belongs to the relevant app",
|
|
57
|
+
);
|
|
58
|
+
expect(options.systemPrompt).toContain(
|
|
59
|
+
"Do not use this action for external customer/account/company data.",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
48
62
|
});
|
|
@@ -92,7 +92,7 @@ Use the standard workspace primitives:
|
|
|
92
92
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
93
93
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
94
94
|
- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.
|
|
95
|
-
-
|
|
95
|
+
- Use list-dispatch-usage-metrics only for this workspace's own members' agent/LLM usage and spend, or aggregate adoption of an accessible workspace app. For agent/LLM usage or spend, call it with scope=workspace and the requested sinceDays; it reports workspace-member usage only. For app adoption, call it with scope=app and appId; app creators can see aggregate daily/weekly active users and tracked actions for their own app, while organization owners/admins can inspect any accessible app. App scope omits individual users and prompt previews. External customer, account, or company usage, credits, activity, or product data belongs to the relevant app: use its identity in <relevant-apps> when clear, or call describe-workspace-apps with a query when ownership is unclear. Do not use this action for external customer/account/company data.
|
|
96
96
|
|
|
97
97
|
When a user asks for something like a digest, reminder, routing rule, or saved behavior:
|
|
98
98
|
- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.
|