The MCP server could answer telemetry questions ("why did this session fail")
but not product ones ("how do I deploy the agent", "where is my data stored").
Those answers already exist in the autopilotmonitor-docs bundle; search_docs
makes them semantically searchable.
Deliberately a separate index from search_knowledge: rules and prose answer
different questions, and sharing one topK would dilute both. Registered for
every role — published documentation carries no tenant data — and skipped
entirely when no corpus was baked in, so a doc-less build advertises no
broken tool.
Corpus handling:
- Chunked at h2/h3 and <details>/<summary>, each chunk prefixed with a
"Page > Section > Subsection" breadcrumb so a mid-page hit is self-describing.
- Newlines are normalized before any parsing. This is a correctness
precondition, not cosmetics: the bundle ships CRLF and JS `.` does not match
\r, so /^(#{2,3})\s+(.*)$/ matched zero headings — silently, degrading every
page to unlabelled paragraph splits.
- Two size exceptions: tables are never split (a split table leaves `| --- |`
rows with no header), and unbroken changelog lists split at line level (left
whole, their tails fall outside the model window and become unsearchable).
- Short sections are kept — a one-line answer is often the best hit, and the
breadcrumb supplies the context a short body lacks.
Build and delivery:
- Docs are embedded at image build time into an optional third
PrecomputedSection. Optional so a checkout without the docs repo does not
invalidate the rules and event-type vectors; the reverse (server has docs,
file does not) is staleness and is rejected.
- The deploy workflow checks the public docs repo into the build context and
passes its SHA as DOCS_COMMIT, which /health reports alongside the chunk
count — editing the docs does not update a running server, so staleness is
made checkable rather than memorable.
- Embeddings are rounded to 6 decimals on serialize: 55% smaller index (2.63
vs 5.88 MB) for a ~1e-5 norm error against a 1e-3 tolerance. indexPrecomputed
now asserts unit norm, since the hydrate path never checked it.
Infra: the Bicep template was two changes behind the live deployment (EU
cutover and ACR hardening, both applied directly in Azure). Reconciled against
the running resources — managed-identity image pull replaces the admin
credentials and the acr-password secret — and infra/ is no longer gitignored so
it stops drifting. Container raised to 0.5 vCPU / 1.0 GiB for the cold-start
query-embedder load. The template does not model the mcp.autopilotmonitor.com
custom domain, so a full deployment would remove it; the header warns about it.
Verified: 428 tests pass; 45 pages produce 324 chunks; sample queries hit the
right page top-1/2; all 8 derived URLs return 200 live; the precompute output
is accepted by the boot validator and a mutated corpus is rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The section was registered for routing but not in the sidebar, so it was
reachable only by typing the URL. Sidebar entries come from
globalNavConfig.tsx, which is not derived from tenantNavSections.ts -
both have to be touched for a section to exist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforcement and service notices need somewhere to go. Until now the only
address on file was the Private Preview notification email, which is
onboarding data in the PreviewWhitelist table and not something a tenant
can maintain.
Adds TenantConfiguration.ContactEmail, owned and edited by the tenant's
own admins under Settings -> Tenant -> Contact:
- Seeded once when a preview notification address is saved, and only
while ContactEmail is still empty. It is never re-synced, so an edit in
the portal cannot be overwritten later by the onboarding source.
- Backfilled for tenants onboarded before the field existed, as a
manual-trigger maintenance task alongside the other backfills. It skips
any tenant that already has an address, so it converges to a no-op and
can be dropped once everyone is covered.
- Surfaced read-only in Admin -> Tenants next to the preview address, so
an operator who needs to warn a tenant does not have to read the table.
Purpose is stated where it is entered and in the Privacy Policy: service
matters only - a technical problem, a security matter, or a change
needing an administrator's attention. Never marketing, never shared.
The Terms already say a warning is sent "where a tenant contact address
is on file"; this is the field that makes that promise meaningful.
Wired through the full tenant-config round-trip (types, context state,
hydrate, save payload, reset, provider) so a save from any other section
cannot clobber it.
Verified: solution builds clean, 3101 backend tests and 557 web tests
pass, tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Intellectual Property & Licensing section: the repository licence
governs the code, not the hosted service, its infrastructure, its data,
or the name and branding. Customer data stays with the customer.
- Acceptable Use now states the enforcement ladder (warning, reduced rate
limits, suspension, termination) instead of an unqualified right to
suspend, and routes security research to GitHub Security Advisories
with explicit testing boundaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Privacy: "Sub-processors and Outbound Data Flows" becomes "External
Services". It still names who receives data, which is a controller's own
duty, but drops the Art. 28 vocabulary and the promise to announce new
sub-processors - that commitment lives in the data processing agreement.
Points at the renamed docs page and notes the DPA is available on
request.
Terms: the MSP clause now says a managing organization is responsible for
having the necessary data protection agreements with its customers,
instead of classifying it as a processor or sub-processor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependabot alerts #63/#64/#65 (all high): brace-expansion exponential-time
expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp,
both the 1.x and 2.x trees) and js-yaml quadratic CPU consumption via
YAML merge-key chains (GHSA-52cp-r559-cp3m).
All three are transitive dev-only dependencies of the eslint toolchain
(eslint -> @eslint/eslintrc -> js-yaml; eslint/typescript-estree ->
minimatch -> brace-expansion) and resolve within their existing semver
ranges, so only the lockfile changes.
npx tsc --noEmit clean, vitest 557 passed, npm audit reports 0
vulnerabilities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependabot alert #62 (protobufjs 7.6.4, DoS via infinite loop in .proto
option parsing, GHSA fixed in 7.6.5) plus the tar (critical) and
body-parser (low) advisories npm audit surfaced alongside it. All three
are transitive under @huggingface/transformers and resolve within their
existing semver ranges, so only the lockfile changes.
Lockfile also picks up the pending 1.5.0 -> 1.5.1 package version sync.
npx tsc --noEmit clean, vitest 400 passed / 41 skipped, npm audit
reports 0 vulnerabilities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to gather-rule phase scoping. One-shot collection at a phase START
already existed (phase_change dedups per (rule, phase)) and at enrollment END
(on_event + enrollment_complete); the END of a specific phase had no trigger.
New trigger value "phase_exit": fires once when TriggerPhase is LEFT (empty =
every phase transition). Together with phase_change it forms the two one-shot
bookends of a phase.
Agent (GatherRuleExecutor.OnPhaseChanged):
- Exit rules are evaluated BEFORE _currentPhase advances, so both the
TriggerPhase match and the phase-scope gate see the phase being LEFT. A rule
scoped to DeviceSetup therefore fires on its own exit even though the phase
being entered is out of scope.
- A transition into Failed fires the exit rules of the failing phase — the
snapshot at a failure boundary is the point of the trigger.
- Exit of Unknown never fires; a repeated identical phase signal is not a
transition.
- Dedup key "exit:{ruleId}|{phase}" shares _phaseRulesExecuted with the
phase_change keys but stays in a disjoint key space, so an enter-rule and an
exit-rule on the same phase cannot consume each other's slot.
Portal: trigger labels are now "Phase Start" / "Phase End"; the canonical phase
dropdown and the create/save payloads cover both via PHASE_TRIGGERS; the card
renders "entering X" / "leaving X".
No ConfigVersion bump: no new field is transported. Old agents do not know the
value and simply never fire the rule (silent no-op).
Tests: GatherRuleExecutorPhaseExitTests (11) — enter-vs-exit, no refire on
re-entry, empty-triggerPhase, exit-of-Unknown, same-phase-signal, into-Failed,
Complete, scope-against-left-phase, out-of-scope slot preservation, key-space
disjointness, on_change interplay. Build 0 Errors; V2.Core.Tests 1884/1884;
Functions.Tests 3099/3099; vitest 557/557; tsc silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md ships with every request. Removed the example from the
operator-only-infrastructure rule - rules state the rule, not an
instance - and applied the same yardstick to the rest of the file.
Merged what said the same thing twice: "would a staff engineer approve"
and "what would a perfectionist reviewer reject"; Demand Elegance and
Code Quality and Core Principles; Task Management's lessons item and the
Self-Improvement Loop; Task Management's plan steps and Plan Mode
Default. Dropped motivational framing that steers no decision, and a
stray heading level.
Every actual rule survives: the 3+ step plan threshold, stop-and-re-plan,
subagent usage, verify-before-done with a diff against main, autonomous
bug fixing, lessons capture, 500 LOC chunked reads, no absence claims
from one grep, the structural-fix mandate, and the Codex review note.
425 words, down from 614 at the start of the session - and that now
includes the customer-facing claims section, which did not exist then.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLAUDE.md ships with every request, so length is a running cost. Cut the
new claims section from 12 lines to 4 and tightened the OKF section,
dropping the spec URL and restating the rules imperatively. Net effect:
the file is now smaller than before the claims section was added
(654 words vs 856).
No rules were removed - verify-before-writing, the change triggers, the
durable-phrasing rule, and the operator-only-infra caveat all remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terms, Privacy, the About page and the trust docs make statements that
customers and security reviewers rely on, but nothing opened them during
a refactor, so they drifted. The July 2026 sweep found a ".NET 8" backend
that was .NET 10, a "Next.js 18" that does not exist, ETL log collection
that never happened, and "each tenant runs its own isolated instance" for
a shared multi-tenant service.
Records which surfaces exist, which kinds of change must update them in
the same commit, and that claims are verified against the code rather
than carried forward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terms: replace the blanket business-use-only clause. It excluded exactly
the wrong group - a registered association is not a consumer under German
law anyway (only natural persons can be), while the clause did shut out
private individuals running their own lab or personal tenant, who are a
real and intended audience for a free community edition. Community is now
stated as open to anyone with an eligible Entra ID/Intune tenant;
business-only stays where it means something, on the Enterprise contract.
About page - claims verified against the code and corrected:
- "each customer tenant runs its own isolated instance" was wrong and
contradicted the same page two sections earlier. It is one shared
multi-tenant service with partition-key isolation and delegated MSP
access.
- Diagnostics: no ETL tracing and no Windows event log export happens;
the bundle is agent logs, IME logs, agent state and device info. Upload
is tenant-configured (off/always/on failure) at the end of enrollment,
not triggered on demand from the portal - the portal downloads it.
- .NET 8 Isolated -> .NET 10 Isolated on Flex Consumption.
- "Next.js 18" -> Next.js 15 (React 18); 18 was React's version.
- Roles: Admin/Operator -> Admin/Operator/Viewer plus delegated MSP.
- Notifications: Teams only -> Teams, Slack, generic JSON webhooks, with
start/success/failure and SLA triggers. Telegram stays unlisted; it is
operator-only alerting, not a tenant feature.
- Agent deployment: Win32 app -> Intune platform script; self-destruct is
on by default, not optional-sounding.
- Dropped the "tamper-evident" audit claim - records are ordinary table
entities with no hash chain, signing, or immutability policy.
Confirmed while verifying: no tenant-configurable email channel exists
anywhere in the codebase, so the published privacy statement that Resend
serves only the onboarding mail is accurate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the public Terms of Use and Privacy Policy from their early-days
wording to match how the service actually runs today, and fix a setting
that looked like a privacy control but gated nothing.
Legal positioning:
- glueckkanja AG operates the service and is the counterparty on BOTH
plans (Community/Enterprise is a flag in one system, one subscription).
Oliver Kieselbach is named as creator and maintainer of the open-source
project and the Community edition, acting on behalf of glueckkanja AG
and explicitly not a separate contracting party.
- Community: free, "AS-IS", no availability or support commitment.
Enterprise: written agreement incl. DPA, higher limits, MSP delegation.
- Added business-use-only clause, simplified governing law (dropped the
CISG exclusion - it governs sale of goods, not SaaS), split liability
per plan, added an MSP delegation section and a rules-contribution
clause covering the community rule pool.
- Imprint links to the glueckkanja AG imprint, whose scope clause covers
every service linking to it - no duplicated company data to go stale.
Reachable from the landing footer and both help menus.
Privacy Policy corrections (previous claims were inaccurate):
- "No personal data is transmitted" - backend telemetry carries the UPN;
only the browser SDK is cookie- and UPN-free.
- "Not shared with third parties" - qualified for MSP delegation,
customer-configured notification channels, the customer's own LLM via
MCP, and agent-side geolocation.
- Removed "data subject rights are not guaranteed"; GDPR rights are not
optional. Added region, sub-processors, retention and IP handling.
Agent - SendTraceEvents gate:
The setting was plumbed from TenantConfiguration through to
AgentConfiguration but never consumed; Trace severity only hides an event
from the timeline view. The outbound IP was therefore always collected
when geolocation is on (which is the default). Gate is now enforced in
TelemetryEventEmitter.Emit, the single choke point before the transport
enqueue. Dropped before the sequence counter advances so suppression
leaves no gaps. Evaluated per event via Func<bool> so a mid-session
remote-config merge takes effect. Fails open: an unwired or throwing
accessor lets trace events flow, because the gate suppresses diagnostics
and must not silently blind the timeline.
Default stays on and the switch remains operator-only via the config
table - deliberately not surfaced in the UI or documentation.
Verified: solution builds clean, 1873 agent core tests pass,
tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework after the first real enrollment (session b2e890c1) exposed three
defects in a654c730:
- Shell-Core "RebootCoalescing" match was a false positive by construction:
the token appears in the ROUTINE SubcategoryProcessing_Started bootstrap
marker ("Starting subcategory DeviceSetup.RebootCoalescing...") on EVERY
enrollment. esp_reboot_coalescing removed entirely (branch, constant,
catalog, tests).
- 2800 reboot-required flags fire at every policy sync; only DeviceSetup-time
flags cause the coalesced reboot. The session's flags fired during
AccountSetup - no reboot - yet our Warning events claimed "device will
restart". Raw event is now a neutral Info observation; the reboot CLAIM
moved to ANALYZE-ESP-005 with a REQUIRED system_reboot_detected >= 1
condition (no observed reboot -> advisory silent).
- One event per 2800 record was spam: records are now buffered and debounced
(10 s) into ONE aggregated event per burst (rebootUris[]/uriCount/
firstRebootUri/recordCount/unparsedCount). Watermark is persisted at FLUSH,
not claim - a pre-emit reboot kill loses nothing, the post-restart backfill
re-emits the burst at its historical time.
Verified 2800 description shape recorded in code, tests, and concept doc:
"The following URI has triggered a reboot: (./Device/...)."; extraction
covered by a real-text regression test. Lesson added to tasks/lessons.md
(observation-level events vs outcome-gated claims; substring tokens appear
in framework bookkeeping records).
Tests: Agent.V2.Core 1867/1867, Functions 3099/3099, MCP 400 passed,
solution build 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Device-assigned MDM policies whose CSP URIs match the OS reboot-required
catalog force a coalesced reboot at the end of ESP DeviceSetup - the user
sees an unexpected restart and a second sign-in before AccountSetup.
Invisible in the Intune console; admins simply lived with it. Now attributed
and actionable (PMPC research).
- MdmRebootPolicyTracker (+host): watches DeviceManagement-Enterprise-
Diagnostics-Provider/Admin EventID 2800, emits one mdm_policy_reboot_required
per reboot-forcing URI (Warning, ImmediateUpload; tolerant EventData-first
URI extraction with regex fallback; fail-soft OMADM RebootRequired probe).
Cross-restart RecordId watermark (mdm-reboot-watermark.json) - mandatory,
the coalesced reboot restarts the agent and the backfill re-reads the log.
- ShellCoreTracker: RebootCoalescing branch on the already-watched 62407
stream -> esp_reboot_coalescing corroboration event; deliberate deviation:
emits on backfill too (live emit races the reboot kill).
- ANALYZE-ESP-005 advisory (warning): names the first URI via {{rebootUri}},
remediation = reassign profiles device -> user groups; +20 confidence on
coalescing corroboration, +10 on >=2 URIs.
- Config: Collectors.MdmRebootPolicyWatcherEnabled (default ON, internal
steering only - agent-native collector, NOT an IAgentAnalyzer, no admin UI),
MdmRebootPolicyBackfillLookbackMinutes=60; ConfigVersion 35.
- Event types deliberately distinct from system_reboot_detected so the
backend RebootCount (exact-string match, 2 sites) stays uninflated.
- Docs: docs/agent/mdm-reboot-coalescing.md (+index/log). Exact 2800/62407
message texts are unverified until E2E VM validation (tolerant matching).
Tests: Agent.V2.Core 1868/1868 (21 new), Functions 3099/3099, MCP 400 passed
(event-types drift), solution build 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ConsoleBypassWatcher: re-check _disposed after watcher.Start() — a Dispose()
racing the background arm could leave an armed WMI subscription no code path
could ever stop (leaked until process exit).
- SignalRContext: collapse rapid accessRevoked pushes into one restart via an
in-flight guard — a second start() on a non-Disconnected connection throws
and burns the retry budget.
- config/all: negative-cache managed ids a rescue scan proved missing (5 min
TTL) — one stale grant (offboarded tenant) no longer makes every delegated
config/all call pay a full table scan.
- DelegatedAdmin/TenantGroup functions: extract testable revoke-enforcement
cores (RevokeCoreAsync, SetEnabledCoreAsync, UnassignCoreAsync,
DeleteGroupCoreAsync, ValidateGrantRequest) and cover the security flows:
404-on-missing-row writes no audit row and fires no disconnect; real
revoke/disable/unassign/group-delete always cuts live streams; grant
validation is fail-closed (GUID tenantId, recognized roles only).
- dashboard/page.tsx: drop unused next/link import left by the banner removal.
Tests: backend 3099 passed, web 557 passed, agent ConsoleBypassWatcher suite
passed, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-07-18 storage migration reset every row's system Timestamp,
exposing that AuditLogs, OpsEvents and Events displayed, filtered and
retention-swept on it (custom "Timestamp" writes were silent no-ops —
reserved system property). All migrated rows read as 2026-07-18.
- New Helpers/BusinessTimestamp: RowKey decoders (audit !revtick_guid,
ops bare revtick, event yyyyMMddHHmmssfff_seq), RowKey-range OData
clause builders (tick-exact, with the mandatory RowKey lt '"' guard
so no lower bound can ever match legacy bare-GUID audit rows), and
dual DateTimeOffset/DateTime column reads.
- Writes in all three tables now persist OccurredUtc (custom column,
copied verbatim by future migrations); no-op ["Timestamp"] removed.
- Read mappers resolve OccurredUtc -> RowKey decode -> system Timestamp;
GetEarliestSessionEventTimestampAsync uses the same resolution so
StartedAt/Duration reconciliation can no longer absorb the migration
date on migrated sessions.
- Date-window filters and retention moved off the system Timestamp onto
RowKey ranges; ops retention loses its full-table scan and the
post-migration mass-delete cliff; audit retention runs a temporary
second pass for legacy GUID rows (TODO: remove after 2027-03-01).
- New GlobalAdmin backfill endpoint POST /api/maintenance/
backfill-occurred-utc (dry-run by default, batched, idempotent MERGE
of only OccurredUtc, table=audit|ops) — Events rows are deliberately
never mutated; their read path decodes the RowKey instead.
- Tests: decoders, clause boundary proofs incl. GUID-safety pins,
mapper precedence, backfill row decisions (Functions.Tests 3078).
- Docs: docs/backend/business-timestamps.md (system Timestamp is never
authoritative; migration checklist rule).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New rules/ section in the knowledge bundle: semantics table, from-phase latch
behavior (never via Failed, sticky through it), on_change x emitOnlyIfExists
composition, restart re-emit, no-phase-signal caveat, old-agent degradation,
and the prerequisite toggle-wipe fix. Index + log updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 4: portal round-trip for the new fields.
- types.ts: GatherRule + activePhases/activeFromPhase/emitMode; NewRuleForm +
scopeMode ("always" | "during" | "from") as UI-only view state; canonical
GATHER_PHASES list (EnrollmentPhase names Start..Complete with display
labels) + EMIT_MODES; new rules default to emitMode "on_change".
- withDerivedScopeMode: JSON-mode merges rule-shaped JSON (no scopeMode key)
over form state — the helper derives scopeMode from the data in all four
JSON paths (create apply/save, edit apply, card edit save) so pasted scope
fields survive into the payload instead of being silently dropped.
- GatherRuleFormFields: "Active During" mode select + phase multi-select /
from-phase dropdown (mode switch clears the other mode's fields), "Emit
Mode" select with helper text; triggerPhase free-text replaced by the same
canonical dropdown (placeholder used to suggest invalid tokens like
"DeviceESP"; legacy stored values remain selectable).
- page.tsx: create/save payloads carry the three fields; startEditing maps
them back (existing rules load as "always", the on_change default is for
NEW rules only); handleSaveEdit now also sends tags — same wipe class as
the toggle bug, the full-replace PUT dropped them on every edit.
- GatherRuleCard: trigger section shows the scope + emit mode, e.g.
"Interval every 60 seconds - from Account Setup - emit on change".
npx tsc --noEmit silent; vitest 557/557.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 3: the executor honors the new GatherRule fields.
Phase scope (ActivePhases / ActiveFromPhase), applied to ALL trigger types:
- interval: timers keep running, an out-of-scope tick is a free early-return
(no start/stop lifecycle races on phase transitions).
- startup: scoped rules not yet in scope at UpdateRules are deferred and run
exactly once from OnPhaseChanged when their scope activates (same
_startupRulesExecuted dedup; not part of the WaitForStartupRules latch).
- phase_change: evaluated against the NEW phase; scope gate runs BEFORE the
(rule, phase) dedup add so an out-of-scope pass does not consume the slot.
- on_event: gated by current phase.
- From-phase latch: phase != Unknown/Failed && ordinal >= threshold; sticky for
the session incl. through Failed; re-evaluated on config refresh so a rule
delivered after its threshold passed latches immediately.
- Before the first phase signal, scoped rules are inactive.
- IgnorePhaseScope bypass for --run-gather-rules (no phase context; logged).
EmitMode on_change:
- Canonical hash (keys ordinal-sorted at every level, invariant formatting,
SHA-256) of the raw collector result BEFORE ruleId/ruleTitle injection.
- First non-empty result always emits; unchanged results increment a per-rule
suppression counter; the next emitted event carries suppressedPolls +
suppressedSinceUtc. Empty results (emitOnlyIfExists miss) touch nothing, so
the combo yields zero noise, one event on appearance, then only on change.
- State survives UpdateRules refreshes (agent restart => at most one re-emit).
Tests: GatherRuleExecutorPhaseScopeTests (scope matrix synchronous via internal
IsRuleInScope; trigger wiring async with positive waits),
GatherRuleExecutorEmitModeTests (state machine via internal ShouldEmitOnChange,
canonical-hash properties, ExecuteRule wiring). V2.Core.Tests 1847/1847.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 0-2 of the gather-rule anti-spam plan:
- GatherRuleService.UpdateRuleAsync: toggle-style partial PUTs ({enabled,
isBuiltIn, isCommunity}) previously full-replaced the stored custom rule and
wiped Title/Target/Trigger/Parameters. Partials now merge Enabled into the
existing row; full edit payloads preserve the original CreatedAt.
- GatherRule: new opt-in fields ActivePhases (run only during listed phases),
ActiveFromPhase (sticky latch from a phase onwards), EmitMode (always |
on_change). Absent fields = legacy behavior; schema updated accordingly.
- TableStorageService.Rules: Store+Map for the three columns (absent column /
stored-empty -> null); MapToGatherRule internal for roundtrip tests.
- ContentEquivalent covers the new fields, null-tolerant for ActivePhases so
pre-existing rows never diff against seeds (no reseed churn).
- GatherRulesFunction.ValidateScopeAndEmitMode: canonical EnrollmentPhase name
list Start..Complete (Unknown/Failed and numeric tokens rejected), mutual
exclusion of the two scope fields, emitMode whitelist; 400 on violation.
- ConfigVersion 33 -> 34 (33 was taken by MigrateToApiBaseUrl).
Tests: GatherRuleUpdatePartialMergeTests (merge/replace/unknown-rule/built-in
path), GatherRuleScopeFieldsTests (Store<->Map roundtrip incl. legacy rows,
ContentEquivalent matrix, validation accept/reject). Functions.Tests 3029/3029.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
null = no migration is the documented contract and the agent already
assigns null when stripping the field from the on-disk cache; annotate
the property as string? so the backend assignment needs no ! suppression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Agent Binaries check + ops watcher now probe the canonical download alias
(download.autopilotmonitor.com) AND the legacy blob keepalive; alias failure
is unhealthy, legacy failure a warning
- McpServerBaseUrl fallback moved from the stale kindwave WEU ACA FQDN to the
stable custom domain mcp.autopilotmonitor.com (CNAME to the new GWC instance)
- health/detailed and health/mcp include the probed endpoint URLs in check
details for Global Admins only (storage table endpoint, WEBSITE_HOSTNAME,
alias/legacy download URLs, MCP server URL)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-home agents to a new API base URL through the agent/config response
(backend move or per-tenant EU->US region move) - the lesson from the
2026-07-18 WEU->GWC cutover, where the compiled-in API hostname hard-cut
every in-flight agent (no DNS indirection possible: mTLS breaks behind a
proxy, Flex custom-domain TLS is broken).
Backend (ConfigVersion 33):
- AdminConfiguration.AgentMigrateApiBaseUrl (global) +
AgentMigrateTenantOverridesJson (tenantId->URL; empty value pins a
tenant against the global target; override wins)
- GetAgentConfigFunction.ResolveMigrateTarget serves the validated target;
AgentMigrateServed / AgentMigrateRejected warnings as delivery evidence
- TableConfigRepository Store+Map; admin portal Global Settings round-trip
Agent (kill-switch-mirrored hardening):
- Shared AgentEndpointMigrationRules: https-only, *.azurewebsites.net on a
label boundary, no path/port/userinfo - enforced on BOTH sides
- Live fetch only: RemoteConfigService cache strips the field on write AND
read; EndpointMigration.ResolveTarget re-checks the fetch outcome
- AgentRuntimeConfig.Resolve returns before merge/integrity/bootstrap
cleanup on a pending migration (no spurious IntegrityCheckFailed against
a stale migration stub), Program.RunAgent rebuilds Phase 3+4 against the
new URL - single hop, kill signal wins before and after the hop
Tests: AgentEndpointMigrationTests (backend rules + resolution),
EndpointMigrationTests + cache-hardening strip (agent). Full suites green
(Functions 2992, Agent Core 1822, vitest 557, tsc silent).
Doc: docs/agent/endpoint-migration.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The brave-grass filename and secret name referred to the pre-migration
Static Web App hostname. Workflow now matches the deploy-backend/-mcp
naming scheme and uses AZURE_SWA_DEPLOY_TOKEN (already set to the new
SWA's token). The old secret is removed after the first green run.
Menu/dashboard/tip labels now match the docs page title. The temporary
maintenance banners on landing and dashboard are removed — the completion
notice lives in Service Announcements. Also checks in the migration
follow-up plan and the runbook outcome addendum.
Nothing required net8.0 -- the test runner just predated the .NET 10
migration. Agent projects stay net48 (Intune/OOBE constraint).
474/474 tests green on net10.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- global.json pins SDK 10.0.2xx (latestFeature) for local and CI parity
- build-agent.yml: SDK 10 as build toolchain, 8.0.x kept for the
net8.0 DecisionCore.Tests runtime
- build.ps1: stale .NET 8 hints updated to .NET 10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AdminConfiguration table SAS is issued for autopilotmonitoreu; the
MERGE URL still pointed at the legacy account and failed auth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cross-tenant re-claim of recently deleted global names is denied by
Azure by design (dangling-DNS/subdomain-takeover protection). New
tenant resources therefore use eu-suffixed names; the old names live
on in the old subscription (storage 'autopilotmonitor' recreated as
legacy bootstrap-URL keepalive, FuncApp/ACR kept as tombstones).
- API base URL -> autopilotmonitor-api-eu.azurewebsites.net
(Shared Constants, web CSP + /go fallback, MCP default)
- Agent blob container + build counter -> autopilotmonitoreu
- deploy-backend app-name, deploy-mcp ACR -> eu names
- Bootstrap script URLs stay on the legacy storage (customers
migrate once to a domain alias later)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gh attestation verify only passes with the repo's current name
(okieselbach/AutopilotMonitor); the signed workflow identity carries it,
and the old dashed name fails identity matching even though git-remote
redirects still work. Verified against the first attested dry-run ZIP.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First real run of build-agent.yml failed the counter CAS write with
"AuthenticationFailed ... sp is mandatory": the AZURE_BLOB_AGENT_SAS_TOKEN
secret stores the query with its leading '?', and the workflow prepends
another one, so Azure saw '?sp' as the first parameter name. The pre-existing
upload step had the same latent bug -- it had just never run. Normalize with
TrimStart('?') at all three usage sites (counter, blob upload, table merge).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build numbers now come from a public counter blob (buildcounter-v2.txt,
reserve-before-build with ETag-CAS) shared by local build.ps1 and CI, so the
two paths can never mint the same number. The CI step that committed
buildcounter.txt bumps to main is gone; the repo file is now a gitignored
offline fallback for -SkipUpload smoke builds only.
CI parity fixes vs the local build:
- ship SummaryDialog.exe.config in the ZIP (WPF AppContextSwitchOverrides;
without it the dialog dies with 0x8007045A when spawned from SYSTEM)
- verify EXE version == manifest == reserved build number before zipping
- update the AdminConfiguration hash-oracle only on publish_as_stable
CI-built release ZIPs now carry a GitHub Artifact Attestation (Sigstore
keyless provenance, actions/attest-build-provenance@v4).
Doc: docs/agent/build-counter-blob.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two dashboard flow gaps for cross-tenant (GA/MSP) tenant drilling:
1. Emptying the tenant filter input by hand (backspace/delete) only widened
the client-side view over the already-loaded filtered sessions; the
backend scope stayed on the old tenant until Filter was pressed again.
An emptied input now acts as an implicit clear and refetches all sessions.
2. When a filtered tenant had no sessions, the whole SessionTable — and with
it the tenant filter bar — unmounted, leaving only the welcome view with
no way to clear or change the filter without leaving the page. The filter
bar is now extracted into TenantFilterBar and stays on screen (with a
short hint) above the welcome message in cross-tenant mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe enrollment e7dbac7a (24H2) showed Windows writes IsAutoPilotDisabled
(capital P) while the troubleshooting FAQ documents IsAutopilotDisabled —
the missing-event flat evidence lookup was case-sensitive and would have
missed it. Registry value names are case-insensitive by contract, so the
dump dictionary now uses OrdinalIgnoreCase; original casing is preserved
in the emitted payload. Doc notes the casing caveat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In the normal IME deployment model the agent installs only after MDM
enrollment, so TPM attestation and ZTD errors from earlier (retried)
attempts sat invisible in the event log. The tracker now replays
Autopilot-channel Level 1-2 records from the last 4h at startup through
the regular emission pipeline.
Spam containment (answering "spam or backfilled event?" — individual
events, but marked and bounded):
- errors only (Warning-level 100 heartbeat excluded by XPath)
- harmless-ID downgrade + burst rollup still applies to backfilled records
- cap of 25 records per scan, newest first (tail logged, not emitted)
- RecordId watermark persisted to autopilot-error-backfill.json, advanced
by both backfill and live watcher -> restarts never re-emit
- events carry backfilled=true + original timeCreated; the event's own
timestamp is emission time, so backend StartedAt alignment is not
dragged backwards by pre-enrollment records
Docs: Observability matrix updated (TPM row now visible-via-backfill) +
new "Startup error backfill" section. Tests: 5 new (XPath, watermark
roundtrip/monotonicity, backfilled/live markers, rollup-on-backfill);
full V2 suite 1804 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The agent is normally installed by an IME platform script AFTER MDM
enrollment succeeded, so pre-enrollment failures (enrollment blockers,
TPM attestation) are invisible in the normal flow — no enrollment means
no agent and no authenticable session. Await-enrollment mode does not
help either: the binary only polls for the MDM certificate and cannot
authenticate without it. Only bootstrap-token deployments (one pilot
customer) can report pre-MDM.
- New Observability section in docs/agent/autopilot-ztd-diagnostics.md
with a per-rule visibility matrix (normal / bootstrap / await) and the
follow-up option of backfilling Autopilot-channel error records
- ANALYZE-ENRL-003/004 descriptions state the limitation explicitly so
portal users understand why these rules rarely fire
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend admits agent sessions via up to three device-validation
paths (Autopilot S/N lookup, Corporate Identifier, Device Association
once DevPrep becomes a hard gate) plus the pre-MDM Bootstrap token —
but the winning path was only returned to the agent, never stored, so
operators could not see HOW a device was accepted.
- SessionRegistration.ValidatedBy: server-populated in
ProcessRegisterAsync from the SecurityValidationResult (agent-sent
values are overwritten unconditionally)
- Sessions row + SessionsIndex mirror + MapToSessionSummary carry
ValidatedBy; latest non-Unknown validation wins (Bootstrap sessions
upgrade to the cert-path validator on re-register), Unknown preserves
the earlier value (tenants with device validation off)
- Web: "Device Validation" item on the session detail Session Info card
(hidden for legacy sessions / validation-off tenants)
- MCP: get_session overview exposes validatedBy; raw endpoints carry it
verbatim automatically
- Tests: SessionIndexProjectionTests pin the new index column
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sessions only exist after the backend's device-registration validation
(Autopilot S/N or Corporate Identifier via Graph) passed, so a device
with ProfileAvailable=0 is known to the tenant — the actual gap is the
deployment-profile ASSIGNMENT (not assigned, or still propagating when
the user went through OOBE), not the registration. Reword agent event,
UI callout, and MCP field docs accordingly; likelyCauses now
profile_not_assigned/profile_assignment_not_propagated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Devices that reach OOBE without an Autopilot profile (ProfileAvailable=0
in AutopilotPolicyCache — not registered, assignment not propagated, or
deleted from Autopilot) still enroll via manual Entra join + MDM
auto-enrollment and previously looked like regular Autopilot sessions
with an inexplicably empty profile section (session 423b5360).
- Agent: CollectAutopilotProfile emits autopilot_profile_missing
(Warning) on explicit ProfileAvailable=0 evidence, with ZeroTouch
tenant-domain extraction as supporting data; EmitDeviceInfoEvent
gains an optional severity parameter
- Web: DeviceDetailsCard shows a callout in the Autopilot Profile
section (also retroactively for old sessions via ProfileAvailable=0)
and renders Profile Available 0 as "No"
- MCP: resource-catalog gains the new event type + field docs
- Tests: 11 unit tests for the detection/extraction helpers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
list_tenants gains two optional filter params so the model resolves a
tenant in ONE tool call instead of paging manually:
- query: case-insensitive substring match on domainName (and tenantId
for pasted partial GUIDs), e.g. query="rewe" -> rewe-group.com
- tenantId: exact case-insensitive point lookup; wins over query
In filter mode the tool auto-paginates /api/config/all internally
(pageSize 1000, 10-page runaway budget -> `truncated` + recallNote if
ever hit) and returns only the matches. Filtering runs AFTER
extractTenantList secret-stripping and delegatedTenantListView MSP
scoping, so it can only narrow — never widen — what the caller may see.
Pure filterTenantList helper exported + 7 unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>