# How External Plugins Access Runtime Environment Variables in GeoLibre: A Complete Guide to AI Assistant API Keys

> Learn how GeoLibre external plugins access runtime environment variables for AI assistant API keys using a secure snapshot mechanism and the @geolibre/plugins runtime API.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-15

---

**External plugins in GeoLibre access runtime environment variables through a secure, allow-listed snapshot mechanism where the Rust backend reads OS environment variables at startup and exposes them via the `@geolibre/plugins` runtime API.**

GeoLibre's plugin architecture enforces strict security boundaries between the desktop application and external code. Since webview contexts cannot access `process.env` directly, the framework implements a controlled pipeline for sensitive credentials like AI assistant API keys. This design ensures that only explicitly permitted variables reach plugin code while preventing environment leakage into browser builds.

## Why Direct Environment Access Is Blocked

The GeoLibre desktop application runs a webview front-end hosted by a Tauri Rust backend. This architecture creates a fundamental security boundary: the JavaScript runtime inside the webview has no native access to the host operating system's environment variables. According to the source code in [`apps/geolibre-desktop/src/lib/assistant/os-env.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/assistant/os-env.ts), attempting to read `process.env` from plugin code would return an empty or mocked object, making a backend-mediated solution essential.

## The OS Environment Loading Pipeline

GeoLibre resolves this limitation through a three-stage pipeline that loads, caches, and exposes environment variables.

### Stage 1: Rust Backend Variable Extraction

The Tauri backend exposes a command called **`read_env_vars`** that accepts an array of allowed variable names and returns their values from the host OS. This command is invoked exclusively from `loadOsEnvVars()` in [`apps/geolibre-desktop/src/lib/assistant/os-env.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/assistant/os-env.ts):

```typescript
// From apps/geolibre-desktop/src/lib/assistant/os-env.ts
export async function loadOsEnvVars(): Promise<RuntimeEnv> {
  // Only invoke the Tauri backend once; subsequent calls reuse the same promise.
  loadPromise ??= readOsEnvVars();
  return loadPromise;
}

```

This singleton pattern prevents redundant IPC calls. The function caches the promise in `loadPromise`, ensuring all concurrent requests share the same asynchronous operation.

### Stage 2: Allow-List Enforcement

Security is enforced through the **`OS_ENV_VAR_NAMES`** constant defined in [`assistant/provider.ts`](https://github.com/opengeos/GeoLibre/blob/main/assistant/provider.ts). This whitelist restricts readable variables to AI-provider credentials only:

| Variable | Purpose |
|----------|---------|
| `GEMINI_API_KEY` | Google Gemini primary key |
| `GOOGLE_API_KEY` | Google general AI key |
| `GOOGLE_GENAI_API_KEY` | Google GenAI-specific key |
| `OPENAI_API_KEY` | OpenAI API access |
| `OPENAI_COMPATIBLE_BASE_URL` | Custom endpoint URL |
| `OPENAI_COMPATIBLE_API_KEY` | Custom endpoint key |
| `OPENAI_COMPATIBLE_MODEL` | Custom endpoint model |

The Rust backend rejects any request for variables outside this list, preventing plugins from accessing system-sensitive values like `PATH`, `HOME`, or `SSH_PRIVATE_KEY`.

### Stage 3: Browser-Safe Caching

Once retrieved, environment values are stored in a global cache at `window.__GEOLIBRE_OS_ENV__` for synchronous access:

```typescript
// Simplified from os-env.ts implementation
const cached = window.__GEOLIBRE_OS_ENV__;
if (cached) return cached;

// Otherwise load and cache
const env = await loadOsEnvVars();
window.__GEOLIBRE_OS_ENV__ = env;
return env;

```

If the application runs outside Tauri—such as in web builds or Jupyter embeddings—the function immediately resolves to an empty object `{}`, ensuring zero secret exposure in browser contexts.

## Accessing Environment Variables from Plugin Code

External plugins retrieve runtime environment variables through the **`@geolibre/plugins`** package rather than any global object.

### The getRuntimeEnv() Method

The plugin runtime API exposes a `getRuntimeEnv()` function that returns the merged `RuntimeEnv` object:

```typescript
import { getRuntimeEnv } from '@geolibre/plugins';

export function init() {
  const env = getRuntimeEnv();           // RuntimeEnv object
  const apiKey = env.OPENAI_API_KEY;     // string | undefined
  // Plugin initialization logic
}

```

This approach provides several advantages:
- **Consistency**: Same interface across desktop and web builds (web returns empty object)
- **Type safety**: `RuntimeEnv` is fully typed with optional fields for all allow-listed variables
- **Testability**: Plugins can mock `getRuntimeEnv()` without stubbing global objects

### Complete Working Example

Here's production-ready plugin code for making authenticated AI requests:

```typescript
import { getRuntimeEnv } from '@geolibre/plugins';

export async function runAIQuery(prompt: string) {
  const { 
    OPENAI_API_KEY, 
    OPENAI_COMPATIBLE_BASE_URL 
  } = getRuntimeEnv();

  if (!OPENAI_API_KEY) {
    throw new Error(
      'OpenAI API key not configured. Set OPENAI_API_KEY in your OS environment.'
    );
  }

  const baseUrl = OPENAI_COMPATIBLE_BASE_URL ?? 'https://api.openai.com';
  
  const response = await fetch(`${baseUrl}/v1/chat/completions`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${OPENAI_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: prompt }]
    })
  });

  if (!response.ok) {
    throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
  }

  return await response.json();
}

