Skip to content

Labscript#70

Open
rong-xyz wants to merge 3 commits into
mainfrom
labscript
Open

Labscript#70
rong-xyz wants to merge 3 commits into
mainfrom
labscript

Conversation

@rong-xyz

@rong-xyz rong-xyz commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added a Labscript MCP Server providing a run_labscript tool to execute inline or file-based labscript with optional globals injection.
    • Execution includes 60-second timeout protection, structured exit/results, and robust error reporting.
  • Documentation

    • Added user documentation with usage examples, launcher invocation, and local development steps.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add labscript MCP server for experiment script execution

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add labscript MCP server for running experiment scripts
• Implement run_labscript tool supporting inline code and file paths
• Support globals injection into script namespace before execution
• Include comprehensive documentation and project configuration
Diagram
flowchart LR
  A["User Input<br/>script/script_path/globals"] --> B["run_labscript Tool"]
  B --> C["Create Temp Files<br/>if needed"]
  C --> D["Inject Globals<br/>via wrapper"]
  D --> E["Execute Python<br/>subprocess"]
  E --> F["Return Output<br/>exit code/stdout/stderr"]
Loading

Grey Divider

File Changes

1. servers/labscript/src/labscript_server/__init__.py ✨ Enhancement +3/-0

Package initialization and main export

• Export main function from module
• Provide clean public API for package initialization

servers/labscript/src/labscript_server/init.py


2. servers/labscript/src/labscript_server/main.py ✨ Enhancement +136/-0

Core MCP server with labscript execution tool

• Implement run_labscript MCP tool for executing labscript experiments
• Support inline script code and file path execution modes
• Add globals injection via builtins wrapper for parameter passing
• Handle temporary file creation/cleanup and subprocess execution with timeout
• Include comprehensive docstring explaining labscript-suite capabilities

servers/labscript/src/labscript_server/main.py


3. servers/labscript/README.md 📝 Documentation +56/-0

Complete documentation for labscript server

• Document labscript MCP server overview and capabilities
• Provide tool parameter specifications and usage examples
• Include inline script, globals injection, and file-based examples
• Document installation and local development instructions

servers/labscript/README.md


View more (2)
4. servers/labscript/manifest.json ⚙️ Configuration changes +30/-0

MCP server manifest and configuration

• Define MCP server manifest with metadata and configuration
• Specify entry point as uvx mcp-science labscript
• Document run_labscript tool in manifest
• Include author, license, and repository information

servers/labscript/manifest.json


5. servers/labscript/pyproject.toml ⚙️ Configuration changes +19/-0

Python project configuration and dependencies

• Configure Python project with name mcp-labscript version 0.1.0
• Declare dependencies on mcp[cli] and labscript-suite
• Define console script entry point mcp-labscript
• Configure hatchling build system and wheel package

servers/labscript/pyproject.toml


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (6) 📎 Requirement gaps (0)

Grey Divider


Action required

1. No run_labscript audit logs 📘 Rule violation ✓ Correctness
Description
The tool executes scripts without recording an audit event (who invoked it, what was run, and the
outcome). This prevents reconstructing critical actions and troubleshooting/security investigations.
Code

servers/labscript/src/labscript_server/main.py[R103-119]

+        # Step 3: execute
+        try:
+            result = subprocess.run(
+                [sys.executable, run_file],
+                capture_output=True,
+                text=True,
+                timeout=TIMEOUT,
+            )
+        except subprocess.TimeoutExpired:
+            return f"Exit code: -1\n--- stdout ---\n\n--- stderr ---\nExecution timed out after {TIMEOUT} seconds.\n"
+
+        # Step 4: format output
+        return (
+            f"Exit code: {result.returncode}\n"
+            f"--- stdout ---\n{result.stdout}\n"
+            f"--- stderr ---\n{result.stderr}"
+        )
Evidence
The compliance checklist requires logging critical actions with sufficient context; run_labscript
executes code via subprocess.run(...) and returns output but does not emit any audit log entry
capturing actor/context/outcome.

Rule 1: Generic: Comprehensive Audit Trails
servers/labscript/src/labscript_server/main.py[103-110]
servers/labscript/src/labscript_server/main.py[114-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`run_labscript` executes experiment scripts but does not emit audit-trail logs containing sufficient context (who/what/outcome). This makes it difficult to reconstruct critical actions.
## Issue Context
The tool runs arbitrary experiment code via a subprocess and returns stdout/stderr/exit code to the caller. This should be treated as a critical action and logged with safe, minimal context.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[21-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unvalidated script execution 📘 Rule violation ⛨ Security
Description
The tool directly executes inline code or any filesystem script_path without allowlisting,
authentication/authorization context, or sandboxing. This enables untrusted code execution and
access to local files/resources.
Code

servers/labscript/src/labscript_server/main.py[R70-110]

+        if script is not None:
+            tmp_script = tempfile.NamedTemporaryFile(
+                suffix=".py", mode="w", delete=False, encoding="utf-8"
+            )
+            tmp_script.write(script)
+            tmp_script.flush()
+            tmp_script.close()
+            labscript_file = tmp_script.name
+        else:
+            labscript_file = os.path.abspath(script_path)
+            if not os.path.isfile(labscript_file):
+                raise ValueError(f"script_path does not exist: {labscript_file}")
+
+        # Step 2: build wrapper if globals provided
+        if globals:
+            globals_repr = repr(globals)
+            wrapper_code = (
+                "import builtins as _builtins\n"
+                f"_globals_dict = {globals_repr}\n"
+                "for _k, _v in _globals_dict.items():\n"
+                "    setattr(_builtins, _k, _v)\n"
+                f"exec(open({repr(labscript_file)}).read(), {{'__file__': {repr(labscript_file)}}})\n"
+            )
+            tmp_wrapper = tempfile.NamedTemporaryFile(
+                suffix=".py", mode="w", delete=False, encoding="utf-8"
+            )
+            tmp_wrapper.write(wrapper_code)
+            tmp_wrapper.flush()
+            tmp_wrapper.close()
+            run_file = tmp_wrapper.name
+        else:
+            run_file = labscript_file
+
+        # Step 3: execute
+        try:
+            result = subprocess.run(
+                [sys.executable, run_file],
+                capture_output=True,
+                text=True,
+                timeout=TIMEOUT,
+            )
Evidence
The compliance checklist requires security-first validation/handling of external inputs; here,
untrusted inputs (script/script_path/globals) are used to create/choose a Python file and
executed with sys.executable with no restrictions beyond file existence.

Rule 6: Generic: Security-First Input Validation and Data Handling
servers/labscript/src/labscript_server/main.py[70-82]
servers/labscript/src/labscript_server/main.py[105-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server executes untrusted Python from `script` or `script_path` with minimal validation (only mutual exclusivity and file existence). This is a security risk because it allows arbitrary code execution and filesystem access.
## Issue Context
`run_labscript` is exposed as an MCP tool and will be invoked from external clients. Treat all provided inputs as untrusted.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[60-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Returns raw stderr output 📘 Rule violation ⛨ Security
Description
The tool returns raw stderr from executed scripts to the caller, which may include stack traces,
paths, and other internal details. This can leak sensitive implementation/environment information
through user-facing errors.
Code

servers/labscript/src/labscript_server/main.py[R114-119]

+        # Step 4: format output
+        return (
+            f"Exit code: {result.returncode}\n"
+            f"--- stdout ---\n{result.stdout}\n"
+            f"--- stderr ---\n{result.stderr}"
+        )
Evidence
The secure error handling requirement prohibits exposing internal details to end users; the function
formats and returns result.stderr directly, which commonly contains tracebacks and environment
details.

Rule 4: Generic: Secure Error Handling
servers/labscript/src/labscript_server/main.py[114-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`run_labscript` returns raw stderr to the caller, which can expose stack traces, file paths, and internal details.
## Issue Context
Executed scripts may fail with Python tracebacks and environment-specific information. Compliance requires user-facing errors be generic while detailed errors go to secure internal logs.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[111-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Wrapper breaks script context 🐞 Bug ✓ Correctness
Description
When globals are provided, execution switches to a temp wrapper that exec()s the real script
with a minimal globals dict; this changes __name__ and the effective import search path compared
to running the script directly, potentially breaking scripts that rely on `if __name__ ==
"__main__"` or local/relative imports.
Code

servers/labscript/src/labscript_server/main.py[R84-110]

+        if globals:
+            globals_repr = repr(globals)
+            wrapper_code = (
+                "import builtins as _builtins\n"
+                f"_globals_dict = {globals_repr}\n"
+                "for _k, _v in _globals_dict.items():\n"
+                "    setattr(_builtins, _k, _v)\n"
+                f"exec(open({repr(labscript_file)}).read(), {{'__file__': {repr(labscript_file)}}})\n"
+            )
+            tmp_wrapper = tempfile.NamedTemporaryFile(
+                suffix=".py", mode="w", delete=False, encoding="utf-8"
+            )
+            tmp_wrapper.write(wrapper_code)
+            tmp_wrapper.flush()
+            tmp_wrapper.close()
+            run_file = tmp_wrapper.name
+        else:
+            run_file = labscript_file
+
+        # Step 3: execute
+        try:
+            result = subprocess.run(
+                [sys.executable, run_file],
+                capture_output=True,
+                text=True,
+                timeout=TIMEOUT,
+            )
Evidence
The implementation creates a temporary wrapper script and runs that wrapper as the subprocess
entrypoint. The wrapper executes the target labscript via exec(..., {'__file__': ...}) without
setting __name__='__main__' or ensuring the target script’s directory is used for imports, so
behavior can diverge from python .py.

servers/labscript/src/labscript_server/main.py[84-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `globals` are provided, `run_labscript()` creates a temp wrapper file and executes the target script with `exec()` using a minimal globals dict. This can break labscript scripts that rely on normal script semantics (e.g., `__name__ == &amp;#x27;__main__&amp;#x27;`) or that import other modules located next to the labscript file.
### Issue Context
The wrapper currently executes the target script with:
- `exec(open(labscript_file).read(), {&amp;#x27;__file__&amp;#x27;: labscript_file})`
- `subprocess.run([sys.executable, run_file])` where `run_file` is the wrapper path
This changes the executed entrypoint from the labscript itself to a temp wrapper.
### Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[84-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Swallowed temp-file delete error 📘 Rule violation ⛯ Reliability
Description
Cleanup errors when deleting temporary files are silently ignored. This can hide operational
problems and leave sensitive temporary scripts on disk.
Code

servers/labscript/src/labscript_server/main.py[R121-127]

+    finally:
+        for tmp in (tmp_script, tmp_wrapper):
+            if tmp is not None:
+                try:
+                    os.unlink(tmp.name)
+                except OSError:
+                    pass
Evidence
The robust error handling requirement forbids ignored errors without logging; the OSError during
temp-file removal is caught and ignored with pass.

Rule 3: Generic: Robust Error Handling and Edge Case Management
servers/labscript/src/labscript_server/main.py[121-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Temporary file deletion failures are swallowed, preventing detection of cleanup issues and potentially leaving sensitive temp scripts on disk.
## Issue Context
The code writes inline scripts/wrappers to disk using `delete=False` and relies on a `finally` cleanup. If cleanup fails, it should be observable.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[65-127]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Unstructured basicConfig logging 📘 Rule violation ✓ Correctness
Description
Logging is configured with a plain text formatter rather than structured logs. This reduces
auditability and makes monitoring/alerting harder.
Code

servers/labscript/src/labscript_server/main.py[R10-13]

+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+)
Evidence
The secure logging requirement calls for structured logs; the configured format is an unstructured
string format rather than JSON/structured fields.

Rule 5: Generic: Secure Logging Practices
servers/labscript/src/labscript_server/main.py[10-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server configures unstructured text logs, which makes auditing and automated parsing harder.
## Issue Context
Compliance requires logs be structured for easy auditing and monitoring.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[10-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. globals parameter is misleading 📘 Rule violation ✓ Correctness
Description
The parameter name globals shadows Python's built-in globals() concept and does not clearly
convey that values are injected into script execution. This reduces code clarity and can confuse
readers and tool users.
Code

servers/labscript/src/labscript_server/main.py[R22-26]

+def run_labscript(
+    script: Optional[str] = None,
+    script_path: Optional[str] = None,
+    globals: Optional[dict] = None,
+) -> str:
Evidence
The naming compliance item requires identifiers to clearly express intent; globals is ambiguous
and conflicts with a well-known Python concept, while the implementation injects values into
builtins for the executed script.

Rule 2: Generic: Meaningful Naming and Self-Documenting Code
servers/labscript/src/labscript_server/main.py[22-26]
servers/labscript/src/labscript_server/main.py[87-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `globals` argument name is ambiguous and risks confusion with Python&amp;#x27;s `globals()`/global scope. The code actually injects parameters into the execution environment.
## Issue Context
Clear, self-documenting parameter names are required; this is a public tool interface.
## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[22-56]
- servers/labscript/README.md[15-42]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new Labscript MCP Server: packaging, manifest, README, and a FastMCP-based Python server exposing a run_labscript tool that runs inline or on-disk labscript code, supports optional globals injection, subprocess execution with timeout, and temporary-file cleanup.

Changes

Cohort / File(s) Summary
Configuration & Documentation
servers/labscript/README.md, servers/labscript/manifest.json, servers/labscript/pyproject.toml
New README, manifest, and pyproject describing the server, tool run_labscript, dependencies, packaging, and usage examples.
Package exports
servers/labscript/src/labscript_server/__init__.py
Re-exports main and defines __all__ = ["main"].
Server implementation
servers/labscript/src/labscript_server/main.py
Adds run_labscript and main: validates mutually exclusive inputs, resolves inline or file scripts (writes temp file if needed), builds wrapper for Qt stub and optional globals injection, runs script in subprocess with 60s timeout, captures stdout/stderr/exit code, and cleans up temp files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as MCP Client
    participant Server as FastMCP Server
    participant Validator as Validator
    participant Resolver as Script Resolver
    participant Executor as Subprocess Executor
    participant Cleaner as Temp Cleanup

    Client->>Server: call run_labscript(script?, script_path?, globals?)
    Server->>Validator: check parameters (mutual exclusivity / presence)
    Validator-->>Server: validation result

    Server->>Resolver: resolve script source
    alt inline script
        Resolver->>Resolver: write temporary .py file
    else file path
        Resolver->>Resolver: validate path exists
    end
    Resolver-->>Server: resolved script path

    alt globals provided
        Server->>Resolver: create wrapper injecting globals and Qt stub
        Resolver-->>Server: wrapper path
    end

    Server->>Executor: run wrapper (timeout=60s), capture stdout/stderr, exit code
    alt timeout
        Executor-->>Server: timeout (exit -1)
    else complete
        Executor-->>Server: exit code, stdout, stderr
    end

    Server->>Cleaner: remove temporary files
    Cleaner-->>Server: cleanup done
    Server-->>Client: return {exit_code, stdout, stderr}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I hop through temp files, tidy and spry,

Injecting globals, with Qt stub nearby,
Subprocess hums for sixty ticks true,
Then cleans its footprints and hands results to you,
A tiny rabbit server—hoppy review!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Labscript' is vague and generic, providing no meaningful information about what the pull request accomplishes beyond naming the component being added. Use a more descriptive title that explains the main change, such as 'Add Labscript MCP Server implementation' or 'Add Labscript MCP Server with tool runner'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch labscript

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +103 to +119
# Step 3: execute
try:
result = subprocess.run(
[sys.executable, run_file],
capture_output=True,
text=True,
timeout=TIMEOUT,
)
except subprocess.TimeoutExpired:
return f"Exit code: -1\n--- stdout ---\n\n--- stderr ---\nExecution timed out after {TIMEOUT} seconds.\n"

# Step 4: format output
return (
f"Exit code: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}\n"
f"--- stderr ---\n{result.stderr}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. No run_labscript audit logs 📘 Rule violation ✧ Quality

The tool executes scripts without recording an audit event (who invoked it, what was run, and the
outcome). This prevents reconstructing critical actions and troubleshooting/security investigations.
Agent Prompt
## Issue description
`run_labscript` executes experiment scripts but does not emit audit-trail logs containing sufficient context (who/what/outcome). This makes it difficult to reconstruct critical actions.

## Issue Context
The tool runs arbitrary experiment code via a subprocess and returns stdout/stderr/exit code to the caller. This should be treated as a critical action and logged with safe, minimal context.

## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[21-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +70 to +110
if script is not None:
tmp_script = tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
)
tmp_script.write(script)
tmp_script.flush()
tmp_script.close()
labscript_file = tmp_script.name
else:
labscript_file = os.path.abspath(script_path)
if not os.path.isfile(labscript_file):
raise ValueError(f"script_path does not exist: {labscript_file}")

# Step 2: build wrapper if globals provided
if globals:
globals_repr = repr(globals)
wrapper_code = (
"import builtins as _builtins\n"
f"_globals_dict = {globals_repr}\n"
"for _k, _v in _globals_dict.items():\n"
" setattr(_builtins, _k, _v)\n"
f"exec(open({repr(labscript_file)}).read(), {{'__file__': {repr(labscript_file)}}})\n"
)
tmp_wrapper = tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
)
tmp_wrapper.write(wrapper_code)
tmp_wrapper.flush()
tmp_wrapper.close()
run_file = tmp_wrapper.name
else:
run_file = labscript_file

# Step 3: execute
try:
result = subprocess.run(
[sys.executable, run_file],
capture_output=True,
text=True,
timeout=TIMEOUT,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Unvalidated script execution 📘 Rule violation ⛨ Security

The tool directly executes inline code or any filesystem script_path without allowlisting,
authentication/authorization context, or sandboxing. This enables untrusted code execution and
access to local files/resources.
Agent Prompt
## Issue description
The server executes untrusted Python from `script` or `script_path` with minimal validation (only mutual exclusivity and file existence). This is a security risk because it allows arbitrary code execution and filesystem access.

## Issue Context
`run_labscript` is exposed as an MCP tool and will be invoked from external clients. Treat all provided inputs as untrusted.

## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[60-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +114 to +119
# Step 4: format output
return (
f"Exit code: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}\n"
f"--- stderr ---\n{result.stderr}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Returns raw stderr output 📘 Rule violation ⛨ Security

The tool returns raw stderr from executed scripts to the caller, which may include stack traces,
paths, and other internal details. This can leak sensitive implementation/environment information
through user-facing errors.
Agent Prompt
## Issue description
`run_labscript` returns raw stderr to the caller, which can expose stack traces, file paths, and internal details.

## Issue Context
Executed scripts may fail with Python tracebacks and environment-specific information. Compliance requires user-facing errors be generic while detailed errors go to secure internal logs.

## Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[111-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +84 to +110
if globals:
globals_repr = repr(globals)
wrapper_code = (
"import builtins as _builtins\n"
f"_globals_dict = {globals_repr}\n"
"for _k, _v in _globals_dict.items():\n"
" setattr(_builtins, _k, _v)\n"
f"exec(open({repr(labscript_file)}).read(), {{'__file__': {repr(labscript_file)}}})\n"
)
tmp_wrapper = tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
)
tmp_wrapper.write(wrapper_code)
tmp_wrapper.flush()
tmp_wrapper.close()
run_file = tmp_wrapper.name
else:
run_file = labscript_file

# Step 3: execute
try:
result = subprocess.run(
[sys.executable, run_file],
capture_output=True,
text=True,
timeout=TIMEOUT,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Wrapper breaks script context 🐞 Bug ✓ Correctness

When globals are provided, execution switches to a temp wrapper that exec()s the real script
with a minimal globals dict; this changes __name__ and the effective import search path compared
to running the script directly, potentially breaking scripts that rely on `if __name__ ==
"__main__"` or local/relative imports.
Agent Prompt
### Issue description
When `globals` are provided, `run_labscript()` creates a temp wrapper file and executes the target script with `exec()` using a minimal globals dict. This can break labscript scripts that rely on normal script semantics (e.g., `__name__ == '__main__'`) or that import other modules located next to the labscript file.

### Issue Context
The wrapper currently executes the target script with:
- `exec(open(labscript_file).read(), {'__file__': labscript_file})`
- `subprocess.run([sys.executable, run_file])` where `run_file` is the wrapper path

This changes the executed entrypoint from the labscript itself to a temp wrapper.

### Fix Focus Areas
- servers/labscript/src/labscript_server/main.py[84-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@servers/labscript/src/labscript_server/main.py`:
- Around line 60-110: The run_labscript tool currently allows arbitrary code
execution via the script and script_path parameters (see run_labscript, script,
script_path, subprocess.run, and the tmp_wrapper/tmp_script logic); to fix,
disallow direct inline execution (reject non-empty script) unless an explicit
opt-in guard is present (e.g. require an environment variable like
ENABLE_LABSCRIPT or a server-side permission/token) and validate script_path
against a strict allowlist of base directories (resolve with os.path.realpath
and ensure it is under an allowed root) and that it is a regular file;
additionally run the subprocess with a locked-down execution context (use a
minimal env, set cwd to a safe temp dir, close file descriptors, and drop
privileges / run as a non-privileged user) and remove or restrict the
globals-injection wrapper unless explicitly permitted.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4dcc2cb and 7364a3d.

⛔ Files ignored due to path filters (1)
  • servers/labscript/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • servers/labscript/README.md
  • servers/labscript/manifest.json
  • servers/labscript/pyproject.toml
  • servers/labscript/src/labscript_server/__init__.py
  • servers/labscript/src/labscript_server/main.py

Comment on lines +60 to +110
if script is not None and script_path is not None:
raise ValueError("Provide either 'script' or 'script_path', not both.")
if script is None and script_path is None:
raise ValueError("Provide either 'script' or 'script_path'.")

tmp_script = None
tmp_wrapper = None

try:
# Step 1: resolve the labscript file path
if script is not None:
tmp_script = tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
)
tmp_script.write(script)
tmp_script.flush()
tmp_script.close()
labscript_file = tmp_script.name
else:
labscript_file = os.path.abspath(script_path)
if not os.path.isfile(labscript_file):
raise ValueError(f"script_path does not exist: {labscript_file}")

# Step 2: build wrapper if globals provided
if globals:
globals_repr = repr(globals)
wrapper_code = (
"import builtins as _builtins\n"
f"_globals_dict = {globals_repr}\n"
"for _k, _v in _globals_dict.items():\n"
" setattr(_builtins, _k, _v)\n"
f"exec(open({repr(labscript_file)}).read(), {{'__file__': {repr(labscript_file)}}})\n"
)
tmp_wrapper = tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
)
tmp_wrapper.write(wrapper_code)
tmp_wrapper.flush()
tmp_wrapper.close()
run_file = tmp_wrapper.name
else:
run_file = labscript_file

# Step 3: execute
try:
result = subprocess.run(
[sys.executable, run_file],
capture_output=True,
text=True,
timeout=TIMEOUT,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "main.py" -path "*/labscript_server/*" | head -5

Repository: pathintegral-institute/mcp.science

Length of output: 126


🏁 Script executed:

git ls-files | grep -E "labscript.*main\.py"

Repository: pathintegral-institute/mcp.science

Length of output: 124


🏁 Script executed:

fd "main.py" | grep labscript

Repository: pathintegral-institute/mcp.science

Length of output: 124


🏁 Script executed:

cat -n servers/labscript/src/labscript_server/main.py

Repository: pathintegral-institute/mcp.science

Length of output: 5829


🏁 Script executed:

find . -name "README*" -o -name "*SECURITY*" -o -name "*security*" | grep -i labscript | head -10

Repository: pathintegral-institute/mcp.science

Length of output: 107


🏁 Script executed:

find servers/labscript -type f \( -name "*.md" -o -name "*.txt" -o -name "*.rst" \) | head -10

Repository: pathintegral-institute/mcp.science

Length of output: 105


🏁 Script executed:

cat servers/labscript/README.md 2>/dev/null | head -100

Repository: pathintegral-institute/mcp.science

Length of output: 1591


🏁 Script executed:

find . -name "*.md" | xargs grep -l "security\|threat\|authentication" | head -10

Repository: pathintegral-institute/mcp.science

Length of output: 160


🏁 Script executed:

cat servers/labscript/pyproject.toml 2>/dev/null

Repository: pathintegral-institute/mcp.science

Length of output: 492


🏁 Script executed:

find . -type f -name "SECURITY*" -o -name "THREAT*"

Repository: pathintegral-institute/mcp.science

Length of output: 60


🏁 Script executed:

cat README.md | head -150

Repository: pathintegral-institute/mcp.science

Length of output: 5848


🏁 Script executed:

cat servers/python-code-execution/README.md | head -100

Repository: pathintegral-institute/mcp.science

Length of output: 1385


🏁 Script executed:

cat servers/ssh-exec/README.md | head -100

Repository: pathintegral-institute/mcp.science

Length of output: 3745


Arbitrary Python code execution with no opt-in guard or path boundaries.

The run_labscript function is exposed as an MCP tool (line 21: @mcp.tool()) and accepts arbitrary Python code via the script parameter (lines 70–77) or file paths via script_path (line 79), executing both under the server process's privileges via subprocess.run (lines 105–110). No environment variable checks, permission gates, or path restrictions exist. Any MCP client can trigger unrestricted code execution.

The suggested hardening aligns with security patterns used in similar tools like ssh-exec (which restricts commands and paths via environment variables):

Suggested hardening baseline
 def run_labscript(
     script: Optional[str] = None,
     script_path: Optional[str] = None,
     globals: Optional[dict] = None,
 ) -> str:
+    if os.getenv("MCP_LABSCRIPT_ALLOW_UNSAFE_EXEC") != "1":
+        raise PermissionError(
+            "Set MCP_LABSCRIPT_ALLOW_UNSAFE_EXEC=1 to enable script execution."
+        )
+
     if script is not None and script_path is not None:
         raise ValueError("Provide either 'script' or 'script_path', not both.")
@@
         else:
             labscript_file = os.path.abspath(script_path)
+            allowed_root = os.getenv("MCP_LABSCRIPT_ALLOWED_ROOT")
+            if allowed_root:
+                root = os.path.realpath(allowed_root)
+                real_file = os.path.realpath(labscript_file)
+                if real_file != root and not real_file.startswith(root + os.sep):
+                    raise PermissionError(f"script_path must be inside {root}")
             if not os.path.isfile(labscript_file):
                 raise ValueError(f"script_path does not exist: {labscript_file}")
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 61-61: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 63-63: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 81-81: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 91-91: Use explicit conversion flag

Replace with conversion flag

(RUF010)


[warning] 91-91: Use explicit conversion flag

Replace with conversion flag

(RUF010)


[error] 105-105: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@servers/labscript/src/labscript_server/main.py` around lines 60 - 110, The
run_labscript tool currently allows arbitrary code execution via the script and
script_path parameters (see run_labscript, script, script_path, subprocess.run,
and the tmp_wrapper/tmp_script logic); to fix, disallow direct inline execution
(reject non-empty script) unless an explicit opt-in guard is present (e.g.
require an environment variable like ENABLE_LABSCRIPT or a server-side
permission/token) and validate script_path against a strict allowlist of base
directories (resolve with os.path.realpath and ensure it is under an allowed
root) and that it is a regular file; additionally run the subprocess with a
locked-down execution context (use a minimal env, set cwd to a safe temp dir,
close file descriptors, and drop privileges / run as a non-privileged user) and
remove or restrict the globals-injection wrapper unless explicitly permitted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
servers/labscript/src/labscript_server/main.py (1)

69-129: ⚠️ Potential issue | 🔴 Critical

Add an explicit execution gate and path boundary before running scripts.

At Line 69 onward, the tool still executes caller-provided code/path under server privileges without an opt-in guard or root restriction. That leaves a high-risk remote code execution surface.

🔒 Proposed hardening baseline
 `@mcp.tool`()
 def run_labscript(
     script: Optional[str] = None,
     script_path: Optional[str] = None,
     globals: Optional[dict] = None,
 ) -> str:
+    if os.getenv("MCP_LABSCRIPT_ALLOW_UNSAFE_EXEC") != "1":
+        raise PermissionError(
+            "Set MCP_LABSCRIPT_ALLOW_UNSAFE_EXEC=1 to enable run_labscript."
+        )
+
     if script is not None and script_path is not None:
         raise ValueError("Provide either 'script' or 'script_path', not both.")
     if script is None and script_path is None:
         raise ValueError("Provide either 'script' or 'script_path'.")
@@
-        else:
-            labscript_file = os.path.abspath(script_path)
-            if not os.path.isfile(labscript_file):
+        else:
+            labscript_file = os.path.abspath(script_path)
+            allowed_root = os.getenv("MCP_LABSCRIPT_ALLOWED_ROOT")
+            if not allowed_root:
+                raise PermissionError("MCP_LABSCRIPT_ALLOWED_ROOT must be configured.")
+            root = os.path.realpath(allowed_root)
+            real_file = os.path.realpath(labscript_file)
+            if real_file != root and not real_file.startswith(root + os.sep):
+                raise PermissionError(f"script_path must be inside: {root}")
+            if not os.path.isfile(real_file):
                 raise ValueError(f"script_path does not exist: {labscript_file}")
+            labscript_file = real_file
@@
-            result = subprocess.run(
+            result = subprocess.run(
                 [sys.executable, tmp_wrapper.name],
                 capture_output=True,
                 text=True,
                 timeout=TIMEOUT,
+                cwd=tempfile.gettempdir(),
             )

Also applies to: 157-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@servers/labscript/src/labscript_server/main.py` around lines 69 - 129,
run_labscript currently executes caller-provided code/paths with server
privileges; add an explicit opt-in gate and path boundary to prevent accidental
RCE: require a new boolean flag (e.g. allow_execution) or an environment/config
check before any execution and raise if not set, enforce that the effective UID
is non-root (os.geteuid() != 0) and refuse to run as root, and validate
script_path is inside an allowed base directory (canonicalize with
os.path.realpath and ensure it startswith an allowed root) before creating
tmp_script/tmp_wrapper or executing; update run_labscript, and any helper that
invokes the temp wrapper, to perform these checks and fail early with a clear
error when the gate/constraints are not met.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@servers/labscript/src/labscript_server/main.py`:
- Around line 157-170: The subprocess.run call currently uses
capture_output=True which can buffer unlimited stdout/stderr and cause OOM;
change the execution to capture pipes but bound their size: use stdout=PIPE and
stderr=PIPE (or subprocess.Popen with streaming reads) and enforce a
MAX_OUTPUT_BYTES constant, then after the process completes (or on timeout)
truncate result.stdout and result.stderr to MAX_OUTPUT_BYTES and append a clear
"[truncated]" notice before returning; update references around the
subprocess.run call and the return that uses result.returncode, result.stdout
and result.stderr (the invocation using tmp_wrapper.name and TIMEOUT) so the
server never holds arbitrarily large child output in memory.

---

Duplicate comments:
In `@servers/labscript/src/labscript_server/main.py`:
- Around line 69-129: run_labscript currently executes caller-provided
code/paths with server privileges; add an explicit opt-in gate and path boundary
to prevent accidental RCE: require a new boolean flag (e.g. allow_execution) or
an environment/config check before any execution and raise if not set, enforce
that the effective UID is non-root (os.geteuid() != 0) and refuse to run as
root, and validate script_path is inside an allowed base directory (canonicalize
with os.path.realpath and ensure it startswith an allowed root) before creating
tmp_script/tmp_wrapper or executing; update run_labscript, and any helper that
invokes the temp wrapper, to perform these checks and fail early with a clear
error when the gate/constraints are not met.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7364a3d and 5abdcb1.

📒 Files selected for processing (1)
  • servers/labscript/src/labscript_server/main.py

Comment on lines +157 to +170
result = subprocess.run(
[sys.executable, tmp_wrapper.name],
capture_output=True,
text=True,
timeout=TIMEOUT,
)
except subprocess.TimeoutExpired:
return f"Exit code: -1\n--- stdout ---\n\n--- stderr ---\nExecution timed out after {TIMEOUT} seconds.\n"

# Step 4: format output
return (
f"Exit code: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}\n"
f"--- stderr ---\n{result.stderr}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound captured stdout/stderr to prevent memory blowups.

At Line 157, capture_output=True keeps all child output in memory. A noisy script can exhaust memory and take down the server process.

📉 Suggested capped-output pattern
 TIMEOUT = 60
+MAX_OUTPUT_BYTES = 1_000_000
@@
-            result = subprocess.run(
-                [sys.executable, tmp_wrapper.name],
-                capture_output=True,
-                text=True,
-                timeout=TIMEOUT,
-            )
+            with tempfile.TemporaryFile() as out_f, tempfile.TemporaryFile() as err_f:
+                result = subprocess.run(
+                    [sys.executable, tmp_wrapper.name],
+                    stdout=out_f,
+                    stderr=err_f,
+                    timeout=TIMEOUT,
+                )
+                out_f.seek(0)
+                err_f.seek(0)
+                stdout = out_f.read(MAX_OUTPUT_BYTES + 1).decode("utf-8", errors="replace")
+                stderr = err_f.read(MAX_OUTPUT_BYTES + 1).decode("utf-8", errors="replace")
+                if len(stdout.encode("utf-8")) > MAX_OUTPUT_BYTES:
+                    stdout = stdout[:MAX_OUTPUT_BYTES] + "\n...[truncated]"
+                if len(stderr.encode("utf-8")) > MAX_OUTPUT_BYTES:
+                    stderr = stderr[:MAX_OUTPUT_BYTES] + "\n...[truncated]"
@@
         return (
             f"Exit code: {result.returncode}\n"
-            f"--- stdout ---\n{result.stdout}\n"
-            f"--- stderr ---\n{result.stderr}"
+            f"--- stdout ---\n{stdout}\n"
+            f"--- stderr ---\n{stderr}"
         )
🧰 Tools
🪛 Ruff (0.15.2)

[error] 157-157: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@servers/labscript/src/labscript_server/main.py` around lines 157 - 170, The
subprocess.run call currently uses capture_output=True which can buffer
unlimited stdout/stderr and cause OOM; change the execution to capture pipes but
bound their size: use stdout=PIPE and stderr=PIPE (or subprocess.Popen with
streaming reads) and enforce a MAX_OUTPUT_BYTES constant, then after the process
completes (or on timeout) truncate result.stdout and result.stderr to
MAX_OUTPUT_BYTES and append a clear "[truncated]" notice before returning;
update references around the subprocess.run call and the return that uses
result.returncode, result.stdout and result.stderr (the invocation using
tmp_wrapper.name and TIMEOUT) so the server never holds arbitrarily large child
output in memory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant