fix(wsl): refuse render-driven hook checks on a stopped distro instead of booting it (#35155)

hooks:check takes an optional background flag. The sidebar setup-script prompt and the setup
guide's progress probe, which run on render, pass it: on a stopped or unverifiable WSL distro
the check answers status 'error' without opening orca.yaml over \\\\wsl.localhost, which would boot
the distro. Launch preflight and settings, user acts that start the distro anyway, are unchanged.
This commit is contained in:
Neil
2026-10-28 07:33:15 -07:00
committed by GitHub
parent 51d25013f1
commit c58591e56d
8 changed files with 99 additions and 9 deletions
@@ -3,6 +3,7 @@ import { getRepoDialableSshTargetId, type ExecutionHostId } from '../../../share
import { isFolderRepo } from '../../../shared/repo-kind'
import { hasHooksFile, loadHooks, hasUnrecognizedOrcaYamlKeys } from '../../hooks'
import { resolveRepoForExecutionHost } from '../worktrees/repo-host-ownership'
import { blockedWslDistroForPath } from '../../wsl/wsl-unc-access-gate'
import type { WorktreeIpcContext } from '../worktrees/worktree-ipc-context'
export function registerWorktreeHookCheckHandler(context: WorktreeIpcContext): void {
@@ -10,7 +11,7 @@ export function registerWorktreeHookCheckHandler(context: WorktreeIpcContext): v
ipcMain.handle(
'hooks:check',
async (_event, args: { repoId: string; hostId?: ExecutionHostId }) => {
async (_event, args: { repoId: string; hostId?: ExecutionHostId; background?: boolean }) => {
const repo = resolveRepoForExecutionHost(store, args.repoId, args.hostId)
if (!repo) {
const repoIdExists = store.getRepos().some((candidate) => candidate.id === args.repoId)
@@ -35,6 +36,11 @@ export function registerWorktreeHookCheckHandler(context: WorktreeIpcContext): v
}
}
// A render-driven check must not open a stopped distro's `\\wsl.localhost` paths (E9).
if (args.background && (await blockedWslDistroForPath(repo.path))) {
return { status: 'error', hasHooks: false, hooks: null, mayNeedUpdate: false }
}
const has = hasHooksFile(repo.path)
const hooks = has ? loadHooks(repo.path) : null
// Well-formed unknown keys may come from a newer Orca; suggest updating instead of parse repair.
@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../shared/repo-types'
const mocks = vi.hoisted(() => ({
handlers: new Map<string, (event: unknown, args: unknown) => unknown>(),
hasHooksFile: vi.fn((_path: string) => false),
blocked: vi.fn<(path: string) => Promise<{ distro: string; state: 'stopped' } | null>>()
}))
vi.mock('electron', () => ({
ipcMain: {
handle: (channel: string, handler: (event: unknown, args: unknown) => unknown) =>
mocks.handlers.set(channel, handler)
}
}))
vi.mock('../../hooks', () => ({
hasHooksFile: mocks.hasHooksFile,
loadHooks: () => null,
hasUnrecognizedOrcaYamlKeys: () => false
}))
vi.mock('../../wsl/wsl-unc-access-gate', () => ({ blockedWslDistroForPath: mocks.blocked }))
import { registerWorktreeHookCheckHandler } from './register-worktree-hook-check-handler'
const UNC = String.raw`\\wsl.localhost\Ubuntu\home\u\app`
function check(args: { background?: boolean }) {
const repo: Partial<Repo> = { id: 'repo', path: UNC, kind: 'git', executionHostId: 'local' }
const store = { getRepos: () => [repo], getRepo: () => repo }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The handler reads only these store accessors.
registerWorktreeHookCheckHandler({ store } as never)
return mocks.handlers.get('hooks:check')!(null, { repoId: 'repo', ...args })
}
describe('hooks:check on a stopped WSL distro', () => {
beforeEach(() => {
mocks.hasHooksFile.mockClear()
mocks.blocked.mockReset()
mocks.blocked.mockResolvedValue({ distro: 'Ubuntu', state: 'stopped' })
})
it('refuses a background check without opening orca.yaml, so it cannot boot the distro', async () => {
await expect(check({ background: true })).resolves.toEqual({
status: 'error',
hasHooks: false,
hooks: null,
mayNeedUpdate: false
})
expect(mocks.blocked).toHaveBeenCalledWith(UNC)
expect(mocks.hasHooksFile).not.toHaveBeenCalled()
})
it('still reads for a launch, which is a user act that starts the distro anyway', async () => {
await expect(check({})).resolves.toMatchObject({ status: 'ok' })
expect(mocks.blocked).not.toHaveBeenCalled()
expect(mocks.hasHooksFile).toHaveBeenCalledWith(UNC)
})
it('reads a running distro for a background check as before', async () => {
mocks.blocked.mockResolvedValue(null)
await expect(check({ background: true })).resolves.toMatchObject({ status: 'ok' })
expect(mocks.hasHooksFile).toHaveBeenCalledWith(UNC)
})
})
+2 -1
View File
@@ -3,7 +3,8 @@ import type { ExecutionHostId } from '../../shared/execution-host'
import type { SetupScriptImportCandidate } from '../../shared/setup-script-imports'
export type HooksApi = {
check: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{
/** `background`: a render-driven probe, refused (status 'error') rather than booting a stopped WSL distro. */
check: (args: { repoId: string; hostId?: ExecutionHostId; background?: boolean }) => Promise<{
status?: 'ok' | 'error'
hasHooks: boolean
hooks: OrcaHooks | null
+1 -1
View File
@@ -3,7 +3,7 @@ import type { ExecutionHostId } from '../../shared/execution-host'
import type { PreloadApi } from '../api-types'
export const hooksApi = {
check: (args: { repoId: string; hostId?: ExecutionHostId }) =>
check: (args: { repoId: string; hostId?: ExecutionHostId; background?: boolean }) =>
ipcRenderer.invoke('hooks:check', args),
inspectSetupScriptImports: (args: { repoId: string; hostId?: ExecutionHostId }) =>
@@ -169,9 +169,9 @@ export function useSetupGuideProgress(
async function refreshSetupScriptState(): Promise<void> {
for (const repo of orderedGitRepos) {
const hooksResult = await checkRuntimeHooks(getRepoExecutionHostId(repo), repo.id).catch(
() => null
)
const hooksResult = await checkRuntimeHooks(getRepoExecutionHostId(repo), repo.id, {
background: true
}).catch(() => null)
if (stale) {
return
}
@@ -75,7 +75,7 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
const hostId = getRepoExecutionHostId(repo)
const inspection = await inspectSetupScriptPromptState({
repo,
checkHooks: () => checkRuntimeHooks(hostId, repo.id),
checkHooks: () => checkRuntimeHooks(hostId, repo.id, { background: true }),
inspectImports: () => inspectRuntimeSetupScriptImports(hostId, repo.id)
})
if (!cancelled) {
@@ -73,6 +73,18 @@ describe('runtime hooks client', () => {
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
})
it('marks a render-driven check as background, so a stopped WSL distro is refused, not booted', async () => {
hooksCheck.mockResolvedValue({ hasHooks: false, hooks: null, mayNeedUpdate: false })
await checkRuntimeHooks('local', 'repo-1', { background: true })
expect(hooksCheck).toHaveBeenLastCalledWith({
repoId: 'repo-1',
hostId: 'local',
background: true
})
})
it('routes hook operations through the repo owner runtime environment', async () => {
runtimeEnvironmentCall.mockResolvedValue({
id: 'rpc-1',
@@ -30,13 +30,19 @@ export type IssueCommandReadResult = {
source: 'local' | 'shared' | 'none'
}
/** `background`: a render-driven probe that must not boot a stopped WSL distro (E9); launches omit it. */
export async function checkRuntimeHooks(
hostId: ExecutionHostId,
repoId: string
repoId: string,
options: { background?: boolean } = {}
): Promise<HookCheckResult> {
const target = getHookInspectionTarget(hostId)
if (target.kind !== 'environment') {
return window.api.hooks.check({ repoId, hostId })
return window.api.hooks.check({
repoId,
hostId,
...(options.background ? { background: true } : {})
})
}
return callRuntimeRpc<HookCheckResult>(
target,