@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +28 -25
  2. package/dist/actions/connect-external-agent.d.ts.map +1 -1
  3. package/dist/actions/connect-external-agent.js +2 -9
  4. package/dist/actions/connect-external-agent.js.map +1 -1
  5. package/dist/actions/list-connected-agents.js +7 -3
  6. package/dist/actions/list-connected-agents.js.map +1 -1
  7. package/dist/actions/list-dispatch-usage-metrics.js +1 -1
  8. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  9. package/dist/actions/list-workspace-connections.d.ts.map +1 -1
  10. package/dist/actions/list-workspace-connections.js +13 -18
  11. package/dist/actions/list-workspace-connections.js.map +1 -1
  12. package/dist/actions/provider-api-register.d.ts +8 -8
  13. package/dist/actions/provider-api-register.js +2 -14
  14. package/dist/actions/provider-api-register.js.map +1 -1
  15. package/dist/components/action-query-error.d.ts +2 -1
  16. package/dist/components/action-query-error.d.ts.map +1 -1
  17. package/dist/components/action-query-error.js +25 -2
  18. package/dist/components/action-query-error.js.map +1 -1
  19. package/dist/components/chat-home-apps.d.ts.map +1 -1
  20. package/dist/components/chat-home-apps.js +2 -2
  21. package/dist/components/chat-home-apps.js.map +1 -1
  22. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  23. package/dist/components/dispatch-control-plane.js +1 -1
  24. package/dist/components/dispatch-control-plane.js.map +1 -1
  25. package/dist/components/layout/Layout.d.ts +1 -0
  26. package/dist/components/layout/Layout.d.ts.map +1 -1
  27. package/dist/components/layout/Layout.js +7 -0
  28. package/dist/components/layout/Layout.js.map +1 -1
  29. package/dist/db/index.d.ts.map +1 -1
  30. package/dist/db/index.js +4 -9
  31. package/dist/db/index.js.map +1 -1
  32. package/dist/routes/pages/$appId.d.ts.map +1 -1
  33. package/dist/routes/pages/$appId.js +1 -1
  34. package/dist/routes/pages/$appId.js.map +1 -1
  35. package/dist/routes/pages/apps.js +1 -1
  36. package/dist/routes/pages/apps.js.map +1 -1
  37. package/dist/server/lib/admin-config.d.ts.map +1 -1
  38. package/dist/server/lib/admin-config.js +3 -1
  39. package/dist/server/lib/admin-config.js.map +1 -1
  40. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  41. package/dist/server/lib/app-creation-store.js +124 -65
  42. package/dist/server/lib/app-creation-store.js.map +1 -1
  43. package/dist/server/lib/community-apps.js +1 -1
  44. package/dist/server/lib/community-apps.js.map +1 -1
  45. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  46. package/dist/server/lib/mcp-access-store.js +2 -10
  47. package/dist/server/lib/mcp-access-store.js.map +1 -1
  48. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  49. package/dist/server/lib/thread-debug-store.js +2 -6
  50. package/dist/server/lib/thread-debug-store.js.map +1 -1
  51. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  52. package/dist/server/lib/usage-metrics-store.js +2 -11
  53. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  54. package/dist/server/lib/vault-store.d.ts.map +1 -1
  55. package/dist/server/lib/vault-store.js +2 -9
  56. package/dist/server/lib/vault-store.js.map +1 -1
  57. package/dist/server/lib/workspace-app-action-auth.d.ts.map +1 -1
  58. package/dist/server/lib/workspace-app-action-auth.js +7 -2
  59. package/dist/server/lib/workspace-app-action-auth.js.map +1 -1
  60. package/dist/server/plugins/agent-chat.js +1 -1
  61. package/dist/server/plugins/agent-chat.js.map +1 -1
  62. package/package.json +3 -3
  63. package/src/actions/connect-external-agent.spec.ts +25 -11
  64. package/src/actions/connect-external-agent.ts +2 -9
  65. package/src/actions/index.spec.ts +3 -0
  66. package/src/actions/list-connected-agents.spec.ts +17 -1
  67. package/src/actions/list-connected-agents.ts +7 -2
  68. package/src/actions/list-dispatch-usage-metrics.ts +1 -1
  69. package/src/actions/list-workspace-connections.spec.ts +14 -3
  70. package/src/actions/list-workspace-connections.ts +13 -17
  71. package/src/actions/provider-api-register.spec.ts +15 -9
  72. package/src/actions/provider-api-register.ts +5 -13
  73. package/src/components/action-query-error.spec.tsx +20 -0
  74. package/src/components/action-query-error.tsx +40 -0
  75. package/src/components/chat-home-apps.spec.tsx +32 -1
  76. package/src/components/chat-home-apps.tsx +6 -1
  77. package/src/components/dispatch-control-plane.tsx +1 -0
  78. package/src/components/layout/Layout.tsx +9 -0
  79. package/src/db/index.spec.ts +74 -0
  80. package/src/db/index.ts +4 -9
  81. package/src/routes/pages/$appId.tsx +1 -0
  82. package/src/routes/pages/apps.spec.tsx +26 -2
  83. package/src/routes/pages/apps.tsx +2 -0
  84. package/src/server/lib/admin-config.spec.ts +51 -0
  85. package/src/server/lib/admin-config.ts +2 -1
  86. package/src/server/lib/app-creation-store.spec.ts +308 -10
  87. package/src/server/lib/app-creation-store.ts +174 -70
  88. package/src/server/lib/community-apps.ts +1 -1
  89. package/src/server/lib/mcp-access-store.spec.ts +39 -12
  90. package/src/server/lib/mcp-access-store.ts +3 -10
  91. package/src/server/lib/thread-debug-store.spec.ts +64 -0
  92. package/src/server/lib/thread-debug-store.ts +2 -9
  93. package/src/server/lib/usage-metrics-store.spec.ts +64 -0
  94. package/src/server/lib/usage-metrics-store.ts +2 -17
  95. package/src/server/lib/vault-store.spec.ts +26 -9
  96. package/src/server/lib/vault-store.ts +3 -10
  97. package/src/server/lib/workspace-app-action-auth.spec.ts +37 -6
  98. package/src/server/lib/workspace-app-action-auth.ts +12 -2
  99. package/src/server/plugins/agent-chat.spec.ts +14 -0
  100. 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 getViewerOrgRole(orgId, viewerEmail);
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.getDbExec.mockReturnValue({
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.getDbExec.mockReturnValue({
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.getDbExec.mockReturnValue({
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: unknown = null;
151
+ let role: string | null = null;
152
152
  try {
153
- const result = await getDbExec().execute({
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 (!(await isOrgMember(claimedOrgId, identity.email))) {
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 (!(await isOrgMember(org.orgId, identity.email))) {
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
- - For Builder.io or AI credit spend, LLM usage by workspace member or month, or workspace app/Builder branch creation history, call list-dispatch-usage-metrics with scope=workspace and the requested sinceDays. Its monthlyByUser and workspaceAppCreationsByUserMonth fields are the authoritative shared-database result; do not ask for a user export or BigQuery schema. 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.
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.