1package tools
2
3import (
4 "context"
5 "encoding/base64"
6 "encoding/binary"
7 "encoding/json"
8 "fmt"
9 "os/exec"
10 "runtime"
11 "strings"
12 "unicode/utf16"
13
14 "github.com/patriceckhart/zot/packages/core"
15)
16
17// PowerShellTool provides opt-in native command execution on Windows.
18type PowerShellTool struct {
19 CWD string
20 Sandbox *Sandbox
21}
22
23func (t *PowerShellTool) Name() string { return "powershell" }
24func (t *PowerShellTool) Description() string {
25 return "Run a native PowerShell command on Windows (pwsh.exe, falling back to powershell.exe), without profiles or interactive input. stdout+stderr merged. Optional timeout in seconds."
26}
27func (t *PowerShellTool) Schema() json.RawMessage { return json.RawMessage(bashSchema) }
28
29func (t *PowerShellTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
30 return executeShell(ctx, raw, progress, t.CWD, "PS>", t.checkPermission, func(ctx context.Context, command string) (*exec.Cmd, error) {
31 path, err := resolvePowerShell(runtime.GOOS, exec.LookPath)
32 if err != nil {
33 return nil, err
34 }
35 return exec.CommandContext(ctx, path, powerShellArgs(command)...), nil
36 })
37}
38
39func (t *PowerShellTool) checkPermission(_ string) error {
40 // The existing shell heuristics parse neither PowerShell expressions nor
41 // cmdlets. Do not treat them as enforcement for a different language.
42 if t.Sandbox.Locked() {
43 return fmt.Errorf("jailed: PowerShell execution is unavailable while jailed")
44 }
45 if t.Sandbox != nil && t.Sandbox.Permissions != nil {
46 if strings.ToLower(strings.TrimSpace(t.Sandbox.Permissions.Bash.Mode)) != "ask" {
47 return fmt.Errorf("permission denied: PowerShell requires bash permission mode ask; bash allowlists do not apply to PowerShell")
48 }
49 }
50 return nil
51}
52
53func resolvePowerShell(goos string, lookPath func(string) (string, error)) (string, error) {
54 if goos != "windows" {
55 return "", fmt.Errorf("the powershell tool is only available on Windows")
56 }
57 for _, name := range []string{"pwsh.exe", "powershell.exe"} {
58 if path, err := lookPath(name); err == nil {
59 return path, nil
60 }
61 }
62 return "", fmt.Errorf("PowerShell not found: install PowerShell or add pwsh.exe or powershell.exe to PATH")
63}
64
65func powerShellArgs(command string) []string {
66 // EncodedCommand preserves quotes, multiline scripts and Unicode through
67 // Windows command-line parsing. PowerShell expects UTF-16LE here.
68 script := "[Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n" + command
69 units := utf16.Encode([]rune(script))
70 buf := make([]byte, len(units)*2)
71 for i, unit := range units {
72 binary.LittleEndian.PutUint16(buf[i*2:], unit)
73 }
74 return []string{"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-OutputFormat", "Text", "-EncodedCommand", base64.StdEncoding.EncodeToString(buf)}
75}