LearnNewsExamplesServices
Frontmatter
title>-
authorneo-fable
stateOpen
createdAt7:51 AM
updatedAt7:51 AM
closedAt
mergedAt
branchesdevagent/12456-adr0019-violations-batch3
urlhttps://github.com/neomjs/neo/pull/14942
contentTrust
projected
quarantined1
signals[]
Open
neo-fable
neo-fable commented on 7:51 AM

Contributes to #12456 (batch 3 — MCP server-shared layer env-read census + the clear config-owned fix).

Context

Batch 3 of the ADR-0019 cleanup sweeps the MCP server-shared layer for raw process.env reads: BaseServer.mjs, shared/services/TransportService.mjs, shared/services/DestructiveOperationGuard.mjs, gitlab-workflow/Server.mjs, knowledge-base/Server.mjs + toolService.mjs (remaining reads), and neural-link/Bridge.mjs. Every surveyed read is classified in the census table below; the single CLEAR config-owned violation is fixed on the owning config.

The fix (A1 — module/inline env re-derivation with a hidden default): TransportService.setup() derived the advertised SSE/HTTP transport host as process.env.HOST || 'localhost' at the use site — a raw env read with a shadow default, sitting right next to config-owned aiConfig.publicUrl / aiConfig.mcpHttpPort reads. The deployment host is config, so it now lives as a mcpHttpHost: leaf('localhost', 'HOST', 'string') on the root ai/config.template.mjs transport cluster (between allowedHosts and mcpHttpPort), and TransportService consumes aiConfig.mcpHttpHost at the use site. The existing HOST env binding stays bound through the leaf — no operator-visible env-var rename. Placement on the root (not duplicated per-server) is the sanctioned realm-chain shape: per-server configs resolve mcpHttpHost override-else-inherit via getParent().

Exact-semantics preservation: Neo.util.Env.parseString returns undefined for absent and empty-string env values (Neo.isEmpty guard), so the leaf reproduces the || fallback exactly, including the HOST='' edge. Env re-resolution is bounded (construct / setEnvOverride / load / refreshEnv) per the documented ConfigProvider contract — for a server that reads the value once at boot, behavior is identical.

Spec-shape shift: the three mcpServerUrl resolution tests in TransportService.spec.mjs previously mutated process.env.HOST; they now model the resolved config contract (mcpHttpHost in the mock aiConfig), and the env binding itself is covered by a new root config.template.spec.mjs test (setEnvOverride('HOST', ...) resolving through the leaf on a fresh provider instance). Net: zero process.env mutation left in the transport spec.

Gitignored overlays (ai/config.mjs) were re-synced to the updated template in both the worktree and the operator checkout, so live servers pick up the leaf on next restart.

Evidence: runtime-verified — realm-chain resolution probed on live configs (root/kb/memory-core all resolve mcpHttpHost, and HOST=internal-host resolves through the root leaf from a child config read), plus 402-spec unit run (see Test Evidence).

Deltas from ticket

  • The batch prompt listed knowledge-base/toolService.mjs:23 (NEO_AI_MCP_KB_OPENAPI_PATH) among census targets; it is already converted by open batch-1 PR #14935, so it is census-rowed here but deliberately untouched to avoid a cross-batch conflict.
  • The npm_package_version duplicates in gitlab-workflow/Server.mjs / knowledge-base/Server.mjs are census-deferred rather than fixed: the identical shape exists in three sibling servers outside this batch's file list (github-workflow, file-system, memory-core), and removing only the in-scope two would create sibling drift. See census rows 3–4 for the recommended one-shot fold-out.
  • DestructiveOperationGuard's UNIT_TEST_MODE read is NOT mapped onto the useTestDatabase leaf pattern — deliberate skip with rationale (census row 10): the guard is fail-closed defense-in-depth against config-resolution mistakes; routing its bypass through config would reintroduce the exact trust-the-config dependency it exists to defend against.

Census — every surveyed env read in the batch-3 file set

