@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.
Files changed (46) hide show
  1. package/README.md +4 -4
  2. package/dist/acl-privileges/acl-privilege.controller.mjs +7 -2
  3. package/dist/acl-roles/acl-role.controller.mjs +13 -2
  4. package/dist/alarms/alarm.controller.mjs +3 -1
  5. package/dist/backup-archives/backup-archive.controller.mjs +3 -1
  6. package/dist/backup-jobs/backup-job.controller.mjs +12 -1
  7. package/dist/backup-logs/backup-log.controller.mjs +3 -1
  8. package/dist/backup-repositories/backup-repositories.controller.mjs +3 -1
  9. package/dist/events/event.controller.mjs +4 -1
  10. package/dist/groups/group.controller.mjs +10 -1
  11. package/dist/hosts/host.controller.mjs +16 -1
  12. package/dist/index.mjs +2 -0
  13. package/dist/mcp/mcp.controller.mjs +59 -0
  14. package/dist/mcp/mcp.helper.mjs +11 -0
  15. package/dist/messages/message.controller.mjs +3 -1
  16. package/dist/middlewares/mcp-gate.middleware.mjs +30 -0
  17. package/dist/networks/network.controller.mjs +9 -1
  18. package/dist/open-api/routes/routes.js +118 -5
  19. package/dist/pbds/pbd.controller.mjs +5 -1
  20. package/dist/pcis/pci.controller.mjs +3 -1
  21. package/dist/pgpus/pgpu.controller.mjs +3 -1
  22. package/dist/pifs/pif.controller.mjs +6 -1
  23. package/dist/pools/pool.controller.mjs +20 -1
  24. package/dist/proxies/proxy.controller.mjs +3 -1
  25. package/dist/restore-logs/restore-log.controller.mjs +5 -1
  26. package/dist/schedules/schedule.controller.mjs +4 -1
  27. package/dist/servers/server.controller.mjs +8 -1
  28. package/dist/sms/sm.controller.mjs +3 -1
  29. package/dist/srs/sr.controller.mjs +13 -1
  30. package/dist/tasks/task.controller.mjs +6 -1
  31. package/dist/users/user.controller.mjs +13 -2
  32. package/dist/vbds/vbd.controller.mjs +10 -1
  33. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +10 -1
  34. package/dist/vdis/vdi.controller.mjs +13 -1
  35. package/dist/vifs/vif.controller.mjs +10 -1
  36. package/dist/vm-controller/vm-controller.controller.mjs +9 -1
  37. package/dist/vm-snapshots/vm-snapshot.controller.mjs +11 -1
  38. package/dist/vm-templates/vm-template.controller.mjs +11 -1
  39. package/dist/vms/vm.controller.mjs +77 -2
  40. package/dist/vms/vm.service.mjs +17 -0
  41. package/dist/xoa/xoa.controller.mjs +4 -1
  42. package/eslint-rules/index.cjs +7 -0
  43. package/eslint-rules/require-mcp-expose.cjs +129 -0
  44. package/open-api/spec/swagger.json +1764 -495
  45. package/package.json +3 -3
  46. package/tsoa.json +2 -1
