quartermaster-code-runner 0.0.1__py3-none-any.whl

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 (40) hide show
  1. quartermaster_code_runner/__init__.py +38 -0
  2. quartermaster_code_runner/app.py +269 -0
  3. quartermaster_code_runner/config.py +175 -0
  4. quartermaster_code_runner/errors.py +88 -0
  5. quartermaster_code_runner/execution.py +231 -0
  6. quartermaster_code_runner/images.py +397 -0
  7. quartermaster_code_runner/runtime/bun/Dockerfile +22 -0
  8. quartermaster_code_runner/runtime/bun/completions.json +34 -0
  9. quartermaster_code_runner/runtime/bun/entrypoint.sh +32 -0
  10. quartermaster_code_runner/runtime/bun/sdk.ts +87 -0
  11. quartermaster_code_runner/runtime/deno/Dockerfile +22 -0
  12. quartermaster_code_runner/runtime/deno/completions.json +34 -0
  13. quartermaster_code_runner/runtime/deno/entrypoint.sh +32 -0
  14. quartermaster_code_runner/runtime/deno/sdk.ts +88 -0
  15. quartermaster_code_runner/runtime/go/Dockerfile +18 -0
  16. quartermaster_code_runner/runtime/go/completions.json +22 -0
  17. quartermaster_code_runner/runtime/go/entrypoint.sh +50 -0
  18. quartermaster_code_runner/runtime/go/sdk.go +101 -0
  19. quartermaster_code_runner/runtime/node/Dockerfile +31 -0
  20. quartermaster_code_runner/runtime/node/completions.json +34 -0
  21. quartermaster_code_runner/runtime/node/entrypoint.sh +33 -0
  22. quartermaster_code_runner/runtime/node/mcp-client.js +274 -0
  23. quartermaster_code_runner/runtime/node/sdk.js +109 -0
  24. quartermaster_code_runner/runtime/python/Dockerfile +42 -0
  25. quartermaster_code_runner/runtime/python/completions.json +34 -0
  26. quartermaster_code_runner/runtime/python/entrypoint.sh +30 -0
  27. quartermaster_code_runner/runtime/python/mcp-client.py +276 -0
  28. quartermaster_code_runner/runtime/python/sdk.py +103 -0
  29. quartermaster_code_runner/runtime/rust/Cargo.toml.default +9 -0
  30. quartermaster_code_runner/runtime/rust/Dockerfile +27 -0
  31. quartermaster_code_runner/runtime/rust/completions.json +34 -0
  32. quartermaster_code_runner/runtime/rust/entrypoint.sh +38 -0
  33. quartermaster_code_runner/runtime/rust/sdk/Cargo.toml +9 -0
  34. quartermaster_code_runner/runtime/rust/sdk/src/lib.rs +149 -0
  35. quartermaster_code_runner/schemas.py +154 -0
  36. quartermaster_code_runner/security.py +81 -0
  37. quartermaster_code_runner-0.0.1.dist-info/METADATA +322 -0
  38. quartermaster_code_runner-0.0.1.dist-info/RECORD +40 -0
  39. quartermaster_code_runner-0.0.1.dist-info/WHEEL +4 -0
  40. quartermaster_code_runner-0.0.1.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * MCP Stdio Client - Executes MCP server commands and calls tools via stdio transport
