From 7c65a3313dcfd7e23b1f79103acd770236dcd218 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 25 Jun 2026 19:05:32 +0200 Subject: [PATCH] fix: preserve sandbox state across session scopes --- .kilo/plans/sandbox-toggle-session-state.md | 7 ++-- packages/kilo-vscode/src/KiloProvider.ts | 18 +++++---- .../src/agent-manager/AgentManagerProvider.ts | 8 +++- .../__tests__/AgentManagerProvider.spec.ts | 20 ++++++++++ .../kilo-vscode/src/agent-manager/types.ts | 16 ++++++++ .../cli-backend/connection-service.test.ts | 18 +++++++++ .../cli-backend/connection-service.ts | 2 +- .../unit/kilo-provider-load-messages.test.ts | 29 ++++++++++++-- .../prompt-input-connection-guard.test.ts | 8 ++-- .../src/components/chat/PromptInput.tsx | 14 ++++--- .../src/types/messages/webview-messages.ts | 4 ++ .../opencode/src/kilocode/sandbox/state.ts | 6 +++ packages/opencode/src/tool/task.ts | 2 + .../test/kilocode/sandbox/state.test.ts | 15 +++++++ .../test/kilocode/task-nesting.test.ts | 40 +++++++++++++++++++ 15 files changed, 181 insertions(+), 26 deletions(-) diff --git a/.kilo/plans/sandbox-toggle-session-state.md b/.kilo/plans/sandbox-toggle-session-state.md index 75d0e492188..91fef5e5c31 100644 --- a/.kilo/plans/sandbox-toggle-session-state.md +++ b/.kilo/plans/sandbox-toggle-session-state.md @@ -17,7 +17,7 @@ Auto Approve scope is intentionally excluded and tracked separately in https://g Use two durable values with different responsibilities: 1. **Session state**: Store the desired sandbox state in the existing session metadata under a Kilo-owned key such as `kilocode.sandbox`. -2. **New-session default**: Store the most recently selected state in a shared VS Code `globalState` preference. Fall back to `experimental.sandbox` until the user explicitly selects a state. +2. **New-session default**: Store the most recently selected state in the VS Code workspace state. Fall back to `experimental.sandbox` until the user explicitly selects a state. The effective backend state is: @@ -53,11 +53,12 @@ Fork B -> fork enabled | Session switch | Fetch the selected session's state and discard stale responses from the previously selected session. | | Existing session toggle | Persist the selected state in that session, then update the sticky default. Do not alter any other session. | | Session fork | Copy the source session metadata. Ignore the sticky default. Parent and child become independent after the fork. | +| Task child | Copy the parent session's explicit sandbox state, then keep the child independent. | | Continue in Worktree | Preserve the source session state through the existing fork flow. | | Move or promote a session | Preserve state because the same session is being moved or associated, not created. | | Session deletion | Delete state with the session row and retain serialization against an in-flight toggle. | | Backend restart | Reload existing session state from metadata rather than reverting to config. | -| VS Code reload | Reload the sticky default from `globalState` and existing state from backend metadata. | +| VS Code reload | Reload the sticky default from workspace state and existing state from backend metadata. | | Sidebar and editor tabs | Use the same shared default service; session state remains backend-owned. | | Cloud preview | Keep the control hidden for synthetic `cloud:` sessions. | | Cloud continuation/import | Preserve imported session metadata. Do not overwrite it with the local new-session default. | @@ -105,7 +106,7 @@ Regenerate OpenAPI and `packages/sdk/js/` after adding the endpoint. ### 3. Add a Shared Sticky Default Service -Add a small extension service, for example `packages/kilo-vscode/src/services/sandbox-preference.ts`, backed by `ExtensionContext.globalState`. +Add a small extension service, for example `packages/kilo-vscode/src/services/sandbox-preference.ts`, backed by `ExtensionContext.workspaceState`. The service should: diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2564ade9fba..fd352214937 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1105,10 +1105,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.fetchAndSendSandboxStatus(message.sessionID) break case "requestSandboxDefault": - await this.fetchAndSendSandboxDefault() + await this.fetchAndSendSandboxDefault(message.contextDirectory) break case "setSandboxDefault": - await this.handleSetSandboxDefault(message.enabled, message.requestID) + await this.handleSetSandboxDefault(message.enabled, message.requestID, message.contextDirectory) break case "toggleSandbox": await this.handleToggleSandbox(message) @@ -2494,14 +2494,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage(getWorkStylePayload()) } - private async fetchAndSendSandboxDefault(requestID?: string): Promise { + private async fetchAndSendSandboxDefault(directory = this.getContextDirectory(), requestID?: string): Promise { const revision = ++this.sandboxRevision const generation = this.connectionGeneration const client = this.client const sandbox = sandboxClient(client) if (!client || !sandbox || this.connectionState !== "connected") return try { - const directory = this.getContextDirectory() const [desired, result] = await Promise.all([ sandboxDefault(this.connectionService.sandboxPreference, client, directory), sandbox.support({ directory }, { throwOnError: true }), @@ -2532,20 +2531,23 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } - private async handleSetSandboxDefault(enabled: boolean, requestID: string): Promise { + private async handleSetSandboxDefault( + enabled: boolean, + requestID: string, + directory = this.getContextDirectory(), + ): Promise { const client = this.client const sandbox = sandboxClient(client) if (!client || !sandbox || this.connectionState !== "connected") { - await this.fetchAndSendSandboxDefault(requestID) + await this.fetchAndSendSandboxDefault(directory, requestID) return } - const directory = this.getContextDirectory() try { await this.connectionService.sandboxPreference.set(enabled, async () => { const { data } = await sandbox.support({ directory }, { throwOnError: true }) if (!data.available) throw new Error(data.reason ?? "Sandbox backend is unavailable") }) - await this.fetchAndSendSandboxDefault(requestID) + await this.fetchAndSendSandboxDefault(directory, requestID) vscode.window.showInformationMessage( enabled ? "Sandbox enabled for new sessions" : "Sandbox disabled for new sessions", ) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index b5553776e38..d2c07c39db8 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -440,11 +440,15 @@ export class AgentManagerProvider implements Disposable { return null } - if ((m.type === "sendMessage" || m.type === "sendCommand" || m.type === "toggleSandbox") && !m.sessionID) { + if ( + m.type === "requestSandboxDefault" || + m.type === "setSandboxDefault" || + ((m.type === "sendMessage" || m.type === "sendCommand" || m.type === "toggleSandbox") && !m.sessionID) + ) { const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined if (worktree) { - if (m.draftID) this.activeSessionId = m.draftID + if ("draftID" in m && m.draftID) this.activeSessionId = m.draftID return { ...msg, contextDirectory: worktree.path } } } diff --git a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts index 788db585a0e..1e3b91664a6 100644 --- a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -229,6 +229,26 @@ describe("AgentManagerProvider worktree creation", () => { }) }) + it.each([{ type: "requestSandboxDefault" }, { type: "setSandboxDefault", enabled: false, requestID: "request-1" }])( + "routes $type to the selected worktree directory", + async (message) => { + const manager = createHarness() + const state = { + getWorktree: vi.fn().mockReturnValue({ id: "wt-1", path: "/repo/.kilo/worktrees/wt-1" }), + } + manager.getStateManager.mockReturnValue(state) + manager.contextTarget.mockResolvedValue(undefined) + + const result = await manager.onMessage({ ...message, agentManagerContext: "wt-1" }) + + expect(result).toEqual({ + ...message, + agentManagerContext: "wt-1", + contextDirectory: "/repo/.kilo/worktrees/wt-1", + }) + }, + ) + it("resolves new sandbox toggles to the selected worktree directory", async () => { const manager = createHarness() const state = { diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index e48b9fa7a4f..7a5d686dab5 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -639,6 +639,20 @@ interface SendCommandIn { contextDirectory?: string } +interface RequestSandboxDefaultIn { + type: "requestSandboxDefault" + agentManagerContext?: string + contextDirectory?: string +} + +interface SetSandboxDefaultIn { + type: "setSandboxDefault" + enabled: boolean + requestID: string + agentManagerContext?: string + contextDirectory?: string +} + interface ToggleSandboxIn { type: "toggleSandbox" sessionID?: string @@ -792,6 +806,8 @@ export type AgentManagerInMessage = | LoadMessagesIn | SendMessageIn | SendCommandIn + | RequestSandboxDefaultIn + | SetSandboxDefaultIn | ToggleSandboxIn | RequestTerminalContextIn | ClearSessionIn diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts index 41f92c86e61..11b3f490cbf 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts @@ -1,6 +1,24 @@ import { describe, expect, test } from "bun:test" import { KiloConnectionService } from "./connection-service" +function state(value: boolean) { + return { + get: () => value as T, + update: async () => undefined, + } +} + +describe("KiloConnectionService sandbox preference", () => { + test("uses workspace state instead of extension-global state", () => { + const service = new KiloConnectionService({ + workspaceState: state(false), + globalState: state(true), + } as any) + + expect(service.sandboxPreference.resolve(true)).toBe(false) + }) +}) + describe("KiloConnectionService viewed sessions", () => { test("keeps Agent Manager sessions when sidebar focus changes during a flush", async () => { const service = new KiloConnectionService({} as any) diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts index 1ccf76026fb..d2c4f9bd65c 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -93,7 +93,7 @@ export class KiloConnectionService { constructor(context: vscode.ExtensionContext) { const state = - context.globalState ?? + context.workspaceState ?? ({ get: (_key: string, fallback?: T) => fallback, update: async () => undefined, diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts index c31d5c96852..43d76663e00 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts @@ -74,6 +74,8 @@ function createClient(options?: { const reverted: Array> = [] const created: Array> = [] const sandboxed: Array> = [] + const sandboxSupport: Array> = [] + const configReads: Array> = [] return { calls, stopped, @@ -82,6 +84,8 @@ function createClient(options?: { reverted, created, sandboxed, + sandboxSupport, + configReads, session: { list: async () => ({ data: [] }), create: async (params: Record) => { @@ -118,7 +122,10 @@ function createClient(options?: { }, }, sandbox: { - support: async () => options?.supportDeferred?.promise ?? { data: { available: true } }, + support: async (params: Record) => { + sandboxSupport.push(params) + return options?.supportDeferred?.promise ?? { data: { available: true } } + }, toggle: async (params: Record) => { sandboxed.push(params) options?.sandboxStarted?.resolve(undefined) @@ -137,7 +144,12 @@ function createClient(options?: { }, provider: { list: async () => ({ data: { all: [], connected: {}, default: {} } }) }, app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: {} }) }, + config: { + get: async (params: Record) => { + configReads.push(params) + return { data: {} } + }, + }, kilo: { notifications: async () => ({ data: [] }), profile: async () => ({ data: {} }), @@ -207,7 +219,8 @@ type ProviderInternals = { handleAbort: (sid?: string) => Promise handleRevertSession: (sid: string, messageID: string) => Promise handleSendMessage: (text: string, messageID?: string, sessionID?: string, draftID?: string) => Promise - handleSetSandboxDefault: (enabled: boolean, requestID: string) => Promise + fetchAndSendSandboxDefault: (directory?: string, requestID?: string) => Promise + handleSetSandboxDefault: (enabled: boolean, requestID: string, directory?: string) => Promise handleToggleSandbox: (input: { sessionID: string; requestID: string }) => Promise handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise handleDeleteSession: (sid: string) => Promise @@ -334,6 +347,16 @@ describe("KiloProvider sandbox toggle", () => { notice.mockRestore() }) + it("resolves a blank worktree default against the routed directory", async () => { + const client = createClient() + const { internal } = makeProvider(client) + + await internal.fetchAndSendSandboxDefault("/repo/.kilo/worktrees/wt-1") + + expect(client.configReads).toEqual([{ directory: "/repo/.kilo/worktrees/wt-1" }]) + expect(client.sandboxSupport).toEqual([{ directory: "/repo/.kilo/worktrees/wt-1" }]) + }) + it("waits for a blank toggle before creating the first prompt session", async () => { const support = defer<{ data: { available: boolean } }>() const client = createClient({ supportDeferred: support }) 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 6ecdb8ea74e..1e1689cd822 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 @@ -35,6 +35,7 @@ describe("PromptInput sandbox toggle", () => { expect(toggle).toContain('type: "toggleSandbox"') expect(toggle).toContain('type: "setSandboxDefault"') expect(toggle).toContain("enabled: !sandboxDefault()!.desired") + expect(toggle).toContain("agentManagerContext: ctx()") expect(toggle).toContain("sessionID,") expect(toggle).toContain("requestID,") expect(toggle).not.toContain("draftID:") @@ -57,10 +58,11 @@ describe("PromptInput sandbox toggle", () => { expect(move).toBeGreaterThan(save) }) - it("requires the enabled experiment for visibility and uses effective runtime state for the button", () => { + it("keeps persisted sandbox state visible independently of the configured default", () => { expect(src).toContain( - 'return features().sandboxControls && config().experimental?.sandbox === true && !id?.startsWith("cloud:")', + 'const sandboxVisible = () => features().sandboxControls && !session.currentSessionID()?.startsWith("cloud:")', ) + expect(src).not.toContain("config().experimental?.sandbox === true") expect(src).toContain("") expect(src).toContain("{ action: toggleSandbox, enabled: () => sandboxVisible() && !sandboxDisabled() }") expect(src).toContain('if (!sandboxVisible()) hidden.add("sandbox")') @@ -72,7 +74,7 @@ describe("PromptInput sandbox toggle", () => { expect(src).toContain("const target = untrack(sandboxTarget)") expect(src).toContain("if (target !== undefined && target !== sessionID) clearSandboxRequest()") expect(src).toContain("sandboxID() ? sandbox()?.enabled : sandboxDefault()?.enabled") - expect(src).toContain('type: "requestSandboxDefault"') + expect(src).toContain('type: "requestSandboxDefault", agentManagerContext: ctx()') expect(src).toContain("aria-pressed={sandboxEnabled()}") expect(src).toContain("!sandboxReady()") expect(src).toContain("if (sandboxRequest() && target === null) return") 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 f14a858eb66..c68e070acd9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -202,10 +202,7 @@ export const PromptInput: Component = (props) => { const id = session.currentSessionID() return id?.startsWith("cloud:") ? undefined : id } - const sandboxVisible = () => { - const id = session.currentSessionID() - return features().sandboxControls && config().experimental?.sandbox === true && !id?.startsWith("cloud:") - } + const sandboxVisible = () => features().sandboxControls && !session.currentSessionID()?.startsWith("cloud:") const sandbox = () => { const state = sandboxState() return state?.sessionID === sandboxID() ? state : undefined @@ -224,7 +221,7 @@ export const PromptInput: Component = (props) => { vscode.postMessage({ type: "requestSandboxStatus", sessionID }) return } - vscode.postMessage({ type: "requestSandboxDefault" }) + vscode.postMessage({ type: "requestSandboxDefault", agentManagerContext: ctx() }) } const toggleSandbox = () => { const sessionID = sandboxID() @@ -234,7 +231,12 @@ export const PromptInput: Component = (props) => { setSandboxRequest(requestID) setSandboxTarget(sessionID ?? null) if (!sessionID) { - vscode.postMessage({ type: "setSandboxDefault", enabled: !sandboxDefault()!.desired, requestID }) + vscode.postMessage({ + type: "setSandboxDefault", + enabled: !sandboxDefault()!.desired, + requestID, + agentManagerContext: ctx(), + }) return } vscode.postMessage({ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 208548a9128..363286bd2d2 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -931,12 +931,16 @@ export interface RequestSandboxStatusMessage { export interface RequestSandboxDefaultMessage { type: "requestSandboxDefault" + agentManagerContext?: string + contextDirectory?: string } export interface SetSandboxDefaultMessage { type: "setSandboxDefault" enabled: boolean requestID: string + agentManagerContext?: string + contextDirectory?: string } export interface ToggleSandboxMessage { diff --git a/packages/opencode/src/kilocode/sandbox/state.ts b/packages/opencode/src/kilocode/sandbox/state.ts index 79a98b0744d..f76cf743d06 100644 --- a/packages/opencode/src/kilocode/sandbox/state.ts +++ b/packages/opencode/src/kilocode/sandbox/state.ts @@ -24,6 +24,12 @@ export function merge(metadata: Record | null | undefined, valu return { ...metadata, [key]: value } } +export function inherit(metadata: Record | null | undefined) { + const value = parse(metadata) + if (!value) return + return merge(undefined, { enabled: value.enabled, version: 0 }) +} + export function remove(metadata: Record | null | undefined) { if (!metadata || !(key in metadata)) return metadata const next = { ...metadata } diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index eb7c748f0f3..e9c4d9b0c84 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -14,6 +14,7 @@ import { KiloTask } from "../kilocode/tool/task" // kilocode_change import { KiloCostPropagation } from "../kilocode/session/cost-propagation" // kilocode_change import { KiloSessionProcessor } from "../kilocode/session/processor" // kilocode_change import { KiloSession } from "../kilocode/session" // kilocode_change +import * as SandboxState from "../kilocode/sandbox/state" // kilocode_change import { errorMessage } from "@/util/error" // kilocode_change import { Cause, Effect, Exit, Schema, Scope } from "effect" import { EffectBridge } from "@/effect/bridge" @@ -181,6 +182,7 @@ export const TaskTool = Tool.define( parentID: ctx.sessionID, title: params.description + ` (@${next.name} subagent)`, platform, // kilocode_change + metadata: SandboxState.inherit(parent.metadata), // kilocode_change - preserve explicit parent sandbox state // kilocode_change start - dedupe inherited restrictions before child prompt toggles persist permission: KiloTask.merge( deriveSubagentSessionPermission({ diff --git a/packages/opencode/test/kilocode/sandbox/state.test.ts b/packages/opencode/test/kilocode/sandbox/state.test.ts index 05e08212191..a0fb980bfb7 100644 --- a/packages/opencode/test/kilocode/sandbox/state.test.ts +++ b/packages/opencode/test/kilocode/sandbox/state.test.ts @@ -112,6 +112,21 @@ it.instance( { config: { experimental: { sandbox: true } } }, ) +it.instance("persists metadata without requiring sandbox backend support", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const id = yield* create({ source: "test" }) + + yield* SandboxState.write(id, { enabled: false, version: 7 }) + + expect(yield* SandboxState.read(id)).toEqual({ enabled: false, version: 7 }) + expect((yield* sessions.get(id)).metadata).toEqual({ + source: "test", + [SandboxState.key]: { enabled: false, version: 7 }, + }) + }), +) + it.instance("preserves unrelated metadata through the production persistence callback", () => Effect.gen(function* () { const sessions = yield* Session.Service diff --git a/packages/opencode/test/kilocode/task-nesting.test.ts b/packages/opencode/test/kilocode/task-nesting.test.ts index 89377bfa307..9eaacefa448 100644 --- a/packages/opencode/test/kilocode/task-nesting.test.ts +++ b/packages/opencode/test/kilocode/task-nesting.test.ts @@ -17,6 +17,7 @@ import { Provider } from "../../src/provider/provider" import { Permission } from "../../src/permission" import { TaskTool, type TaskPromptOps } from "../../src/tool/task" import { KiloSessionPrompt } from "../../src/kilocode/session/prompt" +import * as SandboxState from "../../src/kilocode/sandbox/state" import { Truncate } from "../../src/tool/truncate" import { ToolRegistry } from "../../src/tool/registry" import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture" @@ -155,6 +156,45 @@ describe("Kilo task nesting", () => { ), ) + it.live("inherits the parent's explicit sandbox state", () => + provideTmpdirInstance( + () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + yield* sessions.setMetadata({ + sessionID: chat.id, + metadata: SandboxState.merge(chat.metadata, { enabled: false, version: 3 }), + }) + const tool = yield* TaskTool + const def = yield* tool.init() + + const result = yield* def.execute( + { + description: "inspect sandbox", + prompt: "check the child sandbox state", + subagent_type: "explore", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps: stubOps() }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + const child = yield* sessions.get(result.metadata.sessionId) + expect(SandboxState.parse(child.metadata)).toEqual({ enabled: false, version: 0 }) + expect((yield* (yield* Config.Service).get()).experimental?.sandbox).toBe(true) + }), + { config: { experimental: { sandbox: true } } }, + ), + ) + it.live("disables nested task and question tools even when global permissions allow them", () => provideTmpdirInstance( () =>