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
101 changes: 101 additions & 0 deletions packages/kilo-vscode/script/bwrap-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { randomUUID } from "node:crypto"
import {
chmodSync,
constants,
copyFileSync,
lstatSync,
mkdirSync,
realpathSync,
renameSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs"
import { dirname } from "node:path"
import { localBwrapDigest, localBwrapPath, validLocalBwrap } from "../src/services/cli-backend/cli-resources"

export function currentBwrapTarget(): string {
const os = process.platform === "win32" ? "win32" : process.platform
return `${os}-${process.arch}`
}

function arch(target: string): "x64" | "arm64" {
if (target === "linux-x64") return "x64"
if (target === "linux-arm64") return "arm64"
throw new Error(`No Bubblewrap helper configured for target ${target}`)
}

function source() {
const configured = process.env.KILO_BWRAP_PATH
if (configured) return realpathSync(configured)

const found = Bun.which("bwrap")
if (!found) return
const target = realpathSync(found)
const entry = statSync(target)
const uid = process.getuid?.()
const groups = process.getgroups?.() ?? []
const writable =
(entry.mode & 0o002) !== 0 ||
(uid !== undefined && entry.uid === uid && (entry.mode & 0o200) !== 0) ||
(groups.includes(entry.gid) && (entry.mode & 0o020) !== 0)
if (writable) {
throw new Error(`Refusing writable Bubblewrap executable at ${target}; set KILO_BWRAP_PATH to trust it explicitly`)
}
return target
}

function secure(dir: string) {
mkdirSync(dir, { recursive: true, mode: 0o700 })
const entry = lstatSync(dir)
if (
entry.isSymbolicLink() ||
!entry.isDirectory() ||
entry.uid !== process.getuid?.() ||
(entry.mode & 0o077) !== 0
) {
throw new Error(`Bubblewrap cache directory is not private: ${dir}`)
}
}

function stage(source: string, dest: string, digest: string) {
const root = dirname(dirname(dest))
const dir = dirname(dest)
secure(root)
secure(dir)

const token = `${process.pid}-${randomUUID()}`
const executable = `${dest}.${token}.tmp`
const checksum = `${executable}.sha256`
try {
copyFileSync(source, executable, constants.COPYFILE_EXCL)
chmodSync(executable, 0o755)
writeFileSync(checksum, `${digest}\n`, { flag: "wx", mode: 0o600 })
renameSync(executable, dest)
renameSync(checksum, `${dest}.sha256`)
} finally {
rmSync(executable, { force: true })
rmSync(checksum, { force: true })
}

if (!validLocalBwrap(dest)) throw new Error(`Could not validate staged Bubblewrap executable at ${dest}`)
}

export async function ensureBwrapForTarget(target: string, root?: string): Promise<string | undefined> {
const dest = localBwrapPath(target, root)
if (!dest) return

const executable = source()
if (executable) {
const digest = localBwrapDigest(executable)
if (validLocalBwrap(dest) && localBwrapDigest(dest) === digest) return dest
stage(executable, dest, digest)
return dest
}
if (validLocalBwrap(dest)) return dest

const { buildBubblewrap } = await import("../../opencode/script/kilocode/bubblewrap")
const built = await buildBubblewrap(arch(target))
stage(built.executable, dest, built.digest)
return dest
}
15 changes: 12 additions & 3 deletions packages/kilo-vscode/script/local-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
hasKiloSandboxWorker,
hasTreeSitterResources,
kiloSandboxWorkerForBinary,
sanitizeSandboxResources,
} from "../src/services/cli-backend/cli-resources"
import { currentBwrapTarget, ensureBwrapForTarget } from "./bwrap-helper"
import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper"

const forceRebuild = process.argv.includes("--force")
Expand Down Expand Up @@ -183,6 +185,13 @@ async function bundleKiloSandboxWorker() {
await Bun.write(kiloSandboxWorkerForBinary(targetBinPath), result.outputs[0])
}

async function ensureLocalHelpers() {
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
if (process.env.KILO_SKIP_BUNDLED_BWRAP === "1") return
if (await sanitizeSandboxResources(targetBinDir, true)) return
await ensureBwrapForTarget(currentBwrapTarget())
}

async function writeSourceWrapper() {
if (process.platform === "win32") {
throw new Error("Compiled CLI build failed and source wrapper fallback is not supported on Windows.")
Expand All @@ -202,7 +211,7 @@ async function writeSourceWrapper() {
)
chmodSync(targetBinPath, 0o755)
await bundleKiloSandboxWorker()
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
await ensureLocalHelpers()

const hash = await cliSourceHash()
if (hash) await Bun.write(versionFile, hash + "\n")
Expand All @@ -224,7 +233,7 @@ async function main() {
log(
`CLI binary already present at ${relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB). Use --force to rebuild.`,
)
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
await ensureLocalHelpers()
return
}

Expand Down Expand Up @@ -257,7 +266,7 @@ async function main() {
await copySandboxResources(sourceBinPath, targetBinPath)
await copyKiloSandboxWorker(sourceBinPath, targetBinPath)
chmodSync(targetBinPath, 0o755)
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
await ensureLocalHelpers()

const hash = await cliSourceHash()
if (hash) await Bun.write(versionFile, hash + "\n")
Expand Down
92 changes: 86 additions & 6 deletions packages/kilo-vscode/src/services/cli-backend/cli-resources.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as fs from "fs"
import * as crypto from "crypto"
import * as os from "os"
import * as path from "path"

const dir = "tree-sitter"
const runtime = "tree-sitter.wasm"
const kiloSandboxWorker = "kilo-sandbox-mutation-worker.js"
const bwrap = "bwrap"
const bwrapLicense = path.join("licenses", "bubblewrap")
const bwrapLicenseFiles = ["NOTICE", "COPYING", "MUSL-COPYRIGHT", "build.ts"]

function paths(file: string) {
if (/^[a-z]:[\\/]/i.test(file) || file.includes("\\")) return path.win32
Expand Down Expand Up @@ -51,17 +56,17 @@ export async function copyTreeSitterResources(source: string, target: string): P
export async function copySandboxResources(source: string, target: string): Promise<void> {
const from = path.dirname(source)
const to = path.dirname(target)
const helper = path.join(to, "bwrap")
const destination = path.join(to, "licenses", "bubblewrap")
const helper = path.join(to, bwrap)
const destination = path.join(to, bwrapLicense)
await fs.promises.rm(helper, { force: true })
await fs.promises.rm(destination, { recursive: true, force: true })

const bwrap = path.join(from, "bwrap")
if (!fs.existsSync(bwrap)) return
await fs.promises.copyFile(bwrap, helper)
const executable = path.join(from, bwrap)
if (!fs.existsSync(executable)) return
await fs.promises.copyFile(executable, helper)
await fs.promises.chmod(helper, 0o755)

const licenses = path.join(from, "licenses", "bubblewrap")
const licenses = path.join(from, bwrapLicense)
if (!fs.existsSync(licenses)) return
await fs.promises.cp(licenses, destination, { recursive: true })
}
Expand All @@ -72,3 +77,78 @@ export async function copyKiloSandboxWorker(source: string, target: string): Pro
if (!fs.existsSync(from)) throw new Error(`Kilo sandbox mutation worker not found at ${from}`)
await fs.promises.copyFile(from, to)
}

function cacheRoot() {
const root = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache")
return path.join(root, "kilo-vscode", "bwrap")
}

function bwrapLicenseDir(bin: string) {
return path.join(bin, bwrapLicense)
}

function hasBwrapSource(dir: string) {
try {
return fs.readdirSync(dir).some((file) => /^bubblewrap-[a-f0-9]+\.tar\.gz$/.test(file))
} catch {
return false
}
}

function hasProductionBwrap(bin: string) {
const executable = path.join(bin, bwrap)
const dir = bwrapLicenseDir(bin)
try {
const entry = fs.statSync(executable)
if (!entry.isFile()) return false
if (!bwrapLicenseFiles.every((file) => fs.existsSync(path.join(dir, file)))) return false
return hasBwrapSource(dir)
} catch {
return false
}
}

export function localBwrapPath(target: string, root = cacheRoot()): string | undefined {
if (target !== "linux-x64" && target !== "linux-arm64") return undefined
return path.join(root, target, bwrap)
}

export function localBwrapDigest(file: string): string {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")
}

export function validLocalBwrap(file: string): boolean {
try {
const entry = fs.lstatSync(file)
if (entry.isSymbolicLink() || !entry.isFile() || (entry.mode & 0o6000) !== 0) return false
if ((entry.mode & 0o022) !== 0 || (entry.mode & 0o111) === 0) return false
const digest = fs.readFileSync(`${file}.sha256`, "utf8").trim()
if (!/^[a-f0-9]{64}$/.test(digest)) return false
return localBwrapDigest(file) === digest
} catch {
return false
}
}

export function resolveLocalBwrapEnv(
extension: string,
local: boolean,
target = `${process.platform === "win32" ? "win32" : process.platform}-${process.arch}`,
root?: string,
): Record<string, string> {
if (hasProductionBwrap(path.join(extension, "bin"))) return {}
if (!local) return {}

const executable = localBwrapPath(target, root)
if (!executable || !validLocalBwrap(executable)) return {}
return { KILO_BWRAP_PATH: executable }
}

export async function sanitizeSandboxResources(bin: string, local: boolean): Promise<boolean> {
if (hasProductionBwrap(bin)) return true
if (!local) return false

await fs.promises.rm(path.join(bin, bwrap), { force: true })
await fs.promises.rm(bwrapLicenseDir(bin), { recursive: true, force: true })
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as crypto from "crypto"
import * as fs from "fs"
import * as path from "path"
import * as vscode from "vscode"
import { resolveTreeSitterEnv } from "./cli-resources"
import { resolveLocalBwrapEnv, resolveTreeSitterEnv } from "./cli-resources"
import { t } from "./i18n"
import { parseServerPort } from "./server-utils"

Expand Down Expand Up @@ -94,6 +94,10 @@ export class ServerManager {
const spawnCwd = resolveServerCwd(folders, this.context.globalStorageUri.fsPath)
fs.mkdirSync(spawnCwd, { recursive: true })
const indexingEnv = resolveIndexingEnv(folders)
const localCli =
this.context.extensionMode === vscode.ExtensionMode.Development ||
fs.existsSync(path.join(this.context.extensionPath, "bin", ".cli-version"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: .cli-version will mark packaged installs as local too

prepare:cli-binary also runs for the package script, and .vscodeignore includes bin/**, so the marker written by local-bin.ts is shipped in the VSIX as well. That makes this branch true outside F5/dev sessions, so production installs can start injecting KILO_BWRAP_PATH from ~/.cache/kilo-vscode/bwrap whenever the bundled helper looks incomplete, which is broader than the local-only fallback this change is trying to add.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const bwrapEnv = process.env.KILO_BWRAP_PATH ? {} : resolveLocalBwrapEnv(this.context.extensionPath, localCli)
// TLS / corporate-proxy support:
// - Default NODE_USE_SYSTEM_CA=1 so the bundled Bun CLI trusts the OS
// trust store (Windows cert store, macOS keychain, Linux /etc/ssl).
Expand Down Expand Up @@ -141,6 +145,7 @@ export class ServerManager {
KILOCODE_EDITOR_NAME: `${vscode.env.appName} ${vscode.version}`,
...(!claudeCompat && { KILO_DISABLE_CLAUDE_CODE: "true" }),
...resolveTreeSitterEnv(this.context.extensionPath),
...bwrapEnv,
},
stdio: ["ignore", "pipe", "pipe"],
detached: true,
Expand Down
Loading
Loading