> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SPEC-019 v1.1: Environment Resolution Contract v1.1 — modes named local/lan/cloud

> Environment Resolution Contract v1.1 — modes named local/lan/cloud

<Info>**Status:** `accepted` · **Version** `1.1` · Filed 2026-04-19</Info>

# SPEC-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 memory

## v1.1 change log

* Renamed PRISM\_MODE value `personal` -> `local` throughout (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.

Out-of-scope:

* 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 (no `platform.system()` calls in runtime code per the host-contract memory).

| Var                     | Meaning                  | Probe method                                    |
| ----------------------- | ------------------------ | ----------------------------------------------- |
| PRISM\_HOST\_OS         | Darwin / Linux / Windows | platform.system() at install time               |
| PRISM\_HOST\_FQDN       | Full hostname            | socket.getfqdn()                                |
| PRISM\_HOST\_LAN\_IP    | Primary LAN IPv4         | socket.gethostbyname\_ex() filtered for RFC1918 |
| PRISM\_HOST\_USER\_HOME | User home dir            | Path.home()                                     |
| PRISM\_APP\_SUPPORT     | Per-OS app-support dir   | mcp/host\_config.py at install                  |

### 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)

Backend wiring:

| Var                      | Local default                                  | LAN default                 | Cloud default                      |
| ------------------------ | ---------------------------------------------- | --------------------------- | ---------------------------------- |
| PRISM\_API\_URL          | [http://127.0.0.1:8765](http://127.0.0.1:8765) | http\://\${HOST_FQDN}:41765 | https\://\${PRISM_WEB_URL}         |
| PRISM\_API\_KEY          | auto-minted                                    | minted on server install    | minted on install                  |
| PRISM\_BIND\_ADDR        | 127.0.0.1                                      | 0.0.0.0                     | 0.0.0.0                            |
| PRISM\_BACKEND\_PORT     | 8765                                           | 41765                       | 443                                |
| PRISM\_PG\_PORT          | 5433                                           | 45432                       | n/a                                |
| PRISM\_NEO4J\_HTTP\_PORT | 7474                                           | 47474                       | n/a                                |
| PRISM\_NEO4J\_BOLT\_PORT | 7687                                           | 47687                       | n/a                                |
| PRISM\_ALLOWED\_ORIGINS  | [http://127.0.0.1](http://127.0.0.1):\*        | http\://\${HOST_FQDN}:\*    | \${PRISM_WEB_URL} (https required) |

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)

1. System facts - socket/platform/filesystem calls.
2. Environment variables - PRISM\_\* read from process env.
3. Config files - .env, .env.{mode}, backend/app/config (pydantic BaseSettings).
4. MODE\_PROFILES - module-level constant in backend/app/config/env.py. Per-mode fallback dict. Last resort.

## MODE\_PROFILES (the in-code fallback)

```python theme={null}
MODE_PROFILES: dict[str, dict[str, Any]] = {
    "local": {
        "PRISM_BIND_ADDR": "127.0.0.1",
        "PRISM_BACKEND_PORT": 8765,
        "PRISM_PG_PORT": 5433,
        "PRISM_NEO4J_HTTP_PORT": 7474,
        "PRISM_NEO4J_BOLT_PORT": 7687,
        "PRISM_ALLOWED_ORIGINS": ["http://127.0.0.1:*"],
    },
    "lan": {
        "PRISM_BIND_ADDR": "0.0.0.0",
        "PRISM_BACKEND_PORT": 41765,
        "PRISM_PG_PORT": 45432,
        "PRISM_NEO4J_HTTP_PORT": 47474,
        "PRISM_NEO4J_BOLT_PORT": 47687,
        # ALLOWED_ORIGINS derived from HOST_FQDN at resolve-time
    },
    "cloud": {
        "PRISM_BIND_ADDR": "0.0.0.0",
        "PRISM_BACKEND_PORT": 443,
        # ALLOWED_ORIGINS must be explicit, https-only. Refuse otherwise.
    },
}
```

## Provenance logging (mandatory)

At backend startup, after resolving each key:

```
CORS allowed_origins=['http://127.0.0.1:*'] (source: profile:local)
PRISM_API_URL=http://server1.home.lan:41765 (source: env)
PRISM_HOST_OS=Darwin (source: system:platform.system())
```

## Backend integration surface

```python theme={null}
# backend/app/config/env.py
class HostEnv: ...
class PrismEnv: ...
def resolve() -> tuple[HostEnv, PrismEnv]: ...
```

Called once at startup. Result injected into FastAPI app state. Routers/services consume `request.app.state.env.prism.allowed_origins` — no mode-branching in application code.

## install.py integration

1. Probe HOST\_ENV (tier-1).
2. Prompt user for PRISM\_MODE + override knobs (tier-2 user intent).
3. Merge into MODE\_PROFILES\[mode] baseline (tier-4).
4. Validate cloud mode has PRISM\_WEB\_URL https\://.
5. Validate lan mode's PRISM\_API\_URL is FQDN-shaped (ADR-18).
6. Write flat env block to all 4 editor MCP configs.

## Migration path (physical-artifact renames)

The `personal -> 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` (all `personal` symbols) rename to `local`
* Existing running local stack: teardown during TODO #70 cutover; fresh up with new names.

## Test requirements

Smoke battery mcp/smoke\_env\_resolve.py:

1. Mode-profile baseline: resolve on empty env -> MODE\_PROFILES values for each mode.
2. Tier precedence: env overrides config overrides profile.
3. HOST\_ENV probes return expected shape on Darwin/Linux/Windows.
4. ADR-18 FQDN enforcement: lan bare-hostname PRISM\_API\_URL is rejected at install.
5. Cloud https enforcement: cloud plain-http PRISM\_ALLOWED\_ORIGINS is rejected at install.
6. Provenance log contains one line per resolved key at startup.

CORS smoke mcp/smoke\_cors.py:

1. Local mode, same-origin fetch: allowed.
2. Local mode, cross-origin from not-allowed port: denied (no ACAO header).
3. Lan mode, cross-origin from allowed Mac FQDN: allowed.
4. Cloud mode, http\:// origin: startup refuses to boot backend.

## Migration path from current code

1. File SPEC-019 v1.0. (done, superseded)
2. File SPEC-019 v1.1 (this). (done)
3. File ADR-19 + ADR-20 (done, superseded) + ADR-21 + ADR-22 (done).
4. TODO #72 (localhost audit) executes first.
5. Implement backend/app/config/env.py with HostEnv + PrismEnv.
6. Mount CORSMiddleware always in backend/app/main.py.
7. Rename personal -> local across compose files, container names, install.py symbols.
8. Update install.py for MODE\_PROFILES baseline + FQDN/https validation at tier 2.
9. Update docker-compose files so every port/bind reads from env.
10. Drop legacy PRISMGR\_API\_URL fallback in mcp/client.py + smoke scripts.
11. Smoke batteries (env\_resolve + cors) green before merge.

## Exit criteria

* PRISM\_MODE values are `local`, `lan`, `cloud` — no `personal` references 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.
