diff --git a/docs/prds/0038-smolmachines-env-contract.md b/docs/prds/0038-smolmachines-env-contract.md new file mode 100644 index 0000000..94eff3f --- /dev/null +++ b/docs/prds/0038-smolmachines-env-contract.md @@ -0,0 +1,102 @@ +# PRD 0038: smolmachines Env Contract and Secret-Safe Injection + +- **Status:** Draft +- **Author:** didericis-codex +- **Created:** 2026-06-02 +- **Issue:** #135 + +## Summary + +Make smolmachines env handling match Docker's contract: resolve manifest env +entries through `resolve_env()`, keep secret and interpolated values out of +host argv, and document or enforce an explicit env contract for the backend. + +## Problem + +`bot_bottle/backend/smolmachines/prepare.py` builds the guest env from +`bottle.env` directly, bypassing `resolve_env()`. Entries like `?prompt` and +`${HOST_VAR}` can reach the guest literally rather than being prompted or +resolved. In contrast, Docker resolves env through `resolve_env()` before +writing a mode-600 env file. + +`smolmachines/smolvm.py` renders env as `-e KEY=VALUE` on `smolvm machine +create` argv, and `SmolmachinesBottle.agent_argv` / `exec` prepend +`env KEY=VALUE …` onto the `smolvm machine exec` argv. Any literal or resolved +secret value is therefore visible in the host process table. + +The two backends have no shared env contract document. Divergence will silently +widen as new manifest env features are added. + +## Goals / Success Criteria + +- Manifest env entries are resolved through `resolve_env()` before being + injected into the smolmachines guest, matching Docker behaviour. +- No manifest env value (literal or resolved) appears on host argv during + machine creation or exec. +- Define and document an explicit smolmachines env contract covering literals, + `?prompt` secrets, and `${HOST_VAR}` interpolations. +- Unit tests cover: literal passthrough, prompted-secret resolution, + host-var interpolation, and the no-argv-leak invariant. + +## Non-goals + +- No changes to the Docker env path. +- No changes to manifest schema or `resolve_env()` itself. +- No changes to smolmachines networking or mount handling. +- No new runtime dependencies. + +## Scope + +In scope: + +- `bot_bottle/backend/smolmachines/prepare.py` env resolution. +- `bot_bottle/backend/smolmachines/smolvm.py` machine-create argv. +- `bot_bottle/backend/smolmachines/bottle.py` `agent_argv` / `exec` env + injection. +- `bot_bottle/env.py` if helper changes are needed to support the smolmachines + path. +- Unit tests in `tests/unit/` covering the above. + +Out of scope: + +- Integration tests that start a live smolmachines VM. +- Docker backend changes. +- Dashboard or CLI changes. + +## Design + +Run smolmachines env through `resolve_env()` at prepare time, exactly as Docker +does. After resolution, inject env into the guest through a mechanism that does +not expose values on host argv — for example by writing a mode-600 env file +into the machine's state directory and loading it at exec time, or by passing +env through `smolvm`'s stdin if the tool supports it. + +If `smolvm` provides no stdin or env-file injection path, document this as a +known limitation and at minimum move env values behind a per-invocation +tmpfile rather than inline argv. + +The env contract for smolmachines should mirror Docker's: + +- Literals: passed as-is after resolution. +- `?prompt` entries: prompted at prepare time; resolved value injected, never + on argv. +- `${HOST_VAR}` entries: interpolated from the operator's env at prepare time; + resolved value injected, never on argv. + +## Testing Strategy + +- Unit tests for `prepare.py` asserting `resolve_env()` is called and that + resolution results are used rather than raw `bottle.env` values. +- Unit tests for `smolvm.py` machine-create argv asserting no env value appears + inline. +- Unit tests for `bottle.py` exec path asserting the same argv invariant. + +Run: + +- `python3 -m unittest tests.unit.test_smolmachines_prepare` +- `python3 -m unittest discover -s tests/unit` + +## Open Questions + +- Does `smolvm machine create` support an env-file flag or stdin injection that + avoids `-e KEY=VALUE` argv?