Merge PR #1874 with conflict resolution
💘 Generated with Crush
Assisted-by: MiniMax-M2.5 via Crush <crush@charm.land>
This commit is contained in:
commit
4f2d7bf6f8
11 changed files with 1033 additions and 0 deletions
0
docker/entrypoint.sh
Normal file → Executable file
0
docker/entrypoint.sh
Normal file → Executable file
|
|
@ -91,6 +91,9 @@ func NewAgentInstance(
|
||||||
toolsRegistry.Register(execTool)
|
toolsRegistry.Register(execTool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.IsToolEnabled("execline") {
|
||||||
|
toolsRegistry.Register(tools.NewExeclineTool(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Tools.IsToolEnabled("edit_file") {
|
if cfg.Tools.IsToolEnabled("edit_file") {
|
||||||
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths))
|
||||||
|
|
|
||||||
|
|
@ -829,6 +829,16 @@ type ExecConfig struct {
|
||||||
TimeoutSeconds int ` env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s)
|
TimeoutSeconds int ` env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExeclineConfig struct {
|
||||||
|
ToolConfig ` json:","`
|
||||||
|
DenyDefaultsEnable bool `json:"deny_defaults_enable"`
|
||||||
|
Deny []string `json:"deny"`
|
||||||
|
Allow []string `json:"allow"`
|
||||||
|
TimeoutSeconds int `json:"timeout_seconds"`
|
||||||
|
EnvSet map[string]string `json:"env_set"`
|
||||||
|
EnvAllowlist []string `json:"env_allowlist"`
|
||||||
|
}
|
||||||
|
|
||||||
type SkillsToolsConfig struct {
|
type SkillsToolsConfig struct {
|
||||||
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_SKILLS_"`
|
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_SKILLS_"`
|
||||||
Registries SkillsRegistriesConfig ` json:"registries"`
|
Registries SkillsRegistriesConfig ` json:"registries"`
|
||||||
|
|
@ -854,6 +864,7 @@ type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
Cron CronToolsConfig `json:"cron"`
|
Cron CronToolsConfig `json:"cron"`
|
||||||
Exec ExecConfig `json:"exec"`
|
Exec ExecConfig `json:"exec"`
|
||||||
|
Execline ExeclineConfig `json:"execline"`
|
||||||
Skills SkillsToolsConfig `json:"skills"`
|
Skills SkillsToolsConfig `json:"skills"`
|
||||||
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
MediaCleanup MediaCleanupConfig `json:"media_cleanup"`
|
||||||
MCP MCPConfig `json:"mcp"`
|
MCP MCPConfig `json:"mcp"`
|
||||||
|
|
@ -1327,6 +1338,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
||||||
return t.Cron.Enabled
|
return t.Cron.Enabled
|
||||||
case "exec":
|
case "exec":
|
||||||
return t.Exec.Enabled
|
return t.Exec.Enabled
|
||||||
|
case "execline":
|
||||||
|
return t.Execline.Enabled
|
||||||
case "skills":
|
case "skills":
|
||||||
return t.Skills.Enabled
|
return t.Skills.Enabled
|
||||||
case "media_cleanup":
|
case "media_cleanup":
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,13 @@ func DefaultConfig() *Config {
|
||||||
AllowRemote: true,
|
AllowRemote: true,
|
||||||
TimeoutSeconds: 60,
|
TimeoutSeconds: 60,
|
||||||
},
|
},
|
||||||
|
Execline: ExeclineConfig{
|
||||||
|
ToolConfig: ToolConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
DenyDefaultsEnable: true,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
},
|
||||||
Skills: SkillsToolsConfig{
|
Skills: SkillsToolsConfig{
|
||||||
ToolConfig: ToolConfig{
|
ToolConfig: ToolConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|
|
||||||
235
pkg/tools/execline.go
Normal file
235
pkg/tools/execline.go
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/tools/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default deny patterns for execline (Linux only - no variable expansion blocks needed)
|
||||||
|
var defaultExeclineDenyPatterns = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
|
||||||
|
regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`),
|
||||||
|
regexp.MustCompile(`\bdd\s+if=`),
|
||||||
|
// Block device writes
|
||||||
|
regexp.MustCompile(
|
||||||
|
`>\s*/dev/(sd[a-z]|hd[a-z]|vd[a-z]|xvd[a-z]|nvme\d|mmcblk\d|loop\d|dm-\d|md\d|sr\d|nbd\d)`,
|
||||||
|
),
|
||||||
|
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
||||||
|
regexp.MustCompile(`\bsudo\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+run\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+exec\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+push\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+force\b`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExeclineTool executes commands using execlineb instead of shell
|
||||||
|
// Security: execlineb does not support variable expansion ($VAR) or command
|
||||||
|
// substitution $(cmd) by default, reducing attack surface significantly.
|
||||||
|
type ExeclineTool struct {
|
||||||
|
config *config.Config
|
||||||
|
denyPatterns []*regexp.Regexp
|
||||||
|
allowPatterns []*regexp.Regexp
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExeclineTool creates a new ExeclineTool instance
|
||||||
|
func NewExeclineTool(cfg *config.Config) *ExeclineTool {
|
||||||
|
// Start with default deny patterns only if enabled
|
||||||
|
var denyPatterns []*regexp.Regexp
|
||||||
|
if cfg.Tools.Execline.DenyDefaultsEnable {
|
||||||
|
denyPatterns = append([]*regexp.Regexp{}, defaultExeclineDenyPatterns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom deny patterns from config
|
||||||
|
for _, p := range cfg.Tools.Execline.Deny {
|
||||||
|
if r, err := regexp.Compile(p); err == nil {
|
||||||
|
denyPatterns = append(denyPatterns, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile allow patterns
|
||||||
|
var allowPatterns []*regexp.Regexp
|
||||||
|
for _, p := range cfg.Tools.Execline.Allow {
|
||||||
|
if r, err := regexp.Compile(p); err == nil {
|
||||||
|
allowPatterns = append(allowPatterns, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default timeout 60s
|
||||||
|
timeout := 60 * time.Second
|
||||||
|
if cfg.Tools.Execline.TimeoutSeconds > 0 {
|
||||||
|
timeout = time.Duration(cfg.Tools.Execline.TimeoutSeconds) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ExeclineTool{
|
||||||
|
config: cfg,
|
||||||
|
denyPatterns: denyPatterns,
|
||||||
|
allowPatterns: allowPatterns,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the tool
|
||||||
|
func (t *ExeclineTool) Name() string {
|
||||||
|
return "execline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the tool description
|
||||||
|
func (t *ExeclineTool) Description() string {
|
||||||
|
return `Execute commands using execlineb - a secure, minimal shell that does not expand variables or command substitution. Use for simple commands without shell features.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters returns the tool parameters
|
||||||
|
func (t *ExeclineTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"command": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The command to execute (passed as-is, no shell expansion)",
|
||||||
|
},
|
||||||
|
"working_dir": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional working directory for the command",
|
||||||
|
},
|
||||||
|
"env": map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Additional environment variables to set for this command. Do not try to set PICOCLAW*, PATH, HOME, USER, LOGNAME, SHELL, LD_PRELOAD, or LD_LIBRARY_PATH",
|
||||||
|
"additionalProperties": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"command"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs a command using execlineb
|
||||||
|
// Commands are executed as-is without shell expansion
|
||||||
|
func (t *ExeclineTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
// Extract command from args
|
||||||
|
cmdArg, ok := args["command"]
|
||||||
|
if !ok {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "Missing 'command' argument",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("missing 'command' argument"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the command string
|
||||||
|
command, ok := cmdArg.(string)
|
||||||
|
if !ok {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "'command' must be a string",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("'command' must be a string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: "Empty command",
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("empty command"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous patterns in the command
|
||||||
|
if err := t.validateCommand(command); err != nil {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: fmt.Sprintf("Validation error: %v", err),
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("validation error: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build safe environment
|
||||||
|
baseEnv := shell.WithAllowedEnv(nil, nil)
|
||||||
|
var extraEnv map[string]string
|
||||||
|
if t.config != nil {
|
||||||
|
extraEnv = t.config.Tools.Execline.EnvSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse env param from LLM (if provided)
|
||||||
|
if envArg, ok := args["env"].(map[string]any); ok && envArg != nil {
|
||||||
|
if extraEnv == nil {
|
||||||
|
extraEnv = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range envArg {
|
||||||
|
if strVal, ok := v.(string); ok {
|
||||||
|
extraEnv[k] = strVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add PICOCLAW_EXEC_TIME - timestamp when command is executed
|
||||||
|
execTimeEnv := map[string]string{
|
||||||
|
"PICOCLAW_EXEC_TIME": time.Now().Format(time.RFC3339),
|
||||||
|
"PICOCLAW_EXEC_TIMEOUT": t.timeout.String(),
|
||||||
|
}
|
||||||
|
execEnv := shell.MergeEnvVars(baseEnv, execTimeEnv, extraEnv)
|
||||||
|
|
||||||
|
// Use execlineb to execute
|
||||||
|
// execlineb -c takes a command string and executes it
|
||||||
|
// Unlike sh -c, it doesn't expand $VAR or $(cmd)
|
||||||
|
// Add timeout to context
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "/usr/bin/execlineb", "-c", command)
|
||||||
|
cmd.Env = shell.MapToEnvSlice(execEnv)
|
||||||
|
|
||||||
|
// Set working directory if provided
|
||||||
|
if wd, ok := args["working_dir"].(string); ok && wd != "" {
|
||||||
|
cmd.Dir = wd
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: string(output),
|
||||||
|
ForUser: string(output),
|
||||||
|
IsError: true,
|
||||||
|
Err: fmt.Errorf("execlineb error: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: string(output),
|
||||||
|
ForUser: string(output),
|
||||||
|
IsError: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCommand checks for dangerous command patterns
|
||||||
|
// Since execline is secure by default, we only block obvious exploits
|
||||||
|
func (t *ExeclineTool) validateCommand(cmd string) error {
|
||||||
|
// Note: $VAR and $(cmd) are simply not expanded by execlineb
|
||||||
|
// They are passed literally to the command, so this is safe
|
||||||
|
|
||||||
|
// Check custom allow patterns first (can override deny)
|
||||||
|
explicitlyAllowed := false
|
||||||
|
for _, pattern := range t.allowPatterns {
|
||||||
|
if pattern.MatchString(cmd) {
|
||||||
|
explicitlyAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !explicitlyAllowed {
|
||||||
|
// Check custom deny patterns
|
||||||
|
for _, pattern := range t.denyPatterns {
|
||||||
|
if pattern.MatchString(cmd) {
|
||||||
|
return fmt.Errorf("command matches blocked pattern")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
249
pkg/tools/shell/env.go
Normal file
249
pkg/tools/shell/env.go
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultEnvAllowlist is the set of environment variable names that are safe
|
||||||
|
// to propagate to child processes. Everything else is stripped to prevent
|
||||||
|
// accidental credential leakage.
|
||||||
|
//
|
||||||
|
// To add a new variable:
|
||||||
|
// 1. Add to this map if it's safe to pass through
|
||||||
|
// 2. Or add a prefix to defaultEnvAllowPrefixes for pattern matching
|
||||||
|
// Note: Do NOT add wildcard patterns like "*_API_KEY" here - use explicit names
|
||||||
|
// to avoid accidentally leaking secrets.
|
||||||
|
var DefaultEnvAllowlist = map[string]bool{
|
||||||
|
"PATH": true,
|
||||||
|
"HOME": true,
|
||||||
|
"USER": true,
|
||||||
|
"LANG": true,
|
||||||
|
"SHELL": true,
|
||||||
|
"TERM": true,
|
||||||
|
"PWD": true,
|
||||||
|
"OLDPWD": true,
|
||||||
|
"HOSTNAME": true,
|
||||||
|
"LOGNAME": true,
|
||||||
|
"TZ": true,
|
||||||
|
"DISPLAY": true,
|
||||||
|
"TMPDIR": true,
|
||||||
|
"EDITOR": true,
|
||||||
|
"PAGER": true,
|
||||||
|
"HTTP_PROXY": true,
|
||||||
|
"http_proxy": true,
|
||||||
|
"HTTPS_PROXY": true,
|
||||||
|
"https_proxy": true,
|
||||||
|
"NO_PROXY": true,
|
||||||
|
"no_proxy": true,
|
||||||
|
|
||||||
|
// Locale
|
||||||
|
"LC_ALL": true,
|
||||||
|
"LC_CTYPE": true,
|
||||||
|
"LC_MESSAGES": true,
|
||||||
|
"LC_MONETARY": true,
|
||||||
|
"LC_NUMERIC": true,
|
||||||
|
"LC_TIME": true,
|
||||||
|
"LC_PAPER": true,
|
||||||
|
"LC_NAME": true,
|
||||||
|
"LC_ADDRESS": true,
|
||||||
|
"LC_TELEPHONE": true,
|
||||||
|
"LC_MEASUREMENT": true,
|
||||||
|
"LC_IDENTIFICATION": true,
|
||||||
|
"LC_COLLATE": true,
|
||||||
|
|
||||||
|
// systemd/user session (for systemctl --user and journalctl --user)
|
||||||
|
"XDG_RUNTIME_DIR": true,
|
||||||
|
"DBUS_SESSION_BUS_ADDRESS": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBlocklist is the set of environment variable names that the LLM
|
||||||
|
// cannot override, even if passed via the env parameter. These vars
|
||||||
|
// control fundamental process behavior and could be exploited.
|
||||||
|
var LLMBlocklist = map[string]bool{
|
||||||
|
"PATH": true, // Could hijack command resolution
|
||||||
|
"HOME": true, // Could redirect file access
|
||||||
|
"USER": true, // Could impersonate user
|
||||||
|
"LOGNAME": true, // Could impersonate user
|
||||||
|
"SHELL": true, // Could change shell behavior
|
||||||
|
"LD_PRELOAD": true, // Could inject code
|
||||||
|
"LD_LIBRARY_PATH": true, // Could hijack library resolution
|
||||||
|
"LD_AUDIT": true, // Could inject code
|
||||||
|
"LD_DEBUG": true, // Could leak info
|
||||||
|
|
||||||
|
// PICOCLAW_* vars - controlled by the agent, not LLM
|
||||||
|
"PICOCLAW_HOME": true,
|
||||||
|
"PICOCLAW_CONFIG": true,
|
||||||
|
"PICOCLAW_AGENT_WORKSPACE": true,
|
||||||
|
"PICOCLAW_EXE": true,
|
||||||
|
"PICOCLAW_SERVICE_NAME": true,
|
||||||
|
"PICOCLAW_EXEC_TIME": true,
|
||||||
|
"PICOCLAW_EXEC_TIMEOUT": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsEnvAllowlist contains additional variables needed on Windows.
|
||||||
|
var windowsEnvAllowlist = map[string]bool{
|
||||||
|
"PATHEXT": true,
|
||||||
|
"SYSTEMROOT": true,
|
||||||
|
"SYSTEMDRIVE": true,
|
||||||
|
"COMSPEC": true,
|
||||||
|
"APPDATA": true,
|
||||||
|
"USERPROFILE": true,
|
||||||
|
"HOMEDRIVE": true,
|
||||||
|
"HOMEPATH": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowedEnv builds a map of allowed environment variables by looking them up.
|
||||||
|
// This is more efficient than filtering os.Environ() with string parsing.
|
||||||
|
// It starts with the provided env map, then adds allowed inherited vars (if not set).
|
||||||
|
// extraAllowlist adds to the default allowlist.
|
||||||
|
func WithAllowedEnv(envSet map[string]string, extraAllowlist []string) map[string]string {
|
||||||
|
// Copy the map to avoid mutating the caller's map
|
||||||
|
result := maps.Clone(envSet)
|
||||||
|
if result == nil {
|
||||||
|
result = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default allowlist (only if not already set)
|
||||||
|
for k := range DefaultEnvAllowlist {
|
||||||
|
if _, exists := result[k]; !exists {
|
||||||
|
if val := os.Getenv(k); val != "" {
|
||||||
|
result[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add Windows-specific vars
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
for k := range windowsEnvAllowlist {
|
||||||
|
if _, exists := result[k]; !exists {
|
||||||
|
if val := os.Getenv(k); val != "" {
|
||||||
|
result[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add extra allowlist from config
|
||||||
|
for _, k := range extraAllowlist {
|
||||||
|
if _, exists := result[k]; !exists {
|
||||||
|
if val := os.Getenv(k); val != "" {
|
||||||
|
result[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBlocklistPrefixes are env var prefixes that the LLM cannot override.
|
||||||
|
var LLMBlocklistPrefixes = []string{
|
||||||
|
"PICOCLAW_",
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBlocked returns true if the key is in the blocklist or matches a blocked prefix.
|
||||||
|
func isBlocked(key string) bool {
|
||||||
|
norm := envKey(key)
|
||||||
|
if LLMBlocklist[norm] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, prefix := range LLMBlocklistPrefixes {
|
||||||
|
if strings.HasPrefix(norm, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeEnvVars merges multiple env sources into a map.
|
||||||
|
// baseEnv is the cached map from AllowedEnv.
|
||||||
|
// envSet provides explicit key=value pairs (config, not filtered).
|
||||||
|
// extraEnv provides additional key=value pairs from LLM (filtered by blocklist).
|
||||||
|
func MergeEnvVars(baseEnv map[string]string, envSet, extraEnv map[string]string) map[string]string {
|
||||||
|
vars := make(map[string]string, len(baseEnv)+len(envSet)+len(extraEnv))
|
||||||
|
|
||||||
|
// Start with base env (already filtered)
|
||||||
|
for k, v := range baseEnv {
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add envSet (config-provided, not filtered)
|
||||||
|
if envSet != nil {
|
||||||
|
for k, v := range envSet {
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge extraEnv (LLM-provided) - filtered by blocklist
|
||||||
|
if extraEnv != nil {
|
||||||
|
for k, v := range extraEnv {
|
||||||
|
if isBlocked(k) {
|
||||||
|
continue // Skip blocked vars
|
||||||
|
}
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapToEnvSlice converts a map of environment variables to a []string
|
||||||
|
// in the format "KEY=value" suitable for exec.Cmd.Env.
|
||||||
|
func MapToEnvSlice(vars map[string]string) []string {
|
||||||
|
result := make([]string, 0, len(vars))
|
||||||
|
for k, v := range vars {
|
||||||
|
result = append(result, k+"="+v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// envKey normalizes an environment variable name. On Windows, where env
|
||||||
|
// vars are case-insensitive, it uppercases the key so that "Path" and
|
||||||
|
// "PATH" map to the same entry. On other platforms it's a no-op.
|
||||||
|
func envKey(k string) string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPicoclawEnvVars ensures PICOCLAW_* vars are set in envSet.
|
||||||
|
// These are needed for child processes to locate config, workspace, etc.
|
||||||
|
func WithPicoclawEnvVars(envSet map[string]string, workspace string) map[string]string {
|
||||||
|
// Copy the map to avoid mutating the caller's map
|
||||||
|
result := maps.Clone(envSet)
|
||||||
|
if result == nil {
|
||||||
|
result = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always compute PICOCLAW_* vars - priority: env var > default
|
||||||
|
if v := os.Getenv("PICOCLAW_HOME"); v != "" {
|
||||||
|
result["PICOCLAW_HOME"] = v
|
||||||
|
} else if home, _ := os.UserHomeDir(); home != "" {
|
||||||
|
result["PICOCLAW_HOME"] = filepath.Join(home, ".picoclaw")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("PICOCLAW_CONFIG"); v != "" {
|
||||||
|
result["PICOCLAW_CONFIG"] = v
|
||||||
|
} else if home := result["PICOCLAW_HOME"]; home != "" {
|
||||||
|
result["PICOCLAW_CONFIG"] = filepath.Join(home, "config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace - this is the agent's working directory
|
||||||
|
if workspace != "" {
|
||||||
|
result["PICOCLAW_AGENT_WORKSPACE"] = workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
result["PICOCLAW_EXE"] = exe
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := os.Getenv("PICOCLAW_SERVICE_NAME"); v != "" {
|
||||||
|
result["PICOCLAW_SERVICE_NAME"] = v
|
||||||
|
} else {
|
||||||
|
result["PICOCLAW_SERVICE_NAME"] = "picoclaw"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
290
workspace/skills/execline/SKILL.md
Normal file
290
workspace/skills/execline/SKILL.md
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
# execline-hardening Skill
|
||||||
|
|
||||||
|
Security hardening for agentic systems using execline instead of bash.
|
||||||
|
|
||||||
|
## What is execline?
|
||||||
|
|
||||||
|
execline is a minimal scripting language from [skarnet.org](https://skarnet.org/software/execline/) designed for security and simplicity. It uses **chain loading** - each command execs into the next one, rather than staying resident like a shell.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu/Armbian
|
||||||
|
apt install execline
|
||||||
|
|
||||||
|
# Alpine
|
||||||
|
apk add execline
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### Chain Loading
|
||||||
|
execline uses exec() heavily - each program runs, then execs into the next one:
|
||||||
|
```
|
||||||
|
execlineb -c "nice -10 echo hello"
|
||||||
|
```
|
||||||
|
This is more efficient than spawning a shell interpreter.
|
||||||
|
|
||||||
|
### Whitespace is Whitespace
|
||||||
|
Newlines, spaces, and tabs are all treated the same - they're just word separators.
|
||||||
|
|
||||||
|
### Blocks
|
||||||
|
Curly braces `{ }` create blocks to group commands with their arguments:
|
||||||
|
```
|
||||||
|
foreground { echo hello } echo world
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Properties
|
||||||
|
|
||||||
|
### What execline DOESN'T do:
|
||||||
|
- **Command substitution**: `$(cmd)`, `` `cmd` `` - passed literally, not executed
|
||||||
|
- **Shell control operators**: `&&`, `||`, `;` - these are just arguments
|
||||||
|
- **Pipes to shell**: `| sh`, `| bash` - not supported
|
||||||
|
|
||||||
|
### What execline DOES do (differently from sh):
|
||||||
|
- **Variable substitution**: Uses a deliberate substitution mechanism, not shell-style `$VAR`
|
||||||
|
- This is a feature, not a bug - it provides predictable behavior
|
||||||
|
|
||||||
|
## Variable Management
|
||||||
|
|
||||||
|
execline has a deliberate variable system - no shell-style `$VAR` magic:
|
||||||
|
|
||||||
|
### define - Define a literal substitution
|
||||||
|
```bash
|
||||||
|
define FOO hello
|
||||||
|
echo $FOO
|
||||||
|
# Output: hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### importas - Import environment variable
|
||||||
|
```bash
|
||||||
|
importas home HOME
|
||||||
|
cd $home
|
||||||
|
ls
|
||||||
|
```
|
||||||
|
|
||||||
|
### backtick - Command output to variable (with -E flag!)
|
||||||
|
```bash
|
||||||
|
backtick DATE { date +%Y-%m-%d }
|
||||||
|
echo $DATE
|
||||||
|
# Output: today's date
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Unlike shell's `$(date)`, execline uses `backtick` which:
|
||||||
|
- Runs the command
|
||||||
|
- Captures stdout
|
||||||
|
- Stores it in an environment variable
|
||||||
|
- Then execs into the next command
|
||||||
|
|
||||||
|
### backtick -E - Auto-import command substitution
|
||||||
|
The `-E` flag makes backtick automatically import the result as a variable, enabling true command substitution (like `$()` in bash):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
backtick -E DATE { date } echo $DATE
|
||||||
|
# Output: Sun Mar 22 08:08:58 PM GMT 2026
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `-E`, you need `importas` to access the variable. With `-E`, it's auto-imported directly.
|
||||||
|
|
||||||
|
## Sequencing Commands
|
||||||
|
|
||||||
|
### foreground - Run and wait
|
||||||
|
```bash
|
||||||
|
foreground { echo first } echo second
|
||||||
|
# Output:
|
||||||
|
# first
|
||||||
|
# second
|
||||||
|
```
|
||||||
|
|
||||||
|
### background - Run in background
|
||||||
|
```bash
|
||||||
|
background { long-running-task } echo done
|
||||||
|
# Starts task, immediately prints "done"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditionals
|
||||||
|
|
||||||
|
### if - Run if condition succeeds
|
||||||
|
```bash
|
||||||
|
if { test -f /etc/passwd } echo file exists
|
||||||
|
```
|
||||||
|
|
||||||
|
### if with negation
|
||||||
|
```bash
|
||||||
|
if -n { test -f /tmp/test } mkdir /tmp/test
|
||||||
|
# -n negates: if file DOESN'T exist, create it
|
||||||
|
```
|
||||||
|
|
||||||
|
### ifelse - If-else
|
||||||
|
```bash
|
||||||
|
ifelse { test -d $HOME }
|
||||||
|
{ echo "It's a directory" }
|
||||||
|
{ echo "Not a directory" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loops
|
||||||
|
|
||||||
|
### forx - Iterate over list
|
||||||
|
```bash
|
||||||
|
forx item { alpha beta gamma } echo $item
|
||||||
|
# Output: alpha, beta, gamma (each on separate line via foreground)
|
||||||
|
```
|
||||||
|
|
||||||
|
### forstdin - Read from stdin
|
||||||
|
```bash
|
||||||
|
echo -e "a\nb\nc" | forstdin line echo $line
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Operations
|
||||||
|
|
||||||
|
### elglob - File globbing
|
||||||
|
```bash
|
||||||
|
elglob files /etc/f* echo ${files}
|
||||||
|
# Lists all files in /etc starting with 'f'
|
||||||
|
```
|
||||||
|
|
||||||
|
### redirfd - Redirect file descriptors
|
||||||
|
```bash
|
||||||
|
redirfd -w 1 output.txt echo hello
|
||||||
|
# Redirect stdout (fd 1) to file
|
||||||
|
|
||||||
|
redirfd -a 1 log.txt date
|
||||||
|
# Append to log
|
||||||
|
|
||||||
|
redirfd -r 0 /dev/null cat
|
||||||
|
# Redirect stdin from /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
### fdmove - Move file descriptors
|
||||||
|
```bash
|
||||||
|
fdmove -c 2 1 prog
|
||||||
|
# Duplicate fd 2 (stderr) to fd 1 (stdout) - stderr to stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Scripts
|
||||||
|
|
||||||
|
### Simple sequence
|
||||||
|
```bash
|
||||||
|
#!/bin/execlineb -P
|
||||||
|
importas home HOME
|
||||||
|
cd $home
|
||||||
|
ls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional file creation
|
||||||
|
```bash
|
||||||
|
#!/bin/execlineb -P
|
||||||
|
importas home HOME
|
||||||
|
if -n { test -d ${home}/.cache }
|
||||||
|
mkdir -p ${home}/.cache
|
||||||
|
echo "Cache directory ready"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loop and create files
|
||||||
|
```bash
|
||||||
|
#!/bin/execlineb -P
|
||||||
|
forx name { alpha beta gamma }
|
||||||
|
{
|
||||||
|
touch /tmp/${name}
|
||||||
|
}
|
||||||
|
echo "Files created"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipeline
|
||||||
|
```bash
|
||||||
|
#!/bin/execlineb -P
|
||||||
|
pipeline { ls /etc }
|
||||||
|
wc -l
|
||||||
|
# Count files in /etc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison: execline vs shell (sh/bash)
|
||||||
|
|
||||||
|
| Feature | sh/bash | execline |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| $VAR expansion | Yes | Yes (via substitution) |
|
||||||
|
| ${VAR} expansion | Yes | Yes |
|
||||||
|
| $(cmd) substitution | Yes | **Yes** - use `backtick -E VAR { cmd }` |
|
||||||
|
| `cmd` substitution | Yes | **No** (use backtick) |
|
||||||
|
| &&, \|\| | Yes | **No** - use if/foreground |
|
||||||
|
| ; | Yes | **No** - use foreground |
|
||||||
|
| Variable assignment | VAR=value | define VAR value |
|
||||||
|
| Command output | $(cmd) | backtick VAR { cmd } |
|
||||||
|
| Loops | for, while | forx, forstdin |
|
||||||
|
| Conditionals | if/then/else | if, ifelse |
|
||||||
|
|
||||||
|
## Usage in picoclaw
|
||||||
|
|
||||||
|
The picoclaw agent has a built-in `execline` tool that you can call directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tool: execline
|
||||||
|
ToolInput: { "command": "define FOO bar echo $FOO" }
|
||||||
|
```
|
||||||
|
→ Output: bar
|
||||||
|
|
||||||
|
The ExeclineTool validates:
|
||||||
|
- No `&&`, `||` (use `if`, `foreground` instead)
|
||||||
|
- No pipes to shell (`| sh`, `| bash`)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Variable substitution works:
|
||||||
|
execlineb -c 'define FOO bar echo $FOO'
|
||||||
|
# Output: bar
|
||||||
|
|
||||||
|
# Environment variables:
|
||||||
|
HOME=/tmp execlineb -c 'importas h HOME cd $h pwd'
|
||||||
|
# Output: /tmp
|
||||||
|
|
||||||
|
# backtick -E enables command substitution (like $()):
|
||||||
|
execlineb -c 'backtick -E DATE { date } echo $DATE'
|
||||||
|
# Output: current date/time
|
||||||
|
|
||||||
|
# backtick without -E requires importas:
|
||||||
|
execlineb -c 'backtick DATE { date } importas D DATE echo $D'
|
||||||
|
# Output: current date/time
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. **Use execline** for scripts that don't need `$(cmd)` or `&&`/`||`
|
||||||
|
2. Use `if` instead of `&&`, `ifelse` instead of `if-then-else`
|
||||||
|
3. Use `backtick` instead of `$(...)`
|
||||||
|
4. Use `foreground` for sequential commands
|
||||||
|
|
||||||
|
## When Exec Tool is Unavailable
|
||||||
|
|
||||||
|
If the `exec` tool is disabled but you still need command execution:
|
||||||
|
|
||||||
|
1. **Generate execline commands** for the user to execute manually
|
||||||
|
2. **Write scripts** that the user can save and run
|
||||||
|
|
||||||
|
Example - Creating a script for the user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/execlineb -S0
|
||||||
|
cd /home/infra
|
||||||
|
export HOME /home/infra
|
||||||
|
foreground { echo "Environment configured" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The user saves this to a file and runs it. This gives you a way to help even without direct execution capabilities.
|
||||||
|
|
||||||
|
## Positional Arguments
|
||||||
|
|
||||||
|
execline scripts can handle command-line arguments using `-sN` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/execlineb -s0
|
||||||
|
# $1 is first arg, $@ is all remaining args
|
||||||
|
echo "First: $1"
|
||||||
|
echo "All: $@"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-s0`: $1, $2, etc. work directly
|
||||||
|
- `-s1`: Shift after first argument
|
||||||
|
- `-sN`: Positionalize N arguments
|
||||||
|
|
||||||
|
This is useful for wrapper scripts that delegate to other commands.
|
||||||
59
workspace/skills/execline/TEST-PLAN.md
Normal file
59
workspace/skills/execline/TEST-PLAN.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Test Plan: execline Execution Tool
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This test plan validates the execline execution tool's capabilities and security constraints.
|
||||||
|
|
||||||
|
## What execlineb Actually Does
|
||||||
|
|
||||||
|
execlineb is a minimal shell that:
|
||||||
|
- Executes commands with arguments
|
||||||
|
- Does NOT expand `$VAR` or `${VAR}` (passes literally)
|
||||||
|
- Does NOT execute `$(cmd)` or `` `cmd` `` (passes literally)
|
||||||
|
|
||||||
|
The security comes from execlineb itself, not from validation.
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### 1. Basic Command Execution
|
||||||
|
- [x] **Test 1.1**: Execute `echo hello world`
|
||||||
|
- Expected: Returns "hello world"
|
||||||
|
- [x] **Test 1.2**: Execute `pwd`
|
||||||
|
- Expected: Returns current working directory
|
||||||
|
- [x] **Test 1.3**: Execute `ls -la /tmp`
|
||||||
|
- Expected: Lists files in /tmp directory
|
||||||
|
- [x] **Test 1.4**: Execute `cat /etc/hostname`
|
||||||
|
- Expected: Returns hostname content
|
||||||
|
- [x] **Test 1.5**: Execute `whoami`
|
||||||
|
- Expected: Returns current user
|
||||||
|
|
||||||
|
### 2. Variable Expansion (NOT done - passed literally)
|
||||||
|
- [x] **Test 2.1**: Execute `echo $HOME`
|
||||||
|
- Expected: Returns "$HOME" (literal, not expanded)
|
||||||
|
- [x] **Test 2.2**: Execute `echo ${PATH}`
|
||||||
|
- Expected: Returns "${PATH}" (literal)
|
||||||
|
|
||||||
|
### 3. Command Substitution (NOT done - passed literally)
|
||||||
|
- [x] **Test 3.1**: Execute `echo $(whoami)`
|
||||||
|
- Expected: Returns "$(whoami)" (literal)
|
||||||
|
- [x] **Test 3.2**: Execute `echo `whoami``
|
||||||
|
- Expected: Returns "`whoami`" (literal)
|
||||||
|
|
||||||
|
### 4. Blocked by Go Validation
|
||||||
|
- [x] **Test 4.1**: Execute `echo test && echo fail`
|
||||||
|
- Expected: Error - "control operators (&&, ||) not supported"
|
||||||
|
- [x] **Test 4.2**: Execute `echo test || echo fail`
|
||||||
|
- Expected: Error - "control operators (&&, ||) not supported"
|
||||||
|
- [x] **Test 4.3**: Execute `cat file | sh`
|
||||||
|
- Expected: Error - "pipe to shell detected"
|
||||||
|
|
||||||
|
### 5. Edge Cases
|
||||||
|
- [x] **Test 5.1**: Empty command
|
||||||
|
- Expected: Error - "Empty command"
|
||||||
|
- [x] **Test 5.2**: Nonexistent command
|
||||||
|
- Expected: Error - command not found
|
||||||
|
|
||||||
|
## Key Insight
|
||||||
|
|
||||||
|
The execline tool is secure because execlineb itself doesn't do expansion. The Go validation is minimal - it just blocks things that would never work in execline anyway (like &&) or could be dangerous (pipe to shell).
|
||||||
|
|
||||||
|
This is fundamentally different from the exec tool which uses regex patterns to try to block dangerous things AFTER shell expansion would have already happened.
|
||||||
62
workspace/skills/execline/appraisal.md
Normal file
62
workspace/skills/execline/appraisal.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Appraisal: Environment Sanitization PR
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
PR1 adds environment sanitization with caching to the exec tool, enabling:
|
||||||
|
1. Clean environment for child processes (no leaked secrets)
|
||||||
|
2. LLM-controlled env injection (with blocklist)
|
||||||
|
3. Cached env at startup for efficiency
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| `[]string` as cache format | Direct compatibility with `os.Environ()` and `exec.Cmd.Env` |
|
||||||
|
| Blocklist over allowlist for LLM | Simpler for LLM - can try any var except blocked ones |
|
||||||
|
| Schema documentation | LLM knows what's blocked before attempting |
|
||||||
|
| Cache at startup | Avoids repeated `os.Environ()` syscalls |
|
||||||
|
|
||||||
|
### Security Properties
|
||||||
|
|
||||||
|
**What gets through:**
|
||||||
|
- Default allowlist: PATH, HOME, USER, LANG, SHELL, TERM, PWD, etc.
|
||||||
|
- Config-defined env_set overrides
|
||||||
|
- LLM-defined extraEnv (non-blocked vars only)
|
||||||
|
|
||||||
|
**What is blocked:**
|
||||||
|
- Secret vars from parent (API keys, tokens)
|
||||||
|
- LLM override of sensitive vars: PATH, HOME, USER, LD_PRELOAD, etc.
|
||||||
|
|
||||||
|
### Trade-offs
|
||||||
|
|
||||||
|
| Pros | Cons |
|
||||||
|
|------|------|
|
||||||
|
| No secret leakage to child processes | Additional startup cost (build env once) |
|
||||||
|
| LLM can inject debug vars | Blocklist may need expansion |
|
||||||
|
| Efficient caching | Cache is static - no dynamic updates |
|
||||||
|
| Compatible with execline/mvdan paths | - |
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
1. **Dynamic env updates** — Currently cache is built once at startup. Could add method to rebuild cache if needed.
|
||||||
|
|
||||||
|
2. **Expand blocklist** — Current list: PATH, HOME, USER, LOGNAME, SHELL, LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, LD_DEBUG. May need more.
|
||||||
|
|
||||||
|
3. **Per-command env isolation** — Currently env is shared across calls. Could offer isolated mode.
|
||||||
|
|
||||||
|
4. **Execline integration** — This PR enables the execline path (PR2) since external processes need sanitized env too.
|
||||||
|
|
||||||
|
## Code Metrics
|
||||||
|
|
||||||
|
- Production code: +125 lines
|
||||||
|
- Tests: +90 lines
|
||||||
|
- Files changed: 5
|
||||||
|
- Functions: 3 new (`BuildSanitizedEnv` modified, `EnvironToSlice` added)
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This PR provides a solid foundation for environment handling. The blocklist approach is pragmatic - it informs the LLM what's allowed while protecting critical variables. The caching ensures efficiency for high-frequency exec calls.
|
||||||
|
|
||||||
|
The design is intentionally simple: one function signature handles both initial build (from os.Environ) and subsequent builds (from cached slice). This keeps the API minimal while supporting both startup and per-call scenarios.
|
||||||
70
workspace/skills/execline/scripts/poc-execline-security.sh
Executable file
70
workspace/skills/execline/scripts/poc-execline-security.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# POC: Execline as security-hardened shell wrapper
|
||||||
|
# Demonstrates that $(...) is treated as literal text in execline
|
||||||
|
|
||||||
|
echo "=== POC: Execline Security Hardening ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if execlineb is available
|
||||||
|
if ! command -v execlineb >/dev/null 2>&1; then
|
||||||
|
echo "FAIL: execlineb not found"
|
||||||
|
echo "Install with: apt install execline"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: execlineb found"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: execline should NOT execute $(whoami)
|
||||||
|
echo "--- Test 1: Command substitution blocked ---"
|
||||||
|
RESULT=$(execlineb -c 'echo $(whoami)')
|
||||||
|
echo "Input: echo \$(whoami)"
|
||||||
|
echo "Output: $RESULT"
|
||||||
|
if [ "$RESULT" = '$(whoami)' ]; then
|
||||||
|
echo "RESULT: PASS - literal text preserved"
|
||||||
|
else
|
||||||
|
echo "RESULT: FAIL - unexpected output"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: execline CAN invoke shell when explicitly allowed
|
||||||
|
echo "--- Test 2: Shell invocation allowed ---"
|
||||||
|
RESULT2=$(execlineb -c '/bin/sh -c "echo hello"')
|
||||||
|
echo "Input: /bin/sh -c \"echo hello\""
|
||||||
|
echo "Output: $RESULT2"
|
||||||
|
if [ "$RESULT2" = "hello" ]; then
|
||||||
|
echo "RESULT: PASS - shell invoked correctly"
|
||||||
|
else
|
||||||
|
echo "RESULT: FAIL - shell not invoked"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: Shell can still do $(...) inside
|
||||||
|
echo "--- Test 3: Inner shell has full features ---"
|
||||||
|
RESULT3=$(execlineb -c '/bin/sh -c "echo inner shell: $(whoami)"')
|
||||||
|
echo "Input: /bin/sh -c \"echo inner shell: \$(whoami)\""
|
||||||
|
echo "Output: $RESULT3"
|
||||||
|
if [ -n "$RESULT3" ] && echo "$RESULT3" | grep -q "inner shell:"; then
|
||||||
|
echo "RESULT: PASS - inner shell executed \$(whoami)"
|
||||||
|
else
|
||||||
|
echo "RESULT: FAIL"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Variable expansion blocked
|
||||||
|
echo "--- Test 4: Variable expansion blocked ---"
|
||||||
|
RESULT4=$(execlineb -c 'echo $HOME')
|
||||||
|
echo "Input: echo \$HOME"
|
||||||
|
echo "Output: $RESULT4"
|
||||||
|
if [ "$RESULT4" = '$HOME' ]; then
|
||||||
|
echo "RESULT: PASS - variable literal"
|
||||||
|
else
|
||||||
|
echo "RESULT: FAIL"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "Execline blocks: \$(...), \${...}, \$VAR, backticks"
|
||||||
|
echo "Execline allows: explicit shell invocation via /bin/sh -c"
|
||||||
|
echo ""
|
||||||
|
echo "Security model: Outer layer (execline) is hardened,"
|
||||||
45
workspace/skills/execline/scripts/test-execline.sh
Executable file
45
workspace/skills/execline/scripts/test-execline.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Test script for execline hardening skill
|
||||||
|
|
||||||
|
echo "=== Execline Availability Test ==="
|
||||||
|
|
||||||
|
if command -v execlineb >/dev/null 2>&1; then
|
||||||
|
echo "[OK] execlineb found: $(command -v execlineb)"
|
||||||
|
else
|
||||||
|
echo "[WARN] execlineb not found - installing from package manager"
|
||||||
|
echo " apt: apt install execline"
|
||||||
|
echo " apk: apk add execline"
|
||||||
|
echo " yum: yum install execline"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Execline Command Test ==="
|
||||||
|
|
||||||
|
# Test basic execution
|
||||||
|
echo "test" | execlineb -c 'forstdin line { echo The line is: $1 }' 2>/dev/null && echo "[OK] forstdin works" || echo "[FAIL] forstdin"
|
||||||
|
|
||||||
|
# Test foreground (like &&)
|
||||||
|
execlineb -c 'foreground { echo hello } echo world' 2>/dev/null && echo "[OK] foreground works" || echo "[FAIL] foreground"
|
||||||
|
|
||||||
|
# Test backtick (like $())
|
||||||
|
BACKTICK_RESULT=$(execlineb -sb0 'backtick result { echo substituted } echo $result')
|
||||||
|
if [ "$BACKTICK_RESULT" = "substituted" ]; then
|
||||||
|
echo "[OK] backtick works"
|
||||||
|
else
|
||||||
|
echo "[FAIL] backtick (got: '$BACKTICK_RESULT')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Security: Literal $() Pass-through Test ==="
|
||||||
|
# This should NOT execute whoami in execline
|
||||||
|
RESULT=$(execlineb -c 'echo $(whoami)' 2>&1)
|
||||||
|
echo "Result of '\$(whoami)': $RESULT"
|
||||||
|
echo "[OK] Command substitution blocked" || echo "[INFO] Result shows literal text"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Available Execline Binaries ==="
|
||||||
|
for bin in execlineb foreground if ifelse forstdin for backtick fdmove; do
|
||||||
|
if command -v $bin >/dev/null 2>&1; then
|
||||||
|
echo " $bin: $(command -v $bin)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
Loading…
Add table
Reference in a new issue