Skip to content

Commit 1dfb44e

Browse files
committed
test(skills): unit tests for resolver + serialize env-mutation test; docs
Test coverage for the env_passthrough resolver (5 unit tests): - Forbidden vars (LD_PRELOAD, PYTHONPATH) are blocked. - Kernel-reserved vars (PATH, HOME, …) are blocked. - Forbidden match is case-insensitive. - Operator deny patterns (AWS_*, *_KEY) work. - Per-skill override unblocks denied vars only for the named skill, and cannot bypass the FORBIDDEN_PASSTHROUGH hard block. Mark the existing env-mutating integration test #[serial_test::serial(skill_env_passthrough)] (review #4): it calls std::env::set_var/remove_var which mutates process-global state and would race with any other env-touching test under the parallel default of cargo test. The serial harness is already used by hands/ extensions for the same reason. Docs (review #5): add an Environment Variable Passthrough section to docs/src/app/agent/skills/page.mdx covering the two-party opt-in model (skill author declares; operator gates), the resolution order, the FORBIDDEN list, and a worked gog example. Calls out the distinction from skill config variables — credentials should go through config, not env.
1 parent 84ed49d commit 1dfb44e

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/librefang-skills/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ fs2 = "0.4"
2929

3030
[dev-dependencies]
3131
tempfile = { workspace = true }
32+
serial_test = "3"

docs/src/app/agent/skills/page.mdx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,73 @@ db.host = postgres.internal
221221

222222
---
223223

224+
## Environment Variable Passthrough
225+
226+
Skill subprocesses run with `env_clear()` by default — no host environment variables are inherited. This is the right default for third-party code: API keys, tokens, and other secrets in the host environment must not silently leak into a skill's subprocess.
227+
228+
Some skills legitimately need a specific host variable. The canonical example is a skill that wraps a CLI tool which uses an env-based credential helper (e.g. `gog`'s file-backed keyring needs `GOG_KEYRING_PASSWORD`).
229+
230+
This works as a **two-party opt-in**: the skill author declares which variables the skill *wants*, and the operator (the person running LibreFang) decides which of those requests to *grant*.
231+
232+
### Skill author: declare in `skill.toml`
233+
234+
Add `env_passthrough` at the top level of the manifest, sibling to `[skill]` and `[runtime]`:
235+
236+
```toml
237+
env_passthrough = ["GOG_KEYRING_PASSWORD", "GOG_KEYRING_BACKEND"]
238+
239+
[skill]
240+
name = "gog"
241+
#
242+
```
243+
244+
The variable *names* are public (they live in the manifest); only their host-side *values* cross the subprocess boundary, and only when the operator has not blocked the name.
245+
246+
### Operator: gate via `[skills]` config
247+
248+
The operator's config in `~/.librefang/config.toml` decides which requests are honored:
249+
250+
```toml
251+
[skills]
252+
# Glob patterns that block matching env-var names regardless of what the
253+
# skill manifest declares. These are the defaults; replace with your own
254+
# list, or set to [] to disable the deny check.
255+
env_passthrough_denied_patterns = [
256+
"*_KEY",
257+
"*_TOKEN",
258+
"*_PASSWORD",
259+
"*_SECRET",
260+
"*_API_KEY",
261+
"AWS_*",
262+
"GITHUB_*",
263+
]
264+
265+
# Per-skill explicit allow overrides. Lets you grant a specific skill an
266+
# env var that would otherwise be blocked by env_passthrough_denied_patterns.
267+
[skills.env_passthrough_per_skill]
268+
gog = ["GOG_KEYRING_PASSWORD"]
269+
```
270+
271+
If you don't configure `[skills]` at all, the defaults above apply.
272+
273+
### Resolution
274+
275+
For each variable name in a skill's `env_passthrough`, in order:
276+
277+
1. **Hard block** — names like `LD_PRELOAD`, `PYTHONPATH`, `NODE_OPTIONS`, etc. are dropped regardless of skill manifest or operator config. These either inject code or redirect imports/library lookup, and would defeat the `env_clear` isolation. The full list is in `librefang-skills::loader::FORBIDDEN_PASSTHROUGH`.
278+
2. **Kernel-reserved**`PATH`, `HOME`, `PYTHONIOENCODING`, etc. are dropped. The kernel sets these explicitly per-runtime (it may have deliberately narrowed `PATH`); skills cannot override them.
279+
3. **Operator deny** — names matching `env_passthrough_denied_patterns` are dropped *unless* listed under `env_passthrough_per_skill` for the running skill.
280+
4. Anything that survives is forwarded if it's set in the host environment. Variables not present in the host environment are silently skipped.
281+
282+
Each rejection is logged at `WARN` level so operators can debug why a declared variable did not reach a skill subprocess.
283+
284+
### When to use it
285+
286+
- **Use `env_passthrough`** when a skill calls out to a CLI that authenticates via env-based credential helpers (keyring backends, `*_PASSWORD` vars, etc.).
287+
- **Don't use `env_passthrough` for API keys/tokens.** Use [Skill Config Variables](#skill-config-variables) instead — those go through `~/.librefang/config.toml` and are injected via the system prompt without giving the skill subprocess access to host secrets.
288+
289+
---
290+
224291
## Python Skills
225292

226293
Python skills are the simplest to write. They run as subprocesses and communicate via JSON over stdin/stdout.

0 commit comments

Comments
 (0)