@xen-orchestra/rest-api 0.31.1 → 0.33.0
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/README.md +4 -4
- package/dist/acl-privileges/acl-privilege.controller.mjs +7 -2
- package/dist/acl-roles/acl-role.controller.mjs +13 -2
- package/dist/alarms/alarm.controller.mjs +3 -1
- package/dist/backup-archives/backup-archive.controller.mjs +3 -1
- package/dist/backup-jobs/backup-job.controller.mjs +12 -1
- package/dist/backup-logs/backup-log.controller.mjs +3 -1
- package/dist/backup-repositories/backup-repositories.controller.mjs +3 -1
- package/dist/events/event.controller.mjs +4 -1
- package/dist/groups/group.controller.mjs +10 -1
- package/dist/hosts/host.controller.mjs +16 -1
- package/dist/index.mjs +2 -0
- package/dist/mcp/mcp.controller.mjs +59 -0
- package/dist/mcp/mcp.helper.mjs +11 -0
- package/dist/messages/message.controller.mjs +3 -1
- package/dist/middlewares/mcp-gate.middleware.mjs +30 -0
- package/dist/networks/network.controller.mjs +9 -1
- package/dist/open-api/routes/routes.js +118 -5
- package/dist/pbds/pbd.controller.mjs +5 -1
- package/dist/pcis/pci.controller.mjs +3 -1
- package/dist/pgpus/pgpu.controller.mjs +3 -1
- package/dist/pifs/pif.controller.mjs +6 -1
- package/dist/pools/pool.controller.mjs +20 -1
- package/dist/proxies/proxy.controller.mjs +3 -1
- package/dist/restore-logs/restore-log.controller.mjs +5 -1
- package/dist/schedules/schedule.controller.mjs +4 -1
- package/dist/servers/server.controller.mjs +8 -1
- package/dist/sms/sm.controller.mjs +3 -1
- package/dist/srs/sr.controller.mjs +13 -1
- package/dist/tasks/task.controller.mjs +6 -1
- package/dist/users/user.controller.mjs +13 -2
- package/dist/vbds/vbd.controller.mjs +10 -1
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +10 -1
- package/dist/vdis/vdi.controller.mjs +13 -1
- package/dist/vifs/vif.controller.mjs +10 -1
- package/dist/vm-controller/vm-controller.controller.mjs +9 -1
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +11 -1
- package/dist/vm-templates/vm-template.controller.mjs +11 -1
- package/dist/vms/vm.controller.mjs +77 -2
- package/dist/vms/vm.service.mjs +17 -0
- package/dist/xoa/xoa.controller.mjs +4 -1
- package/eslint-rules/index.cjs +7 -0
- package/eslint-rules/require-mcp-expose.cjs +129 -0
- package/open-api/spec/swagger.json +1764 -495
- package/package.json +3 -3
- package/tsoa.json +2 -1
|
@@ -7,13 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Example, Get, Path, Post, Query, Request, Response, Route, Security,
|
|
10
|
+
import { Body, Delete, Example, Extension, Get, Middlewares, Patch, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
|
|
11
11
|
import { json } from 'express';
|
|
12
12
|
import { inject } from 'inversify';
|
|
13
13
|
import { incorrectState, invalidParameters } from 'xo-common/api-errors.js';
|
|
14
14
|
import { provide } from 'inversify-binding-decorators';
|
|
15
15
|
import { PassThrough } from 'node:stream';
|
|
16
|
-
import {
|
|
16
|
+
import { SUPPORTED_ACTIONS_BY_RESOURCE } from '@xen-orchestra/acl';
|
|
17
|
+
import { acl, actionsFromBody } from '../middlewares/acl.middleware.mjs';
|
|
17
18
|
import { asynchronousActionResp, badRequestResp, createdResp, forbiddenOperationResp, incorrectStateResp, internalServerErrorResp, invalidParameters as invalidParametersResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
|
|
18
19
|
import { BASE_URL } from '../index.mjs';
|
|
19
20
|
import { limitAndFilterArray, NDJSON_CONTENT_TYPE } from '../helpers/utils.helper.mjs';
|
|
@@ -28,6 +29,11 @@ import { partialVmBackupJobs, vmBackupJobIds } from '../open-api/oa-examples/bac
|
|
|
28
29
|
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
|
|
29
30
|
import { Task } from '@vates/task';
|
|
30
31
|
const IGNORED_VDIS_TAG = '[NOSNAP]';
|
|
32
|
+
// `datasources` is managed through the dedicated `/vms/{id}/stats/data_source`
|
|
33
|
+
// endpoints, not as a direct VM property, so it cannot be updated via PATCH /vms.
|
|
34
|
+
const UPDATE_VM_ACTIONS = Object.keys(SUPPORTED_ACTIONS_BY_RESOURCE.vm.update)
|
|
35
|
+
.filter(action => action !== 'datasources')
|
|
36
|
+
.map(k => `update:${k}`);
|
|
31
37
|
let VmController = class VmController extends XapiXoController {
|
|
32
38
|
#vmService;
|
|
33
39
|
#backupJobService;
|
|
@@ -74,6 +80,29 @@ let VmController = class VmController extends XapiXoController {
|
|
|
74
80
|
getVm(id) {
|
|
75
81
|
return this.getObject(id);
|
|
76
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Partial update of a VM. Only the fields present in the body are modified;
|
|
85
|
+
* everything else is left untouched.
|
|
86
|
+
*
|
|
87
|
+
* Operations are applied sequentially: if one fails, previously applied
|
|
88
|
+
* changes are not rolled back.
|
|
89
|
+
*
|
|
90
|
+
* Required privilege per field provided in the body:
|
|
91
|
+
* - resource: vm, action: update:<field> (e.g. update:nameLabel, update:cpus, ...)
|
|
92
|
+
*
|
|
93
|
+
* Special fields:
|
|
94
|
+
* - `xenStoreData` keys are automatically prefixed with `vm-data/` when missing
|
|
95
|
+
*
|
|
96
|
+
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
|
|
97
|
+
* @example body {
|
|
98
|
+
* "nameLabel": "web-prod-01",
|
|
99
|
+
* "nameDescription": "Production web frontend — managed by n8n",
|
|
100
|
+
* "notes": "Docker containers: nginx, app-1, app-2"
|
|
101
|
+
* }
|
|
102
|
+
*/
|
|
103
|
+
async updateVm(id, body) {
|
|
104
|
+
await this.#vmService.updateVm(id, body);
|
|
105
|
+
}
|
|
77
106
|
/**
|
|
78
107
|
* Required privilege:
|
|
79
108
|
* - resource: vm, action: delete
|
|
@@ -631,6 +660,7 @@ let VmController = class VmController extends XapiXoController {
|
|
|
631
660
|
__decorate([
|
|
632
661
|
Example(vmIds),
|
|
633
662
|
Example(partialVms),
|
|
663
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
634
664
|
Get(''),
|
|
635
665
|
Security('*', ['acl']),
|
|
636
666
|
__param(0, Request()),
|
|
@@ -641,6 +671,7 @@ __decorate([
|
|
|
641
671
|
__param(5, Query())
|
|
642
672
|
], VmController.prototype, "getVms", null);
|
|
643
673
|
__decorate([
|
|
674
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
644
675
|
Get('{id}.{format}'),
|
|
645
676
|
Middlewares(acl({ resource: 'vm', action: 'export', objectId: 'params.id' })),
|
|
646
677
|
SuccessResponse(200, 'Download started', 'application/octet-stream'),
|
|
@@ -654,6 +685,7 @@ __decorate([
|
|
|
654
685
|
], VmController.prototype, "exportVm", null);
|
|
655
686
|
__decorate([
|
|
656
687
|
Example(vm),
|
|
688
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
657
689
|
Get('{id}'),
|
|
658
690
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
659
691
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -661,6 +693,25 @@ __decorate([
|
|
|
661
693
|
__param(0, Path())
|
|
662
694
|
], VmController.prototype, "getVm", null);
|
|
663
695
|
__decorate([
|
|
696
|
+
Patch('{id}'),
|
|
697
|
+
Middlewares([
|
|
698
|
+
json(),
|
|
699
|
+
acl({
|
|
700
|
+
resource: 'vm',
|
|
701
|
+
actions: actionsFromBody(UPDATE_VM_ACTIONS),
|
|
702
|
+
objectId: 'params.id',
|
|
703
|
+
}),
|
|
704
|
+
]),
|
|
705
|
+
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
706
|
+
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
707
|
+
Response(notFoundResp.status, notFoundResp.description),
|
|
708
|
+
Response(invalidParametersResp.status, invalidParametersResp.description),
|
|
709
|
+
Response(internalServerErrorResp.status, internalServerErrorResp.description),
|
|
710
|
+
__param(0, Path()),
|
|
711
|
+
__param(1, Body())
|
|
712
|
+
], VmController.prototype, "updateVm", null);
|
|
713
|
+
__decorate([
|
|
714
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
664
715
|
Delete('{id}'),
|
|
665
716
|
Middlewares(acl({ resource: 'vm', action: 'delete', objectId: 'params.id' })),
|
|
666
717
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -671,6 +722,7 @@ __decorate([
|
|
|
671
722
|
], VmController.prototype, "deleteVm", null);
|
|
672
723
|
__decorate([
|
|
673
724
|
Example(vmStatsExample),
|
|
725
|
+
Extension('x-mcp-exposure', 'deny'),
|
|
674
726
|
Get('{id}/stats'),
|
|
675
727
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
676
728
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -680,6 +732,7 @@ __decorate([
|
|
|
680
732
|
__param(1, Query())
|
|
681
733
|
], VmController.prototype, "getVmStats", null);
|
|
682
734
|
__decorate([
|
|
735
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
683
736
|
Put('{id}/stats/data_source/{data_source}'),
|
|
684
737
|
Middlewares(acl({ resource: 'vm', action: 'update:datasources', objectId: 'params.id' })),
|
|
685
738
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -690,6 +743,7 @@ __decorate([
|
|
|
690
743
|
__param(1, Path('data_source'))
|
|
691
744
|
], VmController.prototype, "addDataSource", null);
|
|
692
745
|
__decorate([
|
|
746
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
693
747
|
Delete('{id}/stats/data_source/{data_source}'),
|
|
694
748
|
Middlewares(acl({ resource: 'vm', action: 'update:datasources', objectId: 'params.id' })),
|
|
695
749
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -701,6 +755,7 @@ __decorate([
|
|
|
701
755
|
], VmController.prototype, "deleteDataSource", null);
|
|
702
756
|
__decorate([
|
|
703
757
|
Example(taskLocation),
|
|
758
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
704
759
|
Post('{id}/actions/start'),
|
|
705
760
|
Middlewares([
|
|
706
761
|
json(),
|
|
@@ -720,6 +775,7 @@ __decorate([
|
|
|
720
775
|
], VmController.prototype, "startVm", null);
|
|
721
776
|
__decorate([
|
|
722
777
|
Example(taskLocation),
|
|
778
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
723
779
|
Post('{id}/actions/clean_shutdown'),
|
|
724
780
|
Middlewares(acl({ resource: 'vm', action: 'shutdown:clean', objectId: 'params.id' })),
|
|
725
781
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -732,6 +788,7 @@ __decorate([
|
|
|
732
788
|
], VmController.prototype, "cleanShutdownVm", null);
|
|
733
789
|
__decorate([
|
|
734
790
|
Example(taskLocation),
|
|
791
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
735
792
|
Post('{id}/actions/clean_reboot'),
|
|
736
793
|
Middlewares(acl({ resource: 'vm', action: 'reboot:clean', objectId: 'params.id' })),
|
|
737
794
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -743,6 +800,7 @@ __decorate([
|
|
|
743
800
|
], VmController.prototype, "cleanRebootVm", null);
|
|
744
801
|
__decorate([
|
|
745
802
|
Example(taskLocation),
|
|
803
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
746
804
|
Post('{id}/actions/hard_shutdown'),
|
|
747
805
|
Middlewares(acl({ resource: 'vm', action: 'shutdown:hard', objectId: 'params.id' })),
|
|
748
806
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -755,6 +813,7 @@ __decorate([
|
|
|
755
813
|
], VmController.prototype, "hardShutdownVm", null);
|
|
756
814
|
__decorate([
|
|
757
815
|
Example(taskLocation),
|
|
816
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
758
817
|
Post('{id}/actions/hard_reboot'),
|
|
759
818
|
Middlewares(acl({ resource: 'vm', action: 'reboot:hard', objectId: 'params.id' })),
|
|
760
819
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -767,6 +826,7 @@ __decorate([
|
|
|
767
826
|
], VmController.prototype, "hardRebootVm", null);
|
|
768
827
|
__decorate([
|
|
769
828
|
Example(taskLocation),
|
|
829
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
770
830
|
Post('{id}/actions/pause'),
|
|
771
831
|
Middlewares(acl({ resource: 'vm', action: 'pause', objectId: 'params.id' })),
|
|
772
832
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -779,6 +839,7 @@ __decorate([
|
|
|
779
839
|
], VmController.prototype, "pauseVm", null);
|
|
780
840
|
__decorate([
|
|
781
841
|
Example(taskLocation),
|
|
842
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
782
843
|
Post('{id}/actions/suspend'),
|
|
783
844
|
Middlewares(acl({ resource: 'vm', action: 'suspend', objectId: 'params.id' })),
|
|
784
845
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -791,6 +852,7 @@ __decorate([
|
|
|
791
852
|
], VmController.prototype, "suspendVm", null);
|
|
792
853
|
__decorate([
|
|
793
854
|
Example(taskLocation),
|
|
855
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
794
856
|
Post('{id}/actions/resume'),
|
|
795
857
|
Middlewares(acl({ resource: 'vm', action: 'resume', objectId: 'params.id' })),
|
|
796
858
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -803,6 +865,7 @@ __decorate([
|
|
|
803
865
|
], VmController.prototype, "resumeVm", null);
|
|
804
866
|
__decorate([
|
|
805
867
|
Example(taskLocation),
|
|
868
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
806
869
|
Post('{id}/actions/unpause'),
|
|
807
870
|
Middlewares(acl({ resource: 'vm', action: 'unpause', objectId: 'params.id' })),
|
|
808
871
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -815,6 +878,7 @@ __decorate([
|
|
|
815
878
|
], VmController.prototype, "unpauseVm", null);
|
|
816
879
|
__decorate([
|
|
817
880
|
Example(taskLocation),
|
|
881
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
818
882
|
Post('{id}/actions/revert_snapshot'),
|
|
819
883
|
Middlewares([
|
|
820
884
|
json(),
|
|
@@ -839,6 +903,7 @@ __decorate([
|
|
|
839
903
|
], VmController.prototype, "revertSnapshotVm", null);
|
|
840
904
|
__decorate([
|
|
841
905
|
Example(taskLocation),
|
|
906
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
842
907
|
Post('{id}/actions/snapshot'),
|
|
843
908
|
Middlewares([json(), acl({ resource: 'vm', action: 'snapshot', objectId: 'params.id' })]),
|
|
844
909
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -852,6 +917,7 @@ __decorate([
|
|
|
852
917
|
], VmController.prototype, "snapshotVm", null);
|
|
853
918
|
__decorate([
|
|
854
919
|
Example(taskLocation),
|
|
920
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
855
921
|
Post('{id}/actions/clone'),
|
|
856
922
|
Middlewares(json()),
|
|
857
923
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
|
@@ -865,6 +931,7 @@ __decorate([
|
|
|
865
931
|
], VmController.prototype, "cloneVm", null);
|
|
866
932
|
__decorate([
|
|
867
933
|
Example(genericAlarmsExample),
|
|
934
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
868
935
|
Get('{id}/alarms'),
|
|
869
936
|
Security('*', ['acl']),
|
|
870
937
|
Tags('alarms'),
|
|
@@ -879,6 +946,7 @@ __decorate([
|
|
|
879
946
|
], VmController.prototype, "getVmAlarms", null);
|
|
880
947
|
__decorate([
|
|
881
948
|
Example(vmVdis),
|
|
949
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
882
950
|
Get('{id}/vdis'),
|
|
883
951
|
Security('*', ['acl']),
|
|
884
952
|
Tags('vdis'),
|
|
@@ -894,6 +962,7 @@ __decorate([
|
|
|
894
962
|
__decorate([
|
|
895
963
|
Example(vmBackupJobIds),
|
|
896
964
|
Example(partialVmBackupJobs),
|
|
965
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
897
966
|
Get('{id}/backup-jobs'),
|
|
898
967
|
Security('*', ['acl']),
|
|
899
968
|
Tags('backup-jobs'),
|
|
@@ -909,6 +978,7 @@ __decorate([
|
|
|
909
978
|
__decorate([
|
|
910
979
|
Example(messageIds),
|
|
911
980
|
Example(partialMessages),
|
|
981
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
912
982
|
Get('{id}/messages'),
|
|
913
983
|
Security('*', ['acl']),
|
|
914
984
|
Tags('messages'),
|
|
@@ -924,6 +994,7 @@ __decorate([
|
|
|
924
994
|
__decorate([
|
|
925
995
|
Example(taskIds),
|
|
926
996
|
Example(partialTasks),
|
|
997
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
927
998
|
Get('{id}/tasks'),
|
|
928
999
|
Security('*', ['acl']),
|
|
929
1000
|
Tags('tasks'),
|
|
@@ -937,6 +1008,7 @@ __decorate([
|
|
|
937
1008
|
__param(6, Query())
|
|
938
1009
|
], VmController.prototype, "getVmTasks", null);
|
|
939
1010
|
__decorate([
|
|
1011
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
940
1012
|
Put('{id}/tags/{tag}'),
|
|
941
1013
|
Middlewares(acl({ resource: 'vm', action: 'update:tags', objectId: 'params.id' })),
|
|
942
1014
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -946,6 +1018,7 @@ __decorate([
|
|
|
946
1018
|
__param(1, Path())
|
|
947
1019
|
], VmController.prototype, "putVmTag", null);
|
|
948
1020
|
__decorate([
|
|
1021
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
949
1022
|
Delete('{id}/tags/{tag}'),
|
|
950
1023
|
Middlewares(acl({ resource: 'vm', action: 'update:tags', objectId: 'params.id' })),
|
|
951
1024
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -956,6 +1029,7 @@ __decorate([
|
|
|
956
1029
|
], VmController.prototype, "deleteVmTag", null);
|
|
957
1030
|
__decorate([
|
|
958
1031
|
Example(vmDashboard),
|
|
1032
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
959
1033
|
Get('{id}/dashboard'),
|
|
960
1034
|
Middlewares(acl({ resource: 'vm', action: 'read', objectId: 'params.id' })),
|
|
961
1035
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -965,6 +1039,7 @@ __decorate([
|
|
|
965
1039
|
], VmController.prototype, "getVmDashboard", null);
|
|
966
1040
|
__decorate([
|
|
967
1041
|
Example(taskLocation),
|
|
1042
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
968
1043
|
Post('{id}/actions/migrate'),
|
|
969
1044
|
Middlewares(json()),
|
|
970
1045
|
SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description),
|
package/dist/vms/vm.service.mjs
CHANGED
|
@@ -146,6 +146,23 @@ export class VmService {
|
|
|
146
146
|
});
|
|
147
147
|
return alarms;
|
|
148
148
|
}
|
|
149
|
+
async updateVm(id, body) {
|
|
150
|
+
const { resourceSet, share, ...editProps } = body;
|
|
151
|
+
// Touch the object so 404 is raised before any side effect.
|
|
152
|
+
void this.#restApi.getObject(id, 'VM');
|
|
153
|
+
const xoApp = this.#restApi.xoApp;
|
|
154
|
+
if (resourceSet !== undefined) {
|
|
155
|
+
await xoApp.setVmResourceSet(id, resourceSet, true);
|
|
156
|
+
}
|
|
157
|
+
else if (share) {
|
|
158
|
+
// `share: false` is a no-op.
|
|
159
|
+
await xoApp.shareVmResourceSet(id);
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(editProps).length === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await xoApp.getXapi(id).editVm(id, editProps);
|
|
165
|
+
}
|
|
149
166
|
#getDashboardQuickInfo(id) {
|
|
150
167
|
const { power_state, uuid, name_description, CPUs, mainIpAddress, os_version, memory, creation, $pool, virtualizationMode, tags, $container, startTime, pvDriversDetected, pvDriversVersion, pvDriversUpToDate, } = this.#restApi.getObject(id, 'VM');
|
|
151
168
|
return {
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { Controller, Example, Get, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
10
|
+
import { Controller, Example, Extension, Get, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
11
11
|
import { inject } from 'inversify';
|
|
12
12
|
import { PassThrough } from 'node:stream';
|
|
13
13
|
import { provide } from 'inversify-binding-decorators';
|
|
@@ -58,6 +58,7 @@ let XoaController = class XoaController extends Controller {
|
|
|
58
58
|
};
|
|
59
59
|
__decorate([
|
|
60
60
|
Example(xoaDashboard),
|
|
61
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
61
62
|
Get('dashboard'),
|
|
62
63
|
Response(badRequestResp.status, badRequestResp.description),
|
|
63
64
|
Response(unauthorizedResp.status, unauthorizedResp.description),
|
|
@@ -67,11 +68,13 @@ __decorate([
|
|
|
67
68
|
__decorate([
|
|
68
69
|
Security('none'),
|
|
69
70
|
Example(pingResponse),
|
|
71
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
70
72
|
Get('ping')
|
|
71
73
|
], XoaController.prototype, "ping", null);
|
|
72
74
|
__decorate([
|
|
73
75
|
Security('none'),
|
|
74
76
|
Example(guiRoutes),
|
|
77
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
75
78
|
Get('gui-routes')
|
|
76
79
|
], XoaController.prototype, "getGuiRoutes", null);
|
|
77
80
|
XoaController = __decorate([
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Require `@Extension('x-mcp-exposure', 'allow' | 'confirm' | 'deny')` next to
|
|
5
|
+
* every `@Get/@Post/@Put/@Patch/@Delete` method. tsoa detects extension
|
|
6
|
+
* decorators by AST identifier name, so the `Extension` literal must appear in
|
|
7
|
+
* source — a `@McpExpose` wrapper would not be picked up.
|
|
8
|
+
*
|
|
9
|
+
* Scope: this is a SURFACE-CONTROL policy, not a security control. The value
|
|
10
|
+
* only decides which endpoints the official `@xen-orchestra/mcp` server turns
|
|
11
|
+
* into LLM tools; the REST API itself never reads `x-mcp-exposure`. Every
|
|
12
|
+
* endpoint stays gated by its own RBAC/ACL middleware regardless of this
|
|
13
|
+
* annotation, and any REST client with valid credentials (including a
|
|
14
|
+
* non-official MCP) can still call a `'deny'`-tagged endpoint directly. The
|
|
15
|
+
* rule only guarantees the exposure decision is made explicitly per endpoint —
|
|
16
|
+
* it does not harden the REST API.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HTTP_METHOD_DECORATORS = new Set(['Get', 'Post', 'Put', 'Patch', 'Delete'])
|
|
20
|
+
const MCP_EXPOSURE_KEY = 'x-mcp-exposure'
|
|
21
|
+
const VALID_EXPOSURES = new Set(['allow', 'confirm', 'deny'])
|
|
22
|
+
|
|
23
|
+
function getDecoratorCalleeName(decorator) {
|
|
24
|
+
const expr = decorator.expression
|
|
25
|
+
if (!expr) return undefined
|
|
26
|
+
if (expr.type === 'CallExpression' && expr.callee && expr.callee.type === 'Identifier') {
|
|
27
|
+
return expr.callee.name
|
|
28
|
+
}
|
|
29
|
+
if (expr.type === 'Identifier') {
|
|
30
|
+
return expr.name
|
|
31
|
+
}
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findHttpMethodDecorator(decorators) {
|
|
36
|
+
for (const decorator of decorators) {
|
|
37
|
+
const name = getDecoratorCalleeName(decorator)
|
|
38
|
+
if (name !== undefined && HTTP_METHOD_DECORATORS.has(name)) {
|
|
39
|
+
return { decorator, name }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStringLiteralValue(node) {
|
|
46
|
+
if (!node) return undefined
|
|
47
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
48
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
49
|
+
return node.quasis[0].value.cooked
|
|
50
|
+
}
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findMcpExposureExtension(decorators) {
|
|
55
|
+
for (const decorator of decorators) {
|
|
56
|
+
if (!decorator.expression || decorator.expression.type !== 'CallExpression') continue
|
|
57
|
+
const callee = decorator.expression.callee
|
|
58
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'Extension') continue
|
|
59
|
+
|
|
60
|
+
const [keyArg, valueArg] = decorator.expression.arguments
|
|
61
|
+
const key = getStringLiteralValue(keyArg)
|
|
62
|
+
if (key !== MCP_EXPOSURE_KEY) continue
|
|
63
|
+
|
|
64
|
+
return { decorator, valueArg }
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMethodName(methodNode) {
|
|
70
|
+
if (!methodNode.key) return '<anonymous>'
|
|
71
|
+
if (methodNode.key.type === 'Identifier') return methodNode.key.name
|
|
72
|
+
if (methodNode.key.type === 'Literal') return String(methodNode.key.value)
|
|
73
|
+
return '<computed>'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
meta: {
|
|
78
|
+
type: 'problem',
|
|
79
|
+
docs: {
|
|
80
|
+
description:
|
|
81
|
+
'Every REST endpoint must declare its MCP exposure with @Extension("x-mcp-exposure", "allow" | "confirm" | "deny").',
|
|
82
|
+
},
|
|
83
|
+
schema: [],
|
|
84
|
+
messages: {
|
|
85
|
+
missingMcpExpose:
|
|
86
|
+
'Endpoint "{{name}}" decorated with @{{httpMethod}} is missing an @Extension("x-mcp-exposure", ...) decorator. ' +
|
|
87
|
+
'Add @Extension("x-mcp-exposure", "allow"), "confirm", or "deny" to make the MCP exposure decision explicit.',
|
|
88
|
+
invalidMcpExpose:
|
|
89
|
+
'Endpoint "{{name}}" has @Extension("x-mcp-exposure", ...) with an invalid value. ' +
|
|
90
|
+
'Expected one of: "allow", "confirm", "deny". Got: {{got}}.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
create(context) {
|
|
94
|
+
return {
|
|
95
|
+
MethodDefinition(node) {
|
|
96
|
+
if (node.kind === 'constructor') return
|
|
97
|
+
const decorators = node.decorators
|
|
98
|
+
if (!decorators || decorators.length === 0) return
|
|
99
|
+
|
|
100
|
+
const httpMethod = findHttpMethodDecorator(decorators)
|
|
101
|
+
if (!httpMethod) return
|
|
102
|
+
|
|
103
|
+
const methodName = getMethodName(node)
|
|
104
|
+
const mcpExposure = findMcpExposureExtension(decorators)
|
|
105
|
+
|
|
106
|
+
if (!mcpExposure) {
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
messageId: 'missingMcpExpose',
|
|
110
|
+
data: { name: methodName, httpMethod: httpMethod.name },
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const exposureValue = getStringLiteralValue(mcpExposure.valueArg)
|
|
116
|
+
if (exposureValue === undefined || !VALID_EXPOSURES.has(exposureValue)) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: mcpExposure.valueArg ?? mcpExposure.decorator,
|
|
119
|
+
messageId: 'invalidMcpExpose',
|
|
120
|
+
data: {
|
|
121
|
+
name: methodName,
|
|
122
|
+
got: exposureValue === undefined ? '<non-literal>' : JSON.stringify(exposureValue),
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|