@@ -0,0 +1,59 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Controller, Example, Get, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
11
+ import { inject } from 'inversify';
12
+ import { provide } from 'inversify-binding-decorators';
13
+ import { ApiError } from '../helpers/error.helper.mjs';
14
+ import { isMcpEnabled, MCP_DISABLED_ERROR } from './mcp.helper.mjs';
15
+ import { RestApi } from '../rest-api/rest-api.mjs';
16
+ const ENABLED_RESPONSE = {
17
+ status: 200,
18
+ description: 'MCP is enabled',
19
+ };
20
+ const DISABLED_RESPONSE = {
21
+ status: 503,
22
+ description: 'MCP is disabled by administrator',
23
+ };
24
+ let McpController = class McpController extends Controller {
25
+ #restApi;
26
+ constructor(restApi) {
27
+ super();
28
+ this.#restApi = restApi;
29
+ }
30
+ /**
31
+ * Returns whether MCP is currently enabled on this XO server.
32
+ *
33
+ * The route is publicly reachable (no authentication required) so the
34
+ * `@xen-orchestra/mcp` binary can check the kill-switch at startup,
35
+ * before any credentials have been configured.
36
+ */
37
+ getMcpStatus() {
38
+ if (!isMcpEnabled(this.#restApi)) {
39
+ throw new ApiError(DISABLED_RESPONSE.description, DISABLED_RESPONSE.status, {
40
+ data: { error: MCP_DISABLED_ERROR },
41
+ });
42
+ }
43
+ return { enabled: true };
44
+ }
45
+ };
46
+ __decorate([
47
+ Security('none'),
48
+ Example({ enabled: true }),
49
+ Get('status'),
50
+ SuccessResponse(ENABLED_RESPONSE.status, ENABLED_RESPONSE.description),
51
+ Response(DISABLED_RESPONSE.status, DISABLED_RESPONSE.description)
52
+ ], McpController.prototype, "getMcpStatus", null);
53
+ McpController = __decorate([
54
+ Route('mcp'),
55
+ Tags('mcp'),
56
+ provide(McpController),
57
+ __param(0, inject(RestApi))
58
+ ], McpController);
59
+ export { McpController };
@@ -0,0 +1,11 @@
1
+ // Single source of truth for reading the MCP kill-switch flag. Defaults to
2
+ // `true` so legacy configs without a `[mcp]` section keep MCP enabled.
3
+ export function isMcpEnabled(restApi) {
4
+ return restApi.xoApp.config.getOptional('mcp.enabled') ?? true;
5
+ }
6
+ // Wire-level identifier used by the kill-switch contract; shared between the
7
+ // gate middleware (`@xen-orchestra/rest-api`) and the controller response.
8
+ export const MCP_DISABLED_ERROR = 'mcp_disabled';
9
+ // Whitelisted by `mcp-gate` so the MCP binary can probe the kill-switch
10
+ // state at boot even when MCP is globally disabled.
11
+ export const MCP_STATUS_PATH = '/mcp/status';
@@ -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 { Example, Get, Middlewares, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
10
+ import { Example, Extension, Get, Middlewares, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
12
  import { noSuchObject } from 'xo-common/api-errors.js';
13
13
  import { provide } from 'inversify-binding-decorators';
@@ -73,6 +73,7 @@ let MessageController = class MessageController extends XapiXoController {
73
73
  __decorate([
74
74
  Example(messageIds),
75
75
  Example(partialMessages),
76
+ Extension('x-mcp-exposure', 'allow'),
76
77
  Get(''),
77
78
  Security('*', ['acl']),
78
79
  __param(0, Request()),
@@ -84,6 +85,7 @@ __decorate([
84
85
  ], MessageController.prototype, "getMessages", null);
85
86
  __decorate([
86
87
  Example(message),
88
+ Extension('x-mcp-exposure', 'allow'),
87
89
  Get('{id}'),
88
90
  Middlewares(acl({
89
91
  resource: 'message',
@@ -0,0 +1,30 @@
1
+ import { ApiError } from '../helpers/error.helper.mjs';
2
+ import { iocContainer } from '../ioc/ioc.mjs';
3
+ import { isMcpEnabled, MCP_DISABLED_ERROR, MCP_STATUS_PATH } from '../mcp/mcp.helper.mjs';
4
+ import { RestApi } from '../rest-api/rest-api.mjs';
5
+ // Identifies requests originating from the `@xen-orchestra/mcp` binary.
6
+ //
7
+ // SECURITY: the header is NOT authenticated — any client can spoof it. Use it
8
+ // only to RESTRICT behaviour (e.g. the MCP kill-switch), never to grant
9
+ // privileges.
10
+ function isMcpRequest(req) {
11
+ // Node lowercases header names; values keep their original case so we
12
+ // normalize before comparing. `Array.isArray` covers the unusual case of
13
+ // a duplicated header value (a malformed or hostile client).
14
+ const header = req.headers['x-xo-client'];
15
+ const value = Array.isArray(header) ? header[0] : header;
16
+ return typeof value === 'string' && value.toLowerCase() === 'mcp';
17
+ }
18
+ export function mcpGateMiddleware(req, _res, next) {
19
+ if (!isMcpRequest(req)) {
20
+ return next();
21
+ }
22
+ // Always let the kill-switch probe through so the MCP binary can fail-fast.
23
+ if (req.path === MCP_STATUS_PATH) {
24
+ return next();
25
+ }
26
+ if (!isMcpEnabled(iocContainer.get(RestApi))) {
27
+ return next(new ApiError('MCP is disabled by administrator', 503, { data: { error: MCP_DISABLED_ERROR } }));
28
+ }
29
+ next();
30
+ }
@@ -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 { Example, Get, Path, Query, Response, Request, Route, Security, Tags, Delete, SuccessResponse, Put, Middlewares, } from 'tsoa';
10
+ import { Delete, Example, Extension, Get, Middlewares, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
12
  import { provide } from 'inversify-binding-decorators';
13
13
  import { AlarmService } from '../alarms/alarm.service.mjs';
@@ -139,6 +139,7 @@ let NetworkController = class NetworkController extends XapiXoController {
139
139
  __decorate([
140
140
  Example(networkIds),
141
141
  Example(partialNetworks),
142
+ Extension('x-mcp-exposure', 'allow'),
142
143
  Get(''),
143
144
  Security('*', ['acl']),
144
145
  __param(0, Request()),
@@ -150,6 +151,7 @@ __decorate([
150
151
  ], NetworkController.prototype, "getNetworks", null);
151
152
  __decorate([
152
153
  Example(network),
154
+ Extension('x-mcp-exposure', 'allow'),
153
155
  Get('{id}'),
154
156
  Middlewares(acl({ resource: 'network', action: 'read', objectId: 'params.id' })),
155
157
  Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
@@ -157,6 +159,7 @@ __decorate([
157
159
  __param(0, Path())
158
160
  ], NetworkController.prototype, "getNetwork", null);
159
161
  __decorate([
162
+ Extension('x-mcp-exposure', 'confirm'),
160
163
  Delete('{id}'),
161
164
  Middlewares(acl({ resource: 'network', action: 'delete', objectId: 'params.id' })),
162
165
  Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
@@ -166,6 +169,7 @@ __decorate([
166
169
  ], NetworkController.prototype, "deleteNetwork", null);
167
170
  __decorate([
168
171
  Example(genericAlarmsExample),
172
+ Extension('x-mcp-exposure', 'allow'),
169
173
  Get('{id}/alarms'),
170
174
  Security('*', ['acl']),
171
175
  Tags('alarms'),
@@ -181,6 +185,7 @@ __decorate([
181
185
  __decorate([
182
186
  Example(messageIds),
183
187
  Example(partialMessages),
188
+ Extension('x-mcp-exposure', 'allow'),
184
189
  Get('{id}/messages'),
185
190
  Security('*', ['acl']),
186
191
  Tags('messages'),
@@ -196,6 +201,7 @@ __decorate([
196
201
  __decorate([
197
202
  Example(taskIds),
198
203
  Example(partialTasks),
204
+ Extension('x-mcp-exposure', 'allow'),
199
205
  Get('{id}/tasks'),
200
206
  Security('*', ['acl']),
201
207
  Tags('tasks'),
@@ -209,6 +215,7 @@ __decorate([
209
215
  __param(6, Query())
210
216
  ], NetworkController.prototype, "getNetworkTasks", null);
211
217
  __decorate([
218
+ Extension('x-mcp-exposure', 'confirm'),
212
219
  Put('{id}/tags/{tag}'),
213
220
  Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
214
221
  SuccessResponse(noContentResp.status, noContentResp.description),
@@ -218,6 +225,7 @@ __decorate([
218
225
  __param(1, Path())
219
226
  ], NetworkController.prototype, "putNetworkTag", null);
220
227
  __decorate([
228
+ Extension('x-mcp-exposure', 'confirm'),
221
229
  Delete('{id}/tags/{tag}'),
222
230
  Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
223
231
  SuccessResponse(noContentResp.status, noContentResp.description),