404 lines
22 KiB
PowerShell
404 lines
22 KiB
PowerShell
#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
|