Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions scripts/drivers/types/codex/codex-bridge-launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,44 @@ _REAP_WAIT_TICKS=50
# recycled pid is always distinguishable from the one we leased.
# else -> `ps -o lstart=` (second precision). Echoes "<src><TAB><token>";
# returns non-zero (indeterminable) so the caller fails closed.
# win32 -> Process.StartTime.Ticks via PowerShell -- the same single source
# codex-bridge.js startToken() writes, so both sides always agree.
# WMIC is deprecated and already absent from some Windows 11 installs;
# a per-side "WMIC, else PowerShell" order would let the two record
# differently-FORMATTED tokens for the same process. powershell.exe
# and pwsh return identical Ticks, so falling back between those two
# binaries introduces no such divergence.
#
# The Windows branch is taken INSTEAD of the /proc branch, not merely before it:
# MSYS/Cygwin do expose a working /proc, but it is keyed by the emulation layer's
# own pid space while a lease records the Windows pid codex-bridge.js sees as
# process.pid. Letting /proc win would return the start time of whatever
# unrelated MSYS process sits at that number -- the recycled-pid confusion this
# token exists to prevent, with nothing to signal it.
_agmsg_is_windows() {
case "${_AGMSG_UNAME_S:=$(uname -s 2>/dev/null || echo unknown)}" in
MINGW*|MSYS*|CYGWIN*|CLANGARM*) return 0 ;;
*) return 1 ;;
esac
}

_start_token() {
local pid="$1" s r tok
local pid="$1" s r tok bin
local -a a
if _agmsg_is_windows; then
for bin in powershell.exe pwsh; do
# `tr -d '\r'` because PowerShell writes CRLF and Node's .trim() on the
# writer side strips it there, so both end up with the bare digits.
tok="$("$bin" -NoProfile -NonInteractive -Command \
"(Get-Process -Id $pid).StartTime.Ticks" 2>/dev/null | tr -d '\r' | head -n 1)"
tok="${tok#"${tok%%[![:space:]]*}"}"
tok="${tok%"${tok##*[![:space:]]}"}"
case "$tok" in ''|*[!0-9]*) continue ;; esac
printf 'pwsh\t%s' "$tok"
return 0
done
return 1
fi
if [ -r "/proc/$pid/stat" ]; then
s="$(cat "/proc/$pid/stat" 2>/dev/null)" || return 1
r="${s##*)}"
Expand All @@ -433,7 +468,7 @@ _start_token() {
# Parse a lease under an EXACT v=1 schema and fail closed on anything else. Sets
# lproj/lpairs/lhost/lpid/lstart/lstartsrc and returns 0 only when the file is
# precisely the seven expected keys, each once, no unknown or duplicate or extra
# line, hashes 40-hex, pid numeric, startsrc proc|ps. A malformed, truncated, or
# line, hashes 40-hex, pid numeric, startsrc proc|ps|pwsh. A malformed, truncated, or
# tampered lease returns non-zero, so the reaper never kills on a doubtful one.
_read_lease() {
local file="$1" line k v nlines=0 lv=""
Expand All @@ -460,10 +495,16 @@ _read_lease() {
case "$lproj" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#lproj}" -eq 40 ] || return 1
case "$lpairs" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#lpairs}" -eq 40 ] || return 1
case "$lpid" in *[!0-9]*|"") return 1 ;; esac
case "$lstartsrc" in proc|ps) ;; *) return 1 ;; esac
case "$lstartsrc" in proc|ps|pwsh) ;; *) return 1 ;; esac
[ -n "$lhost" ] || return 1
[ -n "$lstart" ] || return 1
if [ "$lstartsrc" = proc ]; then case "$lstart" in *[!0-9]*|"") return 1 ;; esac; fi
# proc's starttime ticks and .NET's StartTime.Ticks are both bare integers, so
# a lease carrying anything else under either label fails closed here. ps stays
# exempt: its lstart is a human date string whose punctuation varies by
# platform, and writer and reader only ever compare it byte for byte.
case "$lstartsrc" in
proc|pwsh) case "$lstart" in *[!0-9]*|"") return 1 ;; esac ;;
esac
return 0
}

