CODEX KNOWLEDGE

ps 的gateway

2026/04/24 104 min read CODEX KNOWLEDGE PS 的GATEWAY

[CmdletBinding()] param( [Parameter(ParameterSetName = 'Launch', ValueFromRemainingArguments = $true)] [string[]]$CodexArgs = @(), [Parameter(ParameterSetName = 'Postflight', Mandatory = $true)] [switch]$Postflight, [Parameter(ParameterSetName = 'Postflight', Mandatory = $true)] [string]$GateId, [Parameter(ParameterSetName = 'Postflight')] [string]$PlanId = '', [Parameter(ParameterSetName = 'Postflight', Mandatory = $true)] [string]$StartedAt, [Parameter(ParameterSetName = 'Postflight')] [string]$SessionFile = '', [Parameter(ParameterSetName = 'Postflight')] [string]$RequireDeploy = '0', [Parameter(ParameterSetName = 'Postflight')] [string]$RequireLiveVerification = '0', [Parameter(ParameterSetName = 'Postflight')] [int]$ChildExitCode = 0, [Parameter(ParameterSetName = 'Postflight')] [string]$TargetCwd = '' )

$ErrorActionPreference = 'Stop'

if ($PSVersionTable.PSVersion.Major -lt 6) { $pwsh = Get-Command -Name pwsh -ErrorAction SilentlyContinue if ($pwsh) { $relayArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath) if ($PSCmdlet.ParameterSetName -eq 'Postflight') { $relayArgs += @( '-Postflight', '-GateId', $GateId, '-StartedAt', $StartedAt, '-ChildExitCode', $ChildExitCode, '-PlanId', $PlanId, '-SessionFile', $SessionFile, '-RequireDeploy', $RequireDeploy, '-RequireLiveVerification', $RequireLiveVerification, '-TargetCwd', $TargetCwd ) } else { $relayArgs += '-CodexArgs' $relayArgs += @($CodexArgs) }

    & $pwsh.Source @relayArgs
    exit $LASTEXITCODE
}

}

$codexHome = 'C:\Users\ASUS-KL.codex' $routeConfigPath = Join-Path $codexHome 'rules\execution-gateway.routes.json' $statePath = Join-Path $codexHome '.tmp\execution-gateway\state.json' $preflightOutputPath = Join-Path $codexHome '.tmp\execution-gateway\last-preflight.txt' $sessionsRoot = Join-Path $codexHome 'sessions' $stateTtlHours = 8 $industrialControlRoot = Join-Path $codexHome 'industrial-control' $industrialClassifyTaskScript = Join-Path $industrialControlRoot 'scripts\classify-task.ps1' $industrialWriteLedgerScript = Join-Path $industrialControlRoot 'scripts\write-ledger.ps1'

function Get-ForwardArgs { if (-not [string]::IsNullOrWhiteSpace($env:CODEX_FORWARD_ARGS_JSON)) { try { $parsed = ConvertFrom-JsonCompat -InputObject $env:CODEX_FORWARD_ARGS_JSON -Depth 8 if ($parsed -is [System.Array]) { return @($parsed | ForEach-Object { [string]$_ }) } } catch { throw "Invalid CODEX_FORWARD_ARGS_JSON payload: $($_.Exception.Message)" } }

return @($CodexArgs)

}

