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 }