Skip to content

fix: two host-install papercuts — import-time app construction and container-only identity paths - #72

Merged
Diego Colombo (colombod) merged 2 commits into
mainfrom
fix/host-install-papercuts
Aug 15, 2026
Merged

fix: two host-install papercuts — import-time app construction and container-only identity paths#72
Diego Colombo (colombod) merged 2 commits into
mainfrom
fix/host-install-papercuts

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Summary

Fixes two bugs introduced by container-only assumptions leaking into host installs.

Bug 1: ASGI app constructed at module import time

Issue: main.py had asgi_app: BearerTokenMiddleware = create_asgi_app() at module level. The console-script entry point imports main to reach main(), so merely running --help constructed the entire app and ran every startup guard. An operator with a broken config could not ask the binary anything at all.

Fix: PEP 562 lazy construction using _asgi_app cache + get_asgi_app() + module __getattr__. Now main.asgi_app still works for gunicorn and tests but constructs on first access. The guards inside create_asgi_app() are byte-for-byte unchanged — only the timing moved. An unconfigured server still fails loud on an actual serve.

Bug 2: Identity store defaulted to container-only path

Issue: config.py had api_keys_store_path = "/data/identity/api-keys.json" and entra_identities_store_path = "/data/identity/entra-identities.json" hardcoded. On a host install /data is not writable, so every boot logged identity_store.seed: could not write seed ... PermissionError(13) and the keystore silently never persisted.

Fix: Defaults to a host-writable path via a new _default_identity_store_path() helper, matching the convention already documented in this file's own YamlConfigSettingsSource docstring. Graceful in-memory degradation is preserved. Verified this does not regress ACA/production: amplifier-online.yaml explicitly overrides entra_identities_store_path and never uses static mode.

Files Changed

  • context_intelligence_server/main.py — lazy ASGI app construction
  • context_intelligence_server/config.py — host-writable identity store paths
  • tests/test_identity_map_wire.py — updated for new defaults
  • tests/test_lazy_asgi_app.py — NEW: 9 subprocess-based tests for lazy construction

Test Results

uv run python -c "import context_intelligence_server.main" → IMPORT OK with no config
uv run context-intelligence-server --help → exits 0 with no config
✅ Guard still fires on real serve (NEO4J_REQUIRE_EXPLICIT_CLIENTS=true → RuntimeError, worker exit 3)
uv run pytest tests/ -q -m "not neo4j"1821 passed, 4 skipped, 79 deselected

Important Caveats

⚠️ --version flag note: --version is not an implemented flag in this CLI and never was. The fix removes the import-time crash regardless of which flag is passed; an unrecognised flag now fails via argparse (exit 2) instead of via a startup guard.

⚠️ Removed error string: The exact error string that originally motivated this ("No authentication configured — the server refuses to start") no longer exists at HEAD — it was removed by commit 1b2a38d. Today an empty config boots with fail-closed warnings. The lazy-construction fix still stands on its own: import-time construction still runs other startup guards, which is what --help should never do.

⚠️ Other /data paths: blob_path, queues_path and log_path also still default to /data/... at HEAD. This PR deliberately fixes only the identity paths, because theirs is the one failure that is silent — the others fail loudly at startup, while an unwritable identity store leaves the server running with a keystore that never persists.

Generated with Amplifier

…ntainer-only identity paths

Two bugs, both from container-only assumptions leaking into host installs.

Bug 1 — ASGI app constructed at module import time. main.py had
`asgi_app: BearerTokenMiddleware = create_asgi_app()` at module level.
The console-script entry point imports main to reach main(), so merely
running --help constructed the entire app and ran every startup guard.
An operator with a broken config could not ask the binary anything at all.

Fixed with PEP 562 lazy construction: _asgi_app cache + get_asgi_app() +
module __getattr__, so main.asgi_app still works for gunicorn and tests
but constructs on first access. _App.load() now calls get_asgi_app()
explicitly (a bare global reference would NameError). The guards inside
create_asgi_app() are byte-for-byte unchanged — only the timing moved.
An unconfigured server still fails loud on an actual serve.

