#!/usr/bin/env bash # ps-encoded.sh — dispatch PowerShell through quote-mangling layers safely. # # THE point of this helper: a PS payload that traverses CommandLineToArgvW # (curl.exe args, plink, ScreenConnect's command box, RMM->cmd.exe) gets its # embedded double-quotes stripped and its UNC backslashes halved — the single # most-repeated friction class in errorlog.md (ref=feedback_windows_quote_stripping, # 9+ hits). -EncodedCommand (UTF-16LE base64) has NO quotes, backslashes, or # dollar signs to mangle, so the script arrives byte-exact. Write the script # to a FILE (or stdin), let this helper encode + deliver it. # # Usage: # ps-encoded.sh encode print the one-liner for # ScreenConnect / plink / any paste # ps-encoded.sh rmm dispatch via GuruRMM + poll # [--timeout ] agent-side timeout_seconds (default 120) # [--user-session] context: user_session (WTS-impersonated desktop user) # [--no-wait] dispatch only; print command id, skip polling # [--force] override the size refusal (see below) # # Size guards (agent chokes on ~7KB command bodies; encoding inflates ~2.67x): # encoded > 4000 chars -> [WARNING]; encoded > 6000 chars -> refuse unless # --force (split the script, or stage it on the endpoint and run the file). # # Exit codes: 0 ok; 1 usage/encode error; 2 size refusal; 3 dispatch/poll failure. set -u ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" _logerr() { bash "$ROOT/.claude/scripts/log-skill-error.sh" "ps-encoded" "$@" >/dev/null 2>&1 || true; } usage() { sed -n '2,26p' "$0"; exit 1; } read_script() { # $1 = file path or "-" if [ "$1" = "-" ]; then cat elif [ -f "$1" ]; then cat "$1" else echo "[ERROR] script file not found: $1" >&2; exit 1 fi } encode_b64() { # stdin: UTF-8 script -> stdout: UTF-16LE base64 (no BOM, no wraps) iconv -f UTF-8 -t UTF-16LE | base64 -w0 } size_gate() { # $1 = encoded string, $2 = force flag (0/1) local n=${#1} if [ "$n" -gt 6000 ] && [ "${2:-0}" != "1" ]; then echo "[ERROR] encoded payload is ${n} chars (>6000); the agent fails on bodies this large." >&2 echo " Split the script into sections, or stage it as a file on the endpoint" >&2 echo " and dispatch 'powershell -File '. Override with --force." >&2 return 1 elif [ "$n" -gt 4000 ]; then echo "[WARNING] encoded payload is ${n} chars — near the agent's body-size failure zone (~7KB)." >&2 fi return 0 } cmd_encode() { local src="${1:-}"; [ -z "$src" ] && usage local b64 b64="$(read_script "$src" | encode_b64)" [ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; } size_gate "$b64" 1 || true # encode mode: warn only, the paste target may cope printf 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand %s\n' "$b64" } cmd_rmm() { local agent="${1:-}"; shift || true local src="${1:-}"; shift || true [ -z "$agent" ] || [ -z "$src" ] && usage local timeout=120 context="" wait=1 force=0 while [ $# -gt 0 ]; do case "$1" in --timeout) timeout="${2:?}"; shift 2 ;; --user-session) context="user_session"; shift ;; --no-wait) wait=0; shift ;; --force) force=1; shift ;; *) echo "[ERROR] unknown option: $1" >&2; usage ;; esac done local b64 b64="$(read_script "$src" | encode_b64)" [ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; } size_gate "$b64" "$force" || exit 2 eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh")" if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then echo "[ERROR] RMM auth failed (no TOKEN/RMM)" >&2 _logerr "RMM auth failed via rmm-auth.sh" --context "agent=$agent" exit 3 fi # command_type=shell (cmd.exe): the base64 blob is a single quote-free token, # so no layer between here and powershell.exe can mangle it. timeout_seconds # is the field the agent honors — 'timeout' is silently ignored (errorlog). local oneliner payload resp cmd_id status oneliner="powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${b64}" payload="$(jq -nc --arg ct shell --arg cmd "$oneliner" --argjson to "$timeout" \ --arg cx "$context" \ '{command_type:$ct, command:$cmd, timeout_seconds:$to} + (if $cx != "" then {context:$cx} else {} end)')" resp="$(printf '%s' "$payload" | curl -s -m 20 -X POST "$RMM/api/agents/$agent/command" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ --data-binary @-)" cmd_id="$(printf '%s' "$resp" | jq -r '.command_id // empty' 2>/dev/null)" status="$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)" if [ -z "$cmd_id" ]; then echo "[ERROR] dispatch failed: $resp" >&2 _logerr "EncodedCommand dispatch failed" --context "agent=$agent resp=${resp:0:80}" exit 3 fi echo "[OK] dispatched command_id=$cmd_id (initial status: ${status:-unknown}, timeout_seconds=$timeout)" [ "$wait" = "0" ] && exit 0 local max_polls=$(( (timeout + 30) / 5 )) count=0 result while [ $count -lt $max_polls ]; do result="$(curl -s -m 15 "$RMM/api/commands/$cmd_id" -H "Authorization: Bearer $TOKEN")" status="$(printf '%s' "$result" | jq -r '.status // empty' 2>/dev/null)" case "$status" in completed|failed|cancelled|interrupted) echo "--- status: $status ---" printf '%s' "$result" | jq -r ' "exit_code: \(.exit_code // "n/a")", "--- stdout ---", (.stdout // .output // ""), "--- stderr ---", (.stderr // "")' 2>/dev/null || printf '%s\n' "$result" [ "$status" = "completed" ] && exit 0 || exit 3 ;; running|pending) count=$((count+1)); sleep 5 ;; *) echo "[ERROR] empty/unknown status — response: $result" >&2 _logerr "poll returned empty status" --context "cmd=$cmd_id agent=$agent" exit 3 ;; esac done echo "[WARNING] poll timeout after $((max_polls*5))s — command $cmd_id may still be running (last: $status)" exit 3 } case "${1:-}" in encode) shift; cmd_encode "$@" ;; rmm) shift; cmd_rmm "$@" ;; *) usage ;; esac