Files
reverse-skill/skills/scripts/verify-routing-coherence.ps1
T
2026-08-08 18:32:33 +08:00

404 lines
22 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#Requires -Version 5.1
# reverse-skill routing + ops contract gates (skill-router only; no host platform runtime)
param([string] $ScratchDir = '')
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
$masterRoute = Join-Path $scriptDir 'master-route.ps1'
$caseInit = Join-Path $scriptDir 'case-init.ps1'
$masterDoc = Join-Path $skillsRoot 'MASTER-ROUTING.md'
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
if (-not $ScratchDir) {
$ScratchDir = Join-Path $tmpBase ("rs-verify-{0}" -f (Get-Date -Format 'yyyyMMddHHmmss'))
}
New-Item -ItemType Directory -Force -Path $ScratchDir | Out-Null
$fail = New-Object System.Collections.Generic.List[string]
function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
function Bad($m) { Write-Host "[FAIL] $m" -ForegroundColor Red; [void]$fail.Add($m) }
# --- 新事实源/产物检查(routing.json / benchmark / INDEX) ---
$routingJson = Join-Path $skillsRoot 'config/routing.json'
if (Test-Path -LiteralPath $routingJson) {
$rj = Get-Content -LiteralPath $routingJson -Raw -Encoding UTF8 | ConvertFrom-Json
$rjRoutes = @($rj.routes.PSObject.Properties)
if ($rjRoutes.Count -ge 30) { Ok "routing.json routes=$($rjRoutes.Count)" } else { Bad 'routing.json route count suspicious (<30)' }
$badRoute = @($rjRoutes | Where-Object { -not $_.Value.label -or -not $_.Value.skill -or -not $_.Value.keywords })
if ($badRoute.Count -eq 0) { Ok 'routing.json: all routes have label/skill/keywords' } else { Bad "routing.json routes missing fields: $($badRoute.Name -join ',')" }
$missingRouteSkills = @($rjRoutes | Where-Object {
-not (Test-Path -LiteralPath (Join-Path $skillsRoot ($_.Value.skill -replace '/', [IO.Path]::DirectorySeparatorChar)) -PathType Leaf)
})
if ($missingRouteSkills.Count -eq 0) { Ok 'routing.json: all route skills exist' } else { Bad "routing.json missing skill files: $($missingRouteSkills.Name -join ',')" }
$git = Get-Command git -ErrorAction SilentlyContinue
if ($git) {
$trackedSkills = @(& $git.Source -C $packageRoot ls-files -- 'skills/**/SKILL.md')
if ($LASTEXITCODE -eq 0) {
$untrackedRouteSkills = @($rjRoutes | Where-Object { ('skills/' + $_.Value.skill) -notin $trackedSkills })
if ($untrackedRouteSkills.Count -eq 0) { Ok 'routing.json: all route skills are tracked' } else { Bad "routing.json references untracked skills: $($untrackedRouteSkills.Name -join ',')" }
}
}
$routeIds = @($rjRoutes | ForEach-Object { $_.Name })
$missingPrio = @($routeIds | Where-Object { $_ -notin @($rj.priority) })
$extraPrio = @($rj.priority | Where-Object { $_ -notin $routeIds })
if ($missingPrio.Count -eq 0 -and $extraPrio.Count -eq 0) { Ok 'routing.json priority covers all routes (1:1)' } else { Bad "routing.json priority mismatch: missing=$($missingPrio -join ',') extra=$($extraPrio -join ',')" }
} else {
Bad 'skills/config/routing.json missing (single source of truth)'
}
$benchJson = Join-Path $skillsRoot 'tests/routing-benchmark.json'
if (Test-Path -LiteralPath $benchJson) {
$bj = Get-Content -LiteralPath $benchJson -Raw -Encoding UTF8 | ConvertFrom-Json
$bjCases = @($bj.cases)
if ($bjCases.Count -ge 100) { Ok "benchmark cases=$($bjCases.Count)" } else { Bad "benchmark cases < 100 ($($bjCases.Count))" }
$badExpect = @($bjCases | Where-Object { $_.expect -notmatch '^R\d+$' })
if ($badExpect.Count -eq 0) { Ok 'benchmark expect ids well-formed' } else { Bad "benchmark bad expect: $($badExpect.Count)" }
# benchmark expect 必须存在于 routing.json(防 benchmark 引用已删除的路由)
if (Test-Path -LiteralPath $routingJson) {
$rjIds = @($rjRoutes | ForEach-Object { $_.Name })
$ghostExpect = @($bjCases | Where-Object { $_.expect -notin $rjIds })
if ($ghostExpect.Count -eq 0) { Ok 'benchmark expects all exist in routing.json' } else { Bad "benchmark ghost expects: $(($ghostExpect | Select-Object -First 5).expect -join ',')" }
}
} else {
Bad 'skills/tests/routing-benchmark.json missing'
}
if (Test-Path -LiteralPath (Join-Path $skillsRoot 'INDEX.md')) { Ok 'INDEX.md present (generated)' } else { Bad 'INDEX.md missing (run extract-summaries.ps1)' }
# master-route.ps1 不得回退到硬编码路由表(防绕过 routing.json)
$mrText = Get-Content -LiteralPath (Join-Path $scriptDir 'master-route.ps1') -Raw -Encoding UTF8
if ($mrText -match '\$map\s*=\s*\[ordered\]' -or $mrText -match "R1'\s*=\s*'apk-reverse") {
Bad 'master-route.ps1 contains hardcoded routing table (must read routing.json)'
} else {
Ok 'master-route.ps1 has no hardcoded routing table'
}
# --- ops artifacts exist ---
$opsFiles = @(
'ops/IDENTITY.md',
'ops/scope-contract.md',
'ops/evidence-finding-path.md',
'ops/role-map.md',
'ops/timeline-workitem.md',
'ops/sandbox-profile.md',
'ops/skill-supply-chain.md',
'ops/README.md',
'references/community-security-skills.md',
'references/domain-coverage-map.md',
'attack-chain\references\lifecycle-checklist.md',
'reverse-engineering/references\re-agent-workflow.md',
'pentest-tools/references\recon-pipeline.md',
'MASTER-ROUTING.md',
'scripts\master-route.ps1',
'scripts\case-init.ps1',
'scripts\lib\WorkRoot.ps1',
'case-review/SKILL.md',
'case-review/scripts/review_case.py',
'docs-generator/references\security-report-templates.md',
'field-journal/_template.md'
)
$indexLines = New-Object System.Collections.Generic.List[string]
foreach ($rel in $opsFiles) {
$p = Join-Path $skillsRoot $rel
if (Test-Path -LiteralPath $p) {
Ok "artifact $rel"
[void]$indexLines.Add("OK $rel")
} else {
Bad "missing $rel"
[void]$indexLines.Add("MISS $rel")
}
}
$indexLines | Set-Content -LiteralPath (Join-Path $ScratchDir 'artifacts-index.txt') -Encoding UTF8
# --- links from hubs (skills + RULES single source) ---
foreach ($hub in @('MASTER-ROUTING.md', 'SKILL.md', 'routing.md')) {
$t = Get-Content (Join-Path $skillsRoot $hub) -Raw -Encoding UTF8
if ($t -match 'ops/scope-contract|ops\\scope-contract|case-init') { Ok "hub link scope in $hub" }
else { Bad "hub $hub missing scope/case-init link" }
if ($t -match 'ops/IDENTITY|IDENTITY\.md') { Ok "hub identity $hub" }
else { Bad "hub $hub missing IDENTITY" }
}
# research deposits must be reachable from hubs
$hubAll = ''
foreach ($hub in @('MASTER-ROUTING.md', 'SKILL.md', 'ops/README.md', 'routing.md')) {
$hp = Join-Path $skillsRoot $hub
if (Test-Path $hp) { $hubAll += (Get-Content $hp -Raw -Encoding UTF8) }
}
foreach ($n in @('community-security-skills', 'skill-supply-chain', 're-agent-workflow', 'recon-pipeline')) {
if ($hubAll -match [regex]::Escape($n)) { Ok "hub surfaces $n" }
else { Bad "hub missing surface for $n" }
}
# RULES.md / RULES_zh.md MUST gate case-init/scope before ACT (injection + CRITICAL + chain)
$rulesEn = Join-Path $packageRoot 'RULES.md'
$rulesZh = Join-Path $packageRoot 'RULES_zh.md'
foreach ($rp in @($rulesEn, $rulesZh)) {
$name = Split-Path $rp -Leaf
if (-not (Test-Path -LiteralPath $rp)) { Bad "missing $name"; continue }
$rt = Get-Content -LiteralPath $rp -Raw -Encoding UTF8
if ($rt -match 'case-init' -and ($rt -match 'scope-contract|scope\.md|network_profile')) {
Ok "$name has case-init/scope gate"
} else {
Bad "$name missing case-init/scope/network_profile gate"
}
# Compact or CRITICAL must not jump routing→ACT without scope
if ($rt -match 'auth\.status\s*=\s*granted|auth.status=granted|未就绪禁止|MUST NOT ACT against targets|禁止对目标 ACT') {
Ok "$name has auth hard gate language"
} else {
Bad "$name missing auth hard-gate language"
}
# Post-trigger / 行为链: case-init before ACT pattern
if ($rt -match '(?s)case-init.{0,400}ACT|scope\.md.{0,400}ACT|scope-contract.{0,400}ACT') {
Ok "$name orders scope before ACT (nearby)"
} else {
Bad "$name does not place scope/case-init before ACT"
}
}
# --- template required headings ---
$fieldLog = New-Object System.Collections.Generic.List[string]
function Assert-Fields([string]$path, [string[]]$needles) {
$t = Get-Content $path -Raw -Encoding UTF8
foreach ($n in $needles) {
if ($t -match [regex]::Escape($n)) {
Ok "field '$n' in $(Split-Path $path -Leaf)"
[void]$fieldLog.Add("OK $n @ $path")
} else {
Bad "field '$n' missing in $path"
[void]$fieldLog.Add("MISS $n @ $path")
}
}
}
Assert-Fields (Join-Path $skillsRoot 'ops/scope-contract.md') @('auth', 'in_scope', 'out_of_scope', 'network_profile', 'deliverables')
Assert-Fields (Join-Path $skillsRoot 'ops/evidence-finding-path.md') @('Evidence', 'Finding', 'Path', 'repro_command', 'evidence_ids')
Assert-Fields (Join-Path $skillsRoot 'ops/timeline-workitem.md') @('timeline.md', 'workitems.md', 'Coverage')
Assert-Fields (Join-Path $skillsRoot 'ops/role-map.md') @('lead', 'cie', 'cpe', 'cre', 'Handoff')
Assert-Fields (Join-Path $skillsRoot 'ops/skill-supply-chain.md') @('AST10', 'MCP', 'bootstrap', 'MUST')
Assert-Fields (Join-Path $skillsRoot 'references/community-security-skills.md') @('trailofbits', 'agentskills.io', 'MUST', '2026-07')
Assert-Fields (Join-Path $skillsRoot 'reverse-engineering/references\re-agent-workflow.md') @('Triage', 'Static', 'Dynamic', 'Synthesis')
Assert-Fields (Join-Path $skillsRoot 'pentest-tools/references\recon-pipeline.md') @('auth.status', 'network_profile', 'Evidence', 'nuclei')
Assert-Fields (Join-Path $skillsRoot 'docs-generator/references\security-report-templates.md') @('Evidence Chain', 'Findings', 'Path')
Assert-Fields (Join-Path $skillsRoot 'field-journal/_template.md') @('Scope', 'Evidence', 'Finding')
Assert-Fields (Join-Path $skillsRoot 'case-review/SKILL.md') @('ACTION REQUIRED', 'review_case.py', 'Evidence Graph Review')
$fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8
# --- role map skills exist for primary rows ---
$roleDoc = Get-Content (Join-Path $skillsRoot 'ops/role-map.md') -Raw -Encoding UTF8
foreach ($sk in @('attack-chain', 'pentest-tools', 'ida-reverse', 'docs-generator', 'llm-security')) {
if ($roleDoc -match [regex]::Escape($sk)) { Ok "role-map mentions $sk" } else { Bad "role-map missing $sk" }
}
# --- master-route cases ---
$cases = @(
@{ N = 'dsl'; H = 'dsl vm reverse fireye'; Id = 'R4'; Sub = 'reverse-engineering/dsl-vm-reverse/SKILL.md' },
@{ N = 'apk'; H = 'apk jadx smali reverse'; Id = 'R1'; Sub = 'apk-reverse/SKILL.md' },
@{ N = 'malware'; H = 'malware yara sample analysis'; Id = 'R9'; Sub = 'malware-analysis/SKILL.md' },
@{ N = 'pentest'; H = 'nmap nuclei pentest sqlmap'; Id = 'R11'; Sub = 'pentest-tools/SKILL.md' },
@{ N = 'attack'; H = 'full pentest attack chain from external'; Id = 'R10'; Sub = 'attack-chain/SKILL.md' },
@{ N = 'protocol'; H = 'protobuf custom protocol reverse pcap'; Id = 'R21'; Sub = 'protocol-reverse/SKILL.md' },
@{ N = 'ghidra'; H = 'ghidra headless decompile'; Id = 'R22'; Sub = 'ghidra-reverse/SKILL.md' },
@{ N = 'cloud'; H = 'kubernetes k8s container escape'; Id = 'R23'; Sub = 'cloud-k8s/SKILL.md' },
@{ N = 'ad'; H = 'bloodhound kerberoast active directory'; Id = 'R24'; Sub = 'windows-ad/SKILL.md' },
@{ N = 'forensics'; H = 'volatility memory dump forensics'; Id = 'R25'; Sub = 'digital-forensics/SKILL.md' },
@{ N = 'codeaudit'; H = 'semgrep code audit sast'; Id = 'R26'; Sub = 'code-audit/SKILL.md' },
@{ N = 'hunt'; H = 'threat hunting detection engineering'; Id = 'R27'; Sub = 'threat-hunting/SKILL.md' },
@{ N = 'ot'; H = 'scada plc modbus industrial control'; Id = 'R28'; Sub = 'ot-ics/SKILL.md' },
@{ N = 'wifi'; H = 'wifi aircrack wireless pentest'; Id = 'R29'; Sub = 'wifi-wireless/SKILL.md' },
@{ N = 'extension'; H = 'chrome extension crx reverse'; Id = 'R30'; Sub = 'browser-extension-reverse/SKILL.md' },
@{ N = 'macos'; H = 'macos mach-o codesign reverse'; Id = 'R31'; Sub = 'macos-reverse/SKILL.md' },
@{ N = 'thick'; H = 'thick client electron desktop client'; Id = 'R32'; Sub = 'thick-client/SKILL.md' },
@{ N = 'gorust'; H = 'golang stripped go binary reverse'; Id = 'R33'; Sub = 'go-rust-reverse/SKILL.md' },
@{ N = 'hw'; H = 'uart jtag hardware debug pads'; Id = 'R34'; Sub = 'hardware-security/SKILL.md' },
@{ N = 'db'; H = 'database security mysql postgres redis'; Id = 'R35'; Sub = 'database-security/SKILL.md' },
@{ N = 'email'; H = 'phishing spf dkim dmarc email security'; Id = 'R36'; Sub = 'email-security/SKILL.md' },
@{ N = 'sso'; H = 'saml oidc sso federation'; Id = 'R37'; Sub = 'identity-federation/SKILL.md' },
@{ N = 'sdr'; H = 'sdr hackrf gnu radio rf'; Id = 'R38'; Sub = 'radio-sdr/SKILL.md' }
)
foreach ($c in $cases) {
$out = Join-Path $ScratchDir ("route-{0}" -f $c.N)
$stdout = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint $c.H -OutDir $out 2>&1 | Out-String
$stdout | Set-Content -LiteralPath (Join-Path $ScratchDir ("route-{0}.txt" -f $c.N)) -Encoding UTF8
$scope = Join-Path $out 'route-scope.md'
if (-not (Test-Path $scope)) { Bad "no scope $($c.N)"; continue }
$text = Get-Content $scope -Raw -Encoding UTF8
if ($text -notmatch ("primary: {0}" -f [regex]::Escape($c.Id))) { Bad "$($c.N) id want $($c.Id)" } else { Ok "$($c.N) -> $($c.Id)" }
$abs = Join-Path $skillsRoot ($c.Sub -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path $abs)) { Bad "missing $($c.Sub)" } else { Ok "exists $($c.Sub)" }
}
# default outdir under work
$def = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint 'radare2 analyze' 2>&1 | Out-String
$def | Set-Content (Join-Path $ScratchDir 'default-out.txt') -Encoding UTF8
if ($def -match 'work[\\/]master-route-') { Ok 'default OutDir under work/' } else { Bad 'default OutDir not under work/' }
# project-root output must stay with the analysis project when the skill is invoked elsewhere
$projectRoot = Join-Path $ScratchDir 'analysis-project'
New-Item -ItemType Directory -Force -Path $projectRoot | Out-Null
$projectRoute = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
-Hint 'radare2 analyze' -ProjectRoot $projectRoot 2>&1 | Out-String
$projectWork = Join-Path $projectRoot 'work'
$projectRouteDirs = @(Get-ChildItem -LiteralPath $projectWork -Directory -Filter 'master-route-*' -ErrorAction SilentlyContinue)
if ($projectRouteDirs.Count -eq 1 -and (Test-Path (Join-Path $projectRouteDirs[0].FullName 'route-scope.md'))) {
Ok 'explicit ProjectRoot keeps route artifacts in analysis project'
} else {
Bad 'explicit ProjectRoot did not receive route artifacts'
}
$defaultProjectRoot = Join-Path $ScratchDir 'default-analysis-project'
New-Item -ItemType Directory -Force -Path $defaultProjectRoot | Out-Null
$previousLocation = Get-Location
try {
Set-Location -LiteralPath $defaultProjectRoot
$defaultProjectRoute = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
-Hint 'radare2 analyze' 2>&1 | Out-String
} finally {
Set-Location -LiteralPath $previousLocation
}
$defaultProjectWork = Join-Path $defaultProjectRoot 'work'
$defaultProjectRoutes = @(Get-ChildItem -LiteralPath $defaultProjectWork -Directory -Filter 'master-route-*' -ErrorAction SilentlyContinue)
if ($defaultProjectRoutes.Count -eq 1 -and (Test-Path (Join-Path $defaultProjectRoutes[0].FullName 'route-scope.md'))) {
Ok 'default route artifacts follow the caller project'
} else {
Bad 'default route artifacts did not follow the caller project'
}
# case-init real path
$caseName = 'verify-ops-' + (Get-Date -Format 'HHmmss')
$ci = & powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit -Hint 'apk jadx reverse' -CaseName $caseName -PackageRoot $packageRoot 2>&1 | Out-String
$ci | Set-Content (Join-Path $ScratchDir 'case-init.txt') -Encoding UTF8
$caseRoot = Join-Path $packageRoot ("work/{0}" -f $caseName)
foreach ($f in @('scope.md', 'timeline.md', 'workitems.md')) {
$fp = Join-Path $caseRoot $f
if (Test-Path $fp) { Ok "case-init $f" } else { Bad "case-init missing $f" }
}
if (Test-Path (Join-Path $caseRoot 'scope.md')) {
$sc = Get-Content (Join-Path $caseRoot 'scope.md') -Raw -Encoding UTF8
foreach ($k in @('auth', 'network_profile', 'in_scope', 'ready_for_act')) {
if ($sc -match $k) { Ok "case scope has $k" } else { Bad "case scope missing $k" }
}
}
$projectCaseName = 'verify-project-root-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $projectCaseName -PackageRoot $packageRoot `
-ProjectRoot $projectRoot 2>&1 | Out-Null
$projectCaseRoot = Join-Path $projectWork $projectCaseName
if ((Test-Path (Join-Path $projectCaseRoot 'scope.md')) -and
(Test-Path (Join-Path $projectCaseRoot 'timeline.md')) -and
(Test-Path (Join-Path $projectCaseRoot 'workitems.md'))) {
Ok 'explicit ProjectRoot keeps case artifacts in analysis project'
} else {
Bad 'explicit ProjectRoot did not receive case artifacts'
}
$defaultCaseName = 'verify-default-project-' + (Get-Date -Format 'HHmmss')
try {
Set-Location -LiteralPath $defaultProjectRoot
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $defaultCaseName 2>&1 | Out-Null
} finally {
Set-Location -LiteralPath $previousLocation
}
$defaultCaseRoot = Join-Path (Join-Path $defaultProjectRoot 'work') $defaultCaseName
if ((Test-Path (Join-Path $defaultCaseRoot 'scope.md')) -and
(Test-Path (Join-Path $defaultCaseRoot 'timeline.md')) -and
(Test-Path (Join-Path $defaultCaseRoot 'workitems.md'))) {
Ok 'default case artifacts follow the caller project'
} else {
Bad 'default case artifacts did not follow the caller project'
}
# ghost dsl
foreach ($rel in @('SKILL.md', 'routing.md', 'MASTER-ROUTING.md', 'scripts\master-route.ps1')) {
$p = Join-Path $skillsRoot $rel
if (-not (Test-Path $p)) { continue }
$t = Get-Content $p -Raw -Encoding UTF8
if ($t -match '`dsl-vm-reverse/' -and $t -notmatch 'reverse-engineering/dsl-vm-reverse') {
Bad "ghost dsl path in $rel"
}
}
Ok 'ghost dsl scan done'
# refresh-tool-index parses
$e = $null
[void][System.Management.Automation.Language.Parser]::ParseFile((Join-Path $scriptDir 'refresh-tool-index.ps1'), [ref]$null, [ref]$e)
if ($e -and $e.Count -gt 0) { Bad ("refresh-tool-index parse: {0}" -f $e[0]) } else { Ok 'refresh-tool-index parses' }
# --- bootstrap-manifest parity (skills vs kali) ---
$skillsManifest = Join-Path $scriptDir 'bootstrap-manifest.json'
$kaliManifest = Join-Path $packageRoot 'kali/scripts/bootstrap-manifest.json'
$skillsCaps = @()
if (Test-Path -LiteralPath $skillsManifest) {
$sm = Get-Content -LiteralPath $skillsManifest -Raw -Encoding UTF8 | ConvertFrom-Json
$skillsCaps = @($sm.capabilities | ForEach-Object { $_.name })
if ($skillsCaps.Count -ge 10) { Ok "skills manifest $($skillsCaps.Count) capabilities" } else { Bad 'skills manifest capability count suspicious' }
} else {
Bad 'skills bootstrap-manifest.json missing'
}
if (Test-Path -LiteralPath $kaliManifest) {
$km = Get-Content -LiteralPath $kaliManifest -Raw -Encoding UTF8 | ConvertFrom-Json
$kaliCaps = @($km.capabilities | ForEach-Object { $_.name })
foreach ($missing in ($skillsCaps | Where-Object { $_ -notin $kaliCaps })) {
Bad "kali manifest missing capability: $missing"
}
foreach ($missing in ($kaliCaps | Where-Object { $_ -notin $skillsCaps })) {
Ok "kali-only capability: $missing"
}
} else {
Bad 'kali bootstrap-manifest.json missing'
}
# --- supply-chain pin gate: auto-install download sources MUST be pinned ---
# 统一判定:pinnedVersion / pinnedCommit / pinPolicy 三选一;
# github-release-* 额外接受 assetSha256 / preferApiDigest(GitHub 官方发布资产哈希)。
$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 ($cap in $mc.capabilities) {
if (-not $cap.canAutoInstall) { continue }
$hasPin = ($cap.pinnedVersion -or $cap.pinnedCommit -or $cap.pinPolicy)
switch ($cap.bootstrapKind) {
'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 } # 本地服务,不下载
'winget-package' { $hasPin = $hasPin } # winget-latest 属于 pinPolicy
'apt-package' { $hasPin = $true } # 发行版仓库自带(Kali 侧)
'docker-image' { $hasPin = $true } # fallback 通道
'manual' { $hasPin = $true } # 手工安装
default { $hasPin = $hasPin }
}
if (-not $hasPin) {
Bad "unpinned auto-install capability: $($cap.name) in $mn ($($cap.bootstrapKind))"
} else {
Ok "pinned $($cap.name) in $mn"
}
}
}
# identity: no FastAPI/React requirement in ops IDENTITY
$id = Get-Content (Join-Path $skillsRoot 'ops/IDENTITY.md') -Raw -Encoding UTF8
if ($id -match '不是|不做|NOT|not a Z3r0|FastAPI|React') { Ok 'identity distinguishes platform' } else { Bad 'identity weak' }
if ($id -match 'tool-index|bootstrap|field-journal|路由') { Ok 'identity keeps reverse-skill DNA' } else { Bad 'identity missing DNA' }
$idCheck = @()
$idCheck += "HEAD packageRoot=$packageRoot"
$idCheck += "fastapi-in-ops-deps=false"
$idCheck -join [Environment]::NewLine | Set-Content (Join-Path $ScratchDir 'identity-check.txt') -Encoding UTF8
Ok 'identity-check written'
Write-Host "Scratch=$ScratchDir"
if ($fail.Count -gt 0) {
Write-Host ("FAILED {0}" -f $fail.Count) -ForegroundColor Red
$fail | ForEach-Object { Write-Host " - $_" }
$fail | Set-Content (Join-Path $ScratchDir 'failures.txt') -Encoding UTF8
exit 1
}
Write-Host 'ALL ROUTING COHERENCE CHECKS PASSED' -ForegroundColor Green
'ALL ROUTING COHERENCE CHECKS PASSED' | Set-Content (Join-Path $ScratchDir 'verify.txt') -Encoding UTF8
exit 0