never executed always true always false
1 module PureClaw.Tools.Shell
2 ( -- * Tool registration
3 shellTool
4 ) where
5
6 import Control.Exception
7 import Data.Aeson
8 import Data.Aeson.Types
9 import Data.ByteString.Char8 qualified as BS8
10 import Data.Text (Text)
11 import Data.Text qualified as T
12 import System.Exit
13
14 import PureClaw.Handles.Shell
15 import PureClaw.Providers.Class
16 import PureClaw.Security.Command
17 import PureClaw.Security.Policy
18 import PureClaw.Tools.Registry
19
20 -- | Create a shell tool that executes commands through the security policy.
21 shellTool :: SecurityPolicy -> ShellHandle -> (ToolDefinition, ToolHandler)
22 shellTool policy sh = (def, handler)
23 where
24 def = ToolDefinition
25 { _td_name = "shell"
26 , _td_description = "Execute a shell command. The command is validated against the security policy before execution."
27 , _td_inputSchema = object
28 [ "type" .= ("object" :: Text)
29 , "properties" .= object
30 [ "command" .= object
31 [ "type" .= ("string" :: Text)
32 , "description" .= ("The shell command to execute" :: Text)
33 ]
34 ]
35 , "required" .= (["command"] :: [Text])
36 ]
37 }
38
39 handler = ToolHandler $ \input -> do
40 case parseEither parseInput input of
41 Left err -> pure (T.pack err, True)
42 Right cmd -> do
43 let parts = T.words cmd
44 case parts of
45 [] -> pure ("Empty command", True)
46 (prog:args) ->
47 case authorize policy (T.unpack prog) args of
48 Left (CommandNotAllowed c) ->
49 pure ("Command not allowed: " <> c, True)
50 Left CommandInAutonomyDeny ->
51 pure ("All commands denied by security policy", True)
52 Right authorized -> do
53 result <- try @SomeException (_sh_execute sh authorized)
54 case result of
55 Left e -> pure (T.pack (show e), True)
56 Right pr -> do
57 let out = T.pack (BS8.unpack (_pr_stdout pr))
58 err = T.pack (BS8.unpack (_pr_stderr pr))
59 exitInfo = case _pr_exitCode pr of
60 ExitSuccess -> ""
61 ExitFailure n -> "\nExit code: " <> T.pack (show n)
62 combined = T.strip (out <> err <> exitInfo)
63 pure (if T.null combined then "(no output)" else combined, False)
64
65 parseInput :: Value -> Parser Text
66 parseInput = withObject "ShellInput" $ \o -> o .: "command"