# File:Line Env var(s) Classification Disposition
1 ai/mcp/server/shared/services/TransportService.mjs:160 HOST CONFIG-OWNED (A1) — deployment host re-derived inline with hidden 'localhost' default FIXEDmcpHttpHost leaf on root ai/config.template.mjs; consumed at use site
2 ai/mcp/server/BaseServer.mjs:301 npm_package_version Runtime process metadata — npm-injected package version, not a deployment value Keep — the single canonical fallback site (metadata.version || env || '1.0.0')
3 ai/mcp/server/gitlab-workflow/Server.mjs:37 npm_package_version Redundant duplicate — the expression is always truthy, so BaseServer:301's identical fallback never fires for this server DEFERRED — fold out all five per-server duplicates in one pass (also github-workflow/Server.mjs:38, file-system/Server.mjs:46, memory-core/Server.mjs:84); partial removal here would create sibling drift
4 ai/mcp/server/knowledge-base/Server.mjs:40 npm_package_version Redundant duplicate (same as row 3) DEFERRED — same fold-out
5 ai/mcp/server/knowledge-base/Server.mjs:106 NEO_AGENT_ID, USER Runtime-identity contract — per-process identity injected by the spawner Never config — untouched
6 ai/mcp/server/knowledge-base/Server.mjs:110 NEO_SESSION_ID Runtime-identity contract Never config — untouched
7 ai/mcp/server/knowledge-base/toolService.mjs:23 NEO_AI_MCP_KB_OPENAPI_PATH Config-owned (file path + test-isolation seam) FIXED IN BATCH-1 (open PR #14935openApiPath leaf on the kb server config); untouched here to avoid cross-batch conflict
8 ai/mcp/server/knowledge-base/toolService.mjs:94 NEO_AGENT_ID, USER Runtime-identity contract Never config — untouched
9 ai/mcp/server/knowledge-base/toolService.mjs:95 NEO_SESSION_ID Runtime-identity contract Never config — untouched
10 ai/mcp/server/shared/services/DestructiveOperationGuard.mjs:113 UNIT_TEST_MODE Deliberate ambient safety check — the canonical-collection delete guard is fail-closed defense-in-depth against config-resolution mistakes (its own contract hardcodes canonical names for exactly this reason); a config leaf would reintroduce the trust-the-config dependency the guard defends against Skip — intentional; not the useTestDatabase storage-selection case
11 ai/mcp/server/shared/services/DestructiveOperationGuard.mjs:236 env = process.env DI default (gates NEO_ALLOW_PRODUCTION_DESTRUCTIVE_AI_SUBSTRATE in #isProductionConfirmed) Operator-confirmation contract — dual-gate (env + explicit token) for deliberate production maintenance, injectable-for-tests by design Skip — intentional (lines 78/199/225 are doc-comment mentions of the same contract, not code reads)
12 ai/mcp/server/neural-link/Bridge.mjs:141 NEO_FLEET_BRIDGE_PUBLIC_KEY Credential injection — per-process key material injected by the fleet spawner Never config — untouched

Totals: 12 code-level env-read sites surveyed → 1 fixed here · 1 fixed in batch-1 · 5 runtime-identity/credential · 1 runtime process metadata (canonical) · 2 deferred (duplicate fold-out) · 2 deliberate safety-guard contracts.

Test Evidence

  • npm run test-unit -- test/playwright/unit/ai/mcp/ test/playwright/unit/ai/config.template.spec.mjs test/playwright/unit/ai/scripts/lint/lintConfigTemplateSsot.spec.mjs --workers=1400 passed / 402 on the merged origin/dev base (4d4b20dca), reproducible across two runs.
    • Control run of the identical suite on detached clean origin/dev (no batch-3 diff): 399 passed / 401 with the exact same 2 failures — both are pre-existing in this environment, independent of this diff (the +1 test on the branch is the new mcpHttpHost leaf-binding test, green).
    • BootEnvelopeResolver.spec.mjs:51 ("empty-string env vars are treated as absent (default instance)") — pre-existing, environment-specific (also fails on a clean stashed tree in isolation).
    • memory-core/Server.spec.mjs:383 (#14388 separate-graph-process identity) — order-dependent in the full run; passes 17/17 when the spec file runs in isolation; outside this diff's surface.
  • Touched-surface subset (shared services + root config template + SSOT lint + kb/memory-core config templates): 179/180, same single pre-existing BootEnvelopeResolver failure.
  • SSOT lint (lint-config-template-ssot) green: the new leaf default is a literal (no inline env), and no baseline row referenced TransportService/HOST, so no baseline churn.
  • Runtime probe (Neo-bootstrapped node): root mcpHttpHost === 'localhost'; kb + memory-core configs resolve 'localhost' via the realm chain; HOST=internal-host resolves 'internal-host' from a child-config read.
  • node --check green on all four touched files; whitespace / shorthand / jsdoc-types / ticket-archaeology / block-alignment / aiconfig-test-mutation gates green (lint-staged at commit).

Post-Merge Validation

  • Restart any SSE-mode MCP server (NEO_TRANSPORT=sse) and confirm the advertised mcpServerUrl is unchanged for the deployment (HOST env still wins over the localhost default; publicUrl still wins over both).
  • Operators with a customized ai/config.mjs overlay: re-sync the overlay from the template (or add the mcpHttpHost leaf) — the shipped worktree/checkout overlays were already re-synced; a missing leaf on a stale overlay resolves to undefined and fails loud at SSE boot, per the no-defensive-fallback contract.
  • Batches 4+ can consume census rows 3–4 (npm version duplicate fold-out, five siblings in one pass).

Commits

  • 38fcee8d0 refactor(mcp): ADR-0019 batch-3 — server-shared env reads onto owning config leaves + census (#12456)

Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.

github-actions commented on 7:51 AM

🚨 Agent PR Body Lint Violation

@neo-fable — your PR body on PR #14942 [QUARANTINED_URL: github.com] does not match the pull-request template structure.

Required action: read .agents/skills/pull-request/SKILL.md BEFORE editing the PR body. The skill points at:

  • Minimum-viable PR body structure: .agents/skills/pull-request/references/pull-request-workflow.md §9
  • Self-Identification mandate: .agents/skills/pull-request/references/pull-request-workflow.md §5

Do NOT compose a substitute template or hallucinate section headings. The validator checks more structural anchors than this comment names. The only reliable path to passing is reading the actual workflow file and following its structure.

Diagnostic hint: at least one recognized anchor like ``Resolves #N(mandatory closing keyword —Refs/Related alone is NOT sufficient) is missing.

Visible anchors missing (full list)
  • ``Resolves #N(mandatory closing keyword —Refs/Related alone is NOT sufficient)

This is the CI tool-boundary lint companion to PR #11494's MCP manage_pr_review validator and PR #11502's agent-pr-review-body-lint.yml reviewer-side lint. Resolves #11501.