Add environment variables to executor profiles (#1444)
* The implementation is complete. Here's a summary of what was done:
Successfully added environment variable configuration support to executor profiles. Users can now configure env vars like this:
```json
{
"CLAUDE_CODE": {
"DEFAULT": {
"CLAUDE_CODE": {
"env": {
"ANTHROPIC_AUTH_TOKEN": "...",
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"API_TIMEOUT_MS": "3000000",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.6"
}
}
}
}
}
```
1. **`crates/executors/src/command.rs`**:
- Added `env: Option<HashMap<String, String>>` to `CmdOverrides` struct
- Added `apply_env_vars()` helper function
2. **Updated all executors to apply env vars**:
- `claude.rs` - `spawn_internal()`
- `codex.rs` - `spawn()`
- `amp.rs` - `spawn()` and `spawn_follow_up()`
- `droid.rs` - module-level `spawn()` function
- `copilot.rs` - both spawn methods
- `cursor.rs` - both spawn methods
- `opencode.rs` - both spawn methods
- `acp/harness.rs` - `spawn_with_command()` and `spawn_follow_up_with_command()` (used by Gemini/Qwen)
3. **Type Generation**: All JSON schemas and TypeScript types now include the `env` field
This allows users to set environment variables for all supported coding agents (Claude Code, Codex, Gemini, Amp, Droid, Copilot, Cursor, OpenCode, Qwen) through profile configuration, eliminating the need for external tools like claude-code-router.
* The implementation is complete. Here's a summary of what was done:
## Summary
### Backend Changes
- Added `env: Option<HashMap<String, String>>` field to `CmdOverrides` struct in `crates/executors/src/command.rs`
- Created `apply_env_vars()` helper function to inject env vars into `tokio::process::Command`
- Updated all 9 executors to apply env vars during spawn:
- claude.rs, codex.rs, amp.rs, droid.rs, copilot.rs, cursor.rs, opencode.rs
- gemini.rs and qwen.rs (via ACP harness)
- Modified ACP harness signature to accept `Option<&CmdOverrides>`
### Frontend Changes
- Created `KeyValueField.tsx` - custom RJSF field for editing key-value pairs
- Registered the field in `theme.ts`
- Added `uiSchema` to `ExecutorConfigForm.tsx` to use the custom field for `env`
### Generated Files
- `shared/types.ts` - TypeScript types updated with `env` field
- `shared/schemas/*.json` - All 9 executor schemas include `env` property
The environment variables UI will now appear in Settings > Agent Settings as a key-value editor with "Environment Variables" label and description. Users can add/remove/edit env vars that will be passed to the CLI execution environment.
* cleanup env structs
* fix form
* fmt
* remove mise.toml
* fmt
* Seprate config form per selected variant
---------
Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
Co-authored-by: Solomon <abcpro11051@disroot.org>
This commit is contained in:
committed by
GitHub
parent
e1c9c15f43
commit
7da884bc3a
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import Form from '@rjsf/core';
|
||||
import type { IChangeEvent } from '@rjsf/core';
|
||||
import { RJSFValidationError } from '@rjsf/utils';
|
||||
@@ -44,6 +44,38 @@ export function ExecutorConfigForm({
|
||||
return schemas[executor];
|
||||
}, [executor]);
|
||||
|
||||
// Custom handler for env field updates
|
||||
const handleEnvChange = useCallback(
|
||||
(envData: Record<string, string> | undefined) => {
|
||||
const newFormData = {
|
||||
...(formData as Record<string, unknown>),
|
||||
env: envData,
|
||||
};
|
||||
setFormData(newFormData);
|
||||
if (onChange) {
|
||||
onChange(newFormData);
|
||||
}
|
||||
},
|
||||
[formData, onChange]
|
||||
);
|
||||
|
||||
const uiSchema = useMemo(
|
||||
() => ({
|
||||
env: {
|
||||
'ui:field': 'KeyValueField',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Pass the env update handler via formContext
|
||||
const formContext = useMemo(
|
||||
() => ({
|
||||
onEnvChange: handleEnvChange,
|
||||
}),
|
||||
[handleEnvChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(value || {});
|
||||
setValidationErrors([]);
|
||||
@@ -87,7 +119,9 @@ export function ExecutorConfigForm({
|
||||
<CardContent className="p-0">
|
||||
<Form
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
@@ -97,6 +131,7 @@ export function ExecutorConfigForm({
|
||||
showErrorList={false}
|
||||
widgets={shadcnTheme.widgets}
|
||||
templates={shadcnTheme.templates}
|
||||
fields={shadcnTheme.fields}
|
||||
>
|
||||
{onSave && (
|
||||
<div className="flex justify-end pt-4">
|
||||
|
||||
137
frontend/src/components/rjsf/fields/KeyValueField.tsx
Normal file
137
frontend/src/components/rjsf/fields/KeyValueField.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { FieldProps } from '@rjsf/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
type KeyValueData = Record<string, string>;
|
||||
|
||||
interface EnvFormContext {
|
||||
onEnvChange?: (envData: KeyValueData | undefined) => void;
|
||||
}
|
||||
|
||||
export function KeyValueField({
|
||||
formData,
|
||||
disabled,
|
||||
readonly,
|
||||
registry,
|
||||
}: FieldProps<KeyValueData>) {
|
||||
const [newKey, setNewKey] = useState('');
|
||||
const [newValue, setNewValue] = useState('');
|
||||
|
||||
// Get the custom env change handler from formContext
|
||||
const formContext = registry.formContext as EnvFormContext | undefined;
|
||||
|
||||
// Ensure we have a stable object reference
|
||||
const data: KeyValueData = useMemo(() => formData ?? {}, [formData]);
|
||||
const entries = useMemo(() => Object.entries(data), [data]);
|
||||
|
||||
// Use the formContext handler to update env correctly
|
||||
const updateValue = useCallback(
|
||||
(newData: KeyValueData | undefined) => {
|
||||
formContext?.onEnvChange?.(newData);
|
||||
},
|
||||
[formContext]
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
const trimmedKey = newKey.trim();
|
||||
if (trimmedKey) {
|
||||
updateValue({
|
||||
...data,
|
||||
[trimmedKey]: newValue,
|
||||
});
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
}
|
||||
}, [data, newKey, newValue, updateValue]);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(key: string) => {
|
||||
const updated = { ...data };
|
||||
delete updated[key];
|
||||
updateValue(Object.keys(updated).length > 0 ? updated : undefined);
|
||||
},
|
||||
[data, updateValue]
|
||||
);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(key: string, value: string) => {
|
||||
updateValue({ ...data, [key]: value });
|
||||
},
|
||||
[data, updateValue]
|
||||
);
|
||||
|
||||
const isDisabled = disabled || readonly;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{entries.map(([key, value]) => (
|
||||
<div key={key} className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={key}
|
||||
disabled
|
||||
className="flex-1 font-mono text-sm"
|
||||
aria-label="Environment variable key"
|
||||
/>
|
||||
<Input
|
||||
value={value ?? ''}
|
||||
onChange={(e) => handleValueChange(key, e.target.value)}
|
||||
disabled={isDisabled}
|
||||
className="flex-1 font-mono text-sm"
|
||||
placeholder="Value"
|
||||
aria-label={`Value for ${key}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemove(key)}
|
||||
disabled={isDisabled}
|
||||
className="h-8 w-8 p-0 shrink-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
aria-label={`Remove ${key}`}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add new entry row */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
disabled={isDisabled}
|
||||
placeholder="KEY"
|
||||
className="flex-1 font-mono text-sm"
|
||||
aria-label="New environment variable key"
|
||||
/>
|
||||
<Input
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
disabled={isDisabled}
|
||||
placeholder="value"
|
||||
className="flex-1 font-mono text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAdd();
|
||||
}
|
||||
}}
|
||||
aria-label="New environment variable value"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAdd}
|
||||
disabled={isDisabled || !newKey.trim()}
|
||||
className="h-8 w-8 p-0 shrink-0"
|
||||
aria-label="Add environment variable"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
frontend/src/components/rjsf/fields/index.ts
Normal file
1
frontend/src/components/rjsf/fields/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { KeyValueField } from './KeyValueField';
|
||||
@@ -1,3 +1,9 @@
|
||||
export { shadcnTheme, customWidgets, customTemplates } from './theme';
|
||||
export {
|
||||
shadcnTheme,
|
||||
customWidgets,
|
||||
customTemplates,
|
||||
customFields,
|
||||
} from './theme';
|
||||
export * from './widgets';
|
||||
export * from './templates';
|
||||
export * from './fields';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RegistryWidgetsType } from '@rjsf/utils';
|
||||
import { RegistryFieldsType, RegistryWidgetsType } from '@rjsf/utils';
|
||||
import {
|
||||
TextWidget,
|
||||
SelectWidget,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ObjectFieldTemplate,
|
||||
FormTemplate,
|
||||
} from './templates';
|
||||
import { KeyValueField } from './fields';
|
||||
|
||||
export const customWidgets: RegistryWidgetsType = {
|
||||
TextWidget,
|
||||
@@ -29,7 +30,12 @@ export const customTemplates = {
|
||||
FormTemplate,
|
||||
};
|
||||
|
||||
export const customFields: RegistryFieldsType = {
|
||||
KeyValueField,
|
||||
};
|
||||
|
||||
export const shadcnTheme = {
|
||||
widgets: customWidgets,
|
||||
templates: customTemplates,
|
||||
fields: customFields,
|
||||
};
|
||||
|
||||
@@ -515,6 +515,7 @@ export function AgentSettings() {
|
||||
selectedConfiguration
|
||||
]?.[selectedExecutorType] && (
|
||||
<ExecutorConfigForm
|
||||
key={`${selectedExecutorType}-${selectedConfiguration}`}
|
||||
executor={selectedExecutorType}
|
||||
value={
|
||||
(executorsMap[selectedExecutorType][
|
||||
|
||||
Reference in New Issue
Block a user