Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.
128 changes: 118 additions & 10 deletions packages/opencode/src/kilocode/shell/shell.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,124 @@
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 attr = attrs(command, pos)
const body = command.slice(attr)
const match = /^param\s*\(/i.exec(body)
if (!match) return pos

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

function attrs(command: string, start: number) {
let pos = start
while (pos < command.length) {
const next = scan(command, pos)
if (command[next] !== "[") return next
const end = block(command, next, "[", "]")
if (end === undefined) return start
pos = end
}
return pos
}

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

function trivia(command: string, start: number) {
let pos = start
while (pos < command.length) {
while (/\s/.test(command[pos] ?? "")) pos++
if (command[pos] === "#") {
pos = line(command, pos)
continue
}
if (command.startsWith("<#", pos)) {
const end = command.indexOf("#>", pos + 2)
if (end === -1) return command.length
pos = end + 2
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, open: string, close: string) {
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 (command.startsWith("<#", pos)) {
const end = command.indexOf("#>", pos + 2)
if (end === -1) return
pos = end + 1
continue
}
if (char === "#") {
pos = line(command, pos) - 1
continue
}
if (char === open) depth++
if (char === close) {
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
39 changes: 28 additions & 11 deletions packages/opencode/test/kilocode/shell/shell.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
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
<#
Block comment before attributes.
#>
[CmdletBinding()]
[OutputType([string])]
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(value.slice(0, setup)).toContain("[CmdletBinding()]")
expect(value.slice(0, setup)).toContain("[OutputType([string])]")
expect(value.slice(0, setup)).toContain("param(")
expect(setup).toBeLessThan(value.indexOf("Write-Output"))
})
})
Loading