fix: guard remote apply-patch context

This commit is contained in:
Codex
2026-05-25 09:26:41 +00:00
parent 09b8fa7a02
commit 44f7fd92c8
4 changed files with 199 additions and 157 deletions
+2 -1
View File
@@ -148,7 +148,7 @@ export function sshHelp(): unknown {
usage: [
"bun scripts/cli.ts ssh <route>",
"bun scripts/cli.ts ssh <providerId> argv <command> [args...]",
"bun scripts/cli.ts ssh <providerId> apply-patch < patch.diff",
"bun scripts/cli.ts ssh <providerId> apply-patch [--allow-loose] < patch.diff",
"bun scripts/cli.ts ssh <providerId> py [script-args...] < script.py",
"bun scripts/cli.ts ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
"bun scripts/cli.ts ssh <providerId> skills [--scope all|wsl|windows] [--limit N]",
@@ -169,6 +169,7 @@ export function sshHelp(): unknown {
notes: [
"ssh --help and ssh <route> --help print this JSON help and never open an interactive session.",
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
"apply-patch rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; native k3s providers such as D601 and G14 use <provider>:k3s for the control plane, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
"Do not put operation names in any colon route segment, including nested k3s namespace/workload/container segments.",
+66 -152
View File
@@ -62,152 +62,11 @@ const legacyK3sOperationRouteSegments = new Set([
"api-versions",
]);
const remoteApplyPatchSource = String.raw`#!/usr/bin/env python3
import sys
from pathlib import Path
def die(message):
print(f"apply_patch: {message}", file=sys.stderr)
sys.exit(1)
def write_file(path, text):
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text)
def move_file(source, destination):
source_path = Path(source)
destination_path = Path(destination)
if not source_path.exists():
die(f"file not found: {source}")
if destination_path.exists():
die(f"target file already exists: {destination}")
destination_path.parent.mkdir(parents=True, exist_ok=True)
source_path.rename(destination_path)
def read_file(path):
try:
return Path(path).read_text()
except FileNotFoundError:
die(f"file not found: {path}")
def apply_update(path, body):
old = read_file(path)
output = []
search_from = 0
index = 0
while index < len(body):
if body[index].startswith("*** End of File"):
index += 1
continue
if not body[index].startswith("@@"):
die(f"expected hunk header in {path}")
index += 1
sequence = []
while index < len(body) and not body[index].startswith("@@"):
line = body[index]
index += 1
if line.startswith("*** End of File"):
continue
if line.startswith(" "):
sequence.append((" ", line[1:]))
elif line.startswith("-"):
sequence.append(("-", line[1:]))
elif line.startswith("+"):
sequence.append(("+", line[1:]))
elif line.rstrip("\n") == r"\ No newline at end of file":
continue
else:
die(f"bad hunk line in {path}: {line[:80].rstrip()}")
search = "".join(text for kind, text in sequence if kind in (" ", "-"))
replacement = "".join(text for kind, text in sequence if kind in (" ", "+"))
offset = old.find(search, search_from)
if offset < 0:
offset = old.find(search)
if offset < 0:
die(f"hunk context not found in {path}")
output.append(old[search_from:offset])
output.append(replacement)
search_from = offset + len(search)
output.append(old[search_from:])
write_file(path, "".join(output))
def main():
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
print("apply_patch: read *** Begin Patch format from stdin; supports add/update/delete/move")
return
lines = sys.stdin.read().splitlines(keepends=True)
if not lines or lines[0].strip() != "*** Begin Patch":
die("patch must start with *** Begin Patch")
index = 1
while index < len(lines):
line = lines[index].rstrip("\n")
if line == "*** End Patch":
print("Done!")
return
if line.startswith("*** Add File: "):
path = line[len("*** Add File: "):].strip()
index += 1
content = []
while index < len(lines) and not lines[index].startswith("*** "):
if not lines[index].startswith("+"):
die(f"add file lines must start with + for {path}")
content.append(lines[index][1:])
index += 1
if Path(path).exists():
die(f"file already exists: {path}")
write_file(path, "".join(content))
continue
if line.startswith("*** Delete File: "):
path = line[len("*** Delete File: "):].strip()
try:
Path(path).unlink()
except FileNotFoundError:
die(f"file not found: {path}")
index += 1
continue
if line.startswith("*** Update File: "):
path = line[len("*** Update File: "):].strip()
index += 1
move_to = None
if index < len(lines) and lines[index].startswith("*** Move to: "):
move_to = lines[index][len("*** Move to: "):].strip()
index += 1
body = []
while index < len(lines) and not (
lines[index].startswith("*** Add File: ")
or lines[index].startswith("*** Update File: ")
or lines[index].startswith("*** Delete File: ")
or lines[index].startswith("*** End Patch")
):
if lines[index].startswith("*** Move to: "):
die("move marker must appear before update hunks")
body.append(lines[index])
index += 1
if body:
apply_update(path, body)
elif move_to is None and not Path(path).exists():
die(f"file not found: {path}")
if move_to is not None:
move_file(path, move_to)
continue
die(f"unexpected patch line: {line}")
die("missing *** End Patch")
if __name__ == "__main__":
main()
`;
const remoteShApplyPatchSource = String.raw`#!/bin/sh
export const remoteApplyPatchSource = String.raw`#!/bin/sh
set -eu
allow_loose=0
die() {
printf 'apply_patch: %s\n' "$*" >&2
exit 1
@@ -240,10 +99,16 @@ write_file() {
cp "$source" "$target" || die "failed to write $target"
}
line_number_for_prefix() {
newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ')
printf '%s\n' $((newline_count + 1))
}
replace_once() {
target=$1
search_file=$2
replace_file=$3
hunk_id=$4
[ -e "$target" ] || die "file not found: $target"
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
@@ -255,16 +120,24 @@ replace_once() {
replacement=${"$"}{replacement%"$marker"}
if [ -z "$search" ]; then
[ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2
new=$replacement$old
else
case "$old" in
*"$search"*)
prefix=${"$"}{old%%"$search"*}
suffix=${"$"}{old#*"$search"}
case "$suffix" in
*"$search"*)
[ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review"
;;
esac
printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2
new=$prefix$replacement$suffix
;;
*)
die "hunk context not found in $target"
die "hunk $hunk_id context not found in $target"
;;
esac
fi
@@ -282,10 +155,25 @@ apply_update() {
search_file=
replace_file=
changed=0
hunk_number=0
has_add=0
has_delete=0
seen_add=0
anchor_before_add=0
anchor_after_add=0
explicit_eof=0
finish_hunk() {
[ "$in_hunk" = 1 ] || return 0
replace_once "$target" "$search_file" "$replace_file"
if [ "$allow_loose" != 1 ]; then
[ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then
if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then
die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review"
fi
fi
fi
replace_once "$target" "$search_file" "$replace_file" "$hunk_number"
rm -f "$search_file" "$replace_file"
search_file=
replace_file=
@@ -302,6 +190,13 @@ apply_update() {
finish_hunk
search_file=$(mk_temp)
replace_file=$(mk_temp)
hunk_number=$((hunk_number + 1))
has_add=0
has_delete=0
seen_add=0
anchor_before_add=0
anchor_after_add=0
explicit_eof=0
in_hunk=1
continue
;;
@@ -312,17 +207,25 @@ apply_update() {
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
printf '%s\n' "$text" >> "$replace_file"
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
;;
"-"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
has_delete=1
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
;;
"+"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$replace_file"
has_add=1
seen_add=1
;;
"\\ No newline at end of file")
;;
"*** End of File"*)
explicit_eof=1
;;
*)
die "bad hunk line in $target: $line"
;;
@@ -432,10 +335,21 @@ parse_update_file() {
}
main() {
if [ "${"$"}{1:-}" = "-h" ] || [ "${"$"}{1:-}" = "--help" ]; then
printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin\n'
return 0
fi
while [ "$#" -gt 0 ]; do
case "${"$"}1" in
-h|--help)
printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n'
return 0
;;
--allow-loose)
allow_loose=1
shift
;;
*)
die "unsupported option: ${"$"}1"
;;
esac
done
next_patch_line || die "patch must start with *** Begin Patch"
[ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch"
while next_patch_line; do
@@ -1440,14 +1354,14 @@ function buildShellStdinCommand(args: string[]): string {
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
if (remoteShApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker");
if (remoteApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker");
return {
prefix: [
"set -eu",
'UNIDESK_SSH_TOOL_DIR="${UNIDESK_SSH_TOOL_DIR:-/tmp/unidesk-ssh-tools}"',
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
`cat > "$UNIDESK_SSH_TOOL_DIR/apply_patch" <<'${toolMarker}'`,
remoteShApplyPatchSource.trimEnd(),
remoteApplyPatchSource.trimEnd(),
toolMarker,
'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"',
`"$UNIDESK_SSH_TOOL_DIR/apply_patch" "$@" <<'${patchMarker}'`,