← Files Chronos for CodexARCHIVED FILE

skills/chronos/scripts/hook-intake.ps1

13.2 KB · Oct 5, 2026 · 18:30 UTC

↓ Download file

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$script:HookInputByteLimit = 65536
$script:JsonNodeLimit = 8192
$script:PendingEventLimit = 256
$script:PendingEventByteLimit = 4096
$script:Entropy = [Text.Encoding]::UTF8.GetBytes('Chronos.Supervision.Registry.v1')

function Get-Value {
  param($Object, [string]$Name, $Default = $null)
  if ($null -eq $Object) { return $Default }
  $property = $Object.PSObject.Properties[$Name]
  if ($null -eq $property -or $null -eq $property.Value) { return $Default }
  return $property.Value
}

function Move-JsonWhitespace {
  param([string]$Text, [ref]$Index)
  while ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -in @(' ', "`t", "`r", "`n")) { $Index.Value++ }
}

function Read-StrictJsonString {
  param([string]$Text, [ref]$Index)
  if ($Index.Value -ge $Text.Length -or $Text[$Index.Value] -ne '"') { throw 'hook_json_invalid' }
  $start = $Index.Value
  $Index.Value++
  while ($Index.Value -lt $Text.Length) {
    $character = $Text[$Index.Value]
    if ([int][char]$character -lt 0x20) { throw 'hook_json_invalid' }
    if ($character -eq '"') {
      $Index.Value++
      try {
        $decoded = $Text.Substring($start, $Index.Value - $start) | ConvertFrom-Json -ErrorAction Stop
        if (-not ($decoded -is [string])) { throw 'hook_json_invalid' }
        return $decoded.Normalize([Text.NormalizationForm]::FormC)
      } catch { throw 'hook_json_invalid' }
    }
    if ($character -eq '\') {
      $Index.Value++
      if ($Index.Value -ge $Text.Length) { throw 'hook_json_invalid' }
      $escape = $Text[$Index.Value]
      if ($escape -eq 'u') {
        if ($Index.Value + 4 -ge $Text.Length -or $Text.Substring($Index.Value + 1, 4) -notmatch '^[0-9A-Fa-f]{4}$') { throw 'hook_json_invalid' }
        $Index.Value += 5
        continue
      }
      if ($escape -notin @('"', '\', '/', 'b', 'f', 'n', 'r', 't')) { throw 'hook_json_invalid' }
    }
    $Index.Value++
  }
  throw 'hook_json_invalid'
}

function Assert-StrictJsonValue {
  param([string]$Text, [ref]$Index, [ref]$NodeCount, [int]$Depth)
  if ($Depth -gt 16) { throw 'hook_json_invalid' }
  Move-JsonWhitespace $Text $Index
  if ($Index.Value -ge $Text.Length) { throw 'hook_json_invalid' }
  $NodeCount.Value++
  if ($NodeCount.Value -gt $script:JsonNodeLimit) { throw 'hook_json_invalid' }
  $character = $Text[$Index.Value]
  if ($character -eq '"') { [void](Read-StrictJsonString $Text $Index); return }
  if ($character -eq '{') {
    $Index.Value++
    Move-JsonWhitespace $Text $Index
    $keys = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
    if ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -eq '}') { $Index.Value++; return }
    while ($true) {
      Move-JsonWhitespace $Text $Index
      $key = Read-StrictJsonString $Text $Index
      if (-not $keys.Add($key)) { throw 'hook_json_invalid' }
      Move-JsonWhitespace $Text $Index
      if ($Index.Value -ge $Text.Length -or $Text[$Index.Value] -ne ':') { throw 'hook_json_invalid' }
      $Index.Value++
      Assert-StrictJsonValue $Text $Index $NodeCount ($Depth + 1)
      Move-JsonWhitespace $Text $Index
      if ($Index.Value -ge $Text.Length) { throw 'hook_json_invalid' }
      if ($Text[$Index.Value] -eq '}') { $Index.Value++; return }
      if ($Text[$Index.Value] -ne ',') { throw 'hook_json_invalid' }
      $Index.Value++
    }
  }
  if ($character -eq '[') {
    $Index.Value++
    Move-JsonWhitespace $Text $Index
    if ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -eq ']') { $Index.Value++; return }
    while ($true) {
      Assert-StrictJsonValue $Text $Index $NodeCount ($Depth + 1)
      Move-JsonWhitespace $Text $Index
      if ($Index.Value -ge $Text.Length) { throw 'hook_json_invalid' }
      if ($Text[$Index.Value] -eq ']') { $Index.Value++; return }
      if ($Text[$Index.Value] -ne ',') { throw 'hook_json_invalid' }
      $Index.Value++
    }
  }
  $start = $Index.Value
  while ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -notin @(',', ']', '}', ' ', "`t", "`r", "`n")) { $Index.Value++ }
  $token = $Text.Substring($start, $Index.Value - $start)
  if ($token -notmatch '^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)$') { throw 'hook_json_invalid' }
}

