mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #11783 from Kilo-Org/bittersweet-height
fix(agent-manager): inherit sandbox for tool-started sessions
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/sdk": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Inherit sandbox state when a sandboxed agent starts new Agent Manager sessions.
|
||||
@@ -802,20 +802,20 @@
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"tree-sitter-powershell",
|
||||
"protobufjs",
|
||||
"web-tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
||||
"@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.74",
|
||||
@@ -4754,6 +4754,8 @@
|
||||
|
||||
"@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"@anthropic-ai/sandbox-runtime/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="],
|
||||
|
||||
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="],
|
||||
@@ -4764,8 +4766,6 @@
|
||||
|
||||
"@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="],
|
||||
|
||||
"@anthropic-ai/sandbox-runtime/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="],
|
||||
|
||||
@@ -9,13 +9,12 @@ import { isAbsolutePath } from "../path-utils"
|
||||
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
|
||||
import { remoteRef, WorktreeStateManager, type Worktree } from "./WorktreeStateManager"
|
||||
import { handleSection } from "./section-handler"
|
||||
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
|
||||
import { normalizeBaseBranch } from "./base-branch"
|
||||
import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller"
|
||||
import { PRStatusBridge } from "./pr-status-bridge"
|
||||
import { GitOps } from "./GitOps"
|
||||
import { versionedName } from "./branch-name"
|
||||
import { BranchNamingController } from "./branch-naming"
|
||||
import { classifyWorktreeError } from "./git-import"
|
||||
import { SetupScriptService } from "./SetupScriptService"
|
||||
import { SetupScriptRunner } from "./SetupScriptRunner"
|
||||
import { copyEnvFiles } from "./env-copy"
|
||||
@@ -31,6 +30,11 @@ import { AgentManagerVisiblePresence } from "./am-visible-presence"
|
||||
import { continueInWorktree } from "./continue-in-worktree"
|
||||
import { WorktreeDiffController } from "./worktree-diff-controller"
|
||||
import { WorktreeImporter } from "./worktree-importer"
|
||||
import {
|
||||
createWorktreeOnDisk,
|
||||
type CreateWorktreeOnDiskOptions,
|
||||
type CreateWorktreeOnDiskResult,
|
||||
} from "./worktree-create"
|
||||
import { recordPromotionHandoff } from "./promotion-handoff"
|
||||
import { restoreWorktrees } from "./state-recovery"
|
||||
import { createLocalDiff, diffSummary as localDiffSummary } from "./local-diff"
|
||||
@@ -737,107 +741,19 @@ export class AgentManagerProvider implements Disposable {
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Resolve the effective base branch using the configured default, explicit override, and existence check. */
|
||||
private async resolveBaseBranch(
|
||||
manager: WorktreeManager,
|
||||
state: WorktreeStateManager,
|
||||
explicit?: string,
|
||||
): Promise<string | undefined> {
|
||||
const configured = state.getDefaultBaseBranch()
|
||||
if (!configured && !explicit) return undefined
|
||||
|
||||
const configuredExists = configured ? await manager.branchExists(configured) : false
|
||||
const result = chooseBaseBranch({ explicit, configured, configuredExists })
|
||||
|
||||
if (result.stale) {
|
||||
this.clearStaleDefaultBaseBranch(state, result.stale)
|
||||
}
|
||||
return result.branch
|
||||
}
|
||||
|
||||
/** Reset a stale default base branch and notify the webview. */
|
||||
private clearStaleDefaultBaseBranch(state: WorktreeStateManager, stale: string): void {
|
||||
this.log(`Default base branch "${stale}" no longer exists, clearing`)
|
||||
state.setDefaultBaseBranch(undefined)
|
||||
this.pushState()
|
||||
}
|
||||
|
||||
/** Create a git worktree on disk and register it in state. Returns null on failure. */
|
||||
private async createWorktreeOnDisk(opts?: {
|
||||
groupId?: string
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
existingBranch?: string
|
||||
name?: string
|
||||
label?: string
|
||||
}): Promise<{
|
||||
worktree: ReturnType<WorktreeStateManager["addWorktree"]>
|
||||
result: CreateWorktreeResult
|
||||
} | null> {
|
||||
const manager = this.getWorktreeManager()
|
||||
const state = this.getStateManager()
|
||||
if (!manager || !state) {
|
||||
this.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "error",
|
||||
message: "Open a folder that contains a git repository to use worktrees",
|
||||
errorCode: "not_git_repo",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
this.postToWebview({ type: "agentManager.worktreeSetup", status: "creating", message: "Creating git worktree..." })
|
||||
|
||||
// Resolve effective base branch using configured default
|
||||
const effectiveBase = opts?.existingBranch
|
||||
? undefined
|
||||
: await this.resolveBaseBranch(manager, state, opts?.baseBranch)
|
||||
|
||||
let result: CreateWorktreeResult
|
||||
try {
|
||||
result = await manager.createWorktree({
|
||||
prompt: opts?.name || "kilo",
|
||||
baseBranch: effectiveBase ?? opts?.baseBranch,
|
||||
branchName: opts?.branchName,
|
||||
existingBranch: opts?.existingBranch,
|
||||
})
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
this.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "error",
|
||||
message: msg,
|
||||
errorCode: classifyWorktreeError(msg),
|
||||
})
|
||||
this.host.capture("Agent Manager Session Error", {
|
||||
source: PLATFORM,
|
||||
error: msg,
|
||||
context: "createWorktree",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const worktree = state.addWorktree({
|
||||
branch: result.branch,
|
||||
path: result.path,
|
||||
parentBranch: result.parentBranch,
|
||||
remote: result.remote,
|
||||
groupId: opts?.groupId,
|
||||
label: opts?.label,
|
||||
branchOwned: !opts?.existingBranch,
|
||||
})
|
||||
|
||||
// Push state immediately so the sidebar shows the new worktree with a loading indicator
|
||||
this.pushState()
|
||||
this.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "creating",
|
||||
message: "Setting up worktree...",
|
||||
branch: result.branch,
|
||||
worktreeId: worktree.id,
|
||||
})
|
||||
|
||||
return { worktree, result }
|
||||
private async createWorktreeOnDisk(opts?: CreateWorktreeOnDiskOptions): Promise<CreateWorktreeOnDiskResult | null> {
|
||||
return createWorktreeOnDisk(
|
||||
{
|
||||
getWorktreeManager: () => this.getWorktreeManager(),
|
||||
getStateManager: () => this.getStateManager(),
|
||||
postToWebview: (message) => this.postToWebview(message),
|
||||
capture: (event, properties) => this.host.capture(event, properties),
|
||||
pushState: () => this.pushState(),
|
||||
log: (...args) => this.log(...args),
|
||||
},
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
/** Create a CLI session in a worktree directory. Returns null on failure. */
|
||||
@@ -845,6 +761,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
worktreePath: string,
|
||||
branch: string,
|
||||
worktreeId?: string,
|
||||
source?: { sandboxInheritanceToken?: string },
|
||||
): Promise<Session | null> {
|
||||
let client: KiloClient
|
||||
try {
|
||||
@@ -878,7 +795,16 @@ export class AgentManagerProvider implements Disposable {
|
||||
const { data: session } = await startSession(
|
||||
client,
|
||||
worktreePath,
|
||||
() => client.session.create({ directory: worktreePath, platform: PLATFORM, metadata }, { throwOnError: true }),
|
||||
() =>
|
||||
client.session.create(
|
||||
{
|
||||
directory: worktreePath,
|
||||
platform: PLATFORM,
|
||||
metadata,
|
||||
...(source?.sandboxInheritanceToken ? { sandboxInheritanceToken: source.sandboxInheritanceToken } : {}),
|
||||
},
|
||||
{ throwOnError: true },
|
||||
),
|
||||
(...args) => this.log(...args),
|
||||
)
|
||||
return session
|
||||
@@ -985,7 +911,9 @@ export class AgentManagerProvider implements Disposable {
|
||||
const properties = (event as { properties?: unknown }).properties
|
||||
const req = parseToolRequest(properties)
|
||||
if (!req) return
|
||||
if (directory) req.directory = directory
|
||||
if (directory) {
|
||||
req.directory = directory
|
||||
}
|
||||
void this.startToolRequest(req)
|
||||
}
|
||||
|
||||
@@ -1012,7 +940,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
this.pushState()
|
||||
},
|
||||
setup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id),
|
||||
createSessionInWorktree: (dir, branch, id) => this.createSessionInWorktree(dir, branch, id),
|
||||
createSessionInWorktree: (dir, branch, id, source) => this.createSessionInWorktree(dir, branch, id, source),
|
||||
sessionMetadata: (client, dir) => sandboxSessionMetadata(this.connectionService.sandboxPreference, client, dir),
|
||||
registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
|
||||
notifyReady: (sid, result, wid) => this.notifyWorktreeReady(sid, result, wid),
|
||||
|
||||
@@ -21,11 +21,16 @@ export interface ToolRequest {
|
||||
requestID: string
|
||||
sessionID?: string
|
||||
directory?: string
|
||||
sandboxInheritanceToken?: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: ToolTask[]
|
||||
}
|
||||
|
||||
export interface ToolSource {
|
||||
sandboxInheritanceToken?: string
|
||||
}
|
||||
|
||||
interface WorktreeCreated {
|
||||
worktree: ReturnType<WorktreeStateManager["addWorktree"]>
|
||||
result: CreateWorktreeResult
|
||||
@@ -47,7 +52,7 @@ export interface ToolDeps {
|
||||
claimRequest?: (requestID: string) => boolean
|
||||
cleanupWorktree: (wid: string, dir: string) => Promise<void>
|
||||
setup: (dir: string, branch?: string, id?: string) => Promise<void>
|
||||
createSessionInWorktree: (dir: string, branch: string, id?: string) => Promise<Session | null>
|
||||
createSessionInWorktree: (dir: string, branch: string, id?: string, source?: ToolSource) => Promise<Session | null>
|
||||
sessionMetadata: (client: KiloClient, dir: string) => Promise<Record<string, unknown>>
|
||||
registerWorktreeSession: (sid: string, dir: string) => void
|
||||
notifyReady: (sid: string, result: CreateWorktreeResult, wid?: string) => void
|
||||
@@ -116,7 +121,7 @@ async function prompt(client: KiloClient, sid: string, dir: string, task: ToolTa
|
||||
)
|
||||
}
|
||||
|
||||
async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directory?: string) {
|
||||
async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directory?: string, source?: ToolSource) {
|
||||
const root = deps.getRoot()
|
||||
const state = deps.getState()
|
||||
if (!root || !state) return false
|
||||
@@ -135,7 +140,12 @@ async function local(deps: ToolDeps, client: KiloClient, task: ToolTask, directo
|
||||
const target = wt?.path ?? root
|
||||
const metadata = await deps.sessionMetadata(client, target)
|
||||
const { data } = await client.session.create(
|
||||
{ directory: target, platform: PLATFORM, metadata },
|
||||
{
|
||||
directory: target,
|
||||
platform: PLATFORM,
|
||||
metadata,
|
||||
...(source?.sandboxInheritanceToken ? { sandboxInheritanceToken: source.sandboxInheritanceToken } : {}),
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
const session = data
|
||||
@@ -163,6 +173,7 @@ async function worktree(
|
||||
total: number,
|
||||
groupId?: string,
|
||||
versions?: boolean,
|
||||
source?: ToolSource,
|
||||
) {
|
||||
const baseBranch = branch(task.branchName) ?? branch(task.name)
|
||||
const baseLabel = label(task.name) ?? label(task.branchName) ?? label(task.prompt)
|
||||
@@ -176,7 +187,12 @@ async function worktree(
|
||||
if (!created) return false
|
||||
|
||||
await deps.setup(created.result.path, created.result.branch, created.worktree.id)
|
||||
const session = await deps.createSessionInWorktree(created.result.path, created.result.branch, created.worktree.id)
|
||||
const session = await deps.createSessionInWorktree(
|
||||
created.result.path,
|
||||
created.result.branch,
|
||||
created.worktree.id,
|
||||
source,
|
||||
)
|
||||
if (!session) {
|
||||
await deps.cleanupWorktree(created.worktree.id, created.result.path)
|
||||
return false
|
||||
@@ -216,6 +232,7 @@ export async function startFromTool(deps: ToolDeps, req: ToolRequest): Promise<v
|
||||
const versions = req.mode === "worktree" && req.versions === true && total > 1
|
||||
const groupId = versions ? `grp-${Date.now()}` : undefined
|
||||
const state = { ok: 0 }
|
||||
const source = { sandboxInheritanceToken: req.sandboxInheritanceToken }
|
||||
|
||||
deps.post({ type: "agentManager.multiVersionProgress", status: "creating", total, completed: 0, groupId })
|
||||
for (let i = 0; i < req.tasks.length; i++) {
|
||||
@@ -223,8 +240,8 @@ export async function startFromTool(deps: ToolDeps, req: ToolRequest): Promise<v
|
||||
try {
|
||||
const done =
|
||||
req.mode === "local"
|
||||
? await local(deps, client, task, req.directory)
|
||||
: await worktree(deps, client, task, i, total, groupId, versions)
|
||||
? await local(deps, client, task, req.directory, source)
|
||||
: await worktree(deps, client, task, i, total, groupId, versions, source)
|
||||
if (done) state.ok++
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
@@ -284,6 +301,8 @@ export function parseToolRequest(value: unknown): ToolRequest | undefined {
|
||||
requestID: typeof value.requestID === "string" ? value.requestID : `am-${Date.now()}`,
|
||||
sessionID: typeof value.sessionID === "string" ? value.sessionID : undefined,
|
||||
directory: typeof value.directory === "string" ? value.directory : undefined,
|
||||
sandboxInheritanceToken:
|
||||
typeof value.sandboxInheritanceToken === "string" ? value.sandboxInheritanceToken : undefined,
|
||||
mode,
|
||||
versions: typeof value.versions === "boolean" ? value.versions : undefined,
|
||||
tasks: parsed,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { Worktree, WorktreeStateManager } from "./WorktreeStateManager"
|
||||
import type { WorktreeManager, CreateWorktreeResult } from "./WorktreeManager"
|
||||
import { chooseBaseBranch } from "./base-branch"
|
||||
import { classifyWorktreeError } from "./git-import"
|
||||
import { PLATFORM } from "./constants"
|
||||
import type { AgentManagerOutMessage } from "./types"
|
||||
|
||||
export type CreateWorktreeOnDiskOptions = {
|
||||
groupId?: string
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
existingBranch?: string
|
||||
name?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type CreateWorktreeOnDiskResult = {
|
||||
worktree: Worktree
|
||||
result: CreateWorktreeResult
|
||||
}
|
||||
|
||||
export interface CreateWorktreeOnDiskContext {
|
||||
getWorktreeManager: () => WorktreeManager | undefined
|
||||
getStateManager: () => WorktreeStateManager | undefined
|
||||
postToWebview: (message: AgentManagerOutMessage) => void
|
||||
capture: (event: string, properties?: Record<string, unknown>) => void
|
||||
pushState: () => void
|
||||
log: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a git worktree on disk and register it in state. Returns null on failure.
|
||||
*
|
||||
* Pure orchestration — no vscode imports.
|
||||
*/
|
||||
export async function createWorktreeOnDisk(
|
||||
ctx: CreateWorktreeOnDiskContext,
|
||||
opts?: CreateWorktreeOnDiskOptions,
|
||||
): Promise<CreateWorktreeOnDiskResult | null> {
|
||||
const manager = ctx.getWorktreeManager()
|
||||
const state = ctx.getStateManager()
|
||||
if (!manager || !state) {
|
||||
ctx.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "error",
|
||||
message: "Open a folder that contains a git repository to use worktrees",
|
||||
errorCode: "not_git_repo",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
ctx.postToWebview({ type: "agentManager.worktreeSetup", status: "creating", message: "Creating git worktree..." })
|
||||
|
||||
// Resolve effective base branch using configured default
|
||||
const effectiveBase = opts?.existingBranch
|
||||
? undefined
|
||||
: await resolveBaseBranch(ctx, manager, state, opts?.baseBranch)
|
||||
|
||||
let result: CreateWorktreeResult
|
||||
try {
|
||||
result = await manager.createWorktree({
|
||||
prompt: opts?.name || "kilo",
|
||||
baseBranch: effectiveBase ?? opts?.baseBranch,
|
||||
branchName: opts?.branchName,
|
||||
existingBranch: opts?.existingBranch,
|
||||
})
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
ctx.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "error",
|
||||
message: msg,
|
||||
errorCode: classifyWorktreeError(msg),
|
||||
})
|
||||
ctx.capture("Agent Manager Session Error", {
|
||||
source: PLATFORM,
|
||||
error: msg,
|
||||
context: "createWorktree",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const worktree = state.addWorktree({
|
||||
branch: result.branch,
|
||||
path: result.path,
|
||||
parentBranch: result.parentBranch,
|
||||
remote: result.remote,
|
||||
groupId: opts?.groupId,
|
||||
label: opts?.label,
|
||||
branchOwned: !opts?.existingBranch,
|
||||
})
|
||||
|
||||
// Push state immediately so the sidebar shows the new worktree with a loading indicator
|
||||
ctx.pushState()
|
||||
ctx.postToWebview({
|
||||
type: "agentManager.worktreeSetup",
|
||||
status: "creating",
|
||||
message: "Setting up worktree...",
|
||||
branch: result.branch,
|
||||
worktreeId: worktree.id,
|
||||
})
|
||||
|
||||
return { worktree, result }
|
||||
}
|
||||
|
||||
/** Resolve the effective base branch using the configured default, explicit override, and existence check. */
|
||||
async function resolveBaseBranch(
|
||||
ctx: CreateWorktreeOnDiskContext,
|
||||
manager: WorktreeManager,
|
||||
state: WorktreeStateManager,
|
||||
explicit?: string,
|
||||
): Promise<string | undefined> {
|
||||
const configured = state.getDefaultBaseBranch()
|
||||
if (!configured && !explicit) return undefined
|
||||
|
||||
const configuredExists = configured ? await manager.branchExists(configured) : false
|
||||
const result = chooseBaseBranch({ explicit, configured, configuredExists })
|
||||
|
||||
if (result.stale) clearStaleDefaultBaseBranch(ctx, state, result.stale)
|
||||
return result.branch
|
||||
}
|
||||
|
||||
/** Reset a stale default base branch and notify the webview. */
|
||||
function clearStaleDefaultBaseBranch(
|
||||
ctx: CreateWorktreeOnDiskContext,
|
||||
state: WorktreeStateManager,
|
||||
stale: string,
|
||||
): void {
|
||||
ctx.log(`Default base branch "${stale}" no longer exists, clearing`)
|
||||
state.setDefaultBaseBranch(undefined)
|
||||
ctx.pushState()
|
||||
}
|
||||
@@ -144,6 +144,29 @@ describe("agent manager tool start", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("passes sandbox inheritance token to local sessions", async () => {
|
||||
const client = {
|
||||
session: {
|
||||
create: mock(async () => ({ data: session("s-local") })),
|
||||
promptAsync: mock(async () => ({})),
|
||||
},
|
||||
}
|
||||
const c = deps({ getClient: () => client as never })
|
||||
|
||||
await startFromTool(c, {
|
||||
requestID: "am-local-source",
|
||||
sessionID: "s-parent",
|
||||
sandboxInheritanceToken: "si-token",
|
||||
mode: "local",
|
||||
tasks: [{ prompt: "Do work" }],
|
||||
})
|
||||
|
||||
expect(client.session.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sandboxInheritanceToken: "si-token" }),
|
||||
{ throwOnError: true },
|
||||
)
|
||||
})
|
||||
|
||||
it("starts local sessions when Windows drive-letter casing differs", async () => {
|
||||
setPlatform("win32")
|
||||
const client = {
|
||||
@@ -191,6 +214,8 @@ describe("agent manager tool start", () => {
|
||||
const c = deps({ getClient: () => client as never })
|
||||
await startFromTool(c, {
|
||||
requestID: "am-2",
|
||||
sessionID: "s-parent",
|
||||
sandboxInheritanceToken: "si-token",
|
||||
mode: "worktree",
|
||||
tasks: [
|
||||
{
|
||||
@@ -206,7 +231,9 @@ describe("agent manager tool start", () => {
|
||||
expect.objectContaining({ branchName: "fix-one", name: "fix-one", label: "one" }),
|
||||
)
|
||||
expect(c.setup).toHaveBeenCalled()
|
||||
expect(c.createSessionInWorktree).toHaveBeenCalled()
|
||||
expect(c.createSessionInWorktree).toHaveBeenCalledWith("/repo/.kilo/worktrees/wt-1", "kilo/test", "wt-1", {
|
||||
sandboxInheritanceToken: "si-token",
|
||||
})
|
||||
expect(c.registerWorktreeSession).toHaveBeenCalledWith("s-wt", "/repo/.kilo/worktrees/wt-1")
|
||||
expect(c.notifyReady).toHaveBeenCalled()
|
||||
expect(client.session.promptAsync).toHaveBeenCalledWith(
|
||||
|
||||
@@ -24,6 +24,7 @@ export const AgentManagerMode = Schema.Literals(["worktree", "local"])
|
||||
export const AgentManagerStart = Schema.Struct({
|
||||
requestID: Schema.String,
|
||||
sessionID: SessionID,
|
||||
sandboxInheritanceToken: Schema.optional(Schema.String),
|
||||
mode: AgentManagerMode,
|
||||
versions: Schema.optional(Schema.Boolean),
|
||||
tasks: Schema.Array(AgentManagerTask).check(Schema.isMinLength(1), Schema.isMaxLength(20)),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
|
||||
interface Grant {
|
||||
sessionID: SessionID
|
||||
directory: string
|
||||
expires: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
const ttl = 24 * 60 * 60 * 1000
|
||||
const grants = new Map<string, Grant>()
|
||||
|
||||
function cleanup(now = Date.now()) {
|
||||
for (const [token, grant] of grants) {
|
||||
if (grant.expires <= now || grant.remaining <= 0) grants.delete(token)
|
||||
}
|
||||
}
|
||||
|
||||
export function issue(input: { sessionID: SessionID; directory: string; count: number }) {
|
||||
cleanup()
|
||||
const token = `si-${randomUUID()}`
|
||||
grants.set(token, {
|
||||
sessionID: input.sessionID,
|
||||
directory: input.directory,
|
||||
expires: Date.now() + ttl,
|
||||
remaining: Math.max(1, input.count),
|
||||
})
|
||||
return token
|
||||
}
|
||||
|
||||
export function consume(token: string | undefined) {
|
||||
if (!token) return undefined
|
||||
cleanup()
|
||||
const grant = grants.get(token)
|
||||
if (!grant) return undefined
|
||||
grant.remaining--
|
||||
if (grant.remaining <= 0) grants.delete(token)
|
||||
return { sessionID: grant.sessionID, directory: grant.directory }
|
||||
}
|
||||
@@ -364,12 +364,14 @@ export const inherit = Effect.fn("SandboxPolicy.inherit")(function* (
|
||||
parentID: SessionID,
|
||||
sessionID: SessionID,
|
||||
fallback?: Omit<Snapshot, "version">,
|
||||
sourceDirectory?: string,
|
||||
) {
|
||||
const directory = yield* InstanceState.directory
|
||||
const source = sourceDirectory ?? directory
|
||||
yield* locked(
|
||||
parentID,
|
||||
Effect.gen(function* () {
|
||||
const stored = yield* read(directory, parentID)
|
||||
const stored = yield* read(source, parentID)
|
||||
const parent: Snapshot | undefined = stored ?? (fallback && { ...fallback, version: 0 })
|
||||
if (!parent) return
|
||||
// Only persist the parent snapshot when it actually belongs to this directory. A fallback
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// kilocode_change - new file
|
||||
import { Bus } from "@/bus"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AgentManagerEvent, type AgentManagerTask } from "@/kilocode/agent-manager/event"
|
||||
import * as SandboxInheritance from "@/kilocode/sandbox/inheritance"
|
||||
import { KiloSessionMessageOrder } from "@/kilocode/session/message-order"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Tool } from "@/tool/tool"
|
||||
@@ -237,9 +239,16 @@ export const AgentManagerTool = Tool.define<
|
||||
})
|
||||
|
||||
const requestID = `am-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
const directory = yield* InstanceState.directory
|
||||
const sandboxInheritanceToken = SandboxInheritance.issue({
|
||||
sessionID: ctx.sessionID,
|
||||
directory,
|
||||
count: params.tasks.length,
|
||||
})
|
||||
yield* bus.publish(AgentManagerEvent.Start, {
|
||||
requestID,
|
||||
sessionID: ctx.sessionID,
|
||||
sandboxInheritanceToken,
|
||||
mode: params.mode,
|
||||
versions: params.versions,
|
||||
tasks,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Permission } from "@/permission"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
// kilocode_change start - Kilo session behavior extensions
|
||||
import { BackgroundProcess } from "@/kilocode/background-process"
|
||||
import * as SandboxInheritance from "@/kilocode/sandbox/inheritance"
|
||||
import { InteractiveTerminal } from "@/kilocode/interactive-terminal"
|
||||
import { KiloSession } from "@/kilocode/session"
|
||||
import { kiloSessionFork } from "@/kilocode/session/fork-command"
|
||||
@@ -259,7 +260,10 @@ export const CreateInput = Schema.optional(
|
||||
metadata: Schema.optional(Metadata),
|
||||
permission: Schema.optional(PermissionV1.Ruleset),
|
||||
platform: Schema.optional(Schema.String), // kilocode_change - per-session platform override for telemetry attribution
|
||||
// kilocode_change start - server-issued sandbox inheritance grant
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID),
|
||||
sandboxInheritanceToken: Schema.optional(Schema.String),
|
||||
// kilocode_change end
|
||||
}),
|
||||
)
|
||||
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||
@@ -490,6 +494,7 @@ export type NotFound = NotFoundError
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (input?: ListInput) => Effect.Effect<Info[]>
|
||||
// kilocode_change start - session create metadata and sandbox inheritance extensions
|
||||
readonly listGlobal: (input?: GlobalListInput) => Effect.Effect<GlobalInfo[]>
|
||||
readonly create: (input?: {
|
||||
parentID?: SessionID
|
||||
@@ -500,7 +505,9 @@ export interface Interface {
|
||||
permission?: PermissionV1.Ruleset
|
||||
platform?: string // kilocode_change - per-session platform override for telemetry attribution
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
sandboxInheritanceToken?: string
|
||||
}) => Effect.Effect<Info>
|
||||
// kilocode_change end
|
||||
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
|
||||
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly get: (id: SessionID) => Effect.Effect<Info, NotFound>
|
||||
@@ -569,6 +576,7 @@ export const layer: Layer.Layer<
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
// kilocode_change start - inherited sandbox policy source
|
||||
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
||||
id?: SessionID
|
||||
title?: string
|
||||
@@ -582,6 +590,7 @@ export const layer: Layer.Layer<
|
||||
permission?: PermissionV1.Ruleset
|
||||
platform?: string // kilocode_change - per-session platform override for telemetry attribution
|
||||
sourceID?: SessionID // kilocode_change - inherited sandbox policy source
|
||||
sourceDirectory?: string
|
||||
sandboxFallback?: SandboxPolicy.Snapshot // kilocode_change - confinement to seed when source state lives in another directory
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
@@ -607,6 +616,7 @@ export const layer: Layer.Layer<
|
||||
},
|
||||
}
|
||||
log.info("created", result)
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - legacy sessions must satisfy the upstream project foreign key
|
||||
yield* db
|
||||
@@ -627,7 +637,7 @@ export const layer: Layer.Layer<
|
||||
// kilocode_change start - initialize inherited state before session.created subscribers run
|
||||
KiloSession.register({ id: result.id, parentID: result.parentID, platform: input.platform })
|
||||
const source = input.sourceID ?? result.parentID
|
||||
if (source) yield* SandboxPolicy.inherit(source, result.id, input.sandboxFallback)
|
||||
if (source) yield* SandboxPolicy.inherit(source, result.id, input.sandboxFallback, input.sourceDirectory)
|
||||
// kilocode_change end
|
||||
|
||||
yield* events.publish(SessionV1.Event.Created, { sessionID: result.id, info: result })
|
||||
@@ -769,6 +779,7 @@ export const layer: Layer.Layer<
|
||||
} as SessionV1.Part
|
||||
})
|
||||
|
||||
// kilocode_change start - session create metadata and sandbox inheritance extensions
|
||||
const create = Effect.fn("Session.create")(function* (input?: {
|
||||
parentID?: SessionID
|
||||
title?: string
|
||||
@@ -778,9 +789,14 @@ export const layer: Layer.Layer<
|
||||
permission?: PermissionV1.Ruleset
|
||||
platform?: string // kilocode_change - per-session platform override for telemetry attribution
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
sandboxInheritanceToken?: string
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const workspace = yield* InstanceState.workspaceID
|
||||
const grant = SandboxInheritance.consume(input?.sandboxInheritanceToken)
|
||||
if (input?.sandboxInheritanceToken && !grant) yield* Effect.die(new Error("Invalid sandbox inheritance token"))
|
||||
// kilocode_change end
|
||||
// kilocode_change start - propagate trusted sandbox inheritance grant
|
||||
const session = yield* createNext({
|
||||
parentID: input?.parentID,
|
||||
directory: ctx.directory,
|
||||
@@ -791,8 +807,11 @@ export const layer: Layer.Layer<
|
||||
metadata: input?.metadata,
|
||||
permission: input?.permission,
|
||||
platform: input?.platform, // kilocode_change
|
||||
sourceID: grant?.sessionID, // kilocode_change
|
||||
sourceDirectory: grant?.directory, // kilocode_change
|
||||
workspaceID: input?.workspaceID ?? workspace,
|
||||
})
|
||||
// kilocode_change end
|
||||
return session
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { $ } from "bun"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Deferred, Effect, Exit, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
@@ -14,8 +14,10 @@ import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { BackgroundProcess } from "@/kilocode/background-process"
|
||||
import { Notebook } from "@/kilocode/notebook/service"
|
||||
import * as SandboxActivation from "@/kilocode/sandbox/activation"
|
||||
import * as SandboxInheritance from "@/kilocode/sandbox/inheritance"
|
||||
import * as SandboxPolicy from "@/kilocode/sandbox/policy"
|
||||
import { SandboxStore } from "@/kilocode/sandbox/store"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { Shell } from "@/shell/shell"
|
||||
@@ -95,6 +97,19 @@ function activate(sessionID: Session.Info["id"]) {
|
||||
}
|
||||
|
||||
describe("sandbox session cleanup", () => {
|
||||
test("keeps inheritance grants valid across slow worktree setup", () => {
|
||||
const now = Date.now
|
||||
try {
|
||||
Date.now = () => 1_700_000_000_000
|
||||
const sid = "session" as SessionID
|
||||
const token = SandboxInheritance.issue({ sessionID: sid, directory: "/repo", count: 1 })
|
||||
Date.now = () => 1_700_000_000_000 + 6 * 60 * 1000
|
||||
expect(SandboxInheritance.consume(token)).toEqual({ sessionID: sid, directory: "/repo" })
|
||||
} finally {
|
||||
Date.now = now
|
||||
}
|
||||
})
|
||||
|
||||
it.live("forks inherit the source session snapshot", () =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
@@ -112,6 +127,25 @@ describe("sandbox session cleanup", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("created sessions inherit the source snapshot across directories", () =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
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))
|
||||
if (!status.available) return
|
||||
const token = SandboxInheritance.issue({ sessionID: source.id, directory: dir, count: 1 })
|
||||
|
||||
const child = yield* provideInstance(worktree)(sessions.create({ title: "sandbox-child", sandboxInheritanceToken: token }))
|
||||
expect((yield* provideInstance(worktree)(SandboxPolicy.status(child.id))).enabled).toBe(true)
|
||||
|
||||
yield* provideInstance(dir)(SandboxPolicy.toggle(source.id))
|
||||
expect((yield* provideInstance(dir)(SandboxPolicy.status(source.id))).enabled).toBe(false)
|
||||
expect((yield* provideInstance(worktree)(SandboxPolicy.status(child.id))).enabled).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("forks into another directory carry the source confinement", () =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
|
||||
@@ -4134,6 +4134,7 @@ export class Session2 extends HeyApiClient {
|
||||
permission?: PermissionRuleset
|
||||
platform?: string
|
||||
workspaceID?: string
|
||||
sandboxInheritanceToken?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
@@ -4152,6 +4153,7 @@ export class Session2 extends HeyApiClient {
|
||||
{ in: "body", key: "permission" },
|
||||
{ in: "body", key: "platform" },
|
||||
{ in: "body", key: "workspaceID" },
|
||||
{ in: "body", key: "sandboxInheritanceToken" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3538,6 +3538,7 @@ export type EventKilocodeAgentManagerStart = {
|
||||
properties: {
|
||||
requestID: string
|
||||
sessionID: string
|
||||
sandboxInheritanceToken?: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: Array<{
|
||||
@@ -8796,6 +8797,7 @@ export type SessionCreateData = {
|
||||
permission?: PermissionRuleset
|
||||
platform?: string
|
||||
workspaceID?: string
|
||||
sandboxInheritanceToken?: string
|
||||
}
|
||||
path?: never
|
||||
query?: {
|
||||
|
||||
@@ -6143,6 +6143,9 @@
|
||||
"workspaceID": {
|
||||
"type": "string",
|
||||
"pattern": "^wrk"
|
||||
},
|
||||
"sandboxInheritanceToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -32398,6 +32401,9 @@
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"sandboxInheritanceToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["worktree", "local"]
|
||||
|
||||
Reference in New Issue
Block a user