function Invoke-IndustrialTaskClassification { param([string]$TaskSummary)

if ([string]::IsNullOrWhiteSpace($taskSummary)) { Write-IndustrialRunLedger -TaskSummary 'interactive_session_start' -Level 'L0' -Decision 'allow' -Reason 'interactive mode' -TargetCwd $targetCwd $result = New-GatewayResult -Allow $true -Message 'Codex interactive mode (controlled)' -ExecArgs @() $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

function Write-IndustrialRunLedger { param( [string]$TaskSummary, [string]$Level, [string]$Decision, [string]$Reason, [string]$PlanId = '', [string]$Route = '', [string]$TargetCwd = '' )

if (-not (Test-Path -LiteralPath $industrialWriteLedgerScript)) {
    return
}

& powershell -ExecutionPolicy Bypass -File $industrialWriteLedgerScript -Task $TaskSummary -Level $Level -Decision $Decision -Reason $Reason -PlanId $PlanId -Route $Route -TargetCwd $TargetCwd | Out-Null

}

function New-GatewayResult { param( [bool]$Allow, [string]$Message, [string[]]$ExecArgs, [hashtable]$Env = @{}, [hashtable]$Meta = @{} )

[pscustomobject]@{
    allow = $Allow
    message = $Message
    execArgs = @($ExecArgs)
    env = $Env
    meta = $Meta
}

}

function Normalize-AbsolutePath { param([string]$Path)

if ([string]::IsNullOrWhiteSpace($Path)) {
    return $null
}

try {
    return [System.IO.Path]::GetFullPath($Path)
} catch {
    return $Path
}

}

function Test-PathPrefix { param( [string]$Path, [string]$Prefix )

if ([string]::IsNullOrWhiteSpace($Path) -or [string]::IsNullOrWhiteSpace($Prefix)) {
    return $false
}

$normalizedPath = (Normalize-AbsolutePath $Path).TrimEnd('\')
$normalizedPrefix = (Normalize-AbsolutePath $Prefix).TrimEnd('\')

return $normalizedPath.StartsWith($normalizedPrefix, [System.StringComparison]::OrdinalIgnoreCase)

}

function Get-GatewayDirectiveFromRepoRoot { param([string]$RepoRoot)

if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
    return $null
}

$jsonPath = Join-Path $RepoRoot '.codex-project.json'
if (Test-Path -LiteralPath $jsonPath) {
    try {
        $json = ConvertFrom-JsonCompat -InputObject (Read-TextUtf8 -Path $jsonPath) -Depth 8
        if ($json.executionGateway -and $json.executionGateway.mode) {
            return [pscustomobject]@{
                mode = [string]$json.executionGateway.mode
                source = $jsonPath
            }
        }
    } catch {
        # Fall back to AGENTS.md parsing below.
    }
}

$agentsPath = Join-Path $RepoRoot 'AGENTS.md'
if (Test-Path -LiteralPath $agentsPath) {
    $match = [regex]::Match(
        (Read-TextUtf8 -Path $agentsPath),
        '(?im)^\s*(?:-|\*)?\s*CodexGateway:\s*(project-plan-context|disabled)\s*$'
    )
    if ($match.Success) {
        return [pscustomobject]@{
            mode = $match.Groups[1].Value.Trim().ToLowerInvariant()
            source = $agentsPath
        }
    }
}

return $null

}

function Find-GatewayProjectProfile { param([string]$StartPath)

if ([string]::IsNullOrWhiteSpace($StartPath)) {
    return $null
}

$current = Normalize-AbsolutePath $StartPath
if ([string]::IsNullOrWhiteSpace($current)) {
    return $null
}

if (Test-Path -LiteralPath $current -PathType Leaf) {
    $current = Split-Path -Parent $current
}

while (-not [string]::IsNullOrWhiteSpace($current)) {
    $directive = Get-GatewayDirectiveFromRepoRoot -RepoRoot $current
    $codexRoot = Join-Path $current 'codex'
    $projectContextPath = Join-Path $codexRoot 'PROJECT-CONTEXT.md'
    $sessionHandoffPath = Join-Path $codexRoot 'SESSION-HANDOFF.md'
    $planPath = Join-Path $codexRoot 'plugins\obsidian\data\docs\agent\plan.md'
    $planPreflightScript = Join-Path $codexRoot 'skills\plan-history-recall\scripts\invoke-plan-preflight.ps1'
    $appendPlanScript = Join-Path $codexRoot 'skills\plan-history-recall\scripts\append-plan.ps1'
    $hasCanonicalLayout =
        (Test-Path -LiteralPath $projectContextPath) -and
        (Test-Path -LiteralPath $sessionHandoffPath) -and
        (Test-Path -LiteralPath $planPreflightScript) -and
        (Test-Path -LiteralPath $appendPlanScript)

    if ($directive -and $directive.mode -eq 'disabled') {
        return [pscustomobject]@{
            enabled = $false
            profileComplete = $false
            repoRoot = $current
            repoLabel = (Split-Path -Path $current -Leaf)
            codexRoot = $codexRoot
            directiveMode = $directive.mode
            directiveSource = $directive.source
        }
    }

    if ($directive -and $directive.mode -eq 'project-plan-context') {
        return [pscustomobject]@{
            enabled = $true
            profileComplete = $hasCanonicalLayout
            repoRoot = $current
            codexRoot = $codexRoot
            repoLabel = (Split-Path -Path $current -Leaf)
            directiveMode = $directive.mode
            directiveSource = $directive.source
            projectContextPath = $projectContextPath
            sessionHandoffPath = $sessionHandoffPath
            planPath = $planPath
            planPreflightScript = $planPreflightScript
            appendPlanScript = $appendPlanScript
        }
    }

    if ($hasCanonicalLayout) {
        return [pscustomobject]@{
            enabled = $true
            profileComplete = $true
            repoRoot = $current
            codexRoot = $codexRoot
            repoLabel = (Split-Path -Path $current -Leaf)
            directiveMode = 'implicit-canonical-layout'
            directiveSource = ''
            projectContextPath = $projectContextPath
            sessionHandoffPath = $sessionHandoffPath
            planPath = $planPath
            planPreflightScript = $planPreflightScript
            appendPlanScript = $appendPlanScript
        }
    }

    $parent = Split-Path -Parent $current
    if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $current) {
        break
    }

    $current = $parent
}

return $null

}

function Get-HashString { param([string]$Value)

$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$sha = [System.Security.Cryptography.SHA256]::Create()
try {
    $hashBytes = $sha.ComputeHash($bytes)
} finally {
    $sha.Dispose()
}

return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()

}

function Ensure-ParentDirectory { param([string]$Path)

$directory = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($directory) -and -not (Test-Path -LiteralPath $directory)) {
    New-Item -ItemType Directory -Path $directory -Force | Out-Null
}

}

function ConvertFrom-JsonCompat { param( [Parameter(Mandatory = $true)] [string]$InputObject, [int]$Depth = 8 )

$convertFromJson = Get-Command -Name ConvertFrom-Json -ErrorAction Stop
if ($convertFromJson.Parameters.ContainsKey('Depth')) {
    return $InputObject | Microsoft.PowerShell.Utility\ConvertFrom-Json -Depth $Depth
}

return $InputObject | Microsoft.PowerShell.Utility\ConvertFrom-Json

}

function Read-TextUtf8 { param([string]$Path)

return Get-Content -LiteralPath $Path -Raw -Encoding utf8

}

function Read-LinesUtf8 { param([string]$Path)

return Get-Content -LiteralPath $Path -Encoding utf8

}

function Get-State { if (-not (Test-Path -LiteralPath $statePath)) { return [pscustomobject]@{ tasks = [pscustomobject]@{} } }

try {
    $raw = Read-TextUtf8 -Path $statePath
    if ([string]::IsNullOrWhiteSpace($raw)) {
        return [pscustomobject]@{ tasks = [pscustomobject]@{} }
    }

    $parsed = ConvertFrom-JsonCompat -InputObject $raw -Depth 8
    if (-not $parsed.tasks) {
        $parsed | Add-Member -NotePropertyName tasks -NotePropertyValue ([pscustomobject]@{})
    }
    return $parsed
} catch {
    return [pscustomobject]@{ tasks = [pscustomobject]@{} }
}

}

function Save-State { param([pscustomobject]$State)

Ensure-ParentDirectory $statePath
$State | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $statePath -Encoding utf8

}

function Get-RouteConfig { if (-not (Test-Path -LiteralPath $routeConfigPath)) { return $null }

return ConvertFrom-JsonCompat -InputObject (Read-TextUtf8 -Path $routeConfigPath) -Depth 8

}

function Test-TextMatchesPatterns { param( [string]$Text, [object[]]$Patterns )

if ([string]::IsNullOrWhiteSpace($Text) -or -not $Patterns) {
    return $false
}

foreach ($pattern in $Patterns) {
    if ($Text -match [string]$pattern) {
        return $true
    }
}

return $false

}

function ConvertTo-BoolFlag { param([string]$Value)

return $Value -in @('1', 'true', 'True', 'TRUE', 'yes', 'Yes', 'YES')

}

function Get-CloseoutRequirements { param([string]$TaskSummary)

$config = Get-RouteConfig
$deployPatterns = @()
$livePatterns = @()

if ($config -and $config.closeoutGates) {
    if ($config.closeoutGates.deploy -and $config.closeoutGates.deploy.patterns) {
        $deployPatterns = @($config.closeoutGates.deploy.patterns)
    }
    if ($config.closeoutGates.liveVerification -and $config.closeoutGates.liveVerification.patterns) {
        $livePatterns = @($config.closeoutGates.liveVerification.patterns)
    }
}

[pscustomobject]@{
    requireDeploy = (Test-TextMatchesPatterns -Text $TaskSummary -Patterns $deployPatterns)
    requireLiveVerification = (Test-TextMatchesPatterns -Text $TaskSummary -Patterns $livePatterns)
}

}

function Get-TaskRouteDecision { param([string]$TaskSummary)

$config = Get-RouteConfig
if (-not $config) {
    return $null
}

foreach ($route in $config.routes) {
    foreach ($pattern in $route.patterns) {
        if ($TaskSummary -match $pattern) {
            return [pscustomobject]@{
                id = $route.id
                kind = $route.kind
                requiredTool = $route.requiredTool
                directive = $route.directive
                defaultDirective = $config.defaultDirective
            }
        }
    }
}

return [pscustomobject]@{
    id = 'default-gated'
    kind = 'gate'
    requiredTool = ''
    directive = $config.defaultDirective
    defaultDirective = $config.defaultDirective
}

}

function Get-TitleFromTask { param([string]$TaskSummary)

$clean = ($TaskSummary -replace '\s+', ' ').Trim()
if ($clean.Length -le 42) {
    return "Gateway: $clean"
}

return "Gateway: $($clean.Substring(0, 42).TrimEnd())..."

}

function ConvertTo-PlanTargets { param( [pscustomobject]$GatewayProfile, [string]$TargetCwd )

$targets = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace($TargetCwd)) {
    [void]$targets.Add((Normalize-AbsolutePath $TargetCwd))
}

if ($GatewayProfile -and (Test-Path -LiteralPath $GatewayProfile.planPath)) {
    [void]$targets.Add($GatewayProfile.planPath)
}

return @($targets)

}

