feat(auth): dual-auth — static key + Microsoft Entra (az login) across hook, query tools, upload CLI#46
Open
colombod wants to merge 5 commits into
Conversation
…ureCliCredential (V1)
Adds the thinnest dual-auth slice to the upload CLI, stacked on pr-45
(layered-behaviours branch). Gate 0 proved the server contract end-to-end
against the pr-29 DTU before this line was written.
What this adds
--------------
context_intelligence/auth.py (NEW — shared by all surfaces)
- AuthStrategy Protocol: headers() -> dict[str,str]
- ApiKeyAuth: wraps a static Bearer key
- EntraTokenAuth: calls credential.get_token('<resource>/.default')
- build_auth_strategy(): dispatcher; FAIL LOUD, no silent fallback
- _make_cli_credential(): lazy AzureCliCredential factory, isolated
so unit tests can patch it without needing azure-identity installed
context_intelligence/config.py
- resolve_config() gains optional auth_mode='static' param
- In 'entra' mode, api_key is NOT required (empty string returned)
- All other behaviour + return type unchanged (backward-compatible)
modules/tool-context-intelligence-upload/
uploader.py
- run_upload() gains optional auth_strategy kwarg (keyword-only)
- When None, derives ApiKeyAuth(api_key) — all existing tests GREEN
- headers = auth_strategy.headers() replaces the literal header dict
cli.py
- Adds --auth-mode (choices: static, entra; default: static)
- Adds --auth-resource (default None, also from env AMPLIFIER_CONTEXT_INTELLIGENCE_AUTH_RESOURCE)
- Reads AMPLIFIER_CONTEXT_INTELLIGENCE_AUTH_MODE from env
- Calls build_auth_strategy() → passes auth_strategy to run_upload()
pyproject.toml
- Adds azure-identity>=1.19 to dependencies
Tests
-----
tests/test_auth.py (NEW, 16 tests)
- ApiKeyAuth.headers() shape
- EntraTokenAuth.headers() with FakeCredential
- build_auth_strategy guards (entra+no resource, static+no key, unknown mode)
- Static mode succeeds even when azure.identity is unavailable
modules/.../tests/test_auth_wiring.py (NEW, 12 tests)
- run_upload() with injected auth_strategy uses strategy headers
- run_upload() without auth_strategy derives ApiKeyAuth (backward compat)
- resolve_config(auth_mode='entra') does NOT SystemExit on missing api_key
- --auth-mode / --auth-resource CLI flags (accepted/defaults/validation)
- main() entra mode passes EntraTokenAuth to run_upload
- main() static mode passes ApiKeyAuth to run_upload
Results
-------
Root bundle: 692 passed
Upload module: 174 passed (PYTHONPATH=bundle root)
ruff: all changed files clean
pyright: 0 errors on upload module; 1 expected reportMissingImports on
azure.identity in root bundle (azure-identity not in root dev deps
— correct by design; lazy import only executes at runtime)
V1 rule honoured
----------------
EntraTokenAuth only acquires tokens via AzureCliCredential (az login rung).
DefaultAzureCredential, managed identity, workload identity, env creds —
all deferred. No bundle-side token cache.
Next slice: wire the same auth_strategy into hook (logging_handler.py) and
query tools (client.py / AsyncCIClient).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…nder; auth_resource on all surfaces
A. FIX _PLACEHOLDER_RE REGEX (config.py)
- Trailing '\\}' required a literal backslash before '}' — never matched.
- Fix: drop the extra backslash → r"\$\{([^}:]+)(?::([^}]*))?}"
- ${VAR}, ${VAR:}, ${VAR:default}, api://${X}/y all now expand correctly.
- Confirmed via 10 new tests in tests/test_placeholder_expander.py.
B. auth_resource ${VAR}-SUBSTITUTABLE ON ALL THREE SURFACES
- Hook destinations: app-cli pre-expands; auth_resource parsed verbatim ✓
- Query-tool sources: _expand() now applied to auth_mode + auth_resource in
ToolConfigResolver.sources property (+ existing api_key, url).
Tested in tests/test_auth_resource_expansion.py (3 tests).
- Upload CLI: _expand_env_placeholders() applied to resolved auth_resource,
server_url, and api_key after resolve_config() in cli.py main().
C. HOOK WRITE PATH — ENTRA AUTH WIRED (async, per-request token)
- Destination dataclass: + auth_mode (default 'static') + auth_resource.
- destinations property: parses auth_mode/auth_resource from config dict.
- validate_destinations(): per-target XOR — entra→auth_resource required,
static→api_key required, unknown mode→ValueError (naming the dest).
- _DestinationDispatcher.__init__: + auth_mode/auth_resource params;
builds AuthStrategy via build_auth_strategy() ONCE at construction.
- _DestinationDispatcher._post: NO header baked into httpx.AsyncClient
constructor; strategy.headers() called PER-REQUEST so Entra SDK can
refresh tokens on long-lived dispatchers.
- __init__.py on_session_ready: threads auth_mode/auth_resource from
each Destination to its _DestinationDispatcher.
- 15 new tests in modules/hook-context-intelligence/tests/test_hook_auth.py.
D. QUERY TOOLS READ PATH — ENTRA AUTH WIRED (per-request token)
- Source NamedTuple: + auth_mode (default 'static') + auth_resource.
- sources property: _expand() applied to auth_mode + auth_resource.
- ToolConfigResolver.validate_sources(): XOR validation mirrors hook.
- resolve_query_auth_strategy(hook_resolver, tool_resolver, api_key)
added to tool_resolver.py — returns ApiKeyAuth or EntraTokenAuth
following same source→hook-dest→env priority chain as endpoint.
- CIClient + AsyncCIClient: + auth_strategy: AuthStrategy | None param;
defaults to ApiKeyAuth(api_key) for backward compat; all header
lookups use self._strategy.headers() PER-REQUEST.
- graph_query_tool.py + blob_read_tool.py: resolve auth strategy and
pass to AsyncCIClient. __init__.py mount() calls validate_sources().
- 18 new tests in modules/tool-context-intelligence-query/tests/test_query_auth.py.
- Updated 2 existing test_blob_read_tool.py assertions to accept new kwarg.
TEST TOTALS (all passing):
Root bundle: 705 passed (was 692; +13 new tests)
Hook module: 328 passed (12 pre-existing test_bundle/test_module_loading
failures from PR#45 YAML refactor; NOT introduced here)
Query tool module: 194 passed, 2 skipped (was 174+2; +18 new, -2 fixed)
Upload module: 174 passed (unchanged)
ruff format+lint: CLEAN on all 11 modified source files.
pyright: 0 errors on root bundle (1 pre-existing warning on requests import).
Stacked on pr-45 (feat/layered-behaviours-opt-in-skill-sync).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Add comprehensive documentation for the new per-target authentication mode:
- README.md: Add 'Authentication — auth_mode / auth_resource' section
documenting static (default) and Microsoft Entra bearer token modes,
per-target configuration on hook destinations and query-tool sources,
${VAR} substitution, fail-loud mount behaviour, and auth scope notes
(Entra-V1 = interactive 'az login' parity only; CI/cloud non-interactive
stays on static key until follow-up; server-side validation prerequisite).
- modules/tool-context-intelligence-upload/README.md: Document
--auth-mode/--auth-resource CLI flags, environment variable
alternatives, and matching scope constraints.
Relates to: feat/dual-auth-entra (commits 146c8ea, b0f82e4)
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Process-level singleton AzureCliCredential with lock-free in-memory cache
keyed by scope. Cache strategy:
- Single TokenCredential per process (no repeated az subprocess overhead)
- Dict read on cache hit (~0.002 ms), threading.Lock only on refresh
- Fail-loud on token refresh; reset() seam for test/switch scenarios
- AMPLIFIER_CONTEXT_INTELLIGENCE_TOKEN_REFRESH_MARGIN_S env var for TTL tuning
Eliminates ~487-913 ms-per-request az subprocess cost on hot path.
Measured on real EntraTokenAuth against live az:
- mount / build_auth_strategy(entra) x8: first 63.9 ms, rest 0.0015 ms
- headers() hot path: first 914 ms (real az), cached 0.0017 ms (~500,000x)
- 8 in-process subsessions sharing singleton: ONE az spawn (437 ms) + 7
cached (~0.0009 ms each) = 437 ms total (~9x over isolated, grows with N)
- cached bearer token still validates (HTTP 202 from server)
Council-approved conditional GO; implements all five conditions:
- Threading.Lock (loop-agnostic, not asyncio.Lock per COE)
- Scope + tenant keying (TB)
- Fail-loud on refresh (ROB + TB + UA)
- reset() seam + fake-credential injection (UA)
- Re-measured against real az (ROB's merge gate, GREEN)
Stale 'SDK refreshes' comment replaced with justification: measurement
proves the reversal necessary, and future engineers have evidence.
Relates to 146c8ea (dual auth design), b0f82e4 (entra wiring),
fc964eb (PR #29 server validation)
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Document the Entra token cache for operators (in-memory/process-scoped; lives ~token lifetime ~67min; auto-refresh at expires_on - margin; AMPLIFIER_CONTEXT_INTELLIGENCE_TOKEN_REFRESH_MARGIN_S knob; fail-loud; az-account-switch behavior), and make the auth flow + token cache visible in the bundle's .dot diagrams: - NEW docs/auth-flow.dot - dedicated diagram centered on the token-cache lane (static vs entra; cache hit lock-free vs miss -> az refresh; expiry/margin; fail-loud; reset; server scp=access_as_user -> 202/200) - context/config-resolution.dot - auth_mode/auth_resource on Destination/Source - docs/dispatch-circuit-breaker.dot - auth-header build before POST All diagrams render-checked (readable/right-altitude/grounded). Full bundle validation passed in full mode. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Microsoft Entra ID (
az login) bearer authentication as a per-target, opt-in alternative to the existing static API key, across all three surfaces — the hook (write path), the query tools (read path), and the upload CLI. The static-key path is the default and unchanged (backward compatible).Base is
feat/layered-behaviours-opt-in-skill-sync(#45), notmain. This builds on #45's layered-behaviours refactor. Merge after #45; GitHub will retarget the base tomainautomatically when #45 merges.What's included (4 commits)
f670983slice 1 — sharedcontext_intelligence/auth.py(AuthStrategy/ApiKeyAuth/EntraTokenAuth/build_auth_strategy) + upload CLI wiring +azure-identitydepc969c9fslice 2 — hook + query-tools wiring; per-target XOR; fail-loud mount guards; fixes a latent${VAR}env-substitution regex bug;auth_resource${VAR}-substitutable on all surfacesa3b54a5docs — README + upload README: config shape, V1 scope, operator notesef65cb7perf — minimal-impact expiry-aware token cache (see Performance)Config shape
Per target (hook
destinations, querysources):auth_mode: static|entra(defaultstatic) +auth_resource: api://<server-app-client-id>(required for entra),${VAR}-substitutable. Upload CLI:--auth-mode/--auth-resource+ env. Each target chooses independently (XOR — never both).Server prerequisite (proven, not assumed)
Validated against the PR #29 server (microsoft/amplifier-context-intelligence): it accepts delegated
scp=access_as_usertokens (whataz loginmints) and rejects app/rolestokens. Proven end-to-end in an isolated Digital Twin against the PR-#29 server build: valid token → 202; missingdata.timestamp→ 400; garbage/expired → 401.V1 scope
Entra =
az login(delegated user) only, viaAzureCliCredentialexplicitly (notDefaultAzureCredential, which would auto-select app credentials in CI/cloud and get rejected by the server). Non-interactive credentials (managed identity / workload identity / service principal) are deferred — they mintrolestokens the V1 server rejects, and require a coordinated server-side change.Performance (hot-path safe)
mount()runs per session and per in-process subsession, so auth is on the hot path. Measured against realaz:AzureCliCredential.get_token()shells out to a subprocess (~487–913 ms) with no in-memory cache. Added an expiry-aware in-memory token cache + a process-level singleton credential:mount()~free, a cachedheaders()is a lock-free dict read (~0.002 ms), and N in-process subsessions share oneazspawn per ~67-min token lifetime. Concurrency viathreading.Lock(loop-agnostic), fail-loud on refresh failure,reset()seam foraz accountswitch + tests.Testing
Root bundle 723+ tests green; new
test_auth_cache.py(18) covers expiry boundary, clock skew, concurrency, exception-not-cached, andreset()isolation. The previously-failing 12 behaviour-YAML fixtures are fixed by #45's latest update (now green on this rebased branch). All end-to-end Entra validation was run in DTUs, never against a live server.