mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
test(sandbox): cover production network classification
This commit is contained in:
@@ -65,7 +65,7 @@ test.describe("settings tab accessibility", () => {
|
||||
await expect(tab).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page.getByRole("tabpanel", { name: "Sandboxing" })).toBeVisible()
|
||||
const network = page.getByRole("switch", { name: "Restrict Network Access" })
|
||||
await expect(page.getByText(/Local MCP servers and plugin hooks run outside this restriction/)).toBeVisible()
|
||||
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 expect(network).not.toBeChecked()
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useConfig } from "../../context/config"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import SettingsRow from "./SettingsRow"
|
||||
|
||||
const description = "sandbox-network-description"
|
||||
|
||||
const SandboxingTab: Component = () => {
|
||||
const { config, updateConfig } = useConfig()
|
||||
const language = useLanguage()
|
||||
@@ -15,10 +17,12 @@ const SandboxingTab: Component = () => {
|
||||
<SettingsRow
|
||||
title={language.t("settings.sandboxing.network.title")}
|
||||
description={language.t("settings.sandboxing.network.description")}
|
||||
descriptionId={description}
|
||||
last
|
||||
>
|
||||
<Switch
|
||||
checked={experimental().sandbox_restrict_network !== false}
|
||||
inputProps={{ "aria-describedby": description }}
|
||||
onChange={(checked) =>
|
||||
updateConfig({
|
||||
experimental: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Component, JSX, Show } from "solid-js"
|
||||
const SettingsRow: Component<{
|
||||
title: string
|
||||
description?: string
|
||||
descriptionId?: string
|
||||
tag?: () => string | undefined
|
||||
last?: boolean
|
||||
children: JSX.Element
|
||||
@@ -32,7 +33,9 @@ const SettingsRow: Component<{
|
||||
<Show when={props.tag?.()}>{(tag) => <Tag>{tag()}</Tag>}</Show>
|
||||
</div>
|
||||
{props.description !== null && props.description !== undefined && (
|
||||
<div data-slot="settings-row-label-subtitle">{props.description}</div>
|
||||
<div id={props.descriptionId} data-slot="settings-row-label-subtitle">
|
||||
{props.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div data-slot="settings-row-input">{props.children}</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { expect, mock, beforeEach } from "bun:test"
|
||||
import { Cause, Effect, Exit } from "effect"
|
||||
import type { MCP as MCPNS } from "../../src/mcp/index"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import * as SandboxNetwork from "../../src/kilocode/sandbox/network" // kilocode_change
|
||||
import { run as runSandbox, type Profile } from "@kilocode/sandbox" // kilocode_change
|
||||
|
||||
// --- Mock infrastructure ---
|
||||
|
||||
@@ -183,11 +185,77 @@ const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
|
||||
|
||||
const it = testEffect(MCP.defaultLayer)
|
||||
|
||||
// kilocode_change start
|
||||
function sandboxProfile(): Profile {
|
||||
return {
|
||||
filesystem: { allowWrite: [], denyWrite: [], denyNames: [] },
|
||||
network: { mode: "deny", allowedHosts: [] },
|
||||
environment: { deny: [], set: {} },
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server: string) {
|
||||
if ("status" in status) return status.status
|
||||
return status[server]?.status
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
it.instance(
|
||||
"classifies production remote MCP tools while leaving local MCP tools available",
|
||||
() =>
|
||||
MCP.Service.use((mcp: MCPNS.Interface) =>
|
||||
Effect.gen(function* () {
|
||||
lastCreatedClientName = "local-server"
|
||||
getOrCreateClientState("local-server")
|
||||
yield* mcp.add("local-server", { type: "local", command: ["node", "fake.js"] })
|
||||
|
||||
lastCreatedClientName = "remote-server"
|
||||
getOrCreateClientState("remote-server")
|
||||
yield* mcp.add("remote-server", {
|
||||
type: "remote",
|
||||
url: "http://localhost:9999/mcp",
|
||||
oauth: false,
|
||||
})
|
||||
|
||||
const tools = yield* mcp.tools()
|
||||
const local = Object.entries(tools).find(([key]) => key.startsWith("local") && key.endsWith("_test_tool"))?.[1]
|
||||
const remote = Object.entries(tools).find(
|
||||
([key]) => key.startsWith("remote") && key.endsWith("_test_tool"),
|
||||
)?.[1]
|
||||
if (!local || !remote) return yield* Effect.die(new Error("expected MCP tools are missing"))
|
||||
|
||||
let localCalled = false
|
||||
let remoteCalled = false
|
||||
const localExit = yield* runSandbox(
|
||||
sandboxProfile(),
|
||||
SandboxNetwork.mcp(
|
||||
local,
|
||||
Effect.sync(() => {
|
||||
localCalled = true
|
||||
}),
|
||||
),
|
||||
).pipe(Effect.exit)
|
||||
const remoteExit = yield* runSandbox(
|
||||
sandboxProfile(),
|
||||
SandboxNetwork.mcp(
|
||||
remote,
|
||||
Effect.sync(() => {
|
||||
remoteCalled = true
|
||||
}),
|
||||
),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isSuccess(localExit)).toBe(true)
|
||||
expect(localCalled).toBe(true)
|
||||
expect(Exit.isFailure(remoteExit)).toBe(true)
|
||||
expect(remoteCalled).toBe(false)
|
||||
}),
|
||||
),
|
||||
{ config: { mcp: {} } },
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// ========================================================================
|
||||
// Test: tools() are cached after connect
|
||||
// ========================================================================
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { Effect, Layer, Result, Schema } from "effect"
|
||||
import { Effect, Exit, Layer, Result, Schema } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { Tool } from "@/tool/tool"
|
||||
@@ -35,6 +35,8 @@ import { ToolJsonSchema } from "@/tool/json-schema"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Command } from "@/command" // kilocode_change
|
||||
import * as SandboxNetwork from "@/kilocode/sandbox/network" // kilocode_change
|
||||
import { run as runSandbox, type Profile } from "@kilocode/sandbox" // kilocode_change
|
||||
|
||||
const node = CrossSpawnSpawner.defaultLayer
|
||||
const configLayer = TestConfig.layer({
|
||||
@@ -106,12 +108,56 @@ const scout = testEffect(
|
||||
const withBrokenPlugin = testEffect(
|
||||
Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer),
|
||||
)
|
||||
const sandboxed = testEffect(
|
||||
Layer.mergeAll(registryLayer({ flags: { experimentalLspTool: true } }), node, Agent.defaultLayer),
|
||||
) // kilocode_change
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
// kilocode_change start
|
||||
function sandboxProfile(): Profile {
|
||||
return {
|
||||
filesystem: { allowWrite: [], denyWrite: [], denyNames: [] },
|
||||
network: { mode: "deny", allowedHosts: [] },
|
||||
environment: { deny: [], set: {} },
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
describe("tool.registry", () => {
|
||||
// kilocode_change start
|
||||
sandboxed.instance("preserves built-in network classification through production tool definition processing", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const agent = yield* Agent.Service
|
||||
const build = yield* agent.get("build")
|
||||
if (!build) return yield* Effect.die(new Error("build agent not found"))
|
||||
const tools = yield* registry.tools({
|
||||
providerID: ProviderID.opencode,
|
||||
modelID: ModelID.make("test"),
|
||||
agent: build,
|
||||
})
|
||||
const all = yield* registry.all()
|
||||
const read = tools.find((tool) => tool.id === "read")
|
||||
const search = all.find((tool) => tool.id === "lsp")
|
||||
if (!read || !search) return yield* Effect.die(new Error("expected built-in tools are missing"))
|
||||
|
||||
const allowed = yield* runSandbox(sandboxProfile(), SandboxNetwork.tool(read, Effect.succeed("allowed"))).pipe(
|
||||
Effect.exit,
|
||||
)
|
||||
const denied = yield* runSandbox(
|
||||
sandboxProfile(),
|
||||
SandboxNetwork.tool(search, Effect.succeed("unexpected")),
|
||||
).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isSuccess(allowed)).toBe(true)
|
||||
expect(Exit.isFailure(denied)).toBe(true)
|
||||
}),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
it.instance("hides repo research tools unless experimental", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
@@ -5,13 +5,14 @@ import type { ComponentProps, ParentProps } from "solid-js"
|
||||
export interface SwitchProps extends ParentProps<ComponentProps<typeof Kobalte>> {
|
||||
hideLabel?: boolean
|
||||
description?: string
|
||||
inputProps?: ComponentProps<typeof Kobalte.Input>
|
||||
}
|
||||
|
||||
export function Switch(props: SwitchProps) {
|
||||
const [local, others] = splitProps(props, ["children", "class", "hideLabel", "description"])
|
||||
const [local, others] = splitProps(props, ["children", "class", "hideLabel", "description", "inputProps"])
|
||||
return (
|
||||
<Kobalte {...others} class={local.class} data-component="switch">
|
||||
<Kobalte.Input data-slot="switch-input" />
|
||||
<Kobalte.Input {...local.inputProps} data-slot="switch-input" />
|
||||
<Show when={local.children}>
|
||||
<Kobalte.Label data-slot="switch-label" classList={{ "sr-only": local.hideLabel }}>
|
||||
{local.children}
|
||||
|
||||
Reference in New Issue
Block a user