```

## Runtime Variable Merging

The hook **`useRuntimeEnvironmentVariables`** in [`apps/geolibre-desktop/src/hooks/useRuntimeEnvironmentVariables.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/useRuntimeEnvironmentVariables.ts) combines multiple variable sources into the final `RuntimeEnv` passed to plugins:

1. **OS environment snapshot** from `readOsEnv()` (desktop only)
2. **Deployment-time variables** from build configuration or remote config services

This merging allows administrators to override OS variables via deployment pipelines without modifying host machine environments.

## Error Handling and Resilience

The pipeline includes graceful degradation for edge cases. From [`apps/geolibre-desktop/src/lib/assistant/os-env.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/assistant/os-env.ts) lines 66-73:

```typescript
// Error recovery: reset promise to allow retry on next call
loadPromise.catch((err) => {
  console.error('Failed to load OS environment variables:', err);
  loadPromise = undefined;  // Clear cache so next attempt retries
  return {};                // Return empty object, don't break app
});

```

If the IPC channel fails or the Rust backend returns an error, the promise is reset for subsequent retry attempts and the application continues with an empty environment object rather than crashing.

## Security Architecture Summary

GeoLibre's environment variable access implements defense in depth:

- **Whitelist enforcement**: Rust backend only reads explicitly permitted variables
- **IPC mediation**: No direct OS access from JavaScript/webview
- **Build-time safety**: Browser builds receive empty objects by design
- **Cache isolation**: Values stored in closure-scoped promise, not persistent storage
- **No logging**: API keys are never written to application logs or crash reports

## Summary

- **External plugins access environment variables through `getRuntimeEnv()`** from `@geolibre/plugins`, not through `process.env` or global objects.
- **Only allow-listed AI-provider variables are readable**, enforced by the `OS_ENV_VAR_NAMES` constant in [`assistant/provider.ts`](https://github.com/opengeos/GeoLibre/blob/main/assistant/provider.ts) and the Rust `read_env_vars` command.
- **Variables are loaded once at startup** by `loadOsEnvVars()` in [`apps/geolibre-desktop/src/lib/assistant/os-env.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/assistant/os-env.ts) and cached for synchronous access.
- **Non-desktop builds receive empty objects**, ensuring secrets never leak into browser contexts.
- **The `useRuntimeEnvironmentVariables` hook** merges OS and deployment variables before exposing them to the plugin runtime.

## Frequently Asked Questions

### Can plugins request additional environment variables beyond the AI provider list?

No. The whitelist in [`assistant/provider.ts`](https://github.com/opengeos/GeoLibre/blob/main/assistant/provider.ts) is the authoritative source, and the Rust backend enforces this restriction at the IPC boundary. Plugin developers wishing to access other variables must request changes to the core `OS_ENV_VAR_NAMES` constant or implement custom configuration through the plugin's own settings system.

### What happens if an API key contains special characters or newlines?

The environment reading pipeline preserves raw string values exactly as returned by the OS. GeoLibre performs no transformation or validation on key contents—this responsibility falls to the plugin's HTTP client. The `fetch`-based example above handles standard Bearer token formatting automatically.

### Is there any performance cost to calling `getRuntimeEnv()` repeatedly?

No. The implementation uses a singleton promise pattern where `loadOsEnvVars()` executes the expensive Tauri IPC call exactly once. Subsequent calls return the cached promise, and synchronous reads from `window.__GEOLIBRE_OS_ENV__` have negligible overhead. Plugins may safely call `getRuntimeEnv()` on every AI request without performance concerns.