1- import { lstat , mkdir , mkdtemp , readFile , rm , writeFile } from "node:fs/promises" ;
1+ import { lstat , mkdir , mkdtemp , readFile , rm , stat , symlink , writeFile } from "node:fs/promises" ;
22import os from "node:os" ;
33import path from "node:path" ;
44import { afterEach , describe , expect , it , vi } from "vitest" ;
@@ -120,7 +120,14 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
120120 async function runTeardown ( input : {
121121 sandboxAuth : string ;
122122 hostAuth : string ;
123- } ) : Promise < { finalHostAuth : string ; finalHostMode : number } > {
123+ externalHostAuth ?: string ;
124+ externalAuthViaSymlink ?: boolean ;
125+ } ) : Promise < {
126+ finalHostAuth : string ;
127+ finalHostMode : number ;
128+ finalSharedHostAuth : string ;
129+ configuredAuthIsSymlink : boolean ;
130+ } > {
124131 const rootDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-copyback-e2e-" ) ) ;
125132 cleanupDirs . push ( rootDir ) ;
126133 const workspaceDir = path . join ( rootDir , "workspace" ) ;
@@ -132,6 +139,20 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
132139 await mkdir ( sharedHostHome , { recursive : true } ) ;
133140 const hostAuthPath = path . join ( sharedHostHome , "auth.json" ) ;
134141 await writeFile ( hostAuthPath , input . hostAuth , { mode : 0o600 } ) ;
142+ const configuredHome = input . externalHostAuth == null
143+ ? sharedHostHome
144+ : path . join ( rootDir , "external-codex-home" ) ;
145+ if ( input . externalHostAuth != null ) {
146+ await mkdir ( configuredHome , { recursive : true } ) ;
147+ const configuredAuthPath = path . join ( configuredHome , "auth.json" ) ;
148+ if ( input . externalAuthViaSymlink ) {
149+ const externalAuthSource = path . join ( rootDir , "external-auth-source.json" ) ;
150+ await writeFile ( externalAuthSource , input . externalHostAuth , { mode : 0o600 } ) ;
151+ await symlink ( externalAuthSource , configuredAuthPath ) ;
152+ } else {
153+ await writeFile ( configuredAuthPath , input . externalHostAuth , { mode : 0o600 } ) ;
154+ }
155+ }
135156
136157 savedCodexHomeEnv = process . env . CODEX_HOME ;
137158 process . env . CODEX_HOME = sharedHostHome ;
@@ -151,8 +172,9 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
151172 command : "codex" ,
152173 engine : "cli" ,
153174 // External CODEX_HOME (outside the managed company tree) so no managed
154- // seeding rewrites auth.json before teardown; equals the shared host home.
155- env : { CODEX_HOME : sharedHostHome } ,
175+ // seeding rewrites auth.json before teardown. Most cases use the shared
176+ // home; multi-subscription coverage supplies a distinct external home.
177+ env : { CODEX_HOME : configuredHome } ,
156178 } ,
157179 context : {
158180 paperclipWorkspace : {
@@ -176,8 +198,10 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
176198 } ) ;
177199
178200 return {
179- finalHostAuth : await readFile ( hostAuthPath , "utf8" ) ,
180- finalHostMode : ( await lstat ( hostAuthPath ) ) . mode & 0o777 ,
201+ finalHostAuth : await readFile ( path . join ( configuredHome , "auth.json" ) , "utf8" ) ,
202+ finalHostMode : ( await stat ( path . join ( configuredHome , "auth.json" ) ) ) . mode & 0o777 ,
203+ finalSharedHostAuth : await readFile ( hostAuthPath , "utf8" ) ,
204+ configuredAuthIsSymlink : ( await lstat ( path . join ( configuredHome , "auth.json" ) ) ) . isSymbolicLink ( ) ,
181205 } ;
182206 }
183207
@@ -211,6 +235,42 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
211235 expect ( result . finalHostMode ) . toBe ( 0o600 ) ;
212236 } ) ;
213237
238+ it . each ( [
239+ { authStorage : "regular file" , externalAuthViaSymlink : false } ,
240+ { authStorage : "symlink source" , externalAuthViaSymlink : true } ,
241+ ] ) (
242+ "round-trips an external CODEX_HOME identity from a $authStorage without overwriting the shared subscription" ,
243+ async ( { externalAuthViaSymlink } ) => {
244+ const sharedHostAuth = subscriptionAuth ( {
245+ accountId : "acct-primary" ,
246+ lastRefresh : "2026-07-09T03:00:00Z" ,
247+ marker : "shared-primary" ,
248+ } ) ;
249+ const externalHostAuth = subscriptionAuth ( {
250+ accountId : "acct-secondary" ,
251+ lastRefresh : "2026-07-09T01:00:00Z" ,
252+ marker : "external-older" ,
253+ } ) ;
254+ const sandboxAuth = subscriptionAuth ( {
255+ accountId : "acct-secondary" ,
256+ lastRefresh : "2026-07-09T02:00:00Z" ,
257+ marker : "external-refreshed" ,
258+ } ) ;
259+
260+ const result = await runTeardown ( {
261+ sandboxAuth,
262+ hostAuth : sharedHostAuth ,
263+ externalHostAuth,
264+ externalAuthViaSymlink,
265+ } ) ;
266+
267+ expect ( result . finalHostAuth ) . toBe ( sandboxAuth ) ;
268+ expect ( result . finalHostMode ) . toBe ( 0o600 ) ;
269+ expect ( result . finalSharedHostAuth ) . toBe ( sharedHostAuth ) ;
270+ expect ( result . configuredAuthIsSymlink ) . toBe ( externalAuthViaSymlink ) ;
271+ } ,
272+ ) ;
273+
214274 it ( "keeps the host auth.json when the sandbox copy is a tie or older on teardown" , async ( ) => {
215275 const cases = [
216276 {
0 commit comments