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>
Every "chore: bump internal gitlink" commit ran the full CI matrix and
both CodeQL analyses, because a gitlink bump reports the bare path
'internal' (no trailing slash) and the existing 'internal/**' filter
does not match it. Both workflows now ignore the bare and nested forms
of the three private submodule paths plus markdown, LICENSE and the
.github metadata files. CodeQL's weekly unfiltered schedule remains
the backstop.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- .gitmodules: docs -> internal, repo renamed to Autopilot-Monitor-internal; the OKF bundle now lives at internal/docs/
- .claude/CLAUDE.md rewritten: where knowledge lives, working-file rules, hard rules; imports the private half from internal/CLAUDE.md (silently absent without submodule access)
- .claude/architecture-principles.md moved into the internal bundle (reconciled against the code)
- tasks/ (gitignored) retired; working files live in internal/work/
- doc path references docs/<bundle> -> internal/docs/<bundle> in code comments, skills gitlink, codeql paths, build script
- rule descriptions no longer cite an internal doc path (customer-visible text); rules/dist regenerated
- public tree no longer names private operator tooling
- .claude/commands gitlink: skill docs point at the new paths
A domain typed into the cross-tenant filter only became a tenant ID when a suggestion was
picked; submitted raw it was dropped by the fetch (not a GUID) while the client scope compared
it against session tenant IDs, leaving zero rows and "No sessions found for this tenant yet".
Submit now resolves GUID / exact domain / unique substring, updates the box to the ID, fetches
with the resolved value, and the empty state says "No tenant matches" for unresolvable input.
The search box never matched the tenant column: tenant ID and domain are now part of the
searchable text and the suggestion dropdown (cross-tenant only), sourced from one
tenantId-to-domain map shared by table and filter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two tenants clicked the migration banner, consented the new app, and were refused by the
role-superset probe without ever learning why: the previous app's service principal holds a
tenant-side add-on grant (DeviceManagementScripts.Read.All) that no re-consent can produce, and
the grant command on the Optional Graph capabilities page pre-fills the homed (previous) app.
Backend: the probe now separates manifest roles (still propagating after consent ⇒ transient,
the retry converges) from add-on roles (permanent ⇒ MissingRoles names them). consent-status and
access-check carry appHomingPending + appHomingMissingRoles; the manual flip's probe payload
carries missingRoles. The access-check invalidates the Graph caches BEFORE the homing probe so
"Detect existing access" judges a fresh primary token after the admin's grant.
Web: TenantConfigContext learns the blocking roles from either response and the post-consent
poll keeps going while the switch is still converging (previously it stopped on the first
legacy-side reconcile and left the tenant un-switched). Settings → Autopilot Validation shows a
"One more step" banner with the roles, the grant command pre-filled with the NEW app's client id
and -Permissions, and the re-check button; the admin flip error names the roles too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The agent's 48h emergency break is wall-clock, so devices powered on after a shelf
period fire it days after the sweep already terminalized the session (2026-09-03
audit: 224 of 269 breaks in three days were such late cleanups from one kiosk tenant,
all Warning, burying the real ones).
- ReportAgentErrorFunction.ClassifyEmergencyBreak: session already terminal
(Succeeded/Failed/Incomplete) => Info + lateCleanup=true; open, unknown or
unreadable => Warning (historical semantics). Row read is best-effort.
- OpsEventService.RecordAgentEmergencyBreakAsync takes the severity and stamps
sessionStatusAtBreak/lateCleanup into message and details.
- Timeline event agent_emergency_break stays Warning (classifier rule 3 keys on type).
- SessionOwnerBindingObserver: document that the ops-event throttle is per Function
instance (IMemoryCache) and deliberately so.
- Tests for both branches, the fail-safe path and the pure rule; docs submodule bumped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Performance Map gets a "Color by" selector (enrollment duration, success rate,
API latency, DO peer efficiency, App-Load-Score) and a legend under the map. One
bucket definition per mode (mapColorModes.ts) feeds both the Leaflet markers and
the Location Performance table badges, so map, legend and table share thresholds.
The map's duration scale moves to the table's 0.8/1.0/1.2/1.5 ladder.
Also: page.tsx uses the generated wire types instead of drifted local interfaces,
formatThroughput comes from lib/formatting, marker tooltips show latency and DO share.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four README screenshots were still from the private preview (old blue
logo, preview banner). Retaken in the light theme with the current logo,
tenant view only, no banner. Fleet Health uses the 90-day window; the geo
map has too few locations in the own tenant to read as a world map.
Also stores the GitHub social preview as source (HTML composition plus the
anonymized dashboard capture it embeds) and the rendered 1280x640 PNG.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Root LICENSE is now the verbatim AGPL-3.0 text so GitHub detects the license
instead of showing "Other"; the split model (MIT for Agent + Shared) is
explained in the README and each component keeps its own license file.
- README: MCP server in the component list, an "AI Integration (MCP)" section
with example prompts and the server URL, and a neutral Feedback section.
- SECURITY.md routes reports to private GitHub Security Advisories.
- CONTRIBUTING.md is a feedback routing page (issues, discussions, security,
Report Session button), CODE_OF_CONDUCT.md is short and neutral.
- Issue forms for bug reports and feature requests; blank issues disabled.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- /about: "Common Questions" section (15 user problems → feature answers) from one source array, mirrored as FAQPage JSON-LD
- root layout: SoftwareApplication JSON-LD becomes an @graph with Person (author/publisher), Organization glueckkanja AG (provider), WebSite, featureList, softwareHelp
- public/llms.txt with portal, docs, and source links
- robots: explicit allow block for GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Google-Extended
- drop the unverified "policy conflicts" claim from the analyze-rules feature card
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- TenantConfiguration.McpUsagePlanOverride: a SectionUsagePlans plan name pinned to a whole
tenant (members' default user plan + organization windows); precedence per-user override >
tenant override > edition default; never changes the edition
- PATCH config/{tenantId}/plan accepts mcpUsagePlan (validated against the definitions,
400 on unknown names; null/blank clears), audited, echoed as mcpUsagePlan/mcpUsagePlanOverride
- Tenant Management: "MCP usage plan (override)" select fed by global/config/plan-tiers
- Quota 429: every Community block now says Community is sized for occasional use and Pro
lifts the window; managed-tenant block names whose plan governs it
- Field in BaseDeniedFields/RevertProtectedFields/projection, Store/Map, manifests regenerated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Phase A — the budget follows the data
- A delegated (MSP) read is charged to the MANAGED tenant's organization MCP budget and checked against
its plan; the caller's own budget still follows the home tenant. 429 names the managed tenant (domain)
and the Pro upgrade path; new McpQuotaExceededResponse.targetTenantId.
- Bounded fleet aggregate (global/sessions, global/stats/sessions): exhausted managed tenants are excluded
softly — RequestContext.AllowedTenantIds is narrowed (never widened, never null; pinned) and echoed as
quotaExcludedTenants. Global Admins stay exempt and are tracked on their home tenant only.
- Tenant Group flag ChargeHomeTenantQuota (PATCH global/tenant-groups/{id}, audited per tenant): reads
through the group are charged to the assignee's home tenant (operator-managed service groups).
- McpTenantUsage rows carry UserPrincipalName / HomeTenantId / LastRequestAt; new
GET metrics/mcp-usage/organization (budget by account incl. delegated readers) rendered on MCP Usage.
- MCP: get_fleet_overview (delegated + platform) over the two subset-tier routes; error handler names
the managed tenant. Other delegated tools stay tenant-mandatory.
Phase B — delegated tenant slots
- EditionEntitlements.MaxDelegatedTenants (Community 0 / Pro 2), TenantConfiguration.MaxDelegatedTenantsOverride
(GA-only via PATCH config/{tenantId}/plan { maxDelegatedTenants }), DelegatedSlotService counting distinct
managed tenants per home tenant through identity bindings (+ pending invitations + 24 h holds), enforced
fresh on grant / group assign / add-tenant with 409 DelegatedSlotLimitReached; GA UI offers
"raise to N & retry"; GET global/delegated-slots/{tenantId}; feature-flags entitlements.maxDelegatedTenants.
Phase C — self-service delegation
- One implicit Tenant Group msp-{tenantId} per managing tenant (OwnerTenantId on the meta row);
DelegationInvitations table (invitations + slot release holds); DelegationInviteTicket (HMAC, 7 days,
locates the row only — the accepting tenant is always the JWT tenant; one-shot via ETag).
- DelegationSelfService: ordered accept chain (distinct codes), remove/revoke with 24-hour slot hold, dual
audit and live-stream disconnect, assignees bound to tenant membership, "who manages my tenant" view;
GA release-hold escape hatch. No delegations/* route carries {tenantId} (pinned).
- Web: Settings → Tenant → Delegated Access, /delegations/accept?token=, AuthGate keeps the deep link
across the activation detour, Tenant Management holds + release, owned groups read-only in GA UI.
- Offboarding wipes both sides (documented two-bucket membership); 90-day sweep of terminal rows.
- Trust pages (terms, privacy) and generated wire types updated; docs submodule bumped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL flagged the re-save in AuthGate (js/clear-text-storage-of-sensitive-data,
alert #82): the value flowed back into sessionStorage from the consumed
return URL. The link only has to survive one more sign-in round trip, so the
re-home path now reads it without clearing and never writes it again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>