function Assert-StrictJson {
  param([string]$Text)
  if ([string]::IsNullOrWhiteSpace($Text)) { throw 'hook_json_invalid' }
  $index = 0
  $nodeCount = 0
  Assert-StrictJsonValue $Text ([ref]$index) ([ref]$nodeCount) 0
  Move-JsonWhitespace $Text ([ref]$index)
  if ($index -ne $Text.Length) { throw 'hook_json_invalid' }
}

function Get-TextHash {
  param([string]$Value)
  $sha = [Security.Cryptography.SHA256]::Create()
  try {
    return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value)))).Replace('-', '').ToLowerInvariant()
  } finally {
    $sha.Dispose()
  }
}

function Test-ContainedPath {
  param([string]$Path, [string]$Root)
  try {
    $full = [IO.Path]::GetFullPath($Path)
    $base = [IO.Path]::GetFullPath($Root).TrimEnd([char[]]@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar))
  } catch { return $false }
  return $full.StartsWith($base + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)
}

function Test-NoReparseAncestors {
  param([string]$Path)
  try { $full = [IO.Path]::GetFullPath($Path) } catch { return $false }
  $cursorPath = $full
  while (-not (Test-Path -LiteralPath $cursorPath)) {
    $parent = Split-Path -Parent $cursorPath
    if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $cursorPath) { break }
    $cursorPath = $parent
  }
  $cursor = Get-Item -LiteralPath $cursorPath -Force -ErrorAction SilentlyContinue
  while ($cursor) {
    if ($cursor.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false }
    if ($cursor -is [IO.DirectoryInfo]) { $cursor = $cursor.Parent } else { $cursor = $cursor.Directory }
  }
  return $true
}

function Normalize-OpaqueId {
  param($Value)
  if (-not ($Value -is [string])) { throw 'hook_id_invalid' }
  $text = $Value.Trim()
  if ($text.Length -lt 1 -or $text.Length -gt 192 -or $text -notmatch '^[A-Za-z0-9._:/-]+$') { throw 'hook_id_invalid' }
  if ($text -match '^[A-Za-z]:[\\/]' -or $text -match '^\\' -or ($text.StartsWith('/') -and -not $text.StartsWith('/root/'))) { throw 'hook_id_invalid' }
  return $text
}

function Protect-OpaqueId {
  param([string]$Value)
  Add-Type -AssemblyName System.Security -ErrorAction SilentlyContinue
  $bytes = [Text.Encoding]::UTF8.GetBytes($Value)
  $protected = [Security.Cryptography.ProtectedData]::Protect($bytes, $script:Entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser)
  return [Convert]::ToBase64String($protected)
}

function Resolve-CodexHomeIdentity {
  $candidate = if (-not [string]::IsNullOrWhiteSpace([string]$env:CODEX_HOME)) { [string]$env:CODEX_HOME } else { Join-Path $HOME '.codex' }
  $full = [IO.Path]::GetFullPath($candidate)
  if (-not [string]::IsNullOrWhiteSpace([string]$env:CODEX_HOME)) {
    $item = Get-Item -LiteralPath $full -Force -ErrorAction Stop
    if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'hook_home_invalid' }
    $full = [IO.Path]::GetFullPath($item.FullName)
  }
  if (-not (Test-NoReparseAncestors $full)) { throw 'hook_home_invalid' }
  $full = $full.TrimEnd([char[]]@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar))
  if ([string]::IsNullOrWhiteSpace($full)) { throw 'hook_home_invalid' }
  return $full.ToUpperInvariant()
}

function Resolve-HookInboxDirectory {
  $tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
  $homeIdentity = Resolve-CodexHomeIdentity
  $scopeHash = Get-TextHash ('{0}|{1}' -f $env:COMPUTERNAME.ToUpperInvariant(), $homeIdentity)
  $directory = [IO.Path]::GetFullPath((Join-Path $tempRoot ('Chronos-Supervision-Inbox-v1-{0}' -f $scopeHash.Substring(0, 24))))
  if (-not (Test-ContainedPath $directory $tempRoot) -or -not (Test-NoReparseAncestors $directory)) { throw 'hook_inbox_invalid' }
  if (-not (Test-Path -LiteralPath $directory -PathType Container)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
  $item = Get-Item -LiteralPath $directory -Force -ErrorAction Stop
  if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $directory)) { throw 'hook_inbox_invalid' }
  return $directory
}

function New-HookRecord {
  param($Data, [DateTimeOffset]$ObservedAt)
  $event = [string](Get-Value $Data 'hook_event_name' '')
  if ($event -notin @('SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop', 'Stop')) { throw 'hook_event_invalid' }
  $session = Normalize-OpaqueId (Get-Value $Data 'session_id')
  $agent = $null
  if ($event -in @('SubagentStart', 'SubagentStop')) { $agent = Normalize-OpaqueId (Get-Value $Data 'agent_id') }
  $source = if ($event -eq 'SessionStart') { [string](Get-Value $Data 'source' 'startup') } elseif ($event -in @('SubagentStart', 'SubagentStop')) { 'subagent' } else { 'fallback' }
  if ($event -eq 'SessionStart' -and $source -notin @('startup', 'resume', 'clear', 'compact')) { throw 'hook_source_invalid' }
  $workspace = Get-Value $Data 'cwd'
  $workspaceHash = if ($workspace -is [string] -and $workspace.Length -ge 1 -and $workspace.Length -le 4096) {
    try { Get-TextHash ([IO.Path]::GetFullPath($workspace).TrimEnd([char[]]@('\\', '/')).ToUpperInvariant()) } catch { Get-TextHash 'workspace-unavailable' }
  } else { Get-TextHash 'workspace-unavailable' }
  $modelValue = Get-Value $Data 'model'
  $model = if ($modelValue -is [string] -and $modelValue.Trim() -match '^[A-Za-z0-9._:/-]{1,128}$') { $modelValue.Trim() } else { 'unavailable' }
  return [ordered]@{
    schema = 2
    event = $event
    protectedSessionId = Protect-OpaqueId $session
    protectedAgentId = if ($null -ne $agent) { Protect-OpaqueId $agent } else { $null }
    workspaceHash = $workspaceHash
    model = $model
    source = $source
    signalHash = if ($event -eq 'Stop') { Get-TextHash (Normalize-OpaqueId (Get-Value $Data 'turn_id')) } else { $null }
    observedAtUtc = $ObservedAt.ToString('o')
  }
}

function Write-HookRecord {
  param([string]$Directory, $Record)
  $bytes = [Text.UTF8Encoding]::new($false).GetBytes(($Record | ConvertTo-Json -Compress -Depth 4))
  if ($bytes.Length -gt $script:PendingEventByteLimit) { throw 'hook_event_oversize' }
  foreach ($staleReservation in @(Get-ChildItem -LiteralPath $Directory -File -Force -Filter 'pending-slot-*.lock' | Select-Object -First $script:PendingEventLimit)) {
    if ($staleReservation.LastWriteTimeUtc -lt [DateTime]::UtcNow.AddMinutes(-5)) { Remove-Item -LiteralPath $staleReservation.FullName -Force -ErrorAction SilentlyContinue }
  }
  $start = [int]([BitConverter]::ToUInt32(([guid]::NewGuid()).ToByteArray(), 0) % [uint32]$script:PendingEventLimit)
  $reservation = $null
  $final = $null
  for ($offset = 0; $offset -lt $script:PendingEventLimit; $offset++) {
    $slot = ($start + $offset) % $script:PendingEventLimit
    $candidateFinal = Join-Path $Directory ('pending-slot-{0:d3}.json' -f $slot)
    $candidateReservation = Join-Path $Directory ('pending-slot-{0:d3}.lock' -f $slot)
    if (Test-Path -LiteralPath $candidateFinal -PathType Leaf) { continue }
    try {
      $reserveStream = New-Object IO.FileStream($candidateReservation, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
      $reserveStream.Dispose()
      if (Test-Path -LiteralPath $candidateFinal -PathType Leaf) { Remove-Item -LiteralPath $candidateReservation -Force -ErrorAction SilentlyContinue; continue }
      $reservation = $candidateReservation
      $final = $candidateFinal
      break
    } catch [IO.IOException] { continue }
  }
  if ([string]::IsNullOrWhiteSpace($final)) { throw 'hook_inbox_capacity' }
  $temporary = Join-Path $Directory ('.pending-' + [guid]::NewGuid().ToString('N') + '.tmp')
  try {
    $stream = New-Object IO.FileStream($temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
    try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
    [IO.File]::Move($temporary, $final)
  } finally {
    Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
    if ($reservation) { Remove-Item -LiteralPath $reservation -Force -ErrorAction SilentlyContinue }
  }
}

try {
  $reader = [IO.StreamReader]::new([Console]::OpenStandardInput(), [Text.UTF8Encoding]::new($false, $true), $true, 4096, $false)
  try { $raw = $reader.ReadToEnd() } finally { $reader.Dispose() }
  if ([string]::IsNullOrWhiteSpace($raw)) { throw 'hook_input_empty' }
  if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
  if ([Text.Encoding]::UTF8.GetByteCount($raw) -gt $script:HookInputByteLimit) { throw 'hook_input_oversize' }
  Assert-StrictJson $raw
  $data = $raw | ConvertFrom-Json -ErrorAction Stop
  if ($data.GetType().FullName -ne 'System.Management.Automation.PSCustomObject') { throw 'hook_shape_invalid' }
  $record = New-HookRecord $data ([DateTimeOffset]::UtcNow)
  Write-HookRecord (Resolve-HookInboxDirectory) $record
} catch {
  # Lifecycle hints are optional acceleration. The Governor's complete host
  # inventory remains authoritative when a bounded hook cannot persist a hint.
}
exit 0

SHA-256: 6ca0b003a451fee1cf1b6f1cbd391b84d0677e7f90814ccd83dc27a0eebc6752