From 2969ba5ca27bf10998c7aa38893582ce42e510dc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 05:14:36 -0700 Subject: [PATCH] fix(codex): make WSL auth drain transaction recoverable --- ...sl-runtime-auth-drain-apply-script.test.ts | 166 +++++++++++++++--- .../legacy-wsl-runtime-auth-drain-scripts.ts | 140 +++++++++++++++ .../legacy-wsl-runtime-auth-drain.test.ts | 36 +++- .../legacy-wsl-runtime-auth-drain.ts | 79 +-------- 4 files changed, 317 insertions(+), 104 deletions(-) create mode 100644 src/main/codex-accounts/legacy-wsl-runtime-auth-drain-scripts.ts diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts index f1b2ce9f33d..8a176a5960a 100644 --- a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts @@ -1,6 +1,14 @@ import { execFileSync } from 'node:child_process' import { createHash } from 'node:crypto' -import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' @@ -11,8 +19,26 @@ const SOURCE = '{"tokens":{"expires_at":2000}}\n' const TARGET = '{"tokens":{"expires_at":1000}}\n' const NEWER = '{"tokens":{"expires_at":3000}}\n' const sha256 = (value: string) => createHash('sha256').update(value).digest('hex') +type ApplyOutcome = { + firstStatus?: number + marker: boolean + quarantine?: string | null + source: string | null + status: number + target: string +} -function runApply(rewriteAfterHashCall?: number) { +function runApply( + options: { + deleteSource?: boolean + crashBeforeCommit?: boolean + promoteAuth?: boolean + retryAfterCrash?: boolean + rewriteAfterHashCall?: number + rewriteBeforeDelete?: boolean + rewriteSourceBeforeDelete?: boolean + } = {} +): ApplyOutcome { const root = mkdtempSync(join(tmpdir(), 'orca-drain-race-')) const legacy = join(root, 'legacy') const target = join(root, 'target') @@ -38,55 +64,139 @@ if(process.env.REWRITE && n===Number(process.env.REWRITE)) fs.writeFileSync(proc ` ) chmodSync(shim, 0o755) - let status = 0 - try { - execFileSync( - '/bin/sh', - [ - '-c', - _internals.applyLegacyAuthScript, - 'sh', - legacy, - join(root, 'active'), - marker, - target, - sha256(SOURCE), - sha256(TARGET), - '0', - '1', - 'missing' - ], - { + const mvShim = join(bin, 'mv') + writeFileSync( + mvShim, + `#!/usr/bin/env node +const { spawnSync } = require('node:child_process') +const fs = require('node:fs') +const args = process.argv.slice(2) +const retiresSource = args.at(-2)?.endsWith('/legacy/auth.json') +if (process.env.REWRITE_BEFORE_DELETE === '1' && retiresSource) { + const replacement = process.env.TARGET + '.replacement' + fs.writeFileSync(replacement, process.env.BYTES) + fs.renameSync(replacement, process.env.TARGET) +} +if (process.env.REWRITE_SOURCE_BEFORE_DELETE === '1' && retiresSource) { + const source = args.at(-2) + const replacement = source + '.replacement' + fs.writeFileSync(replacement, process.env.BYTES) + fs.renameSync(replacement, source) +} +const result = spawnSync('/bin/mv', args, { stdio: 'inherit' }) +if (result.status === 0 && process.env.CRASH_BEFORE_COMMIT === '1' && retiresSource) { + process.kill(process.ppid, 'SIGKILL') +} +process.exit(result.status ?? 1) +` + ) + chmodSync(mvShim, 0o755) + const applyArgs = [ + '-c', + _internals.applyLegacyAuthScript, + 'sh', + legacy, + join(root, 'active'), + marker, + target, + sha256(SOURCE), + sha256(TARGET), + options.promoteAuth ? '1' : '0', + options.deleteSource === false ? '0' : '1', + 'missing' + ] + const apply = (crashBeforeCommit: boolean): number => { + try { + execFileSync('/bin/sh', applyArgs, { env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ''}`, COUNTER: counter, - REWRITE: rewriteAfterHashCall ? String(rewriteAfterHashCall) : '', + CRASH_BEFORE_COMMIT: crashBeforeCommit ? '1' : '', + REWRITE: options.rewriteAfterHashCall ? String(options.rewriteAfterHashCall) : '', + REWRITE_BEFORE_DELETE: options.rewriteBeforeDelete ? '1' : '', + REWRITE_SOURCE_BEFORE_DELETE: options.rewriteSourceBeforeDelete ? '1' : '', TARGET: targetPath, BYTES: NEWER }, stdio: 'ignore' - } - ) - } catch (error) { - status = (error as { status?: number }).status ?? -1 + }) + return 0 + } catch (error) { + return (error as { status?: number }).status ?? -1 + } } - return { + const firstStatus = apply(Boolean(options.crashBeforeCommit)) + let status = firstStatus + if (options.retryAfterCrash) { + execFileSync( + '/bin/sh', + ['-c', _internals.inspectLegacyAuthScript, 'sh', legacy, join(root, 'active'), marker], + { env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ''}` }, stdio: 'ignore' } + ) + writeFileSync(counter, '0') + status = apply(false) + } + const outcome = { status, source: existsSync(sourcePath) ? readFileSync(sourcePath, 'utf8') : null, target: readFileSync(targetPath, 'utf8'), marker: existsSync(marker) } + const quarantine = `${marker}.orca-drain-live-source` + const quarantineContents = existsSync(quarantine) ? readFileSync(quarantine, 'utf8') : null + rmSync(root, { recursive: true, force: true }) + return { + ...outcome, + ...(options.retryAfterCrash ? { firstStatus } : {}), + ...(options.rewriteSourceBeforeDelete ? { quarantine: quarantineContents } : {}) + } } describe.skipIf(isWindows)('legacy WSL auth drain race guard', () => { it('retires an unchanged source', () => expect(runApply()).toEqual({ status: 0, source: null, target: TARGET, marker: true })) + it('promotes the verified source before retiring it', () => + expect(runApply({ promoteAuth: true })).toEqual({ + status: 0, + source: null, + target: SOURCE, + marker: true + })) + it('retains the source and marker while a legacy pane is live', () => + expect(runApply({ deleteSource: false, promoteAuth: true })).toEqual({ + status: 0, + source: SOURCE, + target: SOURCE, + marker: false + })) it('retains source when destination is rewritten during validation', () => { - const outcome = runApply(3) + const outcome = runApply({ rewriteAfterHashCall: 3 }) expect(outcome.status).toBe(45) expect(outcome.source).toBe(SOURCE) expect(outcome.target).toBe(NEWER) expect(outcome.marker).toBe(false) }) + it('retains source when destination is rewritten immediately before deletion', () => { + const outcome = runApply({ rewriteBeforeDelete: true }) + expect(outcome.status).not.toBe(0) + expect(outcome.source).toBe(SOURCE) + expect(outcome.target).toBe(NEWER) + expect(outcome.marker).toBe(false) + }) + it('restores verified source bytes when the source path is atomically replaced', () => { + const outcome = runApply({ rewriteSourceBeforeDelete: true }) + expect(outcome.status).toBe(40) + expect(outcome.source).toBe(SOURCE) + expect(outcome.quarantine).toBe(NEWER) + expect(outcome.marker).toBe(false) + }) + it('recovers and retries after interruption before the completion marker', () => { + const outcome = runApply({ crashBeforeCommit: true, retryAfterCrash: true }) + expect(outcome.firstStatus).not.toBe(0) + expect(outcome.status).toBe(0) + expect(outcome.source).toBeNull() + expect(outcome.target).toBe(TARGET) + expect(outcome.marker).toBe(true) + }) }) diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-scripts.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-scripts.ts new file mode 100644 index 00000000000..884bc85bbcb --- /dev/null +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain-scripts.ts @@ -0,0 +1,140 @@ +export const MARKER_PRESENT_EXIT = 20 +export const SOURCE_AUTH_ABSENT_EXIT = 21 + +const RESOLVE_LEGACY_HOME_SCRIPT = ` +legacy_home="$1" +legacy_home_resolved=0 +if [ -e "$1" ] || [ -L "$1" ]; then legacy_home=$(readlink -f -- "$1") || exit 30; legacy_home_resolved=1; fi +if [ -e "$2" ] || [ -L "$2" ]; then active_home=$(readlink -f -- "$2") || exit 31; if [ "$legacy_home_resolved" = 1 ]; then [ "$active_home" = "$legacy_home" ] || exit 32; else legacy_home="$active_home"; legacy_home_resolved=1; fi; fi +` + +export const INSPECT_LEGACY_AUTH_SCRIPT = ` +set -eu +source_recovery_auth="$3.orca-drain-source" +source_quarantine_auth="$3.orca-drain-live-source" +destination_recovery_auth="$3.orca-drain-destination" +if [ -e "$3" ] || [ -L "$3" ]; then + [ -f "$3" ] && [ ! -L "$3" ] || exit 46 + if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then chmod 600 "$destination_recovery_auth"; fi + rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" + exit ${MARKER_PRESENT_EXIT} +fi +[ ! -e "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || { [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46; chmod 600 "$destination_recovery_auth"; } +${RESOLVE_LEGACY_HOME_SCRIPT} +source_auth="$legacy_home/auth.json" +if [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then + if [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ]; then + mv -- "$source_recovery_auth" "$source_auth"; chmod 600 "$source_auth" + elif [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ]; then + mv -- "$source_quarantine_auth" "$source_auth"; chmod 600 "$source_auth" + elif [ -e "$source_quarantine_auth" ] || [ -L "$source_quarantine_auth" ] || [ -e "$source_recovery_auth" ] || [ -L "$source_recovery_auth" ]; then + exit 46 + fi +fi +[ -f "$source_auth" ] || exit ${SOURCE_AUTH_ABSENT_EXIT} +encode_file() { base64 < "$1" | tr -d '\\n'; } +encode_file "$source_auth"; printf '\\n' +source_credentials="$legacy_home/.credentials.json" +if [ -f "$source_credentials" ]; then printf 'present\\n'; encode_file "$source_credentials"; printf '\\n'; elif [ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ]; then printf 'missing\\n\\n'; else exit 44; fi +` + +// Freeze a verified destination snapshot before source quarantine so the +// marker commits only while both credential copies remain recoverable. +export const APPLY_LEGACY_AUTH_SCRIPT = ` +set -eu +source_recovery_auth="$3.orca-drain-source" +source_quarantine_auth="$3.orca-drain-live-source" +destination_recovery_auth="$3.orca-drain-destination" +if [ -e "$3" ] || [ -L "$3" ]; then + [ -f "$3" ] && [ ! -L "$3" ] || exit 46 + if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then chmod 600 "$destination_recovery_auth"; fi + rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" + exit 0 +fi +${RESOLVE_LEGACY_HOME_SCRIPT} +target_home=$(readlink -f -- "$4") || exit 33 +[ "$legacy_home" != "$target_home" ] || exit 34 +source_auth="$legacy_home/auth.json"; target_auth="$target_home/auth.json" +[ -f "$source_auth" ] || exit 35; [ -f "$target_auth" ] || exit 36 +hash_file() { sha256sum -- "$1" | cut -d ' ' -f 1; } +if [ -e "$source_quarantine_auth" ] || [ -L "$source_quarantine_auth" ]; then + [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ] || exit 46 + [ "$(hash_file "$source_auth")" = "$5" ] || exit 37 + rm -- "$source_quarantine_auth" +fi +if [ -e "$source_recovery_auth" ] || [ -L "$source_recovery_auth" ]; then + [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ] || exit 46 + [ "$(hash_file "$source_auth")" = "$5" ] || exit 37 + rm -- "$source_recovery_auth" +fi +if [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ]; then + [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46 + if [ "$target_auth" -ef "$destination_recovery_auth" ]; then chmod 600 "$target_auth"; fi + rm -- "$destination_recovery_auth" +fi +[ "$(hash_file "$source_auth")" = "$5" ] || exit 37 +[ "$(hash_file "$target_auth")" = "$6" ] || exit 38 +umask 077 +temporary_auth="$target_auth.orca-drain-$$"; temporary_credentials="$target_home/.credentials.json.orca-drain-$$"; destination_pin="$target_auth.orca-drain-pin-$$"; temporary_destination_snapshot="$target_auth.orca-drain-snapshot-$$"; temporary_source_snapshot="$source_auth.orca-drain-source-$$"; temporary_marker="$3.orca-drain-$$" +drain_marker="$3"; expected_source_hash="$5" +cleanup() { + if [ ! -f "$drain_marker" ] && [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then + if [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ] && [ "$(hash_file "$source_recovery_auth")" = "$expected_source_hash" ]; then mv -- "$source_recovery_auth" "$source_auth" || :; chmod 600 "$source_auth" || : + elif [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ] && [ "$(hash_file "$source_quarantine_auth")" = "$expected_source_hash" ]; then mv -- "$source_quarantine_auth" "$source_auth" || :; chmod 600 "$source_auth" || :; fi + fi + if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then + if [ "$target_auth" -ef "$destination_recovery_auth" ]; then chmod 600 "$target_auth" || :; fi + [ ! -f "$drain_marker" ] || rm -f -- "$destination_recovery_auth" + fi + [ ! -f "$drain_marker" ] || rm -f -- "$source_recovery_auth" "$source_quarantine_auth" + rm -f -- "$temporary_auth" "$temporary_credentials" "$destination_pin" "$temporary_destination_snapshot" "$temporary_source_snapshot" "$temporary_marker" +} +trap cleanup EXIT HUP INT TERM +ln -- "$target_auth" "$destination_pin" +[ "$(hash_file "$destination_pin")" = "$6" ] || exit 38 +[ "$target_auth" -ef "$destination_pin" ] || exit 38 +source_credentials="$legacy_home/.credentials.json"; target_credentials="$target_home/.credentials.json" +if [ -f "$source_credentials" ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then + [ "$9" != missing ] || exit 43; [ "$(hash_file "$source_credentials")" = "$9" ] || exit 43 + cp -- "$source_credentials" "$temporary_credentials"; chmod 600 "$temporary_credentials"; [ "$(hash_file "$temporary_credentials")" = "$9" ] || exit 43; [ "$(hash_file "$source_credentials")" = "$9" ] || exit 43; mv -n -- "$temporary_credentials" "$target_credentials"; [ -f "$target_credentials" ] && [ ! -L "$target_credentials" ] && [ "$(hash_file "$target_credentials")" = "$9" ] || exit 43 +elif [ "$9" = missing ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then + [ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ] || exit 43 +fi +if [ "$7" = 1 ]; then + cp -- "$source_auth" "$temporary_auth"; chmod 600 "$temporary_auth"; [ "$(hash_file "$temporary_auth")" = "$5" ] || exit 42 + [ "$(hash_file "$destination_pin")" = "$6" ] || exit 39; [ "$target_auth" -ef "$destination_pin" ] || exit 39 + mv -f -- "$temporary_auth" "$target_auth" + [ "$(hash_file "$destination_pin")" = "$6" ] || exit 39 + rm -- "$destination_pin"; ln -- "$target_auth" "$destination_pin" +fi +expected_target_hash="$6"; [ "$7" != 1 ] || expected_target_hash="$5" +[ "$(hash_file "$destination_pin")" = "$expected_target_hash" ] || exit 45 +[ "$target_auth" -ef "$destination_pin" ] || exit 45 +if [ "$8" != 1 ]; then exit 0; fi +cp -- "$target_auth" "$temporary_destination_snapshot"; chmod 400 "$temporary_destination_snapshot"; [ ! -w "$temporary_destination_snapshot" ] || exit 45; [ "$(hash_file "$temporary_destination_snapshot")" = "$expected_target_hash" ] || exit 45 +cp -- "$source_auth" "$temporary_source_snapshot"; chmod 400 "$temporary_source_snapshot"; [ ! -w "$temporary_source_snapshot" ] || exit 40; [ "$(hash_file "$temporary_source_snapshot")" = "$5" ] || exit 40 +mv -f -- "$temporary_source_snapshot" "$source_recovery_auth"; [ "$(hash_file "$source_recovery_auth")" = "$5" ] || exit 40 +ln -- "$temporary_destination_snapshot" "$destination_recovery_auth"; [ "$temporary_destination_snapshot" -ef "$destination_recovery_auth" ] || exit 45 +[ "$(hash_file "$destination_pin")" = "$expected_target_hash" ] || exit 45; [ "$target_auth" -ef "$destination_pin" ] || exit 45 +mv -f -- "$temporary_destination_snapshot" "$target_auth" +[ "$(hash_file "$destination_pin")" = "$expected_target_hash" ] || exit 45 +[ ! -w "$target_auth" ] || exit 45 +[ "$(hash_file "$target_auth")" = "$expected_target_hash" ] || exit 45 +[ "$target_auth" -ef "$destination_recovery_auth" ] || exit 45 +[ "$(hash_file "$source_auth")" = "$5" ] || exit 40 +mv -- "$source_auth" "$source_quarantine_auth"; chmod 400 "$source_quarantine_auth"; [ ! -w "$source_quarantine_auth" ] || exit 40 +[ "$(hash_file "$source_quarantine_auth")" = "$5" ] || exit 40 +[ "$(hash_file "$target_auth")" = "$expected_target_hash" ] || exit 45 +[ "$target_auth" -ef "$destination_recovery_auth" ] || exit 45 +printf '%s\\n' '{"completed":true}' > "$temporary_marker"; chmod 600 "$temporary_marker"; mv -f -- "$temporary_marker" "$3" +chmod 600 "$destination_recovery_auth" +rm -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" +` + +export const FINALIZE_ABSENT_AUTH_SCRIPT = ` +set -eu +[ ! -f "$3" ] || exit 0 +${RESOLVE_LEGACY_HOME_SCRIPT} +[ ! -e "$legacy_home/auth.json" ] && [ ! -L "$legacy_home/auth.json" ] || exit 41 +umask 077; marker_parent=\${3%/*}; mkdir -p -- "$marker_parent"; temporary_marker="$3.orca-drain-$$"; trap 'rm -f -- "$temporary_marker"' EXIT HUP INT TERM; printf '%s\\n' '{"completed":true}' > "$temporary_marker"; chmod 600 "$temporary_marker"; mv -f -- "$temporary_marker" "$3" +` diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts index 201821a5daf..80440d64a09 100644 --- a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts @@ -70,6 +70,20 @@ describe('legacy WSL runtime auth drain', () => { expect(runWslProcessMock.mock.calls[1][0].args.slice(-3)).toEqual(['0', '1', 'missing']) }) + it('does not expose captured auth output when the guest apply fails', async () => { + runWslProcessMock + .mockResolvedValueOnce(result(0, inspection(SOURCE))) + .mockResolvedValueOnce(result(45, 'secret-auth-output')) + const operation = drainLegacyWslRuntimeAuth({ + distro: 'Ubuntu', + guestHomeLinuxPath: '/home/alice', + legacyPanePresent: false, + resolveDestination: () => ({ authContents: STALE, linuxHomePath: '/home/alice/.codex' }) + }) + await expect(operation).rejects.toThrow('Legacy WSL auth drain apply failed (exit 45)') + await expect(operation).rejects.not.toThrow('secret-auth-output') + }) + it('coalesces concurrent launches and allows idempotent retries', async () => { let release!: (value: ReturnType) => void runWslProcessMock.mockImplementationOnce( @@ -89,12 +103,11 @@ describe('legacy WSL runtime auth drain', () => { await Promise.resolve() expect(runWslProcessMock).toHaveBeenCalledTimes(1) release(result(0, inspection(SOURCE))) - await Promise.resolve() - await Promise.resolve() runWslProcessMock.mockResolvedValueOnce(result(0, inspection(SOURCE))) - startLegacyWslRuntimeAuthDrain(options) - await Promise.resolve() - expect(runWslProcessMock).toHaveBeenCalledTimes(1) + await vi.waitFor(() => { + startLegacyWslRuntimeAuthDrain(options) + expect(runWslProcessMock).toHaveBeenCalledTimes(2) + }) }) it('is idempotent when the completion marker is already present', async () => { @@ -110,4 +123,17 @@ describe('legacy WSL runtime auth drain', () => { ).resolves.toBe('complete') expect(resolveDestination).not.toHaveBeenCalled() }) + + it('does not share completion across distinct guest homes in one distro', async () => { + runWslProcessMock.mockResolvedValue(result(20)) + const options = { + distro: 'Ubuntu', + legacyPanePresent: false, + resolveDestination: vi.fn() + } + startLegacyWslRuntimeAuthDrain({ ...options, guestHomeLinuxPath: '/home/alice' }) + await vi.waitFor(() => expect(runWslProcessMock).toHaveBeenCalledTimes(1)) + startLegacyWslRuntimeAuthDrain({ ...options, guestHomeLinuxPath: '/home/bob' }) + await vi.waitFor(() => expect(runWslProcessMock).toHaveBeenCalledTimes(2)) + }) }) diff --git a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts index 2fcd8d3ad9c..6395e52a9e5 100644 --- a/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts +++ b/src/main/codex-accounts/legacy-wsl-runtime-auth-drain.ts @@ -3,10 +3,15 @@ import { posix as pathPosix } from 'node:path' import { WSL_CODEX_RUNTIME_HOME_SEGMENTS } from '../pty/codex-home-wsl-env' import { runWslProcess } from '../wsl/wsl-runner' import { compareCodexAuthFreshness, codexAuthIsFresher } from './codex-auth-identity' +import { + APPLY_LEGACY_AUTH_SCRIPT, + FINALIZE_ABSENT_AUTH_SCRIPT, + INSPECT_LEGACY_AUTH_SCRIPT, + MARKER_PRESENT_EXIT, + SOURCE_AUTH_ABSENT_EXIT +} from './legacy-wsl-runtime-auth-drain-scripts' const DRAIN_MARKER_NAME = 'direct-home-auth-drain-v1.json' -const MARKER_PRESENT_EXIT = 20 -const SOURCE_AUTH_ABSENT_EXIT = 21 export type LegacyWslRuntimeAuthDestination = { authContents: string; linuxHomePath: string } type Inspection = { @@ -26,7 +31,7 @@ const inFlight = new Map>() const complete = new Set() export function startLegacyWslRuntimeAuthDrain(options: LegacyWslRuntimeAuthDrainOptions): void { - const key = options.distro.trim().toLowerCase() + const key = `${options.distro.trim().toLowerCase()}\0${pathPosix.normalize(options.guestHomeLinuxPath)}` if (complete.has(key) || inFlight.has(key)) { return } @@ -174,74 +179,6 @@ function sha256(contents: string): string { return createHash('sha256').update(contents).digest('hex') } -const RESOLVE_LEGACY_HOME_SCRIPT = ` -legacy_home="$1" -legacy_home_resolved=0 -if [ -e "$1" ] || [ -L "$1" ]; then legacy_home=$(readlink -f -- "$1") || exit 30; legacy_home_resolved=1; fi -if [ -e "$2" ] || [ -L "$2" ]; then active_home=$(readlink -f -- "$2") || exit 31; if [ "$legacy_home_resolved" = 1 ]; then [ "$active_home" = "$legacy_home" ] || exit 32; else legacy_home="$active_home"; legacy_home_resolved=1; fi; fi -` -const INSPECT_LEGACY_AUTH_SCRIPT = ` -set -eu -[ ! -f "$3" ] || exit ${MARKER_PRESENT_EXIT} -${RESOLVE_LEGACY_HOME_SCRIPT} -source_auth="$legacy_home/auth.json" -[ -f "$source_auth" ] || exit ${SOURCE_AUTH_ABSENT_EXIT} -encode_file() { base64 < "$1" | tr -d '\\n'; } -encode_file "$source_auth"; printf '\\n' -source_credentials="$legacy_home/.credentials.json" -if [ -f "$source_credentials" ]; then printf 'present\\n'; encode_file "$source_credentials"; printf '\\n'; elif [ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ]; then printf 'missing\\n\\n'; else exit 44; fi -` - -// The destination pin is a hard link to the exact inode being validated. A -// concurrent in-place rewrite changes the pin; an atomic replacement breaks -// -ef. Either case aborts before source retirement, preserving credentials. -const APPLY_LEGACY_AUTH_SCRIPT = ` -set -eu -[ ! -f "$3" ] || exit 0 -${RESOLVE_LEGACY_HOME_SCRIPT} -target_home=$(readlink -f -- "$4") || exit 33 -[ "$legacy_home" != "$target_home" ] || exit 34 -source_auth="$legacy_home/auth.json"; target_auth="$target_home/auth.json" -[ -f "$source_auth" ] || exit 35; [ -f "$target_auth" ] || exit 36 -hash_file() { sha256sum -- "$1" | cut -d ' ' -f 1; } -[ "$(hash_file "$source_auth")" = "$5" ] || exit 37 -[ "$(hash_file "$target_auth")" = "$6" ] || exit 38 -umask 077 -temporary_auth="$target_auth.orca-drain-$$"; temporary_credentials="$target_home/.credentials.json.orca-drain-$$"; destination_pin="$target_auth.orca-drain-pin-$$"; temporary_marker="$3.orca-drain-$$" -cleanup() { rm -f -- "$temporary_auth" "$temporary_credentials" "$destination_pin" "$temporary_marker"; } -trap cleanup EXIT HUP INT TERM -source_credentials="$legacy_home/.credentials.json"; target_credentials="$target_home/.credentials.json" -if [ -f "$source_credentials" ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then - [ "$9" != missing ] || exit 43; [ "$(hash_file "$source_credentials")" = "$9" ] || exit 43 - cp -- "$source_credentials" "$temporary_credentials"; chmod 600 "$temporary_credentials"; [ "$(hash_file "$temporary_credentials")" = "$9" ] || exit 43; [ "$(hash_file "$source_credentials")" = "$9" ] || exit 43; mv -n -- "$temporary_credentials" "$target_credentials" -elif [ "$9" = missing ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then - [ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ] || exit 43 -fi -if [ "$7" = 1 ]; then - cp -- "$source_auth" "$temporary_auth"; chmod 600 "$temporary_auth"; [ "$(hash_file "$temporary_auth")" = "$5" ] || exit 42 - [ "$(hash_file "$target_auth")" = "$6" ] || exit 39 - mv -f -- "$temporary_auth" "$target_auth" -fi -ln -- "$target_auth" "$destination_pin" -expected_target_hash="$6"; [ "$7" != 1 ] || expected_target_hash="$5" -[ "$(hash_file "$destination_pin")" = "$expected_target_hash" ] || exit 45 -[ "$target_auth" -ef "$destination_pin" ] || exit 45 -if [ "$8" = 1 ]; then - [ "$(hash_file "$destination_pin")" = "$expected_target_hash" ] || exit 45 - [ "$target_auth" -ef "$destination_pin" ] || exit 45 - [ "$(hash_file "$source_auth")" = "$5" ] || exit 40 - rm -- "$source_auth" - printf '%s\\n' '{"completed":true}' > "$temporary_marker"; chmod 600 "$temporary_marker"; mv -f -- "$temporary_marker" "$3" -fi -` -const FINALIZE_ABSENT_AUTH_SCRIPT = ` -set -eu -[ ! -f "$3" ] || exit 0 -${RESOLVE_LEGACY_HOME_SCRIPT} -[ ! -e "$legacy_home/auth.json" ] && [ ! -L "$legacy_home/auth.json" ] || exit 41 -umask 077; marker_parent=\${3%/*}; mkdir -p -- "$marker_parent"; temporary_marker="$3.orca-drain-$$"; trap 'rm -f -- "$temporary_marker"' EXIT HUP INT TERM; printf '%s\\n' '{"completed":true}' > "$temporary_marker"; chmod 600 "$temporary_marker"; mv -f -- "$temporary_marker" "$3" -` - export const _internals = { applyLegacyAuthScript: APPLY_LEGACY_AUTH_SCRIPT, inspectLegacyAuthScript: INSPECT_LEGACY_AUTH_SCRIPT,