Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ src/
│ └── wizard.rs # CLI setup flow
└── acp/
├── protocol.rs # JSON-RPC types + ACP event classification
├── connection.rs # Spawn CLI, stdio JSON-RPC, env_clear whitelist
├── connection.rs # Spawn CLI, stdio JSON-RPC, [agent.clear_env] policy
└── pool.rs # Session key → AcpConnection map + lifecycle
```

Expand Down Expand Up @@ -65,13 +65,24 @@ The definitive rules — do NOT reinvent this:

### 3. Security — Child Process Environment

Agent subprocesses start with `env_clear()`. The baseline env passed to the child is:
Agent subprocesses start with `env_clear()`. The baseline env always passed to the child is:
- **All platforms:** `HOME`, `PATH`
- **Unix only:** `USER`
- **Windows only:** `USERPROFILE`, `USERNAME`, `SystemRoot`, `SystemDrive`
- Plus any explicit `[agent].env` keys (logged with a prompt-injection warning)

Never leak `DISCORD_BOT_TOKEN` or other OAB credentials to the agent.
Inheritance beyond the baseline is governed by `[agent.clear_env]` (see `docs/config-reference.md`):

```text
if enabled (default true):
if allow_list non-empty: pass only those keys from process env
elif deny_list non-empty: pass all process env EXCEPT deny_list
else: pass nothing (pure secure default)
else:
pass all process env (escape hatch — both lists ignored)
```

Never leak `DISCORD_BOT_TOKEN` or other OAB credentials to the agent. The default (`enabled = true` with empty lists) inherits nothing — when widening, prefer `allow_list` for known-safe keys; reach for `deny_list` only when the inherited set is large and dynamically injected (e.g. AWS-IRSA pods).

### 4. Dockerfile Discipline

Expand Down
19 changes: 17 additions & 2 deletions charts/openab/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,23 @@ data:
{{- if $cfg.env }}
env = { {{ $first := true }}{{ range $k, $v := $cfg.env }}{{ if not $first }}, {{ end }}{{ $k }} = {{ $v | toJson }}{{ $first = false }}{{ end }} }
{{- end }}
{{- if $cfg.inheritEnv }}
inherit_env = {{ $cfg.inheritEnv | toJson }}
{{- if hasKey $cfg "inheritEnv" }}
{{- fail (printf "agents.%s.inheritEnv was removed -- use agents.%s.clearEnv.allowList instead (BREAKING in beta; see CHANGELOG)" $name $name) }}
{{- end }}
{{- $clearEnv := $cfg.clearEnv | default dict }}
{{- $clearEnvDisabled := and (hasKey $clearEnv "enabled") (not $clearEnv.enabled) }}
{{- if or $clearEnvDisabled $clearEnv.allowList $clearEnv.denyList }}

[agent.clear_env]
{{- if $clearEnvDisabled }}
enabled = false
{{- end }}
{{- if $clearEnv.allowList }}
allow_list = {{ $clearEnv.allowList | toJson }}
{{- end }}
{{- if $clearEnv.denyList }}
deny_list = {{ $clearEnv.denyList | toJson }}
{{- end }}
{{- end }}

[pool]
Expand Down
37 changes: 32 additions & 5 deletions charts/openab/tests/configmap_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,45 @@ tests:
path: data["config.toml"]
pattern: 'max_bot_turns = 30'

- it: renders inherit_env as TOML array
- it: renders clear_env.allow_list as TOML array
set:
agents.kiro.inheritEnv:
agents.kiro.clearEnv.allowList:
- "API_BASE_URL"
- "MODEL_NAME"
asserts:
- matchRegex:
path: data["config.toml"]
pattern: 'inherit_env = \["API_BASE_URL","MODEL_NAME"\]'
pattern: '\[agent\.clear_env\]'
- matchRegex:
path: data["config.toml"]
pattern: 'allow_list = \["API_BASE_URL","MODEL_NAME"\]'

- it: renders clear_env.enabled = false when disabled
set:
agents.kiro.clearEnv.enabled: false
agents.kiro.clearEnv.denyList:
- "DISCORD_BOT_TOKEN"
asserts:
- matchRegex:
path: data["config.toml"]
pattern: '\[agent\.clear_env\]'
- matchRegex:
path: data["config.toml"]
pattern: 'enabled = false'
- matchRegex:
path: data["config.toml"]
pattern: 'deny_list = \["DISCORD_BOT_TOKEN"\]'

- it: does not render inherit_env when unset
- it: does not render clear_env section when unset
asserts:
- notMatchRegex:
path: data["config.toml"]
pattern: 'inherit_env'
pattern: '\[agent\.clear_env\]'

- it: fails with helpful message when legacy inheritEnv is used
set:
agents.kiro.inheritEnv:
- "API_BASE_URL"
asserts:
- failedTemplate:
errorPattern: 'inheritEnv was removed'
12 changes: 12 additions & 0 deletions charts/openab/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ agents:
# nameOverride: ""
# env: {}
# envFrom: []
# clearEnv:
# enabled: true # default; set false to disable filtering entirely (escape hatch)
# allowList: [] # only-these mode (deny ignored when set)
# denyList: [] # all-except-these mode (active only when allowList is empty)
# pool:
# maxSessions: 10
# sessionTtlHours: 24
Expand Down Expand Up @@ -76,6 +80,10 @@ agents:
# workingDir: /home/node
# env: {}
# envFrom: []
# clearEnv:
# enabled: true # default; set false to disable filtering entirely (escape hatch)
# allowList: [] # only-these mode (deny ignored when set)
# denyList: [] # all-except-these mode (active only when allowList is empty)
# pool:
# maxSessions: 10
# sessionTtlHours: 24
Expand Down Expand Up @@ -107,6 +115,10 @@ agents:
# workingDir: /home/agent
# env: {}
# envFrom: []
# clearEnv:
# enabled: true # default; set false to disable filtering entirely (escape hatch)
# allowList: [] # only-these mode (deny ignored when set)
# denyList: [] # all-except-these mode (active only when allowList is empty)
# pool:
# maxSessions: 10
# sessionTtlHours: 24
Expand Down
38 changes: 32 additions & 6 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,39 @@ working_dir = "/home/agent"
# Note: env vars here can override baseline vars (HOME, PATH, USER) if needed.
# env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" }
#
# By default, the agent subprocess only inherits these baseline vars:
# Linux/macOS: HOME, PATH, USER
# Windows: USERPROFILE, USERNAME, PATH, SystemRoot, SystemDrive
# Env inheritance from the OAB process is controlled by the [agent.clear_env]
# table. The default is secure: the subprocess starts with env_clear() and
# only receives the baseline (HOME/PATH/USER on Linux/macOS,
# USERPROFILE/USERNAME/PATH/SystemRoot/SystemDrive on Windows) plus
# everything in [agent].env above.
#
# To pass additional env vars from the OAB process (e.g. vars injected via K8s envFrom),
# list them in inherit_env. Keys in [agent].env take precedence over inherited ones.
# inherit_env = ["API_BASE_URL", "MODEL_NAME"]
# Decision tree (when [agent.clear_env] is set):
# if enabled (default true):
# if allow_list non-empty → only those keys pass through from process env
# elif deny_list non-empty → all process env passes through EXCEPT deny_list
# else → nothing inherited (pure secure default)
# else (enabled = false):
# full process env inherited; both lists ignored (pure escape hatch)
#
# allow_list takes priority over deny_list when both are set under enabled=true.
# [agent].env always wins on key conflict (highest precedence).
#
# Example: allow-list mode — explicitly pass a small set of keys.
# [agent.clear_env]
# allow_list = ["API_BASE_URL", "MODEL_NAME"]
#
# Example: deny-list mode for AWS-IRSA / web-identity workloads. K8s auto-
# injects many AWS_* env vars (AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE, ...);
# listing every benign one in allow_list is brittle. Inherit everything and
# strip known secrets:
# [agent.clear_env]
# deny_list = ["DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "ANTHROPIC_API_KEY"]
#
# Example: pure escape hatch — disable filtering entirely (NOT recommended,
# every secret in the OAB env is exposed to the agent and exfil-able via
# prompt injection).
# [agent.clear_env]
# enabled = false

# [agent]
# command = "codex"
Expand Down
32 changes: 30 additions & 2 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,37 @@ The AI agent subprocess that OpenAB spawns to handle messages via ACP.
| `args` | string[] | `[]` | CLI arguments passed to the agent. |
| `working_dir` | string | `"/tmp"` | Working directory for the agent process. |
| `env` | map | `{}` | Extra environment variables (e.g. `{ ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" }`). |
| `inherit_env` | string[] | `[]` | Env var names to inherit from the OAB process (e.g. vars injected via K8s `envFrom`). Keys in `env` take precedence. |
| `clear_env` | table | *(see below)* | Controls how the subprocess inherits env vars from the OAB process. Default is secure (env_clear + baseline + `env`). |

> **Default inherited vars:** After `env_clear()`, the agent always receives `HOME`, `PATH`, and `USER` (on Windows: `USERPROFILE`, `USERNAME`, `PATH`, `SystemRoot`, `SystemDrive`). Use `inherit_env` to pass additional vars beyond this baseline.
#### `[agent.clear_env]` — environment inheritance policy

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `true` | When `true`, `env_clear()` runs and inheritance follows the decision tree below. When `false`, the subprocess inherits the FULL OAB process env and both lists are ignored (escape hatch). |
| `allow_list` | string[] | `[]` | When non-empty under `enabled = true`: only these keys pass through from the OAB process env (deny_list ignored). **Baseline (`HOME`, `PATH`, `USER`, etc.) is always added separately** — `allow_list = ["FOO"]` yields `{baseline + [agent].env + FOO}`, not `{FOO}` alone. |
| `deny_list` | string[] | `[]` | When non-empty under `enabled = true` AND `allow_list` is empty: all process env passes through EXCEPT these keys. **Baseline keys are added unconditionally and cannot be denied** — `deny_list = ["PATH"]` does NOT remove `PATH` from the subprocess. |

**Decision tree** (always after `env_clear()` + baseline + `[agent].env`):

```text
if enabled:
if allow_list non-empty: pass only those keys from process env
elif deny_list non-empty: pass all process env EXCEPT deny_list
else: pass nothing (pure secure default)
else:
pass all process env (escape hatch — both lists ignored)
```

`allow_list` takes priority over `deny_list` when both are set under `enabled = true` (the deny-list branch is only reached when allow-list is empty). `[agent].env` always wins on key conflict (highest precedence).

> **Baseline always set:** Regardless of `clear_env` mode, the subprocess receives `HOME`, `PATH`, and `USER` (on Windows: `USERPROFILE`, `USERNAME`, `PATH`, `SystemRoot`, `SystemDrive`). The baseline ensures agents can locate OAuth/auth files (`~/.codex`, `~/.claude`, `~/.config/gh`).

**Use case — AWS-IRSA / web-identity workloads:** Kubernetes auto-injects many `AWS_*` env vars (`AWS_ROLE_ARN`, `AWS_WEB_IDENTITY_TOKEN_FILE`, ...). Listing every benign one in `allow_list` is brittle. Use `deny_list` to inherit everything but strip known secrets:

```toml
[agent.clear_env]
deny_list = ["DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "ANTHROPIC_API_KEY"]
```

### Agent examples

Expand Down
Loading
Loading