Bug 2 — identity store defaulted to a container-only path. config.py had
`api_keys_store_path = "/data/identity/api-keys.json"` and
`entra_identities_store_path = "/data/identity/entra-identities.json"`
hardcoded. On a host install /data is not writable, so every boot logged
`identity_store.seed: could not write seed ... PermissionError(13)`
and the keystore silently never persisted.

Now defaults to a host-writable path via a new _default_identity_store_path()
helper, matching the convention already documented in this file's own
YamlConfigSettingsSource docstring. Graceful in-memory degradation is
preserved. Verified this does not regress ACA/production:
amplifier-online.yaml explicitly overrides entra_identities_store_path
and never uses static mode.

Verification:
- uv run python -c "import context_intelligence_server.main" → IMPORT OK with no config
- uv run context-intelligence-server --help → exits 0 with no config
- Guard still fires on real serve (NEO4J_REQUIRE_EXPLICIT_CLIENTS=true → RuntimeError, worker exit 3)
- uv run pytest tests/ -q -m "not neo4j" → 1821 passed, 4 skipped, 79 deselected
- New tests/test_lazy_asgi_app.py (9 subprocess-based tests)
- 2 pre-existing tests in test_identity_map_wire.py updated for new defaults

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…table identity paths)

The prior commit on this branch landed only the new test file; the three
source changes it exercises were left uncommitted. This adds them.

- main.py: replace import-time `asgi_app = create_asgi_app()` with PEP 562
  lazy construction (_asgi_app cache + get_asgi_app() + module __getattr__).
  The console-script entry point imports main to reach main(), so --help
  previously constructed the whole app and ran every startup guard. Guards
  inside create_asgi_app() are byte-for-byte unchanged - only the timing
  moved. _App.load() calls get_asgi_app() explicitly (a bare global would
  NameError once the unconditional assignment was removed).

- config.py: api_keys_store_path / entra_identities_store_path defaulted to
  /data/identity/*.json, a container-only path. On a host install /data is
  not writable, so every boot logged PermissionError(13) and the keystore
  silently never persisted. New _default_identity_store_path() helper
  defaults to a host-writable location, matching the convention already
  documented in this file's own YamlConfigSettingsSource docstring.

- tests/test_identity_map_wire.py: two pre-existing tests updated for the
  new defaults, plus an assertion that neither path is under /data/.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@colombod
Diego Colombo (colombod) merged commit 3452e73 into main Aug 15, 2026
3 checks passed
@colombod
Diego Colombo (colombod) deleted the fix/host-install-papercuts branch August 15, 2026 12:49
Diego Colombo (colombod) added a commit that referenced this pull request Aug 17, 2026
…boot

Rebase-integration fixes required where PR #70 (data-quality phase 2) and the
work merged to main after it (PR #72/#73) touched the same lifespan region:

- main.py: seed _sweep_task=None and queue_health="healthy" before the B1
  deploy-safe boot boundary. The #73 periodic crash-recovery sweep task and
  the W-2 queue-health signal are both created INSIDE that boundary, so they
  must be seeded before it or the shutdown finally / a startup failure hits an
  unbound name. queue_health default is "healthy" per PR #70's W-2 contract
  (test_queue_recovery_success_leaves_queue_health_healthy).
- test_queue_manager.py / test_main.py: update 5 QueueManager.commit() call
  sites in PR #73's tests to PR #70 I5b's 3-arg signature commit(sid, offset,
  cursor) (cursor has no default by design, spec 10.4). Cursor=None: these are
  queue-level tests, not cursor-durability tests.

Preserves both feature sets: #73 bounded respawn + spool + sweep, and #70
deploy-safe boot + maintenance gate + W-2 queue health.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants