agent-tower 0.5.1-beta.0 → 0.5.1-beta.2

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 (78) hide show
  1. package/dist/executors/__tests__/codex.executor.test.js +21 -0
  2. package/dist/executors/__tests__/codex.executor.test.js.map +1 -1
  3. package/dist/executors/codex.executor.d.ts.map +1 -1
  4. package/dist/executors/codex.executor.js +10 -4
  5. package/dist/executors/codex.executor.js.map +1 -1
  6. package/dist/mcp/http-client.d.ts +13 -0
  7. package/dist/mcp/http-client.d.ts.map +1 -1
  8. package/dist/mcp/http-client.js +10 -2
  9. package/dist/mcp/http-client.js.map +1 -1
  10. package/dist/mcp/server.d.ts.map +1 -1
  11. package/dist/mcp/server.js +55 -1
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/output/claude-code-parser.d.ts +5 -0
  14. package/dist/output/claude-code-parser.d.ts.map +1 -1
  15. package/dist/output/claude-code-parser.js +35 -0
  16. package/dist/output/claude-code-parser.js.map +1 -1
  17. package/dist/output/codex-parser.d.ts +4 -0
  18. package/dist/output/codex-parser.d.ts.map +1 -1
  19. package/dist/output/codex-parser.js +22 -1
  20. package/dist/output/codex-parser.js.map +1 -1
  21. package/dist/output/cursor-agent-parser.d.ts +8 -0
  22. package/dist/output/cursor-agent-parser.d.ts.map +1 -1
  23. package/dist/output/cursor-agent-parser.js +52 -1
  24. package/dist/output/cursor-agent-parser.js.map +1 -1
  25. package/dist/routes/team-runs.d.ts.map +1 -1
  26. package/dist/routes/team-runs.js +99 -7
  27. package/dist/routes/team-runs.js.map +1 -1
  28. package/dist/services/__tests__/team-reconciler.service.test.js +503 -17
  29. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  30. package/dist/services/__tests__/team-run.service.test.js +163 -0
  31. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  32. package/dist/services/team-run.service.d.ts +29 -7
  33. package/dist/services/team-run.service.d.ts.map +1 -1
  34. package/dist/services/team-run.service.js +285 -21
  35. package/dist/services/team-run.service.js.map +1 -1
  36. package/dist/web/assets/{AgentDemoPage-RIqT-dqT.js → AgentDemoPage-CEWSskKz.js} +1 -1
  37. package/dist/web/assets/{DemoPage-BVfTsgtm.js → DemoPage-DVt_gGQE.js} +1 -1
  38. package/dist/web/assets/{GeneralSettingsPage-CTA0DqKP.js → GeneralSettingsPage-B7DbUzT6.js} +1 -1
  39. package/dist/web/assets/{MemberAvatar-Dczz8ZdX.js → MemberAvatar--HnaCo5f.js} +1 -1
  40. package/dist/web/assets/{NotificationSettingsPage-Cv97qVtx.js → NotificationSettingsPage-BklUEhB2.js} +1 -1
  41. package/dist/web/assets/{ProfileSettingsPage-Bksw5g8X.js → ProfileSettingsPage-Be7yYwbc.js} +1 -1
  42. package/dist/web/assets/{ProjectKanbanPage-CWBhrade.js → ProjectKanbanPage-BZR3HX3_.js} +33 -33
  43. package/dist/web/assets/{ProjectSettingsPage-dgew6R0n.js → ProjectSettingsPage-Cj1h7G2-.js} +1 -1
  44. package/dist/web/assets/{ProviderSettingsPage-GLiNqGDi.js → ProviderSettingsPage-l244dDeI.js} +1 -1
  45. package/dist/web/assets/{TeamSettingsPage-fZ85oI46.js → TeamSettingsPage-CXp-7kqb.js} +1 -1
  46. package/dist/web/assets/{button-B_QmdIDk.js → button-Cc_0rVdG.js} +1 -1
  47. package/dist/web/assets/{chevron-down-yS2R7lcs.js → chevron-down-DAto5uxR.js} +1 -1
  48. package/dist/web/assets/{chevron-right-Cp6F3c0z.js → chevron-right-fcp8yUcf.js} +1 -1
  49. package/dist/web/assets/{chevron-up-Cafj4Xkn.js → chevron-up-BkPAyFXE.js} +1 -1
  50. package/dist/web/assets/{circle-check-BMVglRBw.js → circle-check-BmWaxH2e.js} +1 -1
  51. package/dist/web/assets/{code-block-OCS4YCEC-4uQ4wv9S.js → code-block-OCS4YCEC-U8rjjKMO.js} +1 -1
  52. package/dist/web/assets/{confirm-dialog-8V0YJykz.js → confirm-dialog-D0R59Lv2.js} +1 -1
  53. package/dist/web/assets/{folder-picker-CmybTC3i.js → folder-picker-COVJdb7m.js} +1 -1
  54. package/dist/web/assets/{index-C_HCNd7x.js → index-BCz-PlSG.js} +6 -6
  55. package/dist/web/assets/index-BjIvVVfi.css +1 -0
  56. package/dist/web/assets/{loader-circle-Dlg-qBpd.js → loader-circle-B0dxeUg4.js} +1 -1
  57. package/dist/web/assets/{mermaid-NOHMQCX5-DcBcvySI.js → mermaid-NOHMQCX5-CfVlHQvQ.js} +8 -8
  58. package/dist/web/assets/{modal-e3cMi6GX.js → modal-kzH1vcBa.js} +1 -1
  59. package/dist/web/assets/{pencil-DK53PbzO.js → pencil-DvjVNKIw.js} +1 -1
  60. package/dist/web/assets/{select-BOd6A0P8.js → select-Xt2WadPY.js} +1 -1
  61. package/dist/web/assets/{upload-DkM7kVs8.js → upload-dtpXWuCN.js} +1 -1
  62. package/dist/web/assets/{use-profiles-3teW4L61.js → use-profiles-oZ63gfvO.js} +1 -1
  63. package/dist/web/assets/{use-providers-CZGguLO6.js → use-providers-BVrApYJA.js} +1 -1
  64. package/dist/web/index.html +2 -2
  65. package/node_modules/@agent-tower/shared/dist/types.d.ts +17 -0
  66. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  67. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  68. package/node_modules/@prisma/client/.prisma/client/edge.js +17 -7
  69. package/node_modules/@prisma/client/.prisma/client/index-browser.js +11 -0
  70. package/node_modules/@prisma/client/.prisma/client/index.d.ts +1983 -154
  71. package/node_modules/@prisma/client/.prisma/client/index.js +17 -7
  72. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  73. package/node_modules/@prisma/client/.prisma/client/schema.prisma +41 -21
  74. package/node_modules/@prisma/client/.prisma/client/wasm.js +11 -0
  75. package/package.json +1 -1
  76. package/prisma/migrations/20260602000000_add_team_run_private_messages/migration.sql +29 -0
  77. package/prisma/schema.prisma +20 -0
  78. package/dist/web/assets/index-BTW4-3M0.css +0 -1