Expand Down
27 changes: 27 additions & 0 deletions scripts/drivers/types/codex/codex-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,34 @@ class CodexBridge {
// and the only victim would be a same-(project,pair) bridge in the
// sub-ms window before it overwrites this pid's lease, self-corrected
// by the launcher respawning it.
// win32 -> Process.StartTime.Ticks, read through PowerShell. Windows has no
// /proc, and MSYS's `ps` rejects -o outright, so both POSIX sources
// yield an empty token and no lease can be published at all.
// ONE source, not a preference list: WMIC is deprecated and already
// absent from some Windows 11 installs, and letting each side choose
// between WMIC and PowerShell independently would let this writer and
// _start_token (codex-bridge-launcher.sh) record differently-
// FORMATTED tokens for the same process. powershell.exe and pwsh
// return identical Ticks, so falling back between those two binaries
// is safe: the src label names the format, not the executable.
startToken() {
if (process.platform === "win32") {
for (const bin of ["powershell.exe", "pwsh"]) {
const r = spawnSync(
bin,
[
"-NoProfile",
"-NonInteractive",
"-Command",
`(Get-Process -Id ${process.pid}).StartTime.Ticks`,
],
{ encoding: "utf8" },
);
const ticks = (r.status === 0 ? (r.stdout || "") : "").trim();
if (/^\d+$/.test(ticks)) return { src: "pwsh", token: ticks };
}
return { src: "pwsh", token: "" };
}
try {
const stat = fs.readFileSync(`/proc/${process.pid}/stat`, "utf8");
const after = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
Expand Down
97 changes: 97 additions & 0 deletions tests/test_codex_bridge_launcher.bats
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,100 @@ _fake_alice_lease() { # sets FAKE_PID once its lease file exists
kill -0 "$victim"
kill "$victim" "$disp" "$parent" 2>/dev/null || true; wait "$disp" 2>/dev/null || true; wait "$victim" 2>/dev/null || true
}

# --- Windows start token: the lease schema must admit the source
# codex-bridge.js writeLease() records on Windows, where /proc does not exist and
# the only `ps` likely to be on PATH (MSYS's) rejects -o outright, so both POSIX
# sources yield an empty token and the bridge can never publish a lease at all.
#
# _read_lease is the reaper's ONLY gate on a lease, so its accept/reject set is
# the contract. These exercise it directly -- the pattern test_remote.bats uses
# for _remote_endpoint_display -- rather than through the reaper: the reaper
# needs a spawnable bridge and a live pid, which is exactly what does not work on
# Git Bash (#567), and the schema question has nothing to do with either. Kept
# out of the `windows-native` filter deliberately: nothing here runs PowerShell,
# so these belong on every leg, not only the Windows one. ---
_lease_verdict() { # <startsrc> <start> -> prints accept|reject
local h40=0123456789abcdef0123456789abcdef01234567
printf 'v=1\nproject=%s\npairs=%s\nhost=h\npid=123\nstart=%s\nstartsrc=%s\n' \
"$h40" "$h40" "$2" "$1" > "$TEST_SKILL_DIR/lease-under-test"
bash -c '
pattern="/^_read_lease() {/,/^}/p"
eval "$(sed -n "$pattern" "$1")"
_read_lease "$2" && echo accept || echo reject
' _ "$LAUNCHER" "$TEST_SKILL_DIR/lease-under-test" 2>/dev/null
}

@test "launcher: the lease schema admits a pwsh start token" {
[ "$(_lease_verdict pwsh 639231441791462826)" = accept ]
}

@test "launcher: a pwsh lease whose token is not an integer is rejected, fail-closed" {
# .NET Ticks is a bare integer. Anything else under that label is a lease this
# side did not write, and a doubtful lease must never authorise a kill.
[ "$(_lease_verdict pwsh 6392314.5)" = reject ]
[ "$(_lease_verdict pwsh '')" = reject ]
}

@test "launcher: an unrecognised startsrc is rejected, fail-closed" {
# wmic is here on purpose, not as an arbitrary bad value: WMIC's CreationDate
# was the faster candidate and was deliberately NOT adopted, because a per-side
# "WMIC, else PowerShell" order lets the writer and the reaper resolve different
# sources for the same process whenever only one of them can reach wmic.exe.
# Rejecting the label pins that decision, so reintroducing it fails loudly.
[ "$(_lease_verdict wmic 20260824041348.411807+540)" = reject ]
[ "$(_lease_verdict bogus 123)" = reject ]
}

@test "launcher: proc and ps leases still parse (start-token regression)" {
[ "$(_lease_verdict proc 396341883)" = accept ]
[ "$(_lease_verdict ps 'Sun Aug 24 04:00:00 2026')" = accept ]
# ps stays exempt from the integer check (its token is a human date string
# whose punctuation varies by platform); proc does not.
[ "$(_lease_verdict proc abc)" = reject ]
}

_run_start_token() { # <pid> -> runs _start_token in a subshell
run bash -c '
pattern="/^_agmsg_is_windows() {/,/^}/p;/^_start_token() {/,/^}/p"
eval "$(sed -n "$pattern" "$1")"
_start_token "$2"
' _ "$LAUNCHER" "$1"
}

@test "launcher: a live pid yields a proc or ps start token on POSIX" {
skip_on_windows "Windows has its own source; see the windows-native case"
_run_start_token $$
[ "$status" -eq 0 ]
local tab; tab=$(printf '\t')
case "${output%%"$tab"*}" in proc|ps) ;; *) false ;; esac
[ -n "${output#*"$tab"}" ]
}

@test "launcher: CLANGARM uname selects the Windows start token path" {
local stubdir="$TEST_SKILL_DIR/clangarm-bin"
mkdir -p "$stubdir"
printf '%s\n' '#!/usr/bin/env bash' 'printf "%s\n" CLANGARM64_NT-10.0' > "$stubdir/uname"
printf '%s\n' '#!/usr/bin/env bash' 'printf "%s\n" 639231441791462826' > "$stubdir/powershell.exe"
chmod +x "$stubdir/uname" "$stubdir/powershell.exe"

PATH="$stubdir:$PATH" _run_start_token 123
[ "$status" -eq 0 ]
[ "$output" = $'pwsh\t639231441791462826' ]
}

@test "launcher: windows-native a live pid yields an integer pwsh start token" {
skip_unless_windows "PowerShell and the Windows pid space are the point"
# The pid must be the WINDOWS one. MSYS/Cygwin number processes in their own
# space -- the same shell is MSYS pid 3994449 and winpid 19568 on our runner --
# and Get-Process only knows the latter, which is also the pid
# codex-bridge.js records as process.pid.
local winpid; winpid="$(cat /proc/$$/winpid)"
[ -n "$winpid" ]
_run_start_token "$winpid"
[ "$status" -eq 0 ]
local tab; tab=$(printf '\t')
[ "${output%%"$tab"*}" = pwsh ]
local tok="${output#*"$tab"}"
case "$tok" in ''|*[!0-9]*) false ;; esac
}
Loading