LearnNewsExamplesServices
Frontmatter
id12145
titleResolve pull-mode tenantRepos through the tiered config resolver
stateClosed
labels
documentationenhancementairefactoringarchitecture
assigneesneo-opus-ada
createdAtMay 28, 2026, 8:30 PM
updatedAtMay 28, 2026, 10:13 PM
githubUrlhttps://github.com/neomjs/neo/issues/12145
authorneo-opus-ada
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 28, 2026, 10:13 PM

Resolve pull-mode tenantRepos through the tiered config resolver

Closed v13.0.0/archive-v13-0-0-chunk-14 documentationenhancementairefactoringarchitecture
neo-opus-ada
neo-opus-ada commented on May 28, 2026, 8:30 PM

Context

First-real-world cloud Agent OS deployment dogfooding surfaced another tenant-repo config-resolution gap — sibling to the #12036 cluster (#12042, #12047, #12052) and extending the config-as-SSOT precedent #12097. When an operator declares a tenant in kb-config.yaml, there is no inline indication of where the pull-mode repo URL + credential come from, and — more importantly — the pull-mode sync path never reads kb-config.yaml at all.

The Problem

learn/agentos/cloud-deployment/TenantIngestionModel.md:335 documents the intended tenant-config home:

"Tenant config storage: tenantRepos[] is persisted via KnowledgeBaseIngestionService.setTenantConfig({tenantId, config}) … credential-bearing cloneUrl surfaces stable rejection errors at normalization."

