Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/calm-powershell-alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---

Run Windows PowerShell tool commands without `-EncodedCommand` to reduce antivirus false positives.
86 changes: 76 additions & 10 deletions packages/opencode/src/kilocode/shell/shell.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,82 @@
import { Buffer } from "node:buffer"

export function args(command: string) {
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded(command)]
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script(command)]
}

function encoded(command: string) {
const payload = Buffer.from(command, "utf8").toString("base64")
return Buffer.from(
`[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false);
const setup = `[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false);
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false);
$OutputEncoding = [Console]::OutputEncoding;
& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`,
"utf16le",
).toString("base64")
`

function script(command: string) {
const pos = prologue(command)
const head = command.slice(0, pos)
const body = command.slice(pos)
const gap = head && !/[;\r\n]\s*$/.test(head) ? "\n" : ""
return `${head}${gap}${setup}${body}`
}

function prologue(command: string) {
const pos = scan(command, 0)
const body = command.slice(pos)
const match = /^\s*param\s*\(/i.exec(body)
Comment thread
marius-kilocode marked this conversation as resolved.
Outdated
if (!match) return pos

const start = match[0].lastIndexOf("(")
const end = block(body, start)
if (end === undefined) return pos
return pos + end
}

function scan(command: string, start: number) {
let pos = start
while (pos < command.length) {
const end = line(command, pos)
const value = command.slice(pos, end)
const text = value.trimStart()
if (text.trim() === "" || text.startsWith("#") || /^using\s+(?:assembly|module|namespace|type)\b/i.test(text)) {
pos = end
continue
}
return pos
}
return pos
}

function line(command: string, start: number) {
const index = command.indexOf("\n", start)
if (index === -1) return command.length
return index + 1
}

function block(command: string, start: number) {
let depth = 0
let quote: string | undefined
for (let pos = start; pos < command.length; pos++) {
const char = command[pos]
if (quote) {
if (quote === "'" && char === "'" && command[pos + 1] === "'") {
pos++
continue
}
if (quote === '"' && char === "`") {
pos++
continue
}
if (char === quote) quote = undefined
continue
}
if (char === "'" || char === '"') {
quote = char
continue
}
if (char === "#") {
pos = line(command, pos) - 1
continue
}
if (char === "(") depth++
if (char === ")") {
depth--
if (depth === 0) return pos + 1
}
}
}
4 changes: 2 additions & 2 deletions packages/opencode/src/shell/shell.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import * as PowerShell from "@/kilocode/shell/shell" // kilocode_change - encoded PowerShell args
import * as PowerShell from "@/kilocode/shell/shell" // kilocode_change - PowerShell args
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem"
import { which } from "@/util/which"
Expand Down Expand Up @@ -189,7 +189,7 @@ export function args(file: string, command: string, cwd: string) {
]
}
if (n === "cmd") return ["/c", command]
if (ps(file)) return PowerShell.args(command) // kilocode_change - encoded PowerShell args
if (ps(file)) return PowerShell.args(command) // kilocode_change - PowerShell args
return ["-c", command]
}

Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/tool/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (

function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
if (process.platform === "win32" && Shell.ps(shell)) {
// kilocode_change start - encoded PowerShell args
// kilocode_change start - PowerShell args
return ChildProcess.make(shell, Shell.args(shell, command, cwd), {
// kilocode_change end
cwd,
Expand Down
32 changes: 21 additions & 11 deletions packages/opencode/test/kilocode/shell/shell.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import { describe, expect, test } from "bun:test"
import { Buffer } from "node:buffer"
import * as PowerShell from "@/kilocode/shell/shell"
import { Shell } from "@/shell/shell"

const command = `Write-Output "こんにちは 😀"; Write-Output '$value'; Write-Output \`tick\`
Write-Output "done"`

function script(args: string[]) {
return Buffer.from(args[4], "base64").toString("utf16le")
}

describe("PowerShell arguments", () => {
test("transports commands through UTF-8 inside EncodedCommand", () => {
test("transports commands in plaintext with UTF-8 console setup", () => {
const args = PowerShell.args(command)
const value = script(args)
const payload = value.match(/FromBase64String\('([^']+)'\)/)?.[1]
const value = args[4]

expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"])
expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"])
expect(args).toHaveLength(5)
expect(value).toContain("[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)")
expect(value).toContain("[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)")
expect(value).toContain("$OutputEncoding = [Console]::OutputEncoding")
expect(payload).toBeDefined()
expect(Buffer.from(payload!, "base64").toString("utf8")).toBe(command)
expect(value).toContain(command)
expect(args).not.toContain("-EncodedCommand")
expect(value).not.toContain("FromBase64String")
})

test.each(["powershell", "pwsh"])("routes %s through the Kilo argument builder", (shell) => {
expect(Shell.args(shell, command, "/tmp")).toEqual(PowerShell.args(command))
})

test("keeps script-level prologue before the UTF-8 console setup", () => {
const input = `#requires -Version 5.1
using namespace System.Text
param(
[string]$Name
)
Write-Output $Name`
const value = PowerShell.args(input)[4]
const setup = value.indexOf("[Console]::InputEncoding")

expect(value.startsWith("#requires -Version 5.1")).toBe(true)
expect(setup).toBeGreaterThan(value.indexOf(")"))
expect(setup).toBeLessThan(value.indexOf("Write-Output"))
})
})
Loading