LearnNewsExamplesServices
Frontmatter
id16014
titleTenant repo sync must not bypass its mirror-root leaf
stateClosed
labels
bugaiarchitecture
assigneesneo-gpt-emmy
createdAtJul 26, 2026, 7:53 PM
updatedAtJul 26, 2026, 8:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/16014
authorneo-gpt-emmy
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 26, 2026, 8:46 PM

Tenant repo sync must not bypass its mirror-root leaf

Closed Backlog/active-chunk-10 bugaiarchitecture
neo-gpt-emmy
neo-gpt-emmy commented on Jul 26, 2026, 7:53 PM

Context

Narrow delivery leaf under #15931 after its required consumer census separated two different path owners. #16008 / PR #16009 owns the Neural Link stdio consumer; this ticket owns the remaining TenantRepoSyncService resolver and leaves the umbrella open until both leaves and its full census are complete.

The direct env/hardcoded bridge was introduced by PR #12050 for #12036 when a full-copy, gitignored config overlay could miss a newly added leaf. The current substrate has since changed: ai/config.mjs is a delta-only subclass of ai/configBase.mjs, so an unmentioned base leaf is inherited by construction. ADR 0019 now makes env resolution the leaf's exclusive responsibility and classifies a consumer-side direct env/default chain as A1 re-derivation.

Live latest-open sweep: checked the latest 20 open issues at 2026-07-26T17:52:37Z; no equivalent ticket or in-flight A2A claim exists. Knowledge Base and local issue/discussion sweeps found the historical additive ticket #12036 / PR #12050 and the current umbrella #15931, but no narrow removal leaf.

The Problem

At current dev, TenantRepoSyncService.resolveTenantReposConfig() still owns four resolution layers:

  1. tier1MirrorRoot test seam;
  2. orchestratorConfig?.tenantRepoMirrorRoot;
  3. env.NEO_TENANT_REPO_MIRROR_ROOT;
  4. hardcoded '/app/.neo-ai-data'.

The last three duplicate one existing config leaf. ai/configBase.mjs already declares orchestrator.tenantRepoMirrorRoot with the /app/.neo-ai-data cloud default and the NEO_TENANT_REPO_MIRROR_ROOT env binding. The consumer can therefore disagree with the Provider and can silently keep running when the configured surface is absent. ai/scripts/lint/lint-config-template-ssot.mjs also carries an exact baseline exception for the method signature, so CI records rather than removes the drift.

Patch-blind falsifier: delete the leaf from an injected config stub while supplying env.NEO_TENANT_REPO_MIRROR_ROOT; the current method succeeds from the direct env branch. Under ADR 0019 the consumer must instead fail loud because the Provider surface did not resolve.

The original stale-overlay defense is no longer the right layer. Delta overlays inherit the base leaf; deprecated full-copy snapshots are explicitly migration debt and must not make every consumer a second config resolver.

The Architectural Reality

  • ai/configBase.mjs:721 is the owning surface: orchestrator.tenantRepoMirrorRoot declares the default, env binding, and planeMember: false rationale.
  • ADR 0019 §3 A1 and §5 require an entrypoint-capable consumer that already imports AiConfig to read the resolved leaf at the use site, with no direct env re-read or hidden default.
  • ADR 0019 §10.5 explicitly preserves per-profile-pinned members. This leaf is cloud-profile-pinned, so the fix must not re-anchor it to plane.dataRoot; the placement election owns any future unification.
  • TenantRepoSyncService remains the correct owning service because it materializes absent per-repo mirrorRoot values before GitMirror and ingestion consume them.
  • npm run --silent ai:structure-map -- --files --loc confirms the existing owner/sibling placement under ai/daemons/orchestrator/services/; no new module or directory is needed.

The Fix

  • Simplify resolveTenantReposConfig() to preserve the explicit tier1MirrorRoot test seam, otherwise read AiConfig.orchestrator.tenantRepoMirrorRoot directly at the use site.
  • Remove the orchestratorConfig and env resolver seams, optional chaining, direct process.env read, and hardcoded consumer default.
  • Fail before config enumeration, Git access, or ingestion when the selected root is missing, empty, or whitespace-only.
  • Preserve an explicit per-repo mirrorRoot unchanged.
  • Remove the exact AI_CONFIG_PASS_THROUGH_BASELINE row that currently exempts this method.
  • Replace the two stale-overlay fallback tests with Provider-SSOT and fail-loud coverage; retain the canonical no-double-segment and per-repo override witnesses.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