function Invoke-PlanPreflight { param( [pscustomobject]$GatewayProfile, [string]$TaskSummary, [string]$TargetCwd )

if (-not $GatewayProfile -or -not (Test-Path -LiteralPath $GatewayProfile.planPreflightScript)) {
    throw "Plan preflight script not found: $($GatewayProfile.planPreflightScript)"
}

Ensure-ParentDirectory $preflightOutputPath
$targets = ConvertTo-PlanTargets -GatewayProfile $GatewayProfile -TargetCwd $TargetCwd
$preflightOutput = & $GatewayProfile.planPreflightScript -Query $TaskSummary -Targets $targets 2>&1
Set-Content -LiteralPath $preflightOutputPath -Value ($preflightOutput -join [Environment]::NewLine) -Encoding utf8

}

function Ensure-PlanRecord { param( [pscustomobject]$GatewayProfile, [string]$TaskSummary, [string]$TargetCwd )

Invoke-PlanPreflight -GatewayProfile $GatewayProfile -TaskSummary $TaskSummary -TargetCwd $TargetCwd

if (-not $GatewayProfile -or -not (Test-Path -LiteralPath $GatewayProfile.appendPlanScript)) {
    throw "Plan append script not found: $($GatewayProfile.appendPlanScript)"
}

$taskHashInput = "{0}|{1}" -f (Normalize-AbsolutePath $TargetCwd), ($TaskSummary.Trim())
$taskHash = Get-HashString $taskHashInput
$state = Get-State
$existing = $null

if ($state.tasks.PSObject.Properties.Name -contains $taskHash) {
    $existing = $state.tasks.$taskHash
}

if ($existing) {
    try {
        $parsedTimestamp = [DateTimeOffset]::Parse($existing.timestamp)
        if (((Get-Date).ToUniversalTime() - $parsedTimestamp.UtcDateTime).TotalHours -lt $stateTtlHours) {
            return [pscustomobject]@{
                gateId = $existing.planId
                planId = $existing.planId
                taskHash = $taskHash
                planBound = $true
                reused = $true
            }
        }
    } catch {
        # Ignore malformed timestamps and regenerate the state entry below.
    }
}

$planId = 'PLAN-{0}-GATE-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmss'), $taskHash.Substring(0, 8)
$planStatusInProgress = [string]::Concat([char]0x8FDB, [char]0x884C, [char]0x4E2D)
& $GatewayProfile.appendPlanScript `
    -Title (Get-TitleFromTask -TaskSummary $TaskSummary) `
    -Task $TaskSummary `
    -PlanId $planId `
    -Targets (ConvertTo-PlanTargets -GatewayProfile $GatewayProfile -TargetCwd $TargetCwd) `
    -Assumptions @(
        'This plan entry is auto-created by the execution gateway so plan creation becomes a hard pre-execution gate for repos with canonical context.',
        'If the task expands later, keep appending status updates under the same plan ID instead of opening a parallel plan.'
    ) `
    -ReferencePlans @() `
    -AvoidRepeating @(
        'Do not start execution before plan.md has a bound entry for this task.',
        'Do not replace the required MCP/skill/verification path with explanation-only output.'
    ) `
    -Steps @(
        'Read the repo context and recent plan history to confirm this task boundary.',
        'Choose the tool path required by the execution gateway route and execute it.',
        'Finish real verification, then write back handoff or a plan status update.'
    ) `
    -VerificationCriteria @(
        'A plan.md entry exists for the current task.',
        'Task closeout must include a verification conclusion instead of stopping at explanation-only output.'
    ) `
    -Status $planStatusInProgress 6>$null | Out-Null

$state.tasks | Add-Member -NotePropertyName $taskHash -NotePropertyValue ([pscustomobject]@{
    planId = $planId
    task = $TaskSummary
    cwd = (Normalize-AbsolutePath $TargetCwd)
    timestamp = (Get-Date).ToString('o')
}) -Force
Save-State -State $state

return [pscustomobject]@{
    gateId = $planId
    planId = $planId
    taskHash = $taskHash
    planBound = $true
    reused = $false
}

}

function New-CloseoutGateRecord { param( [string]$TaskSummary, [string]$TargetCwd )

$taskHashInput = "{0}|{1}" -f (Normalize-AbsolutePath $TargetCwd), ($TaskSummary.Trim())
$taskHash = Get-HashString $taskHashInput
$gateId = 'GATE-{0}-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmss'), $taskHash.Substring(0, 8)

return [pscustomobject]@{
    gateId = $gateId
    planId = ''
    taskHash = $taskHash
    planBound = $false
    reused = $false
}

}

function Get-SessionCandidateFiles { param( [string]$StartedAt, [string]$GateId )

if (-not (Test-Path -LiteralPath $sessionsRoot)) {
    return @()
}

$startedAtUtc = [DateTimeOffset]::Parse($StartedAt).UtcDateTime.AddMinutes(-5)
$files = Get-ChildItem -LiteralPath $sessionsRoot -Recurse -File |
    Where-Object { $_.LastWriteTimeUtc -ge $startedAtUtc } |
    Sort-Object LastWriteTimeUtc -Descending

$matched = New-Object System.Collections.Generic.List[System.IO.FileInfo]
foreach ($file in $files) {
    try {
        if (Select-String -LiteralPath $file.FullName -SimpleMatch -Pattern "GateId: $GateId" -Quiet) {
            [void]$matched.Add($file)
        }
    } catch {
        continue
    }
}

return @($matched)

}

function Get-LastAgentMessageFromSessionFile { param([string]$Path)

if (-not (Test-Path -LiteralPath $Path)) {
    return $null
}

$lastMessage = $null
foreach ($line in Read-LinesUtf8 -Path $Path) {
    if ([string]::IsNullOrWhiteSpace($line)) {
        continue
    }

    try {
        $entry = ConvertFrom-JsonCompat -InputObject $line -Depth 20
    } catch {
        continue
    }

    if ($entry.type -eq 'event_msg' -and $entry.payload -and $entry.payload.type -eq 'task_complete') {
        if (-not [string]::IsNullOrWhiteSpace($entry.payload.last_agent_message)) {
            $lastMessage = [string]$entry.payload.last_agent_message
        }
    }
}

return $lastMessage

}

function Get-CloseoutFields { param([string]$Message)

$fields = [ordered]@{
    deployDecision = ''
    deployEvidence = ''
    liveVerificationDecision = ''
    liveVerificationArtifact = ''
}

if ([string]::IsNullOrWhiteSpace($Message)) {
    return [pscustomobject]$fields
}

$patterns = @{
    deployDecision = '(?im)^\s*DeployDecision:\s*(.+?)\s*$'
    deployEvidence = '(?im)^\s*DeployEvidence:\s*(.+?)\s*$'
    liveVerificationDecision = '(?im)^\s*LiveVerificationDecision:\s*(.+?)\s*$'
    liveVerificationArtifact = '(?im)^\s*LiveVerificationArtifact:\s*(.+?)\s*$'
}

foreach ($name in $patterns.Keys) {
    $match = [regex]::Match($Message, $patterns[$name])
    if ($match.Success) {
        $fields[$name] = $match.Groups[1].Value.Trim()
    }
}

return [pscustomobject]$fields

}

function Test-CloseoutGate { param( [string]$Decision, [string[]]$Allowed, [string]$SuccessValue, [string]$ArtifactValue )

$normalizedDecision = $Decision.Trim().ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($normalizedDecision)) {
    return [pscustomobject]@{
        passed = $false
        reason = 'missing decision'
    }
}

if ($Allowed -notcontains $normalizedDecision) {
    return [pscustomobject]@{
        passed = $false
        reason = "invalid decision '$Decision'"
    }
}

if ($normalizedDecision -eq $SuccessValue -and [string]::IsNullOrWhiteSpace($ArtifactValue)) {
    return [pscustomobject]@{
        passed = $false
        reason = "missing artifact for decision '$Decision'"
    }
}

return [pscustomobject]@{
    passed = $true
    reason = 'ok'
}

}

function Invoke-PostflightCheck { param( [string]$GateId, [string]$PlanId, [string]$StartedAt, [string]$SessionFile, [bool]$RequireDeploy, [bool]$RequireLiveVerification, [int]$ChildExitCode )

if ($ChildExitCode -ne 0) {
    return New-GatewayResult -Allow $true -Message 'postflight skipped because child exited non-zero' -ExecArgs @() -Meta @{
        gateId = $GateId
        skipped = $true
    }
}

$candidatePath = $SessionFile
if ([string]::IsNullOrWhiteSpace($candidatePath)) {
    $candidatePath = (Get-SessionCandidateFiles -StartedAt $StartedAt -GateId $GateId | Select-Object -First 1 -ExpandProperty FullName)
}

if ([string]::IsNullOrWhiteSpace($candidatePath)) {
    return New-GatewayResult -Allow $false -Message "execution gateway closeout failed: no session file found for $GateId" -ExecArgs @() -Meta @{
        gateId = $GateId
        planId = $PlanId
    }
}

$lastAgentMessage = Get-LastAgentMessageFromSessionFile -Path $candidatePath
if ([string]::IsNullOrWhiteSpace($lastAgentMessage)) {
    return New-GatewayResult -Allow $false -Message "execution gateway closeout failed: no final agent message found in $candidatePath" -ExecArgs @() -Meta @{
        gateId = $GateId
        planId = $PlanId
        sessionFile = $candidatePath
    }
}

$fields = Get-CloseoutFields -Message $lastAgentMessage
$failures = New-Object System.Collections.Generic.List[string]

if ($RequireDeploy) {
    $deployCheck = Test-CloseoutGate -Decision $fields.deployDecision -Allowed @('deployed', 'not-needed', 'blocked') -SuccessValue 'deployed' -ArtifactValue $fields.deployEvidence
    if (-not $deployCheck.passed) {
        [void]$failures.Add("DeployDecision gate failed: $($deployCheck.reason)")
    }
}

if ($RequireLiveVerification) {
    $liveCheck = Test-CloseoutGate -Decision $fields.liveVerificationDecision -Allowed @('passed', 'not-needed', 'blocked') -SuccessValue 'passed' -ArtifactValue $fields.liveVerificationArtifact
    if (-not $liveCheck.passed) {
        [void]$failures.Add("LiveVerificationDecision gate failed: $($liveCheck.reason)")
    }
}

if ($failures.Count -gt 0) {
    return New-GatewayResult -Allow $false -Message ("execution gateway closeout failed: " + ($failures -join '; ')) -ExecArgs @() -Meta @{
        gateId = $GateId
        planId = $PlanId
        sessionFile = $candidatePath
    }
}

return New-GatewayResult -Allow $true -Message 'execution gateway closeout passed' -ExecArgs @() -Meta @{
    gateId = $GateId
    planId = $PlanId
    sessionFile = $candidatePath
    deployDecision = $fields.deployDecision
    liveVerificationDecision = $fields.liveVerificationDecision
}

}

