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,32 @@
|
|
|
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
|
+
export HOME=/home/runner
|
|
11
|
+
export BUN_INSTALL=/home/runner/.bun
|
|
12
|
+
|
|
13
|
+
# Copy SDK for user code access
|
|
14
|
+
cp /app/sdk.ts /tmp/sdk.ts
|
|
15
|
+
|
|
16
|
+
# Decode code to default filename
|
|
17
|
+
if [ -n "$ENCODED_CODE" ]; then
|
|
18
|
+
echo "$ENCODED_CODE" | base64 -d > main.ts
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Extract additional files if provided
|
|
22
|
+
if [ -n "$ENCODED_FILES" ]; then
|
|
23
|
+
echo "$ENCODED_FILES" | base64 -d | tar -xz
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Execute custom entrypoint or default
|
|
27
|
+
if [ -n "$CUSTOM_ENTRYPOINT" ]; then
|
|
28
|
+
exec sh -c "$CUSTOM_ENTRYPOINT"
|
|
29
|
+
else
|
|
30
|
+
exec bun main.ts
|
|
31
|
+
fi
|
|
32
|
+
' runner
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quartermaster-code-runner SDK for Bun runtime.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const METADATA_FILE = "/metadata/.quartermaster_metadata.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set the result metadata to be returned to the backend.
|
|
9
|
+
*
|
|
10
|
+
* This separates structured results from stdout/stderr logs.
|
|
11
|
+
*
|
|
12
|
+
* @param data - Any JSON-serializable data (object, array, string, number, etc.)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { setMetadata } from './sdk';
|
|
16
|
+
*
|
|
17
|
+
* const result = { status: 'success', count: 42 };
|
|
18
|
+
* setMetadata(result);
|
|
19
|
+
*/
|
|
20
|
+
export function setMetadata(data: unknown): void {
|
|
21
|
+
Bun.write(METADATA_FILE, JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get previously set metadata (useful for reading/modifying).
|
|
26
|
+
*
|
|
27
|
+
* @returns The previously set metadata, or null if not set.
|
|
28
|
+
*/
|
|
29
|
+
export function getMetadata(): unknown {
|
|
30
|
+
const file = Bun.file(METADATA_FILE);
|
|
31
|
+
if (!file.size) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(file.text() as unknown as string);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load a file from the flow's environment.
|
|
39
|
+
* Only available during flow execution, not test runs.
|
|
40
|
+
*
|
|
41
|
+
* @param path - Path to the file within the environment.
|
|
42
|
+
* @returns The file content as a string.
|
|
43
|
+
*/
|
|
44
|
+
export async function loadFile(path: string): Promise<string> {
|
|
45
|
+
const webdavUrl = process.env.QM_WEBDAV_URL;
|
|
46
|
+
if (!webdavUrl) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"loadFile() is only available during flow execution. " +
|
|
49
|
+
"For test runs, use mounted environments instead.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const url = webdavUrl.replace(/\/?$/, "/") + path.replace(/^\//, "");
|
|
53
|
+
const resp = await fetch(url, { method: "GET" });
|
|
54
|
+
if (resp.status === 404) {
|
|
55
|
+
throw new Error(`File not found: ${path}`);
|
|
56
|
+
}
|
|
57
|
+
if (!resp.ok) {
|
|
58
|
+
throw new Error(`Failed to load file: HTTP ${resp.status}`);
|
|
59
|
+
}
|
|
60
|
+
return await resp.text();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Save a file to the flow's environment.
|
|
65
|
+
* Only available during flow execution, not test runs.
|
|
66
|
+
*
|
|
67
|
+
* @param path - Path to the file within the environment.
|
|
68
|
+
* @param content - The file content to save.
|
|
69
|
+
*/
|
|
70
|
+
export async function saveFile(path: string, content: string): Promise<void> {
|
|
71
|
+
const webdavUrl = process.env.QM_WEBDAV_URL;
|
|
72
|
+
if (!webdavUrl) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"saveFile() is only available during flow execution. " +
|
|
75
|
+
"For test runs, use mounted environments instead.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const url = webdavUrl.replace(/\/?$/, "/") + path.replace(/^\//, "");
|
|
79
|
+
const resp = await fetch(url, {
|
|
80
|
+
method: "PUT",
|
|
81
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
82
|
+
body: content,
|
|
83
|
+
});
|
|
84
|
+
if (!resp.ok) {
|
|
85
|
+
throw new Error(`Failed to save file: HTTP ${resp.status}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
FROM denoland/deno:latest
|
|
2
|
+
|
|
3
|
+
LABEL qm.name="Deno"
|
|
4
|
+
LABEL qm.description="Deno runtime with TypeScript support"
|
|
5
|
+
LABEL qm.default_entrypoint="deno run main.ts"
|
|
6
|
+
LABEL qm.file_extension=".ts"
|
|
7
|
+
LABEL qm.main_file="main.ts"
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
RUN apt-get update && \
|
|
12
|
+
apt-get install -y tar && \
|
|
13
|
+
rm -rf /var/lib/apt/lists/* && \
|
|
14
|
+
useradd --uid 1001 --no-create-home --shell /bin/sh runner && \
|
|
15
|
+
mkdir -p /home/runner/.cache/deno && \
|
|
16
|
+
chown -R runner:runner /home/runner
|
|
17
|
+
|
|
18
|
+
COPY entrypoint.sh .
|
|
19
|
+
COPY sdk.ts /app/sdk.ts
|
|
20
|
+
RUN chmod +x entrypoint.sh
|
|
21
|
+
|
|
22
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"caption": "import { setMetadata } from './sdk.ts'",
|
|
4
|
+
"value": "import { setMetadata } from './sdk.ts';",
|
|
5
|
+
"meta": "import",
|
|
6
|
+
"score": 1000
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"caption": "import { getMetadata } from './sdk.ts'",
|
|
10
|
+
"value": "import { getMetadata } from './sdk.ts';",
|
|
11
|
+
"meta": "import",
|
|
12
|
+
"score": 1000
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"caption": "import { setMetadata, getMetadata } from './sdk.ts'",
|
|
16
|
+
"value": "import { setMetadata, getMetadata } from './sdk.ts';",
|
|
17
|
+
"meta": "import",
|
|
18
|
+
"score": 1000
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"caption": "setMetadata",
|
|
22
|
+
"snippet": "setMetadata($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": "getMetadata",
|
|
29
|
+
"snippet": "getMetadata()",
|
|
30
|
+
"meta": "sdk",
|
|
31
|
+
"score": 1000,
|
|
32
|
+
"docText": "Get previously set metadata. Returns null if not set."
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export HOME=/home/runner
|
|
11
|
+
export DENO_DIR=/home/runner/.cache/deno
|
|
12
|
+
|
|
13
|
+
# Copy SDK for user code access
|
|
14
|
+
cp /app/sdk.ts /tmp/sdk.ts
|
|
15
|
+
|
|
16
|
+
# Decode code to default filename
|
|
17
|
+
if [ -n "$ENCODED_CODE" ]; then
|
|
18
|
+
echo "$ENCODED_CODE" | base64 -d > main.ts
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Extract additional files if provided
|
|
22
|
+
if [ -n "$ENCODED_FILES" ]; then
|
|
23
|
+
echo "$ENCODED_FILES" | base64 -d | tar -xz
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Execute custom entrypoint or default (allow all permissions for simplicity)
|
|
27
|
+
if [ -n "$CUSTOM_ENTRYPOINT" ]; then
|
|
28
|
+
exec sh -c "$CUSTOM_ENTRYPOINT"
|
|
29
|
+
else
|
|
30
|
+
exec deno run --allow-all main.ts
|
|
31
|
+
fi
|
|
32
|
+
' runner
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quartermaster-code-runner SDK for Deno runtime.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const METADATA_FILE = "/metadata/.quartermaster_metadata.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set the result metadata to be returned to the backend.
|
|
9
|
+
*
|
|
10
|
+
* This separates structured results from stdout/stderr logs.
|
|
11
|
+
*
|
|
12
|
+
* @param data - Any JSON-serializable data (object, array, string, number, etc.)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { setMetadata } from './sdk.ts';
|
|
16
|
+
*
|
|
17
|
+
* const result = { status: 'success', count: 42 };
|
|
18
|
+
* setMetadata(result);
|
|
19
|
+
*/
|
|
20
|
+
export function setMetadata(data: unknown): void {
|
|
21
|
+
Deno.writeTextFileSync(METADATA_FILE, JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get previously set metadata (useful for reading/modifying).
|
|
26
|
+
*
|
|
27
|
+
* @returns The previously set metadata, or null if not set.
|
|
28
|
+
*/
|
|
29
|
+
export function getMetadata(): unknown {
|
|
30
|
+
try {
|
|
31
|
+
const content = Deno.readTextFileSync(METADATA_FILE);
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load a file from the flow's environment.
|
|
40
|
+
* Only available during flow execution, not test runs.
|
|
41
|
+
*
|
|
42
|
+
* @param path - Path to the file within the environment.
|
|
43
|
+
* @returns The file content as a string.
|
|
44
|
+
*/
|
|
45
|
+
export async function loadFile(path: string): Promise<string> {
|
|
46
|
+
const webdavUrl = Deno.env.get("QM_WEBDAV_URL");
|
|
47
|
+
if (!webdavUrl) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"loadFile() is only available during flow execution. " +
|
|
50
|
+
"For test runs, use mounted environments instead.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const url = webdavUrl.replace(/\/?$/, "/") + path.replace(/^\//, "");
|
|
54
|
+
const resp = await fetch(url, { method: "GET" });
|
|
55
|
+
if (resp.status === 404) {
|
|
56
|
+
throw new Error(`File not found: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
if (!resp.ok) {
|
|
59
|
+
throw new Error(`Failed to load file: HTTP ${resp.status}`);
|
|
60
|
+
}
|
|
61
|
+
return await resp.text();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save a file to the flow's environment.
|
|
66
|
+
* Only available during flow execution, not test runs.
|
|
67
|
+
*
|
|
68
|
+
* @param path - Path to the file within the environment.
|
|
69
|
+
* @param content - The file content to save.
|
|
70
|
+
*/
|
|
71
|
+
export async function saveFile(path: string, content: string): Promise<void> {
|
|
72
|
+
const webdavUrl = Deno.env.get("QM_WEBDAV_URL");
|
|
73
|
+
if (!webdavUrl) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"saveFile() is only available during flow execution. " +
|
|
76
|
+
"For test runs, use mounted environments instead.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const url = webdavUrl.replace(/\/?$/, "/") + path.replace(/^\//, "");
|
|
80
|
+
const resp = await fetch(url, {
|
|
81
|
+
method: "PUT",
|
|
82
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
83
|
+
body: content,
|
|
84
|
+
});
|
|
85
|
+
if (!resp.ok) {
|
|
86
|
+
throw new Error(`Failed to save file: HTTP ${resp.status}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM golang:1.22-alpine
|
|
2
|
+
|
|
3
|
+
LABEL qm.name="Go"
|
|
4
|
+
LABEL qm.description="Go 1.22 runtime"
|
|
5
|
+
LABEL qm.default_entrypoint="go run main.go"
|
|
6
|
+
LABEL qm.file_extension=".go"
|
|
7
|
+
LABEL qm.main_file="main.go"
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
RUN apk add --no-cache tar && \
|
|
12
|
+
adduser -D -u 1001 -s /bin/sh runner
|
|
13
|
+
|
|
14
|
+
COPY entrypoint.sh .
|
|
15
|
+
COPY sdk.go .
|
|
16
|
+
RUN chmod +x entrypoint.sh
|
|
17
|
+
|
|
18
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"caption": "import \"sdk\"",
|
|
4
|
+
"value": "import \"sdk\"",
|
|
5
|
+
"meta": "import",
|
|
6
|
+
"score": 1000
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"caption": "sdk.SetMetadata",
|
|
10
|
+
"snippet": "sdk.SetMetadata($1)",
|
|
11
|
+
"meta": "sdk",
|
|
12
|
+
"score": 1000,
|
|
13
|
+
"docText": "Set structured result metadata to be returned to the backend. Accepts any JSON-serializable data."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"caption": "sdk.GetMetadata",
|
|
17
|
+
"snippet": "sdk.GetMetadata(&$1)",
|
|
18
|
+
"meta": "sdk",
|
|
19
|
+
"score": 1000,
|
|
20
|
+
"docText": "Get previously set metadata into the provided destination. Returns nil if no metadata has been set."
|
|
21
|
+
}
|
|
22
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
chown runner:runner /tmp
|
|
5
|
+
chown runner:runner /metadata 2>/dev/null || true
|
|
6
|
+
chown runner:runner /workspace 2>/dev/null || true
|
|
7
|
+
|
|
8
|
+
exec su -p -s /bin/sh -c '
|
|
9
|
+
set -e
|
|
10
|
+
cd /workspace
|
|
11
|
+
export HOME=/workspace
|
|
12
|
+
export GOCACHE=/workspace/.cache/go-build
|
|
13
|
+
export GOFLAGS="-buildvcs=false"
|
|
14
|
+
mkdir -p $GOCACHE
|
|
15
|
+
|
|
16
|
+
# Copy SDK for user code access and create go module structure
|
|
17
|
+
mkdir -p /workspace/sdk
|
|
18
|
+
cp /app/sdk.go /workspace/sdk/sdk.go
|
|
19
|
+
|
|
20
|
+
# Decode code to default filename
|
|
21
|
+
if [ -n "$ENCODED_CODE" ]; then
|
|
22
|
+
echo "$ENCODED_CODE" | base64 -d > main.go
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Extract additional files if provided
|
|
26
|
+
if [ -n "$ENCODED_FILES" ]; then
|
|
27
|
+
echo "$ENCODED_FILES" | base64 -d | tar -xz
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Create go.mod for the user code with local SDK
|
|
31
|
+
cat > go.mod << EOF
|
|
32
|
+
module main
|
|
33
|
+
go 1.22
|
|
34
|
+
require sdk v0.0.0
|
|
35
|
+
replace sdk => ./sdk
|
|
36
|
+
EOF
|
|
37
|
+
|
|
38
|
+
# Create go.mod for the SDK
|
|
39
|
+
cat > /workspace/sdk/go.mod << EOF
|
|
40
|
+
module sdk
|
|
41
|
+
go 1.22
|
|
42
|
+
EOF
|
|
43
|
+
|
|
44
|
+
# Execute custom entrypoint or default
|
|
45
|
+
if [ -n "$CUSTOM_ENTRYPOINT" ]; then
|
|
46
|
+
exec sh -c "$CUSTOM_ENTRYPOINT"
|
|
47
|
+
else
|
|
48
|
+
exec go run main.go
|
|
49
|
+
fi
|
|
50
|
+
' runner
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Package sdk provides quartermaster-code-runner SDK for Go runtime.
|
|
2
|
+
package sdk
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"os"
|
|
11
|
+
"strings"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const metadataFile = "/metadata/.quartermaster_metadata.json"
|
|
15
|
+
|
|
16
|
+
// SetMetadata sets the result metadata to be returned to the backend.
|
|
17
|
+
//
|
|
18
|
+
// This separates structured results from stdout/stderr logs.
|
|
19
|
+
//
|
|
20
|
+
// Example:
|
|
21
|
+
//
|
|
22
|
+
// import "sdk"
|
|
23
|
+
//
|
|
24
|
+
// result := map[string]interface{}{"status": "success", "count": 42}
|
|
25
|
+
// sdk.SetMetadata(result)
|
|
26
|
+
func SetMetadata(data interface{}) error {
|
|
27
|
+
jsonData, err := json.Marshal(data)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
return os.WriteFile(metadataFile, jsonData, 0644)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// GetMetadata gets previously set metadata (useful for reading/modifying).
|
|
35
|
+
//
|
|
36
|
+
// Returns nil if no metadata has been set.
|
|
37
|
+
func GetMetadata(dest interface{}) error {
|
|
38
|
+
data, err := os.ReadFile(metadataFile)
|
|
39
|
+
if err != nil {
|
|
40
|
+
if os.IsNotExist(err) {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
return err
|
|
44
|
+
}
|
|
45
|
+
return json.Unmarshal(data, dest)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var errNoWebDAV = errors.New(
|
|
49
|
+
"LoadFile/SaveFile is only available during flow execution. " +
|
|
50
|
+
"For test runs, use mounted environments instead",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// LoadFile loads a file from the flow's environment.
|
|
54
|
+
// Only available during flow execution, not test runs.
|
|
55
|
+
func LoadFile(path string) (string, error) {
|
|
56
|
+
webdavURL := os.Getenv("QM_WEBDAV_URL")
|
|
57
|
+
if webdavURL == "" {
|
|
58
|
+
return "", errNoWebDAV
|
|
59
|
+
}
|
|
60
|
+
url := strings.TrimRight(webdavURL, "/") + "/" + strings.TrimLeft(path, "/")
|
|
61
|
+
resp, err := http.Get(url)
|
|
62
|
+
if err != nil {
|
|
63
|
+
return "", fmt.Errorf("failed to load file: %w", err)
|
|
64
|
+
}
|
|
65
|
+
defer resp.Body.Close()
|
|
66
|
+
if resp.StatusCode == 404 {
|
|
67
|
+
return "", fmt.Errorf("file not found: %s", path)
|
|
68
|
+
}
|
|
69
|
+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
70
|
+
return "", fmt.Errorf("failed to load file: HTTP %d", resp.StatusCode)
|
|
71
|
+
}
|
|
72
|
+
body, err := io.ReadAll(resp.Body)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return "", fmt.Errorf("failed to read response: %w", err)
|
|
75
|
+
}
|
|
76
|
+
return string(body), nil
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// SaveFile saves a file to the flow's environment.
|
|
80
|
+
// Only available during flow execution, not test runs.
|
|
81
|
+
func SaveFile(path, content string) error {
|
|
82
|
+
webdavURL := os.Getenv("QM_WEBDAV_URL")
|
|
83
|
+
if webdavURL == "" {
|
|
84
|
+
return errNoWebDAV
|
|
85
|
+
}
|
|
86
|
+
url := strings.TrimRight(webdavURL, "/") + "/" + strings.TrimLeft(path, "/")
|
|
87
|
+
req, err := http.NewRequest("PUT", url, strings.NewReader(content))
|
|
88
|
+
if err != nil {
|
|
89
|
+
return fmt.Errorf("failed to create request: %w", err)
|
|
90
|
+
}
|
|
91
|
+
req.Header.Set("Content-Type", "application/octet-stream")
|
|
92
|
+
resp, err := http.DefaultClient.Do(req)
|
|
93
|
+
if err != nil {
|
|
94
|
+
return fmt.Errorf("failed to save file: %w", err)
|
|
95
|
+
}
|
|
96
|
+
defer resp.Body.Close()
|
|
97
|
+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
98
|
+
return fmt.Errorf("failed to save file: HTTP %d", resp.StatusCode)
|
|
99
|
+
}
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
FROM node:22-slim
|
|
2
|
+
|
|
3
|
+
LABEL qm.name="Node.js"
|
|
4
|
+
LABEL qm.description="Node.js 22 with npm, npx, and bun"
|
|
5
|
+
LABEL qm.default_entrypoint="node main.js"
|
|
6
|
+
LABEL qm.file_extension=".js"
|
|
7
|
+
LABEL qm.main_file="main.js"
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
RUN apt-get update && \
|
|
12
|
+
apt-get install -y tar curl unzip git && \
|
|
13
|
+
rm -rf /var/lib/apt/lists/* && \
|
|
14
|
+
useradd --uid 1001 --no-create-home --shell /bin/sh runner && \
|
|
15
|
+
mkdir -p /home/runner/.npm /home/runner/.bun && \
|
|
16
|
+
chown -R runner:runner /home/runner
|
|
17
|
+
|
|
18
|
+
# Install bun globally accessible
|
|
19
|
+
ENV BUN_INSTALL=/usr/local
|
|
20
|
+
RUN curl -fsSL https://bun.sh/install | bash
|
|
21
|
+
|
|
22
|
+
# Pre-install common packages globally for faster startup
|
|
23
|
+
RUN npm install -g typescript ts-node
|
|
24
|
+
RUN npm install -g @gongrzhe/server-gmail-autoauth-mcp
|
|
25
|
+
|
|
26
|
+
COPY entrypoint.sh .
|
|
27
|
+
COPY sdk.js .
|
|
28
|
+
COPY mcp-client.js .
|
|
29
|
+
RUN chmod +x entrypoint.sh
|
|
30
|
+
|
|
31
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"caption": "const { setMetadata } = require('./sdk')",
|
|
4
|
+
"value": "const { setMetadata } = require('./sdk');",
|
|
5
|
+
"meta": "import",
|
|
6
|
+
"score": 1000
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"caption": "const { getMetadata } = require('./sdk')",
|
|
10
|
+
"value": "const { getMetadata } = require('./sdk');",
|
|
11
|
+
"meta": "import",
|
|
12
|
+
"score": 1000
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"caption": "const { setMetadata, getMetadata } = require('./sdk')",
|
|
16
|
+
"value": "const { setMetadata, getMetadata } = require('./sdk');",
|
|
17
|
+
"meta": "import",
|
|
18
|
+
"score": 1000
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"caption": "setMetadata",
|
|
22
|
+
"snippet": "setMetadata($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": "getMetadata",
|
|
29
|
+
"snippet": "getMetadata()",
|
|
30
|
+
"meta": "sdk",
|
|
31
|
+
"score": 1000,
|
|
32
|
+
"docText": "Get previously set metadata. Returns null if not set."
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
export HOME=/home/runner
|
|
11
|
+
export NPM_CONFIG_CACHE=/tmp/.npm
|
|
12
|
+
|
|
13
|
+
# Copy SDK and MCP client for user code access
|
|
14
|
+
cp /app/sdk.js /tmp/sdk.js
|
|
15
|
+
cp /app/mcp-client.js /tmp/mcp-client.js
|
|
16
|
+
|
|
17
|
+
# Decode code to default filename
|
|
18
|
+
if [ -n "$ENCODED_CODE" ]; then
|
|
19
|
+
echo "$ENCODED_CODE" | base64 -d > main.js
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Extract additional files if provided
|
|
23
|
+
if [ -n "$ENCODED_FILES" ]; then
|
|
24
|
+
echo "$ENCODED_FILES" | base64 -d | tar -xz
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Execute custom entrypoint or default
|
|
28
|
+
if [ -n "$CUSTOM_ENTRYPOINT" ]; then
|
|
29
|
+
exec sh -c "$CUSTOM_ENTRYPOINT"
|
|
30
|
+
else
|
|
31
|
+
exec node main.js
|
|
32
|
+
fi
|
|
33
|
+
' runner
|