Skip to content

Commit 18feed6

Browse files
committed
feat(ai): aiConfig realm split — KB + MC inherit Tier-1 leaves via the getParent chain (#12166)
Stage 2 of #12166: activate the parent chain by removing per-server snapshots of deployment-wide realm leaves so KB + MC inherit them from the Tier-1 root (Neo.ai.Config). Knowledge Base: - Drop the same-path snapshots — backupPath, auth.* (7 leaves), and the verbose dummyEmbeddingFunction literal — so they chain-inherit from Tier-1. This resolves the dummyEmbeddingFunction child-snapshot anti-pattern (no child re-declares a parent-owned leaf). - Keep the top-level chroma aliases (host/port/path) + the AiConfig import: KB exposes chroma at the top level while Tier-1 owns engines.chroma.*, so collapsing them needs a consumer-read change (KBConfig.host -> KBConfig.engines.chroma.host) = S3/S4 codemod. Flagged, deferred. Memory Core: - All 35 snapshots are same-path (MC keeps engines.chroma.* nested) -> all inherited: auth.*, the three providers, ollama.*, openAiCompatible.*, localModels (read keyed), vectorDimension, modelName, embeddingModel, engines.chroma.*, backupPath. - No value ref remains, so the AiConfig import becomes a bare side-effect import (loads the Tier-1 realm root for the chain; no value snapshot). materializeServerConfigTemplate is extended to rewrite side-effect imports template->overlay for the generated runtime config. Tests: - KB + MC config specs: whole-namespace toEqual -> keyed assertions (namespace enumeration is the deferred local-only getTopLevelDataKeys edge); env tests -> env-at-owner (fresh Tier-1 with env + inheriting child); beforeAll installs a deterministic Tier-1 realm root (the template's side-effect import only registers Neo.ai.Config on first module-eval, so a reused Playwright worker would otherwise see it unset and break inheritance non-deterministically). - Orchestrator.invariants: getParent() makes a fresh BaseConfig inherit the registered realm root, so the mlx/lms undefined-safe tests detach the root to simulate a genuinely-missing namespace. Verified: config+base 30/30, KB/MC consumer + orchestrator + initServerConfigs + ConfigCompleteness suites green; node smoke confirms KB + MC inherit the realm via createConfigProxy -> getParent chain.
1 parent 9256e9c commit 18feed6

6 files changed

Lines changed: 189 additions & 248 deletions

File tree

ai/mcp/server/knowledge-base/config.template.mjs

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ class Config extends BaseConfig {
3838
*/
3939
data: {
4040
neoRootDir: leaf(neoRootDir),
41-
/**
42-
* Universal JSONL backup/export directory inherited from Tier-1 config.
43-
* @type {string}
44-
*/
45-
backupPath: leaf(AiConfig.backupPath, 'NEO_BACKUP_PATH', 'string'),
41+
// backupPath is inherited from the Tier-1 realm (Neo.ai.Config) via the
42+
// BaseConfig.getParent() chain (#12166): declared once at Tier-1, resolved up the chain
43+
// at read time, no local snapshot.
4644
/**
4745
* Automatically synchronize the knowledge base on startup.
4846
* @type {boolean}
@@ -85,43 +83,13 @@ class Config extends BaseConfig {
8583
* @type {Function|null}
8684
*/
8785
authMiddleware: leaf(null),
88-
/**
89-
* Authentication configuration for the server (OAuth 2.1 / OIDC).
90-
* Only used when transport is 'sse'.
91-
* @type {Object}
92-
*/
93-
auth: {
94-
host : leaf(AiConfig.auth.host, 'NEO_AUTH_HOST', 'string'),
95-
port : leaf(AiConfig.auth.port, 'NEO_AUTH_PORT', 'port'),
96-
realm : leaf(AiConfig.auth.realm, 'NEO_AUTH_REALM', 'string'),
97-
issuerUrl : leaf(AiConfig.auth.issuerUrl, 'NEO_AUTH_ISSUER_URL', 'string'),
98-
clientId : leaf(AiConfig.auth.clientId, 'NEO_OAUTH_CLIENT_ID', 'string'),
99-
clientSecret : leaf(AiConfig.auth.clientSecret, 'NEO_OAUTH_CLIENT_SECRET', 'string'),
100-
trustProxyIdentity: leaf(AiConfig.auth.trustProxyIdentity, 'NEO_AUTH_TRUST_PROXY_IDENTITY', 'boolean')
101-
},
102-
/**
103-
* A dummy embedding function to satisfy the ChromaDB API when embeddings are provided manually.
104-
*
105-
* NOTE: This verbose structure is strictly required to prevent the ChromaDB client from
106-
* flagging this as a "legacy" function, which triggers a persistent console warning:
107-
* "No embedding function configuration found for collection..."
108-
*
109-
* The `chromadb` library checks for the presence of `name`, `getConfig`, and `buildFromConfig`.
110-
* If any are missing, it defaults to legacy mode.
111-
* @returns {Object} The dummy embedding function satisfying IEmbeddingFunction
112-
*/
113-
dummyEmbeddingFunction: leaf({
114-
generate : () => null,
115-
name : 'dummy_embedding_function',
116-
getConfig : () => ({}),
117-
constructor: {
118-
buildFromConfig: () => ({
119-
generate : () => null,
120-
name : 'dummy_embedding_function',
121-
getConfig: () => ({})
122-
})
123-
}
124-
}, null, 'object'),
86+
// auth.* (OAuth 2.1 / OIDC; used only when transport === 'sse') is inherited from the
87+
// Tier-1 realm (Neo.ai.Config) via the getParent() chain (#12166): same dotted paths +
88+
// env vars, declared once at Tier-1, no local snapshot.
89+
// dummyEmbeddingFunction is inherited from the Tier-1 realm (Neo.ai.Config) via the
90+
// getParent() chain (#12166) — resolving the child-snapshot anti-pattern: the verbose
91+
// chromadb legacy-guard structure (name/getConfig/buildFromConfig) is declared once at
92+
// Tier-1, never re-copied per child.
12593
/**
12694
* The hostname of the ChromaDB server for the knowledge base.
12795
*

ai/mcp/server/memory-core/config.template.mjs

Lines changed: 19 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import path from 'path';
2-
import AiConfig from '../../../config.template.mjs';
2+
// Side-effect import: load the Tier-1 realm root (Neo.ai.Config) so the getParent() chain resolves
3+
// in this MC process (#12166). MC declares no realm leaves locally (all inherited), so there is no
4+
// value binding or value-snapshot of any parent-realm leaf. `materializeServerConfigTemplate` rewrites this to
5+
// the operator overlay (config.mjs) for the generated runtime config.
6+
import '../../../config.template.mjs';
37
import BaseConfig, {createConfigProxy, leaf} from '../../../BaseConfig.mjs';
48
import {fileURLToPath} from 'url';
59

@@ -124,103 +128,15 @@ class Config extends BaseConfig {
124128
* @type {Function|null}
125129
*/
126130
authMiddleware: leaf(null),
127-
/**
128-
* Authentication configuration for the server (OAuth 2.1 / OIDC).
129-
* Only used when transport is 'sse'.
130-
* @type {Object}
131-
*/
132-
auth: {
133-
host : leaf(AiConfig.auth.host, 'NEO_AUTH_HOST', 'string'),
134-
port : leaf(AiConfig.auth.port, 'NEO_AUTH_PORT', 'port'),
135-
realm : leaf(AiConfig.auth.realm, 'NEO_AUTH_REALM', 'string'),
136-
issuerUrl : leaf(AiConfig.auth.issuerUrl, 'NEO_AUTH_ISSUER_URL', 'string'),
137-
clientId : leaf(AiConfig.auth.clientId, 'NEO_OAUTH_CLIENT_ID', 'string'),
138-
clientSecret : leaf(AiConfig.auth.clientSecret, 'NEO_OAUTH_CLIENT_SECRET', 'string'),
139-
trustProxyIdentity: leaf(AiConfig.auth.trustProxyIdentity, 'NEO_AUTH_TRUST_PROXY_IDENTITY', 'boolean')
140-
},
141-
/**
142-
* Explicit override provider for the core LLM Engine (e.g. summarization).
143-
* Supported values: 'gemini', 'ollama', 'openAiCompatible'
144-
* @type {String}
145-
*/
146-
modelProvider: leaf(AiConfig.modelProvider, 'NEO_MODEL_PROVIDER', 'string'),
147-
/**
148-
* Provider selector for Dream/Sandman graph-generation lanes.
149-
* Supported values: 'ollama', 'openAiCompatible'
150-
* @type {String}
151-
*/
152-
graphProvider: leaf(AiConfig.graphProvider, 'NEO_GRAPH_PROVIDER', 'string'),
153-
/**
154-
* Canonical embedding provider for Memory Core and Knowledge Base embedding callsites.
155-
* Supported values: 'gemini', 'ollama', 'openAiCompatible'
156-
* Defaults to the local OpenAI-compatible Qwen3 route so the source tuple matches the
157-
* unified 4096-dimension Chroma collection invariant. Gemini remains available as an
158-
* explicit operator override and must be paired with `vectorDimension: 3072`.
159-
*
160-
* @type {String}
161-
*/
162-
embeddingProvider: leaf(AiConfig.embeddingProvider, 'NEO_EMBEDDING_PROVIDER', 'string'),
163-
/**
164-
* Settings for the Ollama integration
165-
*/
166-
ollama: {
167-
host : leaf(AiConfig.ollama.host, 'NEO_OLLAMA_HOST', 'string'),
168-
model : leaf(AiConfig.ollama.model, 'NEO_OLLAMA_MODEL', 'string'),
169-
embeddingModel : leaf(AiConfig.ollama.embeddingModel, 'NEO_OLLAMA_EMBEDDING_MODEL', 'string'),
170-
keep_alive : leaf(AiConfig.ollama.keep_alive, 'NEO_OLLAMA_KEEP_ALIVE', 'keepAlive'),
171-
requireParallelModels: leaf(AiConfig.ollama.requireParallelModels, 'NEO_OLLAMA_REQUIRE_PARALLEL_MODELS', 'number')
172-
},
173-
/**
174-
* Settings for the OpenAI-Compatible API integration (e.g., mlx-lm or mlx-openai-server)
175-
* WARNING: Never hardcode API keys here. Always export them via .env or globally.
176-
*/
177-
openAiCompatible: {
178-
host : leaf(AiConfig.openAiCompatible.host, 'NEO_OPENAI_COMPATIBLE_HOST', 'string'),
179-
model : leaf(AiConfig.openAiCompatible.model, 'NEO_OPENAI_COMPATIBLE_MODEL', 'string'),
180-
embeddingModel : leaf(AiConfig.openAiCompatible.embeddingModel, 'NEO_OPENAI_COMPATIBLE_EMBEDDING_MODEL', 'string'),
181-
apiKey : leaf(AiConfig.openAiCompatible.apiKey, 'NEO_OPENAI_COMPATIBLE_API_KEY', 'string'),
182-
unloadRetryCount : leaf(AiConfig.openAiCompatible.unloadRetryCount, 'NEO_OPENAI_COMPATIBLE_UNLOAD_RETRY_COUNT', 'number'),
183-
unloadRetryDelayMs : leaf(AiConfig.openAiCompatible.unloadRetryDelayMs, 'NEO_OPENAI_COMPATIBLE_UNLOAD_RETRY_DELAY_MS', 'number'),
184-
keep_alive : leaf(AiConfig.openAiCompatible.keep_alive, 'NEO_OPENAI_COMPATIBLE_KEEP_ALIVE', 'keepAlive'),
185-
requireParallelModels: leaf(AiConfig.openAiCompatible.requireParallelModels, 'NEO_OPENAI_COMPATIBLE_REQUIRE_PARALLEL_MODELS', 'number')
186-
},
187-
/**
188-
* Local-model role-keyed context limits passthrough.
189-
*
190-
* Mirrors `AiConfig.localModels` into the Memory_Config surface so consumers
191-
* importing the MC overlay (chat-path: SemanticGraphExtractor + TopologyInferenceEngine
192-
* + SessionService summarizer; embedding-path: future TextEmbeddingService consumer
193-
* surface) can read the role-keyed context-limit knobs directly. The context-window
194-
* axis is model-role (chat vs embedding), not provider-namespace — local providers
195-
* share these caps because the practical limit comes from the loaded model.
196-
* @type {Object}
197-
*/
198-
localModels: leaf({
199-
chat: {
200-
contextLimitTokens : AiConfig.localModels.chat.contextLimitTokens,
201-
safeProcessingLimitTokens: AiConfig.localModels.chat.safeProcessingLimitTokens
202-
},
203-
embedding: {
204-
contextLimitTokens : AiConfig.localModels.embedding.contextLimitTokens,
205-
safeProcessingLimitTokens: AiConfig.localModels.embedding.safeProcessingLimitTokens
206-
}
207-
}),
208-
/**
209-
* The enforced vector dimension across all SQLite collections.
210-
* Hard-configured here to prevent catastrophic schema wipes due to dynamic model changes.
211-
* @type {number}
212-
*/
213-
vectorDimension: leaf(AiConfig.vectorDimension, 'NEO_VECTOR_DIMENSION', 'number'),
214-
/**
215-
* The name of the Google Generative AI model for content generation.
216-
* @type {string}
217-
*/
218-
modelName: leaf(AiConfig.modelName),
219-
/**
220-
* The name of the Google Generative AI model for text embeddings.
221-
* @type {string}
222-
*/
223-
embeddingModel: leaf(AiConfig.embeddingModel),
131+
// auth.* (OAuth 2.1 / OIDC; used only when transport === 'sse') is inherited from the
132+
// Tier-1 realm (Neo.ai.Config) via the getParent() chain (#12166): same dotted paths +
133+
// env vars, declared once at Tier-1, no local snapshot.
134+
// The deployment-wide model realm — modelProvider, graphProvider, embeddingProvider, the
135+
// ollama + openAiCompatible provider blocks, localModels (role-keyed context limits),
136+
// vectorDimension, modelName, embeddingModel — is inherited from the Tier-1 realm
137+
// (Neo.ai.Config) via the getParent() chain (#12166): same dotted paths + env vars,
138+
// declared once at Tier-1, no local snapshots. localModels resolves keyed (e.g.
139+
// localModels.chat.contextLimitTokens — every consumer reads it dotted, never whole-object).
224140
/**
225141
* Pagination limit for fetching records during session summarization scans.
226142
* Controls the batch size for memory and summary retrieval.
@@ -240,22 +156,9 @@ class Config extends BaseConfig {
240156
* The default is explicitly 'hybrid' per Epic #9922 Two-Pillar RAG architecture.
241157
*/
242158
engine: leaf('hybrid'),
243-
/**
244-
* Database Engine Definitions
245-
* This defines WHERE data is stored physically (for engines MC owns) and WHERE MC reaches
246-
* into other services' engines. The flat `engines.chroma` path specifies the unified
247-
* ChromaDB instance shared with the Knowledge Base.
248-
*/
249-
engines: {
250-
chroma: {
251-
// Read the unified persist dir from the SSOT. Was the stale
252-
// '.neo-ai-data/chroma/memory-core' — MC is a CLIENT of the one unified store
253-
// (ADR 0003), so its physical dir must be the shared dir, not a server-local one.
254-
dataDir: leaf(AiConfig.engines.chroma.dataDir),
255-
host : leaf(AiConfig.engines.chroma.host, 'NEO_CHROMA_HOST', 'string'),
256-
port : leaf(AiConfig.engines.chroma.port, 'NEO_CHROMA_PORT', 'port')
257-
}
258-
},
159+
// engines.chroma.* (the unified Chroma persist dir + host/port shared with the KB, ADR
160+
// 0003) is inherited from the Tier-1 realm (Neo.ai.Config) via the getParent() chain
161+
// (#12166): same nested dotted paths + env vars, declared once at Tier-1, no local snapshot.
259162
/**
260163
* Physical file paths for embedded/local datasets.
261164
*/
@@ -333,11 +236,8 @@ class Config extends BaseConfig {
333236
prScanLimit : leaf(20, 'NEO_CONCEPT_DISCOVERY_PR_SCAN_LIMIT', 'number'),
334237
minSourceLength: leaf(200, 'NEO_CONCEPT_DISCOVERY_MIN_SOURCE_LENGTH', 'number')
335238
},
336-
/**
337-
* Universal JSONL backup/export directory for all databases.
338-
* @type {string}
339-
*/
340-
backupPath: leaf(AiConfig.backupPath, 'NEO_BACKUP_PATH', 'string'),
239+
// backupPath is inherited from the Tier-1 realm (Neo.ai.Config) via the getParent()
240+
// chain (#12166): declared once at Tier-1, resolved up the chain, no local snapshot.
341241
/**
342242
* Phase 4 (#11663): bundle retention policy for `ai/scripts/maintenance/backup.mjs`.
343243
* Bundles older than `maxDays` are eligible for deletion, but the newest

ai/scripts/setup/initServerConfigs.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export function materializeServerConfigTemplate(src) {
135135
return src
136136
.replaceAll("from '../../../config.template.mjs'", "from '../../../config.mjs'")
137137
.replaceAll('from "../../../config.template.mjs"', 'from "../../../config.mjs"')
138+
// Side-effect imports (no `from`) — e.g. a server config that loads the Tier-1 realm root
139+
// purely to register `Neo.ai.Config` for the getParent() chain (#12166) — must also point at
140+
// the operator overlay in the generated runtime config.
141+
.replaceAll("import '../../../config.template.mjs'", "import '../../../config.mjs'")
142+
.replaceAll('import "../../../config.template.mjs"', 'import "../../../config.mjs"')
138143
}
139144

140145
/**

0 commit comments

Comments
 (0)