@vibetools/dokploy-mcp 1.0.0 → 2.0.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 +59 -160
- package/dist/api/client.d.ts +3 -0
- package/dist/api/client.js +38 -13
- package/dist/cli/index.js +1 -0
- package/dist/codemode/context/execute-context.d.ts +590 -0
- package/dist/codemode/context/execute-context.js +64 -0
- package/dist/codemode/context/search-context.d.ts +13851 -0
- package/dist/codemode/context/search-context.js +42 -0
- package/dist/codemode/gateway/api-gateway.d.ts +12 -0
- package/dist/codemode/gateway/api-gateway.js +160 -0
- package/dist/codemode/gateway/error-format.d.ts +13 -0
- package/dist/codemode/gateway/error-format.js +9 -0
- package/dist/codemode/gateway/request-normalizer.d.ts +3 -0
- package/dist/codemode/gateway/request-normalizer.js +21 -0
- package/dist/codemode/gateway/trace.d.ts +13 -0
- package/dist/codemode/gateway/trace.js +11 -0
- package/dist/codemode/sandbox/host.d.ts +11 -0
- package/dist/codemode/sandbox/host.js +28 -0
- package/dist/codemode/sandbox/limits.d.ts +2 -0
- package/dist/codemode/sandbox/limits.js +20 -0
- package/dist/codemode/sandbox/runner.d.ts +8 -0
- package/dist/codemode/sandbox/runner.js +113 -0
- package/dist/codemode/sandbox/runtime.d.ts +2 -0
- package/dist/codemode/sandbox/runtime.js +3 -0
- package/dist/codemode/sandbox/serialize.d.ts +1 -0
- package/dist/codemode/sandbox/serialize.js +45 -0
- package/dist/codemode/sandbox/subprocess-runner.d.ts +10 -0
- package/dist/codemode/sandbox/subprocess-runner.js +91 -0
- package/dist/codemode/sandbox/types.d.ts +12 -0
- package/dist/codemode/sandbox/types.js +1 -0
- package/dist/codemode/sandbox/worker-entry.d.ts +1 -0
- package/dist/codemode/sandbox/worker-entry.js +70 -0
- package/dist/codemode/server-codemode.d.ts +2 -0
- package/dist/codemode/server-codemode.js +17 -0
- package/dist/codemode/tools/execute.d.ts +583 -0
- package/dist/codemode/tools/execute.js +52 -0
- package/dist/codemode/tools/index.d.ts +2 -0
- package/dist/codemode/tools/index.js +3 -0
- package/dist/codemode/tools/search.d.ts +2 -0
- package/dist/codemode/tools/search.js +62 -0
- package/dist/generated/dokploy-catalog.d.ts +27 -0
- package/dist/generated/dokploy-catalog.js +16 -0
- package/dist/generated/dokploy-schemas.d.ts +17533 -0
- package/dist/generated/dokploy-schemas.js +20639 -0
- package/dist/generated/dokploy-sdk.d.ts +558 -0
- package/dist/generated/dokploy-sdk.js +561 -0
- package/dist/generated/openapi-index.json +10591 -0
- package/dist/{tools/_factory.d.ts → mcp/tool-factory.d.ts} +2 -0
- package/dist/{tools/_factory.js → mcp/tool-factory.js} +26 -7
- package/dist/server.d.ts +1 -1
- package/dist/server.js +2 -15
- package/package.json +22 -11
- package/dist/tools/_database.d.ts +0 -12
- package/dist/tools/_database.js +0 -96
- package/dist/tools/admin.d.ts +0 -2
- package/dist/tools/admin.js +0 -61
- package/dist/tools/application.d.ts +0 -2
- package/dist/tools/application.js +0 -464
- package/dist/tools/backup.d.ts +0 -2
- package/dist/tools/backup.js +0 -103
- package/dist/tools/certificates.d.ts +0 -2
- package/dist/tools/certificates.js +0 -54
- package/dist/tools/cluster.d.ts +0 -2
- package/dist/tools/cluster.js +0 -38
- package/dist/tools/compose.d.ts +0 -2
- package/dist/tools/compose.js +0 -213
- package/dist/tools/deployment.d.ts +0 -2
- package/dist/tools/deployment.js +0 -27
- package/dist/tools/destination.d.ts +0 -2
- package/dist/tools/destination.js +0 -78
- package/dist/tools/docker.d.ts +0 -2
- package/dist/tools/docker.js +0 -50
- package/dist/tools/domain.d.ts +0 -2
- package/dist/tools/domain.js +0 -134
- package/dist/tools/index.d.ts +0 -2
- package/dist/tools/index.js +0 -48
- package/dist/tools/mariadb.d.ts +0 -1
- package/dist/tools/mariadb.js +0 -14
- package/dist/tools/mongo.d.ts +0 -1
- package/dist/tools/mongo.js +0 -12
- package/dist/tools/mounts.d.ts +0 -2
- package/dist/tools/mounts.js +0 -65
- package/dist/tools/mysql.d.ts +0 -1
- package/dist/tools/mysql.js +0 -14
- package/dist/tools/port.d.ts +0 -2
- package/dist/tools/port.js +0 -54
- package/dist/tools/postgres.d.ts +0 -1
- package/dist/tools/postgres.js +0 -13
- package/dist/tools/project.d.ts +0 -2
- package/dist/tools/project.js +0 -94
- package/dist/tools/redirects.d.ts +0 -2
- package/dist/tools/redirects.js +0 -53
- package/dist/tools/redis.d.ts +0 -1
- package/dist/tools/redis.js +0 -11
- package/dist/tools/registry.d.ts +0 -2
- package/dist/tools/registry.js +0 -81
- package/dist/tools/security.d.ts +0 -2
- package/dist/tools/security.js +0 -48
- package/dist/tools/settings.d.ts +0 -2
- package/dist/tools/settings.js +0 -258
- package/dist/tools/user.d.ts +0 -2
- package/dist/tools/user.js +0 -12
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dokployCatalog, getCatalogEndpoint, getCatalogEndpointsByTag, } from '../../generated/dokploy-catalog.js';
|
|
2
|
+
import { procedureSchemas } from '../../generated/dokploy-schemas.js';
|
|
3
|
+
export function createSearchCatalogView() {
|
|
4
|
+
return {
|
|
5
|
+
endpoints: dokployCatalog.endpoints,
|
|
6
|
+
byTag: dokployCatalog.byTag,
|
|
7
|
+
byProcedure: dokployCatalog.byProcedure,
|
|
8
|
+
byPath: dokployCatalog.byPath,
|
|
9
|
+
get: (id) => {
|
|
10
|
+
const endpoint = getCatalogEndpoint(id);
|
|
11
|
+
if (!endpoint)
|
|
12
|
+
return null;
|
|
13
|
+
const procedure = endpoint.procedure;
|
|
14
|
+
const schema = procedureSchemas[procedure];
|
|
15
|
+
return {
|
|
16
|
+
...endpoint,
|
|
17
|
+
inputSchema: schema?.inputSchema ?? null,
|
|
18
|
+
outputSchema: schema?.outputSchema ?? null,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
getByTag: (tag) => getCatalogEndpointsByTag(tag),
|
|
22
|
+
searchText: (query) => {
|
|
23
|
+
const normalized = query.trim().toLowerCase();
|
|
24
|
+
if (normalized.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
return dokployCatalog.endpoints.filter((endpoint) => {
|
|
27
|
+
const haystack = [
|
|
28
|
+
endpoint.procedure,
|
|
29
|
+
endpoint.path,
|
|
30
|
+
endpoint.tag,
|
|
31
|
+
endpoint.summary ?? '',
|
|
32
|
+
endpoint.description ?? '',
|
|
33
|
+
...endpoint.requiredInputs,
|
|
34
|
+
...endpoint.optionalInputs,
|
|
35
|
+
]
|
|
36
|
+
.join(' ')
|
|
37
|
+
.toLowerCase();
|
|
38
|
+
return haystack.includes(normalized);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { api } from '../../api/client.js';
|
|
2
|
+
import { procedureSchemas } from '../../generated/dokploy-schemas.js';
|
|
3
|
+
import { type GatewayTraceEntry } from './trace.js';
|
|
4
|
+
type ProcedureName = keyof typeof procedureSchemas;
|
|
5
|
+
type RequestApi = typeof api;
|
|
6
|
+
export interface GatewayCallResult {
|
|
7
|
+
data: unknown;
|
|
8
|
+
trace: GatewayTraceEntry;
|
|
9
|
+
}
|
|
10
|
+
export declare function invokeProcedureWithApi(procedure: ProcedureName | string, input?: Record<string, unknown>, requestApi?: RequestApi): Promise<GatewayCallResult>;
|
|
11
|
+
export declare function invokeProcedure(procedure: ProcedureName | string, input?: Record<string, unknown>): Promise<GatewayCallResult>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ApiError, api } from '../../api/client.js';
|
|
2
|
+
import { procedureSchemas } from '../../generated/dokploy-schemas.js';
|
|
3
|
+
import { formatGatewayError } from './error-format.js';
|
|
4
|
+
import { finishTrace, startTrace } from './trace.js';
|
|
5
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
|
|
6
|
+
function resolveGatewayRetryCount() {
|
|
7
|
+
const parsed = Number.parseInt(process.env.DOKPLOY_MCP_GATEWAY_RETRIES ?? '2', 10);
|
|
8
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 2;
|
|
9
|
+
}
|
|
10
|
+
function shouldRetryGatewayError(error, method, attempt, maxRetries) {
|
|
11
|
+
if (!(error instanceof ApiError)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (method !== 'GET') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (attempt >= maxRetries) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return RETRYABLE_STATUS_CODES.has(error.status);
|
|
21
|
+
}
|
|
22
|
+
async function delay(ms) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
function validateAgainstSchema(value, schema, path = '') {
|
|
26
|
+
if (!schema || typeof schema !== 'object') {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const schemaObject = schema;
|
|
30
|
+
if (schemaObject.anyOf && Array.isArray(schemaObject.anyOf)) {
|
|
31
|
+
const variants = schemaObject.anyOf;
|
|
32
|
+
const variantErrors = variants.map((variant) => validateAgainstSchema(value, variant, path));
|
|
33
|
+
if (variantErrors.some((errors) => errors.length === 0)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return variantErrors[0] ?? [];
|
|
37
|
+
}
|
|
38
|
+
if (schemaObject.enum && Array.isArray(schemaObject.enum)) {
|
|
39
|
+
if (!schemaObject.enum.includes(value)) {
|
|
40
|
+
return [`${path || 'value'} must be one of ${schemaObject.enum.join(', ')}`];
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return validateTypedSchema(value, schemaObject, path);
|
|
45
|
+
}
|
|
46
|
+
function validateTypedSchema(value, schemaObject, path) {
|
|
47
|
+
switch (schemaObject.type) {
|
|
48
|
+
case 'object':
|
|
49
|
+
return validateObjectSchema(value, schemaObject, path);
|
|
50
|
+
case 'array':
|
|
51
|
+
return validateArraySchema(value, schemaObject, path);
|
|
52
|
+
case 'string':
|
|
53
|
+
return validatePrimitive(value, 'string', path);
|
|
54
|
+
case 'number':
|
|
55
|
+
case 'integer':
|
|
56
|
+
return validatePrimitive(value, 'number', path);
|
|
57
|
+
case 'boolean':
|
|
58
|
+
return validatePrimitive(value, 'boolean', path);
|
|
59
|
+
case 'null':
|
|
60
|
+
return value === null ? [] : [`${path || 'value'} must be null`];
|
|
61
|
+
default:
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function validateObjectSchema(value, schemaObject, path) {
|
|
66
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
67
|
+
return [`${path || 'value'} must be an object`];
|
|
68
|
+
}
|
|
69
|
+
const objectValue = value;
|
|
70
|
+
const properties = schemaObject.properties ?? {};
|
|
71
|
+
const required = schemaObject.required ?? [];
|
|
72
|
+
const errors = [];
|
|
73
|
+
for (const key of required) {
|
|
74
|
+
if (!(key in objectValue) || objectValue[key] == null) {
|
|
75
|
+
errors.push(`${path ? `${path}.` : ''}${key} is required`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
79
|
+
if (key in objectValue) {
|
|
80
|
+
errors.push(...validateAgainstSchema(objectValue[key], propertySchema, path ? `${path}.${key}` : key));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
function validateArraySchema(value, schemaObject, path) {
|
|
86
|
+
if (!Array.isArray(value)) {
|
|
87
|
+
return [`${path || 'value'} must be an array`];
|
|
88
|
+
}
|
|
89
|
+
const itemSchema = schemaObject.items;
|
|
90
|
+
const errors = [];
|
|
91
|
+
for (const [index, entry] of value.entries()) {
|
|
92
|
+
errors.push(...validateAgainstSchema(entry, itemSchema, `${path || 'value'}[${index}]`));
|
|
93
|
+
}
|
|
94
|
+
return errors;
|
|
95
|
+
}
|
|
96
|
+
function validatePrimitive(value, type, path) {
|
|
97
|
+
return typeof value === type ? [] : [`${path || 'value'} must be a ${type}`];
|
|
98
|
+
}
|
|
99
|
+
export async function invokeProcedureWithApi(procedure, input = {}, requestApi = api) {
|
|
100
|
+
const schema = procedureSchemas[procedure];
|
|
101
|
+
if (!schema) {
|
|
102
|
+
throw formatGatewayError({
|
|
103
|
+
type: 'validation_error',
|
|
104
|
+
procedure,
|
|
105
|
+
message: `Unknown Dokploy procedure: ${procedure}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const validationErrors = validateAgainstSchema(input, schema.inputSchema);
|
|
109
|
+
if (validationErrors.length > 0) {
|
|
110
|
+
throw formatGatewayError({
|
|
111
|
+
type: 'validation_error',
|
|
112
|
+
procedure,
|
|
113
|
+
message: validationErrors.join('; '),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const trace = startTrace(procedure, schema.method);
|
|
117
|
+
const maxRetries = resolveGatewayRetryCount();
|
|
118
|
+
try {
|
|
119
|
+
let attempt = 0;
|
|
120
|
+
while (true) {
|
|
121
|
+
try {
|
|
122
|
+
const data = schema.method === 'GET'
|
|
123
|
+
? await requestApi.get(schema.path, input)
|
|
124
|
+
: await requestApi.post(schema.path, input);
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
trace: finishTrace(trace),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!shouldRetryGatewayError(error, schema.method, attempt, maxRetries)) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
attempt += 1;
|
|
135
|
+
await delay(50 * attempt);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof ApiError) {
|
|
141
|
+
throw formatGatewayError({
|
|
142
|
+
type: 'dokploy_error',
|
|
143
|
+
procedure,
|
|
144
|
+
status: error.status,
|
|
145
|
+
message: error.message,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (error && typeof error === 'object' && 'type' in error && 'message' in error) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
throw formatGatewayError({
|
|
152
|
+
type: 'sandbox_error',
|
|
153
|
+
procedure,
|
|
154
|
+
message: error instanceof Error ? error.message : 'Unknown gateway error',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function invokeProcedure(procedure, input = {}) {
|
|
159
|
+
return invokeProcedureWithApi(procedure, input, api);
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GatewayErrorPayload {
|
|
2
|
+
ok: false;
|
|
3
|
+
type: 'dokploy_error' | 'validation_error' | 'sandbox_error';
|
|
4
|
+
status?: number;
|
|
5
|
+
procedure?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function formatGatewayError(input: {
|
|
9
|
+
type: GatewayErrorPayload['type'];
|
|
10
|
+
message: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
procedure?: string;
|
|
13
|
+
}): GatewayErrorPayload;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function normalizeTrpcParams(body) {
|
|
2
|
+
if (!body || typeof body !== 'object') {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value != null));
|
|
6
|
+
}
|
|
7
|
+
export function buildTrpcQueryString(body) {
|
|
8
|
+
if (body == null) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
const params = normalizeTrpcParams(body);
|
|
12
|
+
return new URLSearchParams({
|
|
13
|
+
input: JSON.stringify({ json: params }),
|
|
14
|
+
}).toString();
|
|
15
|
+
}
|
|
16
|
+
export function buildTrpcPostBody(body) {
|
|
17
|
+
if (body == null) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify({ json: body });
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GatewayTraceEntry {
|
|
2
|
+
procedure: string;
|
|
3
|
+
method: 'GET' | 'POST';
|
|
4
|
+
startedAt: number;
|
|
5
|
+
finishedAt: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function startTrace(procedure: string, method: 'GET' | 'POST'): {
|
|
9
|
+
procedure: string;
|
|
10
|
+
method: "GET" | "POST";
|
|
11
|
+
startedAt: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function finishTrace(trace: ReturnType<typeof startTrace>): GatewayTraceEntry;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function startTrace(procedure, method) {
|
|
2
|
+
return { procedure, method, startedAt: Date.now() };
|
|
3
|
+
}
|
|
4
|
+
export function finishTrace(trace) {
|
|
5
|
+
const finishedAt = Date.now();
|
|
6
|
+
return {
|
|
7
|
+
...trace,
|
|
8
|
+
finishedAt,
|
|
9
|
+
durationMs: finishedAt - trace.startedAt,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GatewayCallResult } from '../gateway/api-gateway.js';
|
|
2
|
+
interface SandboxHostOptions {
|
|
3
|
+
maxCalls?: number;
|
|
4
|
+
executor?: (procedure: string, input?: Record<string, unknown>) => Promise<GatewayCallResult>;
|
|
5
|
+
}
|
|
6
|
+
export interface SandboxHost {
|
|
7
|
+
call(procedure: string, input?: Record<string, unknown>): Promise<GatewayCallResult>;
|
|
8
|
+
getCalls(): GatewayCallResult['trace'][];
|
|
9
|
+
}
|
|
10
|
+
export declare function createSandboxHost(options?: SandboxHostOptions): SandboxHost;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { invokeProcedure } from '../gateway/api-gateway.js';
|
|
2
|
+
import { resolveSandboxLimits } from './limits.js';
|
|
3
|
+
export function createSandboxHost(options = {}) {
|
|
4
|
+
const limits = resolveSandboxLimits();
|
|
5
|
+
const maxCalls = options.maxCalls ?? limits.maxCalls;
|
|
6
|
+
const executor = options.executor ?? invokeProcedure;
|
|
7
|
+
const traces = [];
|
|
8
|
+
let callCount = 0;
|
|
9
|
+
let responseBytes = 0;
|
|
10
|
+
return {
|
|
11
|
+
async call(procedure, input = {}) {
|
|
12
|
+
callCount += 1;
|
|
13
|
+
if (callCount > maxCalls) {
|
|
14
|
+
throw new Error(`Code Mode execute exceeded ${maxCalls} API calls.`);
|
|
15
|
+
}
|
|
16
|
+
const result = await executor(procedure, input);
|
|
17
|
+
responseBytes += Buffer.byteLength(JSON.stringify(result.data), 'utf8');
|
|
18
|
+
if (responseBytes > limits.maxResponseBytes) {
|
|
19
|
+
throw new Error(`Code Mode execute exceeded ${limits.maxResponseBytes} bytes of Dokploy responses.`);
|
|
20
|
+
}
|
|
21
|
+
traces.push(result.trace);
|
|
22
|
+
return result;
|
|
23
|
+
},
|
|
24
|
+
getCalls() {
|
|
25
|
+
return traces;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
2
|
+
const DEFAULT_MAX_RESULT_BYTES = 128 * 1024;
|
|
3
|
+
const DEFAULT_MAX_LOG_BYTES = 8 * 1024;
|
|
4
|
+
const DEFAULT_MAX_CALLS = 25;
|
|
5
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_MAX_HEAP_DELTA_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
function parsePositiveInt(value, fallback) {
|
|
8
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
9
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
10
|
+
}
|
|
11
|
+
export function resolveSandboxLimits() {
|
|
12
|
+
return {
|
|
13
|
+
timeoutMs: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
|
|
14
|
+
maxResultBytes: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_MAX_RESULT_BYTES, DEFAULT_MAX_RESULT_BYTES),
|
|
15
|
+
maxLogBytes: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_MAX_LOG_BYTES, DEFAULT_MAX_LOG_BYTES),
|
|
16
|
+
maxCalls: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_MAX_CALLS, DEFAULT_MAX_CALLS),
|
|
17
|
+
maxResponseBytes: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_MAX_RESPONSE_BYTES, DEFAULT_MAX_RESPONSE_BYTES),
|
|
18
|
+
maxHeapDeltaBytes: parsePositiveInt(process.env.DOKPLOY_MCP_SANDBOX_MAX_HEAP_DELTA_BYTES, DEFAULT_MAX_HEAP_DELTA_BYTES),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SandboxExecutionResult, SandboxLimits } from './types.js';
|
|
2
|
+
interface RunSandboxedFunctionOptions<TContext extends Record<string, unknown>> {
|
|
3
|
+
code: string;
|
|
4
|
+
context: TContext;
|
|
5
|
+
limits?: SandboxLimits;
|
|
6
|
+
}
|
|
7
|
+
export declare function runSandboxedFunction<TContext extends Record<string, unknown>>({ code, context, limits: providedLimits, }: RunSandboxedFunctionOptions<TContext>): Promise<SandboxExecutionResult>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createContext, Script } from 'node:vm';
|
|
2
|
+
import { resolveSandboxLimits } from './limits.js';
|
|
3
|
+
import { serializeSandboxValue } from './serialize.js';
|
|
4
|
+
function deepFreeze(value) {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
for (const item of value) {
|
|
7
|
+
deepFreeze(item);
|
|
8
|
+
}
|
|
9
|
+
return Object.freeze(value);
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === 'object') {
|
|
12
|
+
for (const item of Object.values(value)) {
|
|
13
|
+
deepFreeze(item);
|
|
14
|
+
}
|
|
15
|
+
return Object.freeze(value);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export async function runSandboxedFunction({ code, context, limits: providedLimits, }) {
|
|
20
|
+
const limits = providedLimits ?? resolveSandboxLimits();
|
|
21
|
+
const logs = [];
|
|
22
|
+
let loggedBytes = 0;
|
|
23
|
+
const frozenContext = deepFreeze(context);
|
|
24
|
+
const sandbox = createContext({
|
|
25
|
+
__context: frozenContext,
|
|
26
|
+
console: {
|
|
27
|
+
log: (...args) => {
|
|
28
|
+
const line = args
|
|
29
|
+
.map((arg) => {
|
|
30
|
+
try {
|
|
31
|
+
return typeof arg === 'string' ? arg : JSON.stringify(arg);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return String(arg);
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.join(' ');
|
|
38
|
+
loggedBytes += Buffer.byteLength(line, 'utf8');
|
|
39
|
+
if (loggedBytes > limits.maxLogBytes) {
|
|
40
|
+
throw new Error(`Sandbox logs exceeded ${limits.maxLogBytes} bytes.`);
|
|
41
|
+
}
|
|
42
|
+
logs.push(line);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
process: undefined,
|
|
46
|
+
fetch: undefined,
|
|
47
|
+
require: undefined,
|
|
48
|
+
module: undefined,
|
|
49
|
+
exports: undefined,
|
|
50
|
+
setTimeout: undefined,
|
|
51
|
+
setInterval: undefined,
|
|
52
|
+
clearTimeout: undefined,
|
|
53
|
+
clearInterval: undefined,
|
|
54
|
+
queueMicrotask: undefined,
|
|
55
|
+
Buffer: undefined,
|
|
56
|
+
Function: undefined,
|
|
57
|
+
eval: undefined,
|
|
58
|
+
WebAssembly: undefined,
|
|
59
|
+
SharedArrayBuffer: undefined,
|
|
60
|
+
}, {
|
|
61
|
+
codeGeneration: {
|
|
62
|
+
strings: false,
|
|
63
|
+
wasm: false,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const script = new Script(`
|
|
67
|
+
(async () => {
|
|
68
|
+
const __fn = (${code})
|
|
69
|
+
if (typeof __fn !== 'function') {
|
|
70
|
+
throw new Error('Sandbox code must evaluate to a function.')
|
|
71
|
+
}
|
|
72
|
+
if (__fn.constructor?.name !== 'AsyncFunction') {
|
|
73
|
+
throw new Error('Sandbox code must evaluate to an async function.')
|
|
74
|
+
}
|
|
75
|
+
return await __fn(__context)
|
|
76
|
+
})()
|
|
77
|
+
`, {
|
|
78
|
+
filename: 'codemode-sandbox.js',
|
|
79
|
+
});
|
|
80
|
+
let timeoutId;
|
|
81
|
+
let settled = false;
|
|
82
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
83
|
+
timeoutId = setTimeout(() => {
|
|
84
|
+
if (settled) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
settled = true;
|
|
88
|
+
reject(new Error(`Sandbox execution timed out after ${limits.timeoutMs}ms.`));
|
|
89
|
+
}, limits.timeoutMs);
|
|
90
|
+
timeoutId.unref?.();
|
|
91
|
+
});
|
|
92
|
+
void timeoutPromise.catch(() => {
|
|
93
|
+
// Intentionally swallowed because Promise.race may resolve through the VM timeout path first.
|
|
94
|
+
});
|
|
95
|
+
const executionPromise = Promise.resolve(script.runInContext(sandbox, {
|
|
96
|
+
timeout: limits.timeoutMs,
|
|
97
|
+
}));
|
|
98
|
+
const result = await Promise.race([executionPromise, timeoutPromise]).finally(() => {
|
|
99
|
+
settled = true;
|
|
100
|
+
if (timeoutId) {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const serializedResult = serializeSandboxValue(result, limits.maxResultBytes);
|
|
105
|
+
const heapEstimateBytes = Buffer.byteLength(JSON.stringify(serializedResult), 'utf8');
|
|
106
|
+
if (heapEstimateBytes > limits.maxHeapDeltaBytes) {
|
|
107
|
+
throw new Error(`Sandbox heap delta exceeded ${limits.maxHeapDeltaBytes} bytes.`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
result: serializedResult,
|
|
111
|
+
logs,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serializeSandboxValue(value: unknown, maxBytes: number): unknown;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
if (typeof value !== 'object' || value === null) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return (prototype === Object.prototype ||
|
|
10
|
+
prototype === null ||
|
|
11
|
+
prototype?.constructor?.name === 'Object');
|
|
12
|
+
}
|
|
13
|
+
export function serializeSandboxValue(value, maxBytes) {
|
|
14
|
+
const normalized = normalizeValue(value);
|
|
15
|
+
const json = JSON.stringify(normalized);
|
|
16
|
+
if (json === undefined) {
|
|
17
|
+
throw new Error('Sandbox returned a non-serializable value.');
|
|
18
|
+
}
|
|
19
|
+
const bytes = Buffer.byteLength(json, 'utf8');
|
|
20
|
+
if (bytes > maxBytes) {
|
|
21
|
+
throw new Error(`Sandbox result exceeded ${maxBytes} bytes.`);
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(json);
|
|
24
|
+
}
|
|
25
|
+
function normalizeValue(value) {
|
|
26
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint') {
|
|
27
|
+
throw new Error('Sandbox returned a non-serializable value.');
|
|
28
|
+
}
|
|
29
|
+
if (value === null ||
|
|
30
|
+
typeof value === 'string' ||
|
|
31
|
+
typeof value === 'number' ||
|
|
32
|
+
typeof value === 'boolean') {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.map((entry) => normalizeValue(entry));
|
|
40
|
+
}
|
|
41
|
+
if (isPlainObject(value)) {
|
|
42
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeValue(entry)]));
|
|
43
|
+
}
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SandboxExecutionResult, SandboxLimits } from './types.js';
|
|
2
|
+
export declare function runSearchInSubprocess(options: {
|
|
3
|
+
code: string;
|
|
4
|
+
limits?: SandboxLimits;
|
|
5
|
+
}): Promise<SandboxExecutionResult>;
|
|
6
|
+
export declare function runExecuteInSubprocess(options: {
|
|
7
|
+
code: string;
|
|
8
|
+
limits?: SandboxLimits;
|
|
9
|
+
onCall: (procedure: string, input?: Record<string, unknown>) => Promise<unknown>;
|
|
10
|
+
}): Promise<SandboxExecutionResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { resolveSandboxLimits } from './limits.js';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const workerPath = resolve(__dirname, '../../../dist/codemode/sandbox/worker-entry.js');
|
|
7
|
+
function createWorker() {
|
|
8
|
+
return fork(workerPath, {
|
|
9
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
10
|
+
env: {},
|
|
11
|
+
execArgv: [],
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function resolveLimits(limits) {
|
|
15
|
+
return limits ?? resolveSandboxLimits();
|
|
16
|
+
}
|
|
17
|
+
function handleWorkerExit(rejectPromise, code) {
|
|
18
|
+
if (code && code !== 0) {
|
|
19
|
+
rejectPromise(new Error(`Sandbox worker exited with code ${code}`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function finishWorker(worker, payload, resolvePromise, rejectPromise) {
|
|
23
|
+
worker.disconnect();
|
|
24
|
+
worker.kill();
|
|
25
|
+
if (payload.ok) {
|
|
26
|
+
resolvePromise({
|
|
27
|
+
result: payload.result,
|
|
28
|
+
logs: payload.logs ?? [],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
rejectPromise(new Error(payload.error ?? 'Unknown sandbox subprocess error'));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function runSearchInSubprocess(options) {
|
|
36
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
37
|
+
const worker = createWorker();
|
|
38
|
+
worker.on('message', (message) => {
|
|
39
|
+
const payload = message;
|
|
40
|
+
if (payload.type !== 'done') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
finishWorker(worker, payload, resolvePromise, rejectPromise);
|
|
44
|
+
});
|
|
45
|
+
worker.on('error', rejectPromise);
|
|
46
|
+
worker.on('exit', (code) => handleWorkerExit(rejectPromise, code));
|
|
47
|
+
worker.send({
|
|
48
|
+
type: 'run',
|
|
49
|
+
mode: 'search',
|
|
50
|
+
code: options.code,
|
|
51
|
+
limits: resolveLimits(options.limits),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function runExecuteInSubprocess(options) {
|
|
56
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
57
|
+
const worker = createWorker();
|
|
58
|
+
worker.on('message', async (message) => {
|
|
59
|
+
const payload = message;
|
|
60
|
+
if (payload.type === 'call') {
|
|
61
|
+
const requestId = payload.requestId;
|
|
62
|
+
try {
|
|
63
|
+
const data = await options.onCall(String(payload.procedure), payload.input ?? {});
|
|
64
|
+
worker.send({ type: 'callResult', requestId, ok: true, data });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
worker.send({
|
|
68
|
+
type: 'callResult',
|
|
69
|
+
requestId,
|
|
70
|
+
ok: false,
|
|
71
|
+
error: error instanceof Error ? error.message : String(error),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const done = payload;
|
|
77
|
+
if (done.type !== 'done') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
finishWorker(worker, done, resolvePromise, rejectPromise);
|
|
81
|
+
});
|
|
82
|
+
worker.on('error', rejectPromise);
|
|
83
|
+
worker.on('exit', (code) => handleWorkerExit(rejectPromise, code));
|
|
84
|
+
worker.send({
|
|
85
|
+
type: 'run',
|
|
86
|
+
mode: 'execute',
|
|
87
|
+
code: options.code,
|
|
88
|
+
limits: resolveLimits(options.limits),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SandboxLimits {
|
|
2
|
+
timeoutMs: number;
|
|
3
|
+
maxResultBytes: number;
|
|
4
|
+
maxLogBytes: number;
|
|
5
|
+
maxCalls: number;
|
|
6
|
+
maxResponseBytes: number;
|
|
7
|
+
maxHeapDeltaBytes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SandboxExecutionResult {
|
|
10
|
+
result: unknown;
|
|
11
|
+
logs: string[];
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|