feat(android): bootstrap rust android connection#2
Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe template crate is replaced with ChangesAndroid MCP and Lifecycle Crate Bootstrap
Sequence Diagram(s)sequenceDiagram
participant User
participant Main as src/main.rs
participant Lib as src/lib.rs
participant Docker
User->>Main: start / status / stop
Main->>Lib: android_spec(...)
Main->>Lib: docker_run_args(...) or docker_stop_args(...)
Main->>Docker: docker run / inspect / rm
Docker-->>Main: stdout / exit status
Main-->>User: lifecycle JSON or error
Estimated code review effort🎯 5 (Critical) | ⏱️ ~100 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)README.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) changelog.d/20260624_000000_bootstrap_android_connection.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) 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. Comment |
2a204e7 to
7fd2c8f
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
tests/integration/cli.rs (1)
17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winParse CLI status output as JSON instead of substring matching.
This makes the test resilient to formatting changes and validates structure, not just text fragments.
Suggested patch
assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("\"project_id\": \"dg-test\"")); - assert!(stdout.contains("\"android_container_name\": \"dg-test-android\"")); + let value: serde_json::Value = + serde_json::from_str(&stdout).expect("status output should be valid JSON"); + assert_eq!(value["project_id"], "dg-test"); + assert_eq!(value["android_container_name"], "dg-test-android");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/cli.rs` around lines 17 - 19, The CLI integration test currently checks status output with substring matching, which is brittle. Update the test in the CLI status assertion block to parse stdout as JSON and assert on the structured fields instead of raw text fragments, using the existing output handling around stdout and the project/android container keys to locate the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 60-95: The smoke-test pipeline in the README is broken because the
second `python3 - <<'PY'` consumes stdin for its inline script instead of
reading from `android-connection`, so the response parser never sees the pipe
output. Update the example so the request generator and response reader are
separated correctly, using the existing `android-connection` handshake flow and
the inline Python blocks as distinct producer/consumer steps.
In `@src/lib.rs`:
- Around line 98-119: The endpoint check in is_safe_adb_endpoint() is too loose
because it only enforces a colon and a character whitelist, so malformed values
like missing host/port, nonnumeric ports, or multiple colons still pass through
validate_adb_endpoint() and later fail in run_adb_raw(). Tighten the validation
by parsing the value into exactly one host and one numeric port (and reject
empty parts or extra separators) before returning Ok in validate_adb_endpoint(),
keeping the existing EndpointError path for invalid shapes. Add regression tests
for android:, :5555, android:abc, and a:b:c alongside the current endpoint test
coverage.
In `@src/mcp.rs`:
- Around line 445-462: The adb subprocess invocations in the command handling
flow can block forever because they use plain .output() without any timeout.
Update the adb connect path and the later Command::new("adb").args(args)
execution in the same mcp.rs flow to run with a bounded timeout and terminate
the child process if it expires, returning a McpToolError::CommandFailed that
reflects the timeout condition.
- Around line 530-543: The current workspace_path helper only rejects absolute
paths and parent-dir segments, so symlinked subpaths can still escape the
workspace. Update workspace_path to resolve and validate the canonicalized
workspace plus the target or parent directory before any fs::write or APK
install path use, and ensure the resulting path stays under the workspace root.
Use the existing workspace_path function as the main guard and apply the same
resolved-path check at the call sites that write files or install APKs.
- Around line 129-134: The request routing in handle_request only treats id-less
methods starting with notifications/ as notifications, which still allows
id-less tools/list, tools/call, or unknown methods to emit a response with id:
null. Update handle_request so any request with request.id.is_none() is handled
as a notification and returns None before building the id, while keeping the
existing notification-specific behavior in the method dispatch.
- Around line 80-83: Cap the peer-controlled Content-Length before allocating
the payload in the MCP request path. In `read_message`, validate the parsed
length from `parse_content_length` against a reasonable maximum and return an
error before creating the `payload` Vec or calling `read_exact` if it is too
large. Keep the guard close to the `if let Some(length)` branch so the safety
check is enforced before any allocation or blocking read.
- Around line 445-461: The ADB invocation in run_adb_raw is only using
state.spec.adb_endpoint for the initial connect, but the follow-up
Command::new("adb") call still runs unpinned and can target the wrong device.
Update the shared ADB execution path in run_adb_raw so every adb command
includes the same state.spec.adb_endpoint selection, likely by threading it
through the helper that builds the command arguments and ensuring the final adb
call uses -s with the endpoint as well.
- Around line 302-378: Escape or strictly validate all user-controlled values
before passing them into adb shell commands in android_type_text,
android_launch_app, and android_open_url. Replace the current ad hoc space
substitution in android_type_text with proper shell-safe encoding/escaping for
free-form text, and validate package/activity as Android component identifiers
before building the -n target. Ensure url is encoded or shell-escaped before
being forwarded to am start.
---
Nitpick comments:
In `@tests/integration/cli.rs`:
- Around line 17-19: The CLI integration test currently checks status output
with substring matching, which is brittle. Update the test in the CLI status
assertion block to parse stdout as JSON and assert on the structured fields
instead of raw text fragments, using the existing output handling around stdout
and the project/android container keys to locate the assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 82cd0695-7ca3-4e9d-85be-0ce42de93168
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
Cargo.tomlLICENSEREADME.mdchangelog.d/20260624_000000_bootstrap_android_connection.mdexamples/basic_usage.rssrc/bin/android-connection.rssrc/lib.rssrc/main.rssrc/mcp.rssrc/sum.rstests/integration/cli.rstests/integration/mod.rstests/integration/sum.rstests/unit/android_connection.rstests/unit/mod.rstests/unit/sum.rs
💤 Files with no reviewable changes (3)
- src/sum.rs
- tests/integration/sum.rs
- tests/unit/sum.rs
| #[must_use] | ||
| pub fn is_safe_adb_endpoint(value: &str) -> bool { | ||
| !value.is_empty() | ||
| && value.len() <= 255 | ||
| && value.contains(':') | ||
| && value.bytes().all(|byte| { | ||
| matches!( | ||
| byte, | ||
| b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':' | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> { | ||
| if is_safe_adb_endpoint(value) { | ||
| Ok(value.to_string()) | ||
| } else { | ||
| Err(EndpointError { | ||
| value: value.to_string(), | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Tighten endpoint validation beyond the character whitelist.
is_safe_adb_endpoint() still accepts malformed values like android:, :5555, android:abc, and a:b:c because it only checks for “contains :” plus allowed characters. android_spec() then blesses those values, and run_adb_raw() later forwards them to adb connect, so the promised upfront validation is bypassed and the failure moves to runtime.
Suggested fix
pub fn is_safe_adb_endpoint(value: &str) -> bool {
- !value.is_empty()
- && value.len() <= 255
- && value.contains(':')
- && value.bytes().all(|byte| {
- matches!(
- byte,
- b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':'
- )
- })
+ if value.is_empty() || value.len() > 255 {
+ return false;
+ }
+
+ let (host, port) = match value.split_once(':') {
+ Some((host, port)) if !host.is_empty() && !port.is_empty() && !port.contains(':') => {
+ (host, port)
+ }
+ _ => return false,
+ };
+
+ host.bytes().all(|byte| {
+ matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_')
+ }) && port.bytes().all(|byte| byte.is_ascii_digit())
}Please add regression cases for the malformed shapes above alongside the existing endpoint tests.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[must_use] | |
| pub fn is_safe_adb_endpoint(value: &str) -> bool { | |
| !value.is_empty() | |
| && value.len() <= 255 | |
| && value.contains(':') | |
| && value.bytes().all(|byte| { | |
| matches!( | |
| byte, | |
| b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b':' | |
| ) | |
| }) | |
| } | |
| pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> { | |
| if is_safe_adb_endpoint(value) { | |
| Ok(value.to_string()) | |
| } else { | |
| Err(EndpointError { | |
| value: value.to_string(), | |
| }) | |
| } | |
| } | |
| #[must_use] | |
| pub fn is_safe_adb_endpoint(value: &str) -> bool { | |
| if value.is_empty() || value.len() > 255 { | |
| return false; | |
| } | |
| let (host, port) = match value.split_once(':') { | |
| Some((host, port)) if !host.is_empty() && !port.is_empty() && !port.contains(':') => { | |
| (host, port) | |
| } | |
| _ => return false, | |
| }; | |
| host.bytes().all(|byte| { | |
| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_') | |
| }) && port.bytes().all(|byte| byte.is_ascii_digit()) | |
| } | |
| pub fn validate_adb_endpoint(value: &str) -> Result<String, EndpointError> { | |
| if is_safe_adb_endpoint(value) { | |
| Ok(value.to_string()) | |
| } else { | |
| Err(EndpointError { | |
| value: value.to_string(), | |
| }) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib.rs` around lines 98 - 119, The endpoint check in
is_safe_adb_endpoint() is too loose because it only enforces a colon and a
character whitelist, so malformed values like missing host/port, nonnumeric
ports, or multiple colons still pass through validate_adb_endpoint() and later
fail in run_adb_raw(). Tighten the validation by parsing the value into exactly
one host and one numeric port (and reject empty parts or extra separators)
before returning Ok in validate_adb_endpoint(), keeping the existing
EndpointError path for invalid shapes. Add regression tests for android:, :5555,
android:abc, and a:b:c alongside the current endpoint test coverage.
| if let Some(length) = parse_content_length(first_line)? { | ||
| read_headers(reader)?; | ||
| let mut payload = vec![0_u8; length]; | ||
| reader.read_exact(&mut payload)?; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Cap Content-Length before allocating the payload.
Content-Length is peer-controlled; a huge value allocates an equally huge Vec and then blocks in read_exact, which can take down the stdio server.
Proposed guard
+const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
+
fn read_next_message<R: BufRead>(reader: &mut R) -> io::Result<Option<String>> {
@@
if let Some(length) = parse_content_length(first_line)? {
+ if length > MAX_FRAME_BYTES {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("Content-Length {length} exceeds limit {MAX_FRAME_BYTES}"),
+ ));
+ }
read_headers(reader)?;
let mut payload = vec![0_u8; length];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let Some(length) = parse_content_length(first_line)? { | |
| read_headers(reader)?; | |
| let mut payload = vec![0_u8; length]; | |
| reader.read_exact(&mut payload)?; | |
| const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024; | |
| if let Some(length) = parse_content_length(first_line)? { | |
| if length > MAX_FRAME_BYTES { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("Content-Length {length} exceeds limit {MAX_FRAME_BYTES}"), | |
| )); | |
| } | |
| read_headers(reader)?; | |
| let mut payload = vec![0_u8; length]; | |
| reader.read_exact(&mut payload)?; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 80 - 83, Cap the peer-controlled Content-Length
before allocating the payload in the MCP request path. In `read_message`,
validate the parsed length from `parse_content_length` against a reasonable
maximum and return an error before creating the `payload` Vec or calling
`read_exact` if it is too large. Keep the guard close to the `if let
Some(length)` branch so the safety check is enforced before any allocation or
blocking read.
| fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> { | ||
| if request.id.is_none() && request.method.starts_with("notifications/") { | ||
| return None; | ||
| } | ||
|
|
||
| let id = request.id.clone().unwrap_or(Value::Null); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Treat every request without an id as a notification.
JSON-RPC notifications are any request without id; currently id-less tools/list, tools/call, or unknown methods still emit responses with id: null, which can desynchronize strict clients.
Proposed fix
- if request.id.is_none() && request.method.starts_with("notifications/") {
+ if request.id.is_none() {
return None;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> { | |
| if request.id.is_none() && request.method.starts_with("notifications/") { | |
| return None; | |
| } | |
| let id = request.id.clone().unwrap_or(Value::Null); | |
| fn handle_request(request: &JsonRpcRequest, state: &McpState) -> Option<Value> { | |
| if request.id.is_none() { | |
| return None; | |
| } | |
| let id = request.id.clone().unwrap_or(Value::Null); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 129 - 134, The request routing in handle_request
only treats id-less methods starting with notifications/ as notifications, which
still allows id-less tools/list, tools/call, or unknown methods to emit a
response with id: null. Update handle_request so any request with
request.id.is_none() is handled as a notification and returns None before
building the id, while keeping the existing notification-specific behavior in
the method dispatch.
| fn android_type_text(state: &McpState, arguments: &Value) -> Result<String, McpToolError> { | ||
| let text = string_argument(arguments, "text")?.replace(' ', "%s"); | ||
| run_adb( | ||
| state, | ||
| &[ | ||
| "shell".to_string(), | ||
| "input".to_string(), | ||
| "text".to_string(), | ||
| text, | ||
| ], | ||
| ) | ||
| } | ||
|
|
||
| fn android_press_key(state: &McpState, arguments: &Value) -> Result<String, McpToolError> { | ||
| let keycode = string_argument(arguments, "keycode")?; | ||
| if !keycode | ||
| .bytes() | ||
| .all(|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) | ||
| { | ||
| return Err(McpToolError::InvalidArgument( | ||
| "keycode may contain only ASCII letters, digits, and '_'".to_string(), | ||
| )); | ||
| } | ||
| run_adb( | ||
| state, | ||
| &[ | ||
| "shell".to_string(), | ||
| "input".to_string(), | ||
| "keyevent".to_string(), | ||
| keycode, | ||
| ], | ||
| ) | ||
| } | ||
|
|
||
| fn android_launch_app(state: &McpState, arguments: &Value) -> Result<String, McpToolError> { | ||
| let package_name = string_argument(arguments, "package")?; | ||
| let activity = optional_string_argument(arguments, "activity")?; | ||
| match activity { | ||
| Some(activity) if !activity.is_empty() => run_adb( | ||
| state, | ||
| &[ | ||
| "shell".to_string(), | ||
| "am".to_string(), | ||
| "start".to_string(), | ||
| "-n".to_string(), | ||
| format!("{package_name}/{activity}"), | ||
| ], | ||
| ), | ||
| _ => run_adb( | ||
| state, | ||
| &[ | ||
| "shell".to_string(), | ||
| "monkey".to_string(), | ||
| "-p".to_string(), | ||
| package_name, | ||
| "-c".to_string(), | ||
| "android.intent.category.LAUNCHER".to_string(), | ||
| "1".to_string(), | ||
| ], | ||
| ), | ||
| } | ||
| } | ||
|
|
||
| fn android_open_url(state: &McpState, arguments: &Value) -> Result<String, McpToolError> { | ||
| let url = string_argument(arguments, "url")?; | ||
| run_adb( | ||
| state, | ||
| &[ | ||
| "shell".to_string(), | ||
| "am".to_string(), | ||
| "start".to_string(), | ||
| "-a".to_string(), | ||
| "android.intent.action.VIEW".to_string(), | ||
| "-d".to_string(), | ||
| url, | ||
| ], | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
ast-grep outline src/mcp.rs --view expandedRepository: ProverCoderAI/rust-android-connection
Length of output: 3391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
wc -l src/mcp.rs
printf '\n--- outline ---\n'
ast-grep outline src/mcp.rs --view expanded | sed -n '1,220p'
printf '\n--- relevant lines ---\n'
sed -n '1,420p' src/mcp.rs | cat -n | sed -n '250,410p'Repository: ProverCoderAI/rust-android-connection
Length of output: 9534
🏁 Script executed:
sed -n '435,520p' src/mcp.rs | cat -nRepository: ProverCoderAI/rust-android-connection
Length of output: 3559
🏁 Script executed:
sed -n '435,520p' src/mcp.rs | cat -nRepository: ProverCoderAI/rust-android-connection
Length of output: 3559
🏁 Script executed:
sed -n '435,520p' src/mcp.rs | cat -nRepository: ProverCoderAI/rust-android-connection
Length of output: 3559
🏁 Script executed:
rg -n "adb shell|input text|am start|monkey|escaping|quote|shell metachar|adb connect" -S README* srcRepository: ProverCoderAI/rust-android-connection
Length of output: 463
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "adb shell|input text|am start|monkey|escaping|quote|shell metachar|adb connect" -S README* src || trueRepository: ProverCoderAI/rust-android-connection
Length of output: 463
🌐 Web query:
ADB shell argument quoting escaping remote shell metacharacters multiple arguments adb shell input text am start docs
💡 Result:
When running commands through adb shell, arguments must be quoted to survive two distinct layers of shell interpretation: your local (host) shell and the Android device's remote shell [1][2][3]. Because adb shell joins all arguments into a single string and executes them via the device's shell, improper quoting often results in arguments being split, special characters being consumed, or quotes being stripped before they reach the intended command [1][2]. For robust execution, follow these general principles: 1. Wrap the entire remote command in quotes: To ensure the host shell does not interpret characters like &, |, <, or >, pass the entire remote command string as a single argument to adb [1][3][4]. Example: adb shell "am start -n package/.Activity -e key 'value'" [2][3]. 2. Escape shell metacharacters: Special characters (', ", &, (, ), <, >, |, ;, \, backtick, *, ~) must be escaped to survive both the host and device shells [5][6]. If you find your quotes being stripped, you may need to use nested or triple-quoting, such as '''\"value\"''' [7][3]. 3. Handling adb shell input text: This command has specific, stricter requirements than general shell commands [5]. * Spaces: Must be encoded as %s [8][9][5]. * Metacharacters: Any shell metacharacter must be escaped (often with a backslash) [5][6]. * Example: To input "Hello World!", use adb shell input text 'Hello%sWorld\!' [8][5]. 4. Using am start (Activity Manager): When passing arguments like JSON or strings with spaces, the most reliable method is to wrap the full command in double quotes and use single quotes for the internal string values [2][10][3]. * For JSON strings: Use complex nested quoting to preserve inner double quotes [7][3]. * Pattern: adb shell "am start -n package/.Activity -e key '{\"json\":\"data\"}'" [3]. If you find that your local shell (like PowerShell or Bash) is consuming too many quotes, you can test by escaping the quotes with backslashes or using a different quoting style (e.g., switching from double quotes to single quotes for the outer wrapper) [7][2][10]. If execution remains unreliable, consider writing the command to a file, pushing it to the device, and executing the script directly with adb shell sh /path/to/script.sh [1][7]. Top results: [1][7][5][2][3]
Citations:
- 1: https://stackoverflow.com/questions/51827034/adb-shell-are-these-two-shells-chained
- 2: https://stackoverflow.com/questions/57931024/adb-starting-activity-with-extra-string-data
- 3: https://stackoverflow.com/questions/27940118/sending-json-as-extra-data-in-an-android-broadcast-via-adb-gets-incorrectly-form
- 4: Using "optionalIntentArguments" to pass a JSON string argument to an activity fails due to quoting issues through 2 levels of shells (host -> Android) appium/appium-adb#91
- 5: https://github.com/skydoves/android-testing-skills/blob/main/adb/control/injecting-input-and-state/SKILL.md
- 6: https://stackoverflow.com/questions/25791423/adb-shell-input-text-does-not-take-ampersand-character
- 7: https://stackoverflow.com/questions/42016553/preserving-json-quotes-sent-via-adb-shell
- 8: https://stackoverflow.com/questions/45041320/adb-shell-input-text-with-space
- 9: https://stackoverflow.com/questions/3978299/typing-on-an-android-device-straight-from-computer
- 10: https://stackoverflow.com/questions/32470219/mac-os-x-send-adb-shell-am-start-intent-string-extra-with-space-characters
🌐 Web query:
adb shell multiple arguments remote shell quoting documentation "adb shell" "shell metacharacters" Android developers
💡 Result:
Since Android Platform Tools 23, adb handles arguments using ssh(1)-like semantics [1][2]. This means that when you run a command via adb shell, the arguments are joined with spaces into a single string and passed to the remote device, where they are interpreted by the remote shell [3][4][2]. Because the command string passes through both your local shell and the remote shell on the device, any shell metacharacters (such as quotes, backticks, or redirection operators) risk being interpreted prematurely by the local shell [4][1][2]. To ensure commands reach the remote device as intended, you generally need to quote or escape the arguments twice: once for the local shell and once for the remote shell [1][2]. Key strategies for handling arguments: 1. Double Quoting: Wrap the entire remote command in quotes, and use inner quotes for arguments that require them [1][2]. Example: adb shell setprop key "'two words'" In this case, the local shell consumes the outer quotes, and the remote shell receives setprop key 'two words' [1][2]. 2. Escaping: Alternatively, you can use backslashes to escape metacharacters, though nesting quotes is often more readable [1][2]. Example: adb shell ping `cat /data/my_address.pst` 3. Complexity Handling: For highly complex commands involving nested quotes, JSON, or multiple metacharacters, it is often safer to avoid the complexity of escaping entirely by writing the command to a script file, pushing that file to the device, and executing the script via adb shell [5]. Example: adb push myscript.sh /data/local/tmp/ adb shell sh /data/local/tmp/myscript.sh This approach bypasses the pitfalls of multi-stage shell interpretation [5].
Citations:
- 1: https://developer.android.google.cn/tools/adb
- 2: https://developer.android.com/tools/adb
- 3: https://android.stackexchange.com/questions/201680/how-to-correctly-quote-adb-shell-su-c
- 4: https://stackoverflow.com/questions/51827034/adb-shell-are-these-two-shells-chained
- 5: https://stackoverflow.com/questions/42016553/preserving-json-quotes-sent-via-adb-shell
🌐 Web query:
site:developer.android.com adb shell quoting escaping arguments input text am start url
💡 Result:
When passing arguments containing special characters, spaces, or shell metacharacters through adb shell, you must account for the local shell (the one on your computer) and the remote shell (the one on the Android device) [1][2]. Since Android Platform Tools 23, adb handles arguments similarly to ssh, meaning command arguments are passed through the local shell and then interpreted by the remote shell [1][2]. Key Quoting and Escaping Rules: 1. Double Quoting (The "Quote Twice" Method): If you need to pass a string containing spaces or shell-sensitive characters (like ampersands or wildcards) so that the remote shell receives them intact, wrap the entire command argument in an outer layer of quotes (for the local shell) and an inner layer of quotes (for the remote shell) [1]. Example: adb shell setprop key "'two words'" [1] 2. Escaping Special Characters: If a URL or argument contains characters that have special meaning to the shell (such as & or *), you can escape them with a backslash () [3][4]. Example: adb shell am start -a android.intent.action.VIEW -d "example://path?param=red&color=blue" [3] 3. URI Encoding for URLs: For URLs or intent data strings, ensure the content itself is properly URI-encoded before passing it to adb [4][5]. Characters like spaces, ampersands, and commas should be replaced by their percent-encoded equivalents (e.g., %20 for space, %26 for &) [5]. Example: adb shell am start -a android.intent.action.VIEW -d 'geo:0,0?q=restaurants+nearby' [4] 4. JSON Arguments: When passing JSON strings (common for certain commands), wrap the entire JSON object in single quotes to ensure the shell does not attempt to interpret its internal braces or quotes [6]. Example: adb shell cmd app_function execute-app-function --parameters '{"key": "value"}' [6] If you encounter issues, verify if your local shell (e.g., bash, zsh, or Windows CMD/PowerShell) is stripping quotes or escaping characters before they reach adb. Using the "quote twice" approach or wrapping complex strings in single quotes is generally the most reliable way to preserve the argument structure [1][6].
Citations:
- 1: https://developer.android.com/tools/adb
- 2: https://developer.android.com/tools/adb?hl=de
- 3: https://developer.android.com/training/app-links/create-deeplinks
- 4: https://developer.android.com/training/cars/testing/gas-intents/gas-intents-console-tests.txt
- 5: https://developer.android.com/guide/components/google-maps-intents
- 6: https://developer.android.com/agents/skills/device-ai/appfunctions/references/adb-interaction-testing
Escape user-controlled values passed to adb shell. android_type_text only replaces spaces, and package, activity, and url are still shell-interpreted on the device side. Strictly validate component names and shell-escape or encode free-form text and URLs before forwarding them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 302 - 378, Escape or strictly validate all
user-controlled values before passing them into adb shell commands in
android_type_text, android_launch_app, and android_open_url. Replace the current
ad hoc space substitution in android_type_text with proper shell-safe
encoding/escaping for free-form text, and validate package/activity as Android
component identifiers before building the -n target. Ensure url is encoded or
shell-escaped before being forwarded to am start.
| let connect_output = Command::new("adb") | ||
| .arg("connect") | ||
| .arg(&state.spec.adb_endpoint) | ||
| .output() | ||
| .map_err(|error| { | ||
| McpToolError::CommandFailed(format!("failed to execute adb connect: {error}")) | ||
| })?; | ||
| if !connect_output.status.success() { | ||
| return Err(McpToolError::CommandFailed(command_failure( | ||
| "adb connect", | ||
| &connect_output, | ||
| ))); | ||
| } | ||
|
|
||
| Command::new("adb") | ||
| .args(args) | ||
| .output() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
sed -n '400,520p' src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 4107
🏁 Script executed:
sed -n '400,520p' src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 4107
🏁 Script executed:
rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 756
🏁 Script executed:
sed -n '1,220p' src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 6616
🏁 Script executed:
rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 756
🏁 Script executed:
rg -n "run_adb_raw|adb_endpoint|adb connect|Command::new\\(\"adb\"\\)" src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 756
🏁 Script executed:
sed -n '1,240p' src/mcp.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 7534
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
text = Path("src/mcp.rs").read_text()
for needle in ["run_adb_raw", "adb_endpoint", "Command::new(\"adb\")", "adb connect"]:
print(f"--- {needle} ---")
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
start = max(1, i-5)
end = min(len(text.splitlines()), i+10)
for j in range(start, end+1):
print(f"{j}: {text.splitlines()[j-1]}")
break
PYRepository: ProverCoderAI/rust-android-connection
Length of output: 2827
🏁 Script executed:
rg -n "adb_endpoint|allow_install|adb_probe|McpState|connect" -S .Repository: ProverCoderAI/rust-android-connection
Length of output: 7637
🏁 Script executed:
rg -n "struct McpState|adb_probe|adb_endpoint|spec:" src -SRepository: ProverCoderAI/rust-android-connection
Length of output: 1690
Pin all ADB commands to state.spec.adb_endpoint.
run_adb_raw connects first, but then executes plain adb ...; in multi-device setups that can select the wrong target or fail. Pass -s <endpoint> through the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 445 - 461, The ADB invocation in run_adb_raw is only
using state.spec.adb_endpoint for the initial connect, but the follow-up
Command::new("adb") call still runs unpinned and can target the wrong device.
Update the shared ADB execution path in run_adb_raw so every adb command
includes the same state.spec.adb_endpoint selection, likely by threading it
through the helper that builds the command arguments and ensuring the final adb
call uses -s with the endpoint as well.
| let connect_output = Command::new("adb") | ||
| .arg("connect") | ||
| .arg(&state.spec.adb_endpoint) | ||
| .output() | ||
| .map_err(|error| { | ||
| McpToolError::CommandFailed(format!("failed to execute adb connect: {error}")) | ||
| })?; | ||
| if !connect_output.status.success() { | ||
| return Err(McpToolError::CommandFailed(command_failure( | ||
| "adb connect", | ||
| &connect_output, | ||
| ))); | ||
| } | ||
|
|
||
| Command::new("adb") | ||
| .args(args) | ||
| .output() | ||
| .map_err(|error| McpToolError::CommandFailed(format!("failed to execute adb: {error}"))) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Add timeouts around ADB subprocesses.
adb connect and tool commands use blocking .output() calls; if ADB hangs, the single stdio request loop is stuck indefinitely. Wrap these subprocesses with a bounded timeout and kill on expiry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 445 - 462, The adb subprocess invocations in the
command handling flow can block forever because they use plain .output() without
any timeout. Update the adb connect path and the later
Command::new("adb").args(args) execution in the same mcp.rs flow to run with a
bounded timeout and terminate the child process if it expires, returning a
McpToolError::CommandFailed that reflects the timeout condition.
| fn workspace_path(workspace: &Path, value: &str) -> Result<PathBuf, McpToolError> { | ||
| let candidate = PathBuf::from(value); | ||
| if value.is_empty() | ||
| || candidate.is_absolute() | ||
| || candidate | ||
| .components() | ||
| .any(|component| component == Component::ParentDir) | ||
| { | ||
| return Err(McpToolError::InvalidArgument( | ||
| "path must be relative, non-empty, and must not contain '..'".to_string(), | ||
| )); | ||
| } | ||
|
|
||
| Ok(workspace.join(candidate)) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Resolve symlinks before trusting workspace paths.
The helper blocks absolute paths and .., but workspace/link/out.png can still escape if link is a symlink outside the workspace. Canonicalize the workspace plus the target/parent path before fs::write and APK install use the result.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp.rs` around lines 530 - 543, The current workspace_path helper only
rejects absolute paths and parent-dir segments, so symlinked subpaths can still
escape the workspace. Update workspace_path to resolve and validate the
canonicalized workspace plus the target or parent directory before any fs::write
or APK install path use, and ensure the resulting path stays under the workspace
root. Use the existing workspace_path function as the main guard and apply the
same resolved-path check at the call sites that write files or install APKs.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib.rs`:
- Around line 307-308: The default ADB endpoint is mismatched with the Docker
DNS name used by the Android container, so `DEFAULT_ADB_ENDPOINT` should resolve
to the container actually joined by `docker_run_args()`. Update the Docker setup
in `docker_run_args()` / `spec.android_container_name` handling so the Android
container is reachable as `android` (for example via a network alias), or change
the default endpoint derivation to use the generated container name consistently
across the `DEFAULT_ADB_ENDPOINT` and Docker networking logic.
In `@src/main.rs`:
- Around line 352-360: The copy_to_stderr helper currently holds the stderr lock
for the entire io::copy call, which can block the other Docker pipe reader and
cause hangs. Update copy_to_stderr so it reads from the provided Read in chunks
first, then acquires io::stderr().lock() only long enough to write each chunk
and flush as needed, keeping the lock scope minimal within the thread::spawn
closure.
In `@tests/integration/cli.rs`:
- Around line 4-48: The lifecycle CLI `status` path still shells out to Docker
to resolve `noVncUrl`, which makes the `lifecycle_cli_renders_status_json` and
`lifecycle_cli_can_disable_no_vnc_publication` tests depend on Docker. Update
the `status` implementation to avoid calling `docker port` during status
rendering, or add an explicit opt-out/override for test runs so
`parse_stdout_json` can validate `noVncUrl` and `noVncPublished` offline. Focus
the change in the `status` command handling and the `noVncUrl` resolution logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6c3bc73e-4d26-42f3-87d3-4837f21b61a3
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
Cargo.tomlLICENSEREADME.mdchangelog.d/20260624_000000_bootstrap_android_connection.mdexamples/basic_usage.rssrc/bin/android-connection.rssrc/lib.rssrc/main.rssrc/mcp.rssrc/sum.rstests/integration/cli.rstests/integration/mod.rstests/integration/sum.rstests/unit/android_connection.rstests/unit/mod.rstests/unit/sum.rs
💤 Files with no reviewable changes (3)
- tests/unit/sum.rs
- tests/integration/sum.rs
- src/sum.rs
✅ Files skipped from review due to trivial changes (5)
- changelog.d/20260624_000000_bootstrap_android_connection.md
- tests/integration/mod.rs
- LICENSE
- tests/unit/mod.rs
- README.md
🚧 Files skipped from review as they are similar to previous changes (4)
- examples/basic_usage.rs
- src/bin/android-connection.rs
- src/mcp.rs
- Cargo.toml
| "--network".to_string(), | ||
| spec.docker_network.clone(), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file list ==\n'
git ls-files | rg '^(src/lib\.rs|src/|README|.*adb.*|.*docker.*)$' || true
printf '\n== search symbols ==\n'
rg -n "DEFAULT_ADB_ENDPOINT|adb_endpoint|docker_run_args|docker_network|network-alias|android:5555|5555" src README.md
printf '\n== src/lib.rs excerpt ==\n'
sed -n '250,360p' src/lib.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 5761
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file list ==\n'
git ls-files | rg '^(src/lib\.rs|src/|README|.*adb.*|.*docker.*)$' || true
printf '\n== search symbols ==\n'
rg -n "DEFAULT_ADB_ENDPOINT|adb_endpoint|docker_run_args|docker_network|network-alias|android:5555|5555" src README.md
printf '\n== src/lib.rs excerpt ==\n'
sed -n '250,360p' src/lib.rsRepository: ProverCoderAI/rust-android-connection
Length of output: 5761
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' src/lib.rs
printf '\n== main.rs excerpt ==\n'
sed -n '1,120p' src/main.rs
printf '\n== README excerpt ==\n'
sed -n '35,60p' README.mdRepository: ProverCoderAI/rust-android-connection
Length of output: 12023
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' src/lib.rs
printf '\n== main.rs excerpt ==\n'
sed -n '1,120p' src/main.rs
printf '\n== README excerpt ==\n'
sed -n '35,60p' README.mdRepository: ProverCoderAI/rust-android-connection
Length of output: 12023
Align the default ADB host with the Docker container name.
DEFAULT_ADB_ENDPOINT is android:5555, but docker_run_args() only joins the network under spec.android_container_name (for example dg-my-project-android), so the default endpoint does not resolve on the Docker network. Add a --network-alias android or derive the default endpoint from the generated Android container name.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib.rs` around lines 307 - 308, The default ADB endpoint is mismatched
with the Docker DNS name used by the Android container, so
`DEFAULT_ADB_ENDPOINT` should resolve to the container actually joined by
`docker_run_args()`. Update the Docker setup in `docker_run_args()` /
`spec.android_container_name` handling so the Android container is reachable as
`android` (for example via a network alias), or change the default endpoint
derivation to use the generated container name consistently across the
`DEFAULT_ADB_ENDPOINT` and Docker networking logic.
| fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>> | ||
| where | ||
| R: Read + Send + 'static, | ||
| { | ||
| thread::spawn(move || { | ||
| let mut stderr = io::stderr().lock(); | ||
| io::copy(&mut reader, &mut stderr)?; | ||
| stderr.flush() | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Avoid holding stderr locked while draining Docker pipes.
Each copy thread locks stderr for the full io::copy; if one stream holds the lock while reading and Docker fills the other pipe, docker pull can hang. Read chunks first, then lock only for each write.
Suggested fix
fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
- let mut stderr = io::stderr().lock();
- io::copy(&mut reader, &mut stderr)?;
- stderr.flush()
+ let mut buffer = [0_u8; 8192];
+ loop {
+ let bytes_read = reader.read(&mut buffer)?;
+ if bytes_read == 0 {
+ return Ok(());
+ }
+
+ let mut stderr = io::stderr().lock();
+ stderr.write_all(&buffer[..bytes_read])?;
+ stderr.flush()?;
+ }
})
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>> | |
| where | |
| R: Read + Send + 'static, | |
| { | |
| thread::spawn(move || { | |
| let mut stderr = io::stderr().lock(); | |
| io::copy(&mut reader, &mut stderr)?; | |
| stderr.flush() | |
| }) | |
| fn copy_to_stderr<R>(mut reader: R) -> thread::JoinHandle<io::Result<()>> | |
| where | |
| R: Read + Send + 'static, | |
| { | |
| thread::spawn(move || { | |
| let mut buffer = [0_u8; 8192]; | |
| loop { | |
| let bytes_read = reader.read(&mut buffer)?; | |
| if bytes_read == 0 { | |
| return Ok(()); | |
| } | |
| let mut stderr = io::stderr().lock(); | |
| stderr.write_all(&buffer[..bytes_read])?; | |
| stderr.flush()?; | |
| } | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main.rs` around lines 352 - 360, The copy_to_stderr helper currently
holds the stderr lock for the entire io::copy call, which can block the other
Docker pipe reader and cause hangs. Update copy_to_stderr so it reads from the
provided Read in chunks first, then acquires io::stderr().lock() only long
enough to write each chunk and flush as needed, keeping the lock scope minimal
within the thread::spawn closure.
| #[test] | ||
| fn lifecycle_cli_renders_status_json() { | ||
| let output = lifecycle_output(&[ | ||
| "status", | ||
| "--project", | ||
| "dg-test", | ||
| "--endpoint", | ||
| "dg-test-android:5555", | ||
| "--novnc-port", | ||
| "16080", | ||
| ]); | ||
|
|
||
| assert!(output.status.success()); | ||
| let json = parse_stdout_json(&output); | ||
|
|
||
| assert_eq!(json["projectId"], "dg-test"); | ||
| assert_eq!(json["androidContainerName"], "dg-test-android"); | ||
| assert_eq!( | ||
| json["noVncUrl"], | ||
| "http://127.0.0.1:16080/?autoconnect=true&resize=remote" | ||
| ); | ||
| assert_eq!(json["noVncPublished"], true); | ||
| assert_eq!(json["resourceLimits"]["memory"], "3g"); | ||
| assert_eq!(json["resourceLimits"]["memorySwap"], "3g"); | ||
| assert_eq!(json["resourceLimits"]["cpus"], "1.0"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn lifecycle_cli_can_disable_no_vnc_publication() { | ||
| let output = lifecycle_output(&[ | ||
| "status", | ||
| "--project", | ||
| "dg-test", | ||
| "--endpoint", | ||
| "dg-test-android:5555", | ||
| "--no-novnc-publish", | ||
| ]); | ||
|
|
||
| assert!(output.status.success()); | ||
| let json = parse_stdout_json(&output); | ||
|
|
||
| assert_eq!(json["projectId"], "dg-test"); | ||
| assert_eq!(json["noVncPublished"], false); | ||
| assert!(json["noVncUrl"].is_null()); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the lifecycle status subcommand for any docker/adb invocation.
fd -e rs main.rs src | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,400p" {}'
rg -nP '\b(adb|docker)\b|Command::new|probe' src/main.rs -C2Repository: ProverCoderAI/rust-android-connection
Length of output: 14300
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the integration tests and any helper setup that could stub docker/adb.
fd -e rs tests src | sort
echo "---- tests/integration/cli.rs ----"
sed -n '1,260p' tests/integration/cli.rs
echo "---- search for DOCKER_GIT_ANDROID_DOCKER / adb stubs ----"
rg -n "DOCKER_GIT_ANDROID_DOCKER|adb|docker" tests src -C 2Repository: ProverCoderAI/rust-android-connection
Length of output: 41668
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the integration tests and any helper setup that could stub docker/adb.
fd -e rs tests src | sort
echo "---- tests/integration/cli.rs ----"
sed -n '1,260p' tests/integration/cli.rs
echo "---- search for DOCKER_GIT_ANDROID_DOCKER / adb stubs ----"
rg -n "DOCKER_GIT_ANDROID_DOCKER|adb|docker" tests src -C 2Repository: ProverCoderAI/rust-android-connection
Length of output: 41668
status still shells out to Docker for noVncUrl
status calls docker port when --no-novnc-publish isn’t set, so these tests aren’t fully offline. Consider skipping that lookup in status or making it opt-out for tests.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/integration/cli.rs` around lines 4 - 48, The lifecycle CLI `status`
path still shells out to Docker to resolve `noVncUrl`, which makes the
`lifecycle_cli_renders_status_json` and
`lifecycle_cli_can_disable_no_vnc_publication` tests depend on Docker. Update
the `status` implementation to avoid calling `docker port` during status
rendering, or add an explicit opt-out/override for test runs so
`parse_stdout_json` can validate `noVncUrl` and `noVncPublished` offline. Focus
the change in the `status` command handling and the `noVncUrl` resolution logic.
Bootstrap Android connection crate
This PR moves the docker-git Android connection codebase into the standalone
rust-android-connectionrepository.What changed
android-connectioncompatibility binary.Mathematical guarantees
Cargo.lockare committed, so binary installation with--lockedis reproducible for this revision.Proof of fix
Verification
Docker-git integration PR: ProverCoderAI/docker-git#437