function Get-CommandParse { param([string[]]$Arguments)

$commands = @(
    'exec', 'review', 'login', 'logout', 'mcp', 'plugin', 'mcp-server',
    'app-server', 'app', 'completion', 'sandbox', 'debug', 'apply',
    'resume', 'fork', 'cloud', 'exec-server', 'features', 'help'
)
$optionsWithValue = @(
    '-c', '--config', '--enable', '--disable', '--remote', '--remote-auth-token-env',
    '-i', '--image', '-m', '--model', '--local-provider', '-p', '--profile',
    '-s', '--sandbox', '-a', '--ask-for-approval', '-C', '--cd', '--add-dir'
)

$parsed = [ordered]@{
    command = ''
    targetCwd = (Get-Location).Path
    promptTokens = New-Object System.Collections.Generic.List[string]
    beforePromptArgs = New-Object System.Collections.Generic.List[string]
    managementOnly = $false
    hasExplicitPrompt = $false
}

$i = 0
while ($i -lt $Arguments.Count) {
    $current = $Arguments[$i]

    if ($current -eq '-C' -or $current -eq '--cd') {
        $parsed.beforePromptArgs.Add($current)
        $i += 1
        if ($i -lt $Arguments.Count) {
            $parsed.beforePromptArgs.Add($Arguments[$i])
            $parsed.targetCwd = $Arguments[$i]
        }
        $i += 1
        continue
    }

    if ($optionsWithValue -contains $current) {
        $parsed.beforePromptArgs.Add($current)
        $i += 1
        if ($i -lt $Arguments.Count) {
            $parsed.beforePromptArgs.Add($Arguments[$i])
        }
        $i += 1
        continue
    }

    if ($current.StartsWith('-')) {
        $parsed.beforePromptArgs.Add($current)
        $i += 1
        continue
    }

    if ([string]::IsNullOrWhiteSpace($parsed.command) -and ($commands -contains $current)) {
        $parsed.command = $current
        $parsed.beforePromptArgs.Add($current)
        $i += 1
        continue
    }

    break
}

while ($i -lt $Arguments.Count) {
    $parsed.promptTokens.Add($Arguments[$i])
    $i += 1
}

$parsed.hasExplicitPrompt = $parsed.promptTokens.Count -gt 0

if ($parsed.command -in @('login', 'logout', 'mcp', 'plugin', 'mcp-server', 'app-server', 'app', 'completion', 'sandbox', 'debug', 'resume', 'fork', 'cloud', 'exec-server', 'features', 'help')) {
    $parsed.managementOnly = $true
}

return [pscustomobject]$parsed

}

