Skip to content

feat(12factor-env, pi): opt-in dotenv, schema-level validators/checks/secret redaction, injectable pi context - #1718

Merged
pyramation merged 4 commits into
mainfrom
feat/12factor-env-dotenv
Aug 13, 2026
Merged

feat(12factor-env, pi): opt-in dotenv, schema-level validators/checks/secret redaction, injectable pi context#1718
pyramation merged 4 commits into
mainfrom
feat/12factor-env-dotenv

Conversation

@pyramation

@pyramation pyramation commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the config lane end-to-end for #1645 and #1650. Three pieces, one package boundary:

1. .env becomes an opt-in input builder, not a hard-coded file read. New node-only deep entry 12factor-env/dotenv; the main entry stays free of node builtins (a test fails if that regresses) so browser / Next-client / Electron-renderer bundles are unaffected. Nothing reads a file unless you call dotenv(), and process.env is never mutated:

env(dotenv(), { DATABASE_URL: str() })   // .env fills gaps, real env vars win
dotenv({ override: true })               // file wins (desktop-style hosts)

2. pi takes a context source instead of always reading <cwd>/.env — which is what made a headless coding-agent Job impossible and put credentials in a git clone the agent could commit:

type ContextSource = Record<string, string | undefined> | ((name: string) => string | undefined);

resolveProjectContext(fromEnvironment())   // container/Job/CI: nothing on disk
resolveProjectContext(cwd)                 // unchanged: reads <cwd>/.env  ← desktop keeps working
resolveProjectContext((n) => vault.get(n)) // secret store

Injected vars are CONSTRUCTIVE_-prefixed (CONSTRUCTIVE_ACCESS_TOKEN, CONSTRUCTIVE_DATABASE_ID, …_API_ENDPOINT, …_MODULES_ENDPOINT, …_DATABASE_NAME, …_OWNER_ID); bare names still resolve for project .env compatibility, prefixed wins. pi's hand-rolled parseEnv is gone in favor of parseDotenv. Failure codes are unchanged, and the security boundary is unchanged and now covered by a test: source-supplied endpoints pin only the data plane; the control plane keeps the host's backend config, so a cloned project can never redirect the account bearer.

3. The library features consumers were hand-rolling after env() (#1650). Each of these previously lived in an ad-hoc if/split with its own throw, outside the one consolidated report:

env(process.env,
  { DATABASE_URL: url() },                                    // secrets: redacted (see below)
  { AUTH_KINDS: list({ choices: ['api_key','jwt'] as const }), // per-ITEM choices, typed union[]
    POOL_MAX:   int({ min: 1, max: 64 }),                      // bounded (num({min,max,integer}))
    CACHE_TTL:  duration(),                                    // '30s' -> 30000
    MODE:       enumerated(['per-function','combined'] as const) },
  { checks: [ distinct(['CONTROL_ROLE','DATA_ROLE']),          // cross-field, over CLEANED values
              mutuallyExclusive(['TOKEN','TOKEN_FILE']),
              requiredWhen('TLS','...') ] });

Notable semantics:

  • list() treats set-but-empty ('', ',') as an error rather than returning [] — a typo'd allowlist silently meaning "allow nothing" is the failure mode this exists to prevent. parseEnvList's lenient behavior is untouched for non-schema callers.
  • A check is skipped when any var it names already failed, so "both unset" isn't also reported as "both equal"; failures are attributed to the named vars and join the same EnvError.
  • Secrets are no longer printed. envalid embeds the offending value in its message (Invalid url: "postgres://u:pw@h/db"), which put passwords in crash logs. Anything in env()'s secrets argument — or str({ secret: true }) — reports as Invalid url: "[redacted]" (value redacted, 42 chars), and the cleaned env redacts under JSON.stringify/util.inspect while property access still returns the real value. A missing secret keeps its desc verbatim (no value to leak). redactEnvError(err, [known]) for errors you build yourself.

Implementation note: redaction needs customCleanEnv + applyDefaultMiddleware rather than cleanEnv, because cleanEnv freezes its strict proxy — the serializers have to be defined on the plain cleaned object before that.

Compatibility: env()/cleanEnv() signatures are additive (4th options arg), num is a superset of envalid's, resolveProjectContext(cwd) still works. constructive-desktop needs no change; adopting fromEnvironment() there is optional follow-up. @agentic-kit/pi needs a re-publish after this lands.

Verification

12factor-env: 70 tests, lint + build clean. pi: 73 tests, lint + build clean. Also rebuilt every in-repo consumer of the package (pgpm/env, graphql/env, pg-env, pg-cache, graphile-cache, postmaster, pi) and re-ran the env test suites (snapshots included) to confirm the num/serialization changes are non-breaking.

Link to Devin session: https://app.devin.ai/sessions/450ce6d6659c47759c184ae6ec19a2a8
Requested by: @pyramation

@pyramation pyramation self-assigned this Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

…ed env vars

resolveProjectContext takes values (a record, a lookup fn, or process.env via
fromEnvironment) instead of only a cwd, so a headless host - container, Job, CI -
supplies credentials as environment variables with nothing written into the git
clone. Passing a cwd still reads <cwd>/.env, keeping desktop behavior unchanged.
Injected names are prefixed (CONSTRUCTIVE_ACCESS_TOKEN etc.), bare names still
resolve for project .env files. Drops pi's hand-rolled parser for
12factor-env/dotenv's parseDotenv.
@devin-ai-integration devin-ai-integration Bot changed the title feat(12factor-env): opt-in dotenv support feat(12factor-env,pi): opt-in dotenv + injectable pi project context Aug 13, 2026
@devin-ai-integration devin-ai-integration Bot changed the title feat(12factor-env,pi): opt-in dotenv + injectable pi project context feat(12factor-env, pi): opt-in dotenv, schema-level validators/checks/secret redaction, injectable pi context Aug 13, 2026
@pyramation
pyramation merged commit 2d64c5e into main Aug 13, 2026
36 of 37 checks passed
@pyramation
pyramation deleted the feat/12factor-env-dotenv branch August 13, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant