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.
- package/dist/executors/__tests__/codex.executor.test.js +21 -0
- package/dist/executors/__tests__/codex.executor.test.js.map +1 -1
- package/dist/executors/codex.executor.d.ts.map +1 -1
- package/dist/executors/codex.executor.js +10 -4
- package/dist/executors/codex.executor.js.map +1 -1
- package/dist/mcp/http-client.d.ts +13 -0
- package/dist/mcp/http-client.d.ts.map +1 -1
- package/dist/mcp/http-client.js +10 -2
- package/dist/mcp/http-client.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +55 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/output/claude-code-parser.d.ts +5 -0
- package/dist/output/claude-code-parser.d.ts.map +1 -1
- package/dist/output/claude-code-parser.js +35 -0
- package/dist/output/claude-code-parser.js.map +1 -1
- package/dist/output/codex-parser.d.ts +4 -0
- package/dist/output/codex-parser.d.ts.map +1 -1
- package/dist/output/codex-parser.js +22 -1
- package/dist/output/codex-parser.js.map +1 -1
- package/dist/output/cursor-agent-parser.d.ts +8 -0
- package/dist/output/cursor-agent-parser.d.ts.map +1 -1
- package/dist/output/cursor-agent-parser.js +52 -1
- package/dist/output/cursor-agent-parser.js.map +1 -1
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +99 -7
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/services/__tests__/team-reconciler.service.test.js +503 -17
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +163 -0
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/team-run.service.d.ts +29 -7
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +285 -21
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/web/assets/{AgentDemoPage-RIqT-dqT.js → AgentDemoPage-CEWSskKz.js} +1 -1
- package/dist/web/assets/{DemoPage-BVfTsgtm.js → DemoPage-DVt_gGQE.js} +1 -1
- package/dist/web/assets/{GeneralSettingsPage-CTA0DqKP.js → GeneralSettingsPage-B7DbUzT6.js} +1 -1
- package/dist/web/assets/{MemberAvatar-Dczz8ZdX.js → MemberAvatar--HnaCo5f.js} +1 -1
- package/dist/web/assets/{NotificationSettingsPage-Cv97qVtx.js → NotificationSettingsPage-BklUEhB2.js} +1 -1
- package/dist/web/assets/{ProfileSettingsPage-Bksw5g8X.js → ProfileSettingsPage-Be7yYwbc.js} +1 -1
- package/dist/web/assets/{ProjectKanbanPage-CWBhrade.js → ProjectKanbanPage-BZR3HX3_.js} +33 -33
- package/dist/web/assets/{ProjectSettingsPage-dgew6R0n.js → ProjectSettingsPage-Cj1h7G2-.js} +1 -1
- package/dist/web/assets/{ProviderSettingsPage-GLiNqGDi.js → ProviderSettingsPage-l244dDeI.js} +1 -1
- package/dist/web/assets/{TeamSettingsPage-fZ85oI46.js → TeamSettingsPage-CXp-7kqb.js} +1 -1
- package/dist/web/assets/{button-B_QmdIDk.js → button-Cc_0rVdG.js} +1 -1
- package/dist/web/assets/{chevron-down-yS2R7lcs.js → chevron-down-DAto5uxR.js} +1 -1
- package/dist/web/assets/{chevron-right-Cp6F3c0z.js → chevron-right-fcp8yUcf.js} +1 -1
- package/dist/web/assets/{chevron-up-Cafj4Xkn.js → chevron-up-BkPAyFXE.js} +1 -1
- package/dist/web/assets/{circle-check-BMVglRBw.js → circle-check-BmWaxH2e.js} +1 -1
- package/dist/web/assets/{code-block-OCS4YCEC-4uQ4wv9S.js → code-block-OCS4YCEC-U8rjjKMO.js} +1 -1
- package/dist/web/assets/{confirm-dialog-8V0YJykz.js → confirm-dialog-D0R59Lv2.js} +1 -1
- package/dist/web/assets/{folder-picker-CmybTC3i.js → folder-picker-COVJdb7m.js} +1 -1
- package/dist/web/assets/{index-C_HCNd7x.js → index-BCz-PlSG.js} +6 -6
- package/dist/web/assets/index-BjIvVVfi.css +1 -0
- package/dist/web/assets/{loader-circle-Dlg-qBpd.js → loader-circle-B0dxeUg4.js} +1 -1
- package/dist/web/assets/{mermaid-NOHMQCX5-DcBcvySI.js → mermaid-NOHMQCX5-CfVlHQvQ.js} +8 -8
- package/dist/web/assets/{modal-e3cMi6GX.js → modal-kzH1vcBa.js} +1 -1
- package/dist/web/assets/{pencil-DK53PbzO.js → pencil-DvjVNKIw.js} +1 -1
- package/dist/web/assets/{select-BOd6A0P8.js → select-Xt2WadPY.js} +1 -1
- package/dist/web/assets/{upload-DkM7kVs8.js → upload-dtpXWuCN.js} +1 -1
- package/dist/web/assets/{use-profiles-3teW4L61.js → use-profiles-oZ63gfvO.js} +1 -1
- package/dist/web/assets/{use-providers-CZGguLO6.js → use-providers-BVrApYJA.js} +1 -1
- package/dist/web/index.html +2 -2
- package/node_modules/@agent-tower/shared/dist/types.d.ts +17 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/node_modules/@prisma/client/.prisma/client/edge.js +17 -7
- package/node_modules/@prisma/client/.prisma/client/index-browser.js +11 -0
- package/node_modules/@prisma/client/.prisma/client/index.d.ts +1983 -154
- package/node_modules/@prisma/client/.prisma/client/index.js +17 -7
- package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
- package/node_modules/@prisma/client/.prisma/client/schema.prisma +41 -21
- package/node_modules/@prisma/client/.prisma/client/wasm.js +11 -0
- package/package.json +1 -1
- package/prisma/migrations/20260602000000_add_team_run_private_messages/migration.sql +29 -0
- package/prisma/schema.prisma +20 -0
- 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(
|
|
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
|
|
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
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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 });
|