function Get-TaskSummary { param([pscustomobject]$Parse)

if ($Parse.hasExplicitPrompt) {
    return (($Parse.promptTokens | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ' ').Trim()
}

switch ($Parse.command) {
    'review' { return "Run code review in $(Normalize-AbsolutePath $Parse.targetCwd)" }
    'exec' { return "Run codex exec task in $(Normalize-AbsolutePath $Parse.targetCwd)" }
    default { return '' }
}

}

function Build-PromptWithGate { param( [string]$OriginalPrompt, [pscustomobject]$RouteDecision, [pscustomobject]$GateRecord, [pscustomobject]$CloseoutRequirements )

$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('[Execution Gate]')
$lines.Add("GateId: $($GateRecord.gateId)")
if (-not [string]::IsNullOrWhiteSpace($GateRecord.planId)) {
    $lines.Add("PlanId: $($GateRecord.planId)")
}
$lines.Add("TaskHash: $($GateRecord.taskHash)")
if ($RouteDecision.id -ne 'default-gated') {
    $lines.Add("RequiredRoute: $($RouteDecision.id) -> $($RouteDecision.requiredTool)")
}
$lines.Add('Hard requirements:')
if ($GateRecord.planBound) {
    $lines.Add('- Context/plan gate already passed through the launcher; keep work inside this task boundary.')
} else {
    $lines.Add('- Closeout gate is active for this task; do not omit the required final decision lines.')
}
if ($RouteDecision.id -ne 'default-gated') {
    $lines.Add("- Before explanation, satisfy the required route: $($RouteDecision.directive)")
}
$lines.Add('- No verification means the task is not complete.')
if ($CloseoutRequirements.requireDeploy) {
    $lines.Add('- Final closeout must include `DeployDecision: deployed|not-needed|blocked`.')
    $lines.Add('- If `DeployDecision: deployed`, also include `DeployEvidence: <target/artifact/proof>`.')
}
if ($CloseoutRequirements.requireLiveVerification) {
    $lines.Add('- Final closeout must include `LiveVerificationDecision: passed|not-needed|blocked`.')
    $lines.Add('- If `LiveVerificationDecision: passed`, also include `LiveVerificationArtifact: <screenshot/path/evidence>`.')
}
$lines.Add('')
$lines.Add('User task:')
$lines.Add($OriginalPrompt)

return ($lines -join "`n")

}

function Build-ExecArgs { param( [pscustomobject]$Parse, [string]$PromptText )

$execArgs = New-Object System.Collections.Generic.List[string]
foreach ($item in $Parse.beforePromptArgs) {
    $execArgs.Add($item)
}

if ($Parse.command -eq '' -or $Parse.command -eq 'exec') {
    $execArgs.Add($PromptText)
} elseif ($Parse.hasExplicitPrompt) {
    $execArgs.AddRange($Parse.promptTokens)
}

return @($execArgs)

}

if ($Postflight) { $result = Invoke-PostflightCheck -GateId $GateId -PlanId $PlanId -StartedAt $StartedAt -SessionFile $SessionFile -RequireDeploy:(ConvertTo-BoolFlag $RequireDeploy) -RequireLiveVerification:(ConvertTo-BoolFlag $RequireLiveVerification) ` -ChildExitCode $ChildExitCode $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

$forwardArgs = Get-ForwardArgs $parse = Get-CommandParse -Arguments $forwardArgs $targetCwd = Normalize-AbsolutePath $parse.targetCwd $gatewayProfile = Find-GatewayProjectProfile -StartPath $targetCwd $hasProjectGate = ($null -ne $gatewayProfile) -and $gatewayProfile.enabled $taskSummary = Get-TaskSummary -Parse $parse $industrialClassification = Invoke-IndustrialTaskClassification -TaskSummary $taskSummary

if ($industrialClassification.level -eq 'L4') { Write-IndustrialRunLedger -TaskSummary $taskSummary -Level $industrialClassification.level -Decision 'deny' -Reason $industrialClassification.reason -TargetCwd $targetCwd $result = New-GatewayResult -Allow $false -Message ('Codex industrial control denied task: ' + $industrialClassification.reason) -ExecArgs @() $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

if ($industrialClassification.level -eq 'L3' -and $env:ALLOW_L3 -ne '1') { Write-IndustrialRunLedger -TaskSummary $taskSummary -Level $industrialClassification.level -Decision 'deny' -Reason 'L3 task requires ALLOW_L3=1' -TargetCwd $targetCwd $result = New-GatewayResult -Allow $false -Message 'Codex industrial control requires ALLOW_L3=1 for network/deploy/package tasks.' -ExecArgs @() $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

$closeoutRequirements = if ([string]::IsNullOrWhiteSpace($taskSummary)) { [pscustomobject]@{ requireDeploy = $false requireLiveVerification = $false } } else { Get-CloseoutRequirements -TaskSummary $taskSummary } $closeoutGateOnly = (-not $hasProjectGate) -and ($closeoutRequirements.requireDeploy -or $closeoutRequirements.requireLiveVerification)

if ($parse.managementOnly -or ((-not $hasProjectGate) -and -not $closeoutGateOnly)) { $result = New-GatewayResult -Allow $true -Message 'gateway bypassed' -ExecArgs $forwardArgs $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

if ($hasProjectGate -and -not $gatewayProfile.profileComplete) { $result = New-GatewayResult -Allow $false -Message "Project gate is enabled for $($gatewayProfile.repoLabel), but canonical context/plan files are incomplete under $($gatewayProfile.codexRoot). Marker: $($gatewayProfile.directiveSource)" -ExecArgs @() $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

if ($hasProjectGate -and (-not (Test-Path -LiteralPath $gatewayProfile.projectContextPath) -or -not (Test-Path -LiteralPath $gatewayProfile.sessionHandoffPath))) { $result = New-GatewayResult -Allow $false -Message "Project context is missing: $($gatewayProfile.projectContextPath) / $($gatewayProfile.sessionHandoffPath)" -ExecArgs @() $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

if ([string]::IsNullOrWhiteSpace($taskSummary)) { if ($hasProjectGate) { $message = 'Codex launches inside a repo with canonical plan/context gates must include an explicit task summary so the work can be bound to the plan ledger. Start it with a task description, for example: codex "fix xxx".' $result = New-GatewayResult -Allow $false -Message $message -ExecArgs @() } else { $result = New-GatewayResult -Allow $true -Message 'gateway bypassed because no task summary was available' -ExecArgs $CodexArgs } $result | ConvertTo-Json -Depth 8 -Compress exit 0 }

$gateRecord = if ($hasProjectGate) { Ensure-PlanRecord -GatewayProfile $gatewayProfile -TaskSummary $taskSummary -TargetCwd $targetCwd } else { New-CloseoutGateRecord -TaskSummary $taskSummary -TargetCwd $targetCwd }

$routeDecision = Get-TaskRouteDecision -TaskSummary $taskSummary if (-not $routeDecision) { $routeDecision = [pscustomobject]@{ id = 'default-gated' kind = 'gate' requiredTool = '' directive = '' defaultDirective = '' } }

$startedAt = (Get-Date).ToString('o') $envMap = @{ CODEX_EXECUTION_GATEWAY = '1' CODEX_EXECUTION_GATEWAY_GATE_ID = $gateRecord.gateId CODEX_EXECUTION_GATEWAY_PLAN_ID = $gateRecord.planId CODEX_EXECUTION_GATEWAY_TASK_HASH = $gateRecord.taskHash CODEX_EXECUTION_GATEWAY_ROUTE = $routeDecision.id CODEX_EXECUTION_GATEWAY_REQUIRE_DEPLOY = $(if ($closeoutRequirements.requireDeploy) { '1' } else { '0' }) CODEX_EXECUTION_GATEWAY_REQUIRE_LIVE_VERIFICATION = $(if ($closeoutRequirements.requireLiveVerification) { '1' } else { '0' }) CODEX_EXECUTION_GATEWAY_STARTED_AT = $startedAt CODEX_EXECUTION_GATEWAY_TARGET_CWD = $targetCwd }

$execArgs = $forwardArgs if ($parse.command -eq '' -or $parse.command -eq 'exec') { $gatedPrompt = Build-PromptWithGate -OriginalPrompt $taskSummary -RouteDecision $routeDecision -GateRecord $gateRecord -CloseoutRequirements $closeoutRequirements $execArgs = Build-ExecArgs -Parse $parse -PromptText $gatedPrompt }

Write-IndustrialRunLedger -TaskSummary $taskSummary -Level $industrialClassification.level -Decision 'allow' -Reason $industrialClassification.reason -PlanId $gateRecord.planId -Route $routeDecision.id -TargetCwd $targetCwd

$message = if ($gateRecord.planBound) { if ($gateRecord.reused) { "execution gateway passed with existing plan $($gateRecord.planId)" } else { "execution gateway created plan $($gateRecord.planId)" } } else { "execution gateway activated closeout gate $($gateRecord.gateId)" }

$result = New-GatewayResult -Allow $true -Message $message -ExecArgs $execArgs -Env $envMap -Meta @{ targetCwd = $targetCwd task = $taskSummary route = $routeDecision.id gateId = $gateRecord.gateId planId = $gateRecord.planId } $result | ConvertTo-Json -Depth 8 -Compress

⚠️ 2️⃣ 你现在用的其实不是 .ps

你现在这个文件是:

gateway.ps1

👉 重点是:

.ps1.ps


🧠 3️⃣ .ps1 是什么?

.ps1 是:

💻 PowerShell 脚本


📌 它是干嘛的?

👉 用来控制 Windows / 系统 / 自动化任务

比如:

Get-Process
Remove-Item file.txt
Start-Process notepad


📌 你这个 gateway.ps1 在干嘛?

👉 本质是:

🧠 用 PowerShell 写的“AI控制系统脚本”

它在:

  • 拦截 codex
  • 解析命令
  • 判断危险操作
  • 写日志(ledger)
  • 强制 plan 流程
  • 控制执行权限

什么是网关 你可以把“网关”理解成一句话:

👉 所有请求进系统之前,必须经过的“守门人”


🧠 用你现在这个系统举例

你输入:

codex "修复登录页 bug"

实际发生的是:

你 → gateway → codex

👉 网关就在中间拦了一刀


🔥 网关到底在干嘛?

它一般做 4 件核心事情:


① 🛑 决定能不能过(最重要)

这个命令危险吗?
这个任务允许执行吗?

比如:

  • ❌ 删除系统文件 → 拦截

  • ❌ 自动部署生产 → 拦截

  • ✅ 普通改代码 → 放行

👉 就像门禁卡


② 🔍 看你在干啥(解析请求)

比如:

codex -C project "修 bug"

网关会拆:

  • 目录在哪

  • 任务是什么

  • 有没有参数

👉 相当于“安检+翻译”


③ 📋 加规则(强制你按流程走)

比如你现在这套:

必须写 plan
必须验证
必须给 deploy 结论

👉 网关会帮你自动加:

[Execution Gate]
必须完成以下要求...

④ 📊 记录你干了啥(审计)

比如:

  • 什么时候执行的

  • 执行了什么任务

  • 是否成功

  • 有没有违规

👉 相当于“监控+日志系统”


🧠 举个现实世界例子

你去机场:

你 → 安检 → 登机

👉 安检就是网关

它会:

  • 查你带没带危险品(安全)

  • 看你去哪(解析)

  • 让你走流程(规则)

  • 记录你信息(审计)


🧠 再换个技术例子(你肯定更容易懂)

🌐 网站里的网关

浏览器 → API Gateway → 后端服务

👉 API Gateway 会:

  • 校验 token(身份)

  • 限流(防刷)

  • 路由请求(去哪台服务器)

  • 记录日志


🧨 你现在这个 gateway 的级别

你这不是普通网关,而是:

🔥 AI执行网关(Agent Gateway)

它控制的是:

  • AI能不能执行

  • AI必须怎么执行

  • AI有没有乱来


🧠 一句话总结

👉 网关 = 控制入口的一层系统


⚠️ 为什么你这个东西很关键

因为:

👉 没有网关 = AI想干啥就干啥

👉 有网关 = AI必须按你规则干活


🧭 你现在其实已经在做的东西

你现在这套系统,本质是:

🧠 “AI操作系统”的核心控制层


👉 如果你继续往下走

你可以把网关升级成:

  • ✅ 权限系统(谁能执行什么)

  • ✅ 策略系统(JSON规则)

  • ✅ 多Agent调度入口

  • ✅ 审计系统(谁干了什么)


如果你想,我可以帮你直接画一套:

👉 “AI网关 + Agent系统架构图(工业级)”

或者直接帮你:

👉 把你这个 gateway 改成“可维护的架构版本”(不是脚本屎山)

你只要说一句:

👉 “给我升级网关架构”

对,但不只是“可以”,而是——

👉 网关的核心职责之一就是“做门槛”

不过你要把它理解得更完整一点,不然容易低估它。


🧠 先给你一个准确版本

网关 = 可编程的门槛 + 决策中心


🔥 门槛具体是怎么体现的?

不是简单“拦/放”,而是多层门槛:


🛑 1️⃣ 权限门槛(能不能做)

这个人/这个任务,有没有资格执行?

比如:

  • ❌ 普通任务 → 不允许部署

  • ❌ AI → 不允许删系统文件

  • ✅ 开发任务 → 允许改代码


⚠️ 2️⃣ 风险门槛(危险程度)

这个操作有没有风险?

比如你那套:

  • L0:普通(放)

  • L3:网络/部署(需要授权)

  • L4:危险(直接拦)

👉 这就是分级门槛


📋 3️⃣ 流程门槛(必须按步骤来)

你可以做,但必须按流程

比如:

  • 必须先写 plan

  • 必须验证结果

  • 必须给结论(deploy / verification)

👉 不满足:

❌ 不算完成(直接卡死)


🔍 4️⃣ 内容门槛(你在做什么)

网关会看:

你输入的内容是不是符合规则?

比如:

  • 包含危险命令 → 拦

  • 不符合格式 → 拦

  • 没有任务描述 → 拦


📊 5️⃣ 审计门槛(必须可追溯)

你做的事情必须被记录

否则:

❌ 不允许执行 / 不算完成


🧠 用一句更工程化的话说

👉 网关 = 策略执行引擎(Policy Enforcement Layer)


🔥 你现在这套其实已经很高级了

你现在的 gateway 已经在做:

  • ✔ 任务分类(L0/L3/L4)

  • ✔ plan 强绑定

  • ✔ 执行前 gate

  • ✔ 执行后验证 gate

  • ✔ 日志(ledger)

👉 这已经不是“门槛”,而是:

🔥 AI 行为控制系统


🧨 但注意一个关键区别

很多人理解错的点👇


❌ 错误理解

网关 = 简单 if 判断

if (danger) { block }

👉 这种是“低级门槛”


✅ 正确理解

网关 = 规则系统 + 状态系统 + 决策系统

它会:

  • 看任务(输入)

  • 看环境(cwd / 项目)

  • 看历史(plan / state)

  • 看规则(policy)

👉 最后才决定:

allow / deny / modify / enforce

🧭 给你一个非常关键的升级理解

你现在做的不是:

❌ “限制 AI”

而是:

定义 AI 行为边界


🔥 最核心一句话

👉 网关不是“挡住 AI”,而是“让 AI 只在你允许的轨道上运行”


👉 如果你继续往上做

你可以把网关升级成:

  • 🧠 AI 权限系统(像 Linux 用户权限)

  • 🧠 多 Agent 调度入口

  • 🧠 自动化流程引擎(CI/CD + AI)

  • 🧠 行为审计系统


如果你想,我可以下一步直接帮你:

👉 把你现在这个 gateway 升级成:

“策略驱动 + 可扩展 + 不会炸的架构版”

你只要说一句:

👉 “我要做规则驱动网关”

对,你应该做成这个结构:

.codex/
  gateway.ps1                 # 只做执行器
  rules/
    gateway.policy.json        # 规则配置
  logs/
    gateway-ledger.jsonl       # 审计日志

核心原则:

gateway.ps1 不写死业务规则
gateway.ps1 只读取 gateway.policy.json

规则文件示例:

{
  "version": 1,
  "defaultAction": "allow",
  "levels": {
    "L0": "allow",
    "L1": "allow",
    "L2": "allow",
    "L3": "require_env",
    "L4": "deny"
  },
  "envGates": {
    "L3": "ALLOW_L3"
  },
  "rules": [
    {
      "id": "danger-delete",
      "level": "L4",
      "match": ["rm -rf", "Remove-Item -Recurse", "del /s", "format"]
    },
    {
      "id": "deploy-network",
      "level": "L3",
      "match": ["deploy", "ssh", "scp", "npm install", "pip install", "systemctl"]
    },
    {
      "id": "normal-code-edit",
      "level": "L1",
      "match": ["修复", "修改", "重构", "优化", "bug"]
    }
  ]
}

你的执行逻辑应该是:

输入命令

解析任务文本

读取 gateway.policy.json

匹配规则

得到 level

根据 level 决定 allow / deny / require_env

写 ledger

输出 JSON 给 launcher

最小版 gateway.ps1 只需要保留这些能力:

$ErrorActionPreference = 'Stop'
 
$codexHome = 'C:\Users\ASUS-KL\.codex'
$policyPath = Join-Path $codexHome 'rules\gateway.policy.json'
$ledgerPath = Join-Path $codexHome 'logs\gateway-ledger.jsonl'
 
function New-Result {
    param(
        [bool]$Allow,
        [string]$Message,
        [string[]]$ExecArgs = @(),
        [hashtable]$Meta = @{}
    )
 
    [pscustomobject]@{
        allow = $Allow
        message = $Message
        execArgs = @($ExecArgs)
        env = @{}
        meta = $Meta
    }
}
 
function Write-Ledger {
    param([hashtable]$Record)
 
    $dir = Split-Path -Parent $ledgerPath
    if (-not (Test-Path $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }
 
    $Record.time = (Get-Date).ToString('o')
    ($Record | ConvertTo-Json -Compress -Depth 8) | Add-Content -Path $ledgerPath -Encoding utf8
}
 
function Get-ForwardArgs {
    if ($env:CODEX_FORWARD_ARGS_JSON) {
        return @((ConvertFrom-Json $env:CODEX_FORWARD_ARGS_JSON) | ForEach-Object { [string]$_ })
    }
 
    return @($args)
}
 
function Get-TaskText {
    param([string[]]$Args)
 
    return (($Args | Where-Object { $_ -notmatch '^-C$|^--cd$' }) -join ' ').Trim()
}
 
function Match-Policy {
    param(
        [object]$Policy,
        [string]$TaskText
    )
 
    foreach ($rule in $Policy.rules) {
        foreach ($pattern in $rule.match) {
            if ($TaskText -match [regex]::Escape($pattern)) {
                return $rule
            }
        }
    }
 
    return [pscustomobject]@{
        id = 'default'
        level = 'L0'
    }
}
 
$forwardArgs = Get-ForwardArgs
$taskText = Get-TaskText -Args $forwardArgs
 
if ([string]::IsNullOrWhiteSpace($taskText)) {
    $result = New-Result -Allow $true -Message 'interactive mode' -ExecArgs $forwardArgs -Meta @{
        level = 'L0'
        rule = 'interactive'
    }
 
    Write-Ledger @{
        task = 'interactive'
        level = 'L0'
        decision = 'allow'
        rule = 'interactive'
    }
 
    $result | ConvertTo-Json -Compress -Depth 8
    exit 0
}
 
$policy = Get-Content $policyPath -Raw -Encoding utf8 | ConvertFrom-Json
$rule = Match-Policy -Policy $policy -TaskText $taskText
$level = [string]$rule.level
$action = [string]$policy.levels.$level
 
if ($action -eq 'deny') {
    Write-Ledger @{
        task = $taskText
        level = $level
        decision = 'deny'
        rule = $rule.id
    }
 
    $result = New-Result -Allow $false -Message "blocked by gateway rule: $($rule.id)" -Meta @{
        level = $level
        rule = $rule.id
    }
 
    $result | ConvertTo-Json -Compress -Depth 8
    exit 0
}
 
if ($action -eq 'require_env') {
    $envName = [string]$policy.envGates.$level
 
    if ($env:$envName -ne '1') {
        Write-Ledger @{
            task = $taskText
            level = $level
            decision = 'deny'
            rule = $rule.id
            requiredEnv = $envName
        }
 
        $result = New-Result -Allow $false -Message "$level requires $envName=1" -Meta @{
            level = $level
            rule = $rule.id
            requiredEnv = $envName
        }
 
        $result | ConvertTo-Json -Compress -Depth 8
        exit 0
    }
}
 
Write-Ledger @{
    task = $taskText
    level = $level
    decision = 'allow'
    rule = $rule.id
}
 
$result = New-Result -Allow $true -Message "gateway passed: $($rule.id)" -ExecArgs $forwardArgs -Meta @{
    level = $level
    rule = $rule.id
}
 
$result | ConvertTo-Json -Compress -Depth 8

你原来那个 gateway.ps1 已经把分类、plan、postflight、ledger、prompt 注入全塞在一起了,所以才容易被 patch 打烂;这次要把它拆成“规则文件 + 判定器 + 日志”三层。你上传的旧文件里也能看到它已经承担了参数解析、工业分类、计划绑定、收尾验证等多种职责,复杂度已经超过普通脚本维护范围。