mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
Smart App Control refuses the five unsigned binaries gstack compiles locally, and Git Bash reports it as 'Permission denied', so setup's [ -x ] gate passed and skills failed later. bin/gstack-launch-probe.sh runs each binary's side-effect-free --version and classifies it: native, blocked (spawn error, exit 126/127, or an application-control message), broken (launched, exited non-zero) or missing. It shows the raw first output line, since the exact Windows text is unverified. On Windows, setup names each blocked binary and the skills that need it, cites #2595, says which shell ran setup and whether WSL is installed, gives today's workarounds (WSL, or turning Smart App Control off), how to undo, and the new troubleshooting section. gstack-doctor's browse row now says 'blocked at launch' instead of passing, a binaries row covers the other four, and a cso row reads the build-result record. windows-setup-e2e builds with GSTACK_STRICT_BUILD=1, checks all five binaries answer --version, and adds two forced CSO failures: fatal under strict mode, and a completed setup that reports the failure and a doctor cso warning under GSTACK_STRICT_BUILD=0. No shim or launcher: it could not be called from PowerShell and would load the caller's project .env/bunfig. Signing is the follow-up in TODOS.md. Diagnosis credit: @knetics9000 (#2595, #2596), @smartjelic-sys (#2595), @pstilwell90 (#2124), @cko32002 (#2265), @salluexez (#2127). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7afe2751e8
commit
1fa01610ca
7 files changed
+471
-10
No files matched your search
@@ -8,7 +8,11 @@ name: Windows Setup E2E
|
||||
# an isolated HOME and drives the installed runtime root's browse from an
|
||||
# unrelated directory: a piped two-command `chain` flow must print the page
|
||||
# text (the runtime root has no node_modules, so this is the checkout-bundle
|
||||
# path from #3026, and the piped-stdin path from #3039).
|
||||
# path from #3026, and the piped-stdin path from #3039). Every compiled
|
||||
# binary must answer the side-effect-free --version that setup's Smart App
|
||||
# Control probe runs (#2595). Last, two forced CSO failures: fatal under
|
||||
# GSTACK_STRICT_BUILD=1, and a completed setup with the failure reported
|
||||
# under GSTACK_STRICT_BUILD=0 (#3071).
|
||||
#
|
||||
# Separate from windows-free-tests.yml because that one runs a curated
|
||||
# unit-test subset; this one exercises the install path itself.
|
||||
@@ -21,6 +25,8 @@ on:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'scripts/build.sh'
|
||||
- 'scripts/build-cso.sh'
|
||||
- 'scripts/build-cso-windows.ps1'
|
||||
- 'scripts/write-version-files.sh'
|
||||
- 'setup'
|
||||
- 'browse/src/cli.ts'
|
||||
@@ -28,7 +34,12 @@ on:
|
||||
- 'browse/src/server.ts'
|
||||
- 'browse/src/meta-commands.ts'
|
||||
- 'browse/scripts/build-node-server.sh'
|
||||
- 'design/src/cli.ts'
|
||||
- 'make-pdf/src/cli.ts'
|
||||
- 'bin/gstack-global-discover.ts'
|
||||
- 'bin/gstack-paths'
|
||||
- 'bin/gstack-doctor'
|
||||
- 'bin/gstack-launch-probe.sh'
|
||||
- '.github/workflows/windows-setup-e2e.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -45,7 +56,7 @@ permissions:
|
||||
jobs:
|
||||
windows-setup:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
timeout-minutes: 35
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -85,6 +96,7 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GSTACK_SKIP_PLAYWRIGHT: '1'
|
||||
GSTACK_STRICT_BUILD: '1'
|
||||
|
||||
- name: Verify binaries exist (with .exe extension on Windows)
|
||||
run: |
|
||||
@@ -96,6 +108,15 @@ jobs:
|
||||
echo "All binaries present"
|
||||
shell: bash
|
||||
|
||||
- name: 'Every compiled binary launches with --version (Smart App Control probe, #2595)'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. bin/gstack-launch-probe.sh
|
||||
gstack_launch_probe_all "$PWD" .exe
|
||||
printf 'blocked: %s\nbroken: %s\nmissing: %s\n' "$_glp_blocked" "$_glp_broken" "$_glp_missing"
|
||||
test -z "$_glp_blocked$_glp_broken$_glp_missing" || (echo "a compiled binary did not answer --version" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: Verify find-browse resolves to the .exe variant
|
||||
run: |
|
||||
set -e
|
||||
@@ -127,7 +148,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
E2E_HOME="$RUNNER_TEMP/gstack-e2e-home"
|
||||
mkdir -p "$E2E_HOME"
|
||||
HOME="$E2E_HOME" USERPROFILE="$(cygpath -w "$E2E_HOME")" ./setup --host codex
|
||||
HOME="$E2E_HOME" USERPROFILE="$(cygpath -w "$E2E_HOME")" GSTACK_STRICT_BUILD=1 ./setup --host codex
|
||||
ROOT="$E2E_HOME/.codex/skills/gstack"
|
||||
test -f "$ROOT/.source-path" || (echo "MISSING: $ROOT/.source-path" && exit 1)
|
||||
test ! -e "$ROOT/node_modules" || (echo "UNEXPECTED: $ROOT/node_modules (the journey must exercise the copy without it)" && exit 1)
|
||||
@@ -169,3 +190,38 @@ jobs:
|
||||
test "$EMPTY_CODE" -eq 1 || (echo "empty chain exited $EMPTY_CODE, expected 1" && exit 1)
|
||||
echo "$EMPTY_OUT" | grep -q 'no flow to run' || (echo "empty chain lacks the usage error" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: A forced CSO failure is fatal under GSTACK_STRICT_BUILD=1 (#3071)
|
||||
run: |
|
||||
set -uo pipefail
|
||||
rm -f browse/dist/.build-complete
|
||||
set +e
|
||||
GSTACK_SKIP_PLAYWRIGHT=1 GSTACK_STRICT_BUILD=1 GSTACK_CSO_BUILD_TESTING=1 GSTACK_CSO_BUILD_TEST_FAIL_AFTER=core bun run build > strict.log 2>&1
|
||||
CODE=$?
|
||||
set -e
|
||||
tail -n 20 strict.log
|
||||
test "$CODE" -ne 0 || (echo "strict build passed despite the injected CSO failure" && exit 1)
|
||||
grep -q 'GSTACK_STRICT_BUILD=1 makes it fatal' strict.log || (echo "strict failure lacks its message" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: A forced CSO publish failure still completes setup (#3071)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
E2E_HOME="$RUNNER_TEMP/gstack-e2e-cso-fail"
|
||||
mkdir -p "$E2E_HOME"
|
||||
rm -f browse/dist/.build-complete
|
||||
set +e
|
||||
OUT=$(HOME="$E2E_HOME" USERPROFILE="$(cygpath -w "$E2E_HOME")" GSTACK_SKIP_PLAYWRIGHT=1 GSTACK_STRICT_BUILD=0 \
|
||||
GSTACK_CSO_BUILD_TESTING=1 GSTACK_CSO_BUILD_TEST_FAIL_AFTER=publish-core ./setup --host codex 2>&1)
|
||||
CODE=$?
|
||||
set -e
|
||||
echo "$OUT" | tail -n 40
|
||||
test "$CODE" -eq 0 || (echo "setup exited $CODE after an optional CSO failure" && exit 1)
|
||||
echo "$OUT" | grep -qE 'CSO publish step failed \(exit 86\).*previous CSO kept|CSO unavailable: the native helper.s publish step failed' || (echo "setup did not report the CSO publish failure" && exit 1)
|
||||
grep -q '^result=failed' bin/.gstack-cso-build-result
|
||||
grep -q '^stage=publish' bin/.gstack-cso-build-result
|
||||
test -f browse/dist/.build-complete || (echo "the build stopped at CSO" && exit 1)
|
||||
DOCTOR=$(HOME="$E2E_HOME" bash bin/gstack-doctor 2>&1 || true)
|
||||
echo "$DOCTOR"
|
||||
echo "$DOCTOR" | grep -qE '^warn +cso +' || (echo "doctor did not report the CSO failure" && exit 1)
|
||||
shell: bash
|
||||
+71
-6
@@ -6,7 +6,7 @@
|
||||
# gstack-doctor --live also run the paid Codex model probe (one short call)
|
||||
#
|
||||
# Rows: install root, state root, bun, hooks, codex, codex probe, artifacts
|
||||
# sync, browse bundle, claude code, session journal, guard log. Each row is
|
||||
# sync, browse bundle, binaries, cso, claude code, session journal, guard log. Each row is
|
||||
# `ok`, `warn`, `not configured` or `fail`; a row that is not ok carries a fix
|
||||
# line, and every gstack helper in a fix line is an absolute path under this
|
||||
# install root. Exit 1 when any row is `fail`, 2 on a usage error, else 0.
|
||||
@@ -15,8 +15,9 @@
|
||||
# install registry rows behind `./setup --status`, bin/gstack-bun-version.sh,
|
||||
# bin/gstack-hook-check, bin/gstack-codex-probe's subcommands (the paid
|
||||
# `probe-model` only with --live; otherwise the cached result and its age),
|
||||
# the status file bin/gstack-brain-sync writes, and setup's browse build
|
||||
# outputs. The sync row prints only a clamped status code and counts, never
|
||||
# the status file bin/gstack-brain-sync writes, setup's browse build
|
||||
# outputs, setup's launch probe (bin/gstack-launch-probe.sh, which runs each
|
||||
# compiled binary's side-effect-free --version) and the CSO build-result record. The sync row prints only a clamped status code and counts, never
|
||||
# the status message or file paths: the message can quote a matched secret.
|
||||
# Bash 3.2 plus POSIX tools; it still reports when bun or python3 is missing.
|
||||
|
||||
@@ -272,13 +273,26 @@ check_sync() {
|
||||
fi
|
||||
}
|
||||
|
||||
# launch_fix — the fix line for a binary the OS refuses to start.
|
||||
launch_fix() {
|
||||
if [ -n "$EXE" ]; then
|
||||
echo "Windows Smart App Control (or another application-control policy) blocks unsigned local builds (#2595): run gstack inside WSL or turn Smart App Control off, then cd $SRC && ./setup ($DOCS#windows-smart-app-control)"
|
||||
else
|
||||
echo "the OS refused to execute it (permissions or a noexec mount under $ROOT); fix that, then cd $SRC && ./setup"
|
||||
fi
|
||||
}
|
||||
|
||||
check_browse() {
|
||||
local exe="" missing="" f
|
||||
case "${OSTYPE:-}" in msys*|cygwin*|win32*) exe=".exe" ;; esac
|
||||
if [ ! -x "$ROOT/browse/dist/browse$exe" ]; then
|
||||
local missing="" f
|
||||
if [ ! -x "$ROOT/browse/dist/browse$EXE" ]; then
|
||||
row warn "browse bundle" "not built; the gstack browser fallback for /browse, /qa, /qa-only, /design-review, /make-pdf, /pair-agent is unavailable (the Aside browser is unaffected)" "cd $SRC && ./setup"
|
||||
return
|
||||
fi
|
||||
gstack_launch_probe "$ROOT/browse/dist/browse$EXE"
|
||||
case "$_glp_state" in
|
||||
blocked) row warn "browse bundle" "built, but blocked at launch (${_glp_detail:-no output}); the gstack browser fallback is unavailable (the Aside browser is unaffected)" "$(launch_fix)"; return ;;
|
||||
broken) row warn "browse bundle" "built, but browse --version failed (${_glp_detail})" "cd $SRC && bun run build"; return ;;
|
||||
esac
|
||||
for f in server-node.mjs .build-complete; do [ -e "$ROOT/browse/dist/$f" ] || missing="${missing:+$missing, }$f"; done
|
||||
if [ -n "$missing" ]; then
|
||||
row warn "browse bundle" "incomplete build (missing browse/dist: $missing)" "cd $SRC && ./setup"
|
||||
@@ -287,6 +301,51 @@ check_browse() {
|
||||
fi
|
||||
}
|
||||
|
||||
# check_binaries — the other compiled binaries, by launch state (browse has its own row).
|
||||
check_binaries() {
|
||||
local rel detail blocked="" broken="" missing="" skills="" n=0
|
||||
for rel in browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover; do
|
||||
gstack_launch_probe "$ROOT/$rel$EXE"
|
||||
detail="${rel##*/}"
|
||||
case "$_glp_state" in
|
||||
native) n=$((n + 1)) ;;
|
||||
blocked) blocked="${blocked:+$blocked, }$detail (${_glp_detail:-no output})"; skills="${skills:+$skills, }$(gstack_launch_probe_skills "$rel")" ;;
|
||||
broken) broken="${broken:+$broken, }$detail (${_glp_detail})" ;;
|
||||
missing) missing="${missing:+$missing, }$detail" ;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$blocked" ]; then
|
||||
row warn binaries "blocked at launch: $blocked; unavailable: $skills" "$(launch_fix)"
|
||||
elif [ -n "$broken" ]; then
|
||||
row warn binaries "launched but failed: $broken" "cd $SRC && bun run build"
|
||||
elif [ -n "$missing" ]; then
|
||||
row warn binaries "not built: $missing" "cd $SRC && ./setup"
|
||||
else
|
||||
row ok binaries "find-browse, design, pdf, gstack-global-discover launch ($n of 4)"
|
||||
fi
|
||||
}
|
||||
|
||||
# check_cso — from the build-result record setup and scripts/build-cso.sh write.
|
||||
check_cso() {
|
||||
local file="$SRC/bin/.gstack-cso-build-result" result stage reason installed diag usable=0
|
||||
[ -x "$SRC/bin/gstack-cso-launcher$EXE" ] && [ -x "$SRC/bin/gstack-cso-core$EXE" ] && usable=1
|
||||
cso_field() { sed -n "s/^$1=//p" "$file" 2>/dev/null | head -1 | clamp ' -~' 200; }
|
||||
result=$(cso_field result) stage=$(cso_field stage) reason=$(cso_field reason) installed=$(cso_field installed) diag=$(cso_field diagnostic)
|
||||
case "$result" in
|
||||
ok) if [ "$usable" -eq 1 ]; then row ok cso "native helper $installed"
|
||||
else row warn cso "the last build succeeded but the native helper is missing; /cso reports not assessed" "cd $SRC && ./setup"; fi ;;
|
||||
unavailable) row "not configured" cso "unavailable: a build prerequisite is missing ($reason${diag:+: $diag}); /cso reports not assessed" "cd $SRC && ./setup names the prerequisite to install" ;;
|
||||
failed)
|
||||
if [ "$usable" -eq 1 ] && [ -n "$installed" ]; then
|
||||
row warn cso "$stage step failed ($reason) for $(cso_field revision); previous CSO kept: $installed" "cd $SRC && bun run build:cso && ./setup (log: ${diag:-none}; $DOCS#cso-build-or-publish-failed)"
|
||||
else
|
||||
row warn cso "unavailable: the $stage step failed ($reason); /cso reports not assessed" "cd $SRC && bun run build:cso && ./setup (log: ${diag:-none}; $DOCS#cso-build-or-publish-failed)"
|
||||
fi ;;
|
||||
*) if [ "$usable" -eq 1 ]; then row ok cso "native helper built (no build record yet)"
|
||||
else row "not configured" cso "native helper not built; /cso reports not assessed" "cd $SRC && ./setup"; fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_claude() {
|
||||
local override cmd raw ver rc a
|
||||
local args=()
|
||||
@@ -376,6 +435,10 @@ STATE_ROOT="$_gstack_sr_root"
|
||||
GSTACK_STATE_ROOT="$STATE_ROOT"
|
||||
. "$BIN/gstack-codex-status.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-codex-status.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
|
||||
. "$BIN/gstack-launch-probe.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-launch-probe.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
EXE=""
|
||||
case "${OSTYPE:-}" in msys*|cygwin*|win32*) EXE=".exe" ;; esac
|
||||
|
||||
echo "gstack doctor $(head -1 "$ROOT/VERSION" 2>/dev/null | clamp '0-9A-Za-z.+-' 40)"
|
||||
check_install
|
||||
check_state_root
|
||||
@@ -385,6 +448,8 @@ check_codex
|
||||
[ "$CODEX_READY" -eq 1 ] && check_codex_probe
|
||||
check_sync
|
||||
check_browse
|
||||
check_binaries
|
||||
check_cso
|
||||
check_claude
|
||||
check_journal
|
||||
check_guard_log
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# shellcheck shell=bash
|
||||
# gstack-launch-probe.sh — sourced by setup and bin/gstack-doctor.
|
||||
#
|
||||
# Windows Smart App Control (and other application-control policies) blocks
|
||||
# unsigned executables at launch, and Git Bash reports that as a plain
|
||||
# "Permission denied" (#2595, #2124). `[ -x ]` cannot see it, so this runs
|
||||
# each compiled binary's side-effect-free `--version` and classifies it:
|
||||
#
|
||||
# native launched and exited 0
|
||||
# blocked could not launch: spawn error, permission denied (exit 126/127),
|
||||
# or an application-control message in its output
|
||||
# broken launched but exited non-zero or timed out: a build problem
|
||||
# missing not built
|
||||
#
|
||||
# The exact Windows error text is unverified on a real SAC machine, so callers
|
||||
# show the raw first output line ($_glp_detail) beside the classification.
|
||||
# Bash 3.2 plus POSIX tools.
|
||||
|
||||
GSTACK_COMPILED_BINARIES="browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover"
|
||||
|
||||
# gstack_launch_probe PATH — sets _glp_state and _glp_detail.
|
||||
gstack_launch_probe() {
|
||||
local out rc
|
||||
_glp_state=missing
|
||||
_glp_detail=""
|
||||
[ -e "$1" ] || return 0
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
out=$(timeout 20 "$1" --version </dev/null 2>&1)
|
||||
else
|
||||
out=$("$1" --version </dev/null 2>&1)
|
||||
fi
|
||||
rc=$?
|
||||
_glp_detail=$(printf '%s\n' "$out" | tr -d '\r' | grep -v '^[[:space:]]*$' | head -1 | cut -c1-200)
|
||||
case "$out" in
|
||||
*[Aa]pplication\ [Cc]ontrol*|*"Smart App Control"*|*"blocked by group policy"*|*"has blocked this file"*)
|
||||
_glp_state=blocked; return 0 ;;
|
||||
esac
|
||||
case "$rc" in
|
||||
0) _glp_state=native ;;
|
||||
126|127) _glp_state=blocked ;;
|
||||
124) _glp_state=broken; _glp_detail="timed out after 20s${_glp_detail:+: $_glp_detail}" ;;
|
||||
*) _glp_state=broken; _glp_detail="exit $rc${_glp_detail:+: $_glp_detail}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gstack_launch_probe_skills REL — the skills that stop working without it.
|
||||
gstack_launch_probe_skills() {
|
||||
case "$1" in
|
||||
browse/dist/browse|browse/dist/find-browse) printf '%s' "/browse, /qa, /qa-only, /design-review, /canary, /benchmark, /pair-agent, /scrape, /make-pdf" ;;
|
||||
design/dist/design) printf '%s' "/design-consultation, /design-shotgun, /design-html, /plan-design-review" ;;
|
||||
make-pdf/dist/pdf) printf '%s' "/make-pdf" ;;
|
||||
bin/gstack-global-discover) printf '%s' "/retro global" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gstack_launch_probe_all ROOT EXE — probes every compiled binary under ROOT
|
||||
# (EXE is ".exe" on Windows). Sets _glp_blocked and _glp_broken to
|
||||
# newline-separated "rel<TAB>detail" rows, and _glp_missing to a list.
|
||||
gstack_launch_probe_all() {
|
||||
local rel
|
||||
_glp_blocked="" _glp_broken="" _glp_missing=""
|
||||
for rel in $GSTACK_COMPILED_BINARIES; do
|
||||
gstack_launch_probe "$1/$rel$2"
|
||||
case "$_glp_state" in
|
||||
blocked) _glp_blocked="$_glp_blocked$rel $_glp_detail
|
||||
" ;;
|
||||
broken) _glp_broken="$_glp_broken$rel $_glp_detail
|
||||
" ;;
|
||||
missing) _glp_missing="${_glp_missing:+$_glp_missing, }$rel" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -4454,6 +4454,50 @@ elif [ "$_CSO_RESULT" = "failed" ]; then
|
||||
log " Details: https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#cso-build-or-publish-failed"
|
||||
fi
|
||||
|
||||
# ─── Windows launch probe (#2595, #2124) ──────────────────────────────────────
|
||||
# Smart App Control blocks unsigned executables at launch, which `[ -x ]` cannot
|
||||
# see and Git Bash reports as "Permission denied". Probe each compiled binary.
|
||||
_windows_launch_summary() {
|
||||
local rel detail shell wsl
|
||||
. "$SOURCE_GSTACK_DIR/bin/gstack-launch-probe.sh" || return 0
|
||||
gstack_launch_probe_all "$SOURCE_GSTACK_DIR" ".exe"
|
||||
if [ -n "$_glp_blocked" ]; then
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*) shell="Git Bash" ;;
|
||||
CYGWIN*) shell="Cygwin" ;;
|
||||
*) shell="$(uname -s)" ;;
|
||||
esac
|
||||
if command -v wsl.exe >/dev/null 2>&1; then wsl="WSL is installed"; else wsl="WSL is not installed (wsl --install)"; fi
|
||||
log ""
|
||||
log "Windows blocked compiled gstack binaries at launch (Smart App Control or another application-control policy, #2595):"
|
||||
while IFS=' ' read -r rel detail; do
|
||||
[ -n "$rel" ] || continue
|
||||
log " $rel.exe: ${detail:-no output} -> $(gstack_launch_probe_skills "$rel")"
|
||||
done <<EOF
|
||||
$_glp_blocked
|
||||
EOF
|
||||
log " This is code integrity, not file permissions: the binaries are compiled on this machine and unsigned."
|
||||
log " Everything else is installed. You ran setup from $shell, where the block applies; $wsl, and gstack inside WSL is not affected."
|
||||
log " Workarounds: install gstack inside WSL, or turn Smart App Control off in Windows Security > App & browser control."
|
||||
log " To undo: once Windows allows the binaries, re-run ./setup and this message goes away."
|
||||
log " Details: https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#windows-smart-app-control"
|
||||
fi
|
||||
if [ -n "$_glp_broken" ]; then
|
||||
log ""
|
||||
log "Compiled gstack binaries launched but failed (a build problem, not Smart App Control):"
|
||||
while IFS=' ' read -r rel detail; do
|
||||
[ -n "$rel" ] || continue
|
||||
log " $rel.exe: $detail -> $(gstack_launch_probe_skills "$rel")"
|
||||
done <<EOF
|
||||
$_glp_broken
|
||||
EOF
|
||||
log " Rebuild: cd $SOURCE_GSTACK_DIR && bun run build"
|
||||
fi
|
||||
}
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
_windows_launch_summary
|
||||
fi
|
||||
|
||||
# ─── Chromium bootstrap summary (best-effort browser, see # 2) ───────────────
|
||||
# Printed LAST so it is the thing the user sees, after every skill registered.
|
||||
# The skills that drive Aside first and use the bundled browser only as fallback
|
||||
|
||||
@@ -16,12 +16,15 @@ import * as path from 'path';
|
||||
|
||||
// setup builds browse/dist/browse.exe on Windows (Git Bash), browse/dist/browse elsewhere.
|
||||
const BROWSE_BIN = process.platform === 'win32' ? 'browse/dist/browse.exe' : 'browse/dist/browse';
|
||||
const EXE = process.platform === 'win32' ? '.exe' : '';
|
||||
const OTHER_BINS = ['browse/dist/find-browse', 'design/dist/design', 'make-pdf/dist/pdf', 'bin/gstack-global-discover'].map(rel => rel + EXE);
|
||||
|
||||
const REPO = path.resolve(import.meta.dir, '..');
|
||||
const COPIED = [
|
||||
'setup', 'VERSION',
|
||||
'bin/gstack-doctor', 'bin/gstack-codex-status.sh', 'bin/gstack-state-root.sh', 'bin/gstack-install-registry.sh',
|
||||
'bin/gstack-render-claude.sh', 'bin/gstack-bun-version.sh', 'bin/gstack-hook-check', 'bin/gstack-config',
|
||||
'bin/gstack-launch-probe.sh',
|
||||
];
|
||||
const bases: string[] = [];
|
||||
afterEach(() => { for (const b of bases.splice(0)) fs.rmSync(b, { recursive: true, force: true }); });
|
||||
@@ -79,7 +82,10 @@ function makeFixture(): Fixture {
|
||||
]);
|
||||
const hooks = spawnSync('bash', [path.join(root, 'bin/gstack-hook-check'), '--list', root], { encoding: 'utf8', timeout: 20_000 }).stdout.trim().split('\n');
|
||||
for (const hook of hooks) write(path.join(root, hook), '#!/bin/sh\nexit 0\n', 0o755);
|
||||
for (const rel of [BROWSE_BIN, 'browse/dist/server-node.mjs', 'browse/dist/.build-complete']) write(path.join(root, rel), 'built\n', 0o755);
|
||||
// The doctor launches each compiled binary's --version (#2595), so the
|
||||
// stand-ins answer it like the real binaries do.
|
||||
for (const rel of [BROWSE_BIN, ...OTHER_BINS]) write(path.join(root, rel), '#!/bin/sh\necho built\n', 0o755);
|
||||
for (const rel of ['browse/dist/server-node.mjs', 'browse/dist/.build-complete']) write(path.join(root, rel), 'built\n', 0o755);
|
||||
const version = fs.readFileSync(path.join(root, 'VERSION'), 'utf8').trim();
|
||||
write(path.join(state, 'installs.tsv'),
|
||||
['claude', 'global', '-', path.join(home, '.claude/skills'), root, root, version, 'false', 'committed', '2026-10-07T00:00:00Z'].join('\t') + '\n');
|
||||
@@ -360,3 +366,61 @@ describe('bug-report template and README (DX-2, DX-12)', () => {
|
||||
expect(section).toContain('`~/.claude/skills/gstack/bin/gstack-doctor`');
|
||||
});
|
||||
});
|
||||
|
||||
// #2595/#2124 and #3071: compiled binaries by launch state, and CSO from the
|
||||
// build-result record. A blocked binary is simulated by the application-control
|
||||
// message reporters captured (#2595), which works on every OS.
|
||||
describe('gstack-doctor launch and CSO rows', () => {
|
||||
const BLOCKED = `#!/bin/sh\necho "Program 'x.exe' failed to run: An Application Control policy has blocked this file" >&2\nexit 1\n`;
|
||||
const record = (f: Fixture, text: string) => write(path.join(f.root, 'bin/.gstack-cso-build-result'), text);
|
||||
const cso = (f: Fixture) => { for (const name of ['gstack-cso-launcher', 'gstack-cso-core']) write(path.join(f.root, 'bin', name + EXE), '#!/bin/sh\necho cso\n', 0o755); };
|
||||
|
||||
test('healthy binaries are ok; a blocked browse says blocked, not "not built"', () => {
|
||||
const f = makeFixture();
|
||||
let r = doctor(f);
|
||||
expect(r.row('binaries').state).toBe('ok');
|
||||
expect(r.row('binaries').detail).toContain('(4 of 4)');
|
||||
write(path.join(f.root, BROWSE_BIN), BLOCKED, 0o755);
|
||||
r = doctor(f);
|
||||
expect(r.row('browse bundle').state).toBe('warn');
|
||||
expect(r.row('browse bundle').detail).toContain('built, but blocked at launch (Program');
|
||||
expect(r.row('browse bundle').detail).not.toContain('not built');
|
||||
expect(r.row('browse bundle').fix).toContain(process.platform === 'win32' ? 'Smart App Control' : 'the OS refused to execute it');
|
||||
expect(r.status).toBe(0);
|
||||
});
|
||||
|
||||
test('blocked, broken and missing binaries are told apart', () => {
|
||||
const f = makeFixture();
|
||||
write(path.join(f.root, OTHER_BINS[1]), BLOCKED, 0o755);
|
||||
let r = doctor(f);
|
||||
expect(r.row('binaries').detail).toContain('blocked at launch: design');
|
||||
expect(r.row('binaries').detail).toContain('unavailable: /design-consultation');
|
||||
write(path.join(f.root, OTHER_BINS[1]), '#!/bin/sh\necho ok\n', 0o755);
|
||||
write(path.join(f.root, OTHER_BINS[2]), '#!/bin/sh\necho "usage" >&2\nexit 2\n', 0o755);
|
||||
r = doctor(f);
|
||||
expect(r.row('binaries').detail).toBe('launched but failed: pdf (exit 2: usage)');
|
||||
expect(r.row('binaries').fix).toBe(`cd ${bashPath(f.root)} && bun run build`);
|
||||
fs.rmSync(path.join(f.root, OTHER_BINS[2]));
|
||||
r = doctor(f);
|
||||
expect(r.row('binaries').detail).toBe('not built: pdf');
|
||||
});
|
||||
|
||||
test('the CSO row reads the build-result record', () => {
|
||||
const f = makeFixture();
|
||||
expect(doctor(f).row('cso').state).toBe('not configured');
|
||||
record(f, 'result=unavailable\nstage=probe\nreason=windows-msvc-toolchain\nrevision=1\ninstalled=\nlauncher=no\ndiagnostic=\n');
|
||||
expect(doctor(f).row('cso').detail).toContain('unavailable: a build prerequisite is missing (windows-msvc-toolchain)');
|
||||
record(f, 'result=failed\nstage=build\nreason=exit 42\nrevision=1.2 (abc)\ninstalled=\nlauncher=no\ndiagnostic=/x/cso.log\n');
|
||||
let r = doctor(f);
|
||||
expect(r.row('cso').state).toBe('warn');
|
||||
expect(r.row('cso').detail).toBe('unavailable: the build step failed (exit 42); /cso reports not assessed');
|
||||
expect(r.row('cso').fix).toContain('bun run build:cso && ./setup (log: /x/cso.log;');
|
||||
cso(f);
|
||||
record(f, 'result=failed\nstage=publish\nreason=exit 1\nrevision=1.2 (abc)\ninstalled=1.1 (def)\nlauncher=yes\ndiagnostic=/x/cso.log\n');
|
||||
expect(doctor(f).row('cso').detail).toBe('publish step failed (exit 1) for 1.2 (abc); previous CSO kept: 1.1 (def)');
|
||||
record(f, 'result=ok\nstage=publish\nreason=committed\nrevision=1.2 (abc)\ninstalled=1.2 (abc)\nlauncher=yes\ndiagnostic=x\n');
|
||||
r = doctor(f);
|
||||
expect(r.row('cso')).toEqual({ state: 'ok', detail: 'native helper 1.2 (abc)', fix: '' });
|
||||
expect(r.status).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { runBashScript } from './helpers/bash-script';
|
||||
|
||||
// #2595/#2124: Smart App Control blocks unsigned binaries at launch and Git
|
||||
// Bash reports it as "Permission denied" (exit 126). bin/gstack-launch-probe.sh
|
||||
// runs each binary's --version and tells blocked from broken from healthy.
|
||||
// The exact Windows text is unverified (no SAC machine); these cases use the
|
||||
// shapes reporters captured in #2595 and #2124.
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const PROBE = path.join(ROOT, 'bin/gstack-launch-probe.sh');
|
||||
const tmps: string[] = [];
|
||||
afterEach(() => { for (const t of tmps.splice(0)) fs.rmSync(t, { recursive: true, force: true }); });
|
||||
|
||||
function fakeBinary(body: string | null, mode = 0o755) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-probe-'));
|
||||
tmps.push(dir);
|
||||
const file = path.join(dir, 'tool');
|
||||
if (body !== null) fs.writeFileSync(file, `#!/bin/sh\n${body}\n`, { mode });
|
||||
return file;
|
||||
}
|
||||
|
||||
function probe(file: string) {
|
||||
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe ${JSON.stringify(file)}\nprintf 'state=%s\\ndetail=%s\\n' "$_glp_state" "$_glp_detail"`, { timeout: 30_000 });
|
||||
expect(r.status).toBe(0);
|
||||
return { state: r.stdout.match(/^state=(.*)$/m)![1], detail: r.stdout.match(/^detail=(.*)$/m)![1] };
|
||||
}
|
||||
|
||||
// POSIX execute bits stand in for a refused launch; Windows has no such bit,
|
||||
// and a real SAC block cannot be produced in CI.
|
||||
describe.skipIf(process.platform === 'win32')('gstack-launch-probe.sh classification', () => {
|
||||
test('a binary that answers --version is native', () => {
|
||||
expect(probe(fakeBinary('[ "$1" = --version ] && echo 1.2.3'))).toEqual({ state: 'native', detail: '1.2.3' });
|
||||
});
|
||||
|
||||
test('a binary the OS refuses to execute is blocked (Git Bash "Permission denied", exit 126)', () => {
|
||||
const r = probe(fakeBinary('echo never', 0o644));
|
||||
expect(r.state).toBe('blocked');
|
||||
expect(r.detail).toContain('Permission denied');
|
||||
});
|
||||
|
||||
test('an application-control message is blocked even with another exit code', () => {
|
||||
const r = probe(fakeBinary("echo \"Program 'browse.exe' failed to run: An Application Control policy has blocked this file\" >&2; exit 1"));
|
||||
expect(r.state).toBe('blocked');
|
||||
expect(r.detail).toContain('An Application Control policy has blocked this file');
|
||||
});
|
||||
|
||||
test('a binary that launches and exits non-zero is broken, not blocked', () => {
|
||||
expect(probe(fakeBinary('echo "Unknown argument: --version" >&2; exit 3'))).toEqual({ state: 'broken', detail: 'exit 3: Unknown argument: --version' });
|
||||
});
|
||||
|
||||
test('an absent binary is missing', () => {
|
||||
expect(probe(fakeBinary(null)).state).toBe('missing');
|
||||
});
|
||||
|
||||
test('probe_all groups blocked, broken and missing binaries by repo path', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-root-'));
|
||||
tmps.push(root);
|
||||
const write = (rel: string, body: string, mode = 0o755) => {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), `#!/bin/sh\n${body}\n`, { mode });
|
||||
};
|
||||
write('browse/dist/browse.exe', 'echo x', 0o644);
|
||||
write('browse/dist/find-browse.exe', 'echo ok');
|
||||
write('design/dist/design.exe', 'echo ok');
|
||||
write('make-pdf/dist/pdf.exe', 'exit 2');
|
||||
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe_all ${JSON.stringify(root)} .exe\nprintf 'blocked=[%s]\\nbroken=[%s]\\nmissing=[%s]\\n' "$_glp_blocked" "$_glp_broken" "$_glp_missing"`, { timeout: 30_000 });
|
||||
expect(r.stdout).toContain('blocked=[browse/dist/browse\t');
|
||||
expect(r.stdout).toContain('broken=[make-pdf/dist/pdf\texit 2');
|
||||
expect(r.stdout).toContain('missing=[bin/gstack-global-discover]');
|
||||
expect(r.stdout).not.toContain('design/dist/design');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { runBashScript } from './helpers/bash-script';
|
||||
|
||||
// #2595/#2124: on Windows, setup launches each compiled binary's --version
|
||||
// after the build and explains a Smart App Control block: the blocked
|
||||
// binaries, the skills that need them, #2595, the shell and WSL status, how to
|
||||
// undo it and the troubleshooting link. Runs setup's real function against
|
||||
// stand-in binaries; a real SAC block cannot be produced in CI.
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
|
||||
const fn = (name: string) => {
|
||||
const start = setup.indexOf(`\n${name}() {`);
|
||||
const end = setup.indexOf('\n}\n', start);
|
||||
if (start < 0 || end < 0) throw new Error(`missing setup function ${name}`);
|
||||
return setup.slice(start + 1, end + 3);
|
||||
};
|
||||
const tmps: string[] = [];
|
||||
afterEach(() => { for (const t of tmps.splice(0)) fs.rmSync(t, { recursive: true, force: true }); });
|
||||
const temp = (prefix: string) => { const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tmps.push(d); return d; };
|
||||
const stub = (dir: string, name: string, body: string) => fs.writeFileSync(path.join(dir, name), `#!/bin/sh\n${body}\n`, { mode: 0o755 });
|
||||
|
||||
test('setup runs the launch summary only on Windows, after the CSO summary', () => {
|
||||
const call = setup.indexOf('if [ "$IS_WINDOWS" -eq 1 ]; then\n _windows_launch_summary\nfi');
|
||||
expect(call).toBeGreaterThan(setup.indexOf('# ─── CSO native-helper summary'));
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('setup: Windows Smart App Control hint (#2595)', () => {
|
||||
function run(binaries: Record<string, string>, wsl: boolean) {
|
||||
const src = temp('gstack-sac-src-');
|
||||
const tools = temp('gstack-sac-tools-');
|
||||
fs.mkdirSync(path.join(src, 'bin'));
|
||||
fs.copyFileSync(path.join(ROOT, 'bin/gstack-launch-probe.sh'), path.join(src, 'bin/gstack-launch-probe.sh'));
|
||||
for (const [rel, body] of Object.entries(binaries)) {
|
||||
fs.mkdirSync(path.dirname(path.join(src, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(src, rel), `#!/bin/sh\n${body}\n`, { mode: body === 'BLOCKED' ? 0o644 : 0o755 });
|
||||
}
|
||||
stub(tools, 'uname', 'echo MINGW64_NT-10.0-26200');
|
||||
if (wsl) stub(tools, 'wsl.exe', 'exit 0');
|
||||
const r = runBashScript(['QUIET=0', `SOURCE_GSTACK_DIR="${src}"`, 'log() { echo "$@"; }', fn('_windows_launch_summary'), '_windows_launch_summary'].join('\n'),
|
||||
{ env: { ...process.env, PATH: `${tools}:${process.env.PATH}` }, timeout: 60_000 });
|
||||
expect(r.status).toBe(0);
|
||||
return { out: r.stdout, src };
|
||||
}
|
||||
const healthy = 'echo 1.0';
|
||||
|
||||
test('blocked binaries are named with their skills, #2595, the shell, WSL status, undo and the docs link', () => {
|
||||
const { out } = run({
|
||||
'browse/dist/browse.exe': 'BLOCKED',
|
||||
'browse/dist/find-browse.exe': healthy,
|
||||
'design/dist/design.exe': healthy,
|
||||
'make-pdf/dist/pdf.exe': "echo \"Program 'pdf.exe' failed to run: An Application Control policy has blocked this file\" >&2; exit 1",
|
||||
'bin/gstack-global-discover.exe': healthy,
|
||||
}, false);
|
||||
expect(out).toContain('Windows blocked compiled gstack binaries at launch (Smart App Control or another application-control policy, #2595):');
|
||||
expect(out).toMatch(/browse\/dist\/browse\.exe: .*Permission denied.* -> \/browse, \/qa, /);
|
||||
expect(out).toContain('make-pdf/dist/pdf.exe: Program \'pdf.exe\' failed to run: An Application Control policy has blocked this file -> /make-pdf');
|
||||
expect(out).toContain('code integrity, not file permissions');
|
||||
expect(out).toContain('You ran setup from Git Bash, where the block applies; WSL is not installed (wsl --install)');
|
||||
expect(out).toContain('To undo: once Windows allows the binaries, re-run ./setup');
|
||||
expect(out).toContain('troubleshooting.md#windows-smart-app-control');
|
||||
expect(out).not.toContain('design/dist/design.exe');
|
||||
});
|
||||
|
||||
test('WSL presence is reported', () => {
|
||||
expect(run({ 'browse/dist/browse.exe': 'BLOCKED' }, true).out).toContain('WSL is installed, and gstack inside WSL is not affected');
|
||||
});
|
||||
|
||||
test('a binary that launches and fails is a build problem, not Smart App Control', () => {
|
||||
const { out, src } = run({
|
||||
'browse/dist/browse.exe': healthy, 'browse/dist/find-browse.exe': healthy, 'design/dist/design.exe': healthy,
|
||||
'make-pdf/dist/pdf.exe': healthy, 'bin/gstack-global-discover.exe': 'echo "Unknown argument: --version" >&2; exit 1',
|
||||
}, false);
|
||||
expect(out).not.toContain('Smart App Control or another');
|
||||
expect(out).toContain('bin/gstack-global-discover.exe: exit 1: Unknown argument: --version -> /retro global');
|
||||
expect(out).toContain(`Rebuild: cd ${src} && bun run build`);
|
||||
});
|
||||
|
||||
test('healthy binaries print nothing', () => {
|
||||
const all = Object.fromEntries(['browse/dist/browse.exe', 'browse/dist/find-browse.exe', 'design/dist/design.exe', 'make-pdf/dist/pdf.exe', 'bin/gstack-global-discover.exe'].map(rel => [rel, healthy]));
|
||||
expect(run(all, false).out).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user