[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 打烂;这次要把它拆成“规则文件 + 判定器 + 日志”三层。你上传的旧文件里也能看到它已经承担了参数解析、工业分类、计划绑定、收尾验证等多种职责,复杂度已经超过普通脚本维护范围。