Route payloads (__next.*.txt, index.txt), static JSON and the router's HEAD
self-requests were 20k of 62k dependency rows per week and nobody reads them.
lib/webTelemetryExclusions.ts builds the patterns (own origins minus /api/,
plus relative non-API URLs) for two layers: excludeRequestFromAutoTracking-
Patterns for string fetches, and a dependency initializer that drops the
finished item by its absolute target - the SDK's own check sees an empty URL
for the URL objects Next's router passes to fetch (verified against the ingest
batch: 11 such fetches, 0 shipped; API, SignalR and token rows still shipped).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
P1 - full loads
- staticwebapp.config.json: /_next/static/* immutable, /landing/* max-age=86400
(SWA default was max-age=30 for hashed assets); guard test pins it
- components/NavLink.tsx: prefetch on hover/focus/touch for the portal chrome
(prefetch={false} also disabled the hover prefetch); landing links stay off
- app/layout.tsx: preconnect to the API origin
- lib/dashboardSeed.ts + lib/prefetchSeeds.ts: the dashboard's first list and
stats requests start with auth/me and are handed to the hooks as one-shot
seeds (the own-tenant list URL carries no tenantId; the backend reads the JWT)
- lib/msalConfig.ts: the login request carries the API scope, so one token
redemption serves the first API call; Graph consent via extraScopesToConsent
P2 - chatter per navigation
- lib/signalrGroupRegistry.ts + contexts/SignalRContext.tsx: group membership
is reference-counted with a 2 s grace leave; reconnect rejoins run in
parallel; on/off/invoke and the context value are memoised; the dashboard's
unconditional leave no longer drops the notification bell's global-admins
membership; consumers pair every join with a cleanup leave
- app/sessions/hooks: both group joins in parallel, the join catch-up is
dropped while the initial walk is in flight, identical session deltas skip
the state update
- lib/cachedAuthFetch.ts: TTL cache for feature flags, tenant list and latest
versions with invalidation on every write path; FeedbackBubble defers its
status call
P3 - pure derivations only (D-274): memoised filter arrays, one-pass phase
summary, status counts, useDeferredValue on both search inputs
P4 - landing
- lib/authHint.ts + AuthGate + globals.css: the loading overlay is applied by
an inline head script only when the site root is opened by a browser with an
auth hint; the prerendered HTML carries no overlay; CSS lifts it after 8 s
- hero as WebP srcset (81 KB instead of a 180 KB PNG), dead landing-v1 CSS
removed, terminal demo pauses off-screen, duplicate twitter image dropped
Telemetry (D-276)
- lib/appInsightsRuntime.ts: the SDK is its own chunk, deferred until idle on
the public host, remote config fetch off (blkCdnCfg), initial trackPageView
on both hosts, Web Vitals as web_vital_* custom metrics
Verified: tsc, eslint --max-warnings 0, vitest 127 files / 1442 tests, next
build, Playwright click-through of every portal route, settings section,
session views and public pages on the production bundle against the live API.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A platform script that ran 10 s was shown as "1h 00m" (session 4377911b): its
start line was self-anchored (+120 min) while its result line, re-read after an
overwrite rewind and therefore never fresh, fell back to the agent's cached zone
(+60 min), was rejected as ahead of the clock, and the duration was taken from
the raw pair. The same shape fired script_timeout_suspected daily in one tenant
on 2-9 s scripts read as 17 h. Fleet 2026-09-20..23: about 1 % of script
durations, 28 of 28 inflated ones carrying derivedTimestamp.
Agent: ScriptExecutionState binds the provenance of its start, result and exit
lines (CmTraceLineProvenance, persisted); EmitScriptCompleted judges the pair -
verified (both measured, or both assumed with the same offset from the same
file), unverified, or suppressed with durationSuppressedReason
(rejected-endpoint | offset-grid, CmTraceOffsetCalibrator.IsOnOffsetGrid) - and
always emits durationProvenance plus startSourceOffset*. A held completion's
sourceOffset* tags now describe its own result line; script_timeout_suspected
only judges a measurable duration; the emitted-run marker is capped at the
clock so a future-shifted result no longer swallows the next run's start.
Web: the reducer applies the same rule to stored events from older agents,
pairing a final with the latest script_started of its policy, and the panel
shows "n/a" with the explanation where the run time would be.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ImeRegistryAppStateObserver reads a fourth IME registry surface,
Win32Apps\OperationalState\<userGuid>\<appGuid>: the install deadline a
downloaded app waits for (ExecutionDeadlineTime, IME 1.106) and the
app-in-use deferral counters (DeferralsUsed, DeferUntilTime,
DeferralMaxDeferrals, DeferralAutoDeferred, IME 1.105). Both travel on the
existing registry_app_state event as data (installDeadlineUtc, deferralsUsed,
deferralMaxDeferrals, deferUntilUtc, deferralAutoDeferred) with the change
triggers installDeadline / deferralsUsed / deferUntil, and the message says
the state in words ("downloaded, install deadline ... UTC", "deferred by user
1/3 until ... UTC", "auto-deferred ..."). No new event type.
ClassifyStatusServiceStatus maps IME's legacy AppInstallStatus explicitly:
1000/1002/1004 installed, 1001/1003 installing, 2000 band notApplicable,
3000 failed, anything else unjudged. The 1000 band was treated as installed,
but IME maps every InProgress enforcement state to 1001, so a running app
counted as terminal success on the registry rail (observability only).
Tests: explicit status mapping, both registry timestamp shapes, the new
change triggers, the wait-state text, and the tick flow for a deadline and a
deferral event. Mutation proof: classifier and trigger broken on purpose
turn exactly the five expected tests red.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ajv-cli 5.0.0 is the latest release and still declares fast-json-patch ^2.0.0,
so neither Dependabot nor npm audit fix can move past CVE-2021-4279. An npm
override forces 3.1.1 into the lockfile; the 3.x API is backward compatible and
the validate command used in combine-rules.yml never touches the package.
Verified: npm ci from the lockfile plus all four ajv validate steps pass locally.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The offboard endpoint tombstones the tenant (Disabled=true, "Offboarding in
progress") before it answers 202, so the departing admin submits the farewell
feedback from an already-suspended tenant. Since the suspension gate moved onto
the shared authorization path (43ddc601, 2026-08-29) every feedback POST was
403 TenantSuspended — invisible, because nobody clicked and the two stored
feedbacks predate the gate (D-270).
- Endpoint catalog: `AllowedDuringOffboarding` on exactly
POST tenants/{tenantId}/offboard/feedback; the middleware ignores the
offboarding tombstone for that route only. An operator suspension still
denies; policy tier, cross-tenant check and the function's active-marker
check are untouched. Middleware tests for all four cases plus a catalog
ratchet pinning the flag to the one route.
- Farewell mail: IOffboardFarewellEmailSender.SendAsync returns whether the
provider accepted; the offboarding worker records FarewellEmailSent /
FarewellEmailSkipped / FarewellEmailFailed like the welcome mail — the
send's own success log is Information and never reaches Application
Insights. Missing Email:ApiKey logs Warning. Handler tests per outcome;
OpsAlertRules catalog and the generated manifests (Web + MCP vocabulary)
carry the three types.
- Web: auth/me 403 TenantSuspended no longer shows alert() and forces a
logout. AuthContext keeps the user signed in with claims-only info and
ProtectedRoute renders a suspended page: for the offboarding tombstone
"Your organization is being offboarded" with the farewell feedback form
(a reload during the 6-minute preparation window keeps the form), for any
other suspension the reason text plus contact links; both with sign-out.
The feedback widget moved from OffboardingSection into
components/OffboardingFeedbackForm.tsx.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every tool description and both role instructions now fit the 2048-char host cap
(Claude Code truncated the Global Admin instructions and two descriptions silently).
Pagination, projection and scope mechanics are said once in the instructions and
the shared pageSize/continuation/tenantId/days texts; role markers left tools only
that role can see; get_resource lists its sections on the argument. The SDK $schema
URL is stripped from tools/list.
Measured (compact tools/list): Global Admin 111.4 KB -> 89.1 KB, Reader 94.7 -> 74.3,
tenant user 64.5 -> 51.3, delegated 75.1 -> 55.6 KB. catalog-budget.test.ts pins the
new ceilings, the variant counts and the empty over-cap list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
list_tenants named a real customer domain as its example; the filterTenantList test
carried three real tenants. Both now use synthetic names and GUIDs.
catalog-budget.test.ts pins the per-role wire catalog as a ratchet (size, repeated
argument texts, no foreign domains or GUIDs, the 2048-char host truncation cap with
the four known over-cap texts listed). scripts/audit-catalog.ts dumps and sizes the
catalog per role for before/after measurements.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
zizmor (static analysis for GitHub Actions) now runs twice: locally before a
workflow commit (hard rule in .claude/CLAUDE.md) and as the `zizmor` job next to
the pin gate in action-pins.yml (zizmorcore/zizmor-action pinned by SHA, zizmor
1.30.1 pinned, SARIF into Code scanning, severity low and above). Like the pin
gate it reports and does not block. zizmor.yml holds the single reasoned ignore.
Baseline fixes so the gate starts green:
- persist-credentials: false on every checkout except combine-rules.yml, the one
job that pushes with the checkout credentials (ignored in zizmor.yml, with reason)
- publish-scripts.yml: id-token: write moves from the workflow to the publish job;
indexnow-notify.yml: permissions: {}
- deploy-mcp.yml: env values referenced as shell variables inside bash steps
instead of template expansion
- pester.yml / publish-scripts.yml: reusable workflow via the self-repository form
$/ (GitHub changelog 2026-07-30); the pin gate treats $/ like ./
- combine-rules.yml: ajv-cli installed with npm ci from rules/scripts/package-lock.json
(no ts-node peer, no lifecycle scripts) instead of an ad-hoc global install;
Dependabot watches /rules/scripts
- azure/login pin comment corrected to the tag the SHA actually is (v3.0.2; v3
moved on to v3.1.0), no runtime change
- Dependabot: 7-day cooldown on every ecosystem (security updates are exempt)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The XML summary added on TenantManagerItem.Name in ef280b7b rides into
shared-manifests.json as the field's doc; CI run 35701895623 caught the drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET delegations/managers no longer exposes the name of an operator-created
Tenant Group to the customer; both operator shapes (groups and direct grants)
carry the neutral label. Assignees, reader count and source stay visible.
Web badge reads "Provisioned by operators". D-267.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
network_state_change and network_connectivity_check join the non-activity
list next to power_state_change. A flapping dock or access point kept the
idle-stopped periodic collectors running for the whole episode and reset
the stall clock of the enrollment it was stalling. The events are emitted
unchanged; the device-info network snapshots stay activity.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IME re-runs remediation policies on its own schedule for as long as the
device is up, so an enrollment that waits for its user reported every run
of every policy. The tracker now emits a policy's first run and every run
whose result differs from the last reported one (compliance, exit code,
stderr, remediation status, error code); repeats are counted and surface
as one script_recurrence_summary per policy when the tracker stops.
The start signal of a known policy is held until its result is known,
because the running indicators pair a start with a later result; a start
without any result is still emitted after a grace. The counters live in
the tracker state file next to the file positions, so a hard kill rolls
both back together. Platform scripts are untouched. The summary does not
count as enrollment activity.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The stored EventCount doubles on replayed upload batches and was only
corrected at terminal transitions, so the sweep judged running sessions on
an inflated number. It now recounts a candidate before it records the ops
event or blocks the device, skips candidates it cannot verify, and limits
the recount to candidates with a pending action. The warn text no longer
claims an agent loop bug.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
73 sites in 68 files built the query collection with
HttpUtility.ParseQueryString(req.Url.Query), 15 files used req.Query and five mixed
both. HttpRequestData.Query is that same call, cached per request, and the ASP.NET
Core request type does not override it, so the change is behaviour-neutral.
- QueryParamGuardTests: ParseQueryString in the Functions project is limited to a
named baseline of three sites that parse something other than the request query.
- A fact pins req.Query against HttpUtility.ParseQueryString (repeated keys, encoded
values, caching) so a worker update that changes the implementation fails a test.
- Test mocks that set Url answer Query through CallBase; a loose mock returns null.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six functions wrote their responses through a private wrapper taking the body as
object, which no typed-response guard could see. Their error bodies carried the code
in the error slot, no correlation id, and RestoreRow returned the raw exception text
on a 500.
- Backup job status, row restore and backup trigger answer through ResponseHelper:
errors are the envelope, the 500 is sanitized, the 202 body is BackupTriggerResponse.
- Same for the session-deletion maintenance trigger, RestoreSession's three error
sites and the delegation acknowledgements (MessageResponse).
- Backup codes are constants (Constants.BackupErrorCodes) and part of apiErrorCodes;
RunAlreadyActive and EnqueueFailed join ApiErrorCodes.
- TypedResponseGuardTests gains guard F: no HttpResponseData-returning method with an
object parameter. BackupWireParityTests pins that the move from the backup
serializer options to the worker pipeline leaves the wire unchanged.
- Web reads BackupTriggerResponse from the generated wire types.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A version whose installer was already in the archive could only turn
Archived through the 409 on the write-once upload, which needs a
distribution host that still serves the build. For a version no host
serves any more the row stayed Failed:VersionMismatch next to its own
blob and re-walked every host once a day.
The archiver now checks the archive first: an entry whose ProductVersion
is the observed version ends the job as Archived with the hash and size
of the archived bytes and no download; a missing provenance sidecar is
written without a source URL. An entry holding another build, or an
unreadable one, is not accepted and the walk runs as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser loads the photo straight from Microsoft Graph with the User.Read
scope already consented at sign-in; it never reaches the backend. Any failure
(no photo, token, network) falls back to the initials. One request per account
and tab, cached in sessionStorage including the no-photo answer.
The four copied avatar blocks in the navbar become one UserAvatar component.
Graph host added to the URL registry and to connect-src in both CSPs; the
privacy page states where the photo comes from and that it is not stored.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A process name is not a protected namespace: any user session can keep a
binary named like the agent resident, which made every agent start during
that session exit 1 before registration and let install mode report a
runtime launch that never happened.
The guard and install mode's launch verification now share one rule
(FindSiblingAgentPid): a name match counts only in session 0 and never for
the caller itself. Every runtime start lands there (Scheduled Task, WMI
launch from the SYSTEM install chain, self-update restart), so behaviour
for real agents is unchanged. The session id comes from the process
snapshot, so no handle is opened; a process that cannot be read does not
count, which fails open into two agents rather than none.
Two interactive --console runs in the same user session no longer block
each other; an interactive run next to the session-0 runtime still does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Banner and the success-rate/duration gauges read the evaluation period
(last 30 days) instead of the current ISO week and name the window in their
labels; the app install gauge says "this week". A target whose period holds
no finished enrollment renders a neutral "No data" card and is left out of
the banner count; with nothing to judge the banner is neutral.
Banner logic moved to lib/slaStatus.ts (pure, tested).
Needs the backend of the previous commit: the page reads
evaluationPeriod / evaluationWindowDays.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SLA breach alert judged the calendar month while the dashboard headline
showed the current ISO week, so an alert could report 78 % while the page it
links to showed 0 everywhere - and an empty week rendered as 0.0 % "Breached".
- SlaEvaluationWindow is the single definition of the SLA populations:
success rate and P95 duration on terminal sessions of the last 30 days
(rolling), app installs on the install attempts of the current ISO week.
The breach evaluator and SlaMetricsService both read it, including the
one-decimal rounding, so the value shown is the value judged.
- SlaMetricsResponse carries EvaluationPeriod and EvaluationWindowDays;
SlaSnapshot carries Period and HasData. A period without a finished
enrollment meets every target flag and is marked HasData = false.
- The dashboard reads from whichever starts earlier, the selected months or
the evaluation window; trend and violators stay on the selected months.
- A lasting breach is notified again only after the cooldown AND once
something finished since the last notification. A new breach episode after
a resolve still notifies. Resolve notifications are unchanged.
- Alert period fact and telemetry tag follow the window constant.
- SlaMetricsService takes a nowProvider; the week-boundary workaround in its
tests is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A configured diagnostics entry is resolved once to folder, pattern and recursion, and
that target is what gets validated and enumerated; recursion asks the guard before
entering a subfolder, and the user-profile token is expanded before validation.
Security, PowerShell and Sysmon event logs are refused per file, by name and by
resolved channel, in every mode; the portal flags such a path before it is saved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Directly under Dashboard the Progress Portal collected misclicks: 418 of 1,050
admin page views in 90 days went on to the dashboard within 5 seconds, from
every origin page. It is the end-user page; regular users keep it as their only
entry via REGULAR_USER_ITEMS. Annotations showed the same walk-through pattern
and moves down with it. Fleet Health, Geographic Perf. and Software lead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All five cards showed the same bar-chart glyph on a differently coloured
block, so the colour carried no meaning. Each card now has a small grey
outline icon for its metric (pulse, check, stopwatch, calendar, x) in the
top-right corner, and the label gets the freed width. Failed Today turns
its value and icon red only when the count is above zero and reads
"No failures" at zero.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The analyze and gather rule pages loaded the community submissions list for
every role. rules/submissions is TenantAdminOrGA, so an Operator or Viewer got
a 403 and a red "Failed to load your community submissions" banner on every
page visit (16 denials in 24 h from two operators of one tenant).
The pages now skip the list for callers who cannot read it and the
contribution box says that only tenant admins can contribute and follow the
tenant's submissions. Tenant admins, Global Admins and a global-scope override
are unchanged; the backend policy stays as it is.
Bumps the internal and .claude/commands gitlinks (platform-health marker
catalog, backlog).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>