Status:
accepted · Version 1.1 · Filed 2026-04-19SPEC-019 v1.1 — Environment Resolution Contract (HOST_ENV + PRISM_ENV + MODE_PROFILES)
Status: draft | v1.1 | Supersedes: v1.0 | Related: ADR-18, ADR-21 (supersedes 19), ADR-22 (supersedes 20), project_host_contract memoryv1.1 change log
- Renamed PRISM_MODE value
personal->localthroughout (aligns with dev conventions: .env.local / .env.lan / .env.cloud). Same semantic, better name. - All other content unchanged from v1.0.
Purpose
Define how Prism reads configuration at install and runtime across three modes (local / lan / cloud), making the codebase environmentally aware (“soft”) without mode-specific control flow in application code. Every config value’s source is knowable at runtime via provenance logging.Scope
In-scope:- Install-time: host probing, mode selection, env-block write to editor MCP configs.
- Runtime: env read, tiered resolution, provenance logging at backend startup.
- Backend, MCP server, installer, and docker-compose files all consume the same resolver surface.
- Secrets management (use OS keychain / vault).
- Per-project config overrides (future).
- Feature flags.
The two buckets
HOST_ENV — discovered facts about the host
Never chosen. Probed by install.py once; written to every editor MCP env block; read by runtime (noplatform.system() calls in runtime code per the host-contract memory).
PRISM_ENV — Prism’s wiring, chosen per mode
Selected at install from prompts + MODE_PROFILES defaults; written to env; read by runtime. The mode knob:- PRISM_MODE: local | lan | cloud (the one switch everything keys off)
Install context:
- PRISM_ROOT, PROJECT_ROOT, PRISM_GITHUB_USER, PRISM_FS_MCP_NAME, LOG_DIR, LOG_LEVEL
Resolution order (tiered — first tier to answer wins)
- System facts - socket/platform/filesystem calls.
- Environment variables - PRISM_* read from process env.
- Config files - .env, .env., backend/app/config (pydantic BaseSettings).
- MODE_PROFILES - module-level constant in backend/app/config/env.py. Per-mode fallback dict. Last resort.
MODE_PROFILES (the in-code fallback)
Provenance logging (mandatory)
At backend startup, after resolving each key:Backend integration surface
request.app.state.env.prism.allowed_origins — no mode-branching in application code.
install.py integration
- Probe HOST_ENV (tier-1).
- Prompt user for PRISM_MODE + override knobs (tier-2 user intent).
- Merge into MODE_PROFILES[mode] baseline (tier-4).
- Validate cloud mode has PRISM_WEB_URL https://.
- Validate lan mode’s PRISM_API_URL is FQDN-shaped (ADR-18).
- Write flat env block to all 4 editor MCP configs.
Migration path (physical-artifact renames)
Thepersonal -> local rename cascades to implementation artifacts. Execute during SPEC-019 implementation, not as a separate PR:
- docker-compose.personal.yml -> docker-compose.local.yml
- .env.personal -> .env.local (already exists as backend/.env.local for dev; reconcile)
- prism-personal-backend / -postgres / -neo4j container names -> prism-local-*
- NEO4J_AUTH neo4j/prism_personal -> neo4j/prism_local
- install/backend.py
BackendMode = Literal["personal","lan","cloud"]->Literal["local","lan","cloud"] - install/backend.py
DEFAULT_PERSONAL_URL,resolve_personal(),start_personal(),COMPOSE_PERSONAL,DEFAULT_CREDENTIALS(allpersonalsymbols) rename tolocal - Existing running local stack: teardown during TODO #70 cutover; fresh up with new names.
Test requirements
Smoke battery mcp/smoke_env_resolve.py:- Mode-profile baseline: resolve on empty env -> MODE_PROFILES values for each mode.
- Tier precedence: env overrides config overrides profile.
- HOST_ENV probes return expected shape on Darwin/Linux/Windows.
- ADR-18 FQDN enforcement: lan bare-hostname PRISM_API_URL is rejected at install.
- Cloud https enforcement: cloud plain-http PRISM_ALLOWED_ORIGINS is rejected at install.
- Provenance log contains one line per resolved key at startup.
- Local mode, same-origin fetch: allowed.
- Local mode, cross-origin from not-allowed port: denied (no ACAO header).
- Lan mode, cross-origin from allowed Mac FQDN: allowed.
- Cloud mode, http:// origin: startup refuses to boot backend.
Migration path from current code
- File SPEC-019 v1.0. (done, superseded)
- File SPEC-019 v1.1 (this). (done)
- File ADR-19 + ADR-20 (done, superseded) + ADR-21 + ADR-22 (done).
- TODO #72 (localhost audit) executes first.
- Implement backend/app/config/env.py with HostEnv + PrismEnv.
- Mount CORSMiddleware always in backend/app/main.py.
- Rename personal -> local across compose files, container names, install.py symbols.
- Update install.py for MODE_PROFILES baseline + FQDN/https validation at tier 2.
- Update docker-compose files so every port/bind reads from env.
- Drop legacy PRISMGR_API_URL fallback in mcp/client.py + smoke scripts.
- Smoke batteries (env_resolve + cors) green before merge.
Exit criteria
- PRISM_MODE values are
local,lan,cloud— nopersonalreferences remain in code, compose files, container names, or docs. - Zero
platform.system()calls in backend or mcp/ runtime code. - Zero hardcoded ports in docker-compose files.
- CORSMiddleware mounted in main.py; allowlist from resolved env.
- Provenance log at startup, one line per resolved key.
- install.py rejects lan bare-hostname URL + cloud http URL.
- mcp/smoke_env_resolve.py green across local/lan/cloud.
- mcp/smoke_cors.py green across local/lan.

