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.
- quartermaster_code_runner/__init__.py +38 -0
- quartermaster_code_runner/app.py +269 -0
- quartermaster_code_runner/config.py +175 -0
- quartermaster_code_runner/errors.py +88 -0
- quartermaster_code_runner/execution.py +231 -0
- quartermaster_code_runner/images.py +397 -0
- quartermaster_code_runner/runtime/bun/Dockerfile +22 -0
- quartermaster_code_runner/runtime/bun/completions.json +34 -0
- quartermaster_code_runner/runtime/bun/entrypoint.sh +32 -0
- quartermaster_code_runner/runtime/bun/sdk.ts +87 -0
- quartermaster_code_runner/runtime/deno/Dockerfile +22 -0
- quartermaster_code_runner/runtime/deno/completions.json +34 -0
- quartermaster_code_runner/runtime/deno/entrypoint.sh +32 -0
- quartermaster_code_runner/runtime/deno/sdk.ts +88 -0
- quartermaster_code_runner/runtime/go/Dockerfile +18 -0
- quartermaster_code_runner/runtime/go/completions.json +22 -0
- quartermaster_code_runner/runtime/go/entrypoint.sh +50 -0
- quartermaster_code_runner/runtime/go/sdk.go +101 -0
- quartermaster_code_runner/runtime/node/Dockerfile +31 -0
- quartermaster_code_runner/runtime/node/completions.json +34 -0
- quartermaster_code_runner/runtime/node/entrypoint.sh +33 -0
- quartermaster_code_runner/runtime/node/mcp-client.js +274 -0
- quartermaster_code_runner/runtime/node/sdk.js +109 -0
- quartermaster_code_runner/runtime/python/Dockerfile +42 -0
- quartermaster_code_runner/runtime/python/completions.json +34 -0
- quartermaster_code_runner/runtime/python/entrypoint.sh +30 -0
- quartermaster_code_runner/runtime/python/mcp-client.py +276 -0
- quartermaster_code_runner/runtime/python/sdk.py +103 -0
- quartermaster_code_runner/runtime/rust/Cargo.toml.default +9 -0
- quartermaster_code_runner/runtime/rust/Dockerfile +27 -0
- quartermaster_code_runner/runtime/rust/completions.json +34 -0
- quartermaster_code_runner/runtime/rust/entrypoint.sh +38 -0
- quartermaster_code_runner/runtime/rust/sdk/Cargo.toml +9 -0
- quartermaster_code_runner/runtime/rust/sdk/src/lib.rs +149 -0
- quartermaster_code_runner/schemas.py +154 -0
- quartermaster_code_runner/security.py +81 -0
- quartermaster_code_runner-0.0.1.dist-info/METADATA +322 -0
- quartermaster_code_runner-0.0.1.dist-info/RECORD +40 -0
- quartermaster_code_runner-0.0.1.dist-info/WHEEL +4 -0
- 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
|