TenantRepoSyncService.resolveTenantReposConfig({tier1MirrorRoot, ingestionService}) ADR 0019 §§3, 5; #15931 Missing per-repo roots use the explicit test seam when supplied, otherwise the resolved AiConfig.orchestrator.tenantRepoMirrorRoot leaf Missing/blank selected root throws before the ingestion resolver is called; no direct env/default recovery Method JSDoc Focused positive, explicit-override, and side-effect-negative unit tests
AiConfig.orchestrator.tenantRepoMirrorRoot ai/configBase.mjs Remains the sole owner of /app/.neo-ai-data default plus NEO_TENANT_REPO_MIRROR_ROOT env resolution No consumer fallback; deprecated snapshot overlays must migrate or fail loud Existing leaf JSDoc remains authoritative Exact source sweep + AiConfig template-SSOT lint
tenantRepos[].mirrorRoot tenantRepoAccessContract.mjs; #12036 path contract Explicit per-repo value continues to win unchanged over the Tier-1 default Falsy/absent value materializes from the selected Tier-1 root Existing resolver JSDoc Existing override and no-double-segment regression tests
AI_CONFIG_PASS_THROUGH_BASELINE entry for TenantRepoSyncService ADR 0019 enforcement stack Remove the exact exception once the signature no longer captures AiConfig or process.env as parameter defaults Reintroduction fails the lint instead of becoming recorded debt Baseline rationale removed with row npm run ai:lint-config-template-ssot

Decision Record impact

aligned-with ADR 0019 — this ticket does not amend or supersede a Decision Record. It removes a PR-era defensive workaround that current ADR authority and inherited config substrate have rendered invalid. PR #12050 remains historical evidence for why the extra layers once existed, not authority to preserve them.

Acceptance Criteria

  • resolveTenantReposConfig() contains no direct process.env read, hardcoded /app/.neo-ai-data fallback, orchestratorConfig seam, or defensive optional access to the Provider subtree.
  • Absent per-repo mirrorRoot resolves from the explicit tier1MirrorRoot test seam when supplied, otherwise from the live AiConfig.orchestrator.tenantRepoMirrorRoot leaf.
  • Missing, empty, or whitespace-only selected roots reject before listConfiguredTenantRepos(), GitMirror, or ingestion side effects.
  • Explicit per-repo roots and the parent-of-tenant-repos path contract remain unchanged.
  • The exact TenantRepoSyncService config-parameter baseline exception is removed.
  • Focused TenantRepoSyncService unit coverage and npm run ai:lint-config-template-ssot pass.
  • Exact-head source sweep proves the config leaf is the only remaining owner of the env binding and /app/.neo-ai-data default for this consumer path.

Out of Scope

  • Changing the /app/.neo-ai-data cloud default, env variable name, planeMember: false decision, or per-profile placement election.
  • Migrating deprecated full-copy operator overlays; migrateConfigOverlay.mjs owns that path.
  • Changing deriveTenantRepoMirrorPath() or the parent-of-tenant-repos contract.
  • ConnectionService / #16008 / PR #16009, Neural Link membership #15984, or the broader #15931 census.

Avoided Traps

  • Re-anchor the cloud-pinned leaf to plane.dataRoot. Rejected: ADR 0019 §10.5 records this as a per-profile placement decision, not a generic plane-member derivation.
  • Keep a smaller direct-env safety net. Rejected: two resolvers can disagree, and a deprecated overlay shape does not justify bypassing every current Provider.
  • Mutate AiConfig in tests. Rejected: ADR 0019 B4 forbids shared-singleton mutation. The existing explicit root seam supplies deterministic evidence.
  • Close #15931 here. Rejected: the umbrella still owns the full derivation census, negative drain-loop finding, and sibling Neural Link receipt.

Evidence Plan

npx playwright test test/playwright/unit/ai/daemons/orchestrator/services/TenantRepoSyncService.spec.mjs \
  -c test/playwright/playwright.config.unit.mjs --project=unit-brain --workers=1
npm run ai:lint-config-template-ssot

Related

Parent #15931 · sibling #16008 / PR #16009 · historical #12036 / PR #12050 · ADR 0019

Origin Session ID: 019f9e1e-2ef1-72c3-a04d-6bc67a531a8b

Retrieval Hint: query_raw_memories("a1-env-rederivation TenantRepoSyncService tenantRepoMirrorRoot PR 12050 ADR 0019"); exact source anchors ai/daemons/orchestrator/services/TenantRepoSyncService.mjs:1277-1323, ai/configBase.mjs:706-721, and ai/scripts/lint/lint-config-template-ssot.mjs AI_CONFIG_PASS_THROUGH_BASELINE.