LearnNewsExamplesServices
Frontmatter
id17237
titleBrain-internal env parser ships as public `Neo.util.Env` API
stateClosed
labels
enhancementairefactoringarchitectureagent-os
assigneesneo-opus-ada
createdAtAug 16, 2026, 4:10 PM
updatedAtAug 17, 2026, 9:40 AM
githubUrlhttps://github.com/neomjs/neo/issues/17237
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 17, 2026, 9:40 AM

Brain-internal env parser ships as public Neo.util.Env API

Closed Backlog/active-chunk-16 enhancementairefactoringarchitectureagent-os
neo-opus-ada
neo-opus-ada commented on Aug 16, 2026, 4:10 PM

Context

Operator direction, 2026-08-16: src/util/Env.mjs does not relate to the Neo engine (Body) — it belongs to the Brain (ai/).

The file's own JSDoc already says so, in its first two paragraphs:

"Internal typed-parser registry for Neo.ai.ConfigProvider.Not consumer-facing. These parsers are the env-decode layer that ConfigProvider wires into its meta-leaf registry … Application code does NOT call Env.parseX directly."

So the file documents itself as Brain-internal and explicitly not consumer-facing — while src/util/_export.mjs:5 publishes it into the public Neo.util.* namespace. The placement contradicts the file's own stated contract.

Live latest-open sweep: checked the latest 22 open issues (created-descending) at 2026-08-16T12:0xZ — no equivalent found. Nearest adjacent are #17217 (plane predicates) and #17158 (tenant-sync knobs); neither concerns Env.mjs placement. A2A in-flight sweep over the 30 most recent messages found no [lane-claim]/[lane-intent] on this scope.

The Problem

Env.mjs was lifted to src/util/ by #11832 ("Neo.util.Env THIN substrate primitive") under the #11870 unified config/env consolidation. At that time the framing was "a thin Neo substrate primitive." What the codebase actually grew into is narrower: exactly one runtime consumer, and it lives in the Brain.

Verified importers (excluding tests and archived ticket JSON):

importer kind
ai/ConfigProvider.mjs:4 the only real runtime consumer
src/util/_export.mjs:5 the barrel that publishes it as public API

Nothing in src/, apps/, or the framework runtime calls Env.parseX. A grep for Neo.util.Env outside ai/ and test/ returns only the file's own log strings, ADR-0019's prose, and archived ticket JSON — zero framework or app consumers.

The cost of the current placement is not a runtime bug. It is that the engine's public API surface advertises a symbol whose own documentation tells callers not to use it, and a reader auditing Neo.util.* has to open the file to learn it is not part of the engine.

The Architectural Reality

Per ADR-0018 OD-3, /src/ is the Body (engine + Possession Interface) and /ai/ is the Brain (Agent OS). Env.mjs sits in the Body and serves only the Brain.

Three structural facts the fix must respect:

  1. Env.mjs depends on the Neo global. Neo.isEmpty at :51, :73, :97, :119, :137, and export default Neo.gatekeep(Env, 'Neo.util.Env') at :211. Relocating does not make it Neo-free.
  2. This does not sever an ai/ → src/ dependency, and must not be framed as if it does. ai/ConfigProvider.mjs:3 already imports ../src/state/Provider.mjs and is @extends Neo.state.Provider. The Brain legitimately consumes Body primitives. What changes is who owns the symbol and whether it is published, not the dependency direction.
  3. ADR-0019 C1 ("NEO imports ONLY in thread-entrypoints") is adjacent but not violated either way: ConfigProvider is a Neo class by construction, so a Neo-global-using sibling in ai/ is consistent with how ai/ConfigProvider.mjs, ai/configBase.mjs and ai/planeConfig.mjs already operate. ADR-0019 does not pin Env.mjs's location (zero matches for Env.mjs / src/util in that ADR).

Sibling precedent for the destination — ai/ root already holds the config layer: ConfigProvider.mjs, configBase.mjs, config.mjs, config.template.mjs, embeddingProviders.mjs, embeddingSafeBand.mjs, planeConfig.mjs, providerLaneLiveShape.mjs. ai/Env.mjs is a direct sibling-pattern match to ai/ConfigProvider.mjs, which is its sole consumer.

The Fix

  1. Move src/util/Env.mjsai/Env.mjs.
  2. Remove Env from src/util/_export.mjs (both the import at :5 and the export {} list) — this is the actual API-surface change.
  3. Update ai/ConfigProvider.mjs:4 to import Env from './Env.mjs'.
  4. Re-namespace: @namespace Neo.util.EnvNeo.ai.Env, and update the Neo.gatekeep(Env, '…') id at :211 plus the six [Neo.util.Env] warn-prefix strings.
  5. Move test/playwright/unit/util/Env.spec.mjstest/playwright/unit/ai/Env.spec.mjs and fix its import path.
  6. Update the remaining test path references: test/playwright/unit/ai/config.template.spec.mjs:11, test/playwright/unit/ai/ConfigProvider.spec.mjs:49, and the initServerConfigs.spec.mjs fixtures (:245:858, which carry src/util/Env.mjs as string literals inside import-drift fixtures — these are data, not imports, and are easy to miss with an IDE rename).
  7. Update the JSDoc prose in ai/scripts/setup/initServerConfigs.mjs:340:347 which cites the old path in examples.
  8. Update ADR-0019's Neo.util.Env mention at :34 to the new namespace.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.util.Env (public export via src/util/_export.mjs:5) src/util/_export.mjs export list Removed from the public Neo.util.* namespace None — no deprecation shim; zero external consumers found ADR-0019 :34 mention updated grep for Neo.util.Env / util.Env across src/, apps/, learn/ returns only self-references, ADR prose, archived JSON
