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)