Status Report · updated 25 August 2026

Live OSCAR telemetry in the Ops Console — and the platform auth work it uncovered

The console you saw last time read committed JSON exports. The objective: have it read live telemetry out of the OSCAR dev environment, without exposing OSCAR to an uncontrolled network. That works. The console authenticates as itself, calls the Action Gateway, and receives live data from monitoring-read.

This page now carries a second, larger subject, because the honest account of the last week is not "we connected two services." Wiring that seam meant auditing what the platform's edges actually accepted — and that audit turned into seven pull requests of authentication and authorization work, most of it well outside the original scope. Both threads are documented here, and the page is deliberate about what is deployed versus what is merged and inert.

Live OSCAR read
Working
proven in-container 2026‑08‑06
Auth & security PRs
7
#153 → #182 · all merged
Recent integration changes
9
4 OSCAR · 5 Ops Console · 14–25 Aug
Console auth
11/11
Easy Auth applied & verified
Forged identities
Refused
dev-token lane closed in dev
Live push proof
11 frames
real dev turn · zero malformed or dropped keys
The objective is met — live OSCAR data reaches the console
Last time, the console rendered committed JSON and no seam existed between the two systems. As of 2026-08-06 the console authenticates as itself to OSCAR, calls the Action Gateway, and receives live telemetry from monitoring-read. The console's own runtime log is the proof: {"name":"monitoring.read_health_summary","provider":"oscar","outcome":"ok","duration_ms":4861} — provider: oscar rather than fixtures, and outcome: ok.

The export-backed panels still deliberately render from a mounted snapshot, not a live feed. The capture is now reproducible and the latest committed rotation is dated 2026-08-24; this page continues to label that provenance separately from the live read.

The larger story since is authentication. Seven PRs between 4 and 11 August closed a set of things the platform was accepting that nobody had catalogued: read views taking no principal at all, PHI readable from an identifier alone, write routes with no auth assertion anywhere in the repository, and a cross-tenant leak in the push fan-out. See section 05 for what changed, and section 12 for what is left.

The integration became observable, priced, and demoable

This supplement records the cross-repository work after the original 11 August status cut. It is deliberately split by evidence: a merged PR is not described as a fresh Azure observation, while the push path is described as proven because it carried a real dev turn end to end. The Linear records are in the canopy-airiam team, across the PMS Backend and Ops Console projects.

PlaneDelivered changeEvidence / consequence
OSCAR
PR #201, CAN-640
Resolved Azure deployment aliases to the actual priced model, including gpt4o-mini-oscar → gpt-4.1-mini. The cost resolver can price the deployment the platform really uses rather than returning an unknown-model gap.
OSCAR
PR #202, CAN-553
Emitted gen_ai.client.cost_cents with the shared service dimension at the LLM trace site. Measured cost became a telemetry signal, not an Ops Console calculation.
OSCAR
PR #207, CAN-719/CAN-720
Made the model label server-owned at the actual emit site, routed RCM through Front Door, and tagged the CI auth probes. Cost cannot be client-labelled; deliberate 401 probes can be separated from product traffic.
OSCAR
PR #209, CAN-721/CAN-722
Completed the measured-cost path and added an authenticated agentic probe plus silence/auth-rejection alerting. The first priced window begins at 2026-08-19T15:11:33.46514Z; the historical estimate is bounded before it, so the two populations do not double count.
Ops Console
PR #14, CAN-638
Consumed OSCAR's push channel through the console BFF and rendered the live agentic tail. A real dev turn produced 11 accepted span frames, with zero malformed frames and zero dropped keys.
Ops Console
PR #15, PR #18, CAN-723
Separated export freshness from the live health wall and separated product-path success rate from all-traffic synthetic probes. The console no longer colours export data as live or presents known synthetic 401s as product health.
Ops Console
CAN-653, CAN-784
Rendered measured cost alongside an explicitly labelled historical estimate, then automated generation and manifest declaration during export capture. The 24 August snapshot carries the split; the capture path now fails rather than silently losing the estimate. CAN-784's Linear state still needs administrative closure.
Ops Console
CAN-788/CAN-789
Moved live category health above snapshot content and made the live trace readable: a per-type timeline, humanised labels, and a session step count. Committed on dev on 24 August; these two Linear issues still show In Progress and need their status reconciled with the merged code.
Ops Console
dev, 25 Aug
Made the snapshot endpoint table directly explorable: exact service selection, case-insensitive route search, and an errors-only control, while retaining its existing sort behaviour. The interaction filters the endpoint DTO already delivered to the browser; it adds no new telemetry source, API, or claim of live endpoint data.
Ops Console
dev, 25 Aug
Added a daily measured AI-cost evidence table: date, service, priced calls, and integer cents, with an inclusive local date-range control. It remains distinct from the historical estimate: measured rows come from the export's gen_ai.client.cost_cents data and are neither combined with nor used to recalculate the earlier reference estimate.
The boundary is now explicit
Measured cost is OSCAR's emitted gen_ai.client.cost_cents telemetry from the cutover onward. Historical cost is an Ops Console-only, rate-card-based reference estimate over earlier snapshot spans, marked non-invoice-grade and non-emitted. The console shows them apart and refuses an estimate that overlaps the measured era.
What this update does not claim
The commits prove the current code and the recorded real-turn probe proves push. This revision did not re-query Azure on 25 August, so deployment-specific claims remain dated to their stated observations. In particular, a committed snapshot rotation is not evidence by itself that the Azure Files mount has already been repointed.

