never executed always true always false
1 module PureClaw.Tools.FileWrite
2 ( -- * Tool registration
3 fileWriteTool
4 ) where
5
6 import Control.Exception
7 import Data.Aeson
8 import Data.Aeson.Types
9 import Data.ByteString qualified as BS
10 import Data.Text (Text)
11 import Data.Text qualified as T
12 import Data.Text.Encoding qualified as TE
13
14 import PureClaw.Core.Types
15 import PureClaw.Handles.File
16 import PureClaw.Providers.Class
17 import PureClaw.Security.Path
18 import PureClaw.Tools.Registry
19
20 -- | Create a file write tool that writes files through SafePath validation.
21 fileWriteTool :: WorkspaceRoot -> FileHandle -> (ToolDefinition, ToolHandler)
22 fileWriteTool root fh = (def, handler)
23 where
24 def = ToolDefinition
25 { _td_name = "file_write"
26 , _td_description = "Write content to a file within the workspace. Creates the file if it does not exist, or overwrites if it does."
27 , _td_inputSchema = object
28 [ "type" .= ("object" :: Text)
29 , "properties" .= object
30 [ "path" .= object
31 [ "type" .= ("string" :: Text)
32 , "description" .= ("The file path relative to the workspace root" :: Text)
33 ]
34 , "content" .= object
35 [ "type" .= ("string" :: Text)
36 , "description" .= ("The content to write" :: Text)
37 ]
38 ]
39 , "required" .= (["path", "content"] :: [Text])
40 ]
41 }
42
43 handler = ToolHandler $ \input ->
44 case parseEither parseInput input of
45 Left err -> pure (T.pack err, True)
46 Right (path, content) -> do
47 pathResult <- mkSafePath root (T.unpack path)
48 case pathResult of
49 Left (PathDoesNotExist _) -> do
50 -- For writes, we allow creating new files. Use the raw path
51 -- but still validate it doesn't escape workspace.
52 writeNew root path content
53 Left pe -> pure (T.pack (show pe), True)
54 Right sp -> do
55 result <- try @SomeException (_fh_writeFile fh sp (TE.encodeUtf8 content))
56 case result of
57 Left e -> pure (T.pack (show e), True)
58 Right () -> pure ("Written to " <> path, False)
59
60 writeNew :: WorkspaceRoot -> Text -> Text -> IO (Text, Bool)
61 writeNew (WorkspaceRoot wr) path content = do
62 -- Create the file first so mkSafePath can validate it exists
63 let fullPath = wr <> "/" <> T.unpack path
64 result <- try @SomeException (TE.encodeUtf8 content `seq` pure ())
65 case result of
66 Left e -> pure (T.pack (show e), True)
67 Right () -> do
68 writeResult <- try @SomeException $ do
69 BS.writeFile fullPath (TE.encodeUtf8 content)
70 case writeResult of
71 Left e -> pure (T.pack (show e), True)
72 Right () -> do
73 -- Validate the written file is safe
74 validated <- mkSafePath (WorkspaceRoot wr) fullPath
75 case validated of
76 Left pe -> pure (T.pack (show pe), True)
77 Right _ -> pure ("Written to " <> path, False)
78
79 parseInput :: Value -> Parser (Text, Text)
80 parseInput = withObject "FileWriteInput" $ \o ->
81 (,) <$> o .: "path" <*> o .: "content"