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) (err error) {
40 defer markPolicyError(&err)
41 // The existing shell heuristics parse neither PowerShell expressions nor
42 // cmdlets. Do not treat them as enforcement for a different language.
43 if t.Sandbox.Locked() {
44 return fmt.Errorf("jailed: PowerShell execution is unavailable while jailed")
45 }
46 if t.Sandbox != nil && t.Sandbox.Permissions != nil {
47 if strings.ToLower(strings.TrimSpace(t.Sandbox.Permissions.Bash.Mode)) != "ask" {
48 return fmt.Errorf("permission denied: PowerShell requires bash permission mode ask; bash allowlists do not apply to PowerShell")
49 }
50 }
51 return nil
52}
53
54func resolvePowerShell(goos string, lookPath func(string) (string, error)) (string, error) {
55 if goos != "windows" {
56 return "", fmt.Errorf("the powershell tool is only available on Windows")
57 }
58 for _, name := range []string{"pwsh.exe", "powershell.exe"} {
59 if path, err := lookPath(name); err == nil {
60 return path, nil
61 }
62 }
63 return "", fmt.Errorf("PowerShell not found: install PowerShell or add pwsh.exe or powershell.exe to PATH")
64}
65
66func powerShellArgs(command string) []string {
67 // EncodedCommand preserves quotes, multiline scripts and Unicode through
68 // Windows command-line parsing. PowerShell expects UTF-16LE here.
69 script := "[Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n" + command
70 units := utf16.Encode([]rune(script))
71 buf := make([]byte, len(units)*2)
72 for i, unit := range units {
73 binary.LittleEndian.PutUint16(buf[i*2:], unit)
74 }
75 return []string{"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-OutputFormat", "Text", "-EncodedCommand", base64.StdEncoding.EncodeToString(buf)}
76}