fix(settings): make WSL skill commands pasteable (#7795) (#7881)

* fix(settings): make WSL skill commands pasteable (#7795)

* Fix WSL skill commands so PowerShell 7 pastes match PowerShell 5.1 argv

- Encode the WSL login-shell script as base64 and decode/eval it inside
  the sh -c invocation, avoiding raw nested quotes at the paste boundary
- Scope $PSNativeCommandArgumentPassing = 'Legacy' to the invocation so
  PS 5.1 and PS 7 both hand wsl.exe the same escaped argv
- Extract powershell-native-argument.ts as the shared quoting module and
  reuse it from ssh-remote-powershell.ts

* test(runtime): stub getRepo in mobile-tab startup cwd test

Main's #7892 made listMobileSessionTabs validate selectors via
this.store?.getRepo; the mock store here only defined
getWorkspaceSession, so the merged CI build threw 'getRepo is not a
function'. Return null (wt-1 is a worktree id, not a repo).

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Rod Boev
2026-07-12 01:33:08 -07:00
committed by GitHub
co-authored by Orca Jinjing
parent 1d1ec02b14
commit 34f74129d6
9 changed files with 160 additions and 49 deletions
+3 -3
View File
@@ -171,9 +171,9 @@ function mergeSystemCodexConfigIntoRuntime(runtimeConfig: string, systemConfig:
.filter((section) => isRuntimeProjectTomlSection(section.header))
.map((section) => getTomlSectionHeaderKey(section.header))
)
const systemProjectSections = deduplicateProjectTomlSections(getTomlSections(systemConfig)).filter(
(section) => isRuntimeProjectTomlSection(section.header)
)
const systemProjectSections = deduplicateProjectTomlSections(
getTomlSections(systemConfig)
).filter((section) => isRuntimeProjectTomlSection(section.header))
const systemUntrustedProjectHeaders = new Set(
systemProjectSections
.filter((section) => getProjectTrustLevel(section.block) === 'untrusted')
+1 -4
View File
@@ -90,10 +90,7 @@ function linkSystemCodexResource(
removeCopiedResourceIfOwned(targetPath, managedHomePath, entryName, sourcePath)
return
}
if (
entryName === CODEX_GLOBAL_INSTRUCTIONS_ENTRY &&
!systemResourceIsRegularFile(sourcePath)
) {
if (entryName === CODEX_GLOBAL_INSTRUCTIONS_ENTRY && !systemResourceIsRegularFile(sourcePath)) {
removeCopiedResourceIfOwned(targetPath, managedHomePath, entryName, sourcePath)
console.warn('[codex-home] Ignoring non-file system Codex resource:', entryName)
return
+7 -11
View File
@@ -1153,11 +1153,7 @@ describe('upsertProjectTrustLevel', () => {
// Linux path underneath is not — .../Repo and .../repo are two projects.
const existingPath = '\\\\wsl$\\Ubuntu\\home\\u\\Repo'
const incomingPath = '\\\\wsl$\\Ubuntu\\home\\u\\repo'
const original = [
`[projects.'${existingPath}']`,
'trust_level = "untrusted"',
''
].join('\n')
const original = [`[projects.'${existingPath}']`, 'trust_level = "untrusted"', ''].join('\n')
const updated = upsertProjectTrustLevelInContent(original, incomingPath, 'trusted', {
alreadyCanonical: true
@@ -1253,9 +1249,9 @@ describe('normalizeCodexProjectPathForLookup', () => {
})
it('treats wsl.localhost like the wsl$ share for the case-sensitive tail', () => {
expect(
normalizeCodexProjectPathForLookup('\\\\wsl.localhost\\Ubuntu\\home\\u\\Repo')
).not.toBe(normalizeCodexProjectPathForLookup('\\\\wsl.localhost\\Ubuntu\\home\\u\\repo'))
expect(normalizeCodexProjectPathForLookup('\\\\wsl.localhost\\Ubuntu\\home\\u\\Repo')).not.toBe(
normalizeCodexProjectPathForLookup('\\\\wsl.localhost\\Ubuntu\\home\\u\\repo')
)
expect(normalizeCodexProjectPathForLookup('\\\\WSL.LOCALHOST\\Ubuntu\\home\\u\\proj')).toBe(
normalizeCodexProjectPathForLookup('//wsl.localhost/ubuntu/home/u/proj')
)
@@ -1291,9 +1287,9 @@ describe('normalizeCodexProjectPathForLookup', () => {
describe('normalizeCodexProjectPathForRevocationLookup', () => {
it('folds WSL tails fully so drifted-case legacy revocations still match', () => {
expect(
normalizeCodexProjectPathForRevocationLookup('\\\\wsl$\\Ubuntu\\home\\u\\Repo')
).toBe(normalizeCodexProjectPathForRevocationLookup('//wsl.localhost/ubuntu/home/u/repo'))
expect(normalizeCodexProjectPathForRevocationLookup('\\\\wsl$\\Ubuntu\\home\\u\\Repo')).toBe(
normalizeCodexProjectPathForRevocationLookup('//wsl.localhost/ubuntu/home/u/repo')
)
})
it('keeps POSIX paths case-sensitive', () => {
+4 -10
View File
@@ -1,14 +1,8 @@
import { encodePowerShellCommand } from '../../shared/powershell-command-encoding'
export function powerShellLiteral(value: string): string {
return `'${value.replace(/'/g, "''")}'`
}
// Why: Windows PowerShell 5.1 does not preserve embedded double quotes when
// passing args to native executables, so pre-escape them for Win32 argv parsing.
export function powerShellNativeArg(value: string): string {
return powerShellLiteral(value.replace(/(\\*)"/g, '$1$1\\"'))
}
export {
quotePowerShellLiteral as powerShellLiteral,
quotePowerShellNativeArgument as powerShellNativeArg
} from '../../shared/powershell-native-argument'
export function powerShellCommand(script: string): string {
return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShellCommand(script)}`
@@ -89,10 +89,12 @@ describe('CliSection project runtime defaults', () => {
sourceKinds: ['global']
})
)
expect(capturedPanel.props?.command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.command).toContain('npx skills add')
expect(capturedPanel.props?.installedCommand).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.installedCommand).toContain('npx skills update orca-cli --global')
expect(capturedPanel.props?.command).toMatch(
/^& \{ \$PSNativeCommandArgumentPassing = 'Legacy'; wsl\.exe -d 'Ubuntu' -- sh -c 'eval \\"`printf %s [A-Za-z0-9+/=]+ \| base64 -d`\\"'/
)
expect(capturedPanel.props?.installedCommand).toMatch(
/^& \{ \$PSNativeCommandArgumentPassing = 'Legacy'; wsl\.exe -d 'Ubuntu' -- sh -c 'eval \\"`printf %s [A-Za-z0-9+/=]+ \| base64 -d`\\"'/
)
expect(getWslInstallStatus).toHaveBeenCalledWith({ distro: 'Ubuntu' })
expect(getWslInstallStatus).toHaveBeenCalledTimes(2)
})
@@ -1,6 +1,11 @@
import { execFileSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { buildAgentFeatureSkillInstallCommand } from '../../../../shared/agent-feature-install-commands'
import { buildWslLoginShellCommand } from '../../../../shared/wsl-login-shell-command'
import {
buildSkillCommandForRuntime,
buildSkillInstallCommandForRuntime,
@@ -8,31 +13,111 @@ import {
getSkillDiscoveryTargetForRuntime
} from './CliSkillRuntimeSetup'
function decodeWslLoginShellScript(command: string): string {
const encoded = /-- sh -c 'eval \\"`printf %s ([A-Za-z0-9+/=]+) \| base64 -d`\\"'/.exec(
command
)?.[1]
expect(encoded).toBeDefined()
return Buffer.from(encoded!, 'base64').toString('utf8')
}
function getWslOuterShellScript(command: string): string {
const script = /-- sh -c '([^']+)' \} # Runs:/.exec(command)?.[1]
expect(script).toBeDefined()
// Simulate PowerShell 5.1's native argv boundary consuming quote escapes.
return script!.replaceAll('\\"', '"')
}
describe('CliSkillRuntimeSetup runtime helpers', () => {
it('wraps WSL skill installs in the selected distro login shell', () => {
const command = buildSkillInstallCommandForRuntime('npx skills add orchestration --global', {
it('wraps WSL skill installs as a directly runnable selected-distro command', () => {
const skillCommand = 'npx skills add orchestration --global'
const command = buildSkillInstallCommandForRuntime(skillCommand, {
runtime: 'wsl',
wslDistro: 'Ubuntu',
label: 'WSL Ubuntu'
})
const encoded = Buffer.from(buildWslLoginShellCommand(skillCommand), 'utf8').toString('base64')
expect(command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills add orchestration --global')
expect(command).toBe(
`& { $PSNativeCommandArgumentPassing = 'Legacy'; wsl.exe -d 'Ubuntu' -- sh -c 'eval \\"\`printf %s ${encoded} | base64 -d\`\\"' } # Runs: ${skillCommand}`
)
expect(decodeWslLoginShellScript(command)).toContain(
'exec "$_orca_wsl_shell" -ilc \'npx skills add orchestration --global\''
)
})
it('wraps WSL skill updates with the same selected distro login shell', () => {
it('wraps WSL skill updates as a directly runnable selected-distro command', () => {
const command = buildSkillCommandForRuntime('npx skills update orchestration --global', {
runtime: 'wsl',
wslDistro: 'Fedora Remix',
label: 'WSL Fedora Remix'
})
expect(command).toContain("wsl.exe -d 'Fedora Remix' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills update orchestration --global')
expect(decodeWslLoginShellScript(command)).toContain(
'exec "$_orca_wsl_shell" -ilc \'npx skills update orchestration --global\''
)
})
it('scopes the PS5-compatible argv mode when pasted into PowerShell 7', () => {
const command = buildSkillCommandForRuntime('npx skills update orchestration --global', {
runtime: 'wsl',
label: 'WSL'
})
expect(command).toMatch(
/^& \{ \$PSNativeCommandArgumentPassing = 'Legacy'; wsl\.exe -- sh -c 'eval \\"`printf/
)
expect(command).toContain('`\\"\' } # Runs: npx skills update orchestration --global')
})
it.skipIf(process.platform === 'win32')(
'runs skill commands with npx from the configured WSL login-shell PATH',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-skill-command-'))
const tools = join(root, 'tools')
const npxBin = join(root, 'npx-bin')
const loginShell = join(root, 'zsh')
mkdirSync(tools)
mkdirSync(npxBin)
writeFileSync(
join(tools, 'getent'),
'#!/bin/sh\nprintf \'%s\\n\' "user:x:1000:1000::/home/user:$ORCA_TEST_LOGIN_SHELL"\n'
)
writeFileSync(
loginShell,
'#!/bin/sh\nexport PATH="$ORCA_TEST_NPX_BIN:/usr/bin:/bin"\nexec /bin/sh -c "$2"\n'
)
writeFileSync(
join(npxBin, 'npx'),
'#!/bin/sh\nread -r input\nprintf \'%s:%s\' "$*" "$input"\n'
)
chmodSync(join(tools, 'getent'), 0o755)
chmodSync(loginShell, 0o755)
chmodSync(join(npxBin, 'npx'), 0o755)
try {
const wrapped = buildSkillCommandForRuntime('npx skills update orchestration --global', {
runtime: 'wsl',
label: 'WSL'
})
expect(
execFileSync('/bin/sh', ['-c', getWslOuterShellScript(wrapped)], {
encoding: 'utf8',
input: 'terminal-input\n',
env: {
...process.env,
PATH: `${tools}:/usr/bin:/bin`,
ORCA_TEST_LOGIN_SHELL: loginShell,
ORCA_TEST_NPX_BIN: npxBin
}
})
).toBe('skills update orchestration --global:terminal-input')
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
it('reinstalls Windows-host skill updates through the add path', () => {
expect(
buildSkillCommandForRuntime(
@@ -4,9 +4,10 @@ import {
normalizeGlobalWindowsRuntimeDefault
} from '../../../../shared/project-execution-runtime'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows
} from '../../../../shared/wsl-login-shell-command'
quotePowerShellLiteral,
quotePowerShellNativeArgument
} from '../../../../shared/powershell-native-argument'
import { buildWslLoginShellCommand } from '../../../../shared/wsl-login-shell-command'
import { buildAgentFeatureSkillInstallCommand } from '../../../../shared/agent-feature-install-commands'
import { toast } from 'sonner'
import type { CliInstallStatus } from '../../../../shared/cli-install-types'
@@ -56,8 +57,13 @@ export function getSelectedAgentRuntime(
return { runtime: 'host', label: getHostRuntimeLabel() }
}
function quotePowerShellSingle(value: string): string {
return `'${value.replaceAll("'", "''")}'`
function encodeWslLoginShellScript(command: string): string {
const bytes = new TextEncoder().encode(buildWslLoginShellCommand(command))
let binary = ''
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
return btoa(binary)
}
export function getWslCliDistroRequest(
@@ -84,10 +90,17 @@ export function buildSkillCommandForRuntime(
}
const distroArg = resolvedRuntime.wslDistro?.trim()
? ` -d ${quotePowerShellSingle(resolvedRuntime.wslDistro.trim())}`
? ` -d ${quotePowerShellLiteral(resolvedRuntime.wslDistro.trim())}`
: ''
const wslCommand = escapeWslShCommandForWindows(buildWslLoginShellCommand(normalizedCommand))
return `wsl.exe${distroArg} -- sh -c ${quotePowerShellSingle(wslCommand)}`
// Why: encoding preserves the user's configured login-shell PATH while
// avoiding raw multiline and nested quotes at the copy/paste boundary.
const encodedScript = encodeWslLoginShellScript(normalizedCommand)
const visibleCommand = normalizedCommand.replace(/[\r\n]+/g, ' ')
const shellScript = `eval "\`printf %s ${encodedScript} | base64 -d\`"`
const wslCommand = `wsl.exe${distroArg} -- sh -c ${quotePowerShellNativeArgument(shellScript)}`
// Why: scope Legacy argv parsing to this invocation so Windows PowerShell
// 5.1 and PowerShell 7 pass the same embedded quotes to wsl.exe.
return `& { $PSNativeCommandArgumentPassing = 'Legacy'; ${wslCommand} } # Runs: ${visibleCommand}`
}
function normalizeWindowsSkillUpdateCommand(
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { quotePowerShellLiteral, quotePowerShellNativeArgument } from './powershell-native-argument'
describe('PowerShell native argument quoting', () => {
it('escapes literals for PowerShell source parsing', () => {
expect(quotePowerShellLiteral("WSL 'Preview'")).toBe("'WSL ''Preview'''")
})
it('pre-escapes embedded quotes for Windows native argv parsing', () => {
expect(quotePowerShellNativeArgument('eval "decoded"')).toBe(String.raw`'eval \"decoded\"'`)
expect(quotePowerShellNativeArgument(String.raw`before\"after`)).toBe(
String.raw`'before\\\"after'`
)
})
})
+9
View File
@@ -0,0 +1,9 @@
export function quotePowerShellLiteral(value: string): string {
return `'${value.replace(/'/g, "''")}'`
}
export function quotePowerShellNativeArgument(value: string): string {
// Why: Windows PowerShell 5.1 drops unescaped embedded quotes when it
// constructs argv for native executables such as wsl.exe.
return quotePowerShellLiteral(value.replace(/(\\*)"/g, '$1$1\\"'))
}