2026-02-04 11:06:13 +00:00
package tools
import (
"context"
2026-02-23 09:09:53 +00:00
"errors"
2026-02-04 11:06:13 +00:00
"fmt"
2026-02-23 09:09:53 +00:00
"io/fs"
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
"regexp"
2026-02-04 11:06:13 +00:00
"strings"
)
2026-02-10 08:05:23 +00:00
// EditFileTool edits a file by replacing old_text with new_text.
// The old_text must exist exactly in the file.
type EditFileTool struct {
2026-02-23 09:09:53 +00:00
fs fileSystem
2026-02-10 08:05:23 +00:00
}
2026-02-04 11:06:13 +00:00
2026-02-10 08:05:23 +00:00
// NewEditFileTool creates a new EditFileTool with optional directory restriction.
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
func NewEditFileTool ( workspace string , restrict bool , allowPaths ... [ ] * regexp . Regexp ) * EditFileTool {
var patterns [ ] * regexp . Regexp
if len ( allowPaths ) > 0 {
patterns = allowPaths [ 0 ]
2026-02-23 09:09:53 +00:00
}
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
return & EditFileTool { fs : buildFs ( workspace , restrict , patterns ) }
2026-02-04 11:06:13 +00:00
}
func ( t * EditFileTool ) Name ( ) string {
return "edit_file"
}
func ( t * EditFileTool ) Description ( ) string {
2026-04-04 09:56:49 +00:00
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n."
2026-02-04 11:06:13 +00:00
}
2026-02-18 19:48:23 +00:00
func ( t * EditFileTool ) Parameters ( ) map [ string ] any {
return map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "object" ,
2026-02-18 19:48:23 +00:00
"properties" : map [ string ] any {
"path" : map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "string" ,
"description" : "The file path to edit" ,
} ,
2026-02-18 19:48:23 +00:00
"old_text" : map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "string" ,
2026-04-04 09:56:49 +00:00
"description" : "The exact text to find and replace. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n." ,
2026-02-04 11:06:13 +00:00
} ,
2026-02-18 19:48:23 +00:00
"new_text" : map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "string" ,
2026-04-04 09:56:49 +00:00
"description" : "The text to replace with. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n." ,
2026-02-04 11:06:13 +00:00
} ,
} ,
"required" : [ ] string { "path" , "old_text" , "new_text" } ,
}
}
2026-02-18 19:48:23 +00:00
func ( t * EditFileTool ) Execute ( ctx context . Context , args map [ string ] any ) * ToolResult {
2026-02-04 11:06:13 +00:00
path , ok := args [ "path" ] . ( string )
if ! ok {
2026-02-12 11:28:56 +00:00
return ErrorResult ( "path is required" )
2026-02-04 11:06:13 +00:00
}
oldText , ok := args [ "old_text" ] . ( string )
if ! ok {
2026-02-12 11:28:56 +00:00
return ErrorResult ( "old_text is required" )
2026-02-04 11:06:13 +00:00
}
newText , ok := args [ "new_text" ] . ( string )
if ! ok {
2026-02-12 11:28:56 +00:00
return ErrorResult ( "new_text is required" )
2026-02-04 11:06:13 +00:00
}
2026-02-23 09:09:53 +00:00
if err := editFile ( t . fs , path , oldText , newText ) ; err != nil {
2026-02-12 17:00:26 +00:00
return ErrorResult ( err . Error ( ) )
2026-02-10 08:05:23 +00:00
}
2026-02-12 11:28:56 +00:00
return SilentResult ( fmt . Sprintf ( "File edited: %s" , path ) )
2026-02-04 11:06:13 +00:00
}
2026-02-11 14:18:13 +00:00
type AppendFileTool struct {
2026-02-23 09:09:53 +00:00
fs fileSystem
2026-02-11 14:18:13 +00:00
}
2026-02-04 11:06:13 +00:00
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
func NewAppendFileTool ( workspace string , restrict bool , allowPaths ... [ ] * regexp . Regexp ) * AppendFileTool {
var patterns [ ] * regexp . Regexp
if len ( allowPaths ) > 0 {
patterns = allowPaths [ 0 ]
2026-02-23 09:09:53 +00:00
}
fix(tools): allow /dev/null redirection and add read/write sandbox split (#967)
* fix(tools): allow /dev/null redirection and add read/write sandbox split
- Remove deny pattern that incorrectly blocked redirects to /dev/null
- Expand block device write pattern to cover nvme, mmcblk, vd, xvd,
hd, loop, dm-, md, sr and nbd in addition to sd
- Add safe path whitelist for kernel pseudo-devices so workspace path
check does not reject /dev/null, /dev/zero, /dev/random, /dev/urandom,
/dev/stdin, /dev/stdout and /dev/stderr
- Add allow_read_outside_workspace config option (default true) so file
read and list tools are unrestricted while write tools stay sandboxed
Closes https://github.com/sipeed/picoclaw/issues/964
Closes https://github.com/sipeed/picoclaw/issues/965
Signed-off-by: Huang Rui <vowstar@gmail.com>
* feat(tools): add configurable allow patterns and path whitelists
- Add custom_allow_patterns to exec config so users can exempt specific
commands from deny pattern checks
- Add allow_read_paths and allow_write_paths regex lists to tools config
for whitelisting specific paths outside the workspace
- Introduce whitelistFs that wraps sandboxFs and falls through to hostFs
for paths matching whitelist patterns
- Use variadic constructor signatures to keep backward compatibility
Suggested-by: lxowalle
Signed-off-by: Huang Rui <vowstar@gmail.com>
---------
Signed-off-by: Huang Rui <vowstar@gmail.com>
2026-03-02 04:22:02 +00:00
return & AppendFileTool { fs : buildFs ( workspace , restrict , patterns ) }
2026-02-04 11:06:13 +00:00
}
func ( t * AppendFileTool ) Name ( ) string {
return "append_file"
}
func ( t * AppendFileTool ) Description ( ) string {
2026-04-04 09:56:49 +00:00
return "Append content to the end of a file. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n."
2026-02-04 11:06:13 +00:00
}
2026-02-18 19:48:23 +00:00
func ( t * AppendFileTool ) Parameters ( ) map [ string ] any {
return map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "object" ,
2026-02-18 19:48:23 +00:00
"properties" : map [ string ] any {
"path" : map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "string" ,
"description" : "The file path to append to" ,
} ,
2026-02-18 19:48:23 +00:00
"content" : map [ string ] any {
2026-02-04 11:06:13 +00:00
"type" : "string" ,
2026-04-04 09:56:49 +00:00
"description" : "The content to append. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n." ,
2026-02-04 11:06:13 +00:00
} ,
} ,
"required" : [ ] string { "path" , "content" } ,
}
}
2026-02-18 19:48:23 +00:00
func ( t * AppendFileTool ) Execute ( ctx context . Context , args map [ string ] any ) * ToolResult {
2026-02-04 11:06:13 +00:00
path , ok := args [ "path" ] . ( string )
if ! ok {
2026-02-12 11:28:56 +00:00
return ErrorResult ( "path is required" )
2026-02-04 11:06:13 +00:00
}
content , ok := args [ "content" ] . ( string )
if ! ok {
2026-02-12 11:28:56 +00:00
return ErrorResult ( "content is required" )
2026-02-04 11:06:13 +00:00
}
2026-02-23 09:09:53 +00:00
if err := appendFile ( t . fs , path , content ) ; err != nil {
2026-02-12 17:00:26 +00:00
return ErrorResult ( err . Error ( ) )
2026-02-11 14:18:13 +00:00
}
2026-02-23 09:09:53 +00:00
return SilentResult ( fmt . Sprintf ( "Appended to %s" , path ) )
}
2026-02-04 11:06:13 +00:00
2026-02-23 09:09:53 +00:00
// editFile reads the file via sysFs, performs the replacement, and writes back.
// It uses a fileSystem interface, allowing the same logic for both restricted and unrestricted modes.
func editFile ( sysFs fileSystem , path , oldText , newText string ) error {
content , err := sysFs . ReadFile ( path )
2026-02-04 11:06:13 +00:00
if err != nil {
2026-02-23 09:09:53 +00:00
return err
2026-02-04 11:06:13 +00:00
}
2026-02-23 09:09:53 +00:00
newContent , err := replaceEditContent ( content , oldText , newText )
if err != nil {
return err
2026-02-04 11:06:13 +00:00
}
2026-02-23 09:09:53 +00:00
return sysFs . WriteFile ( path , newContent )
}
// appendFile reads the existing content (if any) via sysFs, appends new content, and writes back.
func appendFile ( sysFs fileSystem , path , appendContent string ) error {
content , err := sysFs . ReadFile ( path )
if err != nil && ! errors . Is ( err , fs . ErrNotExist ) {
return err
}
newContent := append ( content , [ ] byte ( appendContent ) ... )
return sysFs . WriteFile ( path , newContent )
}
// replaceEditContent handles the core logic of finding and replacing a single occurrence of oldText.
func replaceEditContent ( content [ ] byte , oldText , newText string ) ( [ ] byte , error ) {
contentStr := string ( content )
if ! strings . Contains ( contentStr , oldText ) {
return nil , fmt . Errorf ( "old_text not found in file. Make sure it matches exactly" )
}
count := strings . Count ( contentStr , oldText )
if count > 1 {
return nil , fmt . Errorf ( "old_text appears %d times. Please provide more context to make it unique" , count )
}
newContent := strings . Replace ( contentStr , oldText , newText , 1 )
return [ ] byte ( newContent ) , nil
2026-02-04 11:06:13 +00:00
}