Neo.ai.Env (new) ai/Env.mjs Brain-owned namespace, unexported from the framework barrel n/a file JSDoc + ADR-0019 :34 new location is a direct sibling of its sole consumer ai/ConfigProvider.mjs
ai/ConfigProvider.mjs leaf-type parser wiring ai/ConfigProvider.mjs:4, :7 Import path changes only; parser behavior unchanged n/a JSDoc :7 type-token map ConfigProvider.spec.mjs + config.template.spec.mjs must stay green unmodified except for the path

Decision Record impact

aligned-with ADR 0018 (Body/Brain separation — OD-3 two-hemisphere topology) and aligned-with ADR 0019 (the AiConfig SSOT: this relocates the env-decode layer next to the Provider that owns it, and changes no resolution semantics). No ADR is amended, superseded, or challenged. ADR-0019 needs one prose touch at :34 for the namespace rename, not a decision change.

Acceptance Criteria

  • ai/Env.mjs exists; src/util/Env.mjs no longer exists.
  • src/util/_export.mjs contains no Env import and no Env in its export list.
  • grep -rn 'src/util/Env.mjs' across the repo (excluding resources/content/** archives and apps/portal/resources/data/**) returns zero hits — including the string-literal fixtures in initServerConfigs.spec.mjs.
  • grep -rn 'Neo.util.Env' returns zero hits outside resources/content/** archives.
  • The gatekeep id and all six warn-prefix strings read Neo.ai.Env.
  • Parser behavior is byte-for-byte unchanged: Env.spec.mjs passes at its new location with no assertion edits — only the import path and file location change. An assertion edit in this PR is a scope violation and should fail review.
  • ConfigProvider.spec.mjs and config.template.spec.mjs pass unmodified except for import paths.
  • AiConfig resolution is unchanged end-to-end: a leaf(default, 'NEO_X', type) still resolves its env override identically. Verify by running the AiConfig unit suite, not by inspection.

Out of Scope

  • No parser behavior changes. No new types, no token-set edits, no validation changes. Pure relocation + rename.
  • No deprecation shim / re-export stub at src/util/Env.mjs. Zero external consumers were found; a shim would preserve exactly the public-surface problem this ticket removes.
  • No change to ai/ConfigProvider.mjs's dependency on src/state/Provider.mjs. The Brain→Body dependency is legitimate and stays.
  • No attempt to make Env.mjs Neo-global-free. It uses Neo.isEmpty and Neo.gatekeep; that is consistent with its ai/ siblings and is not this ticket's concern.
  • Archived ticket/PR JSON under apps/portal/resources/data/** and resources/content/** is historical record and must not be rewritten.

Avoided Traps

  • Framing this as "decouple ai/ from src/". It is not. ConfigProvider extends Neo.state.Provider; the dependency direction is unchanged and correct. The defect is a published namespace, not a dependency.
  • IDE-rename-only migration. initServerConfigs.spec.mjs carries '../../../../src/util/Env.mjs' as string literals inside import-drift test fixtures (:245, :253, :278, :369, :402, :417, :424, :467, :482:493, :537, :853, :858). A symbol rename will not touch them, and they assert on the literal path — they will fail, or worse, silently keep asserting a dead path.
  • Leaving a re-export in _export.mjs "for safety". That keeps Neo.util.Env public and defeats the entire ticket.
  • Reopening #11832's decision as a mistake. It was a reasonable call under the "thin substrate primitive" framing at the time; what changed is that the consumer set resolved to exactly one Brain-side caller. Record it as lineage, not as a reversal.

Related

  • #11832 — Sub 6: Neo.util.Env THIN substrate primitive (lift EnvConfig.mjs parsers to src/util/Env.mjs) — the origin of the current placement.
  • #11871 / #11873 — Env-primitive deduplication (EnvConfig.mjs deletion, MCP migrations).
  • #11870 — Unified config + env substrate consolidation (parent framing).
  • ADR-0018 (Body/Brain two-hemisphere topology), ADR-0019 (AiConfig reactive-provider SSOT).

Handoff Retrieval Hints

  • query_raw_memories: "Neo.util.Env placement Body Brain src/util ai relocation"
  • Structural anchors: src/util/_export.mjs:5, src/util/Env.mjs:211, ai/ConfigProvider.mjs:4, learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md:34

Origin Session ID: 3f264a19-c7d4-481e-bc80-5c288bca177f

⚖️ Ada · @neo-opus-ada · Claude Opus 5 · Claude Code

emre155 cross-referenced by PR #17245 on Aug 16, 2026, 9:38 PM
tobiu referenced in commit 28a8700 - "chore(ai): move Env parsers out of the public Neo.util surface (#17237) (#17243) on Aug 17, 2026, 9:40 AM
tobiu closed this issue on Aug 17, 2026, 9:40 AM