#!/usr/bin/env bash
# construct-host-exec — generic shim that proxies a host-binary invocation to
# the Construct host exec bridge. Installed once at /usr/local/bin/construct-host-exec
# and symlinked once per allowlisted binary (e.g. ~/.local/bin/wicket → this
# file). At runtime, basename(os.Args[0]) tells the shim which host binary to
# request; the bridge resolves and runs it on the host and streams output back
# as JSONL frames. See docs/HOST-EXEC.md.
#
# Lifecycle:
#   - reads stdin fully (the bridge is request/response, not duplex: the whole
#     stdin blob ships up front; live interactive input is unsupported by design)
#   - POSTs {argv, stdin} to $CONSTRUCT_HOST_EXEC_URL/exec with the bearer token
#   - streams JSONL frames back through curl (-N disables curl buffering so
#     frames flush as they arrive), decoding stdout/stderr/exit inline
#   - exits with the bridge-reported code; 126 if the bridge is unreachable
#
# IMPORTANT: the JSONL parse loop uses process substitution (< <(...)) rather
# than a pipe so it runs in the CURRENT shell and the exit code parsed from the
# {"type":"exit"} frame survives to the final `exit`. A pipe variant would run
# the loop in a subshell and silently lose the exit code.

set -u

URL="${CONSTRUCT_HOST_EXEC_URL:-}"
TOKEN="${CONSTRUCT_HOST_EXEC_TOKEN:-}"
NAME="$(basename "$0")"

if [[ -z "$URL" || -z "$TOKEN" ]]; then
    echo "${NAME}: host exec bridge not configured (CONSTRUCT_HOST_EXEC_URL/TOKEN unset)." >&2
    echo "${NAME}: this binary is proxied to the host; start it via 'construct <agent>'." >&2
    exit 126
fi

if ! command -v jq >/dev/null 2>&1; then
    echo "${NAME}: jq is required to talk to the host exec bridge but was not found." >&2
    exit 126
fi

# Read stdin once (the bridge expects it up front, base64-encoded).
#
# Three failure modes to avoid:
#   1. Deadlock: a bare `base64` read blocks forever when stdin is an open
#      pipe that never sends data and never closes (no EOF). This is the
#      condition created by non-interactive launchers like pi-unified-exec,
#      which hold stdin open for the session lifetime.
#   2. Latency tax: `timeout N` always runs to its full duration when no data
#      ever arrives, so strapping it on unconditionally would add a permanent
#      N-second wait to every invocation from such launchers.
#   3. Silent truncation: a byte cap or timeout can ship a partial payload,
#      surfacing as a confusing bridge-side parse error instead of a clear
#      shim-side message.
#
# Strategy: peek first with non-blocking `read -t 0` (returns success only
# when data or EOF is immediately available). If nothing is pending, skip the
# read entirely (the common case for flag-only CLIs held open by a launcher).
# Only when data/EOF is pending do we consume it, bounded by `head -c` (forces
# EOF after N bytes or on real EOF, driving base64 to flush+close) and `timeout`
# (defense against a slow trickle). If either bound fires, warn on stderr so
# the truncation is attributable to the shim, not the bridge.
STDIN_B64=""
STDIN_CAP=1048576   # 1 MiB; generous for a CLI that takes config flags
STDIN_TIMEOUT=5     # seconds; bound for a slow trickle
STDIN_TRUNCATED=0
if [[ ! -t 0 ]] && read -r -N 0 -t 0 _ 2>/dev/null; then
    STDIN_B64="$(timeout "${STDIN_TIMEOUT}" head -c "${STDIN_CAP}" | base64 | tr -d '\n')"
    # Exit codes across `$(...)` are unreliable (we see tr's status, not
    # head's or timeout's; pipefail isn't set). Detect truncation by content
    # length: base64 of a clean EOF usually lands short of the cap; hitting
    # it precisely is the signature of `head -c` stopping at the limit.
    if [[ ${#STDIN_B64} -ge $((STDIN_CAP * 4 / 3)) ]]; then
        STDIN_TRUNCATED=1
    fi
fi
if [[ "${STDIN_TRUNCATED}" == "1" ]]; then
    echo "${NAME}: warning: stdin exceeded ${STDIN_CAP} bytes or ${STDIN_TIMEOUT}s read budget; truncated." >&2
    echo "${NAME}: the host backend received a partial stdin payload." >&2
fi

# Build the request payload safely with jq. --args consumes the remaining CLI
# args (argv[1..]) as a JSON array; we prepend $NAME (argv[0]) via a leading
# '--" so the whole argv is byte-safe (quotes/spaces/newlines/unicode cannot
# break the payload — no manual NUL-splitting, which loses embedded newlines).
# $PWD is the agent's container cwd; the bridge translates it to the matching
# host path so cwd-aware host CLIs run in the project the agent is in.
PAYLOAD="$(jq -nc --arg stdin "$STDIN_B64" --arg cwd "$PWD" --args '{argv: $ARGS.positional, stdin: $stdin, cwd: $cwd}' -- "$NAME" "$@")"

# Stream JSONL frames as they arrive. We bind curl's stdout to fd 3 via
# process substitution and capture its PID so we can read its real exit code
# afterwards: `$?` after a `while ... done < <(cmd)` loop reflects the loop's
# exit, not curl's, so the unreachable branch would never fire otherwise.
exit_code=""
exec 3< <(curl -sS -N \
            -H "X-Construct-Exec-Token: ${TOKEN}" \
            -H "Content-Type: application/json" \
            --data-binary "$PAYLOAD" \
            "$URL/exec")
curl_pid=$!
while IFS= read -r line <&3; do
    case "$line" in
        '{"type":"stdout"'*)
            printf '%s' "$line" | jq -r '.data' 2>/dev/null | base64 -d 2>/dev/null
            ;;
        '{"type":"stderr"'*)
            printf '%s' "$line" | jq -r '.data' 2>/dev/null | base64 -d >&2 2>/dev/null
            ;;
        '{"type":"exit"'*)
            exit_code="$(printf '%s' "$line" | jq -r '.code' 2>/dev/null)"
            ;;
    esac
done
exec 3<&-
# Curl exits non-zero on connection failure (curl also writes its own message
# to stderr). Wait on the PID to read its TRUE exit, not the loop's.
wait "$curl_pid" 2>/dev/null
curl_rc=$?

# exit_code may be empty if the bridge is unreachable, the connection dropped
# mid-stream, or curl failed before any exit frame arrived. All collapse to the
# same "backend unreachable" 126 (the shim binary exists, but the host backend
# didn't deliver a result — distinct from 127 "command not found").
if [[ -z "$exit_code" ]]; then
    echo "${NAME}: host exec bridge unreachable or connection dropped: $URL" >&2
    echo "${NAME}: see ~/.config/construct-cli/logs/host_exec.log on the host." >&2
    exit 126
fi
exit "$exit_code"