A single agentic turn, end to end

01 · Request

An authenticated caller reaches /agentic/chat through Front Door. The service identity and correlation marker make the request attributable without placing a user prompt in the console.

02 · OSCAR observes

The orchestrator emits the LLM span, server-owned model identity, token usage, and measured gen_ai.client.cost_cents; it also tees the safe span projection to the live hub.

03 · Console renders

The BFF receives the push frame and the live tail gains a labelled step, matching timeline lane, and session count. Snapshot Token Economics retains its own, separately labelled historical context.

What to show in a short walkthrough

  1. Start at Services. The live category wall now leads; the export-freshness banner expressly describes the snapshot panels below it. In the endpoint table, select a service, search for a route, or isolate endpoints with recorded errors—these are local controls over the same labelled snapshot rows, not a new live query.
  2. Show measured cost in context. On Instrumentation, the daily table exposes each rendered measured row's day, service, priced calls, and cents. Its optional date bounds filter only that table and leave the separately labelled historical estimate untouched.
  3. Open Instrumentation before the turn. The live tail is intentionally empty until it sees a new event; that is evidence it is not replaying a fixture or historical transcript.
  4. Drive one real agentic turn. The stream should gain a humanised step and coloured timeline lane in the same browser session. The measured cost enters OSCAR telemetry; its appearance in export-backed totals depends on the next capture, rather than being falsely presented as instantaneous.
  5. Close on provenance. Measured cost, estimated historical cost, snapshot data, and live push each say what they are. The console does not flatten them into a single convenient but misleading number.
Three limits worth saying out loud
The push panel is a live tail, not history; the historical estimate is directional, not invoice-grade; and measured AI cost currently renders on Instrumentation, not as a per-service value on Services (CAN-783). Naming those limits is part of the product claim, not a disclaimer after it.
25 August live validation: current agentic probe is timing out
A one-shot run from ca-monitoring-prober-dev minted its managed-identity bearer successfully and completed all five ordinary health checks with 200, but its real Front Door POST /agentic/chat exhausted the 120s agentic budget and emitted synthetic.agentic.up with outcome=unreachable. No correlated agent-orchestrator request appeared in the following request-table window, so this attempt did not produce a live span, measured-cost point, or console-tail entry. The standing prober shows the same recurring 120-second pattern; this is current evidence for CAN-781/CAN-782, not a failure of the console UI.

Replace the export with a live read — through a governed seam

The easy version of this is a public endpoint and a bearer token. That was considered and rejected. OSCAR's dev environment is network-isolated, and the console runs in a different resource group with its own managed identity, so the naive shape would have meant making an OSCAR service internet-reachable to a caller whose identity could not be pinned.

The shape we built instead: the console never talks to a data service directly. It authenticates as itself to the Action Gateway, which is the only component permitted to reach monitoring-read, which is the only component that queries Azure Monitor. Every hop is a separate Entra audience with its own app role, and the whole graph is declared in one file that generates both the directory grants and the runtime allow-lists.

Single source of truth

infrastructure/s2s-callgraph.json declares every caller→callee edge. The deploy scripts read it to create app-role assignments and to compute each service's runtime allow-list, so the directory and the code cannot drift apart.

Fail closed

An empty caller allow-list used to warn and accept any tenant app holding s2s.invoke. That was closed in both resolvers during this work; a declared edge that resolves to nothing now stops the deploy.

No new public surface

The console reads through the gateway's existing edge. monitoring-read gained no internet exposure at all — a deliberate reversal of the first design, for the reason in the next section.

