/ src / armitage / Main.hs
Main.hs
   1  {-# LANGUAGE LambdaCase #-}
   2  {-# LANGUAGE OverloadedStrings #-}
   3  
   4  {- |
   5  Module      : Main
   6  Description : Armitage CLI - daemon-free Nix operations
   7  
   8  The armitage command-line tool provides daemon-free Nix operations:
   9    - armitage build <drv>    Build a derivation without daemon
  10    - armitage build-dhall    Build from Dhall target file
  11    - armitage proxy          Run the witness proxy
  12    - armitage store <cmd>    Store operations
  13    - armitage cas <cmd>      CAS operations
  14  
  15  The daemon is hostile infrastructure. armitage routes around it.
  16  
  17  Build:
  18    buck2 build //src/armitage:armitage
  19  
  20  Usage:
  21    armitage build /nix/store/xxx.drv
  22    armitage build-dhall BUILD.dhall
  23    armitage proxy --port 8080
  24    armitage store add ./path
  25    armitage cas upload <hash> <file>
  26  -}
  27  module Main where
  28  
  29  import Control.Exception (SomeException, try)
  30  import Control.Monad (forM_, unless, when)
  31  import Data.List (isPrefixOf)
  32  import Data.Map.Strict ()
  33  import qualified Data.Map.Strict as Map
  34  import Data.Maybe (fromMaybe)
  35  import Data.Set (Set)
  36  import qualified Data.Set as Set
  37  import Data.Text (Text)
  38  import qualified Data.Text as T
  39  import qualified Data.Text.IO as TIO
  40  import System.Directory (createDirectoryIfMissing)
  41  import System.Environment (getArgs, getEnvironment, getProgName)
  42  import System.Exit (ExitCode (..), exitFailure)
  43  import System.IO (hPutStrLn, stderr)
  44  import System.Process (CreateProcess (..), proc, readCreateProcessWithExitCode, readProcessWithExitCode)
  45  import Text.Read (readMaybe)
  46  
  47  import qualified Data.ByteString as BS
  48  import qualified Data.ByteString.Char8 as BC
  49  
  50  import qualified Armitage.Builder as Builder
  51  import qualified Armitage.CAS as CAS
  52  import qualified Armitage.DICE as DICE
  53  import qualified Armitage.Dhall as Dhall
  54  import qualified Armitage.LSP as LSP
  55  import qualified Armitage.Shim as Shim
  56  import qualified Armitage.Trace as Trace
  57  
  58  -- -----------------------------------------------------------------------------
  59  -- Main
  60  -- -----------------------------------------------------------------------------
  61  
  62  main :: IO ()
  63  main = do
  64      args <- getArgs
  65      case args of
  66          [] -> usage
  67          ("build" : rest) -> cmdBuild rest
  68          ("build-dhall" : rest) -> cmdBuildDhall rest
  69          ("analyze" : rest) -> cmdAnalyze rest
  70          ("shim" : rest) -> cmdShim rest
  71          ("lsp" : rest) -> cmdLSP rest
  72          ("run" : rest) -> cmdRun rest
  73          ("trace" : rest) -> cmdTrace rest
  74          ("unroll" : rest) -> cmdUnroll rest
  75          ("proxy" : rest) -> cmdProxy rest
  76          ("store" : rest) -> cmdStore rest
  77          ("cas" : rest) -> cmdCAS rest
  78          ("--help" : _) -> usage
  79          ("-h" : _) -> usage
  80          (cmd : _) -> do
  81              hPutStrLn stderr $ "Unknown command: " <> cmd
  82              usage
  83              exitFailure
  84  
  85  -- -----------------------------------------------------------------------------
  86  -- Commands
  87  -- -----------------------------------------------------------------------------
  88  
  89  -- | Build command (from .drv file)
  90  cmdBuild :: [String] -> IO ()
  91  cmdBuild args = case args of
  92      [] -> do
  93          hPutStrLn stderr "Usage: armitage build <derivation.drv>"
  94          exitFailure
  95      (drvPath : _) -> do
  96          putStrLn $ "Building: " <> drvPath
  97          putStrLn "TODO: Implement daemon-free build"
  98  
  99  -- result <- Builder.runBuild defaultConfig drvPath
 100  -- case result of
 101  --   Left err -> do
 102  --     hPutStrLn stderr $ "Build failed: " <> show err
 103  --     exitFailure
 104  --   Right result -> do
 105  --     putStrLn $ "Build succeeded"
 106  --     forM_ (Map.toList $ Builder.brOutputs result) $ \(name, path) ->
 107  --       putStrLn $ "  " <> T.unpack name <> ": " <> show path
 108  
 109  -- | Build command (from Dhall target file)
 110  cmdBuildDhall :: [String] -> IO ()
 111  cmdBuildDhall args = case args of
 112      [] -> do
 113          hPutStrLn stderr "Usage: armitage build-dhall <BUILD.dhall>"
 114          exitFailure
 115      (dhallPath : _) -> do
 116          putStrLn $ "Loading target from: " <> dhallPath
 117          result <- try $ Dhall.loadTarget dhallPath
 118          case result of
 119              Left (e :: SomeException) -> do
 120                  hPutStrLn stderr $ "Failed to load Dhall: " <> show e
 121                  exitFailure
 122              Right target -> do
 123                  let tc = Dhall.toolchain target
 124                  putStrLn $ "Target: " <> T.unpack (Dhall.targetName target)
 125                  putStrLn $ "Triple: " <> T.unpack (Dhall.renderTriple (Dhall.target tc))
 126                  case Dhall.renderGpu (Dhall.gpu (Dhall.target tc)) of
 127                      Just sm -> putStrLn $ "GPU: " <> T.unpack sm
 128                      Nothing -> pure ()
 129                  putStrLn $ "Coeffects: " <> show (length (Dhall.requires target)) <> " resource(s)"
 130                  forM_ (Dhall.requires target) $ \r ->
 131                      putStrLn $ "  - " <> showResource r
 132                  putStrLn ""
 133                  putStrLn "Converting to derivation..."
 134                  let _drv = Dhall.targetToDerivation target
 135                  putStrLn "TODO: Execute build"
 136    where
 137      showResource = \case
 138          Dhall.Resource_Pure -> "pure"
 139          Dhall.Resource_Network -> "network"
 140          Dhall.Resource_Auth p -> "auth:" <> T.unpack p
 141          Dhall.Resource_Sandbox s -> "sandbox:" <> T.unpack s
 142          Dhall.Resource_Filesystem p -> "fs:" <> T.unpack p
 143  
 144  -- | Proxy command
 145  cmdProxy :: [String] -> IO ()
 146  cmdProxy args = do
 147      putStrLn "Starting witness proxy..."
 148      putStrLn "TODO: Import and run proxy Main"
 149      -- For now, just explain what would happen
 150      let port = parsePort args
 151      putStrLn $ "Would listen on port " <> show port
 152      putStrLn "All fetches would be:"
 153      putStrLn "  - Intercepted (TLS MITM)"
 154      putStrLn "  - Content-hashed"
 155      putStrLn "  - Cached in CAS"
 156      putStrLn "  - Logged as attestations"
 157  
 158  -- | Store command
 159  cmdStore :: [String] -> IO ()
 160  cmdStore args = case args of
 161      [] -> do
 162          hPutStrLn stderr "Usage: armitage store <command>"
 163          hPutStrLn stderr "Commands:"
 164          hPutStrLn stderr "  add <path>       Add path to store"
 165          hPutStrLn stderr "  info <path>      Query path info"
 166          hPutStrLn stderr "  verify <path>    Verify path integrity"
 167          exitFailure
 168      ("add" : path : _) -> do
 169          putStrLn $ "Adding to store: " <> path
 170          putStrLn "TODO: Implement store add"
 171      ("info" : path : _) -> do
 172          putStrLn $ "Querying: " <> path
 173          putStrLn "TODO: Implement store info"
 174      ("verify" : path : _) -> do
 175          putStrLn $ "Verifying: " <> path
 176          putStrLn "TODO: Implement store verify"
 177      (cmd : _) -> do
 178          hPutStrLn stderr $ "Unknown store command: " <> cmd
 179          exitFailure
 180  
 181  -- | CAS command
 182  cmdCAS :: [String] -> IO ()
 183  cmdCAS args = case args of
 184      [] -> do
 185          hPutStrLn stderr "Usage: armitage cas <command> [--fly]"
 186          hPutStrLn stderr "Commands:"
 187          hPutStrLn stderr "  upload <file>    Upload blob to CAS"
 188          hPutStrLn stderr "  download <hash> <size>  Download blob from CAS"
 189          hPutStrLn stderr "  exists <hash> <size>    Check if blob exists"
 190          hPutStrLn stderr "  test             Run CAS integration test"
 191          hPutStrLn stderr ""
 192          hPutStrLn stderr "Options:"
 193          hPutStrLn stderr "  --fly            Use Fly.io deployment (aleph-cas.fly.dev)"
 194          exitFailure
 195      ("upload" : path : _) -> do
 196          putStrLn $ "Uploading to CAS: " <> path
 197          content <- BS.readFile path
 198          let digest = CAS.digestFromBytes content
 199          putStrLn $ "  hash: " <> T.unpack (CAS.digestHash digest)
 200          putStrLn $ "  size: " <> show (CAS.digestSize digest)
 201          CAS.withCASClient CAS.defaultConfig $ \client -> do
 202              CAS.uploadBlob client digest content
 203              putStrLn "Upload complete"
 204      ("download" : hash : sizeStr : rest) -> do
 205          let size = fromMaybe 0 (readMaybe sizeStr)
 206              digest = CAS.Digest (T.pack hash) size
 207              outPath = case rest of
 208                  (p : _) -> p
 209                  [] -> hash <> ".blob"
 210          putStrLn $ "Downloading from CAS: " <> hash
 211          CAS.withCASClient CAS.defaultConfig $ \client -> do
 212              result <- CAS.downloadBlob client digest
 213              case result of
 214                  Nothing -> do
 215                      hPutStrLn stderr "Blob not found"
 216                      exitFailure
 217                  Just content -> do
 218                      BS.writeFile outPath content
 219                      putStrLn $ "Downloaded " <> show (BS.length content) <> " bytes to " <> outPath
 220      ("exists" : hash : sizeStr : _) -> do
 221          let size = fromMaybe 0 (readMaybe sizeStr)
 222              digest = CAS.Digest (T.pack hash) size
 223          putStrLn $ "Checking CAS: " <> hash
 224          CAS.withCASClient CAS.defaultConfig $ \client -> do
 225              exists <- CAS.blobExists client digest
 226              if exists
 227                  then putStrLn "Blob exists"
 228                  else putStrLn "Blob NOT found"
 229      ("test" : rest) -> do
 230          let useFly = "--fly" `elem` rest
 231              config = if useFly then CAS.flyConfig else CAS.defaultConfig
 232          putStrLn $ "Running CAS integration test" <> (if useFly then " (Fly.io)" else " (local)") <> "..."
 233          putStrLn $ "  endpoint: " <> CAS.casHost config <> ":" <> show (CAS.casPort config)
 234          putStrLn ""
 235          CAS.withCASClient config $ \client -> do
 236              -- 1. Create test blob
 237              let testContent = "Hello from Armitage CAS test! " <> BC.pack (show (12345 :: Int))
 238                  digest = CAS.digestFromBytes testContent
 239              putStrLn $ "1. Test blob:"
 240              putStrLn $ "   content: " <> show testContent
 241              putStrLn $ "   hash:    " <> T.unpack (CAS.digestHash digest)
 242              putStrLn $ "   size:    " <> show (CAS.digestSize digest)
 243              putStrLn ""
 244  
 245              -- 2. Check if exists (should not)
 246              putStrLn "2. Checking if blob exists (expect: no)..."
 247              exists1 <- CAS.blobExists client digest
 248              putStrLn $ "   exists: " <> show exists1
 249              putStrLn ""
 250  
 251              -- 3. Upload
 252              putStrLn "3. Uploading blob..."
 253              CAS.uploadBlob client digest testContent
 254              putStrLn "   done"
 255              putStrLn ""
 256  
 257              -- 4. Check again (should exist now)
 258              putStrLn "4. Checking if blob exists (expect: yes)..."
 259              exists2 <- CAS.blobExists client digest
 260              putStrLn $ "   exists: " <> show exists2
 261              putStrLn ""
 262  
 263              -- 5. Download and verify
 264              putStrLn "5. Downloading blob..."
 265              result <- CAS.downloadBlob client digest
 266              case result of
 267                  Nothing -> putStrLn "   ERROR: Download failed"
 268                  Just downloaded -> do
 269                      putStrLn $ "   downloaded: " <> show downloaded
 270                      if downloaded == testContent
 271                          then putStrLn "   VERIFIED: Content matches!"
 272                          else putStrLn "   ERROR: Content mismatch!"
 273              putStrLn ""
 274  
 275              -- 6. FindMissingBlobs test
 276              putStrLn "6. Testing FindMissingBlobs..."
 277              let missingDigest = CAS.Digest "0000000000000000000000000000000000000000000000000000000000000000" 1
 278              missing <- CAS.findMissingBlobs client [digest, missingDigest]
 279              putStrLn $ "   queried: 2 blobs"
 280              putStrLn $ "   missing: " <> show (length missing)
 281              forM_ missing $ \d ->
 282                  putStrLn $ "     - " <> T.unpack (CAS.digestHash d)
 283              putStrLn ""
 284  
 285              putStrLn "CAS test complete!"
 286      (cmd : _) -> do
 287          hPutStrLn stderr $ "Unknown CAS command: " <> cmd
 288          exitFailure
 289  
 290  -- -----------------------------------------------------------------------------
 291  -- Helpers
 292  -- -----------------------------------------------------------------------------
 293  
 294  parsePort :: [String] -> Int
 295  parsePort = go 8080
 296    where
 297      go def [] = def
 298      go def ("--port" : p : rest) = fromMaybe (go def rest) (readMaybe p)
 299      go def ("-p" : p : rest) = fromMaybe (go def rest) (readMaybe p)
 300      go def (_ : rest) = go def rest
 301  
 302  -- | Shim command - run build with fake compilers, extract metadata
 303  cmdShim :: [String] -> IO ()
 304  cmdShim args = case args of
 305      [] -> shimUsage
 306      ("--help" : _) -> shimUsage
 307      ("-h" : _) -> shimUsage
 308      ("run" : rest) -> shimRun rest
 309      ("read" : path : _) -> shimRead path
 310      ("log" : _) -> shimLog
 311      ("env" : _) -> shimEnv
 312      ("analyze" : rest) -> shimAnalyzeCmd rest
 313      -- If first arg doesn't look like a subcommand, treat as flake ref
 314      (arg : rest)
 315          | not ("-" `isPrefixOf` arg) && not (arg `elem` ["run", "read", "log", "env", "analyze"]) ->
 316              shimAnalyzeCmd (arg : rest)
 317      (cmd : _) -> do
 318          hPutStrLn stderr $ "Unknown shim command: " <> cmd
 319          shimUsage
 320          exitFailure
 321  
 322  shimUsage :: IO ()
 323  shimUsage = do
 324      hPutStrLn stderr "Usage: armitage shim <flake-ref|command> [options]"
 325      hPutStrLn stderr ""
 326      hPutStrLn stderr "Analyze builds with shim compilers to extract perfect"
 327      hPutStrLn stderr "dependency information instantly."
 328      hPutStrLn stderr ""
 329      hPutStrLn stderr "Commands:"
 330      hPutStrLn stderr "  <flake-ref>          Analyze a Nix flake reference (full pipeline)"
 331      hPutStrLn stderr "  analyze <flake-ref>  Same as above, explicit form"
 332      hPutStrLn stderr "  run -- <build cmd>   Run arbitrary build with shims"
 333      hPutStrLn stderr "  read <file>          Read metadata from shim-generated file"
 334      hPutStrLn stderr "  log                  Show shim invocation log"
 335      hPutStrLn stderr "  env                  Print shim environment variables"
 336      hPutStrLn stderr ""
 337      hPutStrLn stderr "Options:"
 338      hPutStrLn stderr "  --no-validate        Skip strace validation"
 339      hPutStrLn stderr "  -v, --verbose        Verbose output"
 340      hPutStrLn stderr "  -o <file>            Write Dhall output to file"
 341      hPutStrLn stderr ""
 342      hPutStrLn stderr "Examples:"
 343      hPutStrLn stderr "  armitage shim nixpkgs#hello              # analyze hello"
 344      hPutStrLn stderr "  armitage shim nixpkgs#zlib -o BUILD.dhall"
 345      hPutStrLn stderr "  armitage shim run -- cmake --build build/"
 346      hPutStrLn stderr "  armitage shim read ./build/myapp"
 347      exitFailure
 348  
 349  -- | Run build with shim environment
 350  shimRun :: [String] -> IO ()
 351  shimRun args = do
 352      let (_opts, cmd) = break (== "--") args
 353          buildCmd = drop 1 cmd -- drop the "--"
 354      when (null buildCmd) $ do
 355          hPutStrLn stderr "Error: No build command specified after --"
 356          shimUsage
 357  
 358      -- Get shim paths from environment or use defaults
 359      let shimDir = "/tmp/armitage-shims"
 360          logPath = "/tmp/armitage-shim.log"
 361          shims =
 362              Shim.ShimPaths
 363                  { Shim.spCC = shimDir <> "/cc"
 364                  , Shim.spCXX = shimDir <> "/c++"
 365                  , Shim.spLD = shimDir <> "/ld"
 366                  , Shim.spAR = shimDir <> "/ar"
 367                  , Shim.spLogPath = logPath
 368                  }
 369  
 370      -- Clear log
 371      writeFile logPath ""
 372  
 373      putStrLn $ "Running with shims: " <> unwords buildCmd
 374      putStrLn $ "Log: " <> logPath
 375      putStrLn ""
 376  
 377      -- Build environment
 378      let shimEnvVars = Shim.generateShimEnv shims
 379      currentEnv <- getEnvironment
 380      let fullEnv = shimEnvVars ++ currentEnv
 381  
 382      -- Run the build
 383      case buildCmd of
 384          [] -> hPutStrLn stderr "No command to run"
 385          (exe : cmdArgs) -> do
 386              let p = (proc exe cmdArgs){env = Just fullEnv}
 387              (exitCode, _, _) <- readCreateProcessWithExitCode p ""
 388  
 389              case exitCode of
 390                  ExitSuccess -> do
 391                      putStrLn ""
 392                      putStrLn "Build completed. Reading metadata..."
 393                      -- Show summary from log
 394                      entries <- Shim.parseShimLog logPath
 395                      putStrLn $ "Shim invocations: " <> show (length entries)
 396                      let compiles = length [e | e <- entries, Shim.sleTool e == "CC"]
 397                          links = length [e | e <- entries, Shim.sleTool e == "LD"]
 398                          archives = length [e | e <- entries, Shim.sleTool e == "AR"]
 399                      putStrLn $ "  Compiles: " <> show compiles
 400                      putStrLn $ "  Links: " <> show links
 401                      putStrLn $ "  Archives: " <> show archives
 402                  ExitFailure code -> do
 403                      hPutStrLn stderr $ "Build failed with exit code " <> show code
 404  
 405  -- | Read metadata from a shim-generated file
 406  shimRead :: String -> IO ()
 407  shimRead path = do
 408      putStrLn $ "Reading metadata from: " <> path
 409  
 410      -- Try as executable first
 411      linkInfo <- Shim.readExecutableMetadata path
 412      case linkInfo of
 413          Just li -> do
 414              putStrLn ""
 415              putStrLn "Link metadata:"
 416              putStrLn $ "  Output: " <> T.unpack (Shim.liOutput li)
 417              putStrLn $ "  Objects: " <> show (length $ Shim.liObjects li)
 418              forM_ (Shim.liObjects li) $ \obj ->
 419                  putStrLn $ "    " <> T.unpack obj
 420              TIO.putStrLn $ "  Libraries: " <> T.intercalate ", " (Shim.liLibs li)
 421              putStrLn $ "  Lib paths: " <> show (length $ Shim.liLibPaths li)
 422              putStrLn ""
 423              putStrLn "Aggregated compile info:"
 424              putStrLn $ "  Sources: " <> show (length $ Shim.liAllSources li)
 425              forM_ (Shim.liAllSources li) $ \src ->
 426                  putStrLn $ "    " <> T.unpack src
 427              putStrLn $ "  Includes: " <> show (length $ Shim.liAllIncludes li)
 428              forM_ (take 10 $ Shim.liAllIncludes li) $ \inc ->
 429                  putStrLn $ "    " <> T.unpack inc
 430              when (length (Shim.liAllIncludes li) > 10) $
 431                  putStrLn $
 432                      "    ... and " <> show (length (Shim.liAllIncludes li) - 10) <> " more"
 433              return ()
 434          Nothing -> do
 435              -- Try as object
 436              objInfo <- Shim.readObjectMetadata path
 437              case objInfo of
 438                  Just ci -> do
 439                      putStrLn ""
 440                      putStrLn "Compile metadata:"
 441                      putStrLn $ "  Output: " <> T.unpack (Shim.ciOutput ci)
 442                      TIO.putStrLn $ "  Sources: " <> T.intercalate ", " (Shim.ciSources ci)
 443                      putStrLn $ "  Includes: " <> show (length $ Shim.ciIncludes ci)
 444                      forM_ (Shim.ciIncludes ci) $ \inc ->
 445                          putStrLn $ "    " <> T.unpack inc
 446                      TIO.putStrLn $ "  Defines: " <> T.intercalate ", " (Shim.ciDefines ci)
 447                      TIO.putStrLn $ "  Flags: " <> T.intercalate " " (Shim.ciFlags ci)
 448                  Nothing ->
 449                      putStrLn "No armitage metadata found in file"
 450  
 451  -- | Show shim invocation log
 452  shimLog :: IO ()
 453  shimLog = do
 454      let logPath = "/tmp/armitage-shim.log"
 455      entries <- Shim.parseShimLog logPath
 456      if null entries
 457          then putStrLn "No shim log entries found"
 458          else do
 459              putStrLn $ "Shim log (" <> show (length entries) <> " entries):"
 460              putStrLn ""
 461              forM_ entries $ \e -> do
 462                  TIO.putStrLn $
 463                      "["
 464                          <> Shim.sleTimestamp e
 465                          <> "] "
 466                          <> Shim.sleTool e
 467                          <> " "
 468                          <> T.intercalate " " (take 5 $ Shim.sleArgs e)
 469                          <> if length (Shim.sleArgs e) > 5 then " ..." else ""
 470  
 471  -- | Print shim environment
 472  shimEnv :: IO ()
 473  shimEnv = do
 474      let shimDir = "/tmp/armitage-shims"
 475          logPath = "/tmp/armitage-shim.log"
 476          shims =
 477              Shim.ShimPaths
 478                  { Shim.spCC = shimDir <> "/cc"
 479                  , Shim.spCXX = shimDir <> "/c++"
 480                  , Shim.spLD = shimDir <> "/ld"
 481                  , Shim.spAR = shimDir <> "/ar"
 482                  , Shim.spLogPath = logPath
 483                  }
 484      let envVars = Shim.generateShimEnv shims
 485      putStrLn "# Shim environment variables"
 486      putStrLn "# eval $(armitage shim env)"
 487      forM_ envVars $ \(k, v) ->
 488          putStrLn $ "export " <> k <> "=\"" <> v <> "\""
 489  
 490  -- | Analyze a flake reference with shims (full pipeline)
 491  shimAnalyzeCmd :: [String] -> IO ()
 492  shimAnalyzeCmd args = case args of
 493      [] -> do
 494          hPutStrLn stderr "Usage: armitage shim <flake-ref> [options]"
 495          hPutStrLn stderr ""
 496          hPutStrLn stderr "Analyze a Nix package with shim compilers to extract"
 497          hPutStrLn stderr "complete, validated dependency information."
 498          hPutStrLn stderr ""
 499          hPutStrLn stderr "Options:"
 500          hPutStrLn stderr "  --no-validate    Skip strace validation"
 501          hPutStrLn stderr "  -v, --verbose    Verbose output"
 502          hPutStrLn stderr "  -o <file>        Write Dhall output to file"
 503          hPutStrLn stderr ""
 504          hPutStrLn stderr "Examples:"
 505          hPutStrLn stderr "  armitage shim nixpkgs#hello"
 506          hPutStrLn stderr "  armitage shim nixpkgs#zlib --no-validate"
 507          hPutStrLn stderr "  armitage shim .#mypackage -o BUILD.dhall"
 508          exitFailure
 509      (flakeRef : rest) -> do
 510          let verbose = "--verbose" `elem` rest || "-v" `elem` rest
 511              noValidate = "--no-validate" `elem` rest
 512              outFile = parseOutputFile rest
 513              cfg =
 514                  Shim.defaultAnalysisConfig
 515                      { Shim.acVerbose = verbose
 516                      , Shim.acValidate = not noValidate
 517                      }
 518  
 519          putStrLn $ "Analyzing: " <> flakeRef
 520          putStrLn ""
 521  
 522          result <- Shim.shimAnalyze cfg (T.pack flakeRef)
 523          case result of
 524              Left err -> do
 525                  hPutStrLn stderr $ "Error: " <> T.unpack err
 526                  exitFailure
 527              Right ar -> do
 528                  -- Print summary
 529                  putStrLn "Analysis complete:"
 530                  TIO.putStrLn $ "  Derivation: " <> Shim.arDrvPath ar
 531                  case Shim.arOutputPath ar of
 532                      Just p -> TIO.putStrLn $ "  Output: " <> p
 533                      Nothing -> putStrLn "  Output: (shim build - no real output)"
 534                  putStrLn ""
 535  
 536                  putStrLn $ "Sources: " <> show (length $ Shim.arSources ar)
 537                  forM_ (take 10 $ Shim.arSources ar) $ \src ->
 538                      TIO.putStrLn $ "  " <> src
 539                  when (length (Shim.arSources ar) > 10) $
 540                      putStrLn $
 541                          "  ... and " <> show (length (Shim.arSources ar) - 10) <> " more"
 542                  putStrLn ""
 543  
 544                  putStrLn $ "Include paths: " <> show (length $ Shim.arIncludes ar)
 545                  forM_ (take 5 $ Shim.arIncludes ar) $ \inc ->
 546                      TIO.putStrLn $ "  " <> inc
 547                  when (length (Shim.arIncludes ar) > 5) $
 548                      putStrLn $
 549                          "  ... and " <> show (length (Shim.arIncludes ar) - 5) <> " more"
 550                  putStrLn ""
 551  
 552                  putStrLn $ "Libraries: " <> show (length $ Shim.arLibs ar)
 553                  forM_ (Shim.arLibs ar) $ \lib ->
 554                      TIO.putStrLn $ "  -l" <> lib
 555                  putStrLn ""
 556  
 557                  putStrLn $ "Shim invocations: " <> show (length $ Shim.arShimLog ar)
 558                  putStrLn ""
 559  
 560                  -- Validation results
 561                  case Shim.arValidation ar of
 562                      Nothing -> putStrLn "(validation skipped)"
 563                      Just v -> do
 564                          putStrLn "Validation:"
 565                          putStrLn $ "  Strace outputs: " <> show (Set.size $ Shim.vrStraceOutputs v)
 566                          putStrLn $ "  Strace artifacts: " <> show (Set.size $ Shim.vrStraceArtifacts v)
 567                          putStrLn $ "  Shim outputs: " <> show (Set.size $ Shim.vrShimOutputs v)
 568  
 569                          if Shim.vrMatches v
 570                              then putStrLn "  PASS: All artifacts captured by shim"
 571                              else do
 572                                  -- THIS IS A FAILURE, not a warning
 573                                  hPutStrLn stderr ""
 574                                  hPutStrLn stderr "VALIDATION FAILED"
 575                                  hPutStrLn stderr "Artifacts written to output that shim did not catch:"
 576                                  forM_ (Set.toList $ Shim.vrMissedArtifacts v) $ \f ->
 577                                      TIO.hPutStrLn stderr $ "  " <> f
 578                                  hPutStrLn stderr ""
 579                                  hPutStrLn stderr "This means a compiler/linker ran that we didn't shim."
 580                                  hPutStrLn stderr "Fix: add shim for the missing toolchain."
 581                                  exitFailure
 582  
 583                  -- Generate Dhall output
 584                  case outFile of
 585                      Nothing -> pure ()
 586                      Just path -> do
 587                          putStrLn ""
 588                          putStrLn $ "Writing Dhall to: " <> path
 589                          let dhall = analysisResultToDhall ar
 590                          TIO.writeFile path dhall
 591                          putStrLn "Done."
 592    where
 593      parseOutputFile [] = Nothing
 594      parseOutputFile ("-o" : f : _) = Just f
 595      parseOutputFile (_ : rest) = parseOutputFile rest
 596  
 597  {- | Convert analysis result to Dhall target definition (RFC-008 format)
 598  
 599  The output is the raw extracted data. No interpretation, no language guessing.
 600  The schema matches what shim actually captured.
 601  -}
 602  analysisResultToDhall :: Shim.AnalysisResult -> Text
 603  analysisResultToDhall ar =
 604      T.unlines
 605          [ "-- Generated by: armitage shim " <> Shim.arFlakeRef ar
 606          , "-- Derivation: " <> Shim.arDrvPath ar
 607          , "--"
 608          , "-- Raw extraction. No interpretation."
 609          , ""
 610          , "let Armitage = ./Armitage.dhall"
 611          , ""
 612          , "in Armitage.Extraction {"
 613          , "  , flakeRef = \"" <> escapeText (Shim.arFlakeRef ar) <> "\""
 614          , "  , derivation = \"" <> escapeText (Shim.arDrvPath ar) <> "\""
 615          , "  , sources = " <> listToDhall (Shim.arSources ar)
 616          , "  , includes = " <> listToDhall (Shim.arIncludes ar)
 617          , "  , defines = " <> listToDhall (Shim.arDefines ar)
 618          , "  , libs = " <> listToDhall (Shim.arLibs ar)
 619          , "  , libPaths = " <> listToDhall (Shim.arLibPaths ar)
 620          , "  , validated = " <> validatedToDhall (Shim.arValidation ar)
 621          , "}"
 622          ]
 623    where
 624      escapeText :: Text -> Text
 625      escapeText = T.replace "\\" "\\\\" . T.replace "\"" "\\\"" . T.replace "\n" "\\n"
 626  
 627      listToDhall :: [Text] -> Text
 628      listToDhall [] = "[] : List Text"
 629      listToDhall xs =
 630          let items = map (\x -> "\"" <> escapeText x <> "\"") xs
 631           in "[\n    " <> T.intercalate "\n  , " items <> "\n  ]"
 632  
 633      validatedToDhall :: Maybe Shim.ValidationResult -> Text
 634      validatedToDhall Nothing = "None Bool"
 635      validatedToDhall (Just v) =
 636          if Shim.vrMatches v
 637              then "Some True"
 638              else "Some False  -- FAILED: " <> T.pack (show (Set.size $ Shim.vrMissedArtifacts v)) <> " missed artifacts"
 639  
 640  -- | LSP command - start language server
 641  cmdLSP :: [String] -> IO ()
 642  cmdLSP args = do
 643      let verbose = "--verbose" `elem` args || "-v" `elem` args
 644          config = LSP.defaultLSPConfig{LSP.lcVerbose = verbose}
 645  
 646      -- Check for compile-commands subcommand
 647      case args of
 648          ("compile-commands" : rest) -> cmdCompileCommands rest
 649          _ -> do
 650              putStrLn "Starting armitage LSP server..."
 651              putStrLn "  Fallback enabled: tree-sitter + trace"
 652              putStrLn "  Diagnostics from: real compiler"
 653              putStrLn ""
 654              LSP.startLSP config
 655  
 656  -- | Generate compile_commands.json from shim build
 657  cmdCompileCommands :: [String] -> IO ()
 658  cmdCompileCommands args = case args of
 659      [] -> do
 660          hPutStrLn stderr "Usage: armitage lsp compile-commands <executable>"
 661          hPutStrLn stderr ""
 662          hPutStrLn stderr "Generate compile_commands.json from shim-built executable"
 663          exitFailure
 664      (target : rest) -> do
 665          let outFile = case rest of
 666                  ("-o" : f : _) -> f
 667                  _ -> "compile_commands.json"
 668  
 669          putStrLn $ "Reading metadata from: " <> target
 670          cmds <- LSP.generateCompileCommands "." target
 671          if null cmds
 672              then do
 673                  putStrLn "No compile commands found"
 674                  putStrLn "Make sure the target was built with armitage shims"
 675              else do
 676                  LSP.writeCompileCommands outFile cmds
 677                  putStrLn $ "Wrote " <> show (length cmds) <> " entries to " <> outFile
 678  
 679  -- | Analyze command - resolve deps and build action graph
 680  cmdAnalyze :: [String] -> IO ()
 681  cmdAnalyze args = case args of
 682      [] -> do
 683          hPutStrLn stderr "Usage: armitage analyze <BUILD.dhall>"
 684          exitFailure
 685      (dhallPath : _) -> do
 686          putStrLn $ "Analyzing: " <> dhallPath
 687          target <- Dhall.loadTarget dhallPath
 688  
 689          putStrLn $ "Target: " <> T.unpack (Dhall.targetName target)
 690          putStrLn ""
 691  
 692          -- Show deps before resolution
 693          putStrLn "Dependencies:"
 694          forM_ (Dhall.deps target) $ \dep -> case dep of
 695              Dhall.Dep_Local t -> putStrLn $ "  local: " <> T.unpack t
 696              Dhall.Dep_Flake t -> putStrLn $ "  flake: " <> T.unpack t
 697              Dhall.Dep_PkgConfig t -> putStrLn $ "  pkg-config: " <> T.unpack t
 698              Dhall.Dep_External _ dn -> putStrLn $ "  external: " <> T.unpack dn
 699          putStrLn ""
 700  
 701          -- Analyze (resolves flakes)
 702          putStrLn "Resolving flake references..."
 703          result <- DICE.analyze target
 704  
 705          -- Show resolution results
 706          if null (DICE.arErrors result)
 707              then do
 708                  putStrLn "Resolved:"
 709                  forM_ (DICE.arFlakes result) $ \rf -> do
 710                      putStrLn $ "  " <> T.unpack (DICE.rfRef rf) <> ":"
 711                      forM_ (Map.toList $ DICE.rfOutputs rf) $ \(name, path) ->
 712                          putStrLn $ "    " <> T.unpack name <> " -> " <> T.unpack path
 713                  putStrLn ""
 714  
 715                  -- Show action graph
 716                  let graph = DICE.arGraph result
 717                  putStrLn $ "Action graph: " <> show (length $ DICE.agActions graph) <> " action(s)"
 718                  forM_ (DICE.topoSort graph) $ \key -> do
 719                      let action = DICE.agActions graph Map.! key
 720                      putStrLn $
 721                          "  "
 722                              <> T.unpack (DICE.aIdentifier action)
 723                              <> " ["
 724                              <> show (DICE.aCategory action)
 725                              <> "]"
 726              else do
 727                  hPutStrLn stderr "Resolution errors:"
 728                  forM_ (DICE.arErrors result) $ \e ->
 729                      hPutStrLn stderr $ "  " <> T.unpack e
 730                  exitFailure
 731  
 732  -- | Run command - analyze and execute
 733  cmdRun :: [String] -> IO ()
 734  cmdRun args = case args of
 735      [] -> do
 736          hPutStrLn stderr "Usage: armitage run <BUILD.dhall>"
 737          exitFailure
 738      (dhallPath : _rest) -> do
 739          putStrLn $ "Loading: " <> dhallPath
 740          target <- Dhall.loadTarget dhallPath
 741  
 742          putStrLn $ "Analyzing: " <> T.unpack (Dhall.targetName target)
 743          analysisResult <- DICE.analyze target
 744  
 745          if not (null (DICE.arErrors analysisResult))
 746              then do
 747                  hPutStrLn stderr "Resolution failed:"
 748                  forM_ (DICE.arErrors analysisResult) $ \e ->
 749                      hPutStrLn stderr $ "  " <> T.unpack e
 750                  exitFailure
 751              else do
 752                  let graph = DICE.arGraph analysisResult
 753                  putStrLn $ "Executing " <> show (length $ DICE.agActions graph) <> " action(s)..."
 754                  putStrLn ""
 755  
 756                  -- All execution is witnessed
 757                  execResult <- DICE.executeGraphWitnessed defaultWitnessConfig graph
 758  
 759                  putStrLn $ "Cache hits: " <> show (DICE.erCacheHits execResult)
 760                  putStrLn $ "Executed:   " <> show (DICE.erExecuted execResult)
 761  
 762                  if null (DICE.erFailed execResult)
 763                      then do
 764                          putStrLn ""
 765                          putStrLn "Outputs:"
 766                          forM_ (Map.toList $ DICE.erOutputs execResult) $ \(_key, paths) ->
 767                              forM_ paths $ \p ->
 768                                  putStrLn $ "  " <> T.unpack p
 769  
 770                          -- Print attestations
 771                          putStrLn ""
 772                          putStrLn "━━━ Attestations ━━━"
 773                          forM_ (Map.toList $ DICE.erProofs execResult) $ \(key, proof) -> do
 774                              putStrLn ""
 775                              putStrLn $ "Action: " <> T.unpack (DICE.unActionKey key)
 776                              putStrLn $ "  build-id:    " <> T.unpack (Builder.dpBuildId proof)
 777                              putStrLn $ "  drv-hash:    " <> T.unpack (Builder.dpDerivationHash proof)
 778                              putStrLn $ "  started:     " <> show (Builder.dpStartTime proof)
 779                              putStrLn $ "  completed:   " <> show (Builder.dpEndTime proof)
 780                              putStrLn $ "  coeffects:   " <> renderCoeffects (Builder.dpCoeffects proof)
 781                              unless (null $ Builder.dpNetworkAccess proof) $ do
 782                                  putStrLn "  network:"
 783                                  forM_ (Builder.dpNetworkAccess proof) $ \na ->
 784                                      putStrLn $
 785                                          "    - "
 786                                              <> T.unpack (Builder.naMethod na)
 787                                              <> " "
 788                                              <> T.unpack (Builder.naUrl na)
 789                                              <> " ["
 790                                              <> T.unpack (Builder.naContentHash na)
 791                                              <> "]"
 792                              unless (null $ Builder.dpFilesystemAccess proof) $ do
 793                                  putStrLn "  filesystem:"
 794                                  forM_ (Builder.dpFilesystemAccess proof) $ \fa ->
 795                                      putStrLn $ "    - " <> show (Builder.faMode fa) <> " " <> Builder.faPath fa
 796                              let outHashes = Builder.dpOutputHashes proof
 797                              putStrLn $ "  outputs:     " <> show (length outHashes)
 798                              forM_ outHashes $ \(name, hash) ->
 799                                  putStrLn $ "    " <> T.unpack name <> ": " <> T.unpack hash
 800                      else do
 801                          hPutStrLn stderr ""
 802                          hPutStrLn stderr "Failures:"
 803                          forM_ (DICE.erFailed execResult) $ \(key, err) ->
 804                              hPutStrLn stderr $ "  " <> T.unpack (DICE.unActionKey key) <> ": " <> T.unpack err
 805                          exitFailure
 806  
 807  -- | Trace command - intercept build system via strace
 808  cmdTrace :: [String] -> IO ()
 809  cmdTrace args = case args of
 810      [] -> do
 811          hPutStrLn stderr "Usage: armitage trace [options] -- <build command>"
 812          hPutStrLn stderr ""
 813          hPutStrLn stderr "Options:"
 814          hPutStrLn stderr "  -o <file>    Output Dhall file (default: stdout)"
 815          hPutStrLn stderr "  -v           Verbose mode"
 816          hPutStrLn stderr ""
 817          hPutStrLn stderr "Example:"
 818          hPutStrLn stderr "  armitage trace -- cmake --build build/"
 819          hPutStrLn stderr "  armitage trace -o BUILD.dhall -- make -j8"
 820          exitFailure
 821      _ -> do
 822          let (opts, cmd) = parseTraceArgs args
 823              cfg = Trace.defaultTraceConfig{Trace.tcVerbose = "-v" `elem` opts}
 824              outputFile = parseOutputFile opts
 825  
 826          when (null cmd) $ do
 827              hPutStrLn stderr "Error: No build command specified after --"
 828              exitFailure
 829  
 830          putStrLn $ "Tracing: " <> unwords cmd
 831          putStrLn "Running build under strace..."
 832          putStrLn ""
 833  
 834          result <- Trace.traceCommand cfg cmd
 835          case result of
 836              Left err -> do
 837                  hPutStrLn stderr $ "Trace failed: " <> T.unpack err
 838                  exitFailure
 839              Right traceOutput -> do
 840                  let (compiles, links) = Trace.parseStrace cfg traceOutput
 841                  putStrLn $ "Captured:"
 842                  putStrLn $ "  " <> show (length compiles) <> " compile call(s)"
 843                  putStrLn $ "  " <> show (length links) <> " link call(s)"
 844  
 845                  -- Check for interpreter invocations
 846                  let fullTrace = Trace.parseFullTrace traceOutput
 847                      interpreterCalls = Trace.parseInterpreters cfg (Trace.ftExecves fullTrace)
 848                  unless (null interpreterCalls) $
 849                      putStrLn $
 850                          "  " <> show (length interpreterCalls) <> " interpreter call(s)"
 851                  putStrLn ""
 852  
 853                  -- Analyze into build graph
 854                  buildGraph <-
 855                      if null compiles && null links && not (null interpreterCalls)
 856                          then do
 857                              -- Use interpreter analysis
 858                              putStrLn "Analyzing interpreter execution..."
 859                              Trace.analyzeInterpreterTrace cfg fullTrace
 860                          else do
 861                              -- Use compiler analysis
 862                              when (null compiles && null links) $ do
 863                                  hPutStrLn stderr "Warning: No compiler/linker/interpreter calls detected"
 864                                  hPutStrLn stderr "Make sure the command does something traceable"
 865                              Trace.analyzeTrace cfg (compiles, links)
 866  
 867                  putStrLn $ "Extracted " <> show (length $ Trace.bgTargets buildGraph) <> " target(s)"
 868                  forM_ (Trace.bgTargets buildGraph) $ \t ->
 869                      putStrLn $ "  - " <> T.unpack (Trace.tName t)
 870                  putStrLn ""
 871  
 872                  -- Generate Dhall
 873                  let dhall = Trace.toDhall buildGraph
 874                  case outputFile of
 875                      Nothing -> do
 876                          putStrLn "Generated Dhall:"
 877                          putStrLn "────────────────────────────────────────"
 878                          TIO.putStrLn dhall
 879                      Just path -> do
 880                          Trace.toDhallFile path buildGraph
 881                          putStrLn $ "Wrote: " <> path
 882    where
 883      parseOutputFile [] = Nothing
 884      parseOutputFile ("-o" : f : _) = Just f
 885      parseOutputFile (_ : rest) = parseOutputFile rest
 886  
 887  -- | Parse trace args, splitting on --
 888  parseTraceArgs :: [String] -> ([String], [String])
 889  parseTraceArgs args =
 890      let (before, after) = break (== "--") args
 891       in (before, drop 1 after) -- drop the "--"
 892  
 893  -- | Unroll command - recursively trace a flake ref and all its build deps
 894  cmdUnroll :: [String] -> IO ()
 895  cmdUnroll args = case args of
 896      [] -> unrollUsage
 897      ("--help" : _) -> unrollUsage
 898      ("-h" : _) -> unrollUsage
 899      (flakeRef : _rest) | "-" `isPrefixOf` flakeRef -> unrollUsage
 900      (flakeRef : rest) -> do
 901          let outDir = parseOutDir rest
 902              maxDepth = parseDepth rest
 903              dryRun = "--dry-run" `elem` rest
 904  
 905          putStrLn $ "Unrolling: " <> flakeRef
 906          putStrLn $ "Output:    " <> outDir
 907          putStrLn $ "Max depth: " <> show maxDepth
 908          when dryRun $ putStrLn "DRY RUN - not actually building"
 909          putStrLn ""
 910  
 911          -- Get derivation info
 912          putStrLn "Querying derivation..."
 913          drvInfo <- getDrvInfo flakeRef
 914          case drvInfo of
 915              Left err -> do
 916                  hPutStrLn stderr $ "Failed to get derivation: " <> err
 917                  exitFailure
 918              Right info -> do
 919                  putStrLn $ "Derivation: " <> diDrvPath info
 920                  putStrLn $ "Builder:    " <> diBuilder info
 921                  putStrLn $ "Inputs:     " <> show (length $ diInputs info)
 922                  putStrLn ""
 923  
 924                  -- Recursively unroll
 925                  unless dryRun $ createDirectoryIfMissing True outDir
 926                  unrollRec outDir maxDepth 0 Set.empty dryRun info
 927    where
 928      parseOutDir [] = "./unrolled"
 929      parseOutDir ("-o" : d : _) = d
 930      parseOutDir (_ : rest) = parseOutDir rest
 931  
 932      parseDepth [] = 10
 933      parseDepth ("-d" : n : rest) = fromMaybe (parseDepth rest) (readMaybe n)
 934      parseDepth (_ : rest) = parseDepth rest
 935  
 936  unrollUsage :: IO ()
 937  unrollUsage = do
 938      hPutStrLn stderr "Usage: armitage unroll <flake-ref> [options]"
 939      hPutStrLn stderr ""
 940      hPutStrLn stderr "Options:"
 941      hPutStrLn stderr "  -o <dir>     Output directory (default: ./unrolled)"
 942      hPutStrLn stderr "  -d <depth>   Max recursion depth (default: 10)"
 943      hPutStrLn stderr "  --dry-run    Show what would be traced without building"
 944      hPutStrLn stderr ""
 945      hPutStrLn stderr "Examples:"
 946      hPutStrLn stderr "  armitage unroll nixpkgs#hello"
 947      hPutStrLn stderr "  armitage unroll nixpkgs#protobuf -o ./traced"
 948      hPutStrLn stderr "  armitage unroll .#mypackage --dry-run"
 949      exitFailure
 950  
 951  -- | Derivation info
 952  data DrvInfo = DrvInfo
 953      { diDrvPath :: String
 954      , diBuilder :: String
 955      , diInputs :: [String] -- Input derivation paths
 956      , diSrcs :: [String] -- Source paths (for tracing)
 957      }
 958      deriving (Show)
 959  
 960  -- | Get derivation info from flake ref or drv path
 961  getDrvInfo :: String -> IO (Either String DrvInfo)
 962  getDrvInfo ref = do
 963      -- If it's already a .drv path, query it directly
 964      if ".drv" `isSuffixOf` ref
 965          then getDrvInfoFromPath ref
 966          else do
 967              -- It's a flake ref - resolve to drv path first
 968              (code, out, err) <-
 969                  readProcessWithExitCode
 970                      "nix"
 971                      ["path-info", "--derivation", ref]
 972                      ""
 973              case code of
 974                  ExitFailure _ -> pure $ Left err
 975                  ExitSuccess -> getDrvInfoFromPath (T.unpack $ T.strip $ T.pack out)
 976    where
 977      isSuffixOf suffix s = suffix == drop (length s - length suffix) s
 978  
 979  -- | Get derivation info from a .drv store path
 980  getDrvInfoFromPath :: String -> IO (Either String DrvInfo)
 981  getDrvInfoFromPath drvPath = do
 982      (code, out, err) <-
 983          readProcessWithExitCode
 984              "nix"
 985              ["derivation", "show", drvPath]
 986              ""
 987      case code of
 988          ExitFailure _ -> pure $ Left err
 989          ExitSuccess -> pure $ parseDrvJson drvPath out
 990  
 991  {- | Parse derivation JSON (simplified)
 992  JSON format: {"derivations":{"<hash>-<name>.drv":{"inputs":{"drvs":{"<hash>-<name>.drv":{...}}}}}}
 993  -}
 994  parseDrvJson :: String -> String -> Either String DrvInfo
 995  parseDrvJson drvPath json =
 996      -- TODO: proper JSON parsing with Aeson
 997      -- For now, extract info with string matching
 998      Right
 999          DrvInfo
1000              { diDrvPath = drvPath
1001              , diBuilder = extractBuilder json
1002              , diInputs = extractInputDrvs drvPath json
1003              , diSrcs = []
1004              }
1005    where
1006      extractBuilder s =
1007          case T.breakOn "\"builder\":" (T.pack s) of
1008              (_, rest) ->
1009                  let afterColon = T.drop 10 rest -- drop '"builder":'
1010                      quoted = T.takeWhile (/= '"') $ T.drop 1 $ T.dropWhile (/= '"') afterColon
1011                   in T.unpack quoted
1012  
1013      -- Extract all drv hashes from JSON, excluding the top-level one
1014      extractInputDrvs topDrv s =
1015          let txt = T.pack s
1016              -- Split on ".drv" and look backwards for the hash-name
1017              parts = T.splitOn ".drv\"" txt
1018              -- Extract the hash-name before each ".drv"
1019              drvNames = concatMap extractDrvName (init' parts)
1020              -- Filter out the top-level derivation itself
1021              topHash = takeBaseName topDrv
1022           in map ("/nix/store/" <>) $ filter (/= topHash) drvNames
1023  
1024      extractDrvName part =
1025          -- The drv name is right before the .drv", quoted: "hash-name
1026          let reversed = T.reverse part
1027              -- Take until we hit a quote
1028              beforeQuote = T.takeWhile (/= '"') reversed
1029              drvName = T.unpack $ T.reverse beforeQuote
1030           in if isValidDrvHash drvName then [drvName <> ".drv"] else []
1031  
1032      -- Check if it looks like a valid drv hash (32 chars of base32)
1033      isValidDrvHash s = length s > 32 && all isBase32Char (take 32 s)
1034      isBase32Char c = c `elem` ("0123456789abcdfghijklmnpqrsvwxyz" :: String)
1035  
1036      init' [] = []
1037      init' xs = init xs
1038  
1039      takeBaseName p = reverse $ takeWhile (/= '/') $ reverse p
1040  
1041  -- | Recursively unroll derivation graph
1042  unrollRec :: FilePath -> Int -> Int -> Set String -> Bool -> DrvInfo -> IO ()
1043  unrollRec outDir maxDepth depth seen dryRun info
1044      | depth >= maxDepth = putStrLn $ indent <> "[max depth]"
1045      | diDrvPath info `Set.member` seen = putStrLn $ indent <> "(seen)"
1046      | otherwise = do
1047          let name = takeBaseName (diDrvPath info)
1048  
1049          -- Check if this is a fetch (no build to trace)
1050          if isFetch (diBuilder info)
1051              then putStrLn $ indent <> name <> " [fetch]"
1052              else do
1053                  putStrLn $ indent <> name
1054                  unless dryRun $ do
1055                      -- TODO: Actually build and trace
1056                      -- 1. nix-store --realise <drv>
1057                      -- 2. armitage trace -- <builder> <args>
1058                      -- 3. Write Dhall to outDir/<name>.dhall
1059                      pure ()
1060  
1061          -- Recurse into inputs
1062          let seen' = Set.insert (diDrvPath info) seen
1063          forM_ (diInputs info) $ \inputDrv -> do
1064              inputInfo <- getDrvInfo inputDrv
1065              case inputInfo of
1066                  Left _err -> putStrLn $ indent <> "  (failed: " <> takeBaseName inputDrv <> ")"
1067                  Right ii -> unrollRec outDir maxDepth (depth + 1) seen' dryRun ii
1068    where
1069      indent = replicate (depth * 2) ' '
1070      takeBaseName p = reverse $ takeWhile (/= '/') $ reverse p
1071      isFetch builder = any (`T.isInfixOf` T.pack builder) ["fetchurl", "curl", "fetch"]
1072  
1073  usage :: IO ()
1074  usage = do
1075      prog <- getProgName
1076      putStrLn $ "Usage: " <> prog <> " <command> [options]"
1077      putStrLn ""
1078      putStrLn "Daemon-free Nix operations"
1079      putStrLn ""
1080      putStrLn "Commands:"
1081      putStrLn "  build <drv>        Build derivation without daemon"
1082      putStrLn "  build-dhall <file> Build from Dhall target file"
1083      putStrLn "  analyze <file>     Analyze deps and build action graph"
1084      putStrLn "  shim <flake-ref>   Analyze with shims + strace validation (instant)"
1085      putStrLn "  lsp                Start LSP server with graceful degradation"
1086      putStrLn "  run <file>         Analyze and execute build"
1087      putStrLn "  trace -- <cmd>     Trace build via strace (verification)"
1088      putStrLn "  unroll <ref>       Recursively trace flake ref and deps"
1089      putStrLn "  proxy              Run witness proxy"
1090      putStrLn "  store <cmd>        Store operations"
1091      putStrLn "  cas <cmd>          Content-addressed storage"
1092      putStrLn ""
1093      putStrLn "Key insight: shims intercept compiler calls to capture deps instantly,"
1094      putStrLn "then strace validates completeness. Works with ANY build system."
1095      putStrLn ""
1096      putStrLn "The daemon is hostile infrastructure. armitage routes around it."
1097  
1098  -- | Render coeffects list to readable string
1099  renderCoeffects :: [Builder.Coeffect] -> String
1100  renderCoeffects [] = "pure"
1101  renderCoeffects cs = unwords $ map renderOne cs
1102    where
1103      renderOne = \case
1104          Builder.Pure -> "pure"
1105          Builder.Network -> "network"
1106          Builder.Auth t -> "auth:" <> T.unpack t
1107          Builder.Sandbox t -> "sandbox:" <> T.unpack t
1108          Builder.Filesystem p -> "fs:" <> p
1109          Builder.Combined xs -> "(" <> unwords (map renderOne xs) <> ")"
1110  
1111  {- | Default witness proxy configuration
1112  The proxy runs on the same host, these are constants.
1113  In container: /var/log/armitage, locally: /tmp/armitage
1114  -}
1115  defaultWitnessConfig :: DICE.WitnessConfig
1116  defaultWitnessConfig =
1117      DICE.WitnessConfig
1118          { DICE.wcProxyHost = "127.0.0.1"
1119          , DICE.wcProxyPort = 8888
1120          , DICE.wcCertFile = "/tmp/armitage/certs/ca.pem"
1121          , DICE.wcLogDir = "/tmp/armitage/log"
1122          }