Helpers/QueryParams replaces fourteen private days/limit parsers, the three
TryParseUtc copies, eleven identical pageSize blocks and ~50 inline TryParse sites
under Functions/ and Pagination/. Lenient (Int/IntOrNull/UtcInstant): absent, garbage
or below-minimum means the call site's default, above-maximum clamps. Strict
(TryInt/TryPageSize/TryUtcInstant): absent is null, anything else invalid is a 400
with a message that names the parameter. Integers are invariant-culture, instants are
invariant UTC (AssumeUniversal | AdjustToUniversal), whitespace counts as absent.
Defaults and ranges stay arguments at every call site.
Settled differences: the Apps windows clamp above 365 instead of falling back to 30,
days=0 or negative yields the default everywhere, the two global geo endpoints and
the session list cap days at 365, SLA months is capped at SlaMetricsService.MaxMonths
on the HTTP layer, a whitespace date means no filter on every route, the admin batch
sizes reject values above their maximum, and the three culture-dependent
DateTime.TryParse sites parse as UTC.
QueryParamGuardTests ratchets TryParse out of Functions/ and Pagination/ (baseline:
headers, raw cells, the agent version major, the se= of a stored SAS URL);
QueryParamsTests carries the cases the removed copies pinned.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TelemetryItemKind moves to Shared; TelemetryItem.ToWire() maps onto TelemetryItemDto
and the uploader serialises the DTO, so both ends share one wire class. The spool line
keeps writing Kind by name (StringEnumConverter registered in TelemetryItemSerializer).
IngestTelemetryFunction.PartitionBatch routes by exact kind name and collects every
item it cannot route or parse; a non-empty rejection list answers the batch with 422 +
TelemetryItemsRejectedResponse (poison, rejectedRowKeys, reason) before anything is
persisted, logs a Warning and writes the new TelemetryItemsRejected ops event. The
agent's existing poison protocol drops exactly the named RowKeys and re-uploads the
rest; before this the ingest skipped the items, answered 200 and the agent cleared
its spool.
Contract tests from both sides: TelemetryWireContractTests (agent) freezes the real
batch body under tests/fixtures/telemetry-wire/ and parses the pinned poison body;
TelemetryWireFixtureTests (backend) dispatches every frozen body with zero rejections,
requires an arm per kind and pins the poison body.
Agent-facing responses implement IApiResponse and go through ResponseHelper, request
types carry [WireContract]; plain ingest refusals use the error envelope, the
register-session and upload-url refusals keep their DTO bodies (the agent reads
ErrorCode/Message). Manifests and generated wire types regenerated (also picks up
four types HEAD had left out of the manifest).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PlatformStats(global,current) took a GET plus a full-row upsert without ETag
from every ingest batch: increments were lost under concurrency and a writer
clobbered fields another writer had just changed. RuleStats put every tenant's
daily rule traffic into one date partition (1,459 rows on 2026-09-04), the
writer issued four serial calls per evaluated rule without ETag, and the
reader was a cross-partition scan whose 10k cap dropped the newest dates.
- D-198: TotalEnrollments / SuccessfulEnrollments / TotalEventsProcessed have
no hot-path writer; the two-hourly recompute is the only source. Only
IssuesDetected still increments, through TableCasRetry (ETag + field-only
merge, AddEntity for a missing row, conflicts as StorageCasConflict). The
tenant counters share the helper.
- D-199: RuleStats PartitionKey "{scope}_{date}", RowKey ruleId.
RecordRuleStatsAsync merges a session's increments per rule, reads the
partition once and writes one transaction (UpdateReplace with ETag / Add),
retrying on 412/409. Readers are PartitionKey ranges per scope; aggregation
and the regression radar fan out over the registered tenants; the cleanup
deletes per scope. RuleStats joins the offboarding composite wipe.
- Legacy rows (PartitionKey = date) are read by an extra query and cleaned by
a date range that checks every row for a bare-date key, until 2026-12-05.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- DiagnosticsBlobStreamer disposes the download result on every exit path
(it was disposed only on the 413 branch), measures open and transfer
separately and fires DiagnosticsDownloadProxied after the copy with
Completed=true/false.
- UserDelegationKeyCache: one user delegation key per process (24 h, re-mint
under a lock with 2 h margin or when a SAS would outlive it) instead of one
storage round-trip per download or upload SAS.
- KevDataService is resolved as one singleton over the typed-client handler
pipeline; the cache warmer used to hydrate a throwaway transient instance
while the correlation service held a cold one.
- Admin and tenant configuration services single-flight concurrent cache
misses (Lazy<Task> per key); the MemoryCache and the in-place projection
semantics stay untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The dependency filter dropped every successful storage call and the expected
412/409 outcomes, so storage-side latency and ETag contention were invisible
until a call failed outright (seven days of App Insights held zero storage
dependency rows).
- StorageDependencyFilterProcessor keeps a successful storage call at or above
2 s and stamps SlowStorage=true; the operation name is on the row, the table
name is not (the Azure SDK span carries neither URL nor table).
- StorageMetrics wraps TelemetryClient.GetMetric("StorageCasConflict",
operation, table, outcome): pre-aggregated per minute, bypasses the worker's
adaptive sampling, null-tolerant for tests.
- The CAS loops report conflicts: session event count, session counter
reconcile, session store, tenant stats, MCP user/tenant usage.
- TableStorageService keeps its two-argument constructors (Moq matches by
exact arity); DI resolves the three-argument overload.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every entity-group transaction was chunked by count only; 100 payload-heavy rows
(Signals with chunked PayloadJson, Events with 30k-char Message + DataJson) could
exceed the 4 MiB transaction cap, and the failure surfaced as an opaque 500. The
agent halves its batch only on 413 and treats 5xx as "replay later", so such a
batch wedged the device forever.
- TableTransactionBatcher: 100 actions, 3.5 MB per transaction, 1 MB per entity,
never across partitions; an entity that cannot fit throws
TableEntityTooLargeException. Used by the event, signal, transition, restore
and rules writers; delete-only batches keep their count chunking.
- StorageErrors: one classifier for RequestFailedException (transient 408/429/
5xx, payload-too-large by status or error code, already-exists incl. the
Azurite 400 shape); the two IsAlreadyExistsStatus copies use it.
- Event writer: per-row fallback only for data-shaped rejections; throttling,
outages and oversize batches propagate. Single and batched writer share one
entity builder.
- IngestTelemetryFunction: payload too large -> 413, transient -> 503 with
Retry-After: 5, anything else 500.
- TableStorageChunking.MaxPropertyChars is the one 30,000-char bound.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The five cross-session index tables (SessionsByTerminal, SessionsByStage,
DeadEndsByReason, ClassifierVerdictsByIdLevel, SignalsByKind) were write-only
since M5.d: no endpoint, MCP tool or web page ever read them, while the
dual-write cost a serial queue send per signal/transition on the ingest path,
a 5-13 min full scan of Signals + DecisionTransitions every two hours and five
property-filter scans per session deletion (audit 2026-09-05 F05).
Removed: the tables from TableNames.All (schema hash changes, next cold start
runs a full pass without them), the telemetry-index-reconcile queue name,
producer, envelope, handler, queue function and 2h timer, the cross-partition
Timestamp scans on the signal/transition repositories, the EnableIndexDualWrite
flag (model, config mapping, admin UI, generated wire types), cascade steps
11-15 (later steps renumbered 11-18), the deletion wrappers and the offboarding
wipe entries. DiscriminatorPkProp stays as a step class so manifests written
before this change still resume.
Order of operations: deploy first, then delete the queue pair and the five
tables in the portal (B-88h). Deleting the tables before the deploy 500s every
delete preview/enqueue and poisons offboarding.
Submodules: .claude/commands (import_session.py), internal (D-196, docs, plan).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Audit 2026-09-05 F01/F02. Analyze, VulnerabilityCorrelate, TenantAutoApprove,
IndexReconcile and ImeMsiArchive are now host-managed queue functions under
Functions/Queue bound to the DataStorage connection (identity-based, resolved
by the host): the Flex scale controller wakes an instance for a message, the
host renews the lease and owns the retry/poison ladder. The four cascade
workers with lease/CAS state stay self-polling BackgroundServices; their base
now receives one message per poll so waiting messages can no longer expire
under another worker. Terminal enrollment alerts and hardware-rejection alerts
are enqueued on notification-dispatch (channel ids only, resolved at send
time) instead of fire-and-forget sends the host could drop on scale-in.
Requires the app setting DataStorage__queueServiceUri before deploy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
withToolTelemetry mints a UUID per tool call and runs the handler in a
ToolCallContext (tool name + correlation id); apiFetch stamps it as
X-Correlation-ID next to X-MCP-Tool-Name, so every backend request of the call
(pagination scans included) and the MCP_TOOL_LOGGING tool_call line carry the
same id — a row-level join between the MCP log and the backend's request rows.
ApiError reads the X-Correlation-ID response header (body correlationId as
fallback), and the error handler names it in every branch — 5xx, quota 429,
structured 4xx and the unstructured 401/403/404/429 texts that had none. The
handler reads the envelope's `code` (errorCode as the pre-envelope fallback),
the quota body's `error`, and the log proxy's `upstreamCode`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
authenticatedFetch mints one UUID per logical call (kept across the 401 retry,
where caller headers now survive too), sends it as X-Correlation-ID — the id
the backend echoes, stamps on its request row and writes into every error body
— and tracks api_request_failed {path, method, status, correlationId} for any
final status >= 400. App Insights gets enableCorsCorrelation + W3C
distributedTracingMode scoped to the API host, so portal dependency rows and
backend request rows share an operation_Id.
fetchJson/ApiError read the envelope (error, code, correlationId, hint; the
pre-envelope `message` as fallback), describeApiError renders message +
short reference, addNotification takes an optional reference the bell renders
as "Ref …"; the error boundaries show and track Next's digest. Consumers of
renamed fields follow: delete-session parser (code ?? hint, PascalCase codes),
app-homing (code ?? reason). API_ERROR_CODES comes from the manifest, and
HOME_TENANT_UNRESOLVED plus the cascade codes are `satisfies ApiErrorCode`.
InstallsTab is the first site showing the reference.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every non-2xx body in the Functions project now goes through ResponseHelper
(ErrorAsync / BadRequestAsync / NotFoundAsync / ForbiddenAsync /
UnauthorizedAsync / ConflictAsync / InternalServerErrorAsync): 408 sites by a
pattern-exact rewrite of the CreateResponse + WriteAsJsonAsync + return shape,
50 by hand. The eight private BadRequestAsync shadow helpers are gone; the raw
500 catch blocks use InternalServerErrorAsync (sanitized message, correlation
id, MCP hint). 429s keep Retry-After via retryAfterSeconds.
Specialised error bodies where a consumer reads more than the prefix:
AppHomingDeniedResponse (kebab reason codes now Constants.AppHomingReasonCodes,
probe result), TenantConfigPatchFailedResponse (backupId, drift),
QueryBackendLogsErrorResponse (upstreamCode, statusCode, source, hint,
upstream). Twenty-one message-first 200 bodies that the old error tolerance had
let through are typed too (MessageResponse, PreviewWhitelistActionResponse,
SuccessMessageResponse). New codes: PayloadTooLarge, UpstreamError,
UpstreamTimeout, the three trial-gate codes; the agent security path
(SecurityValidationExtensions) writes the envelope with the retry window.
TypedResponseGuardTests: WriteBaseline is empty again — the anonymous
WriteAsJsonAsync ratchet now covers error bodies with zero tolerated sites.
Manifest and generated TypeScript regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every error body now leads with { error, code, correlationId }: the generic
ApiErrorResponse (hint?, retryAfterSeconds?, operation?) plus the specialised
McpQuotaExceededResponse (reordered; `message` became `error`),
DelegatedSlotLimitReachedResponse (+correlationId) and the new
SessionDeletionRejectedResponse (replaces the anonymous delete arms). The
correlation id is stamped by the writers — ResponseHelper.ErrorAsync and the
new ApiErrorWriter for the middleware side (auth 401, policy 403/503, rate
limit 429, MCP quota 429, global exception 500) — never by call sites.
exceptionType and rateLimitInfo leave the wire; Constants.ApiErrorCodes is the
code vocabulary and is exported as the manifest section `apiErrorCodes`
(API_ERROR_CODES for web + MCP). CorrelationIdMiddleware names the readable
headers via Access-Control-Expose-Headers.
Guard: TypedResponseGuardTests no longer tolerates anonymous error shapes;
the 458 legacy sites in 149 files are frozen in WriteBaseline to melt down.
New facts: envelope-prefix reflection, ApiErrorWriter body/header pins,
exact-shape pins for the generic envelope and the changed specialised bodies.
Reverses the "error bodies stay anonymous" clause of D-043 (decision entry
follows with the docs stage).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first cut refused every reader-zone-fallback endpoint. The DeviceSetup line is
written before the agent starts and is backlog on every device, so that silenced
ANALYZE-ESP-001 for all agents without an era anchor (control sessions 2708e6b4 and
18d80f9c came back "not measurable" after the deploy). Two fallback endpoints share the
writer-zone error and cancel it; only a span mixing a fallback endpoint with an anchored
one (or with "now" for an open phase) is unmeasurable — the a7140f98 shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hybrid_login_pending now measures the absence of a real-user desktop after a reboot
(cancelled by the DesktopArrivalDetector or a real JoinInfo user), is never armed on
WhiteGlove devices (resume marker, Part-2 flag, .part1- archive) and reports the
JoinInfo placeholder as a fact only — on a Hybrid join the AD sign-in never replaces it.
New EntraUserAffinityDetector: armed at the real-user desktop on Hybrid devices, the
first IME-TOKEN-SUCCESS line after the desktop posts ime_user_token_acquired; ten
minutes without one post entra_user_affinity_pending with the IME token-failure codes.
ImeLogTracker exposes raw per-line token callbacks past the historic-replay gate;
HelloTracker surfaces User Device Registration 304/305 as device_registration_event.
ANALYZE-ID-004 v2.0.0 keys on those events (event_type not_exists gained a branch),
GATHER-DEVICE-006 follows, GATHER-DEVICE-007/008 collect the Entra operational log.
Constants + MCP catalog gain the five types (D-072), the guard test learns the
multi-line OnTraceEvent shape, gather_* outputs pass the MCP filter gate by prefix.
CMTrace: ImeLogEraPreScan/CmTraceEraTable anchor the backlog era that holds the
bootstrap execution record against the Deployed install marker (0.13-0.36 s in the
field), whole era or nothing, sibling files by local time; origin era-anchored.
Backend phase_duration/app_install_duration refuse a reader-zone-fallback endpoint.
Decisions D-190, D-191. Session a7140f98.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A service principal (idtyp=app, typically behind a federated credential) is admitted only
with the access_as_application application permission and then keys every role table,
binding, audit actor and throttle bucket as app:<client-id>. Caps at resolution: Viewer in a
tenant, DelegatedReader on managed tenants, never a platform role; the roleless catalog tiers
are closed to applications except GET auth/mcp. MCP guard keys on the same principal key.
Members / MCP Users / Delegated Access get a User / Service principal switch; usage views
badge App; McpServicePrincipalFirstSeen ops event and PrincipalKind request dimension.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A tenant in the owned group of a permanent-Pro tenant is effectively Pro with source "msp":
badge "Pro (MSP)" (also over its own Pro), the Pro entitlement set without the delegation
right (no transitive delegation, no slots), no trial CTA. Nothing is written for that —
TenantConfiguration.ManagedByProTenantId is a load-time projection from a process-wide
index over the Tenant Groups table (owner tier read raw, never through the config service),
so the end of the delegation, the manager losing its permanent tier, or its offboarding
revert automatically. The one write is the retention grace anchor (ProDowngradedUtc),
stamped by ProConferralService from the four paths that end a conferring relationship.
- FeatureEntitlementCatalog.Resolve → EditionResolution { Edition, Source, OwnPro };
Get(resolution) returns ProViaMsp for conferred Pro; the 3-argument ResolveEdition is
gone so every caller resolves from the config (incl. the projection)
- DelegatedAdminService home gate checks the DelegatedAdminAllowed entitlement, not the edition
- TrialExpirySweep reads through the config service: a managed tenant's expired trial emits nothing
- feature-flags + plan response carry editionSource; config/all projects payingCustomer and
managedByProTenantId; shared manifest + web/MCP codegen regenerated
- PayingCustomer: GA-only sales bookkeeping flag on the plan endpoint, no entitlement effect
- web: "Pro (MSP)" label/tooltips, plan section badge, delegated-access hints, tenant
management chips + paying checkbox
Tests: catalog matrix, ManagedTenantProIndexTests (incl. mutual management without recursion,
fail-closed on storage error), ProConferralServiceTests, projection on every config read path,
feature-flags payload, parity facts, serialization (projection never stored), plan transitions,
edition gate, sweep. Backend 5902 green, web tsc/vitest/lint clean, MCP tsc/vitest green.
Concept doc + D-188 in internal (gitlink); customer docs in the docs repo (MCP redeploy needed).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Click a bucket to narrow the map to it, add more with further clicks, Show all
or the last toggle returns to the full map; a Color-by switch resets it. Camera
fit and marker radius scale stay on the full location list so filtering never
moves or rescales the map. Customer docs updated in the docs repo.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL flagged the new denial flow (cs/log-forging #83-#87): header values and the request
path travel from the deny branch into a log line, the ops-events table and the alert
channels. Headers were already stripped of control characters and capped; the path was not,
and Request.Path is percent-DECODED, so %0A arrives as a newline. BuildPrivilegedDenial now
cleans method and path (LogSanitizer) and caps the path at 256 chars, once, before any sink.
Test pins CR/LF stripping and the cap. The remaining alerts are the known false-positive
class (structured ILogger sinks with sanitized discrete properties).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live check after the x29 deploy: a KQL syntax error came back as 502, and the MCP sanitizes
every 5xx down to the error code — the syntax message and the forwarded upstream JSON were
invisible, exactly the parity loss the proxy exists to avoid. Upstream 4xx (other than
401/403) now maps to 400 with message, hint and `upstream`; 401/403 (the managed identity's
missing read grant, with a hint naming the role) and store 5xx stay 502. The MCP's structured
4xx renderer appends `upstream` as a fenced block (capped), and keeps sanitizing 5xx.
Tests: status mapping theory, hint texts, upstream rendering on 400 and non-rendering on 502.
Backend 5849, MCP 693 green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 9eb8cac5 filters covered gitlink bumps and prose but not the
workflow files themselves, so every edit to build-agent.yml or a deploy
workflow still cost a full CI and CodeQL run. Neither reads any other
workflow: CI runs the suites and drift guards, CodeQL analyzes C# and
TypeScript. paths-ignore cannot negate, so each file lists the other
twelve workflows explicitly and leaves itself out; .github/scripts and,
for CI, .github/codeql join the list for the same reason.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
build-agent.yml gains an optional release_notes input; the release body opens with
"What's new" from that input, or a generic fallback linking to the docs changelog
plus a marker the operator tooling uses to anchor the next notes, so releases
dispatched by hand leave no gap. --generate-notes dropped (pull requests only);
a compare link to the previous tag replaces it. Plan-first rule: plans live in
internal/work/plans from the first draft. Gitlinks: internal, .claude/commands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Any signed-in user holds a token the GA-only routes refuse with 403, so the assume-breach
alarm was a push-flood vector: one event per caller AND route per hour, multiplied by
Flex instances. PrivilegedDenialReporter now keys the throttle on the caller alone (a sweep
over the whole GA surface is one event; every attempt still lands in the Warning trace) and
carries a fixed per-instance window budget: 5 events per hour, then ONE Critical storm
marker (same event type, so the same Telegram rule pushes it) and trace-only silence until
the window resets. Worst case per instance and hour: 6 pushes. Global Reader slips count
against the budget too. Tests pin the per-caller throttle, the storm marker and the Reader
accounting; D-185 amended.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One call replaces the six fetches of the backend-version skill: backend /api/health vs
versions/backend.json, portal /version.json vs versions/web.json, this server's own build
(build-info.ts, now shared with /health and the boot line) vs versions/mcp.json, and the
agent release manifest. Per component: live, manifest, drift (in-sync | drifted | unknown |
not-applicable) with a note that names the missing half — a manifest 404 is "never deployed
since manifests exist", never a silent in-sync; failed fetches land in partialErrors
(allSettled). The well-known hosts join config.ts as the URL guard requires. Platform scope
(GA + Global Reader); catalog snapshot updated. The local git comparison stays in the skill.
Closes the build of backlog x29 (all four phases); deploy + live verification tracked as
backlog `sch`, the user-side RBAC / app settings / alert rule as `ket`.
Tests: deployment-state (drift arithmetic, fan-out, 404, unreachable), MCP 691 green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The report ZIP a tenant admin submitted and the preserved diagnostics copy could only be
fetched with a storage key through an az script. They now follow the diagnostics ticket
model: mint under the caller's JWT, download with a short-lived HMAC ticket, bytes proxied
through the backend — a storage SAS never leaves it.
Backend
- DiagnosticsDownloadTicket gains a purpose parameter (default diag-dl-v1, new
report-dl-v1): a ticket for one download surface is rejected on the other (wrong_purpose).
- POST global/session-reports/download-ticket (GlobalReadOrAdmin, same tier as the portal's
download-url): flat-name guard, existence check, best-effort size, ticket bound to the
caller's home tenant; reuses DiagnosticsDownloadTicketResponse (destination SessionReports).
- GET global/session-reports/download (PublicAnonymous, ticket is the sole authority):
per-IP limit + decode via the new TicketDownloadPrelude (shared with the diagnostics route),
streamed from the session-reports container with BlobStorageService.OpenReadAsync.
- Constants.BlobContainers.SessionReports replaces three duplicated literals.
- Pins: catalog completeness/JWT-exempt theories, auth-skip, no-store, purpose tests.
MCP
- get_session_report_download (platform scope) mirrors get_session_diagnostics: kind
(session-report / preserved-diagnostics / diag-files-report), size, download URL, expiry,
instructions; catalog snapshot updated.
Submodules: session-report-debug skill on the tool, fetch-report-zip.sh deleted; plan/todo.
Tests: backend 5839, MCP 683, solution build green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
POST /api/global/raw/logs (MCP query_backend_logs) now takes `source` = backend | web | mcp
and `budgetSeconds` 5..180 (default 30). Parity with the az CLI was already by construction
(same Kusto REST endpoint, verbatim query); the four remaining deltas were properties of the
proxy and are gone (D-186):
Backend
- LogQuerySources (Shared vocabulary, exported as manifest section `logSources`) and
LogQuerySourceCatalog: APPINSIGHTS_APP_ID → backend, APPINSIGHTS_WEB_APP_ID → web (both
api.applicationinsights.io), MCP_LOG_ANALYTICS_WORKSPACE_ID → mcp (api.loganalytics.io).
A missing setting makes one source answer 503 naming it, never the others.
- AppInsightsQueryFunction: injectable IHttpClientFactory (named client without timeout) and
TokenCredential; one CancellationTokenSource per request plus `Prefer: wait=<budget>`
upstream, 504 names the budget; typed QueryBackendLogsResponse keeps the Kusto
tables[].columns/rows verbatim (JsonElement cells) and adds source, budgetSeconds,
elapsedMs and `partial`/`partialReason` when the store cut the result (previously
swallowed); on failure the full upstream error JSON rides along as `upstream`.
- TypedResponseGuard: the last SerializeBaseline entry is gone (typed request record).
MCP
- query_backend_logs: source (z.enum from the generated LOG_SOURCES), budgetSeconds, a client
signal of budget + 15 s, rawTable result cap, description with per-source table hints.
Submodules
- .claude/commands: backend-logs / ux-insights / mcp-telemetry on `source`; query-appinsights.sh
deleted (35 call sites rewritten); az only for the live container tail.
- infra: migration scripts carry the two new app settings and role assignments
(Monitoring Reader on the portal component, Log Analytics Reader on the MCP workspace).
- internal: concept doc backend/log-query-proxy.md, D-186, docs and plan updated.
Tests: catalog resolution, budget clamp, Kusto parsing incl. PartialError, error extraction,
wire parity of the new response; backend 5832, MCP 683, web tsc, solution build green.
Before live use: grant the two roles and set the two app settings (see the plan file).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reverts the HasAccountSetupActivity tightening from f0a3f043: measured on 485 Classic
(SkipUser=false) sessions, 96 see a single Shell-Core 62407 before any AccountSetup
progress and 75 of them complete AccountSetup only through the user-apps-settled synthesis,
which needs that exit remembered as the edge. Windows confirmed categorySucceeded in 0/485,
so the synthesis is the load-bearing completion path; presence semantics stay and are
documented as such.
UserEspKeepAwakeHost instead gets an additive progress probe
(ProvisioningStatusTracker.HasAccountSetupProgress via EspAndHelloHost) and releases on an
EspExiting that arrives after the AccountSetup registry left notStarted — the real user-ESP
page exit (progress precedes it by >= 1 min fleet-wide; the Device->Account handoff exit
always precedes progress). AccountSetupProvisioningComplete no longer releases; desktop
arrival is deliberately not a trigger (explorer.exe ran before the final exit in all 385
two-exit sessions). Sessions with only the handoff exit hold until the safety cap. Release
reason user_esp_exited replaces account_setup_complete. D-183 rewritten.
Tests: host (release on exit+progress, handoff exit holds, completion no longer releases,
exit before engage ignored), tracker (presence stays true / progress false on the pre-written
all-notStarted JSON). Solution build 0 warnings; agent suite 2823 green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 2026-09-04 audit found no bypass of the GA boundary, but a failed attempt left
only a Warning trace without identity, and a non-GA tools/call for a GA-only MCP tool
never reached the backend at all. This makes the boundary observable without adding
a second alarm writer (D-185):
Backend
- PrivilegedRouteDenied (Security): raised from the policy middleware's deny path for
every 403 by an authenticated caller on a GlobalAdminOnly route. Critical for a
caller without a platform role, Warning for a Global Reader. Details carry upn, oid,
tid, client source, MCP tool name, correlation id, reason and policy. Throttled one
hour per caller+path per instance (PrivilegedDenialReporter, IPrivilegedDenialReporter
seam). PolicyResult now surfaces the denied tier; the DENIED log line gains oid, tid,
clientSource and mcpTool.
- GET global/raw/access-probe: no-op GlobalAdminOnly route whose denial IS the signal
(AccessProbeResponse DTO, catalog entry, parity test).
- Defense in depth: AppInsightsQueryFunction and both TableQueryFunction triggers re-check
IsGlobalAdmin in the body (RawGlobalAdminGate); an empty RequestContext fails closed.
- Dual-register: OpsEventTypes.All, OpsAlertRulesSection.tsx, regenerated manifests.
MCP
- ga-tool-probe.ts: before dispatch, a caller without the GlobalAdmin role naming a
strictGa tool leaves an unconditional [mcp-security] line (tool, upn, scope) and fires
the access probe with X-MCP-Tool-Name; the SDK answer stays "Tool not found".
- GA_STRICT_TOOL_NAMES exported and pinned equal to the registered GA-minus-Reader set.
- debug-session prompt names query_backend_logs/query_table only for strictGa (Readers
were told to call tools they cannot see); tool_call_rejected lines carry scope + upn;
config-tool comment corrected against the actual catalog tiers.
Tests: reporter (severity, details, throttle, fail-soft), middleware policy surfacing and
IsPrivilegedDenial/BuildPrivilegedDenial, Program.cs middleware order source scan, raw
gate 403, probe catalog + JWT pins, wire parity; MCP probe unit tests, Global Reader over
HTTP on both eras, runtime refusal fires the probe, prompt gate.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Agent: ProvisioningStatusTracker.HasAccountSetupActivity now requires at least one
AccountSetup subcategory past notStarted. Windows writes AccountSetupCategory.Status with
every subcategory notStarted BEFORE the Device-ESP page exits, so the presence check
confirmed the Device->Account handoff (Shell-Core 62407) as the final ESP exit; the
user-apps-settled synthesis then completed AccountSetup and released the User-ESP
keep-awake minutes before the real exit (fleet: 77 % of keep-awake sessions, median 3 min;
on battery that is enough for Modern Standby). D-183.
Rule engine: filterField/filterOperator/filterValue now apply to event_data and
event_data_array as a same-event filter (shared ApplyValueFilter with event_count).
ANALYZE-DEV-012 v1.1.0 excludes backfilled pre-session episodes (backfilled equals false);
a second independent condition could not, since each condition scans all events on its
own. D-184. Schema, Shared model, MCP authoring guide and generated types follow.
Tests: ProvisioningStatusTrackerAccountSetupActivityTests (the verbatim all-notStarted
registry JSON), SleepEpisodeRuleTests (+3 backfill cases), RuleEngineEventDataFilterTests.
Solution build 0 warnings; agent suite 2820 and backend suite 5791 green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>