voroa-cli 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +193 -2
- package/package.json +41 -4
- package/src/api.js +552 -0
- package/src/commands/db.js +99 -0
- package/src/commands/deploy.js +104 -0
- package/src/commands/domains.js +122 -0
- package/src/commands/env.js +143 -0
- package/src/commands/lifecycle.js +40 -0
- package/src/commands/link.js +92 -0
- package/src/commands/login.js +104 -0
- package/src/commands/logs.js +285 -0
- package/src/commands/services.js +72 -0
- package/src/commands/ssh.js +46 -0
- package/src/commands/status.js +36 -0
- package/src/commands/tokens.js +81 -0
- package/src/commands/workspace.js +63 -0
- package/src/config.js +108 -0
- package/src/errors.js +10 -0
- package/src/flags.js +32 -0
- package/src/index.js +190 -0
- package/src/interactive.js +28 -0
- package/src/link.js +72 -0
- package/src/mcp/build.js +72 -0
- package/src/mcp/kernel/index.js +59 -0
- package/src/mcp/kernel/logger.js +28 -0
- package/src/mcp/lib/config.js +72 -0
- package/src/mcp/lib/credentials.js +51 -0
- package/src/mcp/lib/http.js +136 -0
- package/src/mcp/lib/response.js +14 -0
- package/src/mcp/lib/reversibility.js +33 -0
- package/src/mcp/reversibilityMap.json +77 -0
- package/src/mcp/tools/_action.js +49 -0
- package/src/mcp/tools/_ids.js +42 -0
- package/src/mcp/tools/_shape.js +318 -0
- package/src/mcp/tools/actions.js +94 -0
- package/src/mcp/tools/getBuildLog.js +39 -0
- package/src/mcp/tools/getDeployHistory.js +42 -0
- package/src/mcp/tools/getRuntimeLogs.js +44 -0
- package/src/mcp/tools/getServiceStatus.js +41 -0
- package/src/mcp/tools/getUsage.js +34 -0
- package/src/mcp/tools/listDatabases.js +25 -0
- package/src/mcp/tools/listDomains.js +33 -0
- package/src/mcp/tools/listServices.js +20 -0
- package/src/mcp/tools/propose.js +124 -0
- package/src/mcp/tools/reads.js +756 -0
- package/src/mcp/tools/writes.js +632 -0
- package/src/mcpTools.js +265 -0
- package/src/output.js +56 -0
- package/src/prompt.js +127 -0
- package/src/region.js +5 -0
- package/src/resolve.js +64 -0
- package/src/status.js +178 -0
- package/src/stepup.js +69 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
// Phase C write tools - configuration a customer can change from an AI client.
|
|
2
|
+
//
|
|
3
|
+
// The tier rule, and it is not "reversible": these DESTROY NO RESOURCE. That wording is forced by golden
|
|
4
|
+
// rule 4 - no read path decrypts, so replacing an environment variable or a secret file puts the old value
|
|
5
|
+
// beyond everyone's reach, the customer's included. Those two therefore take `overwrite: true` and are
|
|
6
|
+
// REFUSED without it when the key already exists. The refusal is enforced in the backend
|
|
7
|
+
// (lib/customerMcpOverwrite), not here: this package runs on the customer's laptop, so a guard that lived
|
|
8
|
+
// only in this file would be absent the moment somebody used curl with the same token. The argument exists
|
|
9
|
+
// here so the model can supply it and read back a clear reason when it does not.
|
|
10
|
+
//
|
|
11
|
+
// Every schema is CLOSED (`additionalProperties: false`) and names exactly the fields its route reads. A
|
|
12
|
+
// tool that forwarded a model-authored object wholesale would be a tool whose surface is whatever the model
|
|
13
|
+
// imagined this week.
|
|
14
|
+
//
|
|
15
|
+
// Nothing here creates a billable resource, deletes anything, or opens a network boundary. Those are
|
|
16
|
+
// proposed, and a human confirms them in the dashboard.
|
|
17
|
+
|
|
18
|
+
import { jsonResponse, badInput } from '../lib/response.js';
|
|
19
|
+
import { serviceId } from './_ids.js';
|
|
20
|
+
// Projected through _shape.js like every other tool - tests/staticScan refuses a tool module that shapes
|
|
21
|
+
// its output inline, because an inline projection is invisible to the value-level scan.
|
|
22
|
+
import { pick } from './_shape.js';
|
|
23
|
+
|
|
24
|
+
// writeTool builds one tool from a declaration. `params` become validated path ids; `body` names the
|
|
25
|
+
// arguments that travel in the request body, with their JSON-schema types.
|
|
26
|
+
function writeTool({ name, description, method = 'POST', params = [], body = {}, required = [], path, project }) {
|
|
27
|
+
const properties = {};
|
|
28
|
+
for (const p of params) properties[p.name] = { type: 'string', description: p.description };
|
|
29
|
+
for (const [k, v] of Object.entries(body)) properties[k] = v;
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
kind: 'action',
|
|
33
|
+
scope: 'services:write',
|
|
34
|
+
description,
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties,
|
|
38
|
+
required: [...params.filter((p) => !p.optional).map((p) => p.name), ...required],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
async handler(args, ctx) {
|
|
42
|
+
const ids = {};
|
|
43
|
+
for (const p of params) {
|
|
44
|
+
const raw = args[p.name];
|
|
45
|
+
if (raw == null || raw === '') {
|
|
46
|
+
if (!p.optional) throw badInput(`${p.name} is required.`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const ok = serviceId(raw);
|
|
50
|
+
if (!ok) throw badInput(`${p.name} is not a valid id.`);
|
|
51
|
+
ids[p.name] = ok;
|
|
52
|
+
}
|
|
53
|
+
const payload = {};
|
|
54
|
+
for (const k of Object.keys(body)) if (args[k] !== undefined) payload[k] = args[k];
|
|
55
|
+
const data = await ctx.http[method === 'PUT' ? 'put' : method === 'PATCH' ? 'patch' : 'post'](path(ids), payload);
|
|
56
|
+
// A write's response is projected too. Most routes answer { ok: true }; the ones that echo a row
|
|
57
|
+
// must not echo it whole - the same rule the read tools follow.
|
|
58
|
+
if (project) return jsonResponse(project(data));
|
|
59
|
+
return jsonResponse(pick(data || {}, ['ok', 'id', 'name', 'filename', 'saved', 'previewsRebuilding']));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SERVICE_ARG = { name: 'serviceId', description: 'The service id, from list_services.' };
|
|
65
|
+
const DB_ARG = { name: 'databaseId', description: 'The database id, from list_databases.' };
|
|
66
|
+
const PROJECT_ARG = { name: 'projectId', description: 'The project id, from list_projects.' };
|
|
67
|
+
const GROUP_ARG = { name: 'groupId', description: 'The environment group id, from list_env_groups.' };
|
|
68
|
+
const svc = (v) => encodeURIComponent(v.serviceId);
|
|
69
|
+
const db = (v) => encodeURIComponent(v.databaseId);
|
|
70
|
+
const prj = (v) => encodeURIComponent(v.projectId);
|
|
71
|
+
const grp = (v) => encodeURIComponent(v.groupId);
|
|
72
|
+
|
|
73
|
+
const OVERWRITE = {
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
description:
|
|
76
|
+
'Set true to REPLACE something that already has a value. Without it the request is refused when the '
|
|
77
|
+
+ 'name already exists, because the current value cannot be read back by anyone and replacing it '
|
|
78
|
+
+ 'discards it for good. Use list_env_vars or list_secret_files first to see which names exist.',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const updateServiceSettings = writeTool({
|
|
82
|
+
name: 'update_service_settings',
|
|
83
|
+
description:
|
|
84
|
+
'Rename a service, or change the directory inside the repository that it builds from. Takes effect on '
|
|
85
|
+
+ 'the next deploy.',
|
|
86
|
+
method: 'PATCH',
|
|
87
|
+
params: [SERVICE_ARG],
|
|
88
|
+
body: {
|
|
89
|
+
name: { type: 'string', description: 'A new name for the service.' },
|
|
90
|
+
rootDirectory: { type: 'string', description: 'The directory in the repository to build from, e.g. "apps/api".' },
|
|
91
|
+
},
|
|
92
|
+
path: (v) => `/apps/${svc(v)}/settings`,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const setAutoDeploy = writeTool({
|
|
96
|
+
name: 'set_auto_deploy',
|
|
97
|
+
description:
|
|
98
|
+
'Turn automatic deploys on or off for a service. When on, a push to the configured branch deploys it.',
|
|
99
|
+
method: 'PATCH',
|
|
100
|
+
params: [SERVICE_ARG],
|
|
101
|
+
body: { autoDeploy: { type: 'boolean', description: 'true to deploy on every push to the branch.' } },
|
|
102
|
+
required: ['autoDeploy'],
|
|
103
|
+
path: (v) => `/apps/${svc(v)}/auto-deploy`,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const setEnvVars = writeTool({
|
|
107
|
+
name: 'set_env_vars',
|
|
108
|
+
description:
|
|
109
|
+
'Add or replace environment variables on a service. Values are stored encrypted and can never be read '
|
|
110
|
+
+ 'back - not by you, not by the customer, not by Voroa. Replacing one therefore discards the old value '
|
|
111
|
+
+ 'permanently, so a name that already exists is refused unless overwrite is true. Applies from the '
|
|
112
|
+
+ 'next deploy.',
|
|
113
|
+
method: 'PUT',
|
|
114
|
+
params: [SERVICE_ARG],
|
|
115
|
+
body: {
|
|
116
|
+
envVars: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
description: 'The variables to set.',
|
|
119
|
+
items: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
key: { type: 'string' },
|
|
123
|
+
value: { type: 'string' },
|
|
124
|
+
isBuildTime: { type: 'boolean' },
|
|
125
|
+
},
|
|
126
|
+
required: ['key', 'value'],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
overwrite: OVERWRITE,
|
|
131
|
+
},
|
|
132
|
+
required: ['envVars'],
|
|
133
|
+
path: (v) => `/apps/${svc(v)}/env`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const putSecretFile = writeTool({
|
|
137
|
+
name: 'put_secret_file',
|
|
138
|
+
description:
|
|
139
|
+
'Add or replace a secret file on a service. Contents are stored encrypted and can never be read back, '
|
|
140
|
+
+ 'so replacing a file discards what was there - a filename that already exists is refused unless '
|
|
141
|
+
+ 'overwrite is true. The file appears to the service at /etc/secrets/<filename> from the next deploy.',
|
|
142
|
+
method: 'PUT',
|
|
143
|
+
params: [SERVICE_ARG],
|
|
144
|
+
body: {
|
|
145
|
+
filename: { type: 'string', description: 'The file path, e.g. ".env" or "certs/key.pem".' },
|
|
146
|
+
content: { type: 'string', description: 'The file contents.' },
|
|
147
|
+
overwrite: OVERWRITE,
|
|
148
|
+
},
|
|
149
|
+
required: ['filename', 'content'],
|
|
150
|
+
path: (v) => `/apps/${svc(v)}/secret-files`,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export const setPreviewVariables = writeTool({
|
|
154
|
+
name: 'set_preview_variables',
|
|
155
|
+
description:
|
|
156
|
+
'Set the environment variables applied to a service\'s preview environments. Values are never readable '
|
|
157
|
+
+ 'afterwards, exactly like a service\'s own environment variables.',
|
|
158
|
+
method: 'PUT',
|
|
159
|
+
params: [SERVICE_ARG],
|
|
160
|
+
body: {
|
|
161
|
+
variables: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
description: 'The preview variables to set.',
|
|
164
|
+
items: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
key: { type: 'string' },
|
|
168
|
+
value: { type: 'string' },
|
|
169
|
+
isBuildTime: { type: 'boolean' },
|
|
170
|
+
},
|
|
171
|
+
required: ['key', 'value'],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
required: ['variables'],
|
|
177
|
+
path: (v) => `/apps/${svc(v)}/preview-variables`,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const setPreviewsEnabled = writeTool({
|
|
181
|
+
name: 'set_previews_enabled',
|
|
182
|
+
description:
|
|
183
|
+
'Turn preview environments on or off for a service. When on, an open pull request gets its own '
|
|
184
|
+
+ 'temporary copy of the service.',
|
|
185
|
+
method: 'PATCH',
|
|
186
|
+
params: [SERVICE_ARG],
|
|
187
|
+
body: { enabled: { type: 'boolean', description: 'true to build a preview for each open pull request.' } },
|
|
188
|
+
required: ['enabled'],
|
|
189
|
+
path: (v) => `/apps/${svc(v)}/previews/enabled`,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export const addDomain = writeTool({
|
|
193
|
+
name: 'add_domain',
|
|
194
|
+
description:
|
|
195
|
+
'Add a custom domain to a service. Voroa then requests a certificate for it; the domain only starts '
|
|
196
|
+
+ 'serving once its DNS points at Voroa, which the customer sets up at their registrar. Removing a '
|
|
197
|
+
+ 'domain is not available from here.',
|
|
198
|
+
params: [SERVICE_ARG],
|
|
199
|
+
body: { domain: { type: 'string', description: 'The domain, e.g. "app.example.com".' } },
|
|
200
|
+
required: ['domain'],
|
|
201
|
+
path: (v) => `/apps/${svc(v)}/domains`,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export const reprovisionDomain = writeTool({
|
|
205
|
+
name: 'reprovision_domain',
|
|
206
|
+
description:
|
|
207
|
+
'Ask Voroa to try again on a custom domain whose certificate or routing did not come up. Safe to '
|
|
208
|
+
+ 'repeat; it changes nothing about the domain itself.',
|
|
209
|
+
params: [SERVICE_ARG, { name: 'domainId', description: 'The domain id, from list_domains.' }],
|
|
210
|
+
path: (v) => `/apps/${svc(v)}/domains/${encodeURIComponent(v.domainId)}/reprovision`,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export const createScheduledTask = writeTool({
|
|
214
|
+
name: 'create_scheduled_task',
|
|
215
|
+
description:
|
|
216
|
+
'Add a scheduled task (a cron job) to a service. It runs inside the service\'s own running container, '
|
|
217
|
+
+ 'so the service must be running for it to fire.',
|
|
218
|
+
params: [SERVICE_ARG],
|
|
219
|
+
body: {
|
|
220
|
+
name: { type: 'string', description: 'A name for the task.' },
|
|
221
|
+
command: { type: 'string', description: 'The command to run inside the container.' },
|
|
222
|
+
frequency: { type: 'string', description: 'A five-field cron expression, or a preset such as "daily".' },
|
|
223
|
+
enabled: { type: 'boolean', description: 'Whether it should start enabled. Defaults to true.' },
|
|
224
|
+
},
|
|
225
|
+
required: ['name', 'command', 'frequency'],
|
|
226
|
+
path: (v) => `/apps/${svc(v)}/scheduled-tasks`,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
export const updateScheduledTask = writeTool({
|
|
230
|
+
name: 'update_scheduled_task',
|
|
231
|
+
description: 'Change a scheduled task\'s name, command, frequency, or whether it is enabled.',
|
|
232
|
+
method: 'PATCH',
|
|
233
|
+
params: [SERVICE_ARG, { name: 'taskId', description: 'The task id, from list_scheduled_tasks.' }],
|
|
234
|
+
body: {
|
|
235
|
+
name: { type: 'string' },
|
|
236
|
+
command: { type: 'string' },
|
|
237
|
+
frequency: { type: 'string', description: 'A five-field cron expression, or a preset such as "daily".' },
|
|
238
|
+
enabled: { type: 'boolean' },
|
|
239
|
+
},
|
|
240
|
+
path: (v) => `/apps/${svc(v)}/scheduled-tasks/${encodeURIComponent(v.taskId)}`,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
export const reorderServiceEnvGroups = writeTool({
|
|
244
|
+
name: 'reorder_service_env_groups',
|
|
245
|
+
description:
|
|
246
|
+
'Set the order in which a service\'s environment groups are applied. A later group\'s value wins where '
|
|
247
|
+
+ 'two groups define the same name.',
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
params: [SERVICE_ARG],
|
|
250
|
+
body: {
|
|
251
|
+
groupIds: {
|
|
252
|
+
type: 'array',
|
|
253
|
+
description: 'Every linked group id, in the order they should apply.',
|
|
254
|
+
items: { type: 'string' },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ['groupIds'],
|
|
258
|
+
path: (v) => `/apps/${svc(v)}/env-groups/order`,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export const createProject = writeTool({
|
|
262
|
+
name: 'create_project',
|
|
263
|
+
description:
|
|
264
|
+
'Create a project. A project groups services and their environments; it costs nothing on its own.',
|
|
265
|
+
body: {
|
|
266
|
+
name: { type: 'string', description: 'A name for the project.' },
|
|
267
|
+
environmentName: { type: 'string', description: 'Optional name for its first environment.' },
|
|
268
|
+
},
|
|
269
|
+
required: ['name'],
|
|
270
|
+
path: () => '/projects',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export const renameProject = writeTool({
|
|
274
|
+
name: 'rename_project',
|
|
275
|
+
description:
|
|
276
|
+
'Rename a project. The services and environments inside it are untouched - only the label the customer '
|
|
277
|
+
+ 'sees changes.',
|
|
278
|
+
method: 'PATCH',
|
|
279
|
+
params: [PROJECT_ARG],
|
|
280
|
+
body: { name: { type: 'string', description: 'The new name.' } },
|
|
281
|
+
required: ['name'],
|
|
282
|
+
path: (v) => `/projects/${prj(v)}`,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export const createEnvironment = writeTool({
|
|
286
|
+
name: 'create_environment',
|
|
287
|
+
description:
|
|
288
|
+
'Add an environment to a project, such as "staging". An environment groups the services deployed for '
|
|
289
|
+
+ 'one stage of the work.',
|
|
290
|
+
params: [PROJECT_ARG],
|
|
291
|
+
body: { name: { type: 'string', description: 'A name for the environment.' } },
|
|
292
|
+
required: ['name'],
|
|
293
|
+
path: (v) => `/projects/${prj(v)}/environments`,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
export const renameEnvironment = writeTool({
|
|
297
|
+
name: 'rename_environment',
|
|
298
|
+
description:
|
|
299
|
+
'Rename an environment inside a project. The services deployed in it are untouched; only the label '
|
|
300
|
+
+ 'changes.',
|
|
301
|
+
method: 'PATCH',
|
|
302
|
+
params: [PROJECT_ARG, { name: 'environmentId', description: 'The environment id, from list_environments.' }],
|
|
303
|
+
body: { name: { type: 'string', description: 'The new name.' } },
|
|
304
|
+
required: ['name'],
|
|
305
|
+
path: (v) => `/projects/${prj(v)}/environments/${encodeURIComponent(v.environmentId)}`,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
export const createEnvGroup = writeTool({
|
|
309
|
+
name: 'create_env_group',
|
|
310
|
+
description:
|
|
311
|
+
'Create an environment group - a set of environment variables that several services can share.',
|
|
312
|
+
body: { name: { type: 'string', description: 'A name for the group.' } },
|
|
313
|
+
required: ['name'],
|
|
314
|
+
path: () => '/env-groups',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const renameEnvGroup = writeTool({
|
|
318
|
+
name: 'rename_env_group',
|
|
319
|
+
description:
|
|
320
|
+
'Rename an environment group. The variables in it and the services linked to it are untouched - only '
|
|
321
|
+
+ 'the label changes.',
|
|
322
|
+
method: 'PATCH',
|
|
323
|
+
params: [GROUP_ARG],
|
|
324
|
+
body: { name: { type: 'string', description: 'The new name.' } },
|
|
325
|
+
required: ['name'],
|
|
326
|
+
path: (v) => `/env-groups/${grp(v)}`,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
export const setEnvGroupVariables = writeTool({
|
|
330
|
+
name: 'set_env_group_variables',
|
|
331
|
+
description:
|
|
332
|
+
'Set the variables in an environment group. Every service linked to the group picks them up on its '
|
|
333
|
+
+ 'next deploy. Values can never be read back afterwards.',
|
|
334
|
+
method: 'PUT',
|
|
335
|
+
params: [GROUP_ARG],
|
|
336
|
+
body: {
|
|
337
|
+
variables: {
|
|
338
|
+
type: 'array',
|
|
339
|
+
description: 'The variables for this group.',
|
|
340
|
+
items: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
// `key`, not `name` - the route reads v.key (routes/envGroups.js). A tool that sent `name` would
|
|
343
|
+
// be accepted by the schema and dropped by the handler, which is the worst kind of wrong.
|
|
344
|
+
properties: {
|
|
345
|
+
key: { type: 'string' },
|
|
346
|
+
value: { type: 'string' },
|
|
347
|
+
isBuildTime: { type: 'boolean' },
|
|
348
|
+
},
|
|
349
|
+
required: ['key', 'value'],
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
required: ['variables'],
|
|
355
|
+
path: (v) => `/env-groups/${grp(v)}/env-vars`,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export const linkEnvGroupToService = writeTool({
|
|
359
|
+
name: 'link_env_group_to_service',
|
|
360
|
+
description:
|
|
361
|
+
'Apply an environment group to a service. The service picks up the group\'s variables on its next '
|
|
362
|
+
+ 'deploy. Unlinking is not available from here.',
|
|
363
|
+
params: [GROUP_ARG, SERVICE_ARG],
|
|
364
|
+
path: (v) => `/env-groups/${grp(v)}/services/${svc(v)}`,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export const updateDatabaseSettings = writeTool({
|
|
368
|
+
name: 'update_database_settings',
|
|
369
|
+
description:
|
|
370
|
+
'Change how much memory a database may use. It applies without deleting anything, and a value below '
|
|
371
|
+
+ 'what the plan allows is refused.',
|
|
372
|
+
method: 'PATCH',
|
|
373
|
+
params: [DB_ARG],
|
|
374
|
+
body: { ramMb: { type: 'number', description: 'Memory limit in megabytes.' } },
|
|
375
|
+
required: ['ramMb'],
|
|
376
|
+
path: (v) => `/databases/${db(v)}`,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
export const restartDatabase = writeTool({
|
|
380
|
+
name: 'restart_database',
|
|
381
|
+
description:
|
|
382
|
+
'Restart a database. It is briefly unavailable while it comes back; no data is changed and nothing is '
|
|
383
|
+
+ 'lost.',
|
|
384
|
+
params: [DB_ARG],
|
|
385
|
+
path: (v) => `/databases/${db(v)}/restart`,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
export const stopDatabase = writeTool({
|
|
389
|
+
name: 'stop_database',
|
|
390
|
+
description:
|
|
391
|
+
'Stop a database. Anything connected to it will fail until it is started again. Nothing is deleted, '
|
|
392
|
+
+ 'and start_database brings it back.',
|
|
393
|
+
params: [DB_ARG],
|
|
394
|
+
path: (v) => `/databases/${db(v)}/stop`,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
export const startDatabase = writeTool({
|
|
398
|
+
name: 'start_database',
|
|
399
|
+
description:
|
|
400
|
+
'Start a database that was stopped. It becomes reachable again once it has finished coming up; nothing '
|
|
401
|
+
+ 'about its data changes.',
|
|
402
|
+
params: [DB_ARG],
|
|
403
|
+
path: (v) => `/databases/${db(v)}/start`,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export const createDatabaseUser = writeTool({
|
|
407
|
+
name: 'create_database_user',
|
|
408
|
+
description:
|
|
409
|
+
'Create a database user. Its password is generated by Voroa and shown in the dashboard, never returned '
|
|
410
|
+
+ 'here.',
|
|
411
|
+
params: [DB_ARG],
|
|
412
|
+
body: { name: { type: 'string', description: 'The user name.' } },
|
|
413
|
+
required: ['name'],
|
|
414
|
+
path: (v) => `/databases/${db(v)}/users`,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
export const setDefaultDatabaseUser = writeTool({
|
|
418
|
+
name: 'set_default_database_user',
|
|
419
|
+
description:
|
|
420
|
+
'Choose which database user the connection details in the dashboard are issued for. The other users '
|
|
421
|
+
+ 'keep working; only which one is offered by default changes.',
|
|
422
|
+
params: [DB_ARG, { name: 'userName', description: 'The user name, from list_database_users.' }],
|
|
423
|
+
path: (v) => `/databases/${db(v)}/users/${encodeURIComponent(v.userName)}/default`,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
export const preflightImport = writeTool({
|
|
427
|
+
name: 'preflight_import',
|
|
428
|
+
description:
|
|
429
|
+
'Check whether a dump could be imported into a database - its size, format and whether the database '
|
|
430
|
+
+ 'has room. This is a DRY RUN: it reads the source and changes nothing. The import itself replaces the '
|
|
431
|
+
+ 'database\'s current data, so it is not available from here; propose it and confirm it in the '
|
|
432
|
+
+ 'dashboard.',
|
|
433
|
+
params: [DB_ARG],
|
|
434
|
+
body: { sourceUrl: { type: 'string', description: 'Where the dump can be read from.' } },
|
|
435
|
+
required: ['sourceUrl'],
|
|
436
|
+
path: (v) => `/databases/${db(v)}/import/preflight`,
|
|
437
|
+
project: (d) => d || null,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Reversible, and it belongs beside `stop` rather than in the write tier: the service stops answering the
|
|
441
|
+
// internet until someone turns it back on. Declared here because it is a write in shape, but it carries
|
|
442
|
+
// services:act and a reversibility row.
|
|
443
|
+
export const setMaintenanceMode = {
|
|
444
|
+
name: 'set_maintenance_mode',
|
|
445
|
+
kind: 'action',
|
|
446
|
+
scope: 'services:act',
|
|
447
|
+
description:
|
|
448
|
+
'Put a service into maintenance mode, or take it out. While it is on, visitors see a maintenance page '
|
|
449
|
+
+ 'instead of the service - it is DOWN for them - but nothing is destroyed and turning it off restores '
|
|
450
|
+
+ 'service immediately.',
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
serviceId: { type: 'string', description: 'The service id, from list_services.' },
|
|
455
|
+
on: { type: 'boolean', description: 'true to show the maintenance page, false to serve normally.' },
|
|
456
|
+
pageUrl: { type: 'string', description: 'Optional address of a page to show instead of the default.' },
|
|
457
|
+
},
|
|
458
|
+
required: ['serviceId', 'on'],
|
|
459
|
+
additionalProperties: false,
|
|
460
|
+
},
|
|
461
|
+
async handler(args, ctx) {
|
|
462
|
+
const id = serviceId(args.serviceId);
|
|
463
|
+
if (!id) throw badInput('serviceId is required and must be a valid service id from list_services.');
|
|
464
|
+
if (typeof args.on !== 'boolean') throw badInput('on must be true or false.');
|
|
465
|
+
const body = { on: args.on };
|
|
466
|
+
if (args.pageUrl !== undefined) body.pageUrl = args.pageUrl;
|
|
467
|
+
return jsonResponse(await ctx.http.put(`/apps/${encodeURIComponent(id)}/maintenance`, body));
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// ADR-0263. Reversible in the same sense maintenance is: the service keeps running and nothing is
|
|
472
|
+
// destroyed; turning it back restores the previous path within a few minutes.
|
|
473
|
+
export const setEdgeProtection = {
|
|
474
|
+
name: 'set_edge_protection',
|
|
475
|
+
kind: 'action',
|
|
476
|
+
scope: 'services:act',
|
|
477
|
+
description:
|
|
478
|
+
'Turn edge protection on or off for a service. On: its address is served through an edge network '
|
|
479
|
+
+ 'that absorbs floods, uploads over 100 MB are refused, a request that takes over 100 seconds to start '
|
|
480
|
+
+ 'answering times out, and X-Real-Ip is not provided (the visitor is the second-to-last X-Forwarded-For entry; the last is the edge address). Off: '
|
|
481
|
+
+ 'visitors reach the service directly. The change is applied within a few minutes.',
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: {
|
|
485
|
+
serviceId: { type: 'string', description: 'The service id, from list_services.' },
|
|
486
|
+
enabled: { type: 'boolean', description: 'true to serve through the edge network, false to serve directly.' },
|
|
487
|
+
},
|
|
488
|
+
required: ['serviceId', 'enabled'],
|
|
489
|
+
additionalProperties: false,
|
|
490
|
+
},
|
|
491
|
+
async handler(args, ctx) {
|
|
492
|
+
const id = serviceId(args.serviceId);
|
|
493
|
+
if (!id) throw badInput('serviceId is required and must be a valid service id from list_services.');
|
|
494
|
+
if (typeof args.enabled !== 'boolean') throw badInput('enabled must be true or false.');
|
|
495
|
+
return jsonResponse(await ctx.http.patch(`/apps/${encodeURIComponent(id)}/edge-protection`, { enabled: args.enabled }));
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// Object Storage: write a bucket's endpoint, name and key into a service's environment variables (the
|
|
500
|
+
// dashboard's Connect tab). The secret travels server-side only - this tool never sees or returns it. The
|
|
501
|
+
// service picks the variables up on its next deploy.
|
|
502
|
+
export const connectBucketToService = writeTool({
|
|
503
|
+
name: 'connect_bucket_to_service',
|
|
504
|
+
description:
|
|
505
|
+
'Give a service access to a bucket: writes <prefix>ENDPOINT, BUCKET, REGION, ACCESS_KEY_ID and '
|
|
506
|
+
+ 'SECRET_ACCESS_KEY into the service\'s environment variables (prefix defaults to S3_). Refused if any of '
|
|
507
|
+
+ 'those names already exists on the service. Redeploy the service afterwards so it reads them.',
|
|
508
|
+
params: [{ name: 'bucketId', description: 'The bucket id, from list_buckets.' }],
|
|
509
|
+
body: {
|
|
510
|
+
appUuid: { type: 'string', description: 'The service id, from list_services.' },
|
|
511
|
+
prefix: { type: 'string', description: 'Optional. Uppercase variable prefix, for example S3_ or MEDIA_.' },
|
|
512
|
+
},
|
|
513
|
+
required: ['appUuid'],
|
|
514
|
+
path: (v) => `/buckets/${encodeURIComponent(v.bucketId)}/connect`,
|
|
515
|
+
project: (d) => pick(d || {}, ['keys']),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ---- Wave 2 writes (plan 2026-10-27) ----------------------------------------------------------------------
|
|
519
|
+
export const verifyDomainClaim = writeTool({
|
|
520
|
+
name: 'verify_domain_claim',
|
|
521
|
+
description:
|
|
522
|
+
'Check the DNS of a domain the customer claimed on a service and, if the records are right, attach it. '
|
|
523
|
+
+ 'Nothing changes when the records are not in place yet - the answer says what is missing.',
|
|
524
|
+
params: [SERVICE_ARG, { name: 'claimId', description: 'The claim id, from list_domains (pending claims).' }],
|
|
525
|
+
path: (v) => `/apps/${svc(v)}/domains/claims/${encodeURIComponent(v.claimId)}/verify`,
|
|
526
|
+
project: (d) => pick(d || {}, ['id', 'domain', 'ownership', 'status', 'verifiedStatus']),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
export const importPreviewVariables = writeTool({
|
|
530
|
+
name: 'import_preview_variables',
|
|
531
|
+
description:
|
|
532
|
+
'Copy the service\'s production environment variable NAMES and values into its preview variables, so '
|
|
533
|
+
+ 'preview deploys start with the same configuration. Refused unless overwrite:true when preview variables '
|
|
534
|
+
+ 'already exist (values cannot be read back, so replacing them is final). includeManagedDatabases:true '
|
|
535
|
+
+ 'also copies the connection variables of linked databases - previews would then share production data.',
|
|
536
|
+
params: [SERVICE_ARG],
|
|
537
|
+
body: {
|
|
538
|
+
overwrite: { type: 'boolean', description: 'Required true to replace preview variables that already exist.' },
|
|
539
|
+
includeManagedDatabases: { type: 'boolean', description: 'Also copy linked database connection variables (previews then use production data).' },
|
|
540
|
+
},
|
|
541
|
+
path: (v) => `/apps/${svc(v)}/preview-variables/import`,
|
|
542
|
+
project: (d) => pick(d || {}, ['ok', 'imported', 'skippedExisting', 'skippedDatabases', 'skippedInvalid', 'skippedOverCap', 'previewsRebuilding']),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
export const createDatabaseBackup = writeTool({
|
|
546
|
+
name: 'create_database_backup',
|
|
547
|
+
description:
|
|
548
|
+
'Take a backup of a paid database now. Free of charge; one pending backup per database at a time; kept '
|
|
549
|
+
+ 'for the retention period shown by list_database_backups. Refused on free databases.',
|
|
550
|
+
params: [DB_ARG],
|
|
551
|
+
path: (v) => `/databases/${db(v)}/backups`,
|
|
552
|
+
project: (d) => pick(d || {}, ['backupId', 'status', 'message']),
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
export const enableDatabaseBackups = writeTool({
|
|
556
|
+
name: 'enable_database_backups',
|
|
557
|
+
description:
|
|
558
|
+
'Turn nightly backups ON for a paid database. Turning them OFF is not possible here - it is proposed '
|
|
559
|
+
+ 'with propose(disable_database_backups) and approved by a person, because it is a quiet way to lose data.',
|
|
560
|
+
method: 'PUT',
|
|
561
|
+
params: [DB_ARG],
|
|
562
|
+
body: { enabled: { type: 'boolean', enum: [true], description: 'Must be true.' } },
|
|
563
|
+
required: ['enabled'],
|
|
564
|
+
path: (v) => `/databases/${db(v)}/backups/settings`,
|
|
565
|
+
project: (d) => pick(d || {}, ['backupsEnabled']),
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
export const setNotificationSettings = writeTool({
|
|
569
|
+
name: 'set_notification_settings',
|
|
570
|
+
description:
|
|
571
|
+
'Turn the workspace email alerts on or off: deploy (a failed deploy) and usage (approaching a limit). '
|
|
572
|
+
+ 'Send only the ones to change. Reversible.',
|
|
573
|
+
method: 'PUT',
|
|
574
|
+
body: {
|
|
575
|
+
deploy: { type: 'boolean', description: 'Email on a failed deploy.' },
|
|
576
|
+
usage: { type: 'boolean', description: 'Email when usage approaches a limit.' },
|
|
577
|
+
},
|
|
578
|
+
path: () => '/workspace/notifications',
|
|
579
|
+
project: (d) => pick(d || {}, ['deploy', 'usage']),
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Support tickets (CTO decision 2026-10-27). Capped per token per day on the server (3 new, 20 replies),
|
|
583
|
+
// and the support desk sees every one marked as sent by an AI client.
|
|
584
|
+
const TICKET_ARG = { name: 'ticketId', description: 'The ticket id, from list_tickets.' };
|
|
585
|
+
|
|
586
|
+
export const openTicket = writeTool({
|
|
587
|
+
name: 'open_ticket',
|
|
588
|
+
description:
|
|
589
|
+
'Open a support ticket with Voroa. Use it for a real problem the customer cannot fix themselves, after '
|
|
590
|
+
+ 'checking status, logs and deploy history. Limited to a few per day per token, and marked as sent by '
|
|
591
|
+
+ 'an AI client. Tell the customer you opened it.',
|
|
592
|
+
body: {
|
|
593
|
+
subject: { type: 'string', description: 'One line: what is wrong.' },
|
|
594
|
+
body: { type: 'string', description: 'What happened, what was tried, and any error text.' },
|
|
595
|
+
priority: { type: 'string', enum: ['low', 'normal', 'high'], description: 'Optional. Defaults to normal.' },
|
|
596
|
+
category: { type: 'string', enum: ['billing', 'deployments', 'domains', 'databases', 'account', 'other'], description: 'Optional. Defaults to other.' },
|
|
597
|
+
appUuid: { type: 'string', description: 'Optional. The service id the ticket is about.' },
|
|
598
|
+
},
|
|
599
|
+
required: ['subject', 'body'],
|
|
600
|
+
path: () => '/support/tickets',
|
|
601
|
+
project: (d) => ({ ticket: pick((d && d.ticket) || {}, ['id', 'subject', 'status', 'priority', 'createdAt']) }),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
export const replyTicket = writeTool({
|
|
605
|
+
name: 'reply_ticket',
|
|
606
|
+
description: 'Add a reply to one of the workspace\'s support tickets. Reopens a solved ticket. Limited per day.',
|
|
607
|
+
params: [TICKET_ARG],
|
|
608
|
+
body: { body: { type: 'string', description: 'The reply text.' } },
|
|
609
|
+
required: ['body'],
|
|
610
|
+
path: (v) => `/support/tickets/${encodeURIComponent(v.ticketId)}/reply`,
|
|
611
|
+
project: (d) => ({ ticket: pick((d && d.ticket) || {}, ['id', 'status', 'updatedAt']) }),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
export const solveTicket = writeTool({
|
|
615
|
+
name: 'solve_ticket',
|
|
616
|
+
description: 'Mark a support ticket solved because the problem is fixed. A later reply reopens it.',
|
|
617
|
+
params: [TICKET_ARG],
|
|
618
|
+
path: (v) => `/support/tickets/${encodeURIComponent(v.ticketId)}/solve`,
|
|
619
|
+
project: (d) => ({ ticket: pick((d && d.ticket) || {}, ['id', 'status', 'updatedAt']) }),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
export const WRITE_TOOLS = [
|
|
623
|
+
openTicket, replyTicket, solveTicket,
|
|
624
|
+
verifyDomainClaim, importPreviewVariables, createDatabaseBackup, enableDatabaseBackups, setNotificationSettings,
|
|
625
|
+
updateServiceSettings, setAutoDeploy, setEnvVars, putSecretFile, setPreviewVariables, setPreviewsEnabled,
|
|
626
|
+
addDomain, reprovisionDomain, createScheduledTask, updateScheduledTask, reorderServiceEnvGroups,
|
|
627
|
+
createProject, renameProject, createEnvironment, renameEnvironment, createEnvGroup, renameEnvGroup,
|
|
628
|
+
setEnvGroupVariables, linkEnvGroupToService, updateDatabaseSettings, restartDatabase, stopDatabase,
|
|
629
|
+
startDatabase, createDatabaseUser, setDefaultDatabaseUser, preflightImport, setMaintenanceMode,
|
|
630
|
+
setEdgeProtection,
|
|
631
|
+
connectBucketToService,
|
|
632
|
+
];
|