@@ -449,6 +449,180 @@ public async Task<IActionResult> SaveTextFile(
449449 return Ok ( ) ;
450450 }
451451
452+ private const string UserEnvFilePath = "/etc/profile.d/sdcb-chats-env.sh" ;
453+
454+ [ HttpGet ( "{encryptedSessionId}/environment-variables" ) ]
455+ public async Task < ActionResult < EnvironmentVariablesResponse > > GetEnvironmentVariables (
456+ string encryptedChatId ,
457+ [ Required ] string encryptedSessionId ,
458+ CancellationToken cancellationToken )
459+ {
460+ int chatId = _idEncryption . DecryptChatId ( encryptedChatId ) ;
461+ long sessionId = _idEncryption . DecryptAsInt64 ( encryptedSessionId , EncryptionPurpose . DockerSessionId ) ;
462+ ChatDockerSession ? session = await GetActiveSessionForChat ( chatId , sessionId , cancellationToken ) ;
463+ if ( session == null ) return NotFound ( ) ;
464+
465+ string [ ] shellPrefix = ParseShellPrefixCsv ( session . ShellPrefix ) ;
466+
467+ // Get all environment variables via printenv
468+ CommandExitEvent allEnvResult = await _docker . ExecuteCommandAsync (
469+ session . ContainerId ,
470+ shellPrefix ,
471+ "printenv" ,
472+ _codePodConfig . WorkDir ,
473+ timeoutSeconds : 30 ,
474+ cancellationToken ) ;
475+
476+ Dictionary < string , string > allEnvVars = ParsePrintEnvOutput ( allEnvResult . Stdout ) ;
477+
478+ // Get user environment variables from the user env file
479+ Dictionary < string , string > userEnvVars = [ ] ;
480+ try
481+ {
482+ CommandExitEvent userEnvResult = await _docker . ExecuteCommandAsync (
483+ session . ContainerId ,
484+ shellPrefix ,
485+ $ "cat { UserEnvFilePath } 2>/dev/null || true",
486+ _codePodConfig . WorkDir ,
487+ timeoutSeconds : 30 ,
488+ cancellationToken ) ;
489+
490+ userEnvVars = ParseUserEnvFile ( userEnvResult . Stdout ) ;
491+ }
492+ catch
493+ {
494+ // File may not exist, return empty user variables
495+ }
496+
497+ // Build system variables (exclude user variables)
498+ HashSet < string > userKeys = [ .. userEnvVars . Keys ] ;
499+ List < EnvironmentVariable > systemVariables = allEnvVars
500+ . Where ( kv => ! userKeys . Contains ( kv . Key ) )
501+ . Select ( kv => new EnvironmentVariable ( kv . Key , kv . Value ) )
502+ . OrderBy ( v => v . Key , StringComparer . OrdinalIgnoreCase )
503+ . ToList ( ) ;
504+
505+ List < EnvironmentVariable > userVariables = userEnvVars
506+ . Select ( kv => new EnvironmentVariable ( kv . Key , kv . Value ) )
507+ . OrderBy ( v => v . Key , StringComparer . OrdinalIgnoreCase )
508+ . ToList ( ) ;
509+
510+ await TouchSession ( session . Id , cancellationToken ) ;
511+ return new EnvironmentVariablesResponse ( systemVariables , userVariables ) ;
512+ }
513+
514+ [ HttpPut ( "{encryptedSessionId}/environment-variables" ) ]
515+ public async Task < IActionResult > SaveUserEnvironmentVariables (
516+ string encryptedChatId ,
517+ [ Required ] string encryptedSessionId ,
518+ [ FromBody ] SaveUserEnvironmentVariablesRequest request ,
519+ CancellationToken cancellationToken )
520+ {
521+ if ( ! ModelState . IsValid ) return BadRequest ( ModelState ) ;
522+
523+ int chatId = _idEncryption . DecryptChatId ( encryptedChatId ) ;
524+ long sessionId = _idEncryption . DecryptAsInt64 ( encryptedSessionId , EncryptionPurpose . DockerSessionId ) ;
525+ ChatDockerSession ? session = await GetActiveSessionForChat ( chatId , sessionId , cancellationToken ) ;
526+ if ( session == null ) return NotFound ( ) ;
527+
528+ if ( _codePodConfig . IsWindowsContainer )
529+ {
530+ return BadRequest ( "Saving user environment variables is not supported on Windows containers." ) ;
531+ }
532+
533+ // Validate variable names
534+ foreach ( EnvironmentVariable v in request . Variables )
535+ {
536+ if ( string . IsNullOrWhiteSpace ( v . Key ) )
537+ {
538+ return BadRequest ( "Environment variable key cannot be empty." ) ;
539+ }
540+ if ( ! IsValidEnvVarName ( v . Key ) )
541+ {
542+ return BadRequest ( $ "Invalid environment variable name: { v . Key } . Only alphanumeric characters and underscores are allowed, and it cannot start with a digit.") ;
543+ }
544+ }
545+
546+ // Build the shell script content (use LF line endings for Linux)
547+ StringBuilder sb = new ( ) ;
548+ sb . Append ( "#!/bin/sh\n " ) ;
549+ sb . Append ( "# User environment variables managed by Sdcb Chats\n " ) ;
550+ sb . Append ( "# Do not edit this file manually\n " ) ;
551+ sb . Append ( '\n ' ) ;
552+ foreach ( EnvironmentVariable v in request . Variables )
553+ {
554+ // Escape single quotes in value
555+ string escapedValue = v . Value . Replace ( "'" , "'\" '\" '" ) ;
556+ sb . Append ( $ "export { v . Key } ='{ escapedValue } '\n ") ;
557+ }
558+
559+ string content = sb . ToString ( ) ;
560+ byte [ ] bytes = Encoding . UTF8 . GetBytes ( content ) ;
561+
562+ await _docker . UploadFileAsync ( session . ContainerId , UserEnvFilePath , bytes , cancellationToken ) ;
563+ await TouchSession ( session . Id , cancellationToken ) ;
564+ return Ok ( ) ;
565+ }
566+
567+ private static Dictionary < string , string > ParsePrintEnvOutput ( string output )
568+ {
569+ Dictionary < string , string > result = [ ] ;
570+ foreach ( string line in output . Split ( '\n ' , StringSplitOptions . RemoveEmptyEntries ) )
571+ {
572+ int eqIndex = line . IndexOf ( '=' ) ;
573+ if ( eqIndex > 0 )
574+ {
575+ string key = line [ ..eqIndex ] . Trim ( ) ;
576+ string value = line [ ( eqIndex + 1 ) ..] . TrimEnd ( '\r ' ) ;
577+ result [ key ] = value ;
578+ }
579+ }
580+ return result ;
581+ }
582+
583+ private static Dictionary < string , string > ParseUserEnvFile ( string content )
584+ {
585+ Dictionary < string , string > result = [ ] ;
586+ foreach ( string line in content . Split ( '\n ' , StringSplitOptions . RemoveEmptyEntries ) )
587+ {
588+ string trimmed = line . Trim ( ) ;
589+ if ( trimmed . StartsWith ( "export " , StringComparison . Ordinal ) )
590+ {
591+ // export KEY='value' or export KEY="value" or export KEY=value
592+ string rest = trimmed [ "export " . Length ..] ;
593+ int eqIndex = rest . IndexOf ( '=' ) ;
594+ if ( eqIndex > 0 )
595+ {
596+ string key = rest [ ..eqIndex ] . Trim ( ) ;
597+ string rawValue = rest [ ( eqIndex + 1 ) ..] . Trim ( ) ;
598+ string value = UnquoteValue ( rawValue ) ;
599+ result [ key ] = value ;
600+ }
601+ }
602+ }
603+ return result ;
604+ }
605+
606+ private static string UnquoteValue ( string value )
607+ {
608+ if ( value . Length >= 2 )
609+ {
610+ if ( ( value . StartsWith ( '\' ' ) && value . EndsWith ( '\' ' ) ) ||
611+ ( value . StartsWith ( '"' ) && value . EndsWith ( '"' ) ) )
612+ {
613+ return value [ 1 ..^ 1 ] . Replace ( "'\" '\" '" , "'" ) ;
614+ }
615+ }
616+ return value ;
617+ }
618+
619+ private static bool IsValidEnvVarName ( string name )
620+ {
621+ if ( string . IsNullOrEmpty ( name ) ) return false ;
622+ if ( char . IsDigit ( name [ 0 ] ) ) return false ;
623+ return name . All ( c => char . IsLetterOrDigit ( c ) || c == '_' ) ;
624+ }
625+
452626 private async Task Yield ( CommandStreamLine line , CancellationToken cancellationToken )
453627 {
454628 await Response . Body . WriteAsync ( _dataU8 , cancellationToken ) ;
0 commit comments