@@ -184,7 +184,10 @@ async function createFixture(options = {}) {
184
184
  aliases: stringifyJson([`member-${index + 1}`]),
185
185
  providerId: `provider-${index + 1}`,
186
186
  rolePrompt: `Role ${index + 1}`,
187
- capabilities: stringifyJson(capabilities),
187
+ capabilities: stringifyJson({
188
+ ...capabilities,
189
+ ...(options.memberCapabilities?.[index] ?? {}),
190
+ }),
188
191
  workspacePolicy: 'shared',
189
192
  triggerPolicy: options.triggerPolicies?.[index] ?? 'MENTION_ONLY',
190
193
  sessionPolicy: 'new_per_request',
@@ -783,8 +786,7 @@ describe('TeamReconcilerService', () => {
783
786
  }
784
787
  });
785
788
  it('lists TeamRun members through MCP without exposing role prompts', async () => {
786
- const previousTeamRunId = process.env.AGENT_TOWER_TEAM_RUN_ID;
787
- const previousMemberId = process.env.AGENT_TOWER_MEMBER_ID;
789
+ const previousEnv = captureTeamRunEnv();
788
790
  const { workspace, teamRun, members } = await createFixture({ memberCount: 2 });
789
791
  const runningRequest = await createWorkRequest({
790
792
  teamRunId: teamRun.id,
@@ -800,8 +802,12 @@ describe('TeamReconcilerService', () => {
800
802
  });
801
803
  const app = Fastify({ logger: false });
802
804
  try {
803
- process.env.AGENT_TOWER_TEAM_RUN_ID = teamRun.id;
804
- process.env.AGENT_TOWER_MEMBER_ID = members[0].id;
805
+ setTeamRunEnv({
806
+ AGENT_TOWER_TEAM_RUN_ID: teamRun.id,
807
+ AGENT_TOWER_MEMBER_ID: members[0].id,
808
+ AGENT_TOWER_INVOCATION_ID: undefined,
809
+ AGENT_TOWER_SESSION_ID: undefined,
810
+ });
805
811
  await app.register(teamRunRoutes, { prefix: '/api' });
806
812
  await app.listen({ port: 0, host: '127.0.0.1' });
807
813
  const address = app.server.address();
@@ -850,18 +856,7 @@ describe('TeamReconcilerService', () => {
850
856
  expect(payload.members[0]).not.toHaveProperty('presetId');
851
857
  }
852
858
  finally {
853
- if (previousTeamRunId === undefined) {
854
- delete process.env.AGENT_TOWER_TEAM_RUN_ID;
855
- }
856
- else {
857
- process.env.AGENT_TOWER_TEAM_RUN_ID = previousTeamRunId;
858
- }
859
- if (previousMemberId === undefined) {
860
- delete process.env.AGENT_TOWER_MEMBER_ID;
861
- }
862
- else {
863
- process.env.AGENT_TOWER_MEMBER_ID = previousMemberId;
864
- }
859
+ restoreTeamRunEnv(previousEnv);
865
860
  await app.close();
866
861
  }
867
862
  });
@@ -961,6 +956,497 @@ describe('TeamReconcilerService', () => {
961
956
  await app.close();
962
957
  }
963
958
  });
959
+ it('forbids REST member queue impersonation from another agent invocation', async () => {
960
+ const { workspace, teamRun, members } = await createFixture({
961
+ memberCount: 3,
962
+ teamRunMode: 'CONFIRM',
963
+ });
964
+ const recipient = members[1];
965
+ const observer = members[2];
966
+ const observerRequest = await createWorkRequest({
967
+ teamRunId: teamRun.id,
968
+ targetMemberId: observer.id,
969
+ status: 'STARTED',
970
+ });
971
+ const observerInvocation = await createRunningInvocation({
972
+ teamRunId: teamRun.id,
973
+ workRequestId: observerRequest.id,
974
+ memberId: observer.id,
975
+ workspaceId: workspace.id,
976
+ });
977
+ const app = Fastify({ logger: false });
978
+ try {
979
+ await app.register(teamRunRoutes, { prefix: '/api' });
980
+ const privateMessageResponse = await app.inject({
981
+ method: 'POST',
982
+ url: `/api/team-runs/${teamRun.id}/private-messages`,
983
+ payload: {
984
+ content: 'Hidden private queue preview',
985
+ recipientMemberIds: [recipient.id],
986
+ },
987
+ });
988
+ expect(privateMessageResponse.statusCode).toBe(201);
989
+ const hostQueueResponse = await app.inject({
990
+ method: 'GET',
991
+ url: `/api/team-runs/${teamRun.id}/members/${recipient.id}/work-requests`,
992
+ });
993
+ expect(hostQueueResponse.statusCode).toBe(200);
994
+ expect(hostQueueResponse.body).toContain('Hidden private queue preview');
995
+ const spoofedQueueResponse = await app.inject({
996
+ method: 'GET',
997
+ url: `/api/team-runs/${teamRun.id}/members/${recipient.id}/work-requests`,
998
+ headers: {
999
+ 'x-agent-tower-invocation-id': observerInvocation.id,
1000
+ },
1001
+ });
1002
+ expect(spoofedQueueResponse.statusCode).toBe(403);
1003
+ expect(spoofedQueueResponse.json()).toMatchObject({ code: 'FORBIDDEN' });
1004
+ expect(spoofedQueueResponse.body).not.toContain('Hidden private queue preview');
1005
+ const ownQueueResponse = await app.inject({
1006
+ method: 'GET',
1007
+ url: `/api/team-runs/${teamRun.id}/members/${observer.id}/work-requests`,
1008
+ headers: {
1009
+ 'x-agent-tower-invocation-id': observerInvocation.id,
1010
+ },
1011
+ });
1012
+ expect(ownQueueResponse.statusCode).toBe(200);
1013
+ }
1014
+ finally {
1015
+ await app.close();
1016
+ }
1017
+ });
1018
+ it('derives REST private message agent sender from invocation header and rejects host agent spoofing', async () => {
1019
+ const { workspace, teamRun, members } = await createFixture({
1020
+ memberCount: 3,
1021
+ teamRunMode: 'CONFIRM',
1022
+ });
1023
+ const actualSender = members[0];
1024
+ const forgedSender = members[1];
1025
+ const recipient = members[2];
1026
+ const senderRequest = await createWorkRequest({
1027
+ teamRunId: teamRun.id,
1028
+ targetMemberId: actualSender.id,
1029
+ status: 'STARTED',
1030
+ });
1031
+ const senderInvocation = await createRunningInvocation({
1032
+ teamRunId: teamRun.id,
1033
+ workRequestId: senderRequest.id,
1034
+ memberId: actualSender.id,
1035
+ workspaceId: workspace.id,
1036
+ });
1037
+ const app = Fastify({ logger: false });
1038
+ try {
1039
+ await app.register(teamRunRoutes, { prefix: '/api' });
1040
+ const forgedAgentResponse = await app.inject({
1041
+ method: 'POST',
1042
+ url: `/api/team-runs/${teamRun.id}/private-messages`,
1043
+ headers: {
1044
+ 'x-agent-tower-invocation-id': senderInvocation.id,
1045
+ },
1046
+ payload: {
1047
+ content: 'Forged sender should be ignored',
1048
+ recipientMemberIds: [recipient.id],
1049
+ senderType: 'agent',
1050
+ senderId: forgedSender.id,
1051
+ senderInvocationId: 'forged-invocation-id',
1052
+ },
1053
+ });
1054
+ expect(forgedAgentResponse.statusCode).toBe(201);
1055
+ const forgedAgentMessage = forgedAgentResponse.json();
1056
+ expect(forgedAgentMessage).toMatchObject({
1057
+ senderType: 'agent',
1058
+ senderId: actualSender.id,
1059
+ senderInvocationId: senderInvocation.id,
1060
+ });
1061
+ await expect(prisma.roomMessage.findUnique({ where: { id: forgedAgentMessage.id } })).resolves.toMatchObject({
1062
+ senderType: 'agent',
1063
+ senderId: actualSender.id,
1064
+ senderInvocationId: senderInvocation.id,
1065
+ });
1066
+ await expect(prisma.workRequest.findUnique({
1067
+ where: { id: forgedAgentMessage.workRequestIds[0] },
1068
+ })).resolves.toMatchObject({
1069
+ requesterType: 'agent',
1070
+ requesterMemberId: actualSender.id,
1071
+ });
1072
+ const hostAgentSpoofResponse = await app.inject({
1073
+ method: 'POST',
1074
+ url: `/api/team-runs/${teamRun.id}/private-messages`,
1075
+ payload: {
1076
+ content: 'Host cannot spoof agent sender',
1077
+ recipientMemberIds: [recipient.id],
1078
+ senderType: 'agent',
1079
+ senderId: actualSender.id,
1080
+ },
1081
+ });
1082
+ expect(hostAgentSpoofResponse.statusCode).toBe(400);
1083
+ expect(hostAgentSpoofResponse.json()).toMatchObject({ code: 'VALIDATION_ERROR' });
1084
+ await expect(prisma.roomMessage.findMany({
1085
+ where: { teamRunId: teamRun.id, content: 'Host cannot spoof agent sender' },
1086
+ })).resolves.toEqual([]);
1087
+ }
1088
+ finally {
1089
+ await app.close();
1090
+ }
1091
+ });
1092
+ it('preserves REST user private message senderId for member visibility without leaking to non-participants', async () => {
1093
+ const { workspace, teamRun, members } = await createFixture({
1094
+ memberCount: 3,
1095
+ teamRunMode: 'CONFIRM',
1096
+ });
1097
+ const sender = members[0];
1098
+ const recipient = members[1];
1099
+ const observer = members[2];
1100
+ const senderRequest = await createWorkRequest({
1101
+ teamRunId: teamRun.id,
1102
+ targetMemberId: sender.id,
1103
+ status: 'STARTED',
1104
+ });
1105
+ const senderInvocation = await createRunningInvocation({
1106
+ teamRunId: teamRun.id,
1107
+ workRequestId: senderRequest.id,
1108
+ memberId: sender.id,
1109
+ workspaceId: workspace.id,
1110
+ });
1111
+ const recipientRequest = await createWorkRequest({
1112
+ teamRunId: teamRun.id,
1113
+ targetMemberId: recipient.id,
1114
+ status: 'STARTED',
1115
+ });
1116
+ const recipientInvocation = await createRunningInvocation({
1117
+ teamRunId: teamRun.id,
1118
+ workRequestId: recipientRequest.id,
1119
+ memberId: recipient.id,
1120
+ workspaceId: workspace.id,
1121
+ });
1122
+ const observerRequest = await createWorkRequest({
1123
+ teamRunId: teamRun.id,
1124
+ targetMemberId: observer.id,
1125
+ status: 'STARTED',
1126
+ });
1127
+ const observerInvocation = await createRunningInvocation({
1128
+ teamRunId: teamRun.id,
1129
+ workRequestId: observerRequest.id,
1130
+ memberId: observer.id,
1131
+ workspaceId: workspace.id,
1132
+ });
1133
+ const app = Fastify({ logger: false });
1134
+ try {
1135
+ await app.register(teamRunRoutes, { prefix: '/api' });
1136
+ const response = await app.inject({
1137
+ method: 'POST',
1138
+ url: `/api/team-runs/${teamRun.id}/private-messages`,
1139
+ payload: {
1140
+ content: 'Host user sent private message',
1141
+ recipientMemberIds: [recipient.id],
1142
+ senderType: 'user',
1143
+ senderId: sender.id,
1144
+ },
1145
+ });
1146
+ expect(response.statusCode).toBe(201);
1147
+ const message = response.json();
1148
+ expect(message).toMatchObject({
1149
+ senderType: 'user',
1150
+ senderId: sender.id,
1151
+ senderInvocationId: null,
1152
+ recipientMemberIds: [recipient.id],
1153
+ });
1154
+ expect(new Set(message.participantMemberIds)).toEqual(new Set([sender.id, recipient.id]));
1155
+ const hostMessagesResponse = await app.inject({
1156
+ method: 'GET',
1157
+ url: `/api/team-runs/${teamRun.id}/messages`,
1158
+ });
1159
+ expect(hostMessagesResponse.statusCode).toBe(200);
1160
+ expect(hostMessagesResponse.body).toContain('Host user sent private message');
1161
+ const senderMessagesResponse = await app.inject({
1162
+ method: 'GET',
1163
+ url: `/api/team-runs/${teamRun.id}/messages`,
1164
+ headers: {
1165
+ 'x-agent-tower-invocation-id': senderInvocation.id,
1166
+ },
1167
+ });
1168
+ expect(senderMessagesResponse.statusCode).toBe(200);
1169
+ expect(senderMessagesResponse.body).toContain('Host user sent private message');
1170
+ const recipientMessagesResponse = await app.inject({
1171
+ method: 'GET',
1172
+ url: `/api/team-runs/${teamRun.id}/messages`,
1173
+ headers: {
1174
+ 'x-agent-tower-invocation-id': recipientInvocation.id,
1175
+ },
1176
+ });
1177
+ expect(recipientMessagesResponse.statusCode).toBe(200);
1178
+ expect(recipientMessagesResponse.body).toContain('Host user sent private message');
1179
+ const observerMessagesResponse = await app.inject({
1180
+ method: 'GET',
1181
+ url: `/api/team-runs/${teamRun.id}/messages`,
1182
+ headers: {
1183
+ 'x-agent-tower-invocation-id': observerInvocation.id,
1184
+ },
1185
+ });
1186
+ expect(observerMessagesResponse.statusCode).toBe(200);
1187
+ expect(observerMessagesResponse.body).not.toContain('Host user sent private message');
1188
+ const observerQueueResponse = await app.inject({
1189
+ method: 'GET',
1190
+ url: `/api/team-runs/${teamRun.id}/members/${observer.id}/work-requests`,
1191
+ headers: {
1192
+ 'x-agent-tower-invocation-id': observerInvocation.id,
1193
+ },
1194
+ });
1195
+ expect(observerQueueResponse.statusCode).toBe(200);
1196
+ expect(observerQueueResponse.body).not.toContain('Host user sent private message');
1197
+ await expect(prisma.workRequest.findUnique({
1198
+ where: { id: message.workRequestIds[0] },
1199
+ })).resolves.toMatchObject({
1200
+ requesterType: 'user',
1201
+ requesterMemberId: null,
1202
+ targetMemberId: recipient.id,
1203
+ instruction: 'Host user sent private message',
1204
+ });
1205
+ }
1206
+ finally {
1207
+ await app.close();
1208
+ }
1209
+ });
1210
+ it('normalizes REST system private message senderId without granting sender visibility', async () => {
1211
+ const { workspace, teamRun, members } = await createFixture({
1212
+ memberCount: 3,
1213
+ teamRunMode: 'CONFIRM',
1214
+ });
1215
+ const sender = members[0];
1216
+ const recipient = members[1];
1217
+ const observer = members[2];
1218
+ const senderRequest = await createWorkRequest({
1219
+ teamRunId: teamRun.id,
1220
+ targetMemberId: sender.id,
1221
+ status: 'STARTED',
1222
+ });
1223
+ const senderInvocation = await createRunningInvocation({
1224
+ teamRunId: teamRun.id,
1225
+ workRequestId: senderRequest.id,
1226
+ memberId: sender.id,
1227
+ workspaceId: workspace.id,
1228
+ });
1229
+ const recipientRequest = await createWorkRequest({
1230
+ teamRunId: teamRun.id,
1231
+ targetMemberId: recipient.id,
1232
+ status: 'STARTED',
1233
+ });
1234
+ const recipientInvocation = await createRunningInvocation({
1235
+ teamRunId: teamRun.id,
1236
+ workRequestId: recipientRequest.id,
1237
+ memberId: recipient.id,
1238
+ workspaceId: workspace.id,
1239
+ });
1240
+ const observerRequest = await createWorkRequest({
1241
+ teamRunId: teamRun.id,
1242
+ targetMemberId: observer.id,
1243
+ status: 'STARTED',
1244
+ });
1245
+ const observerInvocation = await createRunningInvocation({
1246
+ teamRunId: teamRun.id,
1247
+ workRequestId: observerRequest.id,
1248
+ memberId: observer.id,
1249
+ workspaceId: workspace.id,
1250
+ });
1251
+ const app = Fastify({ logger: false });
1252
+ try {
1253
+ await app.register(teamRunRoutes, { prefix: '/api' });
1254
+ const response = await app.inject({
1255
+ method: 'POST',
1256
+ url: `/api/team-runs/${teamRun.id}/private-messages`,
1257
+ payload: {
1258
+ content: 'System sent private message',
1259
+ recipientMemberIds: [recipient.id],
1260
+ senderType: 'system',
1261
+ senderId: sender.id,
1262
+ senderInvocationId: senderInvocation.id,
1263
+ },
1264
+ });
1265
+ expect(response.statusCode).toBe(201);
1266
+ const message = response.json();
1267
+ expect(message).toMatchObject({
1268
+ senderType: 'system',
1269
+ senderId: null,
1270
+ senderInvocationId: null,
1271
+ recipientMemberIds: [recipient.id],
1272
+ });
1273
+ expect(message.participantMemberIds).toEqual([recipient.id]);
1274
+ await expect(prisma.roomMessage.findUnique({
1275
+ where: { id: message.id },
1276
+ include: { participants: true },
1277
+ })).resolves.toMatchObject({
1278
+ senderType: 'system',
1279
+ senderId: null,
1280
+ senderInvocationId: null,
1281
+ participants: [
1282
+ expect.objectContaining({
1283
+ memberId: recipient.id,
1284
+ role: 'recipient',
1285
+ }),
1286
+ ],
1287
+ });
1288
+ const hostMessagesResponse = await app.inject({
1289
+ method: 'GET',
1290
+ url: `/api/team-runs/${teamRun.id}/messages`,
1291
+ });
1292
+ expect(hostMessagesResponse.statusCode).toBe(200);
1293
+ expect(hostMessagesResponse.body).toContain('System sent private message');
1294
+ const senderMessagesResponse = await app.inject({
1295
+ method: 'GET',
1296
+ url: `/api/team-runs/${teamRun.id}/messages`,
1297
+ headers: {
1298
+ 'x-agent-tower-invocation-id': senderInvocation.id,
1299
+ },
1300
+ });
1301
+ expect(senderMessagesResponse.statusCode).toBe(200);
1302
+ expect(senderMessagesResponse.body).not.toContain('System sent private message');
1303
+ const recipientMessagesResponse = await app.inject({
1304
+ method: 'GET',
1305
+ url: `/api/team-runs/${teamRun.id}/messages`,
1306
+ headers: {
1307
+ 'x-agent-tower-invocation-id': recipientInvocation.id,
1308
+ },
1309
+ });
1310
+ expect(recipientMessagesResponse.statusCode).toBe(200);
1311
+ expect(recipientMessagesResponse.body).toContain('System sent private message');
1312
+ const observerMessagesResponse = await app.inject({
1313
+ method: 'GET',
1314
+ url: `/api/team-runs/${teamRun.id}/messages`,
1315
+ headers: {
1316
+ 'x-agent-tower-invocation-id': observerInvocation.id,
1317
+ },
1318
+ });
1319
+ expect(observerMessagesResponse.statusCode).toBe(200);
1320
+ expect(observerMessagesResponse.body).not.toContain('System sent private message');
1321
+ await expect(prisma.workRequest.findUnique({
1322
+ where: { id: message.workRequestIds[0] },
1323
+ })).resolves.toMatchObject({
1324
+ requesterType: 'system',
1325
+ requesterMemberId: null,
1326
+ targetMemberId: recipient.id,
1327
+ instruction: 'System sent private message',
1328
+ });
1329
+ }
1330
+ finally {
1331
+ await app.close();
1332
+ }
1333
+ });
1334
+ it('enforces MCP Team Room capabilities for listing and private messages', async () => {
1335
+ const previousEnv = captureTeamRunEnv();
1336
+ const noRead = await createFixture({
1337
+ memberCount: 2,
1338
+ memberCapabilities: [{ readRoom: false }],
1339
+ });
1340
+ const noPost = await createFixture({
1341
+ memberCount: 2,
1342
+ memberCapabilities: [{ postRoomMessage: false }],
1343
+ });
1344
+ const noMention = await createFixture({
1345
+ memberCount: 2,
1346
+ memberCapabilities: [{ mentionMembers: false }],
1347
+ });
1348
+ const noReadRequest = await createWorkRequest({
1349
+ teamRunId: noRead.teamRun.id,
1350
+ targetMemberId: noRead.members[0].id,
1351
+ status: 'STARTED',
1352
+ });
1353
+ const noReadInvocation = await createRunningInvocation({
1354
+ teamRunId: noRead.teamRun.id,
1355
+ workRequestId: noReadRequest.id,
1356
+ memberId: noRead.members[0].id,
1357
+ workspaceId: noRead.workspace.id,
1358
+ });
1359
+ const noPostRequest = await createWorkRequest({
1360
+ teamRunId: noPost.teamRun.id,
1361
+ targetMemberId: noPost.members[0].id,
1362
+ status: 'STARTED',
1363
+ });
1364
+ const noPostInvocation = await createRunningInvocation({
1365
+ teamRunId: noPost.teamRun.id,
1366
+ workRequestId: noPostRequest.id,
1367
+ memberId: noPost.members[0].id,
1368
+ workspaceId: noPost.workspace.id,
1369
+ });
1370
+ const noMentionRequest = await createWorkRequest({
1371
+ teamRunId: noMention.teamRun.id,
1372
+ targetMemberId: noMention.members[0].id,
1373
+ status: 'STARTED',
1374
+ });
1375
+ const noMentionInvocation = await createRunningInvocation({
1376
+ teamRunId: noMention.teamRun.id,
1377
+ workRequestId: noMentionRequest.id,
1378
+ memberId: noMention.members[0].id,
1379
+ workspaceId: noMention.workspace.id,
1380
+ });
1381
+ const app = Fastify({ logger: false });
1382
+ try {
1383
+ await app.register(teamRunRoutes, { prefix: '/api' });
1384
+ await app.listen({ port: 0, host: '127.0.0.1' });
1385
+ const address = app.server.address();
1386
+ if (!address || typeof address === 'string') {
1387
+ throw new Error('Failed to start test server');
1388
+ }
1389
+ const baseUrl = `http://127.0.0.1:${address.port}`;
1390
+ async function callToolForCurrentMember(fixture, invocationId, toolCall) {
1391
+ setTeamRunEnv({
1392
+ AGENT_TOWER_TEAM_RUN_ID: fixture.teamRun.id,
1393
+ AGENT_TOWER_MEMBER_ID: fixture.members[0].id,
1394
+ AGENT_TOWER_INVOCATION_ID: invocationId,
1395
+ AGENT_TOWER_SESSION_ID: undefined,
1396
+ });
1397
+ const server = await createMcpServer(baseUrl);
1398
+ const client = new Client({ name: `${toolCall.name}-capability-test-client`, version: '0.1.0' });
1399
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1400
+ await server.connect(serverTransport);
1401
+ await client.connect(clientTransport);
1402
+ try {
1403
+ return await client.callTool(toolCall);
1404
+ }
1405
+ finally {
1406
+ await client.close();
1407
+ await server.close();
1408
+ }
1409
+ }
1410
+ const listResult = await callToolForCurrentMember(noRead, noReadInvocation.id, {
1411
+ name: 'list_room_messages',
1412
+ arguments: {},
1413
+ });
1414
+ expect(listResult.isError).toBe(true);
1415
+ expect(getMcpToolText(listResult)).toContain('readRoom');
1416
+ const noPostResult = await callToolForCurrentMember(noPost, noPostInvocation.id, {
1417
+ name: 'post_private_message',
1418
+ arguments: {
1419
+ recipient_member_ids: [noPost.members[1].id],
1420
+ content: 'Cannot post without postRoomMessage',
1421
+ },
1422
+ });
1423
+ expect(noPostResult.isError).toBe(true);
1424
+ expect(getMcpToolText(noPostResult)).toContain('postRoomMessage');
1425
+ const noMentionResult = await callToolForCurrentMember(noMention, noMentionInvocation.id, {
1426
+ name: 'post_private_message',
1427
+ arguments: {
1428
+ recipient_member_ids: [noMention.members[1].id],
1429
+ content: 'Cannot trigger private work without mentionMembers',
1430
+ },
1431
+ });
1432
+ expect(noMentionResult.isError).toBe(true);
1433
+ expect(getMcpToolText(noMentionResult)).toContain('mentionMembers');
1434
+ await expect(prisma.roomMessage.count({
1435
+ where: {
1436
+ content: {
1437
+ in: [
1438
+ 'Cannot post without postRoomMessage',
1439
+ 'Cannot trigger private work without mentionMembers',
1440
+ ],
1441
+ },
1442
+ },
1443
+ })).resolves.toBe(0);
1444
+ }
1445
+ finally {
1446
+ restoreTeamRunEnv(previousEnv);
1447
+ await app.close();
1448
+ }
1449
+ });
964
1450
  it('lists and cancels current member queued WorkRequests through MCP', async () => {
965
1451
  const previousEnv = captureTeamRunEnv();
966
1452
  const { teamRun, members } = await createFixture({ memberCount: 2 });