From 394af39c64b2920fa8c84f14670f213820cef2ec Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 19:13:20 +0200 Subject: [PATCH 1/8] fix(vscode): move sandbox toggle into sandbox settings --- .changeset/sandbox-settings-page.md | 5 + .../pages/getting-started/settings/index.md | 7 +- .../getting-started/settings/sandboxing.md | 117 ++++++++++++------ .../tests/settings-accessibility.spec.ts | 9 +- .../tests/unit/sandboxing-settings.test.ts | 11 +- .../components/settings/ExperimentalTab.tsx | 20 +-- .../src/components/settings/SandboxingTab.tsx | 26 +++- .../src/components/settings/Settings.tsx | 4 +- .../src/components/settings/sandboxing.ts | 6 +- .../src/stories/settings.stories.tsx | 7 +- 10 files changed, 131 insertions(+), 81 deletions(-) create mode 100644 .changeset/sandbox-settings-page.md diff --git a/.changeset/sandbox-settings-page.md b/.changeset/sandbox-settings-page.md new file mode 100644 index 0000000000..9d241df655 --- /dev/null +++ b/.changeset/sandbox-settings-page.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show sandbox controls in the dedicated Sandboxing settings page for all supported macOS and Linux users while keeping sandboxing disabled by default. diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index 56d69a1cf7..7a32691755 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -161,6 +161,12 @@ For **session** export and import, use the CLI commands: {% /tab %} {% /tabs %} +## Sandbox + +On macOS and Linux, the VS Code extension includes a dedicated **Sandboxing** settings tab. The sandbox is disabled by default. When enabled, it limits agent filesystem writes and can block outbound network access from model-originated tools. Windows users do not see these settings because Windows sandboxing is not supported. + +See [Sandboxing](/docs/getting-started/settings/sandboxing) for setup instructions, the exact filesystem and network boundaries, and platform limitations. + ## Experimental Features {% tabs %} @@ -175,7 +181,6 @@ Available experimental settings include: - **Paste summary** - summarize large clipboard pastes before including them - **Batch tool** - allow the agent to batch multiple tool calls in one step - **OpenTelemetry** - enable Kilo telemetry and optional OTLP export when configured -- **Sandbox** - confine agent shell commands and file writes to the project and Kilo state directories, with optional outbound network blocking. See [Sandboxing](/docs/getting-started/settings/sandboxing). Advanced options not exposed in the UI can be configured via the `experimental` key in `kilo.jsonc`: diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index ed9e2817fb..ae20cf8c9f 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -1,76 +1,113 @@ --- title: "Sandboxing" -description: "Confine agent shell commands and file writes with the experimental OS-level sandbox" +description: "Understand and configure filesystem write and network restrictions for agent tools" --- # Sandboxing -The experimental sandbox runs agent shell commands and file-tool writes inside an OS-level sandbox that restricts filesystem writes to your project and Kilo state directories, and can block outbound network access from model-originated commands. It is an extra guardrail on top of the permission system: even if the agent is allowed to run a command, the operating system will deny writes outside the allowed roots. +The sandbox adds an operating-system boundary around agent tools. It limits where tools can write and, by default, blocks outbound network access from model-originated commands. This boundary applies even when a tool passes Kilo's permission checks. + +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. Behavior may change between releases, and it is not available on Windows. +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. {% /callout %} -## How it works - -When enabled, the agent's shell commands and file-write tools run confined to a small set of writable directories: - -- Your **project directory** (and its worktree, when running in a linked git worktree) -- Kilo **state directories**: data, cache, config, state, tmp, bin, log, and repos - -Everything else is denied at the OS level. File **reads are not confined** — the agent can still read anywhere it has permission to. The `.git` directory is always denied for writes, regardless of location. - -When network restriction is on (the default), outbound network access is blocked for: - -- Shell commands originated by the model -- First-party HTTP tools (for example web fetch and browser tools) - -The following are **not** affected by the network restriction: - -- **Provider and model inference traffic** — your LLM API calls keep working -- **Local MCP servers and plugin hooks** — these run outside the restriction - ## Enable the sandbox -The sandbox is off by default. Enable it under the `experimental` key in `kilo.jsonc`: +In the VS Code extension: + +1. Open Kilo Code Settings using the gear icon ({% codicon name="gear" /%}). +2. Select **Sandboxing**. +3. Turn on **Sandbox**. +4. Keep **Restrict Network Access** on unless the agent's commands need outbound network access. +5. Save the settings. + +The **Sandboxing** tab is visible to all macOS and Linux users, including when the sandbox is off. Windows users do not see the tab because no Windows backend is available. + +You can also configure the default in the global `kilo.jsonc` file: ```json { "experimental": { "sandbox": true, - "sandbox_restrict_network": true + "sandbox_restrict_network": true, + "sandbox_writable_paths": ["~/shared-output"] } } ``` | Key | Default | Effect | |---|---|---| -| `experimental.sandbox` | `false` | Turn the sandbox on. When `false`, no confinement applies. | -| `experimental.sandbox_restrict_network` | `true` | Block outbound network from model-originated commands and HTTP tools. Set to `false` to allow network (filesystem confinement still applies). | +| `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. | -You can also enable it from the VS Code Settings webview: gear icon ({% codicon name="gear" /%}) → **Experimental** → **Sandbox**. Once the sandbox is on, a dedicated **Sandboxing** tab appears with the **Restrict Network Access** switch for `sandbox_restrict_network`. +## Filesystem restrictions -## Toggle per session +When the sandbox is active, agent tools can read files normally. The sandbox restricts writes, including creating, changing, renaming, and deleting files. -Enabling `experimental.sandbox` sets the default for new sessions, but the setting is ephemeral per session and can be flipped without editing config: +Writes are allowed in: -- **VS Code**: a sandbox toggle appears in the prompt input when `experimental.sandbox` is on (not available for cloud sessions). The tooltip shows whether filesystem writes and network are restricted. -- **CLI / TUI**: run the `/sandbox` slash command or the **Toggle sandbox** palette command. A `◆ Sandbox on` indicator appears next to the prompt when active. +- The active project or worktree +- Kilo's data, cache, config, state, temporary, binary, log, and repository directories +- Paths listed in `experimental.sandbox_writable_paths` -Toggling is in-memory and scoped to the current session, so it does not persist across restarts. If the OS sandbox backend is unavailable on your platform, the toggle reports the reason and confinement stays off. +Writes are denied everywhere else. The following rules still apply inside writable locations: + +- `.git` directories are always read-only to sandboxed tools. +- Kilo's stored sandbox policy and preference files are read-only. +- A permission approval for a path outside the sandbox does not make that path writable. Add the path to **Additional Writable Paths** if the tool must modify it. +- Linked worktree sessions can write to their active worktree, not the primary checkout or sibling worktrees. + +Shell commands and their child processes inherit the same restrictions. Kilo's file tools perform mutations through a sandboxed worker. Writable file handles are unavailable, so a tool that requires an open read-write handle may fail even for an allowed path. + +{% callout type="info" %} +The sandbox is a write boundary, not a privacy boundary. It does not prevent an agent from reading files outside your project if your operating-system account can read them. +{% /callout %} + +## Network restrictions + +**Restrict Network Access** controls outbound network access independently of filesystem writes. Turning it off leaves the filesystem write restrictions active. + +When network restriction is on, Kilo blocks: + +- Outbound network access from model-originated shell commands and their child processes +- Requests made through Kilo's policy-aware first-party HTTP clients +- Remote MCP tool calls and custom or plugin tools that Kilo cannot prove will remain offline +- Built-in tools such as codebase search, semantic search, and LSP that may use opaque or indirect network access + +Network restriction does not block: + +- Provider and model inference traffic, so conversations with the selected model continue to work +- Local MCP server processes +- Plugin hooks that run outside the sandboxed tool execution +- Filesystem reads + +This is not a system-wide firewall. It applies to the sandboxed tool execution boundary, not every Kilo, extension, or local process. Proxy environment variables are removed from sandboxed commands while network access is restricted. + +## Session behavior + +The config setting supplies the initial default for new sessions that do not have a saved preference. Use the lock button in the VS Code prompt or `/sandbox` in the CLI to change the current session. Your latest choice is saved as the default for future sessions in that project, takes precedence over the config default, and persists across restarts. + +Each initialized session keeps its sandbox enabled state and network mode. Changing those settings affects new sessions; use the prompt control or `/sandbox` to change an existing session's enabled state. Changes to **Additional Writable Paths** are read when tools run and therefore also apply to existing sandboxed sessions. + +Forked sessions retain the source session's confinement. Subagents inherit the stricter combination of the parent and child settings: sandboxing remains enabled if either requires it, and network remains blocked if either requires blocking. + +Cloud sessions do not expose the local sandbox control because their tools do not run in your local sandbox. ## Platform support | Platform | Backend | Notes | |---|---|---| -| macOS | `sandbox-exec` (seatbelt) | Uses the system `/usr/bin/sandbox-exec`. | -| Linux | Bubblewrap (`bwrap`) | Uses system `/usr/bin/bwrap`, or a bundled, SHA-256-verified binary. Override the path with the `KILO_BWRAP_PATH` environment variable. The executable is probed at startup to confirm it can create the sandbox. | -| Windows | none | The sandbox backend is unavailable on Windows. Enabling the config has no effect. | +| macOS | `sandbox-exec` (Seatbelt) | Uses `/usr/bin/sandbox-exec`. File reads and inbound networking remain allowed. | +| Linux | Bubblewrap (`bwrap`) | Uses system `/usr/bin/bwrap` or a bundled, SHA-256-verified binary. `KILO_BWRAP_PATH` can select another binary. Kilo probes filesystem and network namespace support before enabling confinement. | +| Windows | None | Unsupported. The VS Code settings and prompt controls are hidden, and enabling the config has no effect. | ## Limitations -- **Windows is not supported.** -- Local MCP servers and plugin hooks are **not** covered by the network restriction. -- File **reads** are not confined — only writes and shell command effects are. -- Writable file handles are unavailable while the sandbox is active; writes are performed through a sandboxed worker, so some tools that open files for writing may behave differently. -- The sandbox is additive to the permission system, not a replacement. Permission rules still apply first. +- The sandbox supplements Kilo's permission system; it does not replace permission prompts or rules. +- Local MCP servers and plugin hooks execute outside the operating-system sandbox. +- Direct filesystem access inside trusted in-process integrations is covered only when the integration uses Kilo's sandbox-aware filesystem service. +- Starting or restarting a background process with the background-process tool is unavailable while sandboxing is active. +- On Linux, an additional writable path must already exist before Bubblewrap starts. diff --git a/packages/kilo-vscode/tests/settings-accessibility.spec.ts b/packages/kilo-vscode/tests/settings-accessibility.spec.ts index b655b29e61..62c8176aed 100644 --- a/packages/kilo-vscode/tests/settings-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/settings-accessibility.spec.ts @@ -54,7 +54,7 @@ test.describe("settings tab accessibility", () => { await expect(page.getByRole("tabpanel", { name: "Models" })).toBeVisible() }) - test("shows sandboxing controls when the feature flag and experiment are enabled", async ({ page }) => { + test("shows sandboxing controls when the platform supports them", async ({ page }) => { await page.setViewportSize({ width: 420, height: 720 }) await page.goto(`/iframe.html?id=settings--sandboxing-panel&viewMode=story&globals=${GLOBALS}`, { waitUntil: "load", @@ -64,10 +64,15 @@ test.describe("settings tab accessibility", () => { await expect(tab).toBeVisible() await expect(tab).toHaveAttribute("aria-selected", "true") await expect(page.getByRole("tabpanel", { name: "Sandboxing" })).toBeVisible() + const sandbox = page.getByRole("switch", { name: "Sandbox", exact: true }) + await expect(sandbox).toHaveAccessibleDescription(/restricts writes to the project and Kilo state directories/) + await expect(sandbox).not.toBeChecked() 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 page.locator('[data-slot="switch-control"]').click() + await page.locator('[data-slot="switch-control"]').nth(0).click() + await expect(sandbox).toBeChecked() + await page.locator('[data-slot="switch-control"]').nth(1).click() await expect(network).not.toBeChecked() await expect(page.locator(".settings-save-bar")).toBeVisible() }) diff --git a/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts index d75037a0b3..a8d9af0b9e 100644 --- a/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts +++ b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts @@ -14,15 +14,12 @@ afterEach(() => { }) describe("Sandboxing settings visibility", () => { - test("requires both sandbox control availability and the sandbox experiment", () => { - expect(visible(features, {})).toBe(false) - expect(visible({ ...features, sandboxControls: true }, {})).toBe(false) - expect(visible(features, { experimental: { sandbox: true } })).toBe(false) - expect(visible({ ...features, sandboxControls: true }, { experimental: { sandbox: false } })).toBe(false) - expect(visible({ ...features, sandboxControls: true }, { experimental: { sandbox: true } })).toBe(true) + test("depends only on sandbox control availability", () => { + expect(visible(features)).toBe(false) + expect(visible({ ...features, sandboxControls: true })).toBe(true) }) - test("enables sandbox controls by default outside Windows", () => { + test("shows sandbox controls outside Windows", () => { setPlatform("darwin") expect(configFeatures().sandboxControls).toBe(true) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index bdabb16fe1..cbd400f47b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -24,7 +24,7 @@ const SHARE_OPTIONS: ShareOption[] = [ ] const ExperimentalTab: Component = () => { - const { config, features, updateConfig } = useConfig() + const { config, updateConfig } = useConfig() const language = useLanguage() const imageModels = useImageModels() const vscode = useVSCode() @@ -255,7 +255,7 @@ const ExperimentalTab: Component = () => { { }} /> - - - - updateExperimental("sandbox", checked)} - hideLabel - > - {language.t("settings.experimental.sandbox.title")} - - - {/* Tool toggles */} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx index 8ec16cb396..7c1497f3ae 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx @@ -8,7 +8,8 @@ import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import SettingsRow from "./SettingsRow" -const description = "sandbox-network-description" +const enabledDescription = "sandbox-enabled-description" +const networkDescription = "sandbox-network-description" const writablePathsDescription = "sandbox-writable-paths-description" const SandboxingTab: Component = () => { @@ -42,14 +43,33 @@ const SandboxingTab: Component = () => { return ( + + + updateConfig({ + experimental: { ...experimental(), sandbox: checked }, + }) + } + hideLabel + > + {language.t("settings.experimental.sandbox.title")} + + + updateConfig({ experimental: { diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index 836bf29af8..d109bb11e3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -39,11 +39,11 @@ const Settings: Component = (props) => { const server = useServer() const language = useLanguage() const vscode = useVSCode() - const { config, loading, isDirty, saving, saveError, saveConfig, discardConfig, features } = useConfig() + const { loading, isDirty, saving, saveError, saveConfig, discardConfig, features } = useConfig() const session = useSession() const [active, setActive] = createSignal(props.tab ?? "models") const [errorExpanded, setErrorExpanded] = createSignal(false) - const sandboxing = createMemo(() => Sandboxing.visible(features(), config())) + const sandboxing = createMemo(() => Sandboxing.visible(features())) const busyCount = () => Object.values(session.allStatusMap()).filter((s) => s.type === "busy").length diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts b/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts index 023549e77d..593690ca6c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts +++ b/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts @@ -1,5 +1,5 @@ -import type { Config, FeatureFlags } from "../../types/messages" +import type { FeatureFlags } from "../../types/messages" -export function visible(features: FeatureFlags, config: Config) { - return features.sandboxControls && config.experimental?.sandbox === true +export function visible(features: FeatureFlags) { + return features.sandboxControls } diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index f37cb81e43..190347c9b4 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -52,12 +52,9 @@ export const SettingsPanel: Story = { } export const SandboxingPanel: Story = { - name: "Settings — sandboxing network restriction", + name: "Settings — sandboxing controls", render: () => ( - +
From 74b6534bea9faf0f5c85be549a30d3a9a20579d5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 19:31:38 +0200 Subject: [PATCH 2/8] docs: clarify sandbox security boundaries --- .../getting-started/settings/sandboxing.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index ae20cf8c9f..ebfd412ef7 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -43,6 +43,52 @@ You can also configure the default in the global `kilo.jsonc` file: | `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. | +## When to use sandboxing + +Use the sandbox when the agent may run unfamiliar commands, install dependencies, execute code from an untrusted repository, or process content that could contain prompt injection. It provides a second boundary if the model makes a mistake or follows malicious instructions embedded in source files, issue text, web pages, or tool output. + +The sandbox can reduce the impact of an unsafe tool call by: + +- Preventing writes outside the project and other explicitly writable locations +- Keeping sandboxed commands from changing `.git` metadata +- Blocking direct outbound connections from sandboxed commands and policy-aware tools when network restriction is on +- Applying the same restrictions to child processes, such as package installation and build scripts launched by a shell command + +This can reduce the risk of auto-approving selected routine commands, such as builds and tests, by placing operating-system limits around many of their effects. It does **not** make **Allow Everything** safe. An allowed command can still modify or delete project files, alter other writable Kilo directories, consume data it can read, or write unsafe code that runs later outside the sandbox. + +The sandbox does not protect against every result of prompt injection. In particular, it does not prevent the agent from reading accessible files or including their contents in model context. It also cannot confine local MCP servers, plugin hooks, or any integration that runs outside the sandbox boundary. + +{% callout type="warning" %} +The network sandbox is not a provider privacy control. Provider and model inference traffic remains available. If Kilo reads a secret and includes it in a prompt, tool result, or conversation context, that content may be sent to the configured model provider even while network restriction is on. Choose providers with data-handling policies appropriate for your work, consider a local model for sensitive projects, and use read permissions to block or prompt for sensitive files. See [Prompt-Training Model Visibility](/docs/getting-started/settings#prompt-training-model-visibility). +{% /callout %} + +## Sandboxing and permissions + +Permissions and sandboxing solve different parts of the security problem and work best together. + +| Control | What it decides | Best used for | +|---|---|---| +| Permissions | Whether Kilo allows, asks about, or denies a matching tool invocation | Prompting for sensitive file reads, blocking specific commands or tools, reviewing consequential actions, and limiting MCP tool or subagent invocation | +| Sandbox | What an allowed tool call can change or connect to while it runs | Limiting the impact of model mistakes, prompt injection, malicious dependencies, and unexpected child-process behavior | + +Permissions can ask or deny Kilo tool invocations that read or change data. For example, set `read` or `external_directory` rules to `ask` or `deny` for credentials, personal files, or directories the agent does not need. Kilo's `read` tool also prompts for `.env` and `.env.*` unless you explicitly create a matching sensitive-file rule. See [Agent Permissions](/docs/customize/agent-permissions) for path and command rules. + +Permission rules are tool-specific and do not create a complete file-confidentiality boundary. A `read` denial controls Kilo's file-reading tool, but an allowed `grep` call, shell command, build script, or other process may read the same file through a different path. A child process can also print sensitive content into tool output, which may then become model context. Configure `grep`, `bash`, and other data-accessing tools separately, and avoid running untrusted code when sensitive files remain readable by your operating-system account. + +For a given tool invocation, approving a shell command does not grant writes outside the sandbox, and a path being writable inside the sandbox does not bypass a matching permission rule. Some integration code runs outside this boundary: plugin hooks can run before a tool's internal permission check, and a local MCP server starts as a separate trusted process. MCP permissions control exposed tool invocations, not everything the server process can do during startup or in the background. Enable only local MCP servers and plugins you trust. + +A practical setup for work on unfamiliar or partially trusted code is: + +- Keep `read`, `grep`, and unnecessary external-directory access set to `ask` or `deny` when they may expose sensitive content. +- Allow only routine tools and command patterns that you want to run without interruption. +- Keep shell approval prompts for commands with important in-project effects or commands that can read sensitive data, because the sandbox still allows project writes and filesystem reads. +- Enable the sandbox and keep network restriction on to reduce write and direct network-exfiltration impact if an approved action behaves unexpectedly. +- Add extra writable paths only when a known workflow requires them. + +For a stronger confidentiality boundary, remove sensitive files from the environment or run Kilo under a separate operating-system account, container, or virtual machine that cannot read them. If file contents must not leave your machine, use local inference and disable other integrations that can send data over the network. If remote processing is acceptable, choose a provider with data-handling terms suitable for the data involved. + +Configure these rules in **Settings > Auto Approve** or `kilo.jsonc`. See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) for the settings UI and default permission behavior. + ## Filesystem restrictions When the sandbox is active, agent tools can read files normally. The sandbox restricts writes, including creating, changing, renaming, and deleting files. @@ -109,5 +155,6 @@ Cloud sessions do not expose the local sandbox control because their tools do no - The sandbox supplements Kilo's permission system; it does not replace permission prompts or rules. - Local MCP servers and plugin hooks execute outside the operating-system sandbox. - Direct filesystem access inside trusted in-process integrations is covered only when the integration uses Kilo's sandbox-aware filesystem service. +- Kilo's config directory is writable to sandboxed tools. A shell command can change configuration, permissions, plugins, or additional writable paths that affect future tool calls, so do not rely on the sandbox alone to protect policy integrity. - Starting or restarting a background process with the background-process tool is unavailable while sandboxing is active. - On Linux, an additional writable path must already exist before Bubblewrap starts. From bdd95dabe0be0c74ecc2e300274f5fcea7409f4d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 19:31:45 +0200 Subject: [PATCH 3/8] fix(ci): restore visual baseline regeneration --- .github/workflows/visual-regression.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index e247ee3949..6181b7b7d5 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -5,6 +5,10 @@ on: pull_request: types: [opened, synchronize, reopened] +permissions: + contents: write + pull-requests: read + jobs: check-paths: name: Check changed paths @@ -63,8 +67,8 @@ jobs: uses: actions/checkout@v6 with: lfs: true - # use BOT_PAT only when later baseline pushes are allowed; github.token is read-only on Dependabot PRs. - token: ${{ secrets.BOT_PAT }} + # Use github.token for LFS access. BOT_PAT is used only for the final ref push. + token: ${{ github.token }} ref: ${{ github.head_ref }} - name: Checkout (read-only) @@ -175,7 +179,7 @@ jobs: if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update != 'true' id: commit-baselines env: - GH_TOKEN: ${{ secrets.BOT_PAT }} + BOT_PAT: ${{ secrets.BOT_PAT }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -186,7 +190,9 @@ jobs: else git commit -m "chore: update visual regression baselines" git lfs push --all origin - git push --no-verify + git -c http.https://github.com/.extraheader= push --no-verify \ + "https://x-access-token:${BOT_PAT}@github.com/${GITHUB_REPOSITORY}.git" \ + "HEAD:${GITHUB_HEAD_REF}" echo "changed=true" >> "$GITHUB_OUTPUT" fi @@ -219,8 +225,8 @@ jobs: uses: actions/checkout@v6 with: lfs: true - # use BOT_PAT only when later baseline pushes are allowed; github.token is read-only on Dependabot PRs. - token: ${{ secrets.BOT_PAT }} + # Use github.token for LFS access. BOT_PAT is used only for the final ref push. + token: ${{ github.token }} ref: ${{ github.head_ref }} - name: Checkout (read-only) @@ -361,7 +367,7 @@ jobs: if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update != 'true' id: commit-baselines-vscode env: - GH_TOKEN: ${{ secrets.BOT_PAT }} + BOT_PAT: ${{ secrets.BOT_PAT }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -372,7 +378,9 @@ jobs: else git commit -m "chore: update kilo-vscode visual regression baselines" git lfs push --all origin - git push --no-verify + git -c http.https://github.com/.extraheader= push --no-verify \ + "https://x-access-token:${BOT_PAT}@github.com/${GITHUB_REPOSITORY}.git" \ + "HEAD:${GITHUB_HEAD_REF}" echo "changed=true" >> "$GITHUB_OUTPUT" fi From ffcc1235cc2791aff2fc647f3a219e90ef79d2b4 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 19:40:53 +0200 Subject: [PATCH 4/8] fix(ci): use maintainer app for baseline commits --- .github/workflows/visual-regression.yml | 71 +++++++++++++++---------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 6181b7b7d5..81baf52d86 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -6,7 +6,7 @@ on: types: [opened, synchronize, reopened] permissions: - contents: write + contents: read pull-requests: read jobs: @@ -46,9 +46,10 @@ jobs: - name: Check baseline auto-commit permissions id: autocommit-check env: - BOT_PAT: ${{ secrets.BOT_PAT }} + MAINTAINER_APP_ID: ${{ secrets.KILO_MAINTAINER_APP_ID }} + MAINTAINER_APP_SECRET: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} run: | - if [ "${{ steps.fork-check.outputs.is_fork }}" != "true" ] && [ -n "$BOT_PAT" ]; then + if [ "${{ steps.fork-check.outputs.is_fork }}" != "true" ] && [ -n "$MAINTAINER_APP_ID" ] && [ -n "$MAINTAINER_APP_SECRET" ]; then echo "can_autocommit=true" >> "$GITHUB_OUTPUT" else echo "can_autocommit=false" >> "$GITHUB_OUTPUT" @@ -67,7 +68,7 @@ jobs: uses: actions/checkout@v6 with: lfs: true - # Use github.token for LFS access. BOT_PAT is used only for the final ref push. + # Use github.token for LFS access. The maintainer app is used only for generated commits. token: ${{ github.token }} ref: ${{ github.head_ref }} @@ -175,27 +176,33 @@ jobs: exit 1 fi - - name: Commit and push new baselines (if any) + - name: Check for baseline updates if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit.outputs.is_baseline_update != 'true' - id: commit-baselines - env: - BOT_PAT: ${{ secrets.BOT_PAT }} + id: baseline-changes run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" git add packages/kilo-docs/public/img/screenshot-tests/kilo-ui/ if git diff --cached --quiet; then - echo "No new baselines — nothing to commit." echo "changed=false" >> "$GITHUB_OUTPUT" else - git commit -m "chore: update visual regression baselines" - git lfs push --all origin - git -c http.https://github.com/.extraheader= push --no-verify \ - "https://x-access-token:${BOT_PAT}@github.com/${GITHUB_REPOSITORY}.git" \ - "HEAD:${GITHUB_HEAD_REF}" echo "changed=true" >> "$GITHUB_OUTPUT" fi + - name: Setup Git Committer + if: steps.baseline-changes.outputs.changed == 'true' + uses: ./.github/actions/setup-git-committer + with: + kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} + kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} + + - name: Commit and push new baselines (if any) + if: steps.baseline-changes.outputs.changed == 'true' + id: commit-baselines + run: | + git commit -m "chore: update visual regression baselines" + git lfs push --all origin + git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}" + echo "changed=true" >> "$GITHUB_OUTPUT" + - name: Fail if baselines changed if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines.outputs.changed == 'true' run: | @@ -225,7 +232,7 @@ jobs: uses: actions/checkout@v6 with: lfs: true - # Use github.token for LFS access. BOT_PAT is used only for the final ref push. + # Use github.token for LFS access. The maintainer app is used only for generated commits. token: ${{ github.token }} ref: ${{ github.head_ref }} @@ -363,27 +370,33 @@ jobs: exit 1 fi - - name: Commit and push new baselines (if any) + - name: Check for baseline updates if: needs.check-paths.outputs.can_autocommit == 'true' && steps.check-baseline-commit-vscode.outputs.is_baseline_update != 'true' - id: commit-baselines-vscode - env: - BOT_PAT: ${{ secrets.BOT_PAT }} + id: baseline-changes-vscode run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" git add packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/ if git diff --cached --quiet; then - echo "No new baselines — nothing to commit." echo "changed=false" >> "$GITHUB_OUTPUT" else - git commit -m "chore: update kilo-vscode visual regression baselines" - git lfs push --all origin - git -c http.https://github.com/.extraheader= push --no-verify \ - "https://x-access-token:${BOT_PAT}@github.com/${GITHUB_REPOSITORY}.git" \ - "HEAD:${GITHUB_HEAD_REF}" echo "changed=true" >> "$GITHUB_OUTPUT" fi + - name: Setup Git Committer + if: steps.baseline-changes-vscode.outputs.changed == 'true' + uses: ./.github/actions/setup-git-committer + with: + kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} + kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} + + - name: Commit and push new baselines (if any) + if: steps.baseline-changes-vscode.outputs.changed == 'true' + id: commit-baselines-vscode + run: | + git commit -m "chore: update kilo-vscode visual regression baselines" + git lfs push --all origin + git push --no-verify origin "HEAD:${GITHUB_HEAD_REF}" + echo "changed=true" >> "$GITHUB_OUTPUT" + - name: Fail if baselines changed if: needs.check-paths.outputs.can_autocommit == 'true' && steps.commit-baselines-vscode.outputs.changed == 'true' run: | From fe7eff7e27d8819536d1d933bbe6f14189bf197c Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 20:48:27 +0200 Subject: [PATCH 5/8] feat: promote sandbox configuration --- .changeset/sandbox-settings-page.md | 4 +- .../getting-started/settings/sandboxing.md | 20 ++++--- .../kilo-vscode/src/shared/sandbox-session.ts | 2 +- .../tests/settings-accessibility.spec.ts | 6 ++ .../unit/new-worktree-dialog-sandbox.test.ts | 2 +- .../prompt-input-connection-guard.test.ts | 6 +- .../tests/unit/sandboxing-settings.test.ts | 6 ++ .../agent-manager/NewWorktreeDialog.tsx | 2 +- .../src/components/chat/PromptInput.tsx | 2 +- .../src/components/settings/SandboxingTab.tsx | 47 +++++++++------- .../src/components/settings/settings-io.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ar.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/br.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/da.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/de.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/en.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/es.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/it.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/no.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/th.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 4 +- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 4 +- .../src/stories/settings.stories.tsx | 2 +- .../webview-ui/src/types/messages/config.ts | 10 +++- packages/opencode/src/config/config.ts | 19 +------ .../opencode/src/kilocode/plugins/sandbox.tsx | 2 +- .../opencode/src/kilocode/sandbox/config.ts | 40 +++++++++++++ .../opencode/src/kilocode/sandbox/policy.ts | 14 +++-- packages/opencode/src/tool/task.ts | 3 +- .../test/kilocode/config/config.test.ts | 42 ++++++++++++-- .../kilocode/sandbox/config-network.test.ts | 5 +- .../test/kilocode/sandbox/sdk-config.test.ts | 7 +-- .../kilocode/sandbox/session-tools.test.ts | 2 +- .../test/kilocode/sandbox/session.test.ts | 6 +- .../kilocode/sandbox/shell-network.test.ts | 5 +- .../test/kilocode/sandbox/state.test.ts | 56 +++++++++++++------ .../test/kilocode/sandbox/tui.test.ts | 2 +- .../test/kilocode/task-nesting.test.ts | 2 +- packages/sdk/js/script/build.ts | 31 ++++++++++ packages/sdk/js/src/gen/types.gen.ts | 25 ++++++--- packages/sdk/js/src/v2/gen/types.gen.ts | 20 ++++++- packages/sdk/openapi.json | 35 ++++++++---- 51 files changed, 333 insertions(+), 173 deletions(-) create mode 100644 packages/opencode/src/kilocode/sandbox/config.ts diff --git a/.changeset/sandbox-settings-page.md b/.changeset/sandbox-settings-page.md index 9d241df655..0ff82ebf30 100644 --- a/.changeset/sandbox-settings-page.md +++ b/.changeset/sandbox-settings-page.md @@ -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. diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index ebfd412ef7..a839755c42 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -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: diff --git a/packages/kilo-vscode/src/shared/sandbox-session.ts b/packages/kilo-vscode/src/shared/sandbox-session.ts index 4b689d3253..1940427e65 100644 --- a/packages/kilo-vscode/src/shared/sandbox-session.ts +++ b/packages/kilo-vscode/src/shared/sandbox-session.ts @@ -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( diff --git a/packages/kilo-vscode/tests/settings-accessibility.spec.ts b/packages/kilo-vscode/tests/settings-accessibility.spec.ts index 62c8176aed..75bd0343b0 100644 --- a/packages/kilo-vscode/tests/settings-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/settings-accessibility.spec.ts @@ -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() diff --git a/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts b/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts index 83dd1ef91f..30ecca6c7d 100644 --- a/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts +++ b/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts @@ -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") }) }) diff --git a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts index 8141a6c54c..c19fcf9621 100644 --- a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts @@ -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("") 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("") expect(src).toContain('tooltipClass="prompt-sandbox-tooltip-content"') expect(button).toContain('') diff --git a/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts index a8d9af0b9e..153a3570c8 100644 --- a/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts +++ b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts @@ -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) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 52639c60a3..5e54247108 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -526,7 +526,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran tooltip={ } tooltipClass="prompt-sandbox-tooltip-content" diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 1593dadf0c..83fea9348f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -183,7 +183,7 @@ export const PromptInput: Component = (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 diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx index 7c1497f3ae..36b11756b4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx @@ -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 ( - updateConfig({ - experimental: { ...experimental(), sandbox: checked }, + updateGlobalConfig({ + sandbox: { ...sandbox(), enabled: checked }, }) } hideLabel > - {language.t("settings.experimental.sandbox.title")} + {language.t("settings.sandboxing.enabled.title")} @@ -68,14 +68,12 @@ const SandboxingTab: Component = () => { descriptionId={networkDescription} > - updateConfig({ - experimental: { - ...experimental(), - sandbox_restrict_network: checked, - }, + updateGlobalConfig({ + sandbox: { ...sandbox(), network: checked ? "deny" : "allow" }, }) } hideLabel @@ -105,6 +103,7 @@ const SandboxingTab: Component = () => {
setNewPath(val)} onKeyDown={(e: KeyboardEvent) => { @@ -114,7 +113,7 @@ const SandboxingTab: Component = () => { label={language.t("settings.sandboxing.writablePaths.title")} />
- @@ -138,7 +137,13 @@ const SandboxingTab: Component = () => { > {path} - removePath(index())} /> + removePath(index())} + /> )} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts index 4bd6834634..d4b1949d2a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts +++ b/packages/kilo-vscode/webview-ui/src/components/settings/settings-io.ts @@ -38,6 +38,7 @@ export const KNOWN_KEYS: ReadonlyArray = [ "terminal_command_display", "code_edit_display", "hide_prompt_training_models", + "sandbox", "indexing", "experimental", ] diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 4c6ae3049b..2d79f5f7e3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index c5e1a4f97d..d7b07e2d8f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 15a9ac1ae1..e607fd977d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d2e353921d..191582c2b5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index b01aec173d..e178d46945 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 6e548a2dd1..244e11254d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 77b2bb5d6f..67a66ba5ef 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 5c139cc2ec..047a49d186 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 5b85cd2233..c4b9bca799 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index ba88ae1233..3b25253a14 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 99816103ac..54f4a299da 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index e87cc634d7..c109c03e2c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 69d22a69a1..107eb79c25 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 082af4a28a..be13a942f8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 2d5db91d2a..f2cf6903a4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 32081195f3..158701ad41 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 18bf4e0177..ca661c228f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -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", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 663cd97242..2042fd8a69 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -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": "Агент за замовчуванням", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 944905e0fd..36cc23affe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 3e4df69b7e..c8365f0bd1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -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": diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index 190347c9b4..3f9bfb7592 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -54,7 +54,7 @@ export const SettingsPanel: Story = { export const SandboxingPanel: Story = { name: "Settings — sandboxing controls", render: () => ( - +
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 73c4116606..6e3b980137 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -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 auto_collapse_reasoning?: boolean experimental?: ExperimentalConfig + sandbox?: SandboxConfig indexing?: IndexingConfig } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f9fd0b2234..f63301c480 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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) }) diff --git a/packages/opencode/src/kilocode/plugins/sandbox.tsx b/packages/opencode/src/kilocode/plugins/sandbox.tsx index bd1075f22d..444de8421e 100644 --- a/packages/opencode/src/kilocode/plugins/sandbox.tsx +++ b/packages/opencode/src/kilocode/plugins/sandbox.tsx @@ -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), ), ) diff --git a/packages/opencode/src/kilocode/sandbox/config.ts b/packages/opencode/src/kilocode/sandbox/config.ts new file mode 100644 index 0000000000..42209d30c2 --- /dev/null +++ b/packages/opencode/src/kilocode/sandbox/config.ts @@ -0,0 +1,40 @@ +import { Schema } from "effect" + +export namespace SandboxConfig { + export const Network = Schema.Literals(["allow", "deny"]) + export type Network = Schema.Schema.Type + + 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 + + export function resolve(config: { sandbox?: Info }) { + return { + enabled: config.sandbox?.enabled ?? false, + mode: config.sandbox?.network ?? "deny", + } + } + + export function scope(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 + } +} diff --git a/packages/opencode/src/kilocode/sandbox/policy.ts b/packages/opencode/src/kilocode/sandbox/policy.ts index 72fbb6654c..4a4b80e178 100644 --- a/packages/opencode/src/kilocode/sandbox/policy.ts +++ b/packages/opencode/src/kilocode/sandbox/policy.ts @@ -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(sessionID: SessionID, effect: Effect.Effect) { 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(sessionID: SessionID, effect: Effect.Effect) 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) }) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 1c24fad80b..f1768b8110 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -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( diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 6aff6b8a7b..89c6b8749a 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -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 { diff --git a/packages/opencode/test/kilocode/sandbox/config-network.test.ts b/packages/opencode/test/kilocode/sandbox/config-network.test.ts index d4a54b87b8..cc0f9b4d9d 100644 --- a/packages/opencode/test/kilocode/sandbox/config-network.test.ts +++ b/packages/opencode/test/kilocode/sandbox/config-network.test.ts @@ -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" }, }), }), ) diff --git a/packages/opencode/test/kilocode/sandbox/sdk-config.test.ts b/packages/opencode/test/kilocode/sandbox/sdk-config.test.ts index 15af3fe8ae..eb09ead790 100644 --- a/packages/opencode/test/kilocode/sandbox/sdk-config.test.ts +++ b/packages/opencode/test/kilocode/sandbox/sdk-config.test.ts @@ -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) }) diff --git a/packages/opencode/test/kilocode/sandbox/session-tools.test.ts b/packages/opencode/test/kilocode/sandbox/session-tools.test.ts index 135b8d2307..ab83730f0e 100644 --- a/packages/opencode/test/kilocode/sandbox/session-tools.test.ts +++ b/packages/opencode/test/kilocode/sandbox/session-tools.test.ts @@ -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), diff --git a/packages/opencode/test/kilocode/sandbox/session.test.ts b/packages/opencode/test/kilocode/sandbox/session.test.ts index 8cb90f4a82..954ab6972f 100644 --- a/packages/opencode/test/kilocode/sandbox/session.test.ts +++ b/packages/opencode/test/kilocode/sandbox/session.test.ts @@ -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 } } }), ) diff --git a/packages/opencode/test/kilocode/sandbox/shell-network.test.ts b/packages/opencode/test/kilocode/sandbox/shell-network.test.ts index f05fbeaa21..a767ba9c0c 100644 --- a/packages/opencode/test/kilocode/sandbox/shell-network.test.ts +++ b/packages/opencode/test/kilocode/sandbox/shell-network.test.ts @@ -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" }, }), }), ) diff --git a/packages/opencode/test/kilocode/sandbox/state.test.ts b/packages/opencode/test/kilocode/sandbox/state.test.ts index 2d41d8a218..9a8906b021 100644 --- a/packages/opencode/test/kilocode/sandbox/state.test.ts +++ b/packages/opencode/test/kilocode/sandbox/state.test.ts @@ -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", () => diff --git a/packages/opencode/test/kilocode/sandbox/tui.test.ts b/packages/opencode/test/kilocode/sandbox/tui.test.ts index 4834c27b3e..fe65da6f87 100644 --- a/packages/opencode/test/kilocode/sandbox/tui.test.ts +++ b/packages/opencode/test/kilocode/sandbox/tui.test.ts @@ -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"') }) diff --git a/packages/opencode/test/kilocode/task-nesting.test.ts b/packages/opencode/test/kilocode/task-nesting.test.ts index ef8e2e497b..cfc2d195b1 100644 --- a/packages/opencode/test/kilocode/task-nesting.test.ts +++ b/packages/opencode/test/kilocode/task-nesting.test.ts @@ -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 } } }, ), ) diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts index 93d5331a91..eb8e7b5976 100755 --- a/packages/sdk/js/script/build.ts +++ b/packages/sdk/js/script/build.ts @@ -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 + } +` +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` diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 0ccb02e9b5..8a4ddf1da6 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -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 + } 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. */ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a64ee1f7b3..032d32e92d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -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 + } model?: string small_model?: string subagent_model?: string @@ -1693,9 +1710,6 @@ export type Config = { openTelemetry?: boolean primary_tools?: Array continue_loop_on_deny?: boolean - sandbox?: boolean - sandbox_restrict_network?: boolean - sandbox_writable_paths?: Array swe_pruner?: boolean swe_pruner_model?: string mcp_timeout?: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 4d20ce87d4..ba8a20a801 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -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" }, From 4ebfea867f0c50e1706e1702c9f5400a25120bd4 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Wed, 8 Jul 2026 18:52:21 +0000 Subject: [PATCH 6/8] chore: update kilo-vscode visual regression baselines --- .../settings/sandboxing-panel-chromium-linux.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png index 6d462eb5f3..3bde409405 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea36a9604fbc773d2312289f46205221e607a927b0d302b1d9a5e6bdc1d80308 -size 28888 +oid sha256:3c0693f1ad6eed615e5a49733ef3a3ec58026fac922cb80e2f3720c94755591f +size 47933 From 5697e7169ad36ea5d9dc0a29bd8228e0d40b3c75 Mon Sep 17 00:00:00 2001 From: Marius Date: Thu, 9 Jul 2026 12:59:51 +0200 Subject: [PATCH 7/8] Update packages/kilo-docs/pages/getting-started/settings/sandboxing.md Co-authored-by: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> --- packages/kilo-docs/pages/getting-started/settings/sandboxing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index a839755c42..fe0fd90308 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -51,7 +51,7 @@ Use the sandbox when the agent may run unfamiliar commands, install dependencies The sandbox can reduce the impact of an unsafe tool call by: -- Preventing writes outside the project and other explicitly writable locations +- Preventing writes outside the workspace and other explicitly writable locations - Keeping sandboxed commands from changing `.git` metadata - Blocking direct outbound connections from sandboxed commands and policy-aware tools when network restriction is on - Applying the same restrictions to child processes, such as package installation and build scripts launched by a shell command From ba06d772ae8288207542f2e82008c26545e5b6ad Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 9 Jul 2026 13:52:10 +0200 Subject: [PATCH 8/8] docs: address sandbox review feedback --- .../getting-started/settings/sandboxing.md | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index fe0fd90308..c057d60959 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -56,7 +56,7 @@ The sandbox can reduce the impact of an unsafe tool call by: - Blocking direct outbound connections from sandboxed commands and policy-aware tools when network restriction is on - Applying the same restrictions to child processes, such as package installation and build scripts launched by a shell command -This can reduce the risk of auto-approving selected routine commands, such as builds and tests, by placing operating-system limits around many of their effects. It does **not** make **Allow Everything** safe. An allowed command can still modify or delete project files, alter other writable Kilo directories, consume data it can read, or write unsafe code that runs later outside the sandbox. +This can reduce the risk of auto-approving selected routine commands, such as builds and tests, by placing operating-system limits around many of their effects. It does **not** make **Allow Everything** safe. An allowed command can still modify or delete workspace files, alter other writable Kilo directories, consume data it can read, or write unsafe code that runs later outside the sandbox. The sandbox does not protect against every result of prompt injection. In particular, it does not prevent the agent from reading accessible files or including their contents in model context. It also cannot confine local MCP servers, plugin hooks, or any integration that runs outside the sandbox boundary. @@ -83,7 +83,7 @@ A practical setup for work on unfamiliar or partially trusted code is: - Keep `read`, `grep`, and unnecessary external-directory access set to `ask` or `deny` when they may expose sensitive content. - Allow only routine tools and command patterns that you want to run without interruption. -- Keep shell approval prompts for commands with important in-project effects or commands that can read sensitive data, because the sandbox still allows project writes and filesystem reads. +- Keep shell approval prompts for commands with important in-workspace effects or commands that can read sensitive data, because the sandbox still allows workspace writes and filesystem reads. - Enable the sandbox and keep network restriction on to reduce write and direct network-exfiltration impact if an approved action behaves unexpectedly. - Add extra writable paths only when a known workflow requires them. @@ -97,10 +97,18 @@ When the sandbox is active, agent tools can read files normally. The sandbox res Writes are allowed in: -- The active project or worktree -- Kilo's data, cache, config, state, temporary, binary, log, and repository directories +- The active workspace or worktree +- Kilo's runtime directories listed below - Paths listed in `sandbox.writable_paths` +| Writable Kilo path | Purpose | +|---|---| +| `$XDG_DATA_HOME/kilo` (normally `~/.local/share/kilo`) | Session data, logs, and Kilo's managed repository cache under `repos/` | +| `$XDG_CACHE_HOME/kilo` (normally `~/.cache/kilo`) | Cached data and downloaded binaries | +| `$XDG_CONFIG_HOME/kilo` (normally `~/.config/kilo`) | Configuration and installed plugins | +| `$XDG_STATE_HOME/kilo` (normally `~/.local/state/kilo`) | Runtime state | +| `$TMPDIR/kilo` | Temporary files; on macOS this is commonly under `/var/folders/.../T/kilo` | + Writes are denied everywhere else. The following rules still apply inside writable locations: - `.git` directories are always read-only to sandboxed tools. @@ -110,8 +118,10 @@ Writes are denied everywhere else. The following rules still apply inside writab Shell commands and their child processes inherit the same restrictions. Kilo's file tools perform mutations through a sandboxed worker. Writable file handles are unavailable, so a tool that requires an open read-write handle may fail even for an allowed path. +Because Kilo's config directory is writable, a shell command can change configuration, permissions, plugins, or additional writable paths that affect future tool calls. Direct filesystem access inside trusted integrations is confined only when the integration uses Kilo's sandbox-aware filesystem service. Starting or restarting a process with the background-process tool is unavailable while sandboxing is active. + {% callout type="info" %} -The sandbox is a write boundary, not a privacy boundary. It does not prevent an agent from reading files outside your project if your operating-system account can read them. +The sandbox is a write boundary, not a privacy boundary. It does not prevent an agent from reading files outside your workspace if your operating-system account can read them. {% /callout %} ## Network restrictions @@ -121,7 +131,7 @@ The sandbox is a write boundary, not a privacy boundary. It does not prevent an When network restriction is on, Kilo blocks: - Outbound network access from model-originated shell commands and their child processes -- Requests made through Kilo's policy-aware first-party HTTP clients +- Requests from built-in HTTP tools such as web fetch and web search - Remote MCP tool calls and custom or plugin tools that Kilo cannot prove will remain offline - Built-in tools such as codebase search, semantic search, and LSP that may use opaque or indirect network access @@ -148,15 +158,6 @@ Cloud sessions do not expose the local sandbox control because their tools do no | Platform | Backend | Notes | |---|---|---| -| macOS | `sandbox-exec` (Seatbelt) | Uses `/usr/bin/sandbox-exec`. File reads and inbound networking remain allowed. | -| Linux | Bubblewrap (`bwrap`) | Uses system `/usr/bin/bwrap` or a bundled, SHA-256-verified binary. `KILO_BWRAP_PATH` can select another binary. Kilo probes filesystem and network namespace support before enabling confinement. | +| macOS | `sandbox-exec` (Seatbelt) | Uses a Seatbelt profile through `/usr/bin/sandbox-exec`. | +| Linux | Bubblewrap (`bwrap`) | Uses system `/usr/bin/bwrap` or a bundled, SHA-256-verified binary. `KILO_BWRAP_PATH` can select another binary. Kilo probes filesystem and network namespace support before enabling confinement. Additional writable paths must already exist before Bubblewrap starts. | | Windows | None | Unsupported. The VS Code settings and prompt controls are hidden, and enabling the config has no effect. | - -## Limitations - -- The sandbox supplements Kilo's permission system; it does not replace permission prompts or rules. -- Local MCP servers and plugin hooks execute outside the operating-system sandbox. -- Direct filesystem access inside trusted in-process integrations is covered only when the integration uses Kilo's sandbox-aware filesystem service. -- Kilo's config directory is writable to sandboxed tools. A shell command can change configuration, permissions, plugins, or additional writable paths that affect future tool calls, so do not rely on the sandbox alone to protect policy integrity. -- Starting or restarting a background process with the background-process tool is unavailable while sandboxing is active. -- On Linux, an additional writable path must already exist before Bubblewrap starts.