feat: promote sandbox configuration

This commit is contained in:
marius-kilocode
2026-07-08 20:48:27 +02:00
parent ffcc1235cc
commit fe7eff7e27
51 changed files with 333 additions and 173 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
---
"kilo-code": patch
"@kilocode/cli": minor
"@kilocode/sdk": minor
---
Show sandbox controls in the dedicated Sandboxing settings page for all supported macOS and Linux users while keeping sandboxing disabled by default.
Configure sandboxing through first-class sandbox settings, and show its controls in the dedicated Sandboxing page for all supported macOS and Linux users while keeping it disabled by default.
@@ -10,7 +10,7 @@ The sandbox adds an operating-system boundary around agent tools. It limits wher
The sandbox is **disabled by default**. It does not restrict filesystem reads. An agent can still read any file that your user account can read, but it can write only to explicitly allowed locations.
{% callout type="warning" %}
Sandboxing is experimental and is not available on Windows. If the macOS or Linux sandbox backend is unavailable, Kilo reports the reason and runs tools without sandbox confinement. The sandbox does not fail closed.
Sandboxing is not available on Windows. If the macOS or Linux sandbox backend is unavailable, Kilo reports the reason and runs tools without sandbox confinement. The sandbox does not fail closed.
{% /callout %}
## Enable the sandbox
@@ -29,19 +29,21 @@ You can also configure the default in the global `kilo.jsonc` file:
```json
{
"experimental": {
"sandbox": true,
"sandbox_restrict_network": true,
"sandbox_writable_paths": ["~/shared-output"]
"sandbox": {
"enabled": true,
"network": "deny",
"writable_paths": ["~/shared-output"]
}
}
```
| Key | Default | Effect |
|---|---|---|
| `experimental.sandbox` | `false` | Use sandbox confinement by default for new sessions. |
| `experimental.sandbox_restrict_network` | `true` | Block outbound network access while filesystem confinement is active. Set this to `false` to allow network access without removing filesystem write restrictions. |
| `experimental.sandbox_writable_paths` | `[]` | Add writable files or directories outside the built-in writable locations. For security, only the global config can set these paths. |
| `sandbox.enabled` | `false` | Use sandbox confinement by default for new sessions. |
| `sandbox.network` | `"deny"` | Control outbound network access while filesystem confinement is active. Set this to `"allow"` to permit network access without removing filesystem write restrictions. |
| `sandbox.writable_paths` | `[]` | Add writable files or directories outside the built-in writable locations. Only global config may set these paths. |
Project config may tighten sandbox policy by setting `enabled` to `true` or `network` to `"deny"`. It cannot disable a globally enabled sandbox, allow network denied by global config, or add writable paths. This prevents repository-controlled configuration from weakening the user's security boundary.
## When to use sandboxing
@@ -97,7 +99,7 @@ Writes are allowed in:
- The active project or worktree
- Kilo's data, cache, config, state, temporary, binary, log, and repository directories
- Paths listed in `experimental.sandbox_writable_paths`
- Paths listed in `sandbox.writable_paths`
Writes are denied everywhere else. The following rules still apply inside writable locations:
@@ -18,7 +18,7 @@ export async function sandboxDefault(preference: SandboxPreference | undefined,
const explicit = preference?.explicit()
if (explicit !== undefined) return explicit
const { data } = await client.config.get({ directory }, { throwOnError: true })
return data.experimental?.sandbox === true
return data.sandbox?.enabled === true
}
export async function sandboxSessionMetadata(
@@ -70,8 +70,14 @@ test.describe("settings tab accessibility", () => {
const network = page.getByRole("switch", { name: "Restrict Network Access" })
await expect(network).toHaveAccessibleDescription(/Local MCP servers and plugin hooks run outside this restriction/)
await expect(network).toBeChecked()
await expect(network).toBeDisabled()
const path = page.getByRole("textbox", { name: "Additional Writable Paths" })
await expect(path).toBeDisabled()
await expect(page.getByRole("button", { name: "Add" })).toBeDisabled()
await page.locator('[data-slot="switch-control"]').nth(0).click()
await expect(sandbox).toBeChecked()
await expect(network).toBeEnabled()
await expect(path).toBeEnabled()
await page.locator('[data-slot="switch-control"]').nth(1).click()
await expect(network).not.toBeChecked()
await expect(page.locator(".settings-save-bar")).toBeVisible()
@@ -20,7 +20,7 @@ describe("NewWorktreeDialog sandbox toggle", () => {
expect(src).toContain("sandbox: sandboxVisible() ? sandboxOverride() : undefined")
expect(src).toContain("const sandboxVisible = () => features().sandboxControls")
expect(provider).toContain("await this.fetchAndSendSandboxDefault(message.contextDirectory, message.requestID)")
expect(src).not.toContain("createSignal(config().experimental?.sandbox === true)")
expect(src).not.toContain("createSignal(config().sandbox?.enabled === true)")
expect(src).not.toContain("visible as isSandboxVisible")
})
})
@@ -85,7 +85,7 @@ describe("PromptInput sandbox toggle", () => {
expect(src).toContain(
'const sandboxVisible = () => features().sandboxControls && !session.currentSessionID()?.startsWith("cloud:")',
)
expect(src).not.toContain("config().experimental?.sandbox === true")
expect(src).not.toContain("config().sandbox?.enabled === true")
expect(src).toContain("<Show when={sandboxVisible()}>")
expect(src).toContain("{ action: toggleSandbox, enabled: () => sandboxVisible() && !sandboxDisabled() }")
expect(src).toContain('if (!sandboxVisible()) hidden.add("sandbox")')
@@ -121,9 +121,7 @@ describe("PromptInput sandbox toggle", () => {
})
it("explains filesystem and network state without changing the lock icon", () => {
expect(src).toContain(
"const sandboxNetworkEnabled = () => config().experimental?.sandbox_restrict_network !== false",
)
expect(src).toContain('const sandboxNetworkEnabled = () => config().sandbox?.network !== "allow"')
expect(src).toContain("<SandboxTooltipContent enabled={sandboxEnabled()} network={sandboxNetworkEnabled()} />")
expect(src).toContain('tooltipClass="prompt-sandbox-tooltip-content"')
expect(button).toContain('<Icon name="lock" size="small" />')
@@ -19,6 +19,12 @@ describe("Sandboxing settings visibility", () => {
expect(visible({ ...features, sandboxControls: true })).toBe(true)
})
test("edits global sandbox config without promoting project policy", async () => {
const src = await Bun.file("webview-ui/src/components/settings/SandboxingTab.tsx").text()
expect(src).toContain("const { globalConfig, updateGlobalConfig } = useConfig()")
expect(src).not.toContain("const { config, updateConfig } = useConfig()")
})
test("shows sandbox controls outside Windows", () => {
setPlatform("darwin")
expect(configFeatures().sandboxControls).toBe(true)
@@ -526,7 +526,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
tooltip={
<SandboxTooltipContent
enabled={sandbox() ?? false}
network={config().experimental?.sandbox_restrict_network !== false}
network={config().sandbox?.network !== "allow"}
/>
}
tooltipClass="prompt-sandbox-tooltip-content"
@@ -183,7 +183,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sandboxAvailable = () => (sandboxID() ? sandbox()?.available : sandboxDefault()?.available) ?? false
const sandboxReason = () => (sandboxID() ? sandbox()?.reason : sandboxDefault()?.reason)
const sandboxReady = () => (sandboxID() ? sandbox() !== undefined : sandboxDefault() !== undefined)
const sandboxNetworkEnabled = () => config().experimental?.sandbox_restrict_network !== false
const sandboxNetworkEnabled = () => config().sandbox?.network !== "allow"
const sandboxRequest = (sessionID?: string) => sandboxRequests()[sessionID ?? ""]
const sandboxDisabled = () =>
!server.isConnected() || !sandboxReady() || !sandboxAvailable() || sandboxRequest(sandboxID()) !== undefined
@@ -13,12 +13,12 @@ const networkDescription = "sandbox-network-description"
const writablePathsDescription = "sandbox-writable-paths-description"
const SandboxingTab: Component = () => {
const { config, updateConfig } = useConfig()
const { globalConfig, updateGlobalConfig } = useConfig()
const language = useLanguage()
const experimental = createMemo(() => config().experimental ?? {})
const sandbox = createMemo(() => globalConfig().sandbox ?? {})
const [newPath, setNewPath] = createSignal("")
const writablePaths = () => experimental().sandbox_writable_paths ?? []
const writablePaths = () => sandbox().writable_paths ?? []
const addPath = () => {
const value = newPath().trim()
@@ -26,8 +26,8 @@ const SandboxingTab: Component = () => {
const current = [...writablePaths()]
if (!current.includes(value)) {
current.push(value)
updateConfig({
experimental: { ...experimental(), sandbox_writable_paths: current },
updateGlobalConfig({
sandbox: { ...sandbox(), writable_paths: current },
})
}
setNewPath("")
@@ -36,29 +36,29 @@ const SandboxingTab: Component = () => {
const removePath = (index: number) => {
const current = [...writablePaths()]
current.splice(index, 1)
updateConfig({
experimental: { ...experimental(), sandbox_writable_paths: current },
updateGlobalConfig({
sandbox: { ...sandbox(), writable_paths: current },
})
}
return (
<Card>
<SettingsRow
title={language.t("settings.experimental.sandbox.title")}
description={language.t("settings.experimental.sandbox.description")}
title={language.t("settings.sandboxing.enabled.title")}
description={language.t("settings.sandboxing.enabled.description")}
descriptionId={enabledDescription}
>
<Switch
checked={experimental().sandbox ?? false}
checked={sandbox().enabled ?? false}
inputProps={{ "aria-describedby": enabledDescription }}
onChange={(checked) =>
updateConfig({
experimental: { ...experimental(), sandbox: checked },
updateGlobalConfig({
sandbox: { ...sandbox(), enabled: checked },
})
}
hideLabel
>
{language.t("settings.experimental.sandbox.title")}
{language.t("settings.sandboxing.enabled.title")}
</Switch>
</SettingsRow>
@@ -68,14 +68,12 @@ const SandboxingTab: Component = () => {
descriptionId={networkDescription}
>
<Switch
checked={experimental().sandbox_restrict_network !== false}
checked={sandbox().network !== "allow"}
disabled={sandbox().enabled !== true}
inputProps={{ "aria-describedby": networkDescription }}
onChange={(checked) =>
updateConfig({
experimental: {
...experimental(),
sandbox_restrict_network: checked,
},
updateGlobalConfig({
sandbox: { ...sandbox(), network: checked ? "deny" : "allow" },
})
}
hideLabel
@@ -105,6 +103,7 @@ const SandboxingTab: Component = () => {
<div style={{ flex: 1 }}>
<TextField
value={newPath()}
disabled={sandbox().enabled !== true}
placeholder="/tmp"
onChange={(val) => setNewPath(val)}
onKeyDown={(e: KeyboardEvent) => {
@@ -114,7 +113,7 @@ const SandboxingTab: Component = () => {
label={language.t("settings.sandboxing.writablePaths.title")}
/>
</div>
<Button variant="secondary" onClick={addPath}>
<Button variant="secondary" disabled={sandbox().enabled !== true} onClick={addPath}>
{language.t("common.add")}
</Button>
</div>
@@ -138,7 +137,13 @@ const SandboxingTab: Component = () => {
>
{path}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removePath(index())} />
<IconButton
size="small"
variant="ghost"
icon="close"
disabled={sandbox().enabled !== true}
onClick={() => removePath(index())}
/>
</div>
)}
</For>
@@ -38,6 +38,7 @@ export const KNOWN_KEYS: ReadonlyArray<string> = [
"terminal_command_display",
"code_edit_display",
"hide_prompt_training_models",
"sandbox",
"indexing",
"experimental",
]
+2 -2
View File
@@ -1561,8 +1561,8 @@ export const dict = {
"settings.agentBehaviour.workflows.empty": "لم يتم تهيئة أوامر مخصصة. أضف أوامر إلى opencode.json لرؤيتها هنا.",
"settings.agentBehaviour.workflows.detail.description": "الوصف",
"settings.agentBehaviour.workflows.detail.template": "القالب",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"تشغيل أوامر shell الخاصة بالوكيل داخل sandbox على مستوى نظام التشغيل يقيّد الكتابة على مجلدات حالة المشروع و Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1603,8 +1603,8 @@ export const dict = {
"Nenhum comando personalizado configurado. Adicione comandos ao opencode.json para vê-los aqui.",
"settings.agentBehaviour.workflows.detail.description": "Descrição",
"settings.agentBehaviour.workflows.detail.template": "Modelo",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Executar os comandos shell do agente dentro de um sandbox a nível de sistema operacional que restringe escritas aos diretórios de estado do projeto e do Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1595,8 +1595,8 @@ export const dict = {
"Nema konfiguriranih prilagođenih komandi. Dodajte komande u opencode.json da ih vidite ovdje.",
"settings.agentBehaviour.workflows.detail.description": "Opis",
"settings.agentBehaviour.workflows.detail.template": "Predložak",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Pokrenite shell komande agenta unutar sandboxa na nivou operativnog sistema koji ograničava pisanje na direktorije stanja projekta i Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1588,8 +1588,8 @@ export const dict = {
"Ingen brugerdefinerede kommandoer konfigureret. Tilføj kommandoer til opencode.json for at se dem her.",
"settings.agentBehaviour.workflows.detail.description": "Beskrivelse",
"settings.agentBehaviour.workflows.detail.template": "Skabelon",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Kør shell-kommandoer for agenten i en sandbox på operativsystemniveau, der begrænser skrivning til projekt- og Kilo-tilstandsmapperne",
"settings.autoApprove.description":
@@ -1622,8 +1622,8 @@ export const dict = {
"Keine benutzerdefinierten Befehle konfiguriert. Fügen Sie Befehle zu opencode.json hinzu, um sie hier zu sehen.",
"settings.agentBehaviour.workflows.detail.description": "Beschreibung",
"settings.agentBehaviour.workflows.detail.template": "Vorlage",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Shell-Befehle des Agenten in einer Sandbox auf Betriebssystemebene ausführen, die Schreibvorgänge auf die Projekt- und Kilo-Statusverzeichnisse beschränkt",
"settings.autoApprove.description":
@@ -1425,8 +1425,8 @@ export const dict = {
"Enable experimental tools for reading, editing, and executing VS Code notebooks",
"settings.experimental.continueOnDeny.title": "Continue on Deny",
"settings.experimental.continueOnDeny.description": "Continue the agent loop when a permission is denied",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Run agent shell commands inside an OS-level sandbox that restricts writes to the project and Kilo state directories",
"settings.sandboxing.title": "Sandboxing",
"settings.sandboxing.network.title": "Restrict Network Access",
+2 -2
View File
@@ -1611,8 +1611,8 @@ export const dict = {
"No hay comandos personalizados configurados. Añada comandos a opencode.json para verlos aquí.",
"settings.agentBehaviour.workflows.detail.description": "Descripción",
"settings.agentBehaviour.workflows.detail.template": "Plantilla",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Ejecutar los comandos de shell del agente dentro de un sandbox a nivel de sistema operativo que restringe las escrituras a los directorios de estado del proyecto y de Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1628,8 +1628,8 @@ export const dict = {
"Aucune commande personnalisée configurée. Ajoutez des commandes à opencode.json pour les voir ici.",
"settings.agentBehaviour.workflows.detail.description": "Description",
"settings.agentBehaviour.workflows.detail.template": "Modèle",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Exécuter les commandes shell de l'agent dans un sandbox au niveau du système d'exploitation qui restreint les écritures aux répertoires d'état du projet et de Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1306,8 +1306,8 @@ export const dict = {
"Fai clic per limitare le scritture nel file system e l'accesso alla rete.",
"prompt.action.sandbox.description.disabledNetworkAllowed":
"Fai clic per limitare le scritture nel file system. L'accesso alla rete resta consentito dalle impostazioni della sandbox.",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Esegui i comandi shell dell'agente all'interno di un sandbox a livello di sistema operativo che limita le scritture alle directory di stato del progetto e di Kilo",
"settings.agentBehaviour.skillPaths": "Percorsi cartelle skill",
+2 -2
View File
@@ -1585,8 +1585,8 @@ export const dict = {
"カスタムコマンドが設定されていません。opencode.json にコマンドを追加するとここに表示されます。",
"settings.agentBehaviour.workflows.detail.description": "説明",
"settings.agentBehaviour.workflows.detail.template": "テンプレート",
"settings.experimental.sandbox.title": "サンドボックス",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "サンドボックス",
"settings.sandboxing.enabled.description":
"エージェントのシェルコマンドを、プロジェクトおよびKiloの状態ディレクトリへの書き込みを制限するOSレベルのサンドボックス内で実行",
"settings.autoApprove.description":
+2 -2
View File
@@ -1573,8 +1573,8 @@ export const dict = {
"구성된 사용자 정의 명령이 없습니다. opencode.json에 명령을 추가하면 여기에 표시됩니다.",
"settings.agentBehaviour.workflows.detail.description": "설명",
"settings.agentBehaviour.workflows.detail.template": "템플릿",
"settings.experimental.sandbox.title": "샌드박스",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "샌드박스",
"settings.sandboxing.enabled.description":
"에이전트 셸 명령을 프로젝트 및 Kilo 상태 디렉터리에 대한 쓰기를 제한하는 OS 수준의 샌드박스 내에서 실행",
"settings.autoApprove.description":
+2 -2
View File
@@ -1472,8 +1472,8 @@ export const dict = {
"settings.experimental.remote.inactive": "Inactief",
"settings.experimental.remote.hint": "Gebruik /remote in de chat om te schakelen",
"settings.experimental.toolToggles": "Tool Schakelaars",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Shell-opdrachten van de agent uitvoeren in een sandbox op besturingssysteemniveau die schrijfbewerkingen beperkt tot de project- en Kilo-statusmappen",
"settings.agentBehaviour.defaultAgent.title": "Standaard Agent",
+2 -2
View File
@@ -1588,8 +1588,8 @@ export const dict = {
"Ingen egendefinerte kommandoer konfigurert. Legg til kommandoer i opencode.json for å se dem her.",
"settings.agentBehaviour.workflows.detail.description": "Beskrivelse",
"settings.agentBehaviour.workflows.detail.template": "Mal",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Kjør shell-kommandoer for agenten i en sandbox på operativsystemnivå som begrenser skriving til prosjekt- og Kilo-tilstandsmapper",
"settings.autoApprove.description":
+2 -2
View File
@@ -1592,8 +1592,8 @@ export const dict = {
"Brak skonfigurowanych niestandardowych komend. Dodaj komendy do opencode.json, aby je tu zobaczyć.",
"settings.agentBehaviour.workflows.detail.description": "Opis",
"settings.agentBehaviour.workflows.detail.template": "Szablon",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Uruchamiaj polecenia shell agenta w sandboxie na poziomie systemu operacyjnego, który ogranicza zapisy do katalogów stanu projektu i Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1593,8 +1593,8 @@ export const dict = {
"Пользовательские команды не настроены. Добавьте команды в opencode.json, чтобы увидеть их здесь.",
"settings.agentBehaviour.workflows.detail.description": "Описание",
"settings.agentBehaviour.workflows.detail.template": "Шаблон",
"settings.experimental.sandbox.title": "Песочница",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Песочница",
"settings.sandboxing.enabled.description":
"Выполнять команды оболочки агента в песочнице на уровне ОС, которая ограничивает запись в каталоги состояния проекта и Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1570,8 +1570,8 @@ export const dict = {
"ไม่มีคำสั่งแบบกำหนดเองที่กำหนดค่าไว้ เพิ่มคำสั่งใน opencode.json เพื่อดูที่นี่",
"settings.agentBehaviour.workflows.detail.description": "คำอธิบาย",
"settings.agentBehaviour.workflows.detail.template": "เทมเพลต",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"เรียกใช้คำสั่ง shell ของ agent ใน sandbox ระดับระบบปฏิบัติการที่จำกัดการเขียนไปยังโฟลเดอร์สถานะของโปรเจ็กต์และ Kilo",
"settings.autoApprove.description":
+2 -2
View File
@@ -1462,8 +1462,8 @@ export const dict = {
"settings.experimental.remote.inactive": "Pasif",
"settings.experimental.remote.hint": "Geçiş yapmak için sohbette /remote kullanın",
"settings.experimental.toolToggles": "Araç Açma/Kapatma",
"settings.experimental.sandbox.title": "Sandbox",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Sandbox",
"settings.sandboxing.enabled.description":
"Agent shell komutlarını, proje ve Kilo durum dizinlerine yazmaları kısıtlanan işletim sistemi düzeyinde bir sandbox içinde çalıştırın",
"settings.agentBehaviour.defaultAgent.title": "Varsayılan Ajan",
+2 -2
View File
@@ -1460,8 +1460,8 @@ export const dict = {
"settings.experimental.remote.inactive": "Неактивний",
"settings.experimental.remote.hint": "Використовуйте /remote у чаті для перемикання",
"settings.experimental.toolToggles": "Перемикачі інструментів",
"settings.experimental.sandbox.title": "Пісочниця",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "Пісочниця",
"settings.sandboxing.enabled.description":
"Виконувати команди оболонки агента в пісочниці на рівні ОС, яка обмежує запис до каталогів стану проєкту та Kilo",
"settings.agentBehaviour.defaultAgent.title": "Агент за замовчуванням",
+2 -2
View File
@@ -1533,8 +1533,8 @@ export const dict = {
"settings.agentBehaviour.workflows.empty": "未配置自定义命令。将命令添加到 opencode.json 即可在此处看到。",
"settings.agentBehaviour.workflows.detail.description": "描述",
"settings.agentBehaviour.workflows.detail.template": "模板",
"settings.experimental.sandbox.title": "沙盒",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "沙盒",
"settings.sandboxing.enabled.description":
"在操作系统级沙盒中运行代理 shell 命令,将写入限制在项目和 Kilo 状态目录内",
"settings.autoApprove.description":
+2 -2
View File
@@ -1499,8 +1499,8 @@ export const dict = {
"settings.agentBehaviour.workflows.empty": "未設定自訂命令。將命令新增至 opencode.json 即可在此處看到。",
"settings.agentBehaviour.workflows.detail.description": "描述",
"settings.agentBehaviour.workflows.detail.template": "範本",
"settings.experimental.sandbox.title": "沙盒",
"settings.experimental.sandbox.description":
"settings.sandboxing.enabled.title": "沙盒",
"settings.sandboxing.enabled.description":
"在作業系統層級沙盒中執行代理 shell 指令,將寫入限制在專案和 Kilo 狀態目錄內",
"settings.autoApprove.description":
@@ -54,7 +54,7 @@ export const SettingsPanel: Story = {
export const SandboxingPanel: Story = {
name: "Settings — sandboxing controls",
render: () => (
<StoryProviders config={{ experimental: { sandbox_restrict_network: true } }} features={{ sandboxControls: true }}>
<StoryProviders config={{ sandbox: { network: "deny" } }} features={{ sandboxControls: true }}>
<div style={{ height: "700px", display: "flex", "flex-direction": "column" }}>
<Settings tab="sandboxing" />
</div>
@@ -48,13 +48,16 @@ export interface ExperimentalConfig {
primary_tools?: string[]
continue_loop_on_deny?: boolean
mcp_timeout?: number
sandbox?: boolean
sandbox_restrict_network?: boolean
sandbox_writable_paths?: string[]
swe_pruner?: boolean
swe_pruner_model?: string
}
export interface SandboxConfig {
enabled?: boolean
network?: "allow" | "deny"
writable_paths?: string[]
}
export interface CommitMessageConfig {
prompt?: string
}
@@ -151,6 +154,7 @@ export interface Config {
tools?: Record<string, boolean>
auto_collapse_reasoning?: boolean
experimental?: ExperimentalConfig
sandbox?: SandboxConfig
indexing?: IndexingConfig
}
+3 -16
View File
@@ -54,6 +54,7 @@ import { primaryPaths } from "../kilocode/primary-worktree"
import { Git } from "@/git"
import { KilocodeDefaultPlugins } from "@/kilocode/config/default-plugins"
import { KilocodeGlobalConfigStamp } from "@/kilocode/config/global-stamp"
import { SandboxConfig } from "@/kilocode/sandbox/config"
import {
IndexingConfig as KiloIndexingConfig,
IndexingSchema as KiloIndexingSchema,
@@ -250,6 +251,7 @@ export const Info = Schema.Struct({
hide_prompt_training_models: Schema.optional(Schema.Boolean).annotate({
description: "Hide Kilo Gateway models that may train on your prompts from model listings",
}),
sandbox: Schema.optional(SandboxConfig.Info),
model: Schema.optional(Schema.NullOr(ConfigModelID)).annotate({
description: "Model to use in the format of provider/model, eg anthropic/claude-2",
}),
@@ -416,18 +418,6 @@ export const Info = Schema.Struct({
description: "Continue the agent loop when a tool call is denied",
}),
// kilocode_change start
sandbox: Schema.optional(Schema.Boolean).annotate({
description:
"Run agent tools inside a sandbox that restricts writes to project and Kilo state directories and can restrict outbound network access",
}),
sandbox_restrict_network: Schema.optional(Schema.Boolean).annotate({
description:
"Restrict outbound network access for model-originated commands and first-party HTTP tools; local MCP servers and plugin hooks are not covered (default: true)",
}),
sandbox_writable_paths: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description:
"Additional filesystem paths the sandbox allows writes to (e.g. ['/tmp', '/var/log']). These are merged with the default writable paths when the sandbox is active.",
}),
swe_pruner: Schema.optional(Schema.Boolean).annotate({
description:
"Enable SWE-Pruner: task-aware pruning of large read/grep tool outputs guided by a focus question provided by the agent (default: false)",
@@ -785,10 +775,7 @@ export const layer = Layer.effect(
// kilocode_change start
const merge = Effect.fnUntraced(function* (source: string, next: Info, kind?: ConfigPlugin.Scope) {
const scope = kind ?? (yield* pluginScopeForSource(source))
// sandbox_writable_paths is security-sensitive — only global config may set it.
// A project kilo.json must not widen the sandbox beyond the user's intent.
if (scope === "local") delete next.experimental?.sandbox_writable_paths
const scoped = KilocodeConfig.scopeIndexing(next, scope)
const scoped = KilocodeConfig.scopeIndexing(SandboxConfig.scope(next, scope), scope)
result = mergeConfigConcatArrays(result, scoped)
return yield* mergePluginOrigins(source, scoped.plugin, scope)
})
@@ -39,7 +39,7 @@ function View(props: {
}) {
createEffect(
on(
() => props.api.state.config.experimental?.sandbox,
() => props.api.state.config.sandbox?.enabled,
() => void props.load(props.sessionID, true),
),
)
@@ -0,0 +1,40 @@
import { Schema } from "effect"
export namespace SandboxConfig {
export const Network = Schema.Literals(["allow", "deny"])
export type Network = Schema.Schema.Type<typeof Network>
export const Info = Schema.Struct({
enabled: Schema.optional(
Schema.Boolean.annotate({ description: "Enable sandbox confinement for new sessions (default: false)" }),
),
network: Schema.optional(
Network.annotate({ description: "Control outbound network access from sandboxed tools (default: deny)" }),
),
writable_paths: Schema.optional(
Schema.mutable(Schema.Array(Schema.String)).annotate({
description: "Additional filesystem paths that sandboxed tools may write to",
}),
),
}).annotate({ description: "Sandbox configuration for agent tools" })
export type Info = Schema.Schema.Type<typeof Info>
export function resolve(config: { sandbox?: Info }) {
return {
enabled: config.sandbox?.enabled ?? false,
mode: config.sandbox?.network ?? "deny",
}
}
export function scope<T extends { sandbox?: Info }>(config: T, source: "global" | "local"): T {
if (source === "global" || config.sandbox === undefined) return config
const scoped = { ...config }
const sandbox: Info = {
...(config.sandbox.enabled === true ? { enabled: true } : {}),
...(config.sandbox.network === "deny" ? { network: "deny" as const } : {}),
}
if (Object.keys(sandbox).length > 0) scoped.sandbox = sandbox
else delete scoped.sandbox
return scoped
}
}
@@ -13,6 +13,7 @@ import { Changed } from "./event"
import * as Network from "./network"
import { SandboxPreference } from "./preference"
import * as SandboxState from "./state"
import { SandboxConfig } from "./config"
import { SandboxStore } from "./store"
export type Snapshot = SandboxStore.Snapshot
@@ -39,8 +40,8 @@ const resolveInitial = Effect.fn("SandboxPolicy.resolveInitial")(function* (dire
const cfg = yield* (yield* Config.Service).get()
const chosen = yield* SandboxState.read(sessionID)
const pref = yield* Effect.promise(() => SandboxPreference.read(directory))
const mode = cfg.experimental?.sandbox_restrict_network === false ? "allow" : "deny"
return initial(chosen?.enabled, pref, cfg.experimental?.sandbox ?? false, mode)
const fallback = SandboxConfig.resolve(cfg)
return initial(chosen?.enabled, pref, fallback.enabled, fallback.mode)
})
function locked<A, E, R>(sessionID: SessionID, effect: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
@@ -170,10 +171,13 @@ const snapshot = Effect.fn("SandboxPolicy.snapshot")(function* (sessionID: Sessi
export const configuredSupport = Effect.fn("SandboxPolicy.configuredSupport")(function* () {
const cfg = yield* (yield* Config.Service).get()
const mode = cfg.experimental?.sandbox_restrict_network === false ? "allow" : "deny"
return backendSupport({ mode, allowedHosts: [] })
return backendSupport({ mode: SandboxConfig.resolve(cfg).mode, allowedHosts: [] })
})
export function fallback(config: Config.Info) {
return SandboxConfig.resolve(config)
}
export const status = Effect.fn("SandboxPolicy.status")(function* (sessionID: SessionID) {
const current = yield* snapshot(sessionID)
const support = backendSupport({ mode: current.state.mode, allowedHosts: [] })
@@ -304,7 +308,7 @@ function execute<A, E, R>(sessionID: SessionID, effect: Effect.Effect<A, E, R>)
const support = backendSupport({ mode: current.state.mode, allowedHosts: [] })
if (!current.state.enabled || !support.available) return yield* unrestricted(effect)
const cfg = yield* (yield* Config.Service).get()
const raw = cfg.experimental?.sandbox_writable_paths
const raw = cfg.sandbox?.writable_paths
const extraWritable = raw?.map((p) => (p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p))
return yield* runSandbox(profile(yield* InstanceState.context, current.state.mode, extraWritable), effect)
})
+1 -2
View File
@@ -178,8 +178,7 @@ export const TaskTool = Tool.define(
const rules = KiloTask.inherited({ caller, session: parent, mcp: cfg.mcp })
// kilocode_change end
// kilocode_change start - refresh current parent restrictions when resuming an existing task session
const mode: "allow" | "deny" = cfg.experimental?.sandbox_restrict_network === false ? "allow" : "deny"
const fallback = { enabled: cfg.experimental?.sandbox ?? false, mode }
const fallback = SandboxPolicy.fallback(cfg)
if (session) {
yield* SandboxPolicy.inherit(ctx.sessionID, session.id, fallback)
const permission = KiloTask.merge(
@@ -242,8 +242,8 @@ describe("kilocode indexing config", () => {
})
})
describe("kilocode sandbox writable paths config", () => {
test("honors sandbox_writable_paths from global config only, ignoring project config", async () => {
describe("kilocode sandbox config", () => {
test("prevents project config from weakening sandbox policy", async () => {
await using globalTmp = await tmpdir()
await using tmp = await tmpdir({ git: true })
@@ -255,18 +255,48 @@ describe("kilocode sandbox writable paths config", () => {
try {
await writeConfig(globalTmp.path, {
$schema: "https://app.kilo.ai/config.json",
experimental: { sandbox_writable_paths: ["/tmp/global"] },
sandbox: { enabled: true, network: "deny", writable_paths: ["/tmp/global"] },
})
// A project kilo.json must not widen the sandbox: its writable paths are dropped at merge time.
await writeConfig(tmp.path, {
experimental: { sandbox_writable_paths: ["/tmp/project"] },
sandbox: { enabled: false, network: "allow", writable_paths: ["/tmp/project"] },
})
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const config = await load()
expect(config.experimental?.sandbox_writable_paths).toEqual(["/tmp/global"])
expect(config.sandbox).toEqual({ enabled: true, network: "deny", writable_paths: ["/tmp/global"] })
},
})
} finally {
;(Global.Path as { config: string }).config = prev
await clear()
await disposeAllInstances()
}
})
test("allows project config to strengthen sandbox policy", async () => {
await using globalTmp = await tmpdir()
await using tmp = await tmpdir({ git: true })
const prev = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
await clear()
await disposeAllInstances()
try {
await writeConfig(globalTmp.path, {
sandbox: { enabled: false, network: "allow", writable_paths: ["/tmp/global"] },
})
await writeConfig(tmp.path, {
sandbox: { enabled: true, network: "deny", writable_paths: ["/tmp/project"] },
})
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const config = await load()
expect(config.sandbox).toEqual({ enabled: true, network: "deny", writable_paths: ["/tmp/global"] })
},
})
} finally {
@@ -29,10 +29,7 @@ function layer(restrict?: boolean) {
TestConfig.layer({
get: () =>
Effect.succeed({
experimental: {
sandbox: true,
sandbox_restrict_network: restrict,
},
sandbox: { enabled: true, network: restrict === false ? "allow" : "deny" },
}),
}),
)
@@ -3,14 +3,11 @@ import type { Config as ConfigV1 } from "@kilocode/sdk"
import type { Config as ConfigV2 } from "@kilocode/sdk/v2"
const value = {
experimental: {
sandbox: true,
sandbox_restrict_network: false,
},
sandbox: { enabled: true, network: "allow" as const, writable_paths: ["/tmp/output"] },
}
test("both public SDK Config types expose sandbox policy fields", () => {
const legacy = value satisfies ConfigV1
const current = value satisfies ConfigV2
expect(legacy.experimental).toEqual(current.experimental)
expect(legacy.sandbox).toEqual(current.sandbox)
})
@@ -89,7 +89,7 @@ function context(directory: string, main: string, sandboxes: string[]): Instance
}
const config = TestConfig.layer({
get: () => Effect.succeed({ experimental: { sandbox: true } }),
get: () => Effect.succeed({ sandbox: { enabled: true } }),
})
const agents = Layer.mock(Agent.Service)({
get: () => Effect.succeed(agent),
@@ -32,7 +32,7 @@ describe("sandbox session cleanup", () => {
it.live("forks inherit the source session snapshot", () =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const dir = yield* tmpdirScoped({ git: true, config: { experimental: { sandbox: true } } })
const dir = yield* tmpdirScoped({ git: true, config: { sandbox: { enabled: true } } })
const source = yield* provideInstance(dir)(sessions.create({ title: "sandbox-source" }))
const status = yield* provideInstance(dir)(SandboxPolicy.status(source.id))
if (!status.available) return
@@ -49,7 +49,7 @@ describe("sandbox session cleanup", () => {
it.live("forks into another directory carry the source confinement", () =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const dir = yield* tmpdirScoped({ git: true, config: { experimental: { sandbox: true } } })
const dir = yield* tmpdirScoped({ git: true, config: { sandbox: { enabled: true } } })
const worktree = yield* tmpdirScoped({ git: true })
const source = yield* provideInstance(dir)(sessions.create({ title: "sandbox-source" }))
const status = yield* provideInstance(dir)(SandboxPolicy.status(source.id))
@@ -68,7 +68,7 @@ describe("sandbox session cleanup", () => {
Effect.gen(function* () {
const sessions = yield* Session.Service
// Config default is disabled; the create-time toggle asks for enabled.
const dir = yield* tmpdirScoped({ git: true, config: { experimental: { sandbox: false } } })
const dir = yield* tmpdirScoped({ git: true, config: { sandbox: { enabled: false } } })
const session = yield* provideInstance(dir)(
sessions.create({ title: "sandbox-explicit", metadata: { "kilocode.sandbox": { enabled: true, version: 0 } } }),
)
@@ -31,10 +31,7 @@ function configured(restrict: boolean) {
TestConfig.layer({
get: () =>
Effect.succeed({
experimental: {
sandbox: true,
sandbox_restrict_network: restrict,
},
sandbox: { enabled: true, network: restrict ? "deny" : "allow" },
}),
}),
)
@@ -6,7 +6,7 @@ import { Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { assertNetwork, enabled as sandboxed } from "@kilocode/sandbox"
import { assertNetwork, assertWrite, enabled as sandboxed } from "@kilocode/sandbox"
import { Bus } from "@/bus"
import { Config } from "@/config/config"
import * as Network from "@/kilocode/sandbox/network"
@@ -69,9 +69,9 @@ test("restores the session snapshot after a backend restart", async () => {
}
try {
const initial = run({ experimental: { sandbox: true, sandbox_restrict_network: true } })
const initial = run({ sandbox: { enabled: true, network: "deny" } })
expect(initial.state).toEqual({ enabled: true, mode: "deny", version: 0 })
const restored = run({ experimental: { sandbox: false, sandbox_restrict_network: false } })
const restored = run({ sandbox: { enabled: false, network: "allow" } })
expect(restored.state).toEqual(initial.state)
expect(restored.status.enabled).toBe(restored.status.available)
} finally {
@@ -127,7 +127,7 @@ linux("reports configured network namespace availability", async () => {
'import { SessionID } from "@/session/schema"',
"const directory = process.cwd()",
'const context = { directory, worktree: directory, project: { id: "sandbox-status", worktree: directory, vcs: "git", time: { created: 0, updated: 0 }, sandboxes: [] } }',
"const status = (restrict) => SandboxPolicy.status(SessionID.make(`ses_sandbox_status_${restrict}`)).pipe(Effect.provide(Layer.mock(Config.Service, { get: () => Effect.succeed({ experimental: { sandbox: true, sandbox_restrict_network: restrict } }) })), Effect.provideService(InstanceRef, context), Effect.runPromise)",
"const status = (restrict) => SandboxPolicy.status(SessionID.make(`ses_sandbox_status_${restrict}`)).pipe(Effect.provide(Layer.mock(Config.Service, { get: () => Effect.succeed({ sandbox: { enabled: true, network: restrict ? 'deny' : 'allow' } }) })), Effect.provideService(InstanceRef, context), Effect.runPromise)",
"const deny = await status(true)",
"const allow = await status(false)",
'if (deny.available || deny.enabled || !deny.reason?.includes("Linux network sandbox")) process.exit(2)',
@@ -161,10 +161,8 @@ it.instance("snapshots the primary kilo config for the session lifetime", () =>
const file = path.join(test.directory, "kilo.json")
const legacy = path.join(test.directory, "opencode.json")
const config = yield* Config.Service
yield* Effect.promise(() =>
Bun.write(file, JSON.stringify({ experimental: { sandbox: true, sandbox_restrict_network: true } })),
)
yield* config.update({ experimental: { sandbox: true, sandbox_restrict_network: true } })
yield* Effect.promise(() => Bun.write(file, JSON.stringify({ sandbox: { enabled: true, network: "deny" } })))
yield* config.update({ sandbox: { enabled: true, network: "deny" } })
const id = SessionID.make("ses_sandbox_config")
const initial = yield* SandboxPolicy.status(id)
@@ -172,12 +170,10 @@ it.instance("snapshots the primary kilo config for the session lifetime", () =>
expect(initial.version).toBe(0)
if (!initial.available) return
yield* Effect.promise(() =>
Bun.write(file, JSON.stringify({ experimental: { sandbox: false, sandbox_restrict_network: false } })),
)
yield* config.update({ experimental: { sandbox: false, sandbox_restrict_network: false } })
yield* Effect.promise(() => Bun.write(file, JSON.stringify({ sandbox: { enabled: false, network: "allow" } })))
yield* config.update({ sandbox: { enabled: false, network: "allow" } })
expect((yield* config.get()).experimental?.sandbox).toBe(false)
expect((yield* config.get()).sandbox?.enabled).toBeUndefined()
expect(yield* Effect.promise(() => Bun.file(legacy).exists())).toBe(false)
expect((yield* SandboxPolicy.status(id)).enabled).toBe(true)
expect(yield* execute(id, sandboxed)).toBe(true)
@@ -191,7 +187,7 @@ it.instance("snapshots the primary kilo config for the session lifetime", () =>
),
)
it.instance("does not enable authless sessions without the experimental sandbox flag", () =>
it.instance("does not enable authless sessions without sandbox enabled", () =>
Effect.acquireUseRelease(
Effect.sync(() => {
const password = Flag.KILO_SERVER_PASSWORD
@@ -215,6 +211,30 @@ it.instance("does not enable authless sessions without the experimental sandbox
),
)
it.instance("applies configured writable paths during tool execution", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const outside = path.join(path.dirname(test.directory), `sandbox-writable-${path.basename(test.directory)}`)
yield* Effect.promise(() => fs.mkdir(outside, { recursive: true }))
yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(outside, { recursive: true, force: true })))
const id = SessionID.make("ses_sandbox_writable_config")
const result = yield* Effect.gen(function* () {
const status = yield* SandboxPolicy.status(id)
if (!status.available) return undefined
return yield* execute(id, assertWrite(path.join(outside, "allowed.txt")).pipe(Effect.exit))
}).pipe(
Effect.provide(
Layer.mock(Config.Service, {
get: () => Effect.succeed({ sandbox: { enabled: true, network: "allow", writable_paths: [outside] } }),
}),
),
)
if (result === undefined) return
expect(Exit.isSuccess(result)).toBe(true)
}),
)
it.instance(
"runs sandboxed when config is on and no override exists",
() =>
@@ -224,7 +244,7 @@ it.instance(
expect(status.enabled).toBe(status.available)
expect(yield* execute(id, sandboxed)).toBe(status.available)
}),
{ config: { experimental: { sandbox: true } } },
{ config: { sandbox: { enabled: true } } },
)
it.instance(
@@ -240,7 +260,7 @@ it.instance(
expect((yield* SandboxPolicy.status(second)).enabled).toBe(false)
expect(yield* execute(second, sandboxed)).toBe(false)
}),
{ config: { experimental: { sandbox: true } } },
{ config: { sandbox: { enabled: true } } },
)
it.instance("persists an authless toggle to later sessions", () =>
@@ -271,7 +291,7 @@ it.instance(
expect((yield* SandboxPolicy.status(third)).enabled).toBe(true)
expect(yield* execute(third, sandboxed)).toBe(true)
}),
{ config: { experimental: { sandbox: true } } },
{ config: { sandbox: { enabled: true } } },
)
it.instance("isolates concurrent session overrides and clears them", () =>
@@ -364,7 +384,7 @@ it.instance(
expect((yield* SandboxPolicy.status(child)).enabled).toBe(true)
expect(yield* execute(child, sandboxed)).toBe(true)
}),
{ config: { experimental: { sandbox: true } } },
{ config: { sandbox: { enabled: true } } },
)
it.instance("enforces writes only while the macOS session override is active", () =>
@@ -26,7 +26,7 @@ describe("sandbox TUI", () => {
expect(content).toContain("await ensureSession(api)")
expect(content).toContain("api.client.session.create")
expect(content).toContain('api.route.navigate("session", { sessionID })')
expect(content).toContain("props.api.state.config.experimental?.sandbox")
expect(content).toContain("props.api.state.config.sandbox?.enabled")
expect(content).toContain("void props.load(props.sessionID, true)")
expect(content).toContain('api.event.on("sandbox.status.changed"')
})
@@ -435,7 +435,7 @@ describe("Kilo task nesting", () => {
expect(count).toBeGreaterThan(0)
expect(resumed.permission?.filter((rule) => rule.permission === "bash")).toHaveLength(count ?? 0)
}),
{ config: { experimental: { sandbox: true } } },
{ config: { sandbox: { enabled: true } } },
),
)
+31
View File
@@ -58,6 +58,37 @@ if (sseTypesPatched === sseTypesSource) {
}
await Bun.write(sseTypesPath, sseTypesPatched)
// The legacy SDK generator is retired, but this public Config type remains exported.
// Keep Kilo's released sandbox settings aligned with the current generated client.
const legacyTypesPath = "./src/gen/types.gen.ts"
const legacyTypesFile = Bun.file(legacyTypesPath)
const legacySource = await legacyTypesFile.text()
const sandbox = ` /**
* Sandbox configuration for agent tools
*/
sandbox?: {
/**
* Enable sandbox confinement for new sessions (default: false)
*/
enabled?: boolean
/**
* Control outbound network access from sandboxed tools (default: deny)
*/
network?: "allow" | "deny"
/**
* Additional filesystem paths that sandboxed tools may write to
*/
writable_paths?: Array<string>
}
`
const legacyPatched = legacySource.includes(sandbox)
? legacySource
: legacySource.replace(" experimental?: {\n", sandbox + " experimental?: {\n")
if (!legacyPatched.includes(sandbox)) {
throw new Error(`Legacy Config sandbox patch did not apply (${legacyTypesPath})`)
}
await Bun.write(legacyTypesPath, legacyPatched)
await $`bun prettier --write src/gen`
await $`bun prettier --write src/v2`
await $`rm -rf dist tsconfig.tsbuildinfo`
+17 -8
View File
@@ -1343,6 +1343,23 @@ export type Config = {
*/
url?: string
}
/**
* Sandbox configuration for agent tools
*/
sandbox?: {
/**
* Enable sandbox confinement for new sessions (default: false)
*/
enabled?: boolean
/**
* Control outbound network access from sandboxed tools (default: deny)
*/
network?: "allow" | "deny"
/**
* Additional filesystem paths that sandboxed tools may write to
*/
writable_paths?: Array<string>
}
experimental?: {
hook?: {
file_edited?: {
@@ -1373,14 +1390,6 @@ export type Config = {
* Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)
*/
openTelemetry?: boolean
/**
* Run agent tools inside a sandbox that restricts writes to project and Kilo state directories and can restrict outbound network access
*/
sandbox?: boolean
/**
* Restrict outbound network access for model-originated commands and first-party HTTP tools; local MCP servers and plugin hooks are not covered (default: true)
*/
sandbox_restrict_network?: boolean
/**
* Tools that should only be available to primary agents.
*/
+17 -3
View File
@@ -1577,6 +1577,23 @@ export type Config = {
terminal_command_display?: "expanded" | "collapsed"
code_edit_display?: "expanded" | "collapsed"
hide_prompt_training_models?: boolean
/**
* Sandbox configuration for agent tools
*/
sandbox?: {
/**
* Enable sandbox confinement for new sessions (default: false)
*/
enabled?: boolean
/**
* Control outbound network access from sandboxed tools (default: deny)
*/
network?: "allow" | "deny"
/**
* Additional filesystem paths that sandboxed tools may write to
*/
writable_paths?: Array<string>
}
model?: string
small_model?: string
subagent_model?: string
@@ -1693,9 +1710,6 @@ export type Config = {
openTelemetry?: boolean
primary_tools?: Array<string>
continue_loop_on_deny?: boolean
sandbox?: boolean
sandbox_restrict_network?: boolean
sandbox_writable_paths?: Array<string>
swe_pruner?: boolean
swe_pruner_model?: string
mcp_timeout?: number
+23 -12
View File
@@ -24916,6 +24916,29 @@
"hide_prompt_training_models": {
"type": "boolean"
},
"sandbox": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable sandbox confinement for new sessions (default: false)"
},
"network": {
"type": "string",
"enum": ["allow", "deny"],
"description": "Control outbound network access from sandboxed tools (default: deny)"
},
"writable_paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional filesystem paths that sandboxed tools may write to"
}
},
"additionalProperties": false,
"description": "Sandbox configuration for agent tools"
},
"model": {
"type": "string"
},
@@ -25266,18 +25289,6 @@
"continue_loop_on_deny": {
"type": "boolean"
},
"sandbox": {
"type": "boolean"
},
"sandbox_restrict_network": {
"type": "boolean"
},
"sandbox_writable_paths": {
"type": "array",
"items": {
"type": "string"
}
},
"swe_pruner": {
"type": "boolean"
},