infrastructure/s2s-callgraph.json services/monitoring-read/manifest.yaml docs/ops-console/monitoring/CONSOLE-READ-CONTRACT.md

Four hops, three identities, one direction

Solid lines are built and running. The dashed line is the one hop that is configured in code but has never been applied to the running console — see section 09.

graph LR
  subgraph RGC["rg-opsconsole-dev"]
    UI["Ops Console
ca-opsconsole-dev"]:::gate MI["Console managed identity"]:::gate end subgraph RGO["rg-oscar-dev · network isolated"] AFD["Front Door edge
+ WAF containment"]:::svc GW["Action Gateway
ca-action-gateway-dev"]:::svc MR["monitoring-read
ca-monitoring-read-dev"]:::svc end subgraph AZM["Azure Monitor"] AI["App Insights
appi-oscar-dev"]:::data LAW["Log Analytics
law-oscar-dev"]:::data end UI ==>|"1 acquire token"| MI MI -.->|"2 Bearer · aud=gateway
NOT YET APPLIED"| AFD AFD ==>|"3 /api/v1/read-proxy"| GW GW ==>|"4 Bearer · aud=monitoring-read"| MR MR ==>|"5 KQL"| AI MR ==>|"5 KQL"| LAW MR -.->|"6 envelope + as_of"| UI classDef svc fill:#1b3a4d,stroke:#5ad1b4,color:#dcf3ec,font-size:16px; classDef gate fill:#3a2f17,stroke:#d8a13a,color:#f4e6c8,font-size:16px; classDef data fill:#173049,stroke:#4aa8d8,color:#d6ecf7,font-size:16px;
+ / − buttons · double-click to zoom in · ⌘/Ctrl + scroll · drag to pan
Why the gateway, and not a direct call
The first design gave monitoring-read its own public Front Door origin. It was abandoned on a security argument, not a cost one: the console's identity lives in a different resource group, and the allow-list resolver only enumerated identities within the target group — so the computed allow-list would have been empty, which at the time meant accept any caller, on a service that had just become internet-reachable. Routing through the gateway kept the seam behind an existing, already-governed edge.

Both repositories, merged

RepoChangeWhat it doesState
OSCAR PR #153console-live-read seam Gateway routing block + six context endpoints on monitoring-read; the s2s call graph gains the console as a caller and monitoring-read as a callee; fail-closed caller resolution in both writers. merged
OSCAR as_of provenanceenvelope correction Freshness stopped meaning "when the API replied" and started meaning when the data was observed — max(TimeGenerated), nullable when nothing was observed. A stale panel can no longer look fresh. merged
OSCAR test pathsCI coverage gap Four test directories — including monitoring-read's own — were never collected by CI. Wiring them in took the repo-wide count from 3,740 to 3,804. merged
Console PR #10live-data seam Driver-neutral data-source seam with a DATA_SOURCE selector, the read-proxy provider with managed-identity token acquisition, the health-summary feature, and the scope health wall. merged
Console PR #11deploy readiness Made the deployment gap detectable: verify-easy-auth.sh is now a CI step, with strict tenant-scoped issuer validation and an id-token check. It compares declared configuration against the running app — which nothing did before. merged
Azure Entra app registrationsowner-executed api://oscar-monitoring-read-dev created; Gateway → monitoring-read granted; Ops Console → Gateway granted. Console → monitoring-read deliberately absent — the console must go through the gateway. granted
OSCAR Edge containment3 commits, merged Front Door WAF rules that close the gateway's public edge to everything except the paths actually consumed, plus a dot-segment rule and case-tolerant host scoping. Deployed and verified enforcing — see the next section. live

Shipped since this document was first written (2026-08-04 → 08-06)

The decision at that point was to take as much of the remaining work in-house as possible rather than queue it behind another team. This is what that produced.

RefWhat it doesState
OSCAR
PR #154
Added an authentication dependency to the nine gateway routes that had none, and made the dev: forgery lane switchable instead of implicit. Also corrected the WAF path allow-list after the first version took dev down — ca-webapp and ca-rcm transit that same edge, which the original "nothing consumes it yet" assumption missed. merged
OSCAR
PR #157
Turned self-asserted dev: tokens off on all three user-plane apps, bound the 837D claims routes to the caller's practice scope, and added a cross-tenant isolation test for the push channel — which found a real leak: an untagged live frame could reach a tenant-pinned subscriber. merged
OSCAR
32db54c7
Made the service-plane say why it refused a bearer instead of falling through silently. This is what turned a day of misdirected investigation into a one-line diagnosis — see section 10. merged
Console
e22077d
Flipped dev to DATA_SOURCE=live as a committed value. Also inverted a dated forcing-function test that would otherwise have demanded the wrong change in November (making an unset DATA_SOURCE mean live). merged
Azure
applied
Created the console's Entra app registration and applied its Bicep for the first time — Easy Auth, Key Vault, and all six runtime variables. CI only ever promoted the container image, so "merged" had never meant "deployed" for this template. 11/11

27 merged pull requests, across two repositories — the 11 August baseline

All authored under danielchavezs. Grouped rather than listed chronologically, because the shape matters more than the sequence: an observability foundation, then the seam that consumed it, then a week of authentication work that the seam made unavoidable.

The auth & security lane — seven PRs, 2026-08-04 → 08-11

PRMergedWhat it closed
#15308-04 Made monitoring-read consumable through the governed read-proxy instead of a fail-open public Front Door origin. The seam this whole page rests on.
#15408-04 Put a verified caller on nine gateway read views that accepted no principal at all, and made dev:-token acceptance an explicit capability rather than an implicit default.
#15708-06 Turned forged identities off in the IaC, closed two 837D PHI/tenancy holes, and found a live cross-tenant delivery bug in the push fan-out.
#17308-07 Shared the staff practice-membership resolver into lib/auth and opened the OIDC path on /live/negotiate. The piece originally scoped elsewhere — see section 06.
#17408-10 Built the OIDC user plane: two-issuer JWKS discovery, scoped read and write surfaces, and a production boot guard. Found three unscoped write surfaces nobody had looked at.
#18008-10 Closed the security-review residuals — OPA's role now comes from the membership row, DENY on the identity tables — and refuted one of its own earlier findings as a change that would have introduced a fail-open.
#18208-11 Contained four unauthenticated orchestrator routes at the edge, and made the CI pipeline able to carry the OIDC settings at all — before this, no environment could produce any value but local.
The observability foundation — 14 PRs

#85 · #89 · #90 · #93 · #105 · #108 · #109 · #110 · #111 · #114 · #115 · #117 · #121 · #122

2026-07-08 → 07-18. The OTel foundation, per-service instrumentation, the App Insights backend, ca-monitoring-read and its read contract, the synthetic prober, and the OBS-7 real-time escalation work that the push channel is built on.

This repository — 6 merged

#6 · #7 · #8 · #9 · #10 · #11

2026-06-22 → 08-04. Cross-repo docs and the read contract, the Next.js scaffold, the first live Azure deployment, then Easy Auth plus the driver-neutral data seam and the live OSCAR read path.

Not counted: #12 is open (UI restyle), and OSCAR #116 was closed without merging. Both would inflate the number.

The platform accepted more than anyone thought it did

Each row was found by looking, not by review comment, and each is stated with what the code actually did rather than a category label. The right-hand column is the honest part: several of these are merged and not yet deployed, and a status page that blurs that distinction is worse than one that omits it.

What it didWhat it does nowState
Nine gateway read views took no principal at all, reading tenancy from a caller-supplied practice_id. Reproduced from the public internet: 50 audit records and 769 KB of event payloads, no credentials. A verified caller and a server-derived read scope on all nine, plus the two 837D routes. live in dev
A self-asserted dev: string was accepted as step one of verification — before any signature check — and mapped the caller's own text into tenant, practice and org. An explicit capability, off unless a deployment opts in, with a hard floor in production. live in dev
GET /claims/{id}/edi-837d took no tenancy input whatsoever — any authenticated caller could read any practice's EDI claim content (PHI) from a claim id alone. No test covered it. Filtered on the stored record's practice; a scope miss and a genuine miss both answer 404, so a 403 cannot be used to confirm a record exists. live in dev
Five write routes had no auth assertion anywhere in the repository, including the one that writes OPA tenant policy overlays. Complete route inventories in test, so a route added without an auth dependency shows up as a missing entry rather than as silence. CI gate
An untagged frame in the live push fan-out was delivered to every tenant-pinned subscriber at once — one tenant's operator traces surfacing in another's stream. Scope comparison no longer skips when the frame side is empty; the in-process lane now matches the Web PubSub lane, so switching transports is a fan-out change, not a policy change. live in dev
OPA's actor_role came from a caller-influenceable claim. An Entra app role assigned to a user became their authorization role; office_manager would have carried 15 action classes. What prevented a write was an accident of policy data, not a control. The role comes from the resolved membership row for the practice being written, with the caller-supplied field stripped rather than merely ignored. live in dev
The identity tables that decide staff authorization were writable by nine service identities. The migration that created them claimed otherwise — but a GRANT only adds, and it issued no DENY. An explicit DENY role, enrolled for every writer. Five database-level tests assert it on every deploy. applied in dev
Token verification derived its key URL from a path Entra 404s, and matched a single issuer — so every staff member whose sign-in came from the consumer tenant would have been permanently, silently denied. Proper OIDC discovery per issuer, with the issuer matched against an allowlist before any key is fetched from a host the token names. merged, inert
Four orchestrator routes were reachable unauthenticated, taking practice_id from the request body. One served the full tool inventory, including which tools write and which return PHI. An edge containment rule covering seven bypass shapes. The application-level fix is deliberately not in that PR — the containment buys time, it is not the repair. merged
Two things this page will not claim
The human security sign-off is open and unsigned — deliberately left blank in the review dossier. The analysis behind it is thorough and adversarial, and it is also agent-produced and uncountersigned; the dossier says so itself, and its own most credible passage is the one where it retracts a justification it had fabricated. Treat the review as prepared, not granted.

And lib/auth is not ours to claim. Commit history across that package is roughly even between three contributors. What this lane did was find and close what it accepted; the package is co-owned.

Separately, a pre-existing production configuration issue was found while verifying the production side. It is unrelated to any of the above, recorded in the security dossier, and tracked on its own.

One piece of this was originally someone else's, and it is worth saying whose

The staff-identity model this work builds on is Abhijit Ramesh's. He designed the practice-membership concept, proposed it into the information model, wrote the migrations that created the identity schema and its resolver role, and implemented the original resolver in the application-api service on 2026-07-15.

With that workstream committed elsewhere for a fortnight, the team agreed on 2026-08-06 to absorb the remaining piece here rather than hold the push channel behind it. What changed is who carried the extraction, not whose design it is. Git history shows the shape plainly: four commits by him, then a 78%-similarity rename into lib/auth, then extensions on top.

Absorbed from that scope

The resolver itself, promoted from one service into the shared library so a second consumer could use it.

The identity concept and schema it reads, inherited as the authority rather than re-derived.

Making the original confinement claim true: the migration stated writes were restricted to one role; in practice nine identities could write. That gap was ours to close, and we did.

Ours from the start

The dev-token capability and its production floor. The entire gateway route surface. Two-issuer verification. The production boot guard. The edge containments.

The whole observability foundation the console reads through.

Every finding in section 05 — each one located by auditing what the edges accepted, not handed over as a task.

Why record this at all
Because the scope shift is the reason a page about one console's read path now documents platform-wide authentication — and because the identity model held up under a great deal of load it was not built for. Both facts belong in the record.

Read from Azure, not from the plan — verified 6 August

Every row below was re-queried against the live subscription on 6 August 2026, not carried over from the previous revision of this page. Two rows had inverted since it was written — the console's environment and its auth state both read "not applied" here until the deployment landed, and leaving them would have understated the position rather than overstated it.

ComponentObservedVerdict
ca-action-gateway-dev Running · revision --0000094 OSCAR_AUTH_ALLOW_DEV_TOKENS=false — self-asserted identities refused. ready
ca-monitoring-read-dev Running · revision --0000072 ready
ca-monitoring-prober-dev Running · revision --0000072 ready
ca-opsconsole-dev — env Seven variables present, including DATA_SOURCE=live, the three OSCAR read-path variables, and OPS_CONSOLE_OBS_EXPORT_DIR. Previously two. Applying the Bicep is what created the rest. applied
ca-opsconsole-dev — auth platform.enabled: true, RedirectToLoginPage, tenant-scoped issuer. Was null — the console was publicly reachable and unauthenticated. 11/11
ca-opsconsole-dev — export mount Azure Files volume obs-export at /mnt/obs-export; 18 of 18 JSON files on the share. Survived a subsequent CI image promotion (revision --0000013) — --set-env-vars merges rather than replaces the template. mounted
wps-oscar-dev · hub oscar_live Free_F1, anonymousConnectPolicy: deny, zero event handlers. ca-orchestrator-dev runs LIVE_HUB_BACKEND=azure + WPS_HUB=oscar_live. From inside ca-opsconsole-dev, /live/negotiate returns 200 {group: "oscar.live.all", expires_in_minutes: 60}; a garbage bearer still returns 401. Azure's own TotalConnectionCount went 0 → 1 during the probe. InboundTraffic is 0 — nothing has been published yet. Connected · idle
wafoscardev custom rules Three rules live: GatewayBlockDotSegments (p40, Block), GatewayPathAllowlist (p50, Block), RateLimitPerIP (p100, Block). The pre-existing rate limit survived being prepended to. enforcing

The containment, verified against the deployed rules

Six probes, run from outside Azure with no credentials. This is the check the runbook specifies, and it is the difference between “we wrote a WAF rule” and “the edge is closed.”

ProbeExpectedObservedWhat it proves
gateway /health/ready200 200 The deploy smoke path still works — the allow-list matches rather than blocking everything.
gateway /openapi.json403 403 The self-documenting schema is no longer public.
gateway /api/v1/worm-audit403 403 The exposure is closed. This returned audit records before.
gateway /health/../openapi.json403 403 The dot-segment bypass is closed — a prefix rule alone would have let this through.
gateway, upper-case Host403 403 Case-varied and trailing-dot host spellings still hit the rule, so the scoping cannot be side-stepped by one keystroke.
delivery /health/ready200 200 The control: the patient-facing edge shares this WAF policy and is untouched.
Why the order of these two changes mattered
Easy Auth went on before the live read was enabled, in the same apply. The reverse order would have produced a publicly-readable console holding an OSCAR service credential — a confused deputy serving OSCAR telemetry to anonymous callers under an identity they could never have obtained themselves. Until that apply, the fail-safe held: with DATA_SOURCE unset the seam resolved to the committed export, so the standing gap was an unauthenticated console showing static data rather than one leaking live OSCAR data.

“Live” is per-surface, and the UI says so

This is the part most worth showing, because it is the part most systems get wrong. Flipping DATA_SOURCE=live does not make every panel live. Twelve of the console's features read App Insights export tables that monitoring-read does not serve and was never intended to serve. Under live, those keep reading the export.

The seam therefore carries two fields, not one: kind is the mode the console is running in, and readsKind is where this particular table's numbers actually came from. They are allowed to differ, and the honest thing to render is the difference.

Why two fields

Collapsing them into one is exactly how export numbers get read as live ones in a review meeting. A test pins the distinction: under live, kind === 'live' while readsKind === 'fixture' for export-backed surfaces.

Consequence: a viewer can always tell which panels are telling them about right now.

Why the flag cannot simply throw

An earlier shape had the export resolver throw under live. That would have broken all twelve pages — and generateStaticParams, hence the build — the moment the flag flipped.

Now: live is a per-surface upgrade, not an all-or-nothing cutover, so the flip is reversible and non-breaking.

services/features/monitoring/source/select.ts tests/unit/monitoringSource.test.ts

The Bicep had never been applied. Now it has.

The console's infrastructure — Easy Auth, the Key Vault wiring, and the OSCAR variables — was fully declared in Bicep and had never been applied. The cause was structural rather than an oversight: the deploy pipeline promotes the container image and sets two variables, and no step ever ran the template. For this file, merged did not mean deployed.

PR #11 deliberately declined to paper over that by having CI apply it silently — the apply needs an app registration and a client secret. Instead it made the gap detectable, and that verification script is what now confirms the fix: it went from 3 of 7 checks failing to 11 of 11 passing.

Applied 2026-08-06Result
Entra app registration + client secretCreated, single-tenant, with enableIdTokenIssuance verified after the fact — the doc flags it as easy to omit and silently fatal. Redirect URI checked against the live FQDN rather than the runbook's copy.
Bicep main deploymentSucceeded, all five modules. Created Easy Auth, kv-opsconsole-dev, and the Key-Vault-backed secret.
Runtime variablesAll six present, including DATA_SOURCE=live.
verify-easy-auth.sh dev11/11 — tenant-scoped issuer (not /common), both audience forms, /health open, /services refusing anonymous callers.
The secret never entered a transcript or a file
Worth recording because it is the reusable part: the client secret was captured by command substitution inside a single continuous script, never echoed, and reached Azure only through Bicep's compile-time readEnvironmentVariable into a @secure() parameter. It now lives in Key Vault. One consequence to know: a future re-apply needs a fresh credential reset, because nothing retained the value.

Connecting the two systems found a real defect in one of them

Wiring the seam meant auditing what the gateway's public edge actually serves. That audit found that nine gateway routes carry no authentication dependency at all and read the tenant from a caller-supplied query parameter. Confirmed reachable from outside Azure with no credentials of any kind: audit records and event payloads, plus a self-documenting schema endpoint.

Two aggravating factors made it worse than a missing check. One route mints the gateway's own managed identity token to fetch data from a second service, so an anonymous caller borrows a service identity. And a prefix-based edge control turns out to be defeatable by a dot-segment prefix, which the audit also verified against the live endpoint rather than assuming.

Found before exposure grew

Nothing legitimate consumes that edge yet — the console is still on the export. So the edge could be closed at no cost to any real consumer, which is rarely true once an integration is live.

Containment, clearly labelled

The Front Door rules are marked TEMPORARY in the filename, the parameter description, the deploy script, the CI knob table, and Linear — with removal instructions. A stopgap that cannot be mistaken for a fix.

The prerequisite, stated

The auth fix must land before the console's read path is reopened at the edge, because reopening that path reopens the defect. Sequencing recorded on the issue, not left implicit.

A temporary mode, so nobody has to sit and wait
The proper fix is an authentication dependency on those routes, and that lives in the OSCAR gateway — outside this workstream's surface, so it needs the gateway's owners. Rather than block on it, the edge containment was built as an explicitly temporary mode: the exposure is closed today, the console keeps working against the export in the meantime, and the console's own integration work continues on schedule. Every trace of it is labelled — filename, parameter description, deploy script, CI knob table, Linear — with removal instructions, so it cannot quietly become permanent. It also ships with a log-only dry-run mode, used to validate the rules before enforcing them.

Stated plainly: the containment closes the exposure, it does not fix the defect, and it does not by itself enable the console's live reads. That still needs the auth work upstream.
Why this belongs in a status report
The integration was the reason the defect was found. A dashboard reading its own JSON would never have surfaced it. Tracked as CAN-283 (raised to Urgent), with the unauthenticated-routes defect recorded against it and the containment documented as temporary.

All five steps are done

  1. Gateway auth dependency. ✅ Every route now carries one and derives tenancy from the verified caller rather than from caller input. The route-level assertions cover complete inventories, not a sample.
  2. A real issuer for the console's token. ✅ Reframed, then resolved. This step was written believing the gateway had to move off its local auth backend. It did not: the service-to-service plane is verified first and never consults that setting. The original symptom was a different misconfiguration whose real cause was being swallowed without a log line.
  3. Apply the console's Bicep once. ✅ Done — section 09.
  4. Reopen the read path at the edge. ✅ Already in the committed allow-list default.
  5. Flip DATA_SOURCE=live. ✅ Committed and applied; the live read returns outcome: ok.
The one-line summary
Live OSCAR telemetry reaches the Ops Console, and the authentication work that turned out to sit underneath it is merged. What remains is not code — it is a signature, a transport ratification, and switching the user plane on.

Nothing here is waiting on another team

Resolved: the export-backed panels now render

The MONITORING_EXPORT_NOT_FOUND message the twelve export-backed panels showed was not an authentication failure and had nothing to do with the live read. Those panels read a committed telemetry export from disk; .dockerignore is deny-by-default and excludes obs_data/ from the image on purpose, and nothing mounted it at runtime. They had no source and — correctly — rendered as unavailable rather than as zeroes. That refusal to substitute a placeholder is the console working as designed; the missing input was the defect.

Fixed by mounting the export from an Azure Files share, which is the remedy both .dockerignore and the reading code already prescribed. Confirmed rendering in a signed-in session. The one-line alternative — un-excluding obs_data/ so it ships inside the image — was declined: it trades a documented data-handling boundary for build convenience, and that boundary is what stops the next directory of production-shaped data from riding along unnoticed.

But read those twelve panels as history, not as now
The original manual capture has been superseded by a committed 2026-08-24 rotation. Deployments still do not refresh it: the data lives on the Azure Files share, not in the image, so redeploying changes nothing. It moves only when someone captures, uploads, and repoints the mounted export. Those panels remain snapshots and report themselves as such, while the health-summary panel beside them is current. That is precisely why the console labels provenance per panel rather than per page.

The capture queries are now versioned in scripts/capture-obs-export.sh. It generates the export manifest and the historical-cost manifest before the provider's declaration gate is written; the loss guard makes an unexplained missing artifact fail loudly. The rate card remains the reviewed input, and the resulting reference estimate stays deliberately separate from OSCAR's measured telemetry.

Ours: the first read of a cold process overruns its budget

The console allows a request 12s; the gateway allows 10s. The first live read after a cold start took 15.0s and was aborted as MONITORING_MODULE_UNREACHABLE; the next took 4.9s and succeeded. monitoring-read holds minReplicas: 1, so this is not a container cold start — it is first-call warmup, most plausibly the JWKS key fetch plus a first uncached Analytics query. It self-heals on reload, which is precisely why it needs recording: an operator's first impression is a failure banner on an otherwise working path.

Also ours, and a deliberate non-decision
The cache-revalidation interval and the freshness threshold are both 300s, so a stale banner can flap between identical loads now that a live source has honest timestamps. This was predicted and is left open on purpose: which of the two numbers moves is a judgement about how fresh the console claims to be, and picking one while flipping a flag is exactly the unreviewed call to avoid.

Resolved: the resolver is no longer upstream, and no longer pending

The earlier version of this page listed a shared identity resolver as an upstream item belonging to another workstream. Both halves of that are now out of date: it was absorbed here on 2026-08-06 (section 06) and shipped across PRs #173, #174 and #180. It was never required for live data on this page — the live read succeeds without it, because the console authenticates as a service while the resolver serves user identity.

What it unlocks is a different capability: real-time push. Since that phrase gets read to mean several things, here is exactly what it does and does not mean.

Pull versus push, in plain terms

Today the console pulls. You load a page, the server asks OSCAR, you see a number. To see a change you reload — and the live panel caches for five minutes, so a reload may show you the same number anyway.

With push, the server sends. The browser holds a connection open; when something happens in OSCAR, a message arrives and the panel updates itself. No reload, no clicking.

The behaviour that matters most is not auto-refreshing dashboards, though. The design is named real-time escalation and safety interrupt, and that is the point: the console stops being something you check and becomes something that interrupts you. An agent run going wrong, a safety interrupt firing — you would know as it happens, not on your next visit.

What to expect

Panels that update themselves while you watch, with no reload.

Being notified when something needs intervention, rather than discovering it later.

Per-person delivery: you receive only your own tenant's events, decided by the server.

What NOT to expect

It will not make the twelve export-backed panels live. Push carries orchestrator events, not App Insights telemetry — a different pipe entirely.

It does not make every panel live or globally filterable. The measured daily-cost table has a local date-range control over its own snapshot rows; that is not a cross-console timeframe query.

It is not "truly instant": telemetry ingestion has its own delay of a minute or two regardless.

What is gating it now — and none of it is code

The resolver was one of three gates and it is closed. What replaced it on the list is not more engineering: it WAS a signature, a decision, and two switches. All three have since landed — see the strikethroughs below.

  1. ✅ The shared resolver. It answers "which practice's data is this person allowed to see?" Done, and it went further than the original ask — it also fixed a single-issuer assumption that would have permanently locked out every staff member signing in from the consumer tenant.
  2. ✓ A human security review of the push boundary. Signed 2026-08-12 on the OSCAR side, scoped to dev. The console-side boundary is covered by DR-26: the stream is BFF-terminated (the negotiate URL is a 60-minute all-tenants receive credential and never reaches a browser), and the console independently re-imposes the closed 44-name frame projection rather than trusting the upstream one — that exact boundary regressed upstream once, when PHI rode a KEY past every value gate.
  3. ✓ Ratifying the transport (DR-11, Web PubSub). Ratified 2026-08-13, with acceptance gate 3 (topic.priority.p0) explicitly carved out rather than pretended: there is no p0 producer in OSCAR at all, so that gate is not closeable by this repo. See the DR-11 detail section in the decision register.
  4. ✓ Turning the user plane on. Live since 2026-08-12: ca-orchestrator-dev now runs OSCAR_AUTH_BACKEND=oidc with a real issuer set and OSCAR_AUTH_ALLOW_DEV_TOKENS=false. That is what let the push transport's own code predicate (_live_hub_auth_is_production_grade()) pass, which is why the channel could be switched on at all — the gate was never an environment name, it was the forgery capability. The console still calls as a SERVICE principal (DR-26): its Easy Auth token store is disabled and an Airiam operator holds no practice membership, so the user path is foreclosed for us regardless.
The one thing to take away
Live data on this page: working. Real-time push: proven on 13 August — the transport is live in dev, DR-11 is ratified (with gate 3 carved out), and a real agentic turn delivered 11 accepted span frames to the console. The stream is still a tail: it begins when the page opens, so a demonstration must create or observe a new turn rather than expect historical events to replay.
Ops Console — live data integration & platform auth hardening · first written 4 August 2026, updated 25 August 2026
Runtime figures in section 07 were read from subscription 8ae2e5a1, resource groups rg-oscar-dev and rg-opsconsole-dev, on 6 August. The 25 August supplement cross-checks merged PRs and local dev history; it deliberately does not relabel that earlier Azure observation as a fresh runtime probe.
Companion documents: docs/DEPLOYMENT.md · docs/decision-register.md · OSCAR docs/DC/CAN-173-user-plane-activation-security-review.md · docs/DC/DR-11-push-transport-gate-dossier.md