Environment Variables Import Feature Design
Overview
This design document outlines a comprehensive solution for importing environment variable definitions from local filesystem or remote URLs into the Rossoctl UI, with support for standard .env files and extended JSON syntax for Kubernetes secret/configMap references.
Requirements
- Import
.envfiles from local filesystem or remote URLs - Extended
.envformat to support JSON values forsecretKeyRefandconfigMapKeyRef - Display imported variables in the "Environment Variables" section (editable)
- Delete functionality for individual environment variables
- Support standard and extended formats simultaneously
Example .env File Format
Standard Format
MCP_URL=http://weather-tool:8080/mcp
LLM_MODEL=llama3.2
PORT=8000
Extended Format (Kubernetes References)
When you need to reference values stored in Kubernetes Secrets or ConfigMaps, use JSON format for the value. The JSON must be enclosed in single quotes to prevent shell interpretation.
Format Rules:
- The entire JSON object must be enclosed in single quotes (
'...') - Use double quotes for JSON keys and string values
- Do not add spaces around the
=sign (standard .env convention) - JSON must be valid and properly escaped
Secret Reference Format:
# Reference a key from a Kubernetes Secret
VARIABLE_NAME='{"valueFrom": {"secretKeyRef": {"name": "secret-name", "key": "key-name"}}}'
ConfigMap Reference Format:
# Reference a key from a Kubernetes ConfigMap
VARIABLE_NAME='{"valueFrom": {"configMapKeyRef": {"name": "configmap-name", "key": "key-name"}}}'
Complete Example:
# Standard direct values
MCP_URL=http://weather-tool:8080/mcp
PORT=8000
LOG_LEVEL=INFO
# Reference to OpenAI API key stored in a Secret
OPENAI_API_KEY='{"valueFrom": {"secretKeyRef": {"name": "openai-secret", "key": "apikey"}}}'
# Reference to LLM API key from the same Secret
LLM_API_KEY='{"valueFrom": {"secretKeyRef": {"name": "openai-secret", "key": "apikey"}}}'
# Reference to configuration from a ConfigMap
APP_CONFIG='{"valueFrom": {"configMapKeyRef": {"name": "app-settings", "key": "config.json"}}}'
Important Notes:
- The Secret or ConfigMap must exist in the same namespace as your agent
- The agent's ServiceAccount must have permission to read the referenced Secret/ConfigMap
- If the referenced key doesn't exist, the pod will fail to start
- You can mix standard values and references in the same
.envfile
Architecture
Component Structure
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React/TypeScript) │
├─────────────────────────────────────────────────────────────┤
│ ImportAgentPage.tsx │
│ ├── EnvImportModal (new component) │
│ │ ├── File upload (local) │
│ │ ├── URL input (remote) │
│ │ └── Preview & validation │
│ │ │
│ └── EnvironmentVariablesSection (enhanced) │
│ ├── Existing manual entry │
│ ├── Import button → EnvImportModal │
│ ├── Edit capabilities │
│ └── Delete functionality │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Backend (FastAPI/Python) │
├─────────────────────────────────────────────────────────────┤
│ /api/agents/parse-env (new endpoint) │
│ ├── Parse .env content │
│ ├── Validate JSON values │
│ └── Return structured EnvVar list │
│ │
│ /api/agents/fetch-env-url (new endpoint) │
│ ├── Fetch from remote URL │
│ ├── Security validations │
│ └── Return file content │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Deployment │
├─────────────────────────────────────────────────────────────┤
│ spec: │
│ template: │
│ spec: │
│ containers: │
│ - env: │
│ - name: MCP_URL │
│ value: "http://weather-tool:8080/mcp" │
│ - name: SECRET_KEY │
│ valueFrom: │
│ secretKeyRef: │
│ name: openai-secret │
│ key: apikey │
└─────────────────────────────────────────────────────────────┘
Detailed Implementation Design
1. Frontend Components
1.1 New Component: EnvImportModal.tsx
Location: /Users/paolo/Projects/aiplatform/rossoctl/rossoctl/ui-v2/src/components/EnvImportModal.tsx
Purpose: Modal dialog for importing environment variables from file or URL
Features:
- Tab interface: "Upload File" and "From URL"
- File upload with drag-and-drop support
- URL input with fetch button
- Preview section showing parsed variables
- Validation feedback
- Import/Cancel actions
Key Props:
interface EnvImportModalProps {
isOpen: boolean;
onClose: () => void;
onImport: (envVars: EnvVar[]) => void;
}
State Management:
const [activeTab, setActiveTab] = useState<'file' | 'url'>('file');
const [fileContent, setFileContent] = useState<string>('');
const [url, setUrl] = useState<string>('');
const [previewVars, setPreviewVars] = useState<EnvVar[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
1.2 Enhanced Data Model: EnvVar Interface
Location: Update in /Users/paolo/Projects/aiplatform/rossoctl/rossoctl/ui-v2/src/pages/ImportAgentPage.tsx
Enhanced Interface:
interface EnvVar {
name: string;
value?: string; // For simple values
valueFrom?: { // For Kubernetes references
secretKeyRef?: {
name: string;
key: string;
};
configMapKeyRef?: {
name: string;
key: string;
};
};
}
1.3 Enhanced: Environment Variables Section in ImportAgentPage.tsx
Updates Required:
-
Add Import Button:
<Buttonvariant="secondary"icon={<UploadIcon />}onClick={() => setShowImportModal(true)}>Import from File/URL</Button> -
Enhanced Display Logic:
- Show "Value" column for simple values
- Show "Source" column indicating: "Value", "Secret", or "ConfigMap"
- Show reference details for Kubernetes sources
- Add delete button for each row
-
Edit Functionality:
- Allow switching between value types
- Provide dropdown to select: "Direct Value", "Secret Reference", "ConfigMap Reference"
- Conditional form fields based on selection
Enhanced UI Structure:
{envVars.map((env, index) => (
<Grid hasGutter key={index}>
<GridItem span={3}>
<TextInput
value={env.name}
onChange={(_e, value) => updateEnvVar(index, 'name', value)}
placeholder="VAR_NAME"
/>
</GridItem>
<GridItem span={2}>
<FormSelect
value={getEnvVarType(env)}
onChange={(_e, value) => handleTypeChange(index, value)}
>
<FormSelectOption value="value" label="Direct Value" />
<FormSelectOption value="secret" label="Secret" />
<FormSelectOption value="configMap" label="ConfigMap" />
</FormSelect>
</GridItem>
<GridItem span={6}>
{renderValueInput(env, index)}
</GridItem>
<GridItem span={1}>
<Button
variant="plain"
onClick={() => removeEnvVar(index)}
>
<TrashIcon />
</Button>
</GridItem>
</Grid>
))}
2. Backend API Endpoints
2.1 New Endpoint: POST /api/agents/parse-env
Purpose: Parse .env file content and return structured environment variables
Request Body:
class ParseEnvRequest(BaseModel):
content: str # Raw .env file content
Response:
class ParseEnvResponse(BaseModel):
envVars: List[EnvVar]
warnings: Optional[List[str]] = None # Parsing warnings
Implementation Logic:
@router.post("/parse-env", response_model=ParseEnvResponse)
async def parse_env_file(request: ParseEnvRequest) -> ParseEnvResponse:
"""
Parse .env file content and return structured environment variables.
Supports:
- Standard KEY=value format
- Extended JSON format for secretKeyRef and configMapKeyRef
"""
env_vars = []
warnings = []
lines = request.content.strip().split('\n')
for line_num, line in enumerate(lines, 1):
# Skip empty lines and comments
line = line.strip()
if not line or line.startswith('#'):
continue
# Parse KEY=VALUE
if '=' not in line:
warnings.append(f"Line {line_num}: Invalid format, missing '='")
continue
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
# Try to parse as JSON (for extended format)
if value.startswith('{') and value.endswith('}'):
try:
parsed = json.loads(value)
if 'valueFrom' in parsed:
env_var = {'name': key, 'valueFrom': parsed['valueFrom']}
env_vars.append(env_var)
continue
except json.JSONDecodeError:
warnings.append(f"Line {line_num}: Invalid JSON in value")
# Standard value
env_vars.append({'name': key, 'value': value})
return ParseEnvResponse(envVars=env_vars, warnings=warnings if warnings else None)
2.2 New Endpoint: POST /api/agents/fetch-env-url
Purpose: Fetch .env file from remote URL
Request Body:
class FetchEnvUrlRequest(BaseModel):
url: str
Response:
class FetchEnvUrlResponse(BaseModel):
content: str
url: str
Implementation Logic:
import httpx
@router.post("/fetch-env-url", response_model=FetchEnvUrlResponse)
async def fetch_env_from_url(request: FetchEnvUrlRequest) -> FetchEnvUrlResponse:
"""
Fetch .env file content from a remote URL.
Supports HTTP/HTTPS URLs with basic security validations.
"""
# Security validation
parsed_url = urlparse(request.url)
if parsed_url.scheme not in ['http', 'https']:
raise HTTPException(
status_code=400,
detail="Only HTTP/HTTPS URLs are supported"
)
# Prevent SSRF attacks - block private IPs
try:
ip = socket.gethostbyname(parsed_url.hostname)
if ipaddress.ip_address(ip).is_private:
raise HTTPException(
status_code=400,
detail="Private IP addresses are not allowed"
)
except socket.gaierror:
pass # Allow domain names that can't be resolved yet
# Fetch content
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(request.url)
response.raise_for_status()
# Validate content type (optional)
content_type = response.headers.get('content-type', '')
if 'text' not in content_type and 'application/octet-stream' not in content_type:
logging.warning(f"Unexpected content-type: {content_type}")
return FetchEnvUrlResponse(
content=response.text,
url=request.url
)
except httpx.HTTPError as e:
raise HTTPException(
status_code=502,
detail=f"Failed to fetch URL: {str(e)}"
)
2.3 Update: Enhanced EnvVar Model
Location: /Users/paolo/Projects/aiplatform/rossoctl/rossoctl/backend/app/routers/agents.py
Enhanced Model:
from typing import Optional, Dict, Any
class SecretKeyRef(BaseModel):
"""Reference to a key in a Secret."""
name: str
key: str
class ConfigMapKeyRef(BaseModel):
"""Reference to a key in a ConfigMap."""
name: str
key: str
class EnvVarSource(BaseModel):
"""Source for environment variable value."""
secretKeyRef: Optional[SecretKeyRef] = None
configMapKeyRef: Optional[ConfigMapKeyRef] = None
class EnvVar(BaseModel):
"""Environment variable with support for direct values and references."""
name: str
value: Optional[str] = None
valueFrom: Optional[EnvVarSource] = None
@validator('valueFrom', 'value', always=True)
def check_value_or_value_from(cls, v, values):
"""Ensure either value or valueFrom is provided, but not both."""
has_value = values.get('value') is not None
has_value_from = v is not None
if not has_value and not has_value_from:
raise ValueError('Either value or valueFrom must be provided')
if has_value and has_value_from:
raise ValueError('Cannot specify both value and valueFrom')
return v
2.4 Update: Agent Manifest Building
Update in: build_agent_manifest() function
Enhanced Environment Variable Processing:
def build_agent_manifest(
request: CreateAgentRequest, build_ref_name: Optional[str] = None
) -> dict:
"""Build an Agent CRD manifest."""
# Build environment variables with support for valueFrom
env_vars = list(DEFAULT_ENV_VARS)
if request.envVars:
for ev in request.envVars:
if ev.value is not None:
# Direct value
env_vars.append({"name": ev.name, "value": ev.value})
elif ev.valueFrom is not None:
# Reference to Secret or ConfigMap
env_entry = {"name": ev.name, "valueFrom": {}}
if ev.valueFrom.secretKeyRef:
env_entry["valueFrom"]["secretKeyRef"] = {
"name": ev.valueFrom.secretKeyRef.name,
"key": ev.valueFrom.secretKeyRef.key,
}
elif ev.valueFrom.configMapKeyRef:
env_entry["valueFrom"]["configMapKeyRef"] = {
"name": ev.valueFrom.configMapKeyRef.name,
"key": ev.valueFrom.configMapKeyRef.key,
}
env_vars.append(env_entry)
# ... rest of manifest building
3. Frontend Service Layer
3.1 New API Client Methods
Location: /Users/paolo/Projects/aiplatform/rossoctl/rossoctl/ui-v2/src/services/api.ts
Add to agentService object:
export const agentService = {
// ... existing methods
parseEnvFile: async (content: string): Promise<ParseEnvResponse> => {
const response = await apiClient.post<ParseEnvResponse>(
'/agents/parse-env',
{ content }
);
return response.data;
},
fetchEnvFromUrl: async (url: string): Promise<FetchEnvUrlResponse> => {
const response = await apiClient.post<FetchEnvUrlResponse>(
'/agents/fetch-env-url',
{ url }
);
return response.data;
},
};
interface ParseEnvResponse {
envVars: EnvVar[];
warnings?: string[];
}
interface FetchEnvUrlResponse {
content: string;
url: string;
}
4. User Workflow
Scenario 1: Import from Local File
- User clicks "Import from File/URL" button in Environment Variables section
- Modal opens with "Upload File" tab active
- User drags
.envfile or clicks to browse - Frontend reads file content using FileReader API
- Frontend calls
/api/agents/parse-envwith file content - Backend parses and returns structured
EnvVar[]with any warnings - Modal shows preview of parsed variables
- User reviews and clicks "Import"
- Variables are merged with existing
envVarsstate - User can edit or delete any variable before final submission
Scenario 2: Import from URL
- User clicks "Import from File/URL" button
- Modal opens, user switches to "From URL" tab
- User enters URL (e.g.,
https://raw.githubusercontent.com/rossoctl/examples/main/a2a/git_issue_agent/.env.openai) - User clicks "Fetch"
- Frontend calls
/api/agents/fetch-env-url - Backend fetches content with security validations
- Frontend automatically calls
/api/agents/parse-envwith fetched content - Modal shows preview
- User imports variables
Scenario 3: Edit Environment Variable Type
- User has imported/added environment variables
- User clicks dropdown next to variable showing "Direct Value"
- Changes to "Secret"
- UI replaces value input with two fields: "Secret Name" and "Key"
- User fills in:
openai-secretandapikey - On save, backend converts to proper Kubernetes format
Scenario 4: Delete Environment Variable
- User reviews environment variables list
- User clicks trash icon next to unwanted variable
- Variable is removed from state
- No backend call until final agent creation
5. Security Considerations
5.1 SSRF Protection (Server-Side Request Forgery)
Implementation in /api/agents/fetch-env-url:
# Block private IP ranges
BLOCKED_IP_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'),
]
def is_ip_blocked(ip_str: str) -> bool:
"""Check if IP is in blocked range."""
try:
ip = ipaddress.ip_address(ip_str)
return any(ip in network for network in BLOCKED_IP_RANGES)
except ValueError:
return False
5.2 Content Validation
- Limit file size (max 1MB)
- Validate
.envformat - Sanitize values before display
- Timeout on URL fetches (10 seconds)
5.3 Kubernetes Security
- Validate secret/configMap names follow Kubernetes naming conventions
- Ensure referenced secrets exist in target namespace (optional backend validation)
- Support RBAC for secret access
6. Testing Strategy
6.1 Unit Tests
Frontend:
EnvImportModal.test.tsx: Test file upload, URL fetch, parsing previewImportAgentPage.test.tsx: Test env var CRUD operations- Test edge cases: empty files, malformed JSON, large files
Backend:
test_parse_env.py: Test standard and extended.envparsingtest_fetch_env_url.py: Test URL fetching with mocked responses- Test security: SSRF protection, content validation
6.2 Integration Tests
- End-to-end: Import file → Create agent → Verify Kubernetes env vars
- Test secret references: Create secret → Import env with secretKeyRef → Deploy
- Test merge behavior: Existing vars + imported vars
6.3 Manual Testing Checklist
- Import standard
.envfile with 10+ variables - Import extended
.envwith secret/configMap references - Fetch from GitHub raw URL
- Edit imported variable from value to secret reference
- Delete imported variable
- Create agent with mixed env var types
- Verify Kubernetes Agent CR has correct env structure
- Test error handling: invalid URL, network timeout, parse errors
7. Implementation Phases
Phase 1: Backend Foundation (Week 1)
- Implement enhanced
EnvVarmodels withvalueFromsupport - Implement
/api/agents/parse-envendpoint - Implement
/api/agents/fetch-env-urlendpoint with security - Update
build_agent_manifest()to handlevalueFrom - Write backend unit tests
Phase 2: Frontend Components (Week 1-2)
- Create
EnvImportModal.tsxcomponent - Implement file upload with drag-and-drop
- Implement URL fetch integration
- Add parsing preview and validation feedback
- Update API service layer
Phase 3: UI Integration (Week 2)
- Enhance Environment Variables section in
ImportAgentPage.tsx - Update
EnvVarinterface - Implement type dropdown (Value/Secret/ConfigMap)
- Implement conditional form fields
- Add delete functionality
- Integrate
EnvImportModal
Phase 4: Testing & Documentation (Week 2-3)
- Write frontend unit tests
- Write integration tests
- Perform manual testing
- Update user documentation
- Create demo video/screenshots
8. File Structure Summary
New Files
rossoctl/ui-v2/src/components/
└── EnvImportModal.tsx # New modal component
rossoctl/ui-v2/src/components/
└── EnvImportModal.test.tsx # Tests
rossoctl/backend/app/routers/
└── (agents.py - enhanced) # New endpoints + models
Modified Files
rossoctl/ui-v2/src/pages/
└── ImportAgentPage.tsx # Enhanced env var section
rossoctl/ui-v2/src/services/
└── api.ts # New API methods
rossoctl/backend/app/routers/
└── agents.py # Enhanced models & manifest building
9. Example Usage
Example 1: Standard .env File
File: my-agent.env
# LLM Configuration
LLM_MODEL=gpt-4
LLM_TEMPERATURE=0.7
MAX_TOKENS=2000
# MCP Tools
MCP_URL=http://weather-tool:8080/mcp
MCP_TIMEOUT=30
# Application
PORT=8000
LOG_LEVEL=info
Result: 7 environment variables imported with direct values
Example 2: Extended .env with Secrets
File: agent-with-secrets.env
# Direct values
PORT=8000
MCP_URL=http://weather-tool:8080/mcp
# Secret references (JSON format)
OPENAI_API_KEY='{"valueFrom": {"secretKeyRef": {"name": "openai-secret", "key": "apikey"}}}'
DATABASE_PASSWORD='{"valueFrom": {"secretKeyRef": {"name": "db-credentials", "key": "password"}}}'
# ConfigMap reference
APP_CONFIG='{"valueFrom": {"configMapKeyRef": {"name": "app-settings", "key": "config.json"}}}'
Resulting Kubernetes Manifest:
spec:
podTemplateSpec:
spec:
containers:
- name: agent
env:
- name: PORT
value: "8000"
- name: MCP_URL
value: "http://weather-tool:8080/mcp"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: openai-secret
key: apikey
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: APP_CONFIG
valueFrom:
configMapKeyRef:
name: app-settings
key: config.json
Conclusion
This design provides a comprehensive solution for importing environment variables from local files or remote URLs, with full support for Kubernetes secret and configMap references. The implementation is split into manageable phases with clear security considerations and testing strategies.
The solution maintains backward compatibility with existing manual environment variable entry while providing powerful new capabilities for agent developers to quickly configure agents using standardized .env files.