Merge pull request #76 from atirna/agent/reverse-skill-impactful-audit

fix(bootstrap): 强制使用清单中的固定来源
This commit is contained in:
Edison
2026-08-14 19:56:25 +08:00
committed by GitHub
11 changed files with 1009 additions and 221 deletions
+10
View File
@@ -32,6 +32,15 @@ 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: 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
@@ -72,6 +81,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
+1
View File
@@ -23,6 +23,7 @@ Desktop.ini
*.tmp
*.bak
*.log
implementation-notes.md
# User-specific config
.claude/
+11
View File
@@ -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",
@@ -156,6 +166,7 @@
"servicePort": 23816,
"docsUrl": "https://github.com/Mouseww/anything-analyzer",
"canAutoInstall": true,
"pinnedCommit": "0ed4791688e5186da051c85eb7ddfe4639e14fd2",
"verificationMode": "service-or-registration"
},
{
+67 -20
View File
@@ -22,6 +22,7 @@ CAPABILITIES=()
START_SERVICES=false
SKIP_REFRESH=false
MANUAL_REQUIRED=false
FAILED=false
LAST_CAPABILITY_MANUAL=false
for arg in "$@"; do
@@ -131,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
@@ -145,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
}
@@ -330,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
@@ -624,23 +661,30 @@ EOF
# ─── 服务启动 ──────────────────────────────────────────────────────────────────────
start_anything_analyzer() {
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 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"
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
if ! command -v pnpm &>/dev/null; then
npm install -g pnpm
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
@@ -681,6 +725,7 @@ for cap in "${CAPABILITIES[@]}"; do
fi
else
RESULTS+=("{\"name\":\"$cap\",\"status\":\"failed\"}")
FAILED=true
fi
done
@@ -691,7 +736,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
+11
View File
@@ -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",
@@ -127,6 +137,7 @@
"servicePort": 23816,
"docsUrl": "https://github.com/Mouseww/anything-analyzer",
"canAutoInstall": true,
"pinnedCommit": "0ed4791688e5186da051c85eb7ddfe4639e14fd2",
"verificationMode": "service-or-registration"
},
{
+33 -143
View File
@@ -23,6 +23,18 @@ $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)
$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)) {
@@ -153,20 +165,6 @@ function Ensure-JavaRuntime {
}
}
function Ensure-Pnpm {
Ensure-NodeRuntime
if (-not (Get-NodeCommandPath -Name 'pnpm')) {
$npm = Get-NodeCommandPath -Name 'npm'
if ([string]::IsNullOrWhiteSpace($npm)) {
throw 'npm is not available after Node.js installation.'
}
& $npm install -g pnpm
if ($LASTEXITCODE -ne 0) {
throw 'Failed to install pnpm globally.'
}
}
}
function Get-AnythingAnalyzerUserDataPaths {
$candidates = @(
(Join-Path $env:APPDATA 'anything-analyzer'),
@@ -341,34 +339,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
}
@@ -777,6 +752,13 @@ function Start-AnythingAnalyzerService {
$AuthToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$Definition.servicePort)
}
$repoDir = [string]$Definition.installDir
$checkoutDefinition = [pscustomobject]@{
repo = [string]$Definition.repoUrl
pinnedCommit = [string]$Definition.pinnedCommit
}
Ensure-GitCloneInstall -Definition $checkoutDefinition -TargetPath $repoDir | Out-Null
if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) {
return
}
@@ -793,65 +775,13 @@ 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.'
}
Push-Location $repoDir
try {
Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir -PnpmPath $pnpm
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
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
}
$git = Get-FirstCommandPath -Names @('git')
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'
@@ -892,55 +822,6 @@ function Ensure-AndroidPlatformTools {
return (Resolve-ReverseToolSpec -Name 'adb')
}
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 (-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"
}
}
return $true
}
if (Test-Path -LiteralPath $TargetPath) {
$backupPath = "$TargetPath.bak-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))"
Move-Item -LiteralPath $TargetPath -Destination $backupPath -Force
}
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)"
}
}
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"
}
}
return $true
}
function Ensure-Capability {
param([Parameter(Mandatory = $true)][string]$Name)
@@ -961,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
@@ -1172,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 = @()
@@ -1231,3 +1118,6 @@ if (-not $SkipRefresh) {
}
$results | ConvertTo-Json -Depth 5
if (-not (Test-BootstrapResultsSucceeded -Results $results)) {
exit 1
}
+258 -57
View File
@@ -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,39 @@ 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
}
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:-}"
@@ -107,7 +142,19 @@ sudo_cmd() {
}
is_kali() {
[[ -f /etc/os-release ]] && grep -qi '^ID=.*kali' /etc/os-release
[[ -f /etc/os-release ]] || return 1
local line
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
ID_LIKE=*|id_like=*) continue ;;
ID=*|id=*)
case "$line" in
*[Kk][Aa][Ll][Ii]*) return 0 ;;
esac
;;
esac
done < /etc/os-release
return 1
}
platform_doc() {
@@ -189,6 +236,22 @@ install_brew_cask() {
}
ensure_python_runtime() {
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
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"
}
ensure_python_interpreter() {
if ! has_cmd python3; then
case "$PLATFORM" in
macos) install_brew python ;;
@@ -196,18 +259,7 @@ 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
fi
python3 -m pipx ensurepath >/dev/null 2>&1 || true
export PATH="$HOME/.local/bin:$PATH"
has_cmd python3 || { log_err "Python 3 installation completed without a usable python3 command."; return 1; }
}
ensure_node_runtime() {
@@ -229,10 +281,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]
@@ -351,6 +410,86 @@ install_github_release() {
log_ok "installed $repo to $dest"
}
install_git_commit() {
local repo="$1"
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
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
log_err "Install path exists but is not a git checkout: $install_dir"
return 1
fi
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
}
write_mcp_server() {
local name="$1"
local json_payload="$2"
@@ -444,12 +583,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) || 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 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 +603,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) || 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)
jar="$TOOLS_ROOT/apktool/apktool.jar"
@@ -480,45 +623,70 @@ 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
pipx install frida-tools || pipx upgrade frida-tools
local package
package=$(manifest_field frida pipPackage) || return 1
pipx install --force "$package" || return 1
export PATH="$HOME/.local/bin:$PATH"
}
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
pipx install 'git+https://github.com/mrexodia/ida-pro-mcp.git' || pipx upgrade ida-pro-mcp
local source
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."
}
ensure_jshookmcp() {
ensure_node_runtime
write_mcp_server "jshook" '{"command":"npx","args":["-y","@jshookmcp/jshook@0.3.4"],"env":{"JSHOOK_BASE_PROFILE":"search"}}'
ensure_node_runtime || return 1
local package
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'}}))
PY
)"
}
ensure_reqable_mcp() {
ensure_node_runtime
write_mcp_server "reqable-mcp" '{"command":"npx","args":["-y","reqable-mcp-server@1.0.1"]}'
ensure_node_runtime || return 1
local package
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]]}))
PY
)"
log_warn "Reqable MCP requires the separately installed Reqable desktop application and its local API."
}
ensure_anything_analyzer() {
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"
local repo commit
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
install_git_commit "$repo" "$commit" "$dir" || return 1
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" || 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)"
@@ -572,16 +740,21 @@ 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
npm install -g agent-browser
local package
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"
if [[ -x "$setup" ]]; then "$setup" --skip-browser-install || true; fi
}
ensure_ghidra_mcp() {
ensure_java_runtime
ensure_java_runtime || return 1
local repo regex
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
@@ -590,7 +763,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 +773,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) || return 1
commit=$(manifest_field seclists pinnedCommit) || return 1
install_git_commit "$repo" "$commit" "$dir" || return 1
}
ensure_proxycat() {
ensure_python_runtime
ensure_python_runtime || return 1
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) || 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
MANUAL_REQUIRED=true
return 0
}
}
ensure_burpsuite_mcp() {
@@ -649,7 +833,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) || 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)"
if [[ -z "$go_bin" ]]; then
@@ -663,8 +850,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
@@ -687,9 +878,11 @@ 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
pipx install pwntools || python3 -m pip install --user pwntools
local package
package=$(manifest_field pwntools pipPackage) || return 1
pipx install "$package" || python3 -m pip install --user "$package" || return 1
}
status_json_line() {
@@ -768,6 +961,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
@@ -779,6 +977,7 @@ for cap in "${EXPANDED[@]}"; do
fi
else
status_json_line "$cap" "failed" "see $(platform_doc)" >> "$RESULTS_FILE"
FAILED=true
fi
done
@@ -787,7 +986,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
+176
View File
@@ -0,0 +1,176 @@
function Ensure-Pnpm {
Ensure-NodeRuntime
$dependency = Get-BootstrapDependency -Name 'pnpm'
$pnpm = Get-NodeCommandPath -Name 'pnpm'
$currentVersion = ''
if ($pnpm) {
$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()
}
}
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
)
$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 ($resolveExitCode -ne 0 -or $resolvedCommit -ne $PinnedCommit) {
throw "Checkout verification failed: expected $PinnedCommit, got $resolvedCommit ($CheckoutPath)"
}
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) {
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
}
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BOOTSTRAP="$SCRIPT_DIR/bootstrap-reverse.sh"
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"
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 || "$name" == nohup ]]; 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}" ;;
brew:install)
if [[ "${2:-}" == python ]]; then
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"
;;
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)
[[ "${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
;;
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 nohup; do ln -s command-stub "$STUB_BIN/$name"; done
ln -s command-stub "$STUB_BIN/brew"
cat > "$STUB_BIN/python3" <<STUB
#!/usr/bin/env bash
{ printf 'python3'; for arg in "\$@"; do printf '|%s' "\$arg"; done; printf '\n'; } >> "\$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"
json_value() {
"$REAL_PYTHON" - "$MANIFEST" "$1" "$2" <<'PY'
import json, pathlib, sys
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_generic() {
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" \
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}" 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; }; }
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"
}
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)
# The manifest parser is bootstrapped before a Node-only sink, without installing pipx.
NO_PYTHON_BIN="$SCRATCH/no-python-bin"
PARSER_FIXTURE="$SCRATCH/parser-bootstrap"
mkdir -p "$NO_PYTHON_BIN"
for name in git node npm npx pipx pnpm sleep nc brew apt-get sudo nohup; do ln -s "$STUB_BIN/command-stub" "$NO_PYTHON_BIN/$name"; done
for tool in bash uname dirname mktemp rm head tr basename mkdir cat ln; do ln -s "$(command -v "$tool")" "$NO_PYTHON_BIN/$tool"; done
mkdir -p "$PARSER_FIXTURE"
cp "$BOOTSTRAP" "$PARSER_FIXTURE/bootstrap-reverse.sh"
cp "$MANIFEST" "$PARSER_FIXTURE/bootstrap-manifest.json"
: > "$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 \
>"$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"
# 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"
STUB_PIPX_VERSION=1.16.5 run_generic "$capability" --skip-refresh >/dev/null
expect_line "$expected"
done <<EOF
frida|pipPackage|pipx|install|--force|$(json_value frida pipPackage)
idalib-mcp|pipSource|pipx|install|--force|$(json_value idalib-mcp pipSource)
agent-browser|npmPackage|npm|install|-g|$(json_value agent-browser npmPackage)
proxycat|repo|pipx|install|git+$(json_value proxycat repo)@$(json_value proxycat pinnedCommit)
pwntools|pipPackage|pipx|install|$(json_value pwntools pipPackage)
EOF
# pipx itself is pinned; a failed pinned install has no mutable fallback.
: > "$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"
: > "$CALL_LOG"
set +e
STUB_PNPM_VERSION=0 run_kali anything-analyzer --start-services --skip-refresh >/dev/null 2>&1
set -e
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
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"
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
echo 'bootstrap manifest source regression passed'
@@ -0,0 +1,192 @@
$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" } }
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'
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 }
$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'
$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 = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT
$stub = Join-Path $bin ($(if ($isWindowsHost) { 'npm.cmd' } else { 'npm' }))
if ($isWindowsHost) {
Set-Content $stub @'
@echo off
echo npm^|%*>>"%BOOTSTRAP_PS_LOG%"
'@
}
else {
Write-UnixExecutable -Path $stub -Content @'
#!/bin/sh
printf "npm|%s\n" "$*" >> "$BOOTSTRAP_PS_LOG"
'@
}
$pnpm = Join-Path $bin ($(if ($isWindowsHost) { 'pnpm.cmd' } else { 'pnpm' }))
if ($isWindowsHost) { Set-Content $pnpm "@echo off`r`necho 0" }
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'
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 {
Write-UnixExecutable -Path $pnpm -Content @'
#!/bin/sh
[ "$1" = --version ] && { echo 10.24.0; exit; }
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' }
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
$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'
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
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 } }
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 {
Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue
}
+16 -1
View File
@@ -389,11 +389,23 @@ 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 }
$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)
@@ -401,7 +413,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 通道