From e8faca63eddbd15983a698ef2dc689ef26f2454b Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:30:36 +0530 Subject: [PATCH 01/10] fix(bootstrap): enforce manifest-pinned sources --- .github/workflows/ci.yml | 1 + .gitignore | 1 + kali/scripts/bootstrap-manifest.json | 1 + kali/scripts/bootstrap-reverse.sh | 15 +- skills/scripts/bootstrap-manifest.json | 1 + skills/scripts/bootstrap-reverse.ps1 | 27 +-- skills/scripts/bootstrap-reverse.sh | 165 +++++++++++--- skills/scripts/test-bootstrap-manifest.sh | 238 ++++++++++++++++++++ skills/scripts/verify-routing-coherence.ps1 | 6 +- 9 files changed, 397 insertions(+), 58 deletions(-) create mode 100644 skills/scripts/test-bootstrap-manifest.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e86139a..6cbfa17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ jobs: run: | set -euo pipefail bash skills/scripts/test-routing.sh + bash skills/scripts/test-bootstrap-manifest.sh scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT diff --git a/.gitignore b/.gitignore index 8073f7e..7254c4a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ Desktop.ini *.tmp *.bak *.log +implementation-notes.md # User-specific config .claude/ diff --git a/kali/scripts/bootstrap-manifest.json b/kali/scripts/bootstrap-manifest.json index 226774b..6232906 100644 --- a/kali/scripts/bootstrap-manifest.json +++ b/kali/scripts/bootstrap-manifest.json @@ -156,6 +156,7 @@ "servicePort": 23816, "docsUrl": "https://github.com/Mouseww/anything-analyzer", "canAutoInstall": true, + "pinnedCommit": "0ed4791688e5186da051c85eb7ddfe4639e14fd2", "verificationMode": "service-or-registration" }, { diff --git a/kali/scripts/bootstrap-reverse.sh b/kali/scripts/bootstrap-reverse.sh index c8cf1f6..e6357e3 100644 --- a/kali/scripts/bootstrap-reverse.sh +++ b/kali/scripts/bootstrap-reverse.sh @@ -22,6 +22,7 @@ CAPABILITIES=() START_SERVICES=false SKIP_REFRESH=false MANUAL_REQUIRED=false +FAILED=false LAST_CAPABILITY_MANUAL=false for arg in "$@"; do @@ -630,11 +631,10 @@ start_anything_analyzer() { fi local repo_dir="$HOME/tools/anything-analyzer" - - if [[ ! -d "$repo_dir" ]]; then - log_info "克隆 anything-analyzer ..." - git clone https://github.com/Mouseww/anything-analyzer.git "$repo_dir" - fi + local repo commit + repo=$(manifest_field anything-analyzer repoUrl) + commit=$(manifest_field anything-analyzer pinnedCommit) + install_git_commit "$repo" "$commit" "$repo_dir" || return 1 if ! command -v pnpm &>/dev/null; then npm install -g pnpm @@ -681,6 +681,7 @@ for cap in "${CAPABILITIES[@]}"; do fi else RESULTS+=("{\"name\":\"$cap\",\"status\":\"failed\"}") + FAILED=true fi done @@ -691,7 +692,9 @@ if [[ "$SKIP_REFRESH" != "true" ]]; then fi final_exit_code=0 -if [[ "$MANUAL_REQUIRED" == "true" ]]; then +if [[ "$FAILED" == "true" ]]; then + final_exit_code=1 +elif [[ "$MANUAL_REQUIRED" == "true" ]]; then final_exit_code=2 fi diff --git a/skills/scripts/bootstrap-manifest.json b/skills/scripts/bootstrap-manifest.json index 6d7b8e5..6457dc0 100644 --- a/skills/scripts/bootstrap-manifest.json +++ b/skills/scripts/bootstrap-manifest.json @@ -127,6 +127,7 @@ "servicePort": 23816, "docsUrl": "https://github.com/Mouseww/anything-analyzer", "canAutoInstall": true, + "pinnedCommit": "0ed4791688e5186da051c85eb7ddfe4639e14fd2", "verificationMode": "service-or-registration" }, { diff --git a/skills/scripts/bootstrap-reverse.ps1 b/skills/scripts/bootstrap-reverse.ps1 index 32e0b60..59d0f2d 100644 --- a/skills/scripts/bootstrap-reverse.ps1 +++ b/skills/scripts/bootstrap-reverse.ps1 @@ -781,6 +781,13 @@ function Start-AnythingAnalyzerService { return } + $repoDir = [string]$Definition.installDir + $checkoutDefinition = [pscustomobject]@{ + repo = [string]$Definition.repoUrl + pinnedCommit = [string]$Definition.pinnedCommit + } + Ensure-GitCloneInstall -Definition $checkoutDefinition -TargetPath $repoDir | Out-Null + Ensure-Pnpm $vsBuildToolsError = '' if (Test-ReverseIsWindows) { @@ -793,26 +800,6 @@ if (Test-ReverseIsWindows) { } } - $repoDir = @($Definition.startupDirCandidates) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace($repoDir)) { - $installDir = $Definition.installDir - $gh = Get-FirstCommandPath -Names @('gh') - $git = Get-FirstCommandPath -Names @('git') - if ($gh) { - & $gh repo clone 'Mouseww/anything-analyzer' $installDir - } - elseif ($git) { - & $git clone $Definition.repoUrl $installDir - } - else { - throw 'Cannot clone anything-analyzer because neither gh nor git is available.' - } - if ($LASTEXITCODE -ne 0) { - throw 'Failed to clone anything-analyzer.' - } - $repoDir = $installDir - } - $pnpm = Get-NodeCommandPath -Name 'pnpm' if ([string]::IsNullOrWhiteSpace($pnpm)) { throw 'pnpm is not available after installation.' diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index 57ca7aa..74c1133 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -27,6 +27,7 @@ if [[ -z "$TOOLS_ROOT" || "$TOOLS_ROOT" == "/" || "$TOOLS_ROOT" == "$HOME" ]]; t exit 2 fi MCP_CONFIG_PATH="${CLAUDE_MCP_CONFIG:-$HOME/.claude/mcp.json}" +MANIFEST_PATH="$SCRIPT_DIR/bootstrap-manifest.json" UNAME_S="$(uname -s 2>/dev/null || echo unknown)" case "$UNAME_S" in @@ -39,6 +40,7 @@ START_SERVICES=false SKIP_REFRESH=false LIST_ONLY=false MANUAL_REQUIRED=false +FAILED=false LAST_CAPABILITY_MANUAL=false CAPABILITIES=() @@ -67,6 +69,26 @@ json_escape() { ensure_dir() { mkdir -p "$1"; } +manifest_field() { + local capability="$1" + local field="$2" + python3 - "$MANIFEST_PATH" "$capability" "$field" <<'PY' +import json, pathlib, sys +manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')) +for capability in manifest.get('capabilities', []): + if capability.get('name') == sys.argv[2]: + value = capability.get(sys.argv[3]) + if value is None or value == '': + raise SystemExit(1) + if isinstance(value, (dict, list)): + print(json.dumps(value, separators=(',', ':'))) + else: + print(value) + raise SystemExit(0) +raise SystemExit(1) +PY +} + safe_remove_install_dir() { local target="$1" local tmp_target="${2:-}" @@ -351,6 +373,39 @@ install_github_release() { log_ok "installed $repo to $dest" } +install_git_commit() { + local repo="$1" + local commit="$2" + local install_dir="$3" + + if [[ -d "$install_dir/.git" ]]; then + local current + current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null || true) + if [[ "$current" != "$commit" ]]; then + log_err "Existing checkout is not at pinned commit $commit: $install_dir" + log_err "Move it aside explicitly, then retry; bootstrap will not overwrite local changes." + return 1 + fi + return 0 + fi + if [[ -e "$install_dir" ]]; then + log_err "Install path exists but is not a git checkout: $install_dir" + return 1 + fi + + ensure_dir "$(dirname "$install_dir")" + git init --quiet "$install_dir" + git -C "$install_dir" remote add origin "$repo" + git -C "$install_dir" fetch --depth 1 origin "$commit" + git -C "$install_dir" checkout --quiet --detach FETCH_HEAD + local resolved + resolved=$(git -C "$install_dir" rev-parse HEAD) + if [[ "$resolved" != "$commit" ]]; then + log_err "Pinned checkout verification failed for $repo: expected $commit, got $resolved" + return 1 + fi +} + write_mcp_server() { local name="$1" local json_payload="$2" @@ -444,12 +499,14 @@ ensure_jeb_pro() { ensure_jadx() { if has_cmd jadx; then log_ok "jadx ready: $(cmd_path jadx)"; return 0; fi ensure_java_runtime - local tag="v1.5.6" - local sha="545ea2be9c242511bc145755cf4bda2485ade42966e096f8b4d3da2a230e8974" - local re='^jadx-1\.5\.6\.zip$' + local repo re tag sha + repo=$(manifest_field jadx repo) + re=$(manifest_field jadx assetRegex) + tag=$(manifest_field jadx releaseTag) + sha=$(manifest_field jadx assetSha256) case "$PLATFORM" in - macos) install_brew jadx || install_github_release skylot/jadx "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; - linux) install_github_release skylot/jadx "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; + macos) install_brew jadx || install_github_release "$repo" "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; + linux) install_github_release "$repo" "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; esac } @@ -462,10 +519,12 @@ ensure_apktool() { if install_apt apktool; then return 0; fi ensure_dir "$TOOLS_ROOT/apktool" local meta url digest jar wrapper - local tag="v3.0.2" - local sha="eee4669a704a14e0623407e6701b0b91887e61e1e4049cb7a82833e14ae8b5fd" - local re='^apktool_3\.0\.2\.jar$' - meta=$(latest_github_asset_meta iBotPeaches/Apktool "$re" "$tag") + local repo tag sha re + repo=$(manifest_field apktool repo) + tag=$(manifest_field apktool releaseTag) + sha=$(manifest_field apktool assetSha256) + re=$(manifest_field apktool assetRegex) + meta=$(latest_github_asset_meta "$repo" "$re" "$tag") url=$(printf '%s' "$meta" | cut -f1) digest=$(printf '%s' "$meta" | cut -f2) jar="$TOOLS_ROOT/apktool/apktool.jar" @@ -482,40 +541,56 @@ ensure_apktool() { ensure_frida_tools() { ensure_python_runtime if has_cmd frida && has_cmd frida-ps; then log_ok "frida-tools ready"; return 0; fi - pipx install frida-tools || pipx upgrade frida-tools + local package + package=$(manifest_field frida pipPackage) + pipx install --force "$package" || return 1 export PATH="$HOME/.local/bin:$PATH" } ensure_idalib_mcp() { ensure_python_runtime if has_cmd ida-pro-mcp; then log_ok "ida-pro-mcp ready: $(cmd_path ida-pro-mcp)"; return 0; fi - pipx install 'git+https://github.com/mrexodia/ida-pro-mcp.git' || pipx upgrade ida-pro-mcp + local source + source=$(manifest_field idalib-mcp pipSource) + pipx install --force "$source" || return 1 export PATH="$HOME/.local/bin:$PATH" log_warn "Post-install: run 'ida-pro-mcp --install', choose Streamable HTTP + Global, then restart IDA Pro." } ensure_jshookmcp() { ensure_node_runtime - write_mcp_server "jshook" '{"command":"npx","args":["-y","@jshookmcp/jshook@0.3.4"],"env":{"JSHOOK_BASE_PROFILE":"search"}}' + local package + package=$(manifest_field jshookmcp npmPackage) + write_mcp_server "jshook" "$(python3 - "$package" <<'PY' +import json, sys +print(json.dumps({'command':'npx','args':['-y',sys.argv[1]],'env':{'JSHOOK_BASE_PROFILE':'search'}})) +PY +)" } ensure_reqable_mcp() { ensure_node_runtime - write_mcp_server "reqable-mcp" '{"command":"npx","args":["-y","reqable-mcp-server@1.0.1"]}' + local package + package=$(manifest_field reqable-mcp npmPackage) + write_mcp_server "reqable-mcp" "$(python3 - "$package" <<'PY' +import json, sys +print(json.dumps({'command':'npx','args':['-y',sys.argv[1]]})) +PY +)" log_warn "Reqable MCP requires the separately installed Reqable desktop application and its local API." } ensure_anything_analyzer() { + local dir="$TOOLS_ROOT/anything-analyzer" + local repo commit + repo=$(manifest_field anything-analyzer repoUrl) + commit=$(manifest_field anything-analyzer pinnedCommit) + if ! has_cmd git; then + case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac + fi + install_git_commit "$repo" "$commit" "$dir" || return 1 ensure_node_runtime ensure_pnpm - local dir="$TOOLS_ROOT/anything-analyzer" - if [[ ! -d "$dir/.git" ]]; then - if ! has_cmd git; then - case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac - fi - rm -rf "$dir" - git clone https://github.com/Mouseww/anything-analyzer "$dir" - fi write_mcp_server "anything-analyzer" '{"url":"http://localhost:23816/mcp"}' if $START_SERVICES; then (cd "$dir" && pnpm install && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &) @@ -574,7 +649,9 @@ ensure_adb() { ensure_agent_browser() { ensure_node_runtime if has_cmd agent-browser; then log_ok "agent-browser ready"; return 0; fi - npm install -g agent-browser + local package + package=$(manifest_field agent-browser npmPackage) + npm install -g "$package" || return 1 if has_cmd npx; then npx playwright install chromium || true; fi local setup="$SKILL_ROOT/browser-automation/scripts/setup.sh" if [[ -x "$setup" ]]; then "$setup" --skip-browser-install || true; fi @@ -582,6 +659,9 @@ ensure_agent_browser() { ensure_ghidra_mcp() { ensure_java_runtime + local repo regex + repo=$(manifest_field ghidra-mcp repo) + regex=$(manifest_field ghidra-mcp assetRegex) case "$PLATFORM" in macos) if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then @@ -590,7 +670,7 @@ ensure_ghidra_mcp() { ;; linux) if ! has_cmd ghidraRun; then - install_github_release NationalSecurityAgency/ghidra '^ghidra_.*_PUBLIC_.*\.zip$' "$TOOLS_ROOT/ghidra" || \ + install_github_release "$repo" "$regex" "$TOOLS_ROOT/ghidra" || \ manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used." fi ;; @@ -600,15 +680,26 @@ ensure_ghidra_mcp() { ensure_seclists() { local dir="$TOOLS_ROOT/SecLists" - if [[ -d "$dir/.git" || -d /usr/share/seclists ]]; then log_ok "SecLists ready"; return 0; fi + if [[ -d /usr/share/seclists ]]; then log_ok "SecLists ready"; return 0; fi if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac; fi - git clone https://github.com/danielmiessler/SecLists "$dir" + local repo commit + repo=$(manifest_field seclists repo) + commit=$(manifest_field seclists pinnedCommit) + install_git_commit "$repo" "$commit" "$dir" || return 1 } ensure_proxycat() { ensure_python_runtime if has_cmd proxycat; then log_ok "proxycat ready"; return 0; fi - pipx install git+https://github.com/honmashironeko/ProxyCat.git || manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'." + local repo commit + repo=$(manifest_field proxycat repo) + commit=$(manifest_field proxycat pinnedCommit) + pipx install "git+${repo}@${commit}" || { + manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'." + LAST_CAPABILITY_MANUAL=true + MANUAL_REQUIRED=true + return 0 + } } ensure_burpsuite_mcp() { @@ -649,7 +740,10 @@ ensure_pentestswarm() { if ! has_cmd go; then case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac fi - if go install github.com/Armur-Ai/Pentest-Swarm-AI/cmd/pentestswarm@v0.1.0; then + local go_package docker_image + go_package=$(manifest_field pentestswarm goPackage) + docker_image=$(manifest_field pentestswarm dockerImage) + if go install "$go_package"; then local go_bin go_bin="$(go env GOBIN 2>/dev/null || true)" if [[ -z "$go_bin" ]]; then @@ -663,8 +757,12 @@ ensure_pentestswarm() { log_warn "pentestswarm installed but no executable was found in GOBIN/GOPATH; trying Docker fallback" fi if has_cmd docker; then - write_mcp_server "pentestswarm" '{"command":"docker","args":["run","--rm","-i","ghcr.io/armur-ai/pentestswarm:v0.1.0","mcp","serve"]}' - log_warn "pentestswarm Go install failed or produced no runnable binary; registered Docker fallback ghcr.io/armur-ai/pentestswarm:v0.1.0" + write_mcp_server "pentestswarm" "$(python3 - "$docker_image" <<'PY' +import json, sys +print(json.dumps({'command':'docker','args':['run','--rm','-i',sys.argv[1],'mcp','serve']})) +PY +)" + log_warn "pentestswarm Go install failed or produced no runnable binary; registered Docker fallback $docker_image" else manual_required pentestswarm "Install Go 1.24+ or Docker, then install Pentest-Swarm-AI and ensure pentestswarm is on PATH." fi @@ -689,7 +787,9 @@ ensure_yara() { ensure_pwntools() { ensure_python_runtime if python3 -c "import pwn" 2>/dev/null; then log_ok "pwntools ready"; return 0; fi - pipx install pwntools || python3 -m pip install --user pwntools + local package + package=$(manifest_field pwntools pipPackage) + pipx install "$package" || python3 -m pip install --user "$package" || return 1 } status_json_line() { @@ -779,6 +879,7 @@ for cap in "${EXPANDED[@]}"; do fi else status_json_line "$cap" "failed" "see $(platform_doc)" >> "$RESULTS_FILE" + FAILED=true fi done @@ -787,7 +888,9 @@ if ! $SKIP_REFRESH; then fi FINAL_EXIT_CODE=0 -if $MANUAL_REQUIRED; then +if $FAILED; then + FINAL_EXIT_CODE=1 +elif $MANUAL_REQUIRED; then FINAL_EXIT_CODE=2 fi diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh new file mode 100644 index 0000000..f2677b4 --- /dev/null +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BOOTSTRAP="$SCRIPT_DIR/bootstrap-reverse.sh" +MANIFEST="$SCRIPT_DIR/bootstrap-manifest.json" +KALI_BOOTSTRAP="$SCRIPT_DIR/../../kali/scripts/bootstrap-reverse.sh" +REAL_PYTHON="$(command -v python3)" +SCRATCH="$(mktemp -d /tmp/reverse-bootstrap-test-XXXXXX)" +trap 'rm -rf "$SCRATCH"' EXIT + +STUB_BIN="$SCRATCH/bin" +CALL_LOG="$SCRATCH/calls.log" +mkdir -p "$STUB_BIN" "$SCRATCH/home" "$SCRATCH/tools" + +cat > "$STUB_BIN/command-stub" <<'STUB' +#!/usr/bin/env bash +name="$(basename "$0")" +{ + printf '%s' "$name" + for arg in "$@"; do printf '|%s' "$arg"; done + printf '\n' +} >> "$CALL_LOG" + +if [[ "${STUB_FAIL_COMMAND:-}" == "$name" ]]; then + exit 1 +fi + +if [[ "$name" == "git" ]]; then + if [[ "${1:-}" == "init" ]]; then + target="${!#}" + mkdir -p "$target/.git" + printf '%s\n' 'unpinned-head' > "$target/.stub-head" + elif [[ "${1:-}" == "-C" && "${3:-}" == "fetch" ]]; then + printf '%s\n' "${7:-}" > "$2/.stub-fetch" + elif [[ "${1:-}" == "-C" && "${3:-}" == "checkout" ]]; then + cat "$2/.stub-fetch" > "$2/.stub-head" + elif [[ "${1:-}" == "-C" && "${3:-}" == "rev-parse" ]]; then + cat "$2/.stub-head" + fi +fi +exit 0 +STUB +chmod +x "$STUB_BIN/command-stub" +for command_name in git node npm npx pipx pnpm sleep; do + ln -s command-stub "$STUB_BIN/$command_name" +done + +cat > "$STUB_BIN/python3" <&2 + failures=$((failures + 1)) + fi +} + +: > "$CALL_LOG" +run_bootstrap frida --skip-refresh >/dev/null +frida_package="$(manifest_value frida pipPackage)" +check_log_line "pipx|install|--force|$frida_package" + +: > "$CALL_LOG" +run_bootstrap idalib-mcp --skip-refresh >/dev/null +idalib_source="$(manifest_value idalib-mcp pipSource)" +check_log_line "pipx|install|--force|$idalib_source" + +: > "$CALL_LOG" +set +e +run_bootstrap_with_failing_pipx idalib-mcp --skip-refresh >/dev/null 2>&1 +idalib_fail_exit=$? +set -e +if [[ "$idalib_fail_exit" -eq 0 ]]; then + printf 'idalib-mcp reported success after its pinned install failed\n' >&2 + failures=$((failures + 1)) +fi +check_log_line "pipx|install|--force|$idalib_source" +if [[ "$(wc -l < "$CALL_LOG" | tr -d ' ')" -ne 1 ]]; then + printf 'idalib-mcp attempted an unpinned fallback after pinned install failure\n' >&2 + failures=$((failures + 1)) +fi + +: > "$CALL_LOG" +run_bootstrap agent-browser --skip-refresh >/dev/null +agent_package="$(manifest_value agent-browser npmPackage)" +check_log_line "npm|install|-g|$agent_package" + +: > "$CALL_LOG" +run_bootstrap seclists --skip-refresh >/dev/null +seclists_repo="$(manifest_value seclists repo)" +seclists_pin="$(manifest_value seclists pinnedCommit)" +seclists_dir="$SCRATCH/tools/SecLists" +check_log_line "git|-C|$seclists_dir|remote|add|origin|$seclists_repo" +check_log_line "git|-C|$seclists_dir|fetch|--depth|1|origin|$seclists_pin" + +: > "$CALL_LOG" +run_bootstrap proxycat --skip-refresh >/dev/null +proxycat_repo="$(manifest_value proxycat repo)" +proxycat_pin="$(manifest_value proxycat pinnedCommit)" +check_log_line "pipx|install|git+${proxycat_repo}@${proxycat_pin}" + +: > "$CALL_LOG" +run_bootstrap pwntools --skip-refresh >/dev/null +pwntools_package="$(manifest_value pwntools pipPackage)" +check_log_line "pipx|install|$pwntools_package" + +: > "$CALL_LOG" +run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null +anything_repo="$(manifest_value anything-analyzer repoUrl)" +anything_pin="$(manifest_value anything-analyzer pinnedCommit)" +anything_dir="$SCRATCH/tools/anything-analyzer" +if [[ -z "$anything_pin" ]]; then + printf 'anything-analyzer is missing pinnedCommit in bootstrap-manifest.json\n' >&2 + failures=$((failures + 1)) +else + check_log_line "git|init|--quiet|$anything_dir" + check_log_line "git|-C|$anything_dir|remote|add|origin|$anything_repo" + check_log_line "git|-C|$anything_dir|fetch|--depth|1|origin|$anything_pin" + check_log_line "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD" + check_log_line "git|-C|$anything_dir|rev-parse|HEAD" +fi +check_log_line 'pnpm|install' +check_log_line 'pnpm|dev' + +if [[ -n "$anything_pin" ]]; then + checkout_line="$(grep -nF "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD" "$CALL_LOG" | cut -d: -f1 | head -n1)" + install_line="$(grep -nF 'pnpm|install' "$CALL_LOG" | cut -d: -f1 | head -n1)" + if [[ -z "$checkout_line" || -z "$install_line" || "$checkout_line" -ge "$install_line" ]]; then + printf 'anything-analyzer dependencies ran before the pinned checkout\n' >&2 + failures=$((failures + 1)) + fi +fi + +: > "$CALL_LOG" +mkdir -p "$anything_dir/.git" +printf '%s\n' 'different-commit' > "$anything_dir/.stub-head" +set +e +run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 +mismatch_exit=$? +set -e +if [[ "$mismatch_exit" -eq 0 ]]; then + printf 'anything-analyzer accepted an existing checkout at a different commit\n' >&2 + failures=$((failures + 1)) +fi +if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then + printf 'anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2 + failures=$((failures + 1)) +fi + +if (( BASH_VERSINFO[0] >= 4 )); then + : > "$CALL_LOG" + kali_dir="$SCRATCH/home/tools/anything-analyzer" + rm -rf "$kali_dir" + set +e + run_kali_bootstrap anything-analyzer --start-services --skip-refresh >"$SCRATCH/kali-bootstrap.out" 2>&1 + set -e + check_log_line "git|init|-q|$kali_dir" + check_log_line "git|-C|$kali_dir|remote|add|origin|$anything_repo" + check_log_line "git|-C|$kali_dir|fetch|--depth|1|origin|$anything_pin" + check_log_line "git|-C|$kali_dir|checkout|-q|--detach|FETCH_HEAD" + check_log_line 'pnpm|install' + check_log_line 'pnpm|dev' + + : > "$CALL_LOG" + mkdir -p "$kali_dir/.git" + printf '%s\n' 'different-commit' > "$kali_dir/.stub-head" + set +e + run_kali_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 + kali_mismatch_exit=$? + set -e + if [[ "$kali_mismatch_exit" -eq 0 ]]; then + printf 'Kali anything-analyzer accepted an existing checkout at a different commit\n' >&2 + failures=$((failures + 1)) + fi + if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then + printf 'Kali anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2 + failures=$((failures + 1)) + fi +fi + +if [[ "$failures" -ne 0 ]]; then + if [[ -f "$SCRATCH/kali-bootstrap.out" ]]; then cat "$SCRATCH/kali-bootstrap.out" >&2; fi + printf '%s\n' 'captured argv:' >&2 + cat "$CALL_LOG" >&2 + exit 1 +fi + +printf '%s\n' 'bootstrap manifest source regression passed' diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index 54469fa..8788539 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -389,6 +389,7 @@ if (Test-Path -LiteralPath $kaliManifest) { # --- supply-chain pin gate: auto-install download sources MUST be pinned --- # 统一判定:pinnedVersion / pinnedCommit / pinPolicy 三选一; # github-release-* 额外接受 assetSha256 / preferApiDigest(GitHub 官方发布资产哈希)。 +# local-http-mcp 只有在不获取外部源码时才可免 pin。 $pinKinds = @('pip-package', 'npm-mcp', 'npm-global', 'go-install', 'git-clone') foreach ($mf in @($skillsManifest, $kaliManifest)) { if (-not (Test-Path -LiteralPath $mf)) { continue } @@ -401,7 +402,10 @@ foreach ($mf in @($skillsManifest, $kaliManifest)) { 'github-release-zip' { $hasPin = $hasPin -or $cap.assetSha256 -or $cap.preferApiDigest } 'github-release-jar-wrapper' { $hasPin = $hasPin -or $cap.assetSha256 } 'github-release-tar' { $hasPin = $hasPin -or $cap.assetSha256 -or $cap.preferApiDigest } - 'local-http-mcp' { $hasPin = $true } # 本地服务,不下载 + 'local-http-mcp' { + $fetchesExternalSource = $cap.repoUrl -or $cap.repo + $hasPin = (-not $fetchesExternalSource) -or $cap.pinnedCommit -or $cap.pinnedVersion + } 'winget-package' { $hasPin = $hasPin } # winget-latest 属于 pinPolicy 'apt-package' { $hasPin = $true } # 发行版仓库自带(Kali 侧) 'docker-image' { $hasPin = $true } # fallback 通道 From 1d929bf5dbb38a6721764ba05747f6857de54c76 Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:00:49 +0530 Subject: [PATCH 02/10] fix(bootstrap): harden pinned checkout promotion --- kali/scripts/bootstrap-manifest.json | 10 + kali/scripts/bootstrap-reverse.sh | 68 ++++- skills/scripts/bootstrap-manifest.json | 10 + skills/scripts/bootstrap-reverse.ps1 | 137 ++++++--- skills/scripts/bootstrap-reverse.sh | 133 ++++++-- skills/scripts/test-bootstrap-manifest.sh | 322 ++++++++------------ skills/scripts/verify-routing-coherence.ps1 | 11 + 7 files changed, 400 insertions(+), 291 deletions(-) diff --git a/kali/scripts/bootstrap-manifest.json b/kali/scripts/bootstrap-manifest.json index 6232906..85d94f8 100644 --- a/kali/scripts/bootstrap-manifest.json +++ b/kali/scripts/bootstrap-manifest.json @@ -23,6 +23,16 @@ "metasploitmcp — Metasploit MCP Server (apt install metasploitmcp, port 8085)", "hexstrike-ai — 150+ 安全工具 MCP 自动化 (apt install hexstrike-ai)" ], + "bootstrapDependencies": { + "pipx": { + "package": "pipx==1.16.5", + "version": "1.16.5" + }, + "pnpm": { + "package": "pnpm@10.24.0", + "version": "10.24.0" + } + }, "capabilities": [ { "name": "jadx", diff --git a/kali/scripts/bootstrap-reverse.sh b/kali/scripts/bootstrap-reverse.sh index e6357e3..651f630 100644 --- a/kali/scripts/bootstrap-reverse.sh +++ b/kali/scripts/bootstrap-reverse.sh @@ -132,13 +132,24 @@ install_git_commit() { local install_dir="$3" if [[ -d "$install_dir/.git" ]]; then - local current - current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null || true) + local current status + if ! current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null); then + log_err "无法解析现有 checkout HEAD: $install_dir" + return 1 + fi if [[ "$current" != "$commit" ]]; then log_err "Existing checkout is not at pinned commit $commit: $install_dir" log_err "Move it aside explicitly, then retry; bootstrap will not overwrite local changes." return 1 fi + if ! status=$(git -C "$install_dir" status --porcelain --untracked-files=all); then + log_err "无法检查 checkout 状态: $install_dir" + return 1 + fi + if [[ -n "$status" ]]; then + log_err "现有 checkout 含本地修改,拒绝执行: $install_dir" + return 1 + fi return 0 fi if [[ -e "$install_dir" ]]; then @@ -146,15 +157,33 @@ install_git_commit() { return 1 fi - mkdir -p "$(dirname "$install_dir")" - git init -q "$install_dir" - git -C "$install_dir" remote add origin "$repo" - git -C "$install_dir" fetch --depth 1 origin "$commit" - git -C "$install_dir" checkout -q --detach FETCH_HEAD - local resolved - resolved=$(git -C "$install_dir" rev-parse HEAD) + local parent stage resolved status + parent=$(dirname "$install_dir") + mkdir -p "$parent" + stage=$(mktemp -d "$parent/.reverse-bootstrap-XXXXXX") || return 1 + if ! git init -q "$stage" || + ! git -C "$stage" remote add origin "$repo" || + ! git -C "$stage" fetch --depth 1 origin "$commit" || + ! git -C "$stage" checkout -q --detach FETCH_HEAD; then + rm -rf "$stage" + return 1 + fi + if ! resolved=$(git -C "$stage" rev-parse HEAD); then + rm -rf "$stage" + return 1 + fi if [[ "$resolved" != "$commit" ]]; then log_err "Pinned checkout verification failed (expected $commit, got $resolved)" + rm -rf "$stage" + return 1 + fi + if ! status=$(git -C "$stage" status --porcelain --untracked-files=all) || [[ -n "$status" ]]; then + log_err "Staged checkout is not clean: $stage" + rm -rf "$stage" + return 1 + fi + if ! mv -T "$stage" "$install_dir"; then + rm -rf "$stage" return 1 fi } @@ -331,6 +360,13 @@ manifest_field() { '.capabilities[] | select(.name == $name) | .[$field] // empty' "$KALI_MANIFEST" } +manifest_dependency() { + local name="$1" + local field="$2" + jq -er --arg name "$name" --arg field "$field" \ + '.bootstrapDependencies[$name][$field] // empty' "$KALI_MANIFEST" +} + install_manifest_release() { local capability="$1" local repo asset_regex install_dir release_tag asset_sha256 @@ -636,11 +672,19 @@ start_anything_analyzer() { commit=$(manifest_field anything-analyzer pinnedCommit) install_git_commit "$repo" "$commit" "$repo_dir" || return 1 - if ! command -v pnpm &>/dev/null; then - npm install -g pnpm + local pnpm_package pnpm_version current_pnpm_version='' + pnpm_package=$(manifest_dependency pnpm package) || return 1 + pnpm_version=$(manifest_dependency pnpm version) || return 1 + if command -v pnpm &>/dev/null; then + current_pnpm_version=$(pnpm --version 2>/dev/null | head -n1 | tr -d '[:space:]') + fi + if [[ "$current_pnpm_version" != "$pnpm_version" ]]; then + npm install -g "$pnpm_package" || return 1 fi - (cd "$repo_dir" && pnpm install && nohup pnpm dev > /tmp/anything-analyzer.log 2>&1 &) + (cd "$repo_dir" && pnpm install --frozen-lockfile) || return 1 + install_git_commit "$repo" "$commit" "$repo_dir" || return 1 + (cd "$repo_dir" && nohup pnpm dev > /tmp/anything-analyzer.log 2>&1 &) log_info "等待 anything-analyzer 启动 (port 23816) ..." if wait_for_port 23816 120; then diff --git a/skills/scripts/bootstrap-manifest.json b/skills/scripts/bootstrap-manifest.json index 6457dc0..b693856 100644 --- a/skills/scripts/bootstrap-manifest.json +++ b/skills/scripts/bootstrap-manifest.json @@ -1,4 +1,14 @@ { + "bootstrapDependencies": { + "pipx": { + "package": "pipx==1.16.5", + "version": "1.16.5" + }, + "pnpm": { + "package": "pnpm@10.24.0", + "version": "10.24.0" + } + }, "capabilities": [ { "name": "jadx", diff --git a/skills/scripts/bootstrap-reverse.ps1 b/skills/scripts/bootstrap-reverse.ps1 index 59d0f2d..212b450 100644 --- a/skills/scripts/bootstrap-reverse.ps1 +++ b/skills/scripts/bootstrap-reverse.ps1 @@ -24,6 +24,17 @@ $OutputEncoding = [System.Text.UTF8Encoding]::new($false) . (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1') +function Get-BootstrapDependency { + param([Parameter(Mandatory = $true)][string]$Name) + + $manifest = Get-Content -LiteralPath (Get-ReverseBootstrapManifestPath) -Raw -Encoding UTF8 | ConvertFrom-Json + $dependency = $manifest.bootstrapDependencies.PSObject.Properties[$Name].Value + if ($null -eq $dependency -or [string]::IsNullOrWhiteSpace([string]$dependency.package) -or [string]::IsNullOrWhiteSpace([string]$dependency.version)) { + throw "bootstrapDependencies.$Name must define package and version." + } + return $dependency +} + $Capability = @( foreach ($item in @($Capability)) { if ([string]::IsNullOrWhiteSpace($item)) { @@ -155,14 +166,23 @@ function Ensure-JavaRuntime { function Ensure-Pnpm { Ensure-NodeRuntime - if (-not (Get-NodeCommandPath -Name 'pnpm')) { + $dependency = Get-BootstrapDependency -Name 'pnpm' + $pnpm = Get-NodeCommandPath -Name 'pnpm' + $currentVersion = '' + if ($pnpm) { + $versionLine = & $pnpm --version 2>$null | Select-Object -First 1 + if ($LASTEXITCODE -eq 0 -and $null -ne $versionLine) { + $currentVersion = ([string]$versionLine).Trim() + } + } + if ($currentVersion -ne [string]$dependency.version) { $npm = Get-NodeCommandPath -Name 'npm' if ([string]::IsNullOrWhiteSpace($npm)) { throw 'npm is not available after Node.js installation.' } - & $npm install -g pnpm + & $npm install -g ([string]$dependency.package) if ($LASTEXITCODE -ne 0) { - throw 'Failed to install pnpm globally.' + throw "Failed to install pinned pnpm dependency $($dependency.package)." } } } @@ -341,34 +361,11 @@ function Set-AnythingAnalyzerPnpmBuildApprovals { function Approve-AnythingAnalyzerBuildScripts { param( - [Parameter(Mandatory = $true)][string]$RepoDir, - [Parameter(Mandatory = $true)][string]$PnpmPath + [Parameter(Mandatory = $true)][string]$RepoDir ) $buildPackages = @('electron', 'esbuild', 'better-sqlite3') - Push-Location $RepoDir - try { - $approveExitCode = 1 - try { - $approveOutput = & $PnpmPath approve-builds --all 2>&1 - $approveExitCode = $LASTEXITCODE - } - catch { - $approveOutput = $_.Exception.Message - $approveExitCode = 1 - } - - if ($approveExitCode -eq 0) { - return - } - - Write-Warning 'pnpm approve-builds --all is unavailable or failed; writing pnpm-workspace.yaml build approvals directly.' - } - finally { - Pop-Location - } - Set-AnythingAnalyzerPnpmBuildApprovals -RepoDir $RepoDir -Packages $buildPackages } @@ -805,9 +802,13 @@ if (Test-ReverseIsWindows) { throw 'pnpm is not available after installation.' } + $workspacePath = Join-Path $repoDir 'pnpm-workspace.yaml' + $workspaceExisted = Test-Path -LiteralPath $workspacePath -PathType Leaf + $workspaceBytes = if ($workspaceExisted) { [IO.File]::ReadAllBytes($workspacePath) } else { $null } + Push-Location $repoDir try { - Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir -PnpmPath $pnpm + Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) { $nodeModules = Join-Path $repoDir 'node_modules' @@ -816,7 +817,7 @@ if (Test-ReverseIsWindows) { } } - & $pnpm install + & $pnpm install --frozen-lockfile if ($LASTEXITCODE -ne 0) { if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) { throw "pnpm install failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError" @@ -838,8 +839,17 @@ if (Test-ReverseIsWindows) { } finally { Pop-Location + if ($workspaceExisted) { + [IO.File]::WriteAllBytes($workspacePath, $workspaceBytes) + } + elseif (Test-Path -LiteralPath $workspacePath) { + Remove-Item -LiteralPath $workspacePath -Force + } } + $git = Get-FirstCommandPath -Names @('git') + Assert-GitCheckoutState -GitPath $git -CheckoutPath $repoDir -PinnedCommit ([string]$Definition.pinnedCommit) + $stdoutLog = Join-Path $repoDir 'anything-analyzer-dev.log' $stderrLog = Join-Path $repoDir 'anything-analyzer-dev.err.log' Remove-Item -LiteralPath $stdoutLog, $stderrLog -Force -ErrorAction SilentlyContinue @@ -879,6 +889,27 @@ function Ensure-AndroidPlatformTools { return (Resolve-ReverseToolSpec -Name 'adb') } +function Assert-GitCheckoutState { + param( + [Parameter(Mandatory = $true)][string]$GitPath, + [Parameter(Mandatory = $true)][string]$CheckoutPath, + [Parameter(Mandatory = $true)][string]$PinnedCommit + ) + + $resolvedLine = & $GitPath -C $CheckoutPath rev-parse HEAD 2>$null | Select-Object -First 1 + $resolvedCommit = if ($null -eq $resolvedLine) { '' } else { ([string]$resolvedLine).Trim() } + if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $PinnedCommit) { + throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)" + } + $status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "Cannot inspect checkout state: $CheckoutPath" + } + if ($status.Count -gt 0) { + throw "Checkout has local changes; refusing to execute it: $CheckoutPath" + } +} + function Ensure-GitCloneInstall { param( [Parameter(Mandatory = $true)]$Definition, @@ -892,36 +923,42 @@ function Ensure-GitCloneInstall { } if ((Test-Path -LiteralPath $TargetPath -PathType Container) -and (Test-Path -LiteralPath (Join-Path $TargetPath '.git'))) { - if (-not [string]::IsNullOrWhiteSpace($pinnedCommit)) { - $currentCommit = (& $git -C $TargetPath rev-parse HEAD).Trim() - if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $pinnedCommit) { - throw "Existing checkout is not at pinned commit $pinnedCommit. Move it aside explicitly, then retry: $TargetPath" - } + if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { + throw "Git capability $($Definition.repo) must define pinnedCommit before an existing checkout can be used." } + Assert-GitCheckoutState -GitPath $git -CheckoutPath $TargetPath -PinnedCommit $pinnedCommit return $true } if (Test-Path -LiteralPath $TargetPath) { - $backupPath = "$TargetPath.bak-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))" - Move-Item -LiteralPath $TargetPath -Destination $backupPath -Force + throw "Install path exists but is not a git checkout: $TargetPath" + } + if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { + throw "Git capability $($Definition.repo) must define pinnedCommit." } - Ensure-DownloadDirectory -Path (Split-Path -Path $TargetPath -Parent) - - if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { - & $git clone --depth 1 $Definition.repo $TargetPath - if ($LASTEXITCODE -ne 0) { - throw "git clone failed for $($Definition.repo)" + $parent = Split-Path -Path $TargetPath -Parent + Ensure-DownloadDirectory -Path $parent + $stagePath = Join-Path $parent ('.reverse-bootstrap-{0}' -f [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $stagePath | Out-Null + try { + & $git init --quiet $stagePath + if ($LASTEXITCODE -ne 0) { throw 'git init failed' } + & $git -C $stagePath remote add origin $Definition.repo + if ($LASTEXITCODE -ne 0) { throw 'git remote add failed' } + & $git -C $stagePath fetch --depth 1 origin $pinnedCommit + if ($LASTEXITCODE -ne 0) { throw 'git fetch failed' } + & $git -C $stagePath checkout --quiet --detach FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw 'git checkout failed' } + Assert-GitCheckoutState -GitPath $git -CheckoutPath $stagePath -PinnedCommit $pinnedCommit + Move-Item -LiteralPath $stagePath -Destination $TargetPath + if ((Test-Path -LiteralPath $stagePath) -or -not (Test-Path -LiteralPath (Join-Path $TargetPath '.git') -PathType Container)) { + throw "Failed to promote staged checkout to $TargetPath" } } - else { - & $git init --quiet $TargetPath - & $git -C $TargetPath remote add origin $Definition.repo - & $git -C $TargetPath fetch --depth 1 origin $pinnedCommit - & $git -C $TargetPath checkout --quiet --detach FETCH_HEAD - $resolvedCommit = (& $git -C $TargetPath rev-parse HEAD).Trim() - if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $pinnedCommit) { - throw "Pinned checkout verification failed for $($Definition.repo): expected $pinnedCommit, got $resolvedCommit" + finally { + if (Test-Path -LiteralPath $stagePath) { + Remove-Item -LiteralPath $stagePath -Recurse -Force } } diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index 74c1133..a8016fa 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -89,6 +89,19 @@ raise SystemExit(1) PY } +manifest_dependency() { + local name="$1" + local field="$2" + python3 - "$MANIFEST_PATH" "$name" "$field" <<'PY' +import json, pathlib, sys +manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')) +value = manifest.get('bootstrapDependencies', {}).get(sys.argv[2], {}).get(sys.argv[3]) +if value is None or value == '': + raise SystemExit(1) +print(value) +PY +} + safe_remove_install_dir() { local target="$1" local tmp_target="${2:-}" @@ -218,15 +231,15 @@ ensure_python_runtime() { *) log_err "Install Python 3 manually. See $(platform_doc)"; return 1 ;; esac fi - if ! has_cmd pipx; then - case "$PLATFORM" in - macos) - python3 -m pip install --user pipx || install_brew pipx - ;; - linux) - install_apt pipx || python3 -m pip install --user pipx - ;; - esac + local pipx_package pipx_version current_version + pipx_package=$(manifest_dependency pipx package) || return 1 + pipx_version=$(manifest_dependency pipx version) || return 1 + current_version="" + if has_cmd pipx; then + current_version=$(pipx --version 2>/dev/null | head -n1 | tr -d '[:space:]') + fi + if [[ "$current_version" != "$pipx_version" ]]; then + python3 -m pip install --user --upgrade "$pipx_package" || return 1 fi python3 -m pipx ensurepath >/dev/null 2>&1 || true export PATH="$HOME/.local/bin:$PATH" @@ -251,10 +264,17 @@ ensure_java_runtime() { } ensure_pnpm() { - ensure_node_runtime - if has_cmd pnpm; then return 0; fi - if has_cmd corepack; then corepack enable || true; fi - if ! has_cmd pnpm; then npm install -g pnpm; fi + ensure_node_runtime || return 1 + local package version current_version + package=$(manifest_dependency pnpm package) || return 1 + version=$(manifest_dependency pnpm version) || return 1 + current_version="" + if has_cmd pnpm; then + current_version=$(pnpm --version 2>/dev/null | head -n1 | tr -d '[:space:]') + fi + if [[ "$current_version" != "$version" ]]; then + npm install -g "$package" || return 1 + fi } # Args: repo regex [release_tag] @@ -378,14 +398,40 @@ install_git_commit() { local commit="$2" local install_dir="$3" + git_checkout_is_clean() { + local checkout="$1" + local status + if ! status=$(git -C "$checkout" status --porcelain --untracked-files=all); then + log_err "Cannot inspect checkout state: $checkout" + return 1 + fi + if [[ -n "$status" ]]; then + log_err "Existing checkout has local changes; refusing to execute it: $checkout" + return 1 + fi + } + + cleanup_git_stage() { + local stage="$1" + local parent="$2" + case "$stage" in + "$parent"/.reverse-bootstrap-*) rm -rf "$stage" ;; + *) log_err "Refusing to clean unexpected staging path: $stage" ;; + esac + } + if [[ -d "$install_dir/.git" ]]; then local current - current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null || true) + if ! current=$(git -C "$install_dir" rev-parse HEAD 2>/dev/null); then + log_err "Cannot resolve existing checkout HEAD: $install_dir" + return 1 + fi if [[ "$current" != "$commit" ]]; then log_err "Existing checkout is not at pinned commit $commit: $install_dir" log_err "Move it aside explicitly, then retry; bootstrap will not overwrite local changes." return 1 fi + git_checkout_is_clean "$install_dir" || return 1 return 0 fi if [[ -e "$install_dir" ]]; then @@ -393,15 +439,36 @@ install_git_commit() { return 1 fi - ensure_dir "$(dirname "$install_dir")" - git init --quiet "$install_dir" - git -C "$install_dir" remote add origin "$repo" - git -C "$install_dir" fetch --depth 1 origin "$commit" - git -C "$install_dir" checkout --quiet --detach FETCH_HEAD - local resolved - resolved=$(git -C "$install_dir" rev-parse HEAD) + local parent stage resolved + parent=$(dirname "$install_dir") + ensure_dir "$parent" + stage=$(mktemp -d "$parent/.reverse-bootstrap-XXXXXX") || return 1 + if ! git init --quiet "$stage" || + ! git -C "$stage" remote add origin "$repo" || + ! git -C "$stage" fetch --depth 1 origin "$commit" || + ! git -C "$stage" checkout --quiet --detach FETCH_HEAD; then + cleanup_git_stage "$stage" "$parent" + return 1 + fi + if ! resolved=$(git -C "$stage" rev-parse HEAD); then + cleanup_git_stage "$stage" "$parent" + return 1 + fi if [[ "$resolved" != "$commit" ]]; then log_err "Pinned checkout verification failed for $repo: expected $commit, got $resolved" + cleanup_git_stage "$stage" "$parent" + return 1 + fi + if ! git_checkout_is_clean "$stage"; then + cleanup_git_stage "$stage" "$parent" + return 1 + fi + if ! python3 - "$stage" "$install_dir" <<'PY' +import os, sys +os.rename(sys.argv[1], sys.argv[2]) +PY + then + cleanup_git_stage "$stage" "$parent" return 1 fi } @@ -539,7 +606,7 @@ ensure_apktool() { } ensure_frida_tools() { - ensure_python_runtime + ensure_python_runtime || return 1 if has_cmd frida && has_cmd frida-ps; then log_ok "frida-tools ready"; return 0; fi local package package=$(manifest_field frida pipPackage) @@ -548,7 +615,7 @@ ensure_frida_tools() { } ensure_idalib_mcp() { - ensure_python_runtime + ensure_python_runtime || return 1 if has_cmd ida-pro-mcp; then log_ok "ida-pro-mcp ready: $(cmd_path ida-pro-mcp)"; return 0; fi local source source=$(manifest_field idalib-mcp pipSource) @@ -558,7 +625,7 @@ ensure_idalib_mcp() { } ensure_jshookmcp() { - ensure_node_runtime + ensure_node_runtime || return 1 local package package=$(manifest_field jshookmcp npmPackage) write_mcp_server "jshook" "$(python3 - "$package" <<'PY' @@ -569,7 +636,7 @@ PY } ensure_reqable_mcp() { - ensure_node_runtime + ensure_node_runtime || return 1 local package package=$(manifest_field reqable-mcp npmPackage) write_mcp_server "reqable-mcp" "$(python3 - "$package" <<'PY' @@ -589,11 +656,13 @@ ensure_anything_analyzer() { case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac fi install_git_commit "$repo" "$commit" "$dir" || return 1 - ensure_node_runtime - ensure_pnpm + ensure_node_runtime || return 1 + ensure_pnpm || return 1 write_mcp_server "anything-analyzer" '{"url":"http://localhost:23816/mcp"}' if $START_SERVICES; then - (cd "$dir" && pnpm install && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &) + (cd "$dir" && pnpm install --frozen-lockfile) || return 1 + install_git_commit "$repo" "$commit" "$dir" || return 1 + (cd "$dir" && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &) if wait_for_port 23816 120; then if test_mcp_http 23816; then log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)" @@ -647,7 +716,7 @@ ensure_adb() { } ensure_agent_browser() { - ensure_node_runtime + ensure_node_runtime || return 1 if has_cmd agent-browser; then log_ok "agent-browser ready"; return 0; fi local package package=$(manifest_field agent-browser npmPackage) @@ -658,7 +727,7 @@ ensure_agent_browser() { } ensure_ghidra_mcp() { - ensure_java_runtime + ensure_java_runtime || return 1 local repo regex repo=$(manifest_field ghidra-mcp repo) regex=$(manifest_field ghidra-mcp assetRegex) @@ -689,7 +758,7 @@ ensure_seclists() { } ensure_proxycat() { - ensure_python_runtime + ensure_python_runtime || return 1 if has_cmd proxycat; then log_ok "proxycat ready"; return 0; fi local repo commit repo=$(manifest_field proxycat repo) @@ -785,7 +854,7 @@ ensure_yara() { } ensure_pwntools() { - ensure_python_runtime + ensure_python_runtime || return 1 if python3 -c "import pwn" 2>/dev/null; then log_ok "pwntools ready"; return 0; fi local package package=$(manifest_field pwntools pipPackage) diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh index f2677b4..e5a0bed 100644 --- a/skills/scripts/test-bootstrap-manifest.sh +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -3,12 +3,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BOOTSTRAP="$SCRIPT_DIR/bootstrap-reverse.sh" -MANIFEST="$SCRIPT_DIR/bootstrap-manifest.json" KALI_BOOTSTRAP="$SCRIPT_DIR/../../kali/scripts/bootstrap-reverse.sh" +MANIFEST="$SCRIPT_DIR/bootstrap-manifest.json" REAL_PYTHON="$(command -v python3)" SCRATCH="$(mktemp -d /tmp/reverse-bootstrap-test-XXXXXX)" trap 'rm -rf "$SCRATCH"' EXIT - STUB_BIN="$SCRATCH/bin" CALL_LOG="$SCRATCH/calls.log" mkdir -p "$STUB_BIN" "$SCRATCH/home" "$SCRATCH/tools" @@ -16,223 +15,152 @@ mkdir -p "$STUB_BIN" "$SCRATCH/home" "$SCRATCH/tools" cat > "$STUB_BIN/command-stub" <<'STUB' #!/usr/bin/env bash name="$(basename "$0")" -{ - printf '%s' "$name" - for arg in "$@"; do printf '|%s' "$arg"; done - printf '\n' -} >> "$CALL_LOG" - -if [[ "${STUB_FAIL_COMMAND:-}" == "$name" ]]; then - exit 1 -fi - -if [[ "$name" == "git" ]]; then - if [[ "${1:-}" == "init" ]]; then - target="${!#}" - mkdir -p "$target/.git" - printf '%s\n' 'unpinned-head' > "$target/.stub-head" - elif [[ "${1:-}" == "-C" && "${3:-}" == "fetch" ]]; then - printf '%s\n' "${7:-}" > "$2/.stub-fetch" - elif [[ "${1:-}" == "-C" && "${3:-}" == "checkout" ]]; then - cat "$2/.stub-fetch" > "$2/.stub-head" - elif [[ "${1:-}" == "-C" && "${3:-}" == "rev-parse" ]]; then - cat "$2/.stub-head" - fi -fi -exit 0 +{ printf '%s' "$name"; for arg in "$@"; do printf '|%s' "$arg"; done; printf '\n'; } >> "$CALL_LOG" +case "$name:${1:-}" in + pipx:--version) printf '%s\n' "${STUB_PIPX_VERSION:-0}" ;; + pnpm:--version) printf '%s\n' "${STUB_PNPM_VERSION:-0}" ;; + git:init) + target="${!#}"; mkdir -p "$target/.git"; printf '%s\n' unpinned-head > "$target/.stub-head" + ;; + git:-C) + case "${3:-}" in + fetch) + [[ "${STUB_FAIL_FETCH:-0}" != 1 ]] || exit 1 + printf '%s\n' "${7:-}" > "$2/.stub-fetch" + ;; + checkout) cp "$2/.stub-fetch" "$2/.stub-head" ;; + rev-parse) cat "$2/.stub-head" ;; + status) [[ ! -e "$2/.stub-dirty" ]] || printf '%s\n' '?? .npmrc' ;; + esac + ;; + nc:-z) + count=0; [[ ! -f "$STUB_NC_STATE" ]] || count="$(cat "$STUB_NC_STATE")" + printf '%s\n' "$((count + 1))" > "$STUB_NC_STATE" + (( count > 0 )) && exit 0 || exit 1 + ;; +esac +[[ "${STUB_FAIL_COMMAND:-}" != "$name" ]] STUB chmod +x "$STUB_BIN/command-stub" -for command_name in git node npm npx pipx pnpm sleep; do - ln -s command-stub "$STUB_BIN/$command_name" -done +for name in git node npm npx pipx pnpm sleep nc; do ln -s command-stub "$STUB_BIN/$name"; done cat > "$STUB_BIN/python3" <> "\$CALL_LOG" +if [[ "\${1:-}" == '-m' && "\${2:-}" == pip ]]; then [[ "\${STUB_FAIL_PIP_INSTALL:-0}" != 1 ]]; exit; fi +if [[ "\${1:-}" == '-m' && "\${2:-}" == pipx ]]; then exit 0; fi +if [[ "\${1:-}" == '-c' && "\${2:-}" == 'import pwn' ]]; then exit 1; fi +if [[ "\${1:-}" == '-' && "\${2:-}" == 23816 ]]; then exit 0; fi exec "$REAL_PYTHON" "\$@" STUB chmod +x "$STUB_BIN/python3" -manifest_value() { +json_value() { "$REAL_PYTHON" - "$MANIFEST" "$1" "$2" <<'PY' import json, pathlib, sys -manifest = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')) -capability = next(item for item in manifest['capabilities'] if item['name'] == sys.argv[2]) -value = capability.get(sys.argv[3], '') -print(value if isinstance(value, str) else json.dumps(value, separators=(',', ':'))) +d=json.loads(pathlib.Path(sys.argv[1]).read_text()) +if sys.argv[2] == 'dependency': v=d['bootstrapDependencies'][sys.argv[3]]['package'] +else: v=next(x for x in d['capabilities'] if x['name']==sys.argv[2])[sys.argv[3]] +print(v) PY } -run_bootstrap() { - env \ - PATH="$STUB_BIN:/usr/bin:/bin" \ - HOME="$SCRATCH/home" \ - CALL_LOG="$CALL_LOG" \ - REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" \ - CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \ - bash "$BOOTSTRAP" "$@" +run_generic() { + env PATH="$STUB_BIN:/usr/bin:/bin" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \ + STUB_PIPX_VERSION="${STUB_PIPX_VERSION:-}" STUB_PNPM_VERSION="${STUB_PNPM_VERSION:-}" \ + STUB_FAIL_PIP_INSTALL="${STUB_FAIL_PIP_INSTALL:-0}" STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" \ + REVERSE_SKILL_TOOLS_DIR="${TEST_TOOLS_ROOT:-$SCRATCH/tools}" \ + CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" bash "$BOOTSTRAP" "$@" +} +run_kali() { + rm -f "$SCRATCH/nc-count" + env PATH="$STUB_BIN:/opt/homebrew/bin:/usr/bin:/bin" HOME="$SCRATCH/home" \ + CALL_LOG="$CALL_LOG" STUB_NC_STATE="$SCRATCH/nc-count" STUB_PNPM_VERSION="${STUB_PNPM_VERSION:-}" \ + STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" bash "$KALI_BOOTSTRAP" "$@" +} +expect_line() { grep -Fqx "$1" "$CALL_LOG" || { echo "missing argv: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; } +expect_fragment() { grep -Fq "$1" "$CALL_LOG" || { echo "missing argv fragment: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; } +rejects_without_pnpm() { + local runner="$1"; shift + : > "$CALL_LOG"; set +e; "$runner" "$@" >/dev/null 2>&1; local rc=$?; set -e + [[ $rc -ne 0 ]] && ! grep -Eq '^pnpm\|(install|dev)' "$CALL_LOG" } -run_bootstrap_with_failing_pipx() { - env \ - PATH="$STUB_BIN:/usr/bin:/bin" \ - HOME="$SCRATCH/home" \ - CALL_LOG="$CALL_LOG" \ - STUB_FAIL_COMMAND=pipx \ - REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" \ - CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \ - bash "$BOOTSTRAP" "$@" -} +pipx_package=$(json_value dependency pipx) +pnpm_package=$(json_value dependency pnpm) +anything_repo=$(json_value anything-analyzer repoUrl) +anything_pin=$(json_value anything-analyzer pinnedCommit) -run_kali_bootstrap() { - env \ - PATH="$STUB_BIN:/opt/homebrew/bin:/usr/bin:/bin" \ - HOME="$SCRATCH/home" \ - CALL_LOG="$CALL_LOG" \ - bash "$KALI_BOOTSTRAP" "$@" -} - -failures=0 -check_log_line() { - if ! grep -Fqx "$1" "$CALL_LOG"; then - printf 'missing argv: %s\n' "$1" >&2 - failures=$((failures + 1)) - fi -} - -: > "$CALL_LOG" -run_bootstrap frida --skip-refresh >/dev/null -frida_package="$(manifest_value frida pipPackage)" -check_log_line "pipx|install|--force|$frida_package" - -: > "$CALL_LOG" -run_bootstrap idalib-mcp --skip-refresh >/dev/null -idalib_source="$(manifest_value idalib-mcp pipSource)" -check_log_line "pipx|install|--force|$idalib_source" - -: > "$CALL_LOG" -set +e -run_bootstrap_with_failing_pipx idalib-mcp --skip-refresh >/dev/null 2>&1 -idalib_fail_exit=$? -set -e -if [[ "$idalib_fail_exit" -eq 0 ]]; then - printf 'idalib-mcp reported success after its pinned install failed\n' >&2 - failures=$((failures + 1)) -fi -check_log_line "pipx|install|--force|$idalib_source" -if [[ "$(wc -l < "$CALL_LOG" | tr -d ' ')" -ne 1 ]]; then - printf 'idalib-mcp attempted an unpinned fallback after pinned install failure\n' >&2 - failures=$((failures + 1)) -fi - -: > "$CALL_LOG" -run_bootstrap agent-browser --skip-refresh >/dev/null -agent_package="$(manifest_value agent-browser npmPackage)" -check_log_line "npm|install|-g|$agent_package" - -: > "$CALL_LOG" -run_bootstrap seclists --skip-refresh >/dev/null -seclists_repo="$(manifest_value seclists repo)" -seclists_pin="$(manifest_value seclists pinnedCommit)" -seclists_dir="$SCRATCH/tools/SecLists" -check_log_line "git|-C|$seclists_dir|remote|add|origin|$seclists_repo" -check_log_line "git|-C|$seclists_dir|fetch|--depth|1|origin|$seclists_pin" - -: > "$CALL_LOG" -run_bootstrap proxycat --skip-refresh >/dev/null -proxycat_repo="$(manifest_value proxycat repo)" -proxycat_pin="$(manifest_value proxycat pinnedCommit)" -check_log_line "pipx|install|git+${proxycat_repo}@${proxycat_pin}" - -: > "$CALL_LOG" -run_bootstrap pwntools --skip-refresh >/dev/null -pwntools_package="$(manifest_value pwntools pipPackage)" -check_log_line "pipx|install|$pwntools_package" - -: > "$CALL_LOG" -run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null -anything_repo="$(manifest_value anything-analyzer repoUrl)" -anything_pin="$(manifest_value anything-analyzer pinnedCommit)" -anything_dir="$SCRATCH/tools/anything-analyzer" -if [[ -z "$anything_pin" ]]; then - printf 'anything-analyzer is missing pinnedCommit in bootstrap-manifest.json\n' >&2 - failures=$((failures + 1)) -else - check_log_line "git|init|--quiet|$anything_dir" - check_log_line "git|-C|$anything_dir|remote|add|origin|$anything_repo" - check_log_line "git|-C|$anything_dir|fetch|--depth|1|origin|$anything_pin" - check_log_line "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD" - check_log_line "git|-C|$anything_dir|rev-parse|HEAD" -fi -check_log_line 'pnpm|install' -check_log_line 'pnpm|dev' - -if [[ -n "$anything_pin" ]]; then - checkout_line="$(grep -nF "git|-C|$anything_dir|checkout|--quiet|--detach|FETCH_HEAD" "$CALL_LOG" | cut -d: -f1 | head -n1)" - install_line="$(grep -nF 'pnpm|install' "$CALL_LOG" | cut -d: -f1 | head -n1)" - if [[ -z "$checkout_line" || -z "$install_line" || "$checkout_line" -ge "$install_line" ]]; then - printf 'anything-analyzer dependencies ran before the pinned checkout\n' >&2 - failures=$((failures + 1)) - fi -fi - -: > "$CALL_LOG" -mkdir -p "$anything_dir/.git" -printf '%s\n' 'different-commit' > "$anything_dir/.stub-head" -set +e -run_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 -mismatch_exit=$? -set -e -if [[ "$mismatch_exit" -eq 0 ]]; then - printf 'anything-analyzer accepted an existing checkout at a different commit\n' >&2 - failures=$((failures + 1)) -fi -if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then - printf 'anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2 - failures=$((failures + 1)) -fi - -if (( BASH_VERSINFO[0] >= 4 )); then +# Table: each generic package-manager sink receives its canonical manifest value. +while IFS='|' read -r capability field expected; do : > "$CALL_LOG" + STUB_PIPX_VERSION=1.16.5 run_generic "$capability" --skip-refresh >/dev/null + expect_line "$expected" +done < "$CALL_LOG" +STUB_FAIL_PIP_INSTALL=1 run_generic frida --skip-refresh >/dev/null 2>&1 && exit 1 || true +expect_line "python3|-m|pip|install|--user|--upgrade|$pipx_package" +[[ $(grep -c '|pip|install|' "$CALL_LOG") -eq 1 ]] +! grep -Eq '^pipx\|(install|upgrade)' "$CALL_LOG" + +# Generic Anything Analyzer: staged checkout, pinned pnpm, frozen install, clean recheck, then dev. +: > "$CALL_LOG" +STUB_PIPX_VERSION=1.16.5 STUB_PNPM_VERSION=0 run_generic anything-analyzer --start-services --skip-refresh >/dev/null +anything_dir="$SCRATCH/tools/anything-analyzer" +expect_line "npm|install|-g|$pnpm_package" +expect_line 'pnpm|install|--frozen-lockfile' +expect_line 'pnpm|dev' +expect_fragment "remote|add|origin|$anything_repo" +expect_fragment "fetch|--depth|1|origin|$anything_pin" +[[ -d "$anything_dir/.git" ]] +[[ $(grep -c '|status|--porcelain|--untracked-files=all' "$CALL_LOG") -ge 2 ]] + +# Dirty sources never reach install/dev. +touch "$anything_dir/.stub-dirty" +rejects_without_pnpm run_generic anything-analyzer --start-services --skip-refresh +rm "$anything_dir/.stub-dirty" + +# Failed fetch leaves no final checkout or staging poison; a retry can succeed. +retry_root="$SCRATCH/retry-tools" +TEST_TOOLS_ROOT="$retry_root" STUB_FAIL_FETCH=1 rejects_without_pnpm run_generic anything-analyzer --start-services --skip-refresh +[[ ! -e "$retry_root/anything-analyzer" ]] +[[ -z "$(find "$retry_root" -maxdepth 1 -name '.reverse-bootstrap-*' -print -quit)" ]] +: > "$CALL_LOG" +TEST_TOOLS_ROOT="$retry_root" STUB_PNPM_VERSION=10.24.0 run_generic anything-analyzer --start-services --skip-refresh >/dev/null +[[ -d "$retry_root/anything-analyzer/.git" ]] + +# Kali exercises the same source-before-execution boundary where associative arrays are supported. +if (( BASH_VERSINFO[0] >= 4 )); then kali_dir="$SCRATCH/home/tools/anything-analyzer" rm -rf "$kali_dir" - set +e - run_kali_bootstrap anything-analyzer --start-services --skip-refresh >"$SCRATCH/kali-bootstrap.out" 2>&1 - set -e - check_log_line "git|init|-q|$kali_dir" - check_log_line "git|-C|$kali_dir|remote|add|origin|$anything_repo" - check_log_line "git|-C|$kali_dir|fetch|--depth|1|origin|$anything_pin" - check_log_line "git|-C|$kali_dir|checkout|-q|--detach|FETCH_HEAD" - check_log_line 'pnpm|install' - check_log_line 'pnpm|dev' - : > "$CALL_LOG" - mkdir -p "$kali_dir/.git" - printf '%s\n' 'different-commit' > "$kali_dir/.stub-head" set +e - run_kali_bootstrap anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 - kali_mismatch_exit=$? + STUB_PNPM_VERSION=0 run_kali anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 set -e - if [[ "$kali_mismatch_exit" -eq 0 ]]; then - printf 'Kali anything-analyzer accepted an existing checkout at a different commit\n' >&2 - failures=$((failures + 1)) - fi - if grep -Eq '^pnpm\|(install|dev)$' "$CALL_LOG"; then - printf 'Kali anything-analyzer ran dependencies from an unpinned existing checkout\n' >&2 - failures=$((failures + 1)) - fi + expect_line "npm|install|-g|$pnpm_package" + expect_line 'pnpm|install|--frozen-lockfile' + [[ $(grep -c '|status|--porcelain|--untracked-files=all' "$CALL_LOG") -ge 2 ]] + touch "$kali_dir/.stub-dirty" + rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh + + rm -rf "$kali_dir" + : > "$CALL_LOG" + STUB_FAIL_FETCH=1 rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh + [[ ! -e "$kali_dir" ]] + [[ -z "$(find "${kali_dir%/*}" -maxdepth 1 -name '.reverse-bootstrap-*' -print -quit)" ]] + set +e + STUB_PNPM_VERSION=10.24.0 run_kali anything-analyzer --start-services --skip-refresh >/dev/null 2>&1 + set -e + [[ -d "$kali_dir/.git" ]] + expect_line 'pnpm|install|--frozen-lockfile' fi -if [[ "$failures" -ne 0 ]]; then - if [[ -f "$SCRATCH/kali-bootstrap.out" ]]; then cat "$SCRATCH/kali-bootstrap.out" >&2; fi - printf '%s\n' 'captured argv:' >&2 - cat "$CALL_LOG" >&2 - exit 1 -fi - -printf '%s\n' 'bootstrap manifest source regression passed' +echo 'bootstrap manifest source regression passed' diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index 8788539..607a3ab 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -395,6 +395,17 @@ foreach ($mf in @($skillsManifest, $kaliManifest)) { if (-not (Test-Path -LiteralPath $mf)) { continue } $mn = Split-Path $mf -Leaf $mc = Get-Content -LiteralPath $mf -Raw -Encoding UTF8 | ConvertFrom-Json + foreach ($dependencyProperty in @($mc.bootstrapDependencies.PSObject.Properties)) { + $dependency = $dependencyProperty.Value + $expectedSuffix = '(?:==|@)' + [regex]::Escape([string]$dependency.version) + '$' + if ([string]::IsNullOrWhiteSpace([string]$dependency.package) -or + [string]::IsNullOrWhiteSpace([string]$dependency.version) -or + [string]$dependency.package -notmatch $expectedSuffix) { + Bad "unpinned bootstrap dependency: $($dependencyProperty.Name) in $mn" + } else { + Ok "pinned bootstrap dependency $($dependencyProperty.Name) in $mn" + } + } foreach ($cap in $mc.capabilities) { if (-not $cap.canAutoInstall) { continue } $hasPin = ($cap.pinnedVersion -or $cap.pinnedCommit -or $cap.pinPolicy) From 87e00b80db30cf6aef0f8c0d4e01f51943b1cc0e Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:22:32 +0530 Subject: [PATCH 03/10] fix(bootstrap): fail closed before manifest installs --- .github/workflows/ci.yml | 4 + skills/scripts/bootstrap-reverse.ps1 | 149 +---------------- skills/scripts/bootstrap-reverse.sh | 72 +++++---- skills/scripts/lib/BootstrapSupplyChain.ps1 | 151 ++++++++++++++++++ skills/scripts/test-bootstrap-manifest.sh | 46 ++++++ .../scripts/test-bootstrap-supply-chain.ps1 | 103 ++++++++++++ 6 files changed, 348 insertions(+), 177 deletions(-) create mode 100644 skills/scripts/lib/BootstrapSupplyChain.ps1 create mode 100644 skills/scripts/test-bootstrap-supply-chain.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cbfa17..e4bebe2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,10 @@ jobs: shell: pwsh run: ./skills/scripts/verify-routing-coherence.ps1 + - name: Bootstrap supply-chain regression + shell: pwsh + run: ./skills/scripts/test-bootstrap-supply-chain.ps1 + - name: Smoke (verify + parse + quick route) shell: pwsh run: ./skills/scripts/smoke.ps1 diff --git a/skills/scripts/bootstrap-reverse.ps1 b/skills/scripts/bootstrap-reverse.ps1 index 212b450..c764186 100644 --- a/skills/scripts/bootstrap-reverse.ps1 +++ b/skills/scripts/bootstrap-reverse.ps1 @@ -23,6 +23,7 @@ $ErrorActionPreference = 'Stop' $OutputEncoding = [System.Text.UTF8Encoding]::new($false) . (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1') +. (Join-Path $PSScriptRoot 'lib\BootstrapSupplyChain.ps1') function Get-BootstrapDependency { param([Parameter(Mandatory = $true)][string]$Name) @@ -164,29 +165,6 @@ function Ensure-JavaRuntime { } } -function Ensure-Pnpm { - Ensure-NodeRuntime - $dependency = Get-BootstrapDependency -Name 'pnpm' - $pnpm = Get-NodeCommandPath -Name 'pnpm' - $currentVersion = '' - if ($pnpm) { - $versionLine = & $pnpm --version 2>$null | Select-Object -First 1 - if ($LASTEXITCODE -eq 0 -and $null -ne $versionLine) { - $currentVersion = ([string]$versionLine).Trim() - } - } - if ($currentVersion -ne [string]$dependency.version) { - $npm = Get-NodeCommandPath -Name 'npm' - if ([string]::IsNullOrWhiteSpace($npm)) { - throw 'npm is not available after Node.js installation.' - } - & $npm install -g ([string]$dependency.package) - if ($LASTEXITCODE -ne 0) { - throw "Failed to install pinned pnpm dependency $($dependency.package)." - } - } -} - function Get-AnythingAnalyzerUserDataPaths { $candidates = @( (Join-Path $env:APPDATA 'anything-analyzer'), @@ -801,54 +779,9 @@ if (Test-ReverseIsWindows) { if ([string]::IsNullOrWhiteSpace($pnpm)) { throw 'pnpm is not available after installation.' } - - $workspacePath = Join-Path $repoDir 'pnpm-workspace.yaml' - $workspaceExisted = Test-Path -LiteralPath $workspacePath -PathType Leaf - $workspaceBytes = if ($workspaceExisted) { [IO.File]::ReadAllBytes($workspacePath) } else { $null } - - Push-Location $repoDir - try { - Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir - - if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) { - $nodeModules = Join-Path $repoDir 'node_modules' - if (Test-Path -LiteralPath $nodeModules) { - Remove-Item -LiteralPath $nodeModules -Recurse -Force - } - } - - & $pnpm install --frozen-lockfile - if ($LASTEXITCODE -ne 0) { - if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) { - throw "pnpm install failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError" - } - throw 'pnpm install failed for anything-analyzer.' - } - - & $pnpm rebuild electron esbuild better-sqlite3 - if ($LASTEXITCODE -ne 0) { - if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) { - throw "pnpm rebuild failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError" - } - throw 'pnpm rebuild failed for anything-analyzer.' - } - - if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) { - throw 'Electron is still not healthy after reinstall/rebuild.' - } - } - finally { - Pop-Location - if ($workspaceExisted) { - [IO.File]::WriteAllBytes($workspacePath, $workspaceBytes) - } - elseif (Test-Path -LiteralPath $workspacePath) { - Remove-Item -LiteralPath $workspacePath -Force - } - } - $git = Get-FirstCommandPath -Names @('git') - Assert-GitCheckoutState -GitPath $git -CheckoutPath $repoDir -PinnedCommit ([string]$Definition.pinnedCommit) + Invoke-AnythingAnalyzerPinnedInstall -RepoDir $repoDir -PnpmPath $pnpm -GitPath $git ` + -PinnedCommit ([string]$Definition.pinnedCommit) -VsBuildToolsError $vsBuildToolsError $stdoutLog = Join-Path $repoDir 'anything-analyzer-dev.log' $stderrLog = Join-Path $repoDir 'anything-analyzer-dev.err.log' @@ -889,82 +822,6 @@ function Ensure-AndroidPlatformTools { return (Resolve-ReverseToolSpec -Name 'adb') } -function Assert-GitCheckoutState { - param( - [Parameter(Mandatory = $true)][string]$GitPath, - [Parameter(Mandatory = $true)][string]$CheckoutPath, - [Parameter(Mandatory = $true)][string]$PinnedCommit - ) - - $resolvedLine = & $GitPath -C $CheckoutPath rev-parse HEAD 2>$null | Select-Object -First 1 - $resolvedCommit = if ($null -eq $resolvedLine) { '' } else { ([string]$resolvedLine).Trim() } - if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $PinnedCommit) { - throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)" - } - $status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "Cannot inspect checkout state: $CheckoutPath" - } - if ($status.Count -gt 0) { - throw "Checkout has local changes; refusing to execute it: $CheckoutPath" - } -} - -function Ensure-GitCloneInstall { - param( - [Parameter(Mandatory = $true)]$Definition, - [Parameter(Mandatory = $true)][string]$TargetPath - ) - - $pinnedCommit = if ($Definition.PSObject.Properties['pinnedCommit']) { [string]$Definition.pinnedCommit } else { '' } - $git = Get-FirstCommandPath -Names @('git') - if ([string]::IsNullOrWhiteSpace($git)) { - throw "Cannot clone $($Definition.repo) because git is not available." - } - - if ((Test-Path -LiteralPath $TargetPath -PathType Container) -and (Test-Path -LiteralPath (Join-Path $TargetPath '.git'))) { - if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { - throw "Git capability $($Definition.repo) must define pinnedCommit before an existing checkout can be used." - } - Assert-GitCheckoutState -GitPath $git -CheckoutPath $TargetPath -PinnedCommit $pinnedCommit - return $true - } - - if (Test-Path -LiteralPath $TargetPath) { - throw "Install path exists but is not a git checkout: $TargetPath" - } - if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { - throw "Git capability $($Definition.repo) must define pinnedCommit." - } - - $parent = Split-Path -Path $TargetPath -Parent - Ensure-DownloadDirectory -Path $parent - $stagePath = Join-Path $parent ('.reverse-bootstrap-{0}' -f [Guid]::NewGuid().ToString('N')) - New-Item -ItemType Directory -Path $stagePath | Out-Null - try { - & $git init --quiet $stagePath - if ($LASTEXITCODE -ne 0) { throw 'git init failed' } - & $git -C $stagePath remote add origin $Definition.repo - if ($LASTEXITCODE -ne 0) { throw 'git remote add failed' } - & $git -C $stagePath fetch --depth 1 origin $pinnedCommit - if ($LASTEXITCODE -ne 0) { throw 'git fetch failed' } - & $git -C $stagePath checkout --quiet --detach FETCH_HEAD - if ($LASTEXITCODE -ne 0) { throw 'git checkout failed' } - Assert-GitCheckoutState -GitPath $git -CheckoutPath $stagePath -PinnedCommit $pinnedCommit - Move-Item -LiteralPath $stagePath -Destination $TargetPath - if ((Test-Path -LiteralPath $stagePath) -or -not (Test-Path -LiteralPath (Join-Path $TargetPath '.git') -PathType Container)) { - throw "Failed to promote staged checkout to $TargetPath" - } - } - finally { - if (Test-Path -LiteralPath $stagePath) { - Remove-Item -LiteralPath $stagePath -Recurse -Force - } - } - - return $true -} - function Ensure-Capability { param([Parameter(Mandatory = $true)][string]$Name) diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index a8016fa..c7f63ca 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -224,13 +224,7 @@ install_brew_cask() { } ensure_python_runtime() { - if ! has_cmd python3; then - case "$PLATFORM" in - macos) install_brew python ;; - linux) install_apt python3 ;; - *) log_err "Install Python 3 manually. See $(platform_doc)"; return 1 ;; - esac - fi + ensure_python_interpreter || return 1 local pipx_package pipx_version current_version pipx_package=$(manifest_dependency pipx package) || return 1 pipx_version=$(manifest_dependency pipx version) || return 1 @@ -245,6 +239,17 @@ ensure_python_runtime() { export PATH="$HOME/.local/bin:$PATH" } +ensure_python_interpreter() { + if ! has_cmd python3; then + case "$PLATFORM" in + macos) install_brew python ;; + linux) install_apt python3 ;; + *) log_err "Install Python 3 manually. See $(platform_doc)"; return 1 ;; + esac + fi + has_cmd python3 || { log_err "Python 3 installation completed without a usable python3 command."; return 1; } +} + ensure_node_runtime() { if has_cmd node && has_cmd npm && has_cmd npx; then return 0; fi case "$PLATFORM" in @@ -567,10 +572,10 @@ ensure_jadx() { if has_cmd jadx; then log_ok "jadx ready: $(cmd_path jadx)"; return 0; fi ensure_java_runtime local repo re tag sha - repo=$(manifest_field jadx repo) - re=$(manifest_field jadx assetRegex) - tag=$(manifest_field jadx releaseTag) - sha=$(manifest_field jadx assetSha256) + repo=$(manifest_field jadx repo) || return 1 + re=$(manifest_field jadx assetRegex) || return 1 + tag=$(manifest_field jadx releaseTag) || return 1 + sha=$(manifest_field jadx assetSha256) || return 1 case "$PLATFORM" in macos) install_brew jadx || install_github_release "$repo" "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; linux) install_github_release "$repo" "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;; @@ -587,10 +592,10 @@ ensure_apktool() { ensure_dir "$TOOLS_ROOT/apktool" local meta url digest jar wrapper local repo tag sha re - repo=$(manifest_field apktool repo) - tag=$(manifest_field apktool releaseTag) - sha=$(manifest_field apktool assetSha256) - re=$(manifest_field apktool assetRegex) + repo=$(manifest_field apktool repo) || return 1 + tag=$(manifest_field apktool releaseTag) || return 1 + sha=$(manifest_field apktool assetSha256) || return 1 + re=$(manifest_field apktool assetRegex) || return 1 meta=$(latest_github_asset_meta "$repo" "$re" "$tag") url=$(printf '%s' "$meta" | cut -f1) digest=$(printf '%s' "$meta" | cut -f2) @@ -609,7 +614,7 @@ ensure_frida_tools() { ensure_python_runtime || return 1 if has_cmd frida && has_cmd frida-ps; then log_ok "frida-tools ready"; return 0; fi local package - package=$(manifest_field frida pipPackage) + package=$(manifest_field frida pipPackage) || return 1 pipx install --force "$package" || return 1 export PATH="$HOME/.local/bin:$PATH" } @@ -618,7 +623,7 @@ ensure_idalib_mcp() { ensure_python_runtime || return 1 if has_cmd ida-pro-mcp; then log_ok "ida-pro-mcp ready: $(cmd_path ida-pro-mcp)"; return 0; fi local source - source=$(manifest_field idalib-mcp pipSource) + source=$(manifest_field idalib-mcp pipSource) || return 1 pipx install --force "$source" || return 1 export PATH="$HOME/.local/bin:$PATH" log_warn "Post-install: run 'ida-pro-mcp --install', choose Streamable HTTP + Global, then restart IDA Pro." @@ -627,7 +632,7 @@ ensure_idalib_mcp() { ensure_jshookmcp() { ensure_node_runtime || return 1 local package - package=$(manifest_field jshookmcp npmPackage) + package=$(manifest_field jshookmcp npmPackage) || return 1 write_mcp_server "jshook" "$(python3 - "$package" <<'PY' import json, sys print(json.dumps({'command':'npx','args':['-y',sys.argv[1]],'env':{'JSHOOK_BASE_PROFILE':'search'}})) @@ -638,7 +643,7 @@ PY ensure_reqable_mcp() { ensure_node_runtime || return 1 local package - package=$(manifest_field reqable-mcp npmPackage) + package=$(manifest_field reqable-mcp npmPackage) || return 1 write_mcp_server "reqable-mcp" "$(python3 - "$package" <<'PY' import json, sys print(json.dumps({'command':'npx','args':['-y',sys.argv[1]]})) @@ -650,8 +655,8 @@ PY ensure_anything_analyzer() { local dir="$TOOLS_ROOT/anything-analyzer" local repo commit - repo=$(manifest_field anything-analyzer repoUrl) - commit=$(manifest_field anything-analyzer pinnedCommit) + repo=$(manifest_field anything-analyzer repoUrl) || return 1 + commit=$(manifest_field anything-analyzer pinnedCommit) || return 1 if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac fi @@ -719,7 +724,7 @@ ensure_agent_browser() { ensure_node_runtime || return 1 if has_cmd agent-browser; then log_ok "agent-browser ready"; return 0; fi local package - package=$(manifest_field agent-browser npmPackage) + package=$(manifest_field agent-browser npmPackage) || return 1 npm install -g "$package" || return 1 if has_cmd npx; then npx playwright install chromium || true; fi local setup="$SKILL_ROOT/browser-automation/scripts/setup.sh" @@ -729,8 +734,8 @@ ensure_agent_browser() { ensure_ghidra_mcp() { ensure_java_runtime || return 1 local repo regex - repo=$(manifest_field ghidra-mcp repo) - regex=$(manifest_field ghidra-mcp assetRegex) + repo=$(manifest_field ghidra-mcp repo) || return 1 + regex=$(manifest_field ghidra-mcp assetRegex) || return 1 case "$PLATFORM" in macos) if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then @@ -752,8 +757,8 @@ ensure_seclists() { if [[ -d /usr/share/seclists ]]; then log_ok "SecLists ready"; return 0; fi if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac; fi local repo commit - repo=$(manifest_field seclists repo) - commit=$(manifest_field seclists pinnedCommit) + repo=$(manifest_field seclists repo) || return 1 + commit=$(manifest_field seclists pinnedCommit) || return 1 install_git_commit "$repo" "$commit" "$dir" || return 1 } @@ -761,8 +766,8 @@ ensure_proxycat() { ensure_python_runtime || return 1 if has_cmd proxycat; then log_ok "proxycat ready"; return 0; fi local repo commit - repo=$(manifest_field proxycat repo) - commit=$(manifest_field proxycat pinnedCommit) + repo=$(manifest_field proxycat repo) || return 1 + commit=$(manifest_field proxycat pinnedCommit) || return 1 pipx install "git+${repo}@${commit}" || { manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'." LAST_CAPABILITY_MANUAL=true @@ -810,8 +815,8 @@ ensure_pentestswarm() { case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac fi local go_package docker_image - go_package=$(manifest_field pentestswarm goPackage) - docker_image=$(manifest_field pentestswarm dockerImage) + go_package=$(manifest_field pentestswarm goPackage) || return 1 + docker_image=$(manifest_field pentestswarm dockerImage) || return 1 if go install "$go_package"; then local go_bin go_bin="$(go env GOBIN 2>/dev/null || true)" @@ -857,7 +862,7 @@ ensure_pwntools() { ensure_python_runtime || return 1 if python3 -c "import pwn" 2>/dev/null; then log_ok "pwntools ready"; return 0; fi local package - package=$(manifest_field pwntools pipPackage) + package=$(manifest_field pwntools pipPackage) || return 1 pipx install "$package" || python3 -m pip install --user "$package" || return 1 } @@ -937,6 +942,11 @@ done < <(expand_capabilities "${CAPABILITIES[@]}") log_info "platform=$PLATFORM doc=$(platform_doc) tools_root=$TOOLS_ROOT" +if ! ensure_python_interpreter; then + log_err "Python 3 is required to read bootstrap-manifest.json; no capability was executed." + exit 1 +fi + for cap in "${EXPANDED[@]}"; do log_info "ensure $cap" LAST_CAPABILITY_MANUAL=false diff --git a/skills/scripts/lib/BootstrapSupplyChain.ps1 b/skills/scripts/lib/BootstrapSupplyChain.ps1 new file mode 100644 index 0000000..6e6c4c8 --- /dev/null +++ b/skills/scripts/lib/BootstrapSupplyChain.ps1 @@ -0,0 +1,151 @@ +function Ensure-Pnpm { + Ensure-NodeRuntime + $dependency = Get-BootstrapDependency -Name 'pnpm' + $pnpm = Get-NodeCommandPath -Name 'pnpm' + $currentVersion = '' + if ($pnpm) { + $versionLine = & $pnpm --version 2>$null | Select-Object -First 1 + if ($LASTEXITCODE -eq 0 -and $null -ne $versionLine) { + $currentVersion = ([string]$versionLine).Trim() + } + } + if ($currentVersion -ne [string]$dependency.version) { + $npm = Get-NodeCommandPath -Name 'npm' + if ([string]::IsNullOrWhiteSpace($npm)) { + throw 'npm is not available after Node.js installation.' + } + & $npm install -g ([string]$dependency.package) + if ($LASTEXITCODE -ne 0) { + throw "Failed to install pinned pnpm dependency $($dependency.package)." + } + } +} + +function Assert-GitCheckoutState { + param( + [Parameter(Mandatory = $true)][string]$GitPath, + [Parameter(Mandatory = $true)][string]$CheckoutPath, + [Parameter(Mandatory = $true)][string]$PinnedCommit + ) + + $resolvedLine = & $GitPath -C $CheckoutPath rev-parse HEAD 2>$null | Select-Object -First 1 + $resolvedCommit = if ($null -eq $resolvedLine) { '' } else { ([string]$resolvedLine).Trim() } + if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $PinnedCommit) { + throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)" + } + $status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "Cannot inspect checkout state: $CheckoutPath" + } + if ($status.Count -gt 0) { + throw "Checkout has local changes; refusing to execute it: $CheckoutPath" + } +} + +function Move-BootstrapDirectory { + param( + [Parameter(Mandatory = $true)][string]$Source, + [Parameter(Mandatory = $true)][string]$Destination + ) + [IO.Directory]::Move($Source, $Destination) +} + +function Ensure-GitCloneInstall { + param( + [Parameter(Mandatory = $true)]$Definition, + [Parameter(Mandatory = $true)][string]$TargetPath + ) + + $git = Get-FirstCommandPath -Names @('git') + if ([string]::IsNullOrWhiteSpace($git)) { + throw 'git is required for git-clone bootstrap definitions.' + } + + $pinnedCommit = if ($Definition.PSObject.Properties['pinnedCommit']) { [string]$Definition.pinnedCommit } else { '' } + if ([string]::IsNullOrWhiteSpace($pinnedCommit)) { + throw "Git capability $($Definition.repo) must define pinnedCommit." + } + + if ((Test-Path -LiteralPath $TargetPath -PathType Container) -and (Test-Path -LiteralPath (Join-Path $TargetPath '.git'))) { + Assert-GitCheckoutState -GitPath $git -CheckoutPath $TargetPath -PinnedCommit $pinnedCommit + return $true + } + if (Test-Path -LiteralPath $TargetPath) { + throw "Install path exists but is not a git checkout: $TargetPath" + } + + $parent = Split-Path -Path $TargetPath -Parent + Ensure-DownloadDirectory -Path $parent + $stagePath = Join-Path $parent ('.reverse-bootstrap-{0}' -f [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $stagePath | Out-Null + try { + & $git init --quiet $stagePath + if ($LASTEXITCODE -ne 0) { throw 'git init failed' } + & $git -C $stagePath remote add origin $Definition.repo + if ($LASTEXITCODE -ne 0) { throw 'git remote add failed' } + & $git -C $stagePath fetch --depth 1 origin $pinnedCommit + if ($LASTEXITCODE -ne 0) { throw 'git fetch failed' } + & $git -C $stagePath checkout --quiet --detach FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw 'git checkout failed' } + Assert-GitCheckoutState -GitPath $git -CheckoutPath $stagePath -PinnedCommit $pinnedCommit + Move-BootstrapDirectory -Source $stagePath -Destination $TargetPath + if (-not (Test-Path -LiteralPath (Join-Path $TargetPath '.git') -PathType Container)) { + throw "Failed to promote staged checkout to $TargetPath" + } + } + finally { + if (Test-Path -LiteralPath $stagePath) { + Remove-Item -LiteralPath $stagePath -Recurse -Force + } + } + + return $true +} + +function Invoke-AnythingAnalyzerPinnedInstall { + param( + [Parameter(Mandatory = $true)][string]$RepoDir, + [Parameter(Mandatory = $true)][string]$PnpmPath, + [Parameter(Mandatory = $true)][string]$GitPath, + [Parameter(Mandatory = $true)][string]$PinnedCommit, + [string]$VsBuildToolsError = '' + ) + + $workspacePath = Join-Path $RepoDir 'pnpm-workspace.yaml' + $workspaceExisted = Test-Path -LiteralPath $workspacePath -PathType Leaf + $workspaceBytes = if ($workspaceExisted) { [IO.File]::ReadAllBytes($workspacePath) } else { $null } + + Push-Location $RepoDir + try { + Approve-AnythingAnalyzerBuildScripts -RepoDir $RepoDir + if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $RepoDir -PnpmPath $PnpmPath)) { + $nodeModules = Join-Path $RepoDir 'node_modules' + Remove-Item -LiteralPath $nodeModules -Recurse -Force -ErrorAction SilentlyContinue + } + + & $PnpmPath install --frozen-lockfile + if ($LASTEXITCODE -ne 0) { + if ($VsBuildToolsError) { throw "pnpm install failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $VsBuildToolsError" } + throw 'pnpm install failed for anything-analyzer.' + } + & $PnpmPath rebuild electron esbuild better-sqlite3 + if ($LASTEXITCODE -ne 0) { + if ($VsBuildToolsError) { throw "pnpm rebuild failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $VsBuildToolsError" } + throw 'pnpm rebuild failed for anything-analyzer.' + } + if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $RepoDir -PnpmPath $PnpmPath)) { + throw 'anything-analyzer Electron dependency is still unhealthy after pnpm rebuild.' + } + } + finally { + Pop-Location + if ($workspaceExisted) { + [IO.File]::WriteAllBytes($workspacePath, $workspaceBytes) + } + elseif (Test-Path -LiteralPath $workspacePath) { + Remove-Item -LiteralPath $workspacePath -Force + } + } + + Assert-GitCheckoutState -GitPath $GitPath -CheckoutPath $RepoDir -PinnedCommit $PinnedCommit +} diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh index e5a0bed..7909484 100644 --- a/skills/scripts/test-bootstrap-manifest.sh +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -19,6 +19,11 @@ name="$(basename "$0")" case "$name:${1:-}" in pipx:--version) printf '%s\n' "${STUB_PIPX_VERSION:-0}" ;; pnpm:--version) printf '%s\n' "${STUB_PNPM_VERSION:-0}" ;; + brew:install) + if [[ "${2:-}" == python ]]; then + ln -sf "$STUB_PYTHON_SOURCE" "$STUB_ACTIVE_BIN/python3" + fi + ;; git:init) target="${!#}"; mkdir -p "$target/.git"; printf '%s\n' unpinned-head > "$target/.stub-head" ;; @@ -43,6 +48,7 @@ esac STUB chmod +x "$STUB_BIN/command-stub" for name in git node npm npx pipx pnpm sleep nc; do ln -s command-stub "$STUB_BIN/$name"; done +ln -s command-stub "$STUB_BIN/brew" cat > "$STUB_BIN/python3" < "$CALL_LOG" +env PATH="$NO_PYTHON_BIN" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \ + STUB_ACTIVE_BIN="$NO_PYTHON_BIN" STUB_PYTHON_SOURCE="$STUB_BIN/python3" \ + REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \ + bash "$PARSER_FIXTURE/bootstrap-reverse.sh" agent-browser --skip-refresh >/dev/null +expect_line 'brew|install|python' +expect_line "npm|install|-g|$(json_value agent-browser npmPackage)" +! grep -Fq '|pip|install|' "$CALL_LOG" + +# A required empty manifest field fails before any package-manager sink. +BROKEN_DIR="$SCRATCH/broken-bootstrap" +mkdir -p "$BROKEN_DIR" +cp "$BOOTSTRAP" "$BROKEN_DIR/bootstrap-reverse.sh" +"$REAL_PYTHON" - "$MANIFEST" "$BROKEN_DIR/bootstrap-manifest.json" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +next(x for x in data['capabilities'] if x['name'] == 'agent-browser')['npmPackage'] = '' +pathlib.Path(sys.argv[2]).write_text(json.dumps(data)) +PY +: > "$CALL_LOG" +set +e +env PATH="$STUB_BIN:/usr/bin:/bin" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \ + STUB_ACTIVE_BIN="$STUB_BIN" STUB_PYTHON_SOURCE="$STUB_BIN/python3" \ + REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \ + bash "$BROKEN_DIR/bootstrap-reverse.sh" agent-browser --skip-refresh >/dev/null 2>&1 +broken_rc=$? +set -e +[[ $broken_rc -ne 0 ]] +! grep -Eq '^npm\|install\|-g(\||$)' "$CALL_LOG" + # Table: each generic package-manager sink receives its canonical manifest value. while IFS='|' read -r capability field expected; do : > "$CALL_LOG" diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 new file mode 100644 index 0000000..315e0df --- /dev/null +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -0,0 +1,103 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$scratch = Join-Path ([IO.Path]::GetTempPath()) ('reverse-bootstrap-ps-' + [Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $scratch | Out-Null + +function Ensure-DownloadDirectory { param([string]$Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null } +function Get-FirstCommandPath { param([string[]]$Names) return (Get-Command $Names[0]).Source } +function Ensure-NodeRuntime {} +function Get-NodeCommandPath { param([string]$Name) $command = Get-Command $Name -ErrorAction SilentlyContinue; if ($command) { return $command.Source } } +function Get-BootstrapDependency { return [pscustomobject]@{ package = 'pnpm@10.24.0'; version = '10.24.0' } } +function Approve-AnythingAnalyzerBuildScripts { param([string]$RepoDir) Set-Content (Join-Path $RepoDir 'pnpm-workspace.yaml') 'generated' } +function Test-AnythingAnalyzerElectronHealthy { return $true } + +. (Join-Path $PSScriptRoot 'lib/BootstrapSupplyChain.ps1') + +function Assert-True { param([bool]$Condition, [string]$Message) if (-not $Condition) { throw $Message } } +function Invoke-Git { param([string[]]$Arguments) & git @Arguments; if ($LASTEXITCODE -ne 0) { throw "git failed: $Arguments" } } + +try { + $source = Join-Path $scratch 'source' + New-Item -ItemType Directory -Path $source | Out-Null + Invoke-Git -Arguments @('-C', $source, 'init', '--quiet') + Invoke-Git -Arguments @('-C', $source, 'config', 'user.email', 'test@example.invalid') + Invoke-Git -Arguments @('-C', $source, 'config', 'user.name', 'test') + Set-Content (Join-Path $source 'package.json') '{}' + Invoke-Git -Arguments @('-C', $source, 'add', 'package.json') + Invoke-Git -Arguments @('-C', $source, 'commit', '--quiet', '-m', 'fixture') + $pin = (& git -C $source rev-parse HEAD).Trim() + $definition = [pscustomobject]@{ repo = $source; pinnedCommit = $pin } + + $target = Join-Path $scratch 'installed' + Ensure-GitCloneInstall -Definition $definition -TargetPath $target | Out-Null + Assert-True ((& git -C $target rev-parse HEAD).Trim() -eq $pin) 'pinned checkout was not promoted' + Set-Content (Join-Path $target 'package.json') '{"dirty":true}' + try { Ensure-GitCloneInstall -Definition $definition -TargetPath $target | Out-Null; throw 'dirty checkout accepted' } catch { Assert-True ($_.Exception.Message -match 'local changes') 'dirty rejection reason changed' } + + $failedTarget = Join-Path $scratch 'failed' + $badDefinition = [pscustomobject]@{ repo = (Join-Path $scratch 'missing'); pinnedCommit = $pin } + try { Ensure-GitCloneInstall -Definition $badDefinition -TargetPath $failedTarget | Out-Null; throw 'failed fetch accepted' } catch {} + Assert-True (-not (Test-Path $failedTarget)) 'failed fetch poisoned final path' + Assert-True (@(Get-ChildItem $scratch -Filter '.reverse-bootstrap-*').Count -eq 0) 'failed fetch left staging path' + + $raceTarget = Join-Path $scratch 'race' + $raceStage = Join-Path $scratch '.reverse-bootstrap-race' + New-Item -ItemType Directory -Path $raceTarget, $raceStage | Out-Null + Set-Content (Join-Path $raceTarget 'owner.txt') owner + $raceRejected = $false + try { Move-BootstrapDirectory -Source $raceStage -Destination $raceTarget } catch { $raceRejected = $true } + Assert-True $raceRejected 'promotion race accepted' + Assert-True ((Get-Content (Join-Path $raceTarget 'owner.txt')) -eq 'owner') 'promotion race modified concurrent target' + Remove-Item -LiteralPath $raceStage -Recurse -Force + + $bin = Join-Path $scratch 'bin' + New-Item -ItemType Directory -Path $bin | Out-Null + $env:PATH = "$bin$([IO.Path]::PathSeparator)$env:PATH" + $env:BOOTSTRAP_PS_LOG = Join-Path $scratch 'commands.log' + $isWindowsHost = $env:OS -eq 'Windows_NT' + $stub = Join-Path $bin ($(if ($isWindowsHost) { 'npm.cmd' } else { 'npm' })) + if ($isWindowsHost) { + Set-Content $stub @' +@echo off +echo npm^|%*>>"%BOOTSTRAP_PS_LOG%" +'@ + } + else { + Set-Content $stub @' +#!/bin/sh +printf "npm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" +'@ + & chmod +x $stub + } + $pnpm = Join-Path $bin ($(if ($isWindowsHost) { 'pnpm.cmd' } else { 'pnpm' })) + if ($isWindowsHost) { Set-Content $pnpm "@echo off`r`necho 0" } + else { Set-Content $pnpm "#!/bin/sh`necho 0"; & chmod +x $pnpm } + Ensure-Pnpm + Assert-True ((Get-Content $env:BOOTSTRAP_PS_LOG) -match 'npm\|install -g pnpm@10.24.0') 'pnpm install was not pinned' + + Invoke-Git -Arguments @('-C', $target, 'checkout', '--quiet', '--', 'package.json') + if ($isWindowsHost) { + Set-Content $pnpm @' +@echo off +if "%1"=="--version" (echo 10.24.0) else (echo pnpm^|%*>>"%BOOTSTRAP_PS_LOG%") +'@ + } + else { + Set-Content $pnpm @' +#!/bin/sh +[ "$1" = --version ] && { echo 10.24.0; exit; } +printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" +'@ + & chmod +x $pnpm + } + function Approve-AnythingAnalyzerBuildScripts { param([string]$RepoDir) Set-Content (Join-Path $RepoDir 'pnpm-workspace.yaml') 'generated'; Set-Content (Join-Path $RepoDir 'package.json') '{"mutated":true}' } + $dirtyRejected = $false + try { Invoke-AnythingAnalyzerPinnedInstall -RepoDir $target -PnpmPath $pnpm -GitPath (Get-Command git).Source -PinnedCommit $pin } catch { $dirtyRejected = $_.Exception.Message -match 'local changes' } + Assert-True $dirtyRejected 'post-install dirty checkout accepted or rejection reason changed' + Assert-True (-not (Test-Path (Join-Path $target 'pnpm-workspace.yaml'))) 'generated workspace file was not removed' + + Write-Host 'PowerShell bootstrap supply-chain regression passed' +} +finally { + Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue +} From fb311078f92647296657e1ef94e5c3174760d83c Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:16:16 +0530 Subject: [PATCH 04/10] fix(bootstrap): run under a grep-free PATH is_kali called grep on every Linux run, so the PATH-stripped parser-bootstrap regression aborted before the pin gate. Detect Kali from os-release with tr, and write Unix stubs as LF/UTF-8 so Linux pwsh can execute them. --- skills/scripts/bootstrap-reverse.sh | 15 ++++++++++++++- skills/scripts/test-bootstrap-supply-chain.ps1 | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index c7f63ca..647cb65 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -142,7 +142,20 @@ sudo_cmd() { } is_kali() { - [[ -f /etc/os-release ]] && grep -qi '^ID=.*kali' /etc/os-release + [[ -f /etc/os-release ]] || return 1 + local line lowered + while IFS= read -r line || [[ -n "$line" ]]; do + lowered="$(printf '%s\n' "$line" | tr '[:upper:]' '[:lower:]')" + case "$lowered" in + id_like=*) continue ;; + id=*) + case "$lowered" in + *kali*) return 0 ;; + esac + ;; + esac + done < /etc/os-release + return 1 } platform_doc() { diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 index 315e0df..a6723f8 100644 --- a/skills/scripts/test-bootstrap-supply-chain.ps1 +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -15,6 +15,16 @@ function Test-AnythingAnalyzerElectronHealthy { return $true } function Assert-True { param([bool]$Condition, [string]$Message) if (-not $Condition) { throw $Message } } function Invoke-Git { param([string[]]$Arguments) & git @Arguments; if ($LASTEXITCODE -ne 0) { throw "git failed: $Arguments" } } +function Write-UnixExecutable { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Content + ) + $normalized = ($Content -replace "`r`n", "`n") -replace "`r", "`n" + if (-not $normalized.EndsWith("`n")) { $normalized += "`n" } + [IO.File]::WriteAllText($Path, $normalized, [Text.UTF8Encoding]::new($false)) + & chmod +x $Path +} try { $source = Join-Path $scratch 'source' @@ -63,15 +73,14 @@ echo npm^|%*>>"%BOOTSTRAP_PS_LOG%" '@ } else { - Set-Content $stub @' + Write-UnixExecutable -Path $stub -Content @' #!/bin/sh printf "npm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" '@ - & chmod +x $stub } $pnpm = Join-Path $bin ($(if ($isWindowsHost) { 'pnpm.cmd' } else { 'pnpm' })) if ($isWindowsHost) { Set-Content $pnpm "@echo off`r`necho 0" } - else { Set-Content $pnpm "#!/bin/sh`necho 0"; & chmod +x $pnpm } + else { Write-UnixExecutable -Path $pnpm -Content "#!/bin/sh`necho 0" } Ensure-Pnpm Assert-True ((Get-Content $env:BOOTSTRAP_PS_LOG) -match 'npm\|install -g pnpm@10.24.0') 'pnpm install was not pinned' @@ -83,12 +92,11 @@ if "%1"=="--version" (echo 10.24.0) else (echo pnpm^|%*>>"%BOOTSTRAP_PS_LOG%") '@ } else { - Set-Content $pnpm @' + Write-UnixExecutable -Path $pnpm -Content @' #!/bin/sh [ "$1" = --version ] && { echo 10.24.0; exit; } printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" '@ - & chmod +x $pnpm } function Approve-AnythingAnalyzerBuildScripts { param([string]$RepoDir) Set-Content (Join-Path $RepoDir 'pnpm-workspace.yaml') 'generated'; Set-Content (Join-Path $RepoDir 'package.json') '{"mutated":true}' } $dirtyRejected = $false From 5e818abbbdf4d26e008c4a7e2eab87857d21552f Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:20:22 +0530 Subject: [PATCH 05/10] fix(bootstrap): stub Linux apt in the PATH-stripped parser test After Kali detection stopped using grep, Ubuntu CI reached python install and called apt-get, which the macOS-oriented fixture never stubbed. Detect Kali with builtins only, and accept brew or apt-get as the python sink. --- skills/scripts/bootstrap-reverse.sh | 13 +++++----- skills/scripts/test-bootstrap-manifest.sh | 29 +++++++++++++++++++---- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index 647cb65..9ec06e4 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -143,14 +143,13 @@ sudo_cmd() { is_kali() { [[ -f /etc/os-release ]] || return 1 - local line lowered + local line while IFS= read -r line || [[ -n "$line" ]]; do - lowered="$(printf '%s\n' "$line" | tr '[:upper:]' '[:lower:]')" - case "$lowered" in - id_like=*) continue ;; - id=*) - case "$lowered" in - *kali*) return 0 ;; + case "$line" in + ID_LIKE=*|id_like=*) continue ;; + ID=*|id=*) + case "$line" in + *[Kk][Aa][Ll][Ii]*) return 0 ;; esac ;; esac diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh index 7909484..de98039 100644 --- a/skills/scripts/test-bootstrap-manifest.sh +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -16,6 +16,10 @@ cat > "$STUB_BIN/command-stub" <<'STUB' #!/usr/bin/env bash name="$(basename "$0")" { printf '%s' "$name"; for arg in "$@"; do printf '|%s' "$arg"; done; printf '\n'; } >> "$CALL_LOG" +if [[ "$name" == sudo ]]; then + next="${1:-}"; shift || true + exec "$(dirname "$0")/$next" "$@" +fi case "$name:${1:-}" in pipx:--version) printf '%s\n' "${STUB_PIPX_VERSION:-0}" ;; pnpm:--version) printf '%s\n' "${STUB_PNPM_VERSION:-0}" ;; @@ -24,6 +28,12 @@ case "$name:${1:-}" in ln -sf "$STUB_PYTHON_SOURCE" "$STUB_ACTIVE_BIN/python3" fi ;; + apt-get:install) + pkg="${3:-${2:-}}" + if [[ "$pkg" == python3 ]]; then + ln -sf "$STUB_PYTHON_SOURCE" "$STUB_ACTIVE_BIN/python3" + fi + ;; git:init) target="${!#}"; mkdir -p "$target/.git"; printf '%s\n' unpinned-head > "$target/.stub-head" ;; @@ -47,7 +57,7 @@ esac [[ "${STUB_FAIL_COMMAND:-}" != "$name" ]] STUB chmod +x "$STUB_BIN/command-stub" -for name in git node npm npx pipx pnpm sleep nc; do ln -s command-stub "$STUB_BIN/$name"; done +for name in git node npm npx pipx pnpm sleep nc apt-get sudo; do ln -s command-stub "$STUB_BIN/$name"; done ln -s command-stub "$STUB_BIN/brew" cat > "$STUB_BIN/python3" < "$CALL_LOG" -env PATH="$NO_PYTHON_BIN" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \ +if ! env PATH="$NO_PYTHON_BIN" HOME="$SCRATCH/home" CALL_LOG="$CALL_LOG" \ STUB_ACTIVE_BIN="$NO_PYTHON_BIN" STUB_PYTHON_SOURCE="$STUB_BIN/python3" \ REVERSE_SKILL_TOOLS_DIR="$SCRATCH/tools" CLAUDE_MCP_CONFIG="$SCRATCH/home/mcp.json" \ - bash "$PARSER_FIXTURE/bootstrap-reverse.sh" agent-browser --skip-refresh >/dev/null -expect_line 'brew|install|python' + bash "$PARSER_FIXTURE/bootstrap-reverse.sh" agent-browser --skip-refresh \ + >"$SCRATCH/parser-out.log" 2>&1; then + echo "parser-bootstrap failed:" >&2 + cat "$SCRATCH/parser-out.log" >&2 + exit 1 +fi +if [[ "$(uname -s)" == Darwin ]]; then + expect_line 'brew|install|python' +else + expect_line 'apt-get|install|-y|python3' +fi expect_line "npm|install|-g|$(json_value agent-browser npmPackage)" ! grep -Fq '|pip|install|' "$CALL_LOG" From 0bcde46f507db120a686806551443199bdfd1489 Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:23:29 +0530 Subject: [PATCH 06/10] fix(bootstrap): stub nohup so Linux CI records pnpm dev The anything-analyzer start path backgrounds `nohup pnpm dev`. On Ubuntu CI that never invoked the pnpm stub, so the source-pin regression missed the argv. Intercept nohup like sudo, and fall back to a plain background when nohup is absent. --- skills/scripts/bootstrap-reverse.sh | 9 ++++++++- skills/scripts/test-bootstrap-manifest.sh | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/skills/scripts/bootstrap-reverse.sh b/skills/scripts/bootstrap-reverse.sh index 9ec06e4..fae4b01 100644 --- a/skills/scripts/bootstrap-reverse.sh +++ b/skills/scripts/bootstrap-reverse.sh @@ -679,7 +679,14 @@ ensure_anything_analyzer() { if $START_SERVICES; then (cd "$dir" && pnpm install --frozen-lockfile) || return 1 install_git_commit "$repo" "$commit" "$dir" || return 1 - (cd "$dir" && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &) + ( + cd "$dir" || exit 1 + if has_cmd nohup; then + nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 & + else + pnpm dev >/tmp/anything-analyzer.log 2>&1 & + fi + ) if wait_for_port 23816 120; then if test_mcp_http 23816; then log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)" diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh index de98039..7234141 100644 --- a/skills/scripts/test-bootstrap-manifest.sh +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -16,7 +16,7 @@ cat > "$STUB_BIN/command-stub" <<'STUB' #!/usr/bin/env bash name="$(basename "$0")" { printf '%s' "$name"; for arg in "$@"; do printf '|%s' "$arg"; done; printf '\n'; } >> "$CALL_LOG" -if [[ "$name" == sudo ]]; then +if [[ "$name" == sudo || "$name" == nohup ]]; then next="${1:-}"; shift || true exec "$(dirname "$0")/$next" "$@" fi @@ -57,7 +57,7 @@ esac [[ "${STUB_FAIL_COMMAND:-}" != "$name" ]] STUB chmod +x "$STUB_BIN/command-stub" -for name in git node npm npx pipx pnpm sleep nc apt-get sudo; do ln -s command-stub "$STUB_BIN/$name"; done +for name in git node npm npx pipx pnpm sleep nc apt-get sudo nohup; do ln -s command-stub "$STUB_BIN/$name"; done ln -s command-stub "$STUB_BIN/brew" cat > "$STUB_BIN/python3" < Date: Fri, 14 Aug 2026 01:45:40 +0800 Subject: [PATCH 07/10] fix(bootstrap): preserve native command exit codes --- .github/workflows/ci.yml | 5 +++ skills/scripts/lib/BootstrapSupplyChain.ps1 | 37 ++++++++++++++++--- .../scripts/test-bootstrap-supply-chain.ps1 | 6 ++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4bebe2..6ca6d51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,11 @@ jobs: shell: pwsh run: ./skills/scripts/test-bootstrap-supply-chain.ps1 + - name: Bootstrap supply-chain regression (Windows PowerShell 5.1) + if: runner.os == 'Windows' + shell: powershell + run: ./skills/scripts/test-bootstrap-supply-chain.ps1 + - name: Smoke (verify + parse + quick route) shell: pwsh run: ./skills/scripts/smoke.ps1 diff --git a/skills/scripts/lib/BootstrapSupplyChain.ps1 b/skills/scripts/lib/BootstrapSupplyChain.ps1 index 6e6c4c8..36dbd27 100644 --- a/skills/scripts/lib/BootstrapSupplyChain.ps1 +++ b/skills/scripts/lib/BootstrapSupplyChain.ps1 @@ -4,8 +4,17 @@ function Ensure-Pnpm { $pnpm = Get-NodeCommandPath -Name 'pnpm' $currentVersion = '' if ($pnpm) { - $versionLine = & $pnpm --version 2>$null | Select-Object -First 1 - if ($LASTEXITCODE -eq 0 -and $null -ne $versionLine) { + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + $versionOutput = @(& $pnpm --version 2>$null) + $versionExitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + $versionLine = $versionOutput | Select-Object -First 1 + if ($versionExitCode -eq 0 -and $null -ne $versionLine) { $currentVersion = ([string]$versionLine).Trim() } } @@ -28,13 +37,29 @@ function Assert-GitCheckoutState { [Parameter(Mandatory = $true)][string]$PinnedCommit ) - $resolvedLine = & $GitPath -C $CheckoutPath rev-parse HEAD 2>$null | Select-Object -First 1 + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + $resolvedOutput = @(& $GitPath -C $CheckoutPath rev-parse HEAD 2>$null) + $resolveExitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + $resolvedLine = $resolvedOutput | Select-Object -First 1 $resolvedCommit = if ($null -eq $resolvedLine) { '' } else { ([string]$resolvedLine).Trim() } - if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $PinnedCommit) { + if ($resolveExitCode -ne 0 -or $resolvedCommit -ne $PinnedCommit) { throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)" } - $status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>&1) - if ($LASTEXITCODE -ne 0) { + try { + $ErrorActionPreference = 'Continue' + $status = @(& $GitPath -C $CheckoutPath status --porcelain --untracked-files=all 2>$null) + $statusExitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($statusExitCode -ne 0) { throw "Cannot inspect checkout state: $CheckoutPath" } if ($status.Count -gt 0) { diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 index a6723f8..0647a46 100644 --- a/skills/scripts/test-bootstrap-supply-chain.ps1 +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -64,7 +64,7 @@ try { New-Item -ItemType Directory -Path $bin | Out-Null $env:PATH = "$bin$([IO.Path]::PathSeparator)$env:PATH" $env:BOOTSTRAP_PS_LOG = Join-Path $scratch 'commands.log' - $isWindowsHost = $env:OS -eq 'Windows_NT' + $isWindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT $stub = Join-Path $bin ($(if ($isWindowsHost) { 'npm.cmd' } else { 'npm' })) if ($isWindowsHost) { Set-Content $stub @' @@ -98,6 +98,10 @@ if "%1"=="--version" (echo 10.24.0) else (echo pnpm^|%*>>"%BOOTSTRAP_PS_LOG%") printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" '@ } + $commandLogBefore = Get-Content -LiteralPath $env:BOOTSTRAP_PS_LOG -Raw + Ensure-Pnpm + $commandLogAfter = Get-Content -LiteralPath $env:BOOTSTRAP_PS_LOG -Raw + Assert-True ($commandLogAfter -eq $commandLogBefore) 'matching pnpm version triggered reinstall' function Approve-AnythingAnalyzerBuildScripts { param([string]$RepoDir) Set-Content (Join-Path $RepoDir 'pnpm-workspace.yaml') 'generated'; Set-Content (Join-Path $RepoDir 'package.json') '{"mutated":true}' } $dirtyRejected = $false try { Invoke-AnythingAnalyzerPinnedInstall -RepoDir $target -PnpmPath $pnpm -GitPath (Get-Command git).Source -PinnedCommit $pin } catch { $dirtyRejected = $_.Exception.Message -match 'local changes' } From 41ee697f542441ac552b21ed49b2a25b9a40405c Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:36:31 +0530 Subject: [PATCH 08/10] fix(bootstrap): fail closed on verified checkouts --- kali/scripts/bootstrap-reverse.sh | 10 +-- skills/scripts/bootstrap-reverse.ps1 | 19 +++-- skills/scripts/test-bootstrap-manifest.sh | 5 +- .../scripts/test-bootstrap-supply-chain.ps1 | 73 ++++++++++++++++++- 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/kali/scripts/bootstrap-reverse.sh b/kali/scripts/bootstrap-reverse.sh index 651f630..b9ff643 100644 --- a/kali/scripts/bootstrap-reverse.sh +++ b/kali/scripts/bootstrap-reverse.sh @@ -661,17 +661,17 @@ EOF # ─── 服务启动 ────────────────────────────────────────────────────────────────────── start_anything_analyzer() { - if test_tcp_port 23816 2>/dev/null; then - log_ok "anything-analyzer 已在运行 (port 23816)" - return 0 - fi - local repo_dir="$HOME/tools/anything-analyzer" local repo commit repo=$(manifest_field anything-analyzer repoUrl) commit=$(manifest_field anything-analyzer pinnedCommit) install_git_commit "$repo" "$commit" "$repo_dir" || return 1 + if test_tcp_port 23816 2>/dev/null; then + log_ok "anything-analyzer 已在运行 (port 23816)" + return 0 + fi + local pnpm_package pnpm_version current_pnpm_version='' pnpm_package=$(manifest_dependency pnpm package) || return 1 pnpm_version=$(manifest_dependency pnpm version) || return 1 diff --git a/skills/scripts/bootstrap-reverse.ps1 b/skills/scripts/bootstrap-reverse.ps1 index c764186..0c12862 100644 --- a/skills/scripts/bootstrap-reverse.ps1 +++ b/skills/scripts/bootstrap-reverse.ps1 @@ -752,10 +752,6 @@ function Start-AnythingAnalyzerService { $AuthToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$Definition.servicePort) } - if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) { - return - } - $repoDir = [string]$Definition.installDir $checkoutDefinition = [pscustomobject]@{ repo = [string]$Definition.repoUrl @@ -763,6 +759,10 @@ function Start-AnythingAnalyzerService { } Ensure-GitCloneInstall -Definition $checkoutDefinition -TargetPath $repoDir | Out-Null + if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) { + return + } + Ensure-Pnpm $vsBuildToolsError = '' if (Test-ReverseIsWindows) { @@ -842,7 +842,7 @@ function Ensure-Capability { } $existingState = Get-ReverseCapabilityState -Name $Name - if ($existingState -and -not $definition.PSObject.Properties['mcpNames']) { + if ($existingState -and -not $definition.PSObject.Properties['mcpNames'] -and $definition.bootstrapKind -ne 'git-clone') { $toolSpec = $null try { $toolSpec = Resolve-ReverseToolSpec -Name $Name @@ -1053,6 +1053,12 @@ function Expand-CapabilityDependencies { return $ordered } +function Test-BootstrapResultsSucceeded { + param([Parameter(Mandatory = $true)][object[]]$Results) + + return (@($Results | Where-Object { $_.status -eq 'failed' }).Count -eq 0) +} + $expandedCapabilities = Expand-CapabilityDependencies -Names $Capability $results = @() @@ -1112,3 +1118,6 @@ if (-not $SkipRefresh) { } $results | ConvertTo-Json -Depth 5 +if (-not (Test-BootstrapResultsSucceeded -Results $results)) { + exit 1 +} diff --git a/skills/scripts/test-bootstrap-manifest.sh b/skills/scripts/test-bootstrap-manifest.sh index 7234141..080e43a 100644 --- a/skills/scripts/test-bootstrap-manifest.sh +++ b/skills/scripts/test-bootstrap-manifest.sh @@ -49,6 +49,7 @@ case "$name:${1:-}" in esac ;; nc:-z) + [[ "${STUB_NC_PREOCCUPIED:-0}" != 1 ]] || exit 0 count=0; [[ ! -f "$STUB_NC_STATE" ]] || count="$(cat "$STUB_NC_STATE")" printf '%s\n' "$((count + 1))" > "$STUB_NC_STATE" (( count > 0 )) && exit 0 || exit 1 @@ -93,7 +94,7 @@ run_kali() { rm -f "$SCRATCH/nc-count" env PATH="$STUB_BIN:/opt/homebrew/bin:/usr/bin:/bin" HOME="$SCRATCH/home" \ CALL_LOG="$CALL_LOG" STUB_NC_STATE="$SCRATCH/nc-count" STUB_PNPM_VERSION="${STUB_PNPM_VERSION:-}" \ - STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" bash "$KALI_BOOTSTRAP" "$@" + STUB_FAIL_FETCH="${STUB_FAIL_FETCH:-0}" STUB_NC_PREOCCUPIED="${STUB_NC_PREOCCUPIED:-0}" bash "$KALI_BOOTSTRAP" "$@" } expect_line() { grep -Fqx "$1" "$CALL_LOG" || { echo "missing argv: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; } expect_fragment() { grep -Fq "$1" "$CALL_LOG" || { echo "missing argv fragment: $1" >&2; cat "$CALL_LOG" >&2; return 1; }; } @@ -215,6 +216,8 @@ if (( BASH_VERSINFO[0] >= 4 )); then [[ $(grep -c '|status|--porcelain|--untracked-files=all' "$CALL_LOG") -ge 2 ]] touch "$kali_dir/.stub-dirty" rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh + STUB_NC_PREOCCUPIED=1 rejects_without_pnpm run_kali anything-analyzer --start-services --skip-refresh + expect_fragment '|status|--porcelain|--untracked-files=all' rm -rf "$kali_dir" : > "$CALL_LOG" diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 index 0647a46..e854367 100644 --- a/skills/scripts/test-bootstrap-supply-chain.ps1 +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -46,7 +46,9 @@ try { $failedTarget = Join-Path $scratch 'failed' $badDefinition = [pscustomobject]@{ repo = (Join-Path $scratch 'missing'); pinnedCommit = $pin } - try { Ensure-GitCloneInstall -Definition $badDefinition -TargetPath $failedTarget | Out-Null; throw 'failed fetch accepted' } catch {} + $failedFetchRejected = $false + try { Ensure-GitCloneInstall -Definition $badDefinition -TargetPath $failedTarget | Out-Null } catch { $failedFetchRejected = $true } + Assert-True $failedFetchRejected 'failed fetch accepted' Assert-True (-not (Test-Path $failedTarget)) 'failed fetch poisoned final path' Assert-True (@(Get-ChildItem $scratch -Filter '.reverse-bootstrap-*').Count -eq 0) 'failed fetch left staging path' @@ -108,6 +110,75 @@ printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" Assert-True $dirtyRejected 'post-install dirty checkout accepted or rejection reason changed' Assert-True (-not (Test-Path (Join-Path $target 'pnpm-workspace.yaml'))) 'generated workspace file was not removed' + Invoke-Git -Arguments @('-C', $target, 'config', 'user.email', 'test@example.invalid') + Invoke-Git -Arguments @('-C', $target, 'config', 'user.name', 'test') + Invoke-Git -Arguments @('-C', $target, 'commit', '--allow-empty', '--quiet', '-m', 'wrong checkout') + $wrongCommitRejected = $false + try { Ensure-GitCloneInstall -Definition $definition -TargetPath $target | Out-Null } catch { $wrongCommitRejected = $_.Exception.Message -match 'expected' } + Assert-True $wrongCommitRejected 'clean wrong-commit checkout accepted' + + $publicProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) + $publicTools = Join-Path $publicProfile 'Tools' + $publicTarget = Join-Path $publicTools 'SecLists' + if (Test-Path -LiteralPath $publicTarget) { + Write-Host 'SKIP: public bootstrap exit regression (existing SecLists checkout)' + } + else { + $createdPublicTools = -not (Test-Path -LiteralPath $publicTools) + try { + New-Item -ItemType Directory -Path $publicTarget -Force | Out-Null + Invoke-Git -Arguments @('-C', $publicTarget, 'init', '--quiet') + Invoke-Git -Arguments @('-C', $publicTarget, 'config', 'user.email', 'test@example.invalid') + Invoke-Git -Arguments @('-C', $publicTarget, 'config', 'user.name', 'test') + Set-Content (Join-Path $publicTarget 'fixture.txt') 'wrong checkout' + Invoke-Git -Arguments @('-C', $publicTarget, 'add', 'fixture.txt') + Invoke-Git -Arguments @('-C', $publicTarget, 'commit', '--quiet', '-m', 'fixture') + + $powerShellHost = if ($PSVersionTable.PSEdition -eq 'Desktop') { Join-Path $PSHOME 'powershell.exe' } else { Join-Path $PSHOME 'pwsh' } + $childOutput = @(& $powerShellHost -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'bootstrap-reverse.ps1') -Capability seclists -SkipRefresh) + $childExitCode = $LASTEXITCODE + $childResult = ($childOutput -join [Environment]::NewLine) | ConvertFrom-Json + Assert-True ($childExitCode -ne 0) 'failed public bootstrap exited successfully' + Assert-True ($childResult.status -eq 'failed') 'failed public bootstrap did not report failed status' + Assert-True ($childResult.error -match 'Checkout verification failed') 'failed public bootstrap did not report checkout verification' + } + finally { + Remove-Item -LiteralPath $publicTarget -Recurse -Force -ErrorAction SilentlyContinue + if ($createdPublicTools -and (Test-Path -LiteralPath $publicTools) -and (@(Get-ChildItem -LiteralPath $publicTools -Force).Count -eq 0)) { + Remove-Item -LiteralPath $publicTools -Force -ErrorAction SilentlyContinue + } + } + } + + . (Join-Path $PSScriptRoot 'bootstrap-reverse.ps1') -Capability '__test_missing__' -SkipRefresh | Out-Null + $script:gitCloneDefinition = [pscustomobject]@{ name = 'test-git-clone'; bootstrapKind = 'git-clone'; canAutoInstall = $true } + $script:gitCloneVerifierCalled = $false + function Get-ReverseBootstrapDefinition { param([string]$Name) return $script:gitCloneDefinition } + function Get-ReverseCapabilityState { param([string]$Name) return [pscustomobject]@{ Ready = $true } } + function Resolve-ReverseToolSpec { param([string]$Name) return [pscustomobject]@{ Available = $true } } + function Ensure-GitCloneInstall { + param($Definition, [string]$TargetPath) + $script:gitCloneVerifierCalled = $true + return [pscustomobject]@{ Verified = $true } + } + $gitCloneResult = Ensure-Capability -Name 'test-git-clone' + Assert-True $script:gitCloneVerifierCalled 'available git-clone capability skipped checkout verification' + Assert-True $gitCloneResult.Verified 'git-clone capability did not return checkout verification result' + + $script:serviceCheckoutVerifierCalled = $false + function Ensure-GitCloneInstall { + param($Definition, [string]$TargetPath) + $script:serviceCheckoutVerifierCalled = $true + } + function Test-ReverseTcpPort { param([int]$Port) return $true } + Start-AnythingAnalyzerService -Definition ([pscustomobject]@{ + installDir = (Join-Path $scratch 'anything-analyzer') + repoUrl = $source + pinnedCommit = $pin + servicePort = 23816 + }) -AuthToken 'test-token' + Assert-True $script:serviceCheckoutVerifierCalled 'running Anything Analyzer service skipped checkout verification' + Write-Host 'PowerShell bootstrap supply-chain regression passed' } finally { From 49a5d285aa45d64c73b62b5c30ee51a25f4195d2 Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:42:03 +0530 Subject: [PATCH 09/10] test(bootstrap): complete git checkout fixture --- skills/scripts/test-bootstrap-supply-chain.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 index e854367..4f46448 100644 --- a/skills/scripts/test-bootstrap-supply-chain.ps1 +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -151,7 +151,12 @@ printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" } . (Join-Path $PSScriptRoot 'bootstrap-reverse.ps1') -Capability '__test_missing__' -SkipRefresh | Out-Null - $script:gitCloneDefinition = [pscustomobject]@{ name = 'test-git-clone'; bootstrapKind = 'git-clone'; canAutoInstall = $true } + $script:gitCloneDefinition = [pscustomobject]@{ + name = 'test-git-clone' + bootstrapKind = 'git-clone' + canAutoInstall = $true + installDir = (Join-Path $scratch 'test-git-clone') + } $script:gitCloneVerifierCalled = $false function Get-ReverseBootstrapDefinition { param([string]$Name) return $script:gitCloneDefinition } function Get-ReverseCapabilityState { param([string]$Name) return [pscustomobject]@{ Ready = $true } } From 7ea6d50732ba78e8dce48e5248b49ebd26b34825 Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:46:50 +0530 Subject: [PATCH 10/10] test(bootstrap): reset expected child exit status --- skills/scripts/test-bootstrap-supply-chain.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/scripts/test-bootstrap-supply-chain.ps1 b/skills/scripts/test-bootstrap-supply-chain.ps1 index 4f46448..e8062de 100644 --- a/skills/scripts/test-bootstrap-supply-chain.ps1 +++ b/skills/scripts/test-bootstrap-supply-chain.ps1 @@ -137,6 +137,7 @@ printf "pnpm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG" $powerShellHost = if ($PSVersionTable.PSEdition -eq 'Desktop') { Join-Path $PSHOME 'powershell.exe' } else { Join-Path $PSHOME 'pwsh' } $childOutput = @(& $powerShellHost -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'bootstrap-reverse.ps1') -Capability seclists -SkipRefresh) $childExitCode = $LASTEXITCODE + $global:LASTEXITCODE = 0 $childResult = ($childOutput -join [Environment]::NewLine) | ConvertFrom-Json Assert-True ($childExitCode -ne 0) 'failed public bootstrap exited successfully' Assert-True ($childResult.status -eq 'failed') 'failed public bootstrap did not report failed status'