Context
Narrow delivery leaf under #15931, created after its required full-path census showed that the umbrella covers more than the Neural Link consumer named in its original evidence. This ticket gives the already-bounded ConnectionService correction an honest close target while #15931 stays open for the remaining runtime-writer census.
Prior art is #13899, which first routed detached Bridge stdout/stderr into .neo-ai-data/logs. The missing boundary is what happens when the injected Neural Link logPath is absent.
The Problem
At current dev, ai/services/neural-link/ConnectionService.mjs resolves Bridge stdio with:
logPath || path.resolve(neoRootDir || cwd, '.neo-ai-data/logs')
That second branch independently reconstructs the canonical data root. On a relocated overlay plane with a real-directory seat, a missing logPath can therefore create and write <canonical>/.neo-ai-data/logs/neural-link-bridge-stdio.log instead of failing. A symlinked seat can mask the defect because both paths may resolve to the same inode.
Patch-Blind Premise Check
Question: Can the current consumer derive a writable canonical path without the injected config leaf?
Result: Yes. Passing logPath: '' plus a canonical-looking neoRootDir returns a canonical .neo-ai-data/logs path; spawnBridge() then opens it before spawning the child.
The Architectural Reality
ADR-0019 §3 A1 forbids a consumer from re-deriving an AiConfig-owned path: require the injected value and fail loud. PR #15876 established that rule at the shared logger boundary; this leaf applies the same rule to the remaining Neural Link child-process consumer.
This is distinct from #15984. That ticket decides and binds the Neural Link logPath leaf as a parity plane member. This ticket ensures that, regardless of profile, ConnectionService cannot silently invent a replacement when the leaf is absent.
The Fix
- Make
getBridgeStdioLogPath() accept only logPath (defaulting to aiConfig.logPath).
- Throw for missing, empty, or whitespace-only values before any directory open or child spawn.
- Remove
neoRootDir from the spawnBridge() log-path contract.
- Preserve the injected overlay path exactly and keep the child working-directory behavior unchanged.
Acceptance Criteria
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
getBridgeStdioLogPath({logPath}) |
ADR-0019 §3 A1; #15931 |
Resolve only <logPath>/neural-link-bridge-stdio.log |
Missing/blank path throws; no root derivation |
Function JSDoc |
Direct helper negative + configured-path unit |
ConnectionService.spawnBridge({logPath}) |
This ticket |
Pass the injected directory into the helper before opening or spawning |
Helper rejection prevents both side effects |
Method JSDoc |
Side-effect spies remain false on rejection |
| Real-directory overlay write boundary |
#15931 falsifier |
File and parent directory exist only under overlay root |
Canonical root remains absent |
Unit-spec summary |
Filesystem assertion under a disposable real directory |
Out of Scope
- The broader #15931 census and any Fleet/concept-store dispositions it yields.
- #15984's
planeMember decision, compose binding, census literal, and parity boot witness.
TenantRepoSyncService placement; #15800's live record owns that cloud-profile decision.
- Changing the Bridge child working directory or startup-delay behavior.
Avoided Traps
- Closing #15931 with a partial enumeration. This leaf closes only the consumer it fully proves.
- Treating #15984 as the same defect. A correctly bound leaf and a fail-loud consumer are complementary boundaries.
- Testing only a symlinked seat. The filesystem witness uses a real disposable directory and independently checks canonical absence.
Evidence Plan
npx playwright test test/playwright/unit/ai/services/neural-link/ConnectionService.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 #15984 · #15875 / PR #15876 · prior #13899 · ADR-0019 §3
Origin Session ID: 019f9e1e-2ef1-72c3-a04d-6bc67a531a8b
Context
Narrow delivery leaf under #15931, created after its required full-path census showed that the umbrella covers more than the Neural Link consumer named in its original evidence. This ticket gives the already-bounded ConnectionService correction an honest close target while #15931 stays open for the remaining runtime-writer census.
Prior art is #13899, which first routed detached Bridge stdout/stderr into
.neo-ai-data/logs. The missing boundary is what happens when the injected Neural LinklogPathis absent.The Problem
At current
dev,ai/services/neural-link/ConnectionService.mjsresolves Bridge stdio with:logPath || path.resolve(neoRootDir || cwd, '.neo-ai-data/logs')That second branch independently reconstructs the canonical data root. On a relocated overlay plane with a real-directory seat, a missing
logPathcan therefore create and write<canonical>/.neo-ai-data/logs/neural-link-bridge-stdio.loginstead of failing. A symlinked seat can mask the defect because both paths may resolve to the same inode.Patch-Blind Premise Check
Question: Can the current consumer derive a writable canonical path without the injected config leaf?
Result: Yes. Passing
logPath: ''plus a canonical-lookingneoRootDirreturns a canonical.neo-ai-data/logspath;spawnBridge()then opens it before spawning the child.The Architectural Reality
ADR-0019 §3 A1 forbids a consumer from re-deriving an AiConfig-owned path: require the injected value and fail loud. PR #15876 established that rule at the shared logger boundary; this leaf applies the same rule to the remaining Neural Link child-process consumer.
This is distinct from #15984. That ticket decides and binds the Neural Link
logPathleaf as a parity plane member. This ticket ensures that, regardless of profile, ConnectionService cannot silently invent a replacement when the leaf is absent.The Fix
getBridgeStdioLogPath()accept onlylogPath(defaulting toaiConfig.logPath).neoRootDirfrom thespawnBridge()log-path contract.Acceptance Criteria
logPathfails beforeopenBridgeLogFile()orspawnBridgeProcess().logPath; the canonical.neo-ai-data/logsdirectory remains absent.Contract Ledger
getBridgeStdioLogPath({logPath})<logPath>/neural-link-bridge-stdio.logConnectionService.spawnBridge({logPath})Out of Scope
planeMemberdecision, compose binding, census literal, and parity boot witness.TenantRepoSyncServiceplacement; #15800's live record owns that cloud-profile decision.Avoided Traps
Evidence Plan
npx playwright test test/playwright/unit/ai/services/neural-link/ConnectionService.spec.mjs \ -c test/playwright/playwright.config.unit.mjs --project=unit-brain --workers=1 npm run ai:lint-config-template-ssotRelated
Parent #15931 · sibling #15984 · #15875 / PR #15876 · prior #13899 · ADR-0019 §3
Origin Session ID: 019f9e1e-2ef1-72c3-a04d-6bc67a531a8b