So the design intent is graph-node (canonical) + kb-config.yaml (deployment bootstrap tier), with the gitignored config.mjs aiConfig.tenantRepos[] as the Tier-3 default fallback (originally the #12029 first-tenant smoke expedient).

But the orchestrator's pull-mode sync resolves tenantRepos[] from aiConfig directly, bypassing the tiered resolver. Consequences:

  • kb-config.yaml's tenants.<id>.tenantRepos (the documented bootstrap tier) is never honored on the pull path.
  • The tenant-repo polling config lives only in the gitignored config.mjs overlay → not version-controlled, no review/history, lost on a crashed session (the empirical trigger here).

This is the same config-as-SSOT principle #12097 enforced for orchestrator env-getters, now applied to the tenant-repo config source — which #12097 did not cover (it added tenantRepoSync.{sweepCadenceMs,jitterRatio} cadence bindings, not the array source).

Key enabler: a tenantRepos[] entry holds no raw secret — credentialRef is an env-var pointer; tokens live in the gitignored token env file, resolved at the GIT_ASKPASS boundary. Normalization already rejects a credential-bearing cloneUrl. So the array is safe to commit to YAML.

The Architectural Reality

  • ai/daemons/orchestrator/services/TenantRepoSyncService.mjs:~582resolveTenantReposConfig normalizes aiConfig.tenantRepos directly (no tier resolution).
  • ai/services/knowledge-base/KnowledgeBaseIngestionService.mjs:799getTenantConfig already returns tenantRepos from all three tiers (graph node :818 / yaml bootstrap :838 / aiConfig default :855). Per-tenant only.
  • ai/services/knowledge-base/KnowledgeBaseIngestionService.mjs:869readKbConfigBootstrap() reads path.join(aiConfig.neoRootDir, 'kb-config.yaml').
  • ai/deploy/docker-compose.yml — canonical compose; the orchestrator service does not mount kb-config.yaml (only the kb-server does). Deployment mirrors inherit the gap.
  • ai/services/knowledge-base/helpers/TenantRepoIngestEnvelopeBuilder.mjs:250 — pull-mode file selection is git ls-tree/diff over the mirror; kb-config.yaml's sourcePaths.root is not applied on the pull path (feeds the full-corpus kbSync lane only).

The Fix

  1. Add a cross-tenant enumeration resolver (e.g. KnowledgeBaseIngestionService.listConfiguredTenantRepos()) that walks all tenant configs across the existing tiers (graph nodes + kb-config.yaml tenants.* + aiConfig.tenantRepos[] default) and returns the normalized union — the array shape TenantRepoSyncService already consumes. (getTenantConfig is per-tenant; the sync lane needs all tenants.)
  2. TenantRepoSyncService.resolveTenantReposConfig delegates to that resolver instead of reading aiConfig.tenantRepos directly. Precedence: graph node > yaml > aiConfig default — matches getTenantConfig's existing tiering (no new precedence invented, no hidden fallback).
  3. Mount kb-config.yaml (read-only) into the orchestrator service in ai/deploy/docker-compose.yml, matching the kb-server mount.
  4. Docs: correct TenantIngestionModel.md + the cloud-deployment README to state (a) pull-mode file selection comes from the git mirror + tenantRepos[], independent of kb-config.yaml's Source/Parser registration; (b) sourcePaths.root is NOT honored on the pull path; (c) the tier order for tenantRepos. (Folds the otherwise-standalone doc-fix.)

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
kb-config.yaml tenants.<id>.tenantRepos[] getTenantConfig yaml tier (KnowledgeBaseIngestionService.mjs:838) Honored on the pull path (currently parsed but ignored by the sync lane) aiConfig.tenantRepos[] (Tier-3 default) TenantIngestionModel.md + README getTenantConfig already returns yaml-tier tenantRepos; TenantRepoSyncService bypasses it
TenantRepoSyncService.resolveTenantReposConfig ai/daemons/orchestrator/services/TenantRepoSyncService.mjs Delegates to the tiered enumeration resolver n/a code JSDoc :~582 reads aiConfig.tenantRepos directly today
orchestrator kb-config.yaml mount ai/deploy/docker-compose.yml RO bind-mount into the orchestrator service n/a README volumes table only kb-server mounts it today

Decision Record impact

aligned-with ADR 0014 (cloud deployment topology + scheduler task taxonomy). Completes the documented tenant-config-storage intent in TenantIngestionModel.md:335; no ADR conflict.

Acceptance Criteria

  • AC1: A cross-tenant resolver enumerates tenantRepos across graph + yaml + aiConfig tiers, returning the normalized array shape.
  • AC2: TenantRepoSyncService resolves via that resolver; no direct aiConfig.tenantRepos read remains in the sync path.
  • AC3: A tenantRepos entry declared only in kb-config.yaml tenants.<id>.tenantRepos is picked up + synced by the pull lane (test).
  • AC4: Precedence graph > yaml > aiConfig asserted by test; no || / ?? hardcoded hidden fallback (per the config-as-SSOT contract, #12097 / #12061).
  • AC5: orchestrator service mounts kb-config.yaml (RO) in ai/deploy/docker-compose.yml.
  • AC6: TenantIngestionModel.md + cloud-deployment README corrected (pull path independent of kb-config.yaml Source config; sourcePaths.root not honored on pull path; tenantRepos tier order documented).
  • AC7: Existing TenantRepoSyncService / KB tests pass; new tier-resolution + negative-fallback tests added.
  • AC8: Capability-framing audit pass (no downstream-deployment-partner names in any artifact).

Out of Scope

  • The setTenantConfig operator MCP tool for the graph-node tier (TenantIngestionModel.md:335 "when a tenant-config operator tool is added") — separate future ticket; this ticket honors the graph tier if present but does not add the write tool.
  • The deployment-side config move (relocating a tenant's repo block from the gitignored config.mjs overlay into committed kb-config.yaml) — dependent follow-on in the deployment repo, gated on this neo wiring landing + a release.
  • The env-vars-for-the-array approach — see Avoided Traps.

Avoided Traps

  • env-vars-for-the-tenant-array: awkward (indexed / JSON env vars), and doesn't collapse the kb-config.yaml / config.mjs split. The array is non-secret (credentialRef is a pointer), so committed YAML is the better SSOT.
  • Status-quo gitignored overlay: brittle — no review/history, lost on a crashed session (the empirical trigger).
  • Inventing a new precedence order: reuse getTenantConfig's existing graph > yaml > default tiering; do not add a parallel resolver with different semantics.
  • New || default / ?? hardcoded fallbacks in the resolver: violates the config-as-SSOT contract (#12097 / #12061).

Related

  • Sibling cluster (first cloud deployment dogfooding): #12036 (parent, 3 Day-0 gaps), #12042, #12047, #12052.
  • Config-as-SSOT precedent: #12097 (orchestrator env-getter collapse), #12061 (no-hidden-default-fallbacks contract).
  • Origin of the config.mjs tenantRepos smoke: #12029 (RawRepoSource opt-in).
  • ADR 0014 (cloud deployment topology).

Handoff Retrieval Hints

  • Retrieval Hint: "pull-mode tenantRepos resolver bypass + orchestrator kb-config.yaml mount gap"
  • Retrieval Hint: TenantRepoSyncService.resolveTenantReposConfig tiered resolution
tobiu referenced in commit 2f8c677 - "feat(kb): resolve pull-mode tenantRepos via tiered config resolver (#12145) (#12146) on May 28, 2026, 10:13 PM
tobiu closed this issue on May 28, 2026, 10:13 PM