3
+ *
4
+ * Environment variables:
5
+ * - MCP_COMMAND: The command to start the MCP server (e.g., "npx @modelcontextprotocol/server-slack")
6
+ * - MCP_OPERATION: The operation to perform: "list_tools" or "call_tool" (default: "call_tool")
7
+ * - MCP_TOOL_NAME: The name of the tool to call (required for call_tool operation)
8
+ * - MCP_TOOL_ARGUMENTS: JSON string of arguments to pass to the tool
9
+ * - MCP_ENV_*: Additional environment variables to pass to the MCP server (prefix stripped)
10
+ */
11
+
12
+ const { spawn } = require("child_process");
13
+ const readline = require("readline");
14
+
15
+ const MCP_PROTOCOL_VERSION = "2024-11-05";
16
+ const INITIALIZE_TIMEOUT = 60000; // 60 seconds for slow package resolution
17
+ const TOOL_CALL_TIMEOUT = 120000; // 2 minutes
18
+
19
+ class McpStdioClient {
20
+ constructor(command, serverEnv = {}) {
21
+ this.command = command;
22
+ this.serverEnv = serverEnv;
23
+ this.process = null;
24
+ this.messageId = 0;
25
+ this.pendingRequests = new Map();
26
+ this.initialized = false;
27
+ this.serverStderrLines = [];
28
+ this.processExited = false;
29
+ this.exitCode = null;
30
+ }
31
+
32
+ async start() {
33
+ return new Promise((resolve, reject) => {
34
+ const env = {
35
+ ...process.env,
36
+ ...this.serverEnv,
37
+ };
38
+
39
+ delete env.MCP_COMMAND;
40
+ delete env.MCP_TOOL_NAME;
41
+ delete env.MCP_TOOL_ARGUMENTS;
42
+
43
+ this.process = spawn(this.command, {
44
+ stdio: ["pipe", "pipe", "pipe"],
45
+ env,
46
+ shell: true,
47
+ });
48
+
49
+ this.process.on("error", (err) => {
50
+ reject(new Error(`Failed to start MCP server: ${err.message}`));
51
+ });
52
+
53
+ this.process.on("exit", (code, signal) => {
54
+ this.processExited = true;
55
+ this.exitCode = code;
56
+
57
+ const stderrTail = this.serverStderrLines.slice(-20).join("\n");
58
+ const msg = `MCP server exited with code ${code}${signal ? `, signal ${signal}` : ""}.\nServer output:\n${stderrTail}`;
59
+
60
+ for (const [id, { reject: rej }] of this.pendingRequests) {
61
+ rej(new Error(msg));
62
+ }
63
+ this.pendingRequests.clear();
64
+
65
+ if (!this.initialized) {
66
+ reject(new Error(msg));
67
+ }
68
+ });
69
+
70
+ const rl = readline.createInterface({
71
+ input: this.process.stdout,
72
+ crlfDelay: Infinity,
73
+ });
74
+
75
+ rl.on("line", (line) => {
76
+ try {
77
+ const message = JSON.parse(line);
78
+ this.handleMessage(message);
79
+ } catch (e) {
80
+ console.error("[MCP Server]", line);
81
+ }
82
+ });
83
+
84
+ this.process.stderr.on("data", (data) => {
85
+ const text = data.toString().trimEnd();
86
+ this.serverStderrLines.push(text);
87
+ console.error("[MCP Server]", text);
88
+ });
89
+
90
+ setTimeout(() => {
91
+ if (this.process && !this.process.killed && !this.processExited) {
92
+ resolve();
93
+ }
94
+ }, 500);
95
+ });
96
+ }
97
+
98
+ handleMessage(message) {
99
+ if (message.id !== undefined && this.pendingRequests.has(message.id)) {
100
+ const { resolve, reject } = this.pendingRequests.get(message.id);
101
+ this.pendingRequests.delete(message.id);
102
+
103
+ if (message.error) {
104
+ reject(
105
+ new Error(message.error.message || JSON.stringify(message.error)),
106
+ );
107
+ } else {
108
+ resolve(message.result);
109
+ }
110
+ }
111
+ }
112
+
113
+ sendRequest(method, params = {}) {
114
+ return new Promise((resolve, reject) => {
115
+ const id = ++this.messageId;
116
+ const request = {
117
+ jsonrpc: "2.0",
118
+ id,
119
+ method,
120
+ params,
121
+ };
122
+
123
+ this.pendingRequests.set(id, { resolve, reject });
124
+ this.process.stdin.write(JSON.stringify(request) + "\n");
125
+ });
126
+ }
127
+
128
+ sendNotification(method, params = {}) {
129
+ const notification = {
130
+ jsonrpc: "2.0",
131
+ method,
132
+ params,
133
+ };
134
+ this.process.stdin.write(JSON.stringify(notification) + "\n");
135
+ }
136
+
137
+ async initialize() {
138
+ const result = await Promise.race([
139
+ this.sendRequest("initialize", {
140
+ protocolVersion: MCP_PROTOCOL_VERSION,
141
+ capabilities: {},
142
+ clientInfo: {
143
+ name: "quartermaster-code-runner",
144
+ version: "1.0.0",
145
+ },
146
+ }),
147
+ new Promise((_, reject) =>
148
+ setTimeout(
149
+ () => reject(new Error("Initialize timeout")),
150
+ INITIALIZE_TIMEOUT,
151
+ ),
152
+ ),
153
+ ]);
154
+
155
+ // Send initialized notification
156
+ this.sendNotification("notifications/initialized");
157
+ this.initialized = true;
158
+
159
+ return result;
160
+ }
161
+
162
+ async listTools() {
163
+ const result = await this.sendRequest("tools/list", {});
164
+ return result.tools || [];
165
+ }
166
+
167
+ async callTool(name, arguments_) {
168
+ const result = await Promise.race([
169
+ this.sendRequest("tools/call", {
170
+ name,
171
+ arguments: arguments_,
172
+ }),
173
+ new Promise((_, reject) =>
174
+ setTimeout(
175
+ () => reject(new Error("Tool call timeout")),
176
+ TOOL_CALL_TIMEOUT,
177
+ ),
178
+ ),
179
+ ]);
180
+
181
+ // Parse content from result
182
+ const content = result.content || [];
183
+ const textParts = [];
184
+
185
+ for (const item of content) {
186
+ if (item.type === "text") {
187
+ textParts.push(item.text || "");
188
+ } else if (item.type === "image") {
189
+ textParts.push(`[Image: ${item.mimeType || "unknown"}]`);
190
+ } else if (item.type === "resource") {
191
+ textParts.push(`[Resource: ${item.uri || "unknown"}]`);
192
+ }
193
+ }
194
+
195
+ return textParts.join("\n");
196
+ }
197
+
198
+ async close() {
199
+ if (this.process && !this.process.killed) {
200
+ this.process.kill();
201
+ }
202
+ }
203
+ }
204
+
205
+ async function main() {
206
+ const command = process.env.MCP_COMMAND;
207
+ const operation = process.env.MCP_OPERATION || "call_tool";
208
+ const toolName = process.env.MCP_TOOL_NAME;
209
+ const toolArgumentsJson = process.env.MCP_TOOL_ARGUMENTS || "{}";
210
+
211
+ if (!command) {
212
+ console.error("Error: MCP_COMMAND environment variable is required");
213
+ process.exit(1);
214
+ }
215
+
216
+ if (operation === "call_tool" && !toolName) {
217
+ console.error(
218
+ "Error: MCP_TOOL_NAME environment variable is required for call_tool operation",
219
+ );
220
+ process.exit(1);
221
+ }
222
+
223
+ let toolArguments;
224
+ try {
225
+ toolArguments = JSON.parse(toolArgumentsJson);
226
+ } catch (e) {
227
+ console.error("Error: MCP_TOOL_ARGUMENTS must be valid JSON");
228
+ process.exit(1);
229
+ }
230
+
231
+ // Extract MCP_ENV_* variables for the server
232
+ const serverEnv = {};
233
+ for (const [key, value] of Object.entries(process.env)) {
234
+ if (key.startsWith("MCP_ENV_")) {
235
+ serverEnv[key.substring(8)] = value;
236
+ }
237
+ }
238
+
239
+ const client = new McpStdioClient(command, serverEnv);
240
+
241
+ try {
242
+ console.error(`Starting MCP server: ${command}`);
243
+ await client.start();
244
+
245
+ console.error("Initializing MCP connection...");
246
+ await client.initialize();
247
+
248
+ if (operation === "list_tools") {
249
+ console.error("Listing tools...");
250
+ const tools = await client.listTools();
251
+ console.log(JSON.stringify(tools));
252
+ } else {
253
+ console.error(`Calling tool: ${toolName}`);
254
+ const result = await client.callTool(toolName, toolArguments);
255
+ console.log(result);
256
+ }
257
+
258
+ await client.close();
259
+ process.exit(0);
260
+ } catch (error) {
261
+ console.error("MCP Error:", error.message);
262
+ if (client.serverStderrLines.length > 0) {
263
+ console.error("--- MCP Server stderr ---");
264
+ for (const line of client.serverStderrLines) {
265
+ console.error(line);
266
+ }
267
+ console.error("--- End server stderr ---");
268
+ }
269
+ await client.close();
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ main();
@@ -0,0 +1,109 @@
1
+ /**
2
+ * quartermaster-code-runner SDK for Node.js runtime.
3
+ */
4
+
5
+ const fs = require('fs');
6
+
7
+ const METADATA_FILE = '/metadata/.quartermaster_metadata.json';
8
+
9
+ /**
10
+ * Set the result metadata to be returned to the backend.
11
+ *
12
+ * This separates structured results from stdout/stderr logs.
13
+ *
14
+ * @param {any} data - Any JSON-serializable data (object, array, string, number, etc.)
15
+ *
16
+ * @example
17
+ * const { setMetadata } = require('./sdk');
18
+ *
19
+ * const result = { status: 'success', count: 42 };
20
+ * setMetadata(result);
21
+ */
22
+ function setMetadata(data) {
23
+ fs.writeFileSync(METADATA_FILE, JSON.stringify(data));
24
+ }
25
+
26
+ /**
27
+ * Get previously set metadata (useful for reading/modifying).
28
+ *
29
+ * @returns {any} The previously set metadata, or null if not set.
30
+ */
31
+ function getMetadata() {
32
+ if (!fs.existsSync(METADATA_FILE)) {
33
+ return null;
34
+ }
35
+ return JSON.parse(fs.readFileSync(METADATA_FILE, 'utf8'));
36
+ }
37
+
38
+ /**
39
+ * Load a file from the flow's environment.
40
+ * Only available during flow execution, not test runs.
41
+ *
42
+ * @param {string} filePath - Path to the file within the environment.
43
+ * @returns {Promise<string>} The file content as a string.
44
+ */
45
+ function loadFile(filePath) {
46
+ return new Promise((resolve, reject) => {
47
+ const webdavUrl = process.env.QM_WEBDAV_URL;
48
+ if (!webdavUrl) {
49
+ return reject(new Error(
50
+ 'loadFile() is only available during flow execution. ' +
51
+ 'For test runs, use mounted environments instead.'
52
+ ));
53
+ }
54
+ const url = new URL(filePath.replace(/^\//, ''), webdavUrl.replace(/\/?$/, '/'));
55
+ const mod = url.protocol === 'https:' ? require('https') : require('http');
56
+ mod.get(url.href, (res) => {
57
+ if (res.statusCode === 404) {
58
+ return reject(new Error(`File not found: ${filePath}`));
59
+ }
60
+ if (res.statusCode < 200 || res.statusCode >= 300) {
61
+ return reject(new Error(`Failed to load file: HTTP ${res.statusCode}`));
62
+ }
63
+ const chunks = [];
64
+ res.on('data', (chunk) => chunks.push(chunk));
65
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
66
+ res.on('error', reject);
67
+ }).on('error', reject);
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Save a file to the flow's environment.
73
+ * Only available during flow execution, not test runs.
74
+ *
75
+ * @param {string} filePath - Path to the file within the environment.
76
+ * @param {string} content - The file content to save.
77
+ * @returns {Promise<void>}
78
+ */
79
+ function saveFile(filePath, content) {
80
+ return new Promise((resolve, reject) => {
81
+ const webdavUrl = process.env.QM_WEBDAV_URL;
82
+ if (!webdavUrl) {
83
+ return reject(new Error(
84
+ 'saveFile() is only available during flow execution. ' +
85
+ 'For test runs, use mounted environments instead.'
86
+ ));
87
+ }
88
+ const url = new URL(filePath.replace(/^\//, ''), webdavUrl.replace(/\/?$/, '/'));
89
+ const mod = url.protocol === 'https:' ? require('https') : require('http');
90
+ const data = Buffer.from(content, 'utf8');
91
+ const options = {
92
+ method: 'PUT',
93
+ headers: {
94
+ 'Content-Type': 'application/octet-stream',
95
+ 'Content-Length': data.length,
96
+ },
97
+ };
98
+ const req = mod.request(url.href, options, (res) => {
99
+ if (res.statusCode < 200 || res.statusCode >= 300) {
100
+ return reject(new Error(`Failed to save file: HTTP ${res.statusCode}`));
101
+ }
102
+ resolve();
103
+ });
104
+ req.on('error', reject);
105
+ req.end(data);
106
+ });
107
+ }
108
+
109
+ module.exports = { setMetadata, getMetadata, loadFile, saveFile };
@@ -0,0 +1,42 @@
1
+ FROM python:3.12-slim
2
+
3
+ LABEL qm.name="Python"
4
+ LABEL qm.description="Python 3.12 with uv/uvx package manager"
5
+ LABEL qm.default_entrypoint="python main.py"
6
+ LABEL qm.file_extension=".py"
7
+ LABEL qm.main_file="main.py"
8
+
9
+ WORKDIR /app
10
+
11
+ RUN apt-get update && \
12
+ apt-get install -y tar curl git && \
13
+ rm -rf /var/lib/apt/lists/* && \
14
+ useradd --uid 1001 --no-create-home --shell /bin/sh runner
15
+
16
+ # Install uv package manager for uvx support
17
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
18
+ cp /root/.local/bin/uv /usr/local/bin/uv && \
19
+ cp /root/.local/bin/uvx /usr/local/bin/uvx && \
20
+ chmod 755 /usr/local/bin/uv /usr/local/bin/uvx
21
+
22
+ # Make uv tool bin dir available to all users
23
+ ENV PATH="/root/.local/bin:$PATH"
24
+
25
+ # Pre-install common Python packages
26
+ RUN pip install --no-cache-dir \
27
+ pandas \
28
+ numpy \
29
+ requests \
30
+ httpx \
31
+ beautifulsoup4 \
32
+ openpyxl \
33
+ google-auth \
34
+ google-auth-oauthlib \
35
+ google-api-python-client
36
+
37
+ COPY entrypoint.sh .
38
+ COPY sdk.py .
39
+ COPY mcp-client.py .
40
+ RUN chmod +x entrypoint.sh
41
+
42
+ ENTRYPOINT ["/app/entrypoint.sh"]
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "caption": "from sdk import set_metadata",
4
+ "value": "from sdk import set_metadata",
5
+ "meta": "import",
6
+ "score": 1000
7
+ },
8
+ {
9
+ "caption": "from sdk import get_metadata",
10
+ "value": "from sdk import get_metadata",
11
+ "meta": "import",
12
+ "score": 1000
13
+ },
14
+ {
15
+ "caption": "from sdk import set_metadata, get_metadata",
16
+ "value": "from sdk import set_metadata, get_metadata",
17
+ "meta": "import",
18
+ "score": 1000
19
+ },
20
+ {
21
+ "caption": "set_metadata",
22
+ "snippet": "set_metadata($1)",
23
+ "meta": "sdk",
24
+ "score": 1000,
25
+ "docText": "Set structured result metadata to be returned to the backend. Accepts any JSON-serializable data."
26
+ },
27
+ {
28
+ "caption": "get_metadata",
29
+ "snippet": "get_metadata()",
30
+ "meta": "sdk",
31
+ "score": 1000,
32
+ "docText": "Get previously set metadata. Returns None if not set."
33
+ }
34
+ ]
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ chown runner:runner /tmp
5
+ chown runner:runner /metadata 2>/dev/null || true
6
+
7
+ exec su -p -s /bin/sh -c '
8
+ set -e
9
+ cd /tmp
10
+
11
+ # Copy SDK for user code access
12
+ cp /app/sdk.py /tmp/sdk.py
13
+
14
+ # Decode code to default filename
15
+ if [ -n "$ENCODED_CODE" ]; then
16
+ echo "$ENCODED_CODE" | base64 -d > main.py
17
+ fi
18
+
19
+ # Extract additional files if provided
20
+ if [ -n "$ENCODED_FILES" ]; then
21
+ echo "$ENCODED_FILES" | base64 -d | tar -xz
22
+ fi
23
+
24
+ # Execute custom entrypoint or default
25
+ if [ -n "$CUSTOM_ENTRYPOINT" ]; then
26
+ exec sh -c "$CUSTOM_ENTRYPOINT"
27
+ else
28
+ exec python main.py
29
+ fi
30
+ ' runner