packages feed

seihou-cli-0.12.0.0: src/Seihou/CLI/AgentMigrationProtocol.hs

-- | Strict validation before a migration may create a receipt. Unknown fields
-- are compatible; corrupt or incomplete evidence is not.
module Seihou.CLI.AgentMigrationProtocol (validateMigrationOutput) where

import Control.Monad (unless, when)
import Data.Aeson (Value (..), eitherDecodeStrict')
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BS
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V

validateMigrationOutput :: Bool -> ByteString -> Either Text Text
validateMigrationOutput claude bytes = case validate of
  Left err -> Left (err <> " Inspect partial edits, check provider permissions/protocol support, then retry the same migration.")
  Right body -> Right body
  where
    validate
      | claude = do
          raw <- decodeValue bytes
          value <- case raw of
            Object _ -> Right raw
            Array events | not (V.null events) -> do
              mapM_ checkClaudeEvent (V.toList events)
              Right (V.last events)
            _ -> Left "Claude result is missing."
          require (field "type" value == Just (String "result")) "Claude result is missing."
          require (field "subtype" value == Just (String "success")) ("Claude did not report success. " <> detail value)
          require (field "is_error" value == Just (Bool False)) ("Claude reported an error or omitted is_error. " <> detail value)
          case field "permission_denials" value of
            Just (Array denials) | V.null denials -> pure ()
            _ -> Left ("Claude reported permission denials or omitted permission evidence. " <> detail value)
          textField "result" value >>= nonblank
      | otherwise = do
          events <- traverse decode (filter (not . BS.null) (BS.lines bytes))
          mapM_ checkEvent events
          require (any (\event -> field "type" event == Just (String "turn.completed")) events) "Codex has no successful terminal turn result."
          let messages = [body | event <- events, field "type" event == Just (String "item.completed"), Just item <- [field "item" event], field "type" item == Just (String "agent_message"), Just (String body) <- [field "text" item]]
          nonblank (T.intercalate "\n" messages)
    detail value = case field "result" value of
      Just (String body) -> T.take 2000 body
      _ -> "Check the Claude CLI authentication and configuration."
    checkEvent event = do
      kind <- textField "type" event
      when (kind `elem` ["error", "turn.failed", "permission_denied"]) (Left "Codex reported an error, failed turn, or permission denial.")
      case field "permission_denials" event of
        Nothing -> pure ()
        Just (Array values) | V.null values -> pure ()
        _ -> Left "Codex reported permission denials."
      when (kind `elem` ["item.completed", "item.started", "item.updated"]) $ do
        item <- maybe (Left "Codex item event omitted item.") Right (field "item" event)
        itemKind <- textField "type" item
        when (itemKind == "error" || field "status" item `elem` [Just (String "declined"), Just (String "failed")]) (Left "Codex reported an item error or permission denial.")
        when (kind == "item.completed" && itemKind == "agent_message") $ do
          _ <- textField "text" item
          pure ()
    checkClaudeEvent event = do
      kind <- textField "type" event
      when (kind == "result" && field "subtype" event /= Just (String "success")) (Left "Claude reported a failed result.")
      case field "permission_denials" event of
        Nothing -> pure ()
        Just (Array values) | V.null values -> pure ()
        _ -> Left ("Claude reported permission denials: " <> deniedTools event <> ". " <> detail event)
      when (field "is_error" event == Just (Bool True) || field "type" event == Just (String "error")) (Left "Claude reported an error.")
    deniedTools event = case field "permission_denials" event of
      Just (Array values) -> T.intercalate ", " [name <> commandDetail value | value <- V.toList values, Just (String name) <- [field "tool_name" value]]
      _ -> "unspecified tool"
    commandDetail value = case field "tool_input" value >>= field "command" of
      Just (String command) -> " (" <> T.take 240 command <> ")"
      _ -> ""
    decodeValue raw = case eitherDecodeStrict' raw of
      Left _ -> Left "Malformed migration provider JSON."
      Right value -> Right value
    decode raw = case eitherDecodeStrict' raw of
      Left _ -> Left "Malformed migration provider JSON."
      Right value@(Object _) -> Right value
      Right _ -> Left "Migration provider event must be a JSON object."
    field key (Object object) = KM.lookup key object
    field _ _ = Nothing
    textField key value = case field key value of
      Just (String body) -> Right body
      _ -> Left "Migration provider omitted a required text field."
    require condition err = unless condition (Left err)
    nonblank body = if T.null (T.strip body) then Left "Provider returned no assistant text." else Right body