mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
61892592e490d2bbb985f2ff83f00cdc27c0da71
108 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f5efadfd07 |
ci: pin every third-party action to a commit SHA, gated and Dependabot-maintained (D-226)
A tag is a mutable pointer owned by the action's publisher. These jobs are not ordinary CI: they hold the code-signing Key Vault token (D-223), the federated storage credentials (D-222), and combine-rules.yml holds a contents:write token that pushes to main. Whoever could repoint v7 would run code with all of that. azure/login and Azure/functions-action were already pinned; the actions/* and github/codeql-action references were not, on the usual argument that GitHub owns them and shares the runner's trust boundary. That argument does not survive the fact that they run in the same jobs as the signing identity. All seven are now pinned by SHA with the version in a trailing comment -- the comment is what keeps the line reviewable, and Dependabot rewrites both together. action-pins.yml is the guard: pins nobody enforces rot, and no review reliably catches one @v4 added to a 200-line workflow. It lives in its own workflow because ci.yml deliberately ignores changes to the other workflow files, so a job placed there would stay silent for exactly the commits it must inspect. Like CI it reports rather than blocks -- main is unprotected; making it binding would mean adding it as a required status check, which is a branch-protection decision. Verified both directions: 49 references pass against the repo, and a fixture with @v7, @main and a bare SHA fails with three annotations while the local reusable workflow refs are skipped. Two floating tool versions closed while here. wix went from the range 5.* to 5.0.2 -- it writes the bytes of the MSI customers install, where AzureSignTool only signs them. ajv was the worse one: unpinned, and npm runs lifecycle scripts at install, so a hijacked release would have executed with the push token before a single rule was validated; now exact versions with --ignore-scripts. Line endings: sed -i rewrote the workflow files as LF. Git normalises on commit, so the content diff is unaffected and indexnow-notify.yml carries no change at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
647581b4ff |
refactor(msi): the MSI carries the shared stage-1 loader instead of its own downloader
Invoke-BootstrapDownload.ps1 and Start-AutopilotMonitor.ps1 had become the same script: download the current installer, run it, log, always exit 0. The MSI copy additionally downloaded and executed WITHOUT verifying what it got, which is exactly the gap the loader was written to close. The MSI is now packaging only -- it installs and runs the same file that is assigned directly in Intune, and both delivery channels get publisher verification from one implementation. Dropped from the MSI path: its own Deployed-marker check. That guard lives in stage 2 and runs there anyway, so the only cost is one download on a device that is already deployed. Kept: the log file name. bootstrap-msi.log is what the diagnostics tooling keys on to tell the MDM-LOB channel apart from the Intune script channel, so the loader takes -LogFileName (default bootstrap-loader.log) and the custom action passes bootstrap-msi.log. A Pester test now pins that contract across the two files -- renaming the parameter on either side would leave the MSI channel logging under the wrong name. Package version 1.0.2 -> 1.1.0 so Intune LOB update detection sees the new payload, and a new component GUID because the key path file changed. Loader 1.0 -> 1.1. Bootstrap suite on Windows PowerShell 5.1: 44/44. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7563435837 |
feat(ci): Authenticode-sign the bootstrap MSI and the runner it embeds (D-223)
MSI carries an Authenticode signature like any PE, so the same identity and the same ordering rules apply: signing identity logs in first and only for the Key Vault token, the storage login stays last, and no environment: on the job. The embedded Invoke-BootstrapDownload.ps1 is signed BEFORE wix build -- wix embeds the file as it finds it, so signing afterwards would leave the copy inside the MSI unsigned, and a device under an AllSigned policy needs the runner to carry its own signature. The MSI is signed after the build and before the hash and the upload, then both files are verified (Valid, timestamped, optional publisher pin) while nothing is published yet. The provenance attestation now also skips runs that upload nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
47dad7143f |
docs(ci): tighten the code-signing comments in build-agent
Public repo: keep the reasoning that a later change could break -- signing must precede the release hashes, the job must never carry an environment, the timestamp is not optional -- and drop the rest. Comments only, no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0ac548bfce |
feat(bootstrap): two-stage loader and Authenticode-signed customer scripts (D-224)
Start-AutopilotMonitor.ps1 is the new stage 1 -- the file an administrator assigns in Intune. It carries no enrollment logic: it downloads the published installer, verifies its Authenticode signature, and runs it in the same process, passing through every remaining argument. Assigning the installer directly stays supported and unchanged. The point of the split is that the customer-held artifact stops changing. Today every bootstrap change forces customers to replace their Intune copy, so the fleet runs a distribution of script versions we do not control. It is also what makes script signing worth doing: signing a file that gets swapped by hand each release is a one-off gain, signing a loader that never changes is permanent. The loader is deliberately minimal, because it is the one piece that cannot be fixed remotely -- every guard placed in it would be frozen for years. All guards stay in stage 2. Verification pins the certificate SUBJECT, never a thumbprint: certificates get renewed and a deployed loader cannot be, so a thumbprint pin would strand the fleet at the next renewal. A hash oracle was rejected for a different reason -- it would have to be fetched from the same host as the script, so it could not survive a compromise of that host, while the expected publisher travels inside the loader. Fail-closed on verification, fail-soft on exit: nothing runs without a valid signature from us (running unverified code as SYSTEM is the risk being removed here), but the loader still exits 0, because a device without monitoring is acceptable and a failed enrollment step is not. The accepted cost is that a device which can never fetch or verify stays silent. Publisher changes: - every published script is signed (loader, installer, both -Dev renders, Test-ShouldBootstrapAgent, Grant-AutopilotMonitorAddOn), and signing is the LAST transformation: it covers exactly the CRLF/UTF-8 bytes Get-PublishBytes produced, so nothing may normalise or re-encode them afterwards. - signing is mandatory for a real publish. Only -DryRun may run unsigned, because the PR gate executes the publisher without any Azure login. - comparisons against a published copy strip the signature block first. A timestamped signature differs on every run, so without stripping the version-bump guard and the drift check would report a change for every unchanged script. - the bump guard became Assert-VersionBump and now covers the loader as well. - version.json gains loaderVersion. Additive, no consumer yet: stage 2 is current by construction, so only stage 1's version says anything about a customer's Intune copy. - -SignedOutputDir writes the exact uploaded bytes for the CI artifact, in a dry run too -- that is how a signed script gets inspected before it reaches anyone. publish-scripts.yml logs in with the signing identity first and only to fetch the Key Vault token (azure/login sets the default az account and the storage login has to be the one that stays), then publishes and offers the signed scripts as an artifact. build-agent.yml passes the same parameters on a stable cutover. Tests: 14 new Pester cases, including that a validly signed script from a FOREIGN publisher is refused -- that is what an attacker with any code-signing certificate can produce. Full bootstrap suite on Windows PowerShell 5.1: 43/43. Not covered: the MSI runner still downloads stage 2 without verifying it, and the customer docs still describe the direct assignment only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9c1d0586ef |
ci: skip the provenance attestation on test runs of build-agent
A dispatch with upload_blob=false and create_release=false publishes nothing, so the Sigstore attestation would write a permanent public transparency-log entry for bytes nobody ever receives. The step now runs only when the run actually ships something. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
21ec5e4773 |
ci: Authenticode-sign the agent binaries before the release hashes are computed (D-223)
Every assembly we author -- AutopilotMonitor.Agent.exe, AutopilotMonitor.SummaryDialog.exe and the three AutopilotMonitor.*.dll beside them -- is signed with AzureSignTool (pinned 7.0.1) against a certificate in the corporate code-signing Key Vault, SHA-256 file digest plus an RFC-3161 timestamp. Third-party assemblies keep their own publisher's signature. The position of the step is load-bearing: signing rewrites the PE file, so it has to run before the staging copy, the ZIP and every SHA-256 in "Create release artifact". Those hashes are the bootstrap's integrity oracle (version.json, AdminConfiguration) -- signing after hashing would publish hashes that no downloaded file can ever match. The signing identity is a SECOND federated principal in the same tenant and logs in FIRST, only to fetch a Key Vault token; the storage login of D-222 follows and owns the az context from there, because azure/login sets the default account and the last login wins. Both federated credentials carry the subject repo:okieselbach/AutopilotMonitor:ref:refs/heads/main, so this job must never be given a GitHub 'environment:' -- that rewrites the OIDC subject claim and would break BOTH logins at once. No unsigned fallback and no dispatch input to skip signing: a missing repository variable, a Key Vault failure or a non-zero azuresigntool exit code fails the run before anything is published. azuresigntool is a native tool, so $LASTEXITCODE is checked explicitly -- PowerShell does not throw on a non-zero exit, and a silently unsigned release is exactly the outcome signing exists to prevent. Verification runs against the STAGED bytes, not the build output: every AutopilotMonitor.* PE in the ZIP must be Valid, must carry a timestamp (without one the signature dies with the certificate) and all of them must share one signing certificate. Setting the optional repository variable CODE_SIGN_EXPECTED_SUBJECT additionally pins the publisher, so the gate proves "signed by us" instead of "signed by someone trusted". The build provenance attestation now covers the ZIP and both EXEs, so an extracted binary can be verified without the ZIP. Authenticode and the attestation answer different questions and both stay: who published this binary (offline, what Windows, SmartScreen and WDAC evaluate) versus which build produced these exact bytes (gh attestation verify). Not covered here: the customer bootstrap script (its signature block changes the published bytes and must follow the dev render; see B-57c, the two-stage loader), the bootstrap MSI, and the local build.ps1 path, which from now on produces unsigned bits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e4a5a6654d |
ci: workflows authorize Azure Storage with the federated identity, not SAS secrets (D-222)
Every storage write against autopilotmonitoreu -- build-counter CAS, agent ZIP and MSI, version manifests, customer bootstrap scripts, the AdminConfiguration hash oracle -- now rides on the existing federated credential (subject repo:.../ref:refs/heads/main) instead of a long-lived SAS in repo secrets. The REST calls themselves are unchanged: '?<sas>' became an Authorization header, so the ETag-CAS of the counters, Content-MD5 and the no-cache contract survive verbatim. That is also why this is not a rewrite onto 'az storage blob upload' -- it cannot express If-Match, and two concurrent builds could then mint the same build number. .github/scripts/Get-StorageAuthHeaders.ps1 is the single place that knows the resource id, the az invocation and the service version. x-ms-version is mandatory for Entra-authorized requests (>= 2017-11-09) and for any Table request without a SAS; 2021-08-06 was verified live against both the blob and the table endpoint of this account. azure/login added to build-agent, build-msi, publish-scripts (plus id-token: write) and deploy-web; deploy-backend and deploy-mcp already had it. Publish-BootstrapScripts.ps1 -DryRun acquires NO token: the PR gate runs it without any Azure login, and that must stay true. The AdminConfiguration merge lost its "secret not configured" skip branch -- a failure there now surfaces instead of silently leaving the hash oracle pointing at an older build. AZURE_BLOB_AGENT_SAS_TOKEN, AZURE_BLOB_VERSIONS_SAS_TOKEN and AZURE_TABLE_ADMIN_SAS_TOKEN are retired once the first runs are green. AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN stays: that account lives in the pre-migration tenant, where this principal has no identity -- it dies with the mirror, not before. Requires on the SP: Storage Blob Data Contributor on the containers 'agent' and 'versions', Storage Table Data Contributor on the table 'AdminConfiguration'. Granted 2026-09-08. |
||
|
|
4e73ba8900 | Agent Host changes for agents/improve-changelog-promotion-ui (#152) | ||
|
|
38b667799c |
ci: workflow-only commits no longer trigger CI and CodeQL
The
|
||
|
|
a5f74d6abc |
ci(agent): release body no longer repeats the release title
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d0e09301a1 |
ci(agent): release body drops the Installation and Included Components sections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b89008e745 |
ci(agent): customer release notes come in through the release_notes dispatch input, never from CI (D-187)
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> |
||
|
|
9eb8cac507 |
ci: skip CI and CodeQL on gitlink bumps and prose-only commits
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> |
||
|
|
db020789e1 |
chore: private submodule docs/ becomes internal/ (knowledge + decisions + work files + private instructions)
- .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 |
||
|
|
4c32d66a63 |
ci: print failed-test messages in the .NET test steps
`dotnet test --verbosity quiet` also sets the console logger to quiet, which drops the "Error Message" block of a failed test — run 33581374985 showed only four "[FAIL]" lines for the V2 Core scenario flake and nothing about which fixture budget blew. An explicit `--logger "console;verbosity=minimal"` keeps the build quiet but prints the failed tests with message and stack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d763a885e2 |
Ops alert channels, ops-events server-side filters, and generated MCP tool vocabularies (#140)
* feat(backend,web): Telegram als Channel-Provider + ein einziger Dispatch-Pfad Notifications hatten zwei Sendewege: WebhookNotificationService.SendToChannelsAsync fuer tenant-konfigurierte Channels, und TelegramNotificationService direkt aus dem Ops-Alert-Pfad. Ein neuer Provider haette beide anfassen muessen. Neu: NotificationChannelDispatcher ist DIE Channel-Sende-API und routet pro Provider — WebhookProviderType.Telegram zum Bot, alles andere als gerenderten Webhook-POST. SendToChannelsAsync wurde aus WebhookNotificationService entfernt statt dupliziert, damit es keinen zweiten Pfad gibt; alle sieben Call-Sites ziehen um (EventIngestProcessor, RegisterSession, ReportDistress, SlaBreachEvaluation, AnalyzeOnEnrollmentEnd, TestWebhookNotification). SendOpsAlertAsync/SendNotificationAsync sind virtual (Test-Seam), TelegramNotificationService bekommt SendAlertWithResultAsync fuer die Test-Endpoints und RenderAlertText als testbaren Kern. Telegram (=40) ist der Sonderfall: kein Webhook. Der Channel-Url traegt die CHAT-ID, der Bot-Token gehoert der Plattform (PreviewConfig). Ein Tenant-Admin wuerde durch UNSEREN Bot senden — deshalb ist das Gate eine Server-Regel, keine versteckte Dropdown-Option: ValidateTelegramChannelGate sitzt in TenantConfigValidation.ValidateModel und deckt damit BEIDE Schreibpfade ab (Full-Model-PUT und den MCP-Field-Patch ueber TenantConfigPatchService) — in der Update-Function allein waere der Patch-Pfad daran vorbeigelaufen. Ein unveraenderter, GA-angelegter Channel passiert das Gate, damit ein Tenant-Admin unrelated Config weiter speichern kann. Telegram-Ziele ueberspringen SsrfGuard.ValidateWebhookUrlFormat (eine Chat-ID ist keine URL und wuerde immer failen) und werden stattdessen als numerische Id oder @username geprueft. Mitgeliefert, weil der Dispatcher und die Ops-Seite sie brauchen: NotificationChannel.RedactList / RestoreRedactedList als geteilte Helfer hochgezogen (TenantConfiguration delegiert jetzt dorthin, statt eine zweite Kopie zu pflegen), AdminConfiguration.OpsNotificationChannelsJson + GetOpsNotificationChannels() mit Legacy-Slot-Synthese, OpsAlertRule.NotifyChannelIds/IncludePayload, NotificationAlert.DataJson (nur GenericJsonRenderer serialisiert es, additiv in schemaVersion 1.0). Web: der Channel-Editor liegt jetzt in components/notifications/ChannelEditor.tsx und wird von Tenant- und Admin-Seite geteilt (showEventToggles trennt die Faelle). Telegram erscheint im Provider-Dropdown nur fuer Global Admins (Prop aus useAuth().user?.isGlobalAdmin, Hausmuster aus SectionAutopilotValidation); ein bereits konfigurierter Telegram-Channel bleibt fuer alle sichtbar, damit ein Speichern ihn nicht still verwirft. Tests: TelegramChannelProviderTests (27) — Dispatcher-Routing inkl. "Telegram erreicht NIE den SSRF-Pfad", Chat-ID-Validierung, GA-Gate auf beiden Schreibpfaden. Mutationsgeprueft: Gate invertiert => 5 rot, zurueck => 27 gruen. OpsNotificationChannelsTests + OpsAlertPayloadTests decken die mitgelieferten Modell-Additionen ab. TestNotifications ist der gemeinsame inerte Stub, der die 17 bestehenden OpsAlertDispatchService-Konstruktionen ersetzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend,web): Ops-Alerts routen auf benannte Channels statt auf drei feste Slots OpsAlertRule hatte kein Ziel. Jede matchende Regel ging an alle drei fest verdrahteten Slots in AdminConfiguration (Telegram-ChatId / Teams-URL / Slack-URL), und nur die ERSTE matchende Regel wurde ueberhaupt ausgewertet (FirstOrDefault). Ein Conversion-Signal an ein Sales-Team zu binden hiess also: das Sales-Team bekommt auch jede Security-Regel. Jetzt: AdminConfiguration.OpsNotificationChannelsJson haelt eine benannte Channel-Liste (dasselbe NotificationChannel-Modell wie tenant-seitig, nur anderer Scope), und OpsAlertRule.NotifyChannelIds bindet Regeln daran. Dispatch wertet ALLE matchenden Regeln aus (Where statt FirstOrDefault) und vereinigt ihre Ziele. Rueckwaertskompatibel ohne Migrationsjob: - Leere Channel-Liste => GetOpsNotificationChannels() synthetisiert je konfiguriertem Legacy-Slot einen Channel mit stabiler Id (legacy-telegram heisst "Autopilot Monitor IT Engineer") und uebernimmt die Legacy-Enabled-Flags — ein bewusst abgeschalteter Provider bleibt abgeschaltet. - GetAdminConfigurationFunction projiziert DIESELBE Synthese in das Read-Model, damit der Editor zeigt, was heute tatsaechlich Alerts empfaengt, und der erste Save exakt das materialisiert. Die Alternative waere eine zweite Kopie der Synthese-Regeln im Web gewesen, die vom Dispatcher wegdriftet. - Leere NotifyChannelIds = alle aktivierten Channels. Das ist, was jede heute existierende Regel traegt, also verhaelt sich am Deploy-Tag nichts anders. - Ids, die nicht mehr aufloesen, werden verworfen statt auf Broadcast zurueckzufallen: eine Regel, deren einziger Channel geloescht wurde, muss NICHTS erreichen, nicht alles. Die Legacy-Felder bleiben als Migrationsquelle stehen und werden von der Alerts-Section nicht mehr geschrieben — aufraeumen erst, wenn produktiv Channels stehen. Sicherheit: OpsNotificationChannelsJson ist secret-tragend und steht in der Deny-List von RedactedCopyForReader() (ConfigRedactionTests bewacht Drift). Der Admin-PUT hat bisher keinen Redacted-Restore — er bekommt jetzt einen fuer die Channel-Liste, damit ein zurueckgespieltes Reader-View nicht "***REDACTED***" als Chat-ID persistiert, plus die strukturelle Validierung, die auch die Tenant-Liste passiert. Neuer Endpoint POST /api/global/config/test-ops-channel (GlobalAdminOnly, im Policy-Catalog registriert). Kein neues Response-DTO: TestWebhookNotificationResponse hat identische Semantik und wird geteilt — nur der Doc-Kommentar nennt jetzt beide Routen. Web: die "Alert Providers"-Karte weicht der Channel-Liste (geteilter ChannelEditor, im Admin-Kontext mit Telegram und ohne die tenant-spezifischen Event-Toggles). Jede Regel bekommt einen Channel-Picker; leere Auswahl rendert als "all channels", nicht als Warnung — hier ist das der gueltige Default. Die Bindungslogik liegt testbar in opsChannelRouting.ts und spiegelt ResolveTargets: der erste explizite Pick startet vom LEEREN Set, sonst liesse sich eine Broadcast-Regel nie verengen. handleSaveOpsAlertConfig nimmt jetzt ein Objekt statt elf positionaler Argumente — bei einem Dutzend Feldern schreibt ein vertauschtes Paar sonst still eine Webhook-URL in den falschen Provider-Slot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend): TenantTrialStarted + Event-Payload optional pro Regel Der Trial-Start war stumm. POST config/{tenantId}/trial schrieb nur einen Audit-Entry, und der GA-Pfad (PATCH plan) feuerte nur beim Downgrade — also war genau der Conversion-Moment, den Sales/Support interessiert, das einzige Plan-Ereignis ohne Ops-Event. RecordTenantTrialStartedAsync (Tenant/Info) feuert jetzt aus BEIDEN Schreibpfaden, unterschieden per selfService-Flag, und jeweils NACH dem persistierten Save (SaveConfigurationAsync ist fail-loud), damit kein Alert eine Trial ankuendigt, die nicht gespeichert wurde. Der GA-Pfad haengt an der aufgezeichneten Aenderung, nicht am Request: ein reiner planTier-Edit bleibt still, ein idempotenter Save mit demselben Datum ebenfalls, und das Beenden einer Trial (explizit null) ist ein Downgrade und bleibt bei TenantPlanDowngraded. Payload-Zustellung: WriteAsync reicht die serialisierten Details an DispatchAsync durch — aber ein Channel bekommt sie nur, wenn eine Regel OpsAlertRule.IncludePayload setzt. Default AUS, bewusst: ein Ops-Alert ist ein "da ist was, schau nach"-Signal, die meisten Payloads sind im Chat Rauschen, und manche tragen Felder, die die Baseline nie traegt (Tenant-Domain, Contact-Adresse). Das Informationsniveau aller bestehenden Regeln bleibt damit unveraendert. ResolveTargets liefert deshalb zwei Gruppen. Ein Channel, den sowohl eine Payload-Regel als auch eine schlichte Regel adressiert, landet nur in der Payload-Gruppe: eine Nachricht pro Channel, und ein unbeteiligtes Geschwister-Match kann ein bewusstes Opt-in nicht aushebeln. Fuer den angereicherten Alert flacht BuildAlert die Top-Level-Skalare in Facts ab (max 12, Werte <= 256 Zeichen, Nulls und Verschachteltes raus), damit Card-Formate und der Telegram-Klartext dieselbe Information tragen wie der GenericJson-Consumer unter "data". EventType wird immer gesetzt — derselbe Wert steckt schon im "Event"-Fact, also keine zusaetzliche Information, nur ein stabiler Schluessel zum Verzweigen. Tests: TenantTrialStartedEventTests (8) — aufgezeichnete Event-Form inkl. Payload-Feldern, und die Emissionsbedingung des GA-Pfads gegen ApplyPlanChanges gepinnt (greift bei Grant und Verlaengerung, schweigt bei Trial-Ende, planTier-only und idempotentem Save). Die HTTP-Entry-Points bleiben ungetestet — Suite-Konvention ist, die reinen Kerne zu testen. Docs: OKF-Concept backend/ops-notification-channels.md. Registrierung in docs/index.md + docs/log.md fehlt bewusst — beide Dateien tragen noch fremdes uncommittetes WIP (ops-events Query-Surface), und ein Teil-Commit darin haette es mitgenommen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend,mcp): ops-events server-side field filters + generated MCP tool vocabularies The ops-events read could only narrow by category and date window, so answering "did any AgentEmergencyBreak fire yesterday?" meant pulling a whole category and filtering it client-side — and for the MCP tool that client is a language model, so the question cost a category-sized response in model context. Ops-events filter surface (mirrors the audit-log filter pattern one-for-one): - OpsEventQueryFilters (Shared) + OpsEventFilterRequest (query -> filters + pagination extras) + TableOpsEventRepository.AppendFieldFilters (filters -> OData clauses). BOTH filter builders call the appender — the single-category / unpaged one and the per-partition fan-out builder that adds the RowKey bound — so "no category named" cannot silently return unfiltered rows. - minSeverity expands to an OR-set, not a range: Table Storage compares strings lexicographically and the severity names sort Critical < Error < Info < Warning, so `Severity ge 'Warning'` would drop exactly the alert-worthy half. The ladder now lives in OpsEventSeverity.Rank, shared with OpsAlertDispatchService, so "Warning and above" in a read means what it meant when the alert rules fired. - severity is normalised onto the canonical vocabulary and an unknown value is a 400: `eq` is case-sensitive, so "?severity=warning" would otherwise return an empty page that reads exactly like "no such events happened". eventType stays verbatim — stored rows outlive the vocabulary, so a retired type must remain searchable in the historical window. - Every filter value is folded into the continuation fingerprint and echoed on nextLink; extras are appended AFTER the pre-existing category/tenantId discriminators and skipped when empty, so tokens minted before the filters existed still fingerprint identically. The tenantId drill deliberately stays in memory (OrdinalIgnoreCase; an OData eq would lose rows on casing). - MCP get_ops_events gains eventType / severity / minSeverity plus a `days` shorthand resolved client-side to a concrete dateFrom (a backend-side "last N days" would re-resolve "now" on every page and blow the token fingerprint); an explicit bound always wins over the shorthand. Fixed alongside: the paged cross-category fan-out skipped Platform (Azure Monitor alerts) because its category list had drifted from OpsEventCategory — written from day one, invisible to every paged reader, still visible on the unpaged full-table path. The list is now OpsEventCategory.All plus a reflection coverage test. That drift had a structural cause, so the same round closes it for the MCP as a whole: wire TYPES reached the MCP from day one, but vocabularies did not — types are erased at runtime and z.enum() needs values, so every tool schema hand-typed its own copy with nothing to compare against. The copies had drifted: search_sessions offered 5 of 8 session statuses (no AwaitingUser, no Incomplete — both real stored statuses), both event readers offered 4 of 6 severities (no Debug, no Trace), get_ops_events named 6 of 7 ops categories. - The codegen now emits src/generated/wire-vocabularies.generated.ts (VALUES: session statuses, event severities, ops categories/severities/types, annotation lanes/verdicts) from the manifest sections listed in MCP_VOCABULARIES; SharedManifestParityTests exports the two new ops vocabularies. Freshness is pinned twice — byte-equality against a fresh codegen run in the MCP suite, and the shared-manifests-in-sync CI job. - Tool schemas derive their z.enum() from it and descriptions interpolate the list instead of spelling it. - vocabulary-drift.guard.test.ts ratchets what is left: every inline z.enum([..]) must sit on a baseline naming why it has no backend owner (MCP-local knobs, plus rule type / connection type / CVE risk, which have no C# constants class yet). A new hand-typed list fails, and so does a stale baseline entry. - OpsEventTypes (Shared) gives the 77 ops event types a canonical home: the OpsEventService write sites use the constants (a raw literal now fails OpsEventTypeDualRegisterTests, which moved from regex-scraping to reflection), the manifest exports them, and get_resource(name="ops_event_types") serves categories + severities + types to the model from the generated copy. Suites: Backend 5427, MCP 629 + tsc, Web tsc + vitest 965, generator idempotent. Deploy order: backend BEFORE the MCP redeploy — an MCP that sends filters to an older backend gets silently unfiltered rows back and presents them as filtered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: register the ops notification channels doc in the index and log The concept doc landed with the feature commits; its index entry and log entry were still uncommitted in the working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
73a6817713 |
build(deps): bump github/codeql-action from 4.37.3 to 4.37.9 (#138)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.9) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c04280cf68 |
feat(mcp): MCP server adopts the generated wire types — the codegen now writes a second copy of wire-types.generated.ts into McpServer/src/generated (MCP_HEADER, buildMcpWireTypesSource, same freshness contract), pinned by a new MCP vitest freshness test (repo-root discovery like event-types-drift, byte-equal to a fresh codegen run, mutation-proven red/green) and enforced in the CI shared-manifests-in-sync diff check; the hand-declared response shapes and structural inline casts in tools/*.ts now import the generated types on the INPUT side only (tool output contracts unchanged): sessions.ts (GetSessionResponse/GetSessionEventsResponse/GetRuleResultsResponse/GetSessionAnnotationsResponse/DiagnosticsDownloadTicketResponse; get_session_summary events are Partial<EnrollmentEvent>, RuleResult drops the dead ruleTitle??title fallback), admin.ts (usage union GetMcpUserUsage/GetGlobalMcpUsage/GetGlobalMcpUsageDaily, GeographicLocationSessionsLeanResponse, GetAllTenantConfigurationsResponse, QueryRawTableResponse, GetUnmatchedSoftwareResponse, SoftwareInventoryResponse; the client-side pagers pin their wire-critical records/summaries/inventory key names via satisfies keyof so a backend rename goes tsc-red instead of silently paging nothing), search.ts (GetSessionEventsResponse on both event readers, SearchSessionsResponse on the legacy fallback, RawEventsPage = Partial<Pick<QueryRawEventsResponse,...>>), rules.ts (DryRunAnalyzeRuleResponse envelope; the inner RuleDryRun trace stays a local reader because the wire type is deliberately unknown); the raw-session fallback in get_session_diagnostics/get_session_summary is removed — { success, session } IS the pinned contract, and the diag test now asserts a non-enveloped object surfaces as a loud tool error instead of being silently absorbed; rule-stats, global/metrics/platform and verdict-calibration keep local readers (backend still serializes anonymous objects there — no DTO exists to import); MCP suite 617 green, tsc green, generator idempotent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d055eed074 |
Typed API contract: every success response is a wire DTO, generated end-to-end into TypeScript (#136)
* feat(backend): typed API response infrastructure — IApiResponse marker, wire-settings single source, widened anonymous-body ratchet (batch 0 of typed-api-contract) IApiResponse (Shared, with SuccessMessageResponse/SuccessOnlyResponse and the ProjectedItems/WireContract attributes for the upcoming manifest export) plus generic OkAsync/CreatedAsync/JsonAsync<T> overloads constrained on it, so anonymous success bodies stop compiling once the legacy object overloads go; ApiJsonOptions is now the single source of the production wire settings (camelCase, WhenWritingNull, string enums) used by Program.cs AND the three endpoint-test harnesses (two serialized PascalCase-with-nulls, one JsonSerializerDefaults.Web — all could pass while the deployed wire differed); TypedResponseGuardTests replaces OkAsyncBaselineGuardTests with the same ratchet mechanics over two frozen baselines (44 anonymous helper sites unchanged, plus 134 anonymous success WriteAsJsonAsync sites across 76 files — error-shaped bodies stay anonymous by design) and a flatness/location reflection guard (IApiResponse implementers derive from object and live in Shared, protecting JSON key order); ApiResponseWireParityTests carries the ordinal old-vs-new serialization proof each converted site must add Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(backend): Sessions/Annotations/Reports/Raw/Progress success responses are typed wire DTOs (batch 1 of typed-api-contract) 34 anonymous success bodies across 24 functions replaced with flat IApiResponse DTOs in Shared (SessionApiModels/ReportApiModels + new Annotation/Raw/ProgressApiModels) — minimal in-place transformation, call structure/status codes untouched, declaration order == wire order, nullable exactly where a site can emit null (WhenWritingNull); fields= projections stay IReadOnlyList<object> marked [ProjectedItems(SessionSummary/ EnrollmentEvent)], raw table rows stay PascalCase-verbatim dictionaries (no DictionaryKeyPolicy — pinned by test), AnnotationWire and the distress-report aggregations now build named item types instead of anonymous ones, and shared shapes collapse to one DTO (SessionListResponse x4, SessionAnnotationListResponse x2, SessionReportListResponse/GetSessionEventsResponse paged+unpaged via absent-null nextLink); 54 new wire-parity facts serialize each OLD anonymous literal (independent fixture copied from the pre-migration site) against the new DTO ordinally, with a null case per vanishing key; ratchet baselines shrink 44->14 helper sites and 134->122 write sites; the three distress-report test files read the now-typed aggregation items directly instead of via reflection-ToDynamic Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(backend): every anonymous API success body is a typed wire DTO — ratchet baselines empty, legacy object overloads deleted (batches 2-5 of typed-api-contract) the remaining 141 anonymous success sites (Admin 36, Rules 24, Config 15, Graph 5, Metrics 16, Vulnerability 17, Infrastructure/Global/Notifications/Bootstrap/ Diagnostics 20 incl. the 429 quota body in McpQuotaEnforcementMiddleware, plus the 14 helper-call sites) now return flat IApiResponse DTOs from Shared (new Admin/ Rules/Config/Graph/Metrics/Vulnerability/Infrastructure/Notifications/Diagnostics ApiModels; GeographicApiModels extended) — same minimal in-place transformation as batch 1: declaration order == wire order, nullable exactly where a site can emit null, dual success/failure sites keep their status-code logic and reuse SuccessMessageResponse, MCP-critical key names (records/summaries/inventory) pinned; wire item shapes that were function-local (CustomsArchive Run/EntrySummary, CpeMappingDto, UnmatchedSoftwareAgg, AutoResolve items, LogPatternTestResult, CveExposureSummary/SeverityBreakdown/TopCve — records rewritten as flat classes, netstandard2.0 has no IsExternalInit) moved to Shared so the manifest export sees them; payload types that must stay in the Functions assembly (RuleDryRun, PreviewWhitelistEntity, GlobalAdminEntity, HealthCheck, GlobalNotificationDto, Backfill/Reclassification/MaintenanceResult, TenantAdminEntity, TenantConfigFieldSchema) ride as object slots — wire-identical via runtime-type serialization, proven by the parity facts; 126 new wire-parity facts total (Admin 26, Rules 12, Config 17, Graph 7, Metrics 16, Vulnerability 19, Infrastructure 21 + Dictionary-key-verbatim and null-value pins); both TypedResponseGuard baselines are now EMPTY regression guards and the untyped OkAsync/CreatedAsync(object) overloads are gone, so a new anonymous success body no longer compiles through the helper and fails the guard through raw WriteAsJsonAsync; deliberately untouched: error bodies (one anonymous shape by design), auth/me (AuthDecisionResult.Body slot shared with error bodies, tests reflect over it), CheckMcpAccess (conditional dictionary keys), raw-byte streamers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(contract): C#→TS wire codegen — shared-manifests v2 exports the full response-type graph as generated TypeScript (batch 6 of typed-api-contract) SharedManifestParityTests now writes schemaVersion 2: the v1 catalog sections stay byte-compatible (sectionFieldMap/signalrMessages/sharedManifestChecks untouched), the consumer-less sessionSummary section is gone, and a new types section carries every wire type — roots are all IApiResponse implementers plus [WireContract]-marked types (SlaViolatorSession, ImeVersionHistoryEntry), transitively closed by the new WireTypeManifestBuilder over refs, collections, dictionaries, enums and [ProjectedItems] item types (208 types). Fields honour [JsonPropertyName]/ [JsonIgnore], get-only members export readonly, a nullable slot exports optional (WhenWritingNull omits the KEY — explicit null survives only inside collections and dictionary values, so the nullable flag lives on nested descriptors only), field order follows STJ's derived-before-base serialization for the one legacy inherited wire type (LocationSessionRow : SessionSummary), and unmappable shapes (non-string dictionary keys, foreign BCL classes) fail the build instead of degrading to unknown. C# <summary> texts travel as JSDoc: Shared builds XML docs now (doc-quality warnings CS1591/1573/1574/1584/0419 suppressed — crefs to foreign assemblies cannot resolve from Shared; CS1570 malformed-XML stays an error, which surfaced a raw 0x01 control char and an unescaped ampersand in existing comments). generate-shared-manifest-types.js emits utils/wire-types.generated.ts (interfaces + string-union enums, 2.7k lines) next to the lean literal module, npm run generate:manifests wraps it, the vitest freshness test pins BOTH outputs, and a new shared-manifests-in-sync CI job (guardrails-in-sync pattern) fails on drift Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hand-written response types re-export the generated wire interfaces (batch 7+8 of typed-api-contract) types/session.ts, types/enrollment.ts and types/adminConfig.ts are re-exports over utils/wire-types.generated.ts (Session = SessionSummary alias for the 25+ barrel importers; adminConfig keeps only the excessiveEventAutoActionMode literal-union narrowing and the client-side OpsAlertRule view) — the generated types are never weakened, consumers adapt: durationSeconds is now optional as on the wire (int? — consumers guard with ?? 0, which also retires the latent NaN-minutes display on in-progress sessions), status is the SessionStatus union (fixtures on canonical values; the generic string-sort test keeps its own fixture type), required bools break hand-built fixtures once → new test/factories.ts makeSession/makeEvent (Object.assign, not spread — Partial spread re-widens and tests must force data: undefined); the string|null lifecycle branches were already truthiness-based. Duplicate local types deleted: geographic-performance now uses the real LocationSessionRow wire type (incl. DO fields), GlobalSearch the generated QuickSearchResult, sla/page.tsx the generated SlaViolatorSession (status stays a number — C# int, rendered via statusLabels); sessionExportUtils aliases SessionExportEvent = EnrollmentEvent and derives RuleResultCsvData via Pick, CSV output unchanged; the three structural ReplayInputEvent clones (InstallEvent, ScriptInputEvent, DownloadEvent) are aliases now. docs: drift-guards.md replaces the shrink-only-OkAsync section with the typed-wire-DTO contract, index.md/log.md updated. tsc 0, vitest 957, eslint 0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): SlaMetricsServiceTests anchor sessions inside the current ISO week — the 'now - 1h' fixture crossed into LAST week during the first hour of every Monday (UTC), emptying CurrentWeek and failing the three snapshot asserts (CI on Mon 2026-08-31 00:20Z; pre-existing Monday-morning time bomb, unrelated to the DTO migration); the anchor is now clamped to the week's Monday 00:00, proven green inside the same failure window Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
01311898a8 |
fix(ci): no third-party action holds release/deploy credentials by mutable tag — build-agent creates releases with first-party gh release create (job holds contents/id-token/attestations:write), combine-rules pushes generated outputs with plain git (contents:write on main), deploy-web builds the static export in CI (setup-node 22, npm ci, next build — same path ci.yml tests) and uploads it with the exactly pinned SWA CLI 2.0.10 instead of Azure/static-web-apps-deploy (Oryx in-container build, deploy token as action input), attests portal-<sha>.tar.gz and drops the unusable per-PR preview environments; azure/login and Azure/functions-action are SHA-pinned with version comments for Dependabot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0190f443d |
fix(ci): build-agent workflow_dispatch build_number can no longer inject PowerShell — the input and the agent blob SAS reach the version step only via env (BUILD_NUMBER_OVERRIDE, AGENT_SAS_TOKEN) instead of ${{ }} in the script body, and the override is validated against ^\d+$ before the [int] cast and the counter-blob CAS; counter semantics unchanged
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0515523e49 |
fix(security): guardrails.json values are emitted into generated TS through a complete encoder (JSON.stringify + U+2028/29) and rejected up-front if they contain control or line-terminator characters; new guardrails schema validated in combine-rules before any bot commit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c39092bf72 |
ci(mcp): image tag unique per run so docs-only redeploys roll a new revision
A redeploy at an unchanged code sha (the documented way to refresh the baked-in search_docs corpus) built an image with the identical tag; az containerapp update saw the same image reference and created no revision, so the verify step timed out against the old build (run 33215583943). Tag = sha-run_number; build commit/version stay separate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
91f9d656ae |
ci(scripts): publish customer scripts on push, decoupled from the agent release
The scripts behind download.autopilotmonitor.com/agent/ rotated only on a publish_as_stable run of build-agent.yml, so a bootstrap fix sat unpublished until the next agent cutover -- while the WDP bootstrap MSI downloads the published copy on every enrolling device. Publish-BootstrapScripts.ps1 becomes the single owner of the chain: dev render with anchor check, $ScriptVersion bump guard, uploads (eu + fail-soft legacy mirror, no-cache), the two version oracles, and verification through the alias. publish-scripts.yml calls it on every push to main touching the sources; the publish_as_stable branch of build-agent.yml calls the same code, so a release still lands a consistent set. Blobs are published in a deterministic CRLF/UTF-8-without-BOM form. Git normalises line endings on checkout, so raw working-copy bytes would differ per environment and make every byte comparison -- guard and verify alike -- depend on where the workflow ran. The form reproduces the currently published bytes exactly. The Pester (PS 5.1) and ASCII gates move into the reusable bootstrap-script-gates.yml and now block the upload; pester.yml keeps the pull-request view and drops its push trigger. AdminConfiguration .LatestBootstrapV2ScriptVersion gets exactly one writer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
587adc5ddf |
ci(msi): build from project dir - wix resolves File/@Source against cwd
WIX0103 on the first run: Invoke-BootstrapDownload.ps1 not found because wix build resolves File/@Source relative to the working directory, not the wxs location. Matches the local build documented in Package.wxs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dcf1b1c227 |
ci: Bootstrap-MSI build workflow - blob publish behind the download alias
workflow_dispatch-only, mirroring the agent release contract: validate the runner script (PS parse + ASCII-only, it runs as SYSTEM on every enrolling device), wix 5 build, upload versioned + stable blob names to the agent container (no-cache, no legacy mirror - the MSI channel never existed there), HEAD-verify the public alias serves the fresh bytes, Sigstore provenance attestation. MSI version stays source-controlled in Package.wxs - bumped to 1.0.2 (runner: encoding fix + bootstrap-msi.log rename). Customer download: https://download.autopilotmonitor.com/agent/AutopilotMonitor-Bootstrap.msi Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c4280e823c |
fragility audit P5.x + P7: drift guards across SignalR names, RowKeys, config sections, scoped routing
P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.
P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).
P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.
P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.
P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).
P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.
P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).
Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8277d70915 |
fix: close live catalog/type drift across web, backend and agent (audit P1)
- adminConfig.ts: drop retired V1 latestAgent*/customSettings ghost fields,
add the 8 backend fields the interface was missing
- ops alerts: register the Sla category (BreachNotification/ConsecutiveFailures/
EvaluationCompleted) + OrphanEventsCleaned so operators can route them
- analyze rules: expose app_install_duration in the builder SOURCES;
fix stale XML docs (event_data_array, count_per_group_gte/in/not_in)
- UploadIntervalSeconds: collapse 5 diverged defaults (10 vs 30) onto
Constants.DefaultUploadIntervalSeconds = 10 (the served value)
- combine-rules workflow: commit ALL three combine.js outputs and include
rules/guardrails.json in the path filters
- api.ts apps sessions builders: move trailing optional strings into an
options object (adjacent optional strings swallowed a forgotten tenantId)
- SummaryDialog: canonical phase display names ("Apps (Device)")
- mcp-server.bicep: fix pre-EU-cutover hostname in deploy docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
83da522dd7 |
ci: freeze the web lint warning cap at zero (batch 5)
The 99-warning legacy backlog from the React 19 / eslint-plugin-react-hooks v6 upgrade is fully burned down — CI now fails on any new warning. The compiler rules stay demoted to "warn" so local dev output remains readable; the eslint config comments now point to the established fix patterns instead of the retired ratchet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
041626494e |
ci: bump actions/cache v4 -> v6 (Node 24 runtime)
Node 20 actions are deprecated on GitHub runners; actions/cache@v4 was the last remaining action targeting it (MCP server tests job warning). v6 runs on node24 like the rest of the workflow actions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
283c2ed214 |
web,ci: make Grant-AutopilotMonitorAddOn.ps1 actually downloadable
The docs page and the admin UI both told customers to run Grant-AutopilotMonitorAddOn.ps1, but the script was never published anywhere -- it only exists in this private repo. Publish it on the download alias next to the agent artifacts and reference it: - build-agent.yml: upload the script on publish_as_stable runs - admin UI: copied grant command now starts with an irm -OutFile download line; prose links the script directly - utils/config.ts: new AGENT_DOWNLOAD_URL registry constant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
024c90d68f |
ci: cache the MiniLM embedding model so MCP tests stop 429ing on huggingface
The vector-search tests tokenize with the real Xenova/all-MiniLM-L6-v2 model. transformers.js caches it under node_modules by default, which npm ci recreates on every run — so each CI run re-downloaded the tokenizer/weights from huggingface.co, and a rate-limited response (429) failed the whole MCP job (run 31252869254; unrelated to the commit under test). Persist the model via actions/cache and point HF_CACHE_DIR (already honored by vector-search-provider.ts) at it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c421b81751 |
web: fix all 16 exhaustive-deps lint warnings, ratchet cap 115 -> 99
Wrap ad-hoc fetch closures in useCallback and complete effect deps (SectionPlatformUsage, usage-metrics, diagnosis, TenantConfigContext, SignalRContext, dashboard, fleet-health, useProgressSignalR). Convert sla's initialLoad to a ref so the fresh-once flag can't retrigger the fetch effect. Document the three intentional live-ref bumps in unmount cleanups (cancellation counters) with justified disables. Also fixes a real stale-closure bug: TenantConfigContext's save callback serialized webhookNotifyOnHardwareRejection without depending on it, so toggling only that setting saved the old value. The 3 react-compiler preserve-manual-memoization infos remain (compiler bailouts, not defects) - cap ratcheted to the exact current count of 99. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
acc95471a6 |
bootstrap(dev): Pester-testable structure + guard/relax test suite + CI
Refactor Install-AutopilotMonitor-dev.ps1 (v2.3-dev) into functions (Get-CloudPcModel, Get-RealUserProfilePaths, Get-RelaxDecision, Get-BootstrapDecision with reason codes) with an Invoke-Bootstrap main and a dot-source entry guard, so the Pester suite can load the logic without executing the bootstrap. Behaviour when run directly is unchanged. Fixes a refactoring leak found by smoke-run: Write-Log used Write-Output, which turned function return values into arrays and swallowed console output; now Write-Host, guarded by a dedicated logging-purity test. Add Install-AutopilotMonitor-dev.Tests.ps1 (25 tests: Cloud PC detection, relax scenarios incl. the W365 field case, guards 1-5, profile collection with WMI failure path) and .github/workflows/pester.yml running the suite under Windows PowerShell 5.1 (IME runtime) plus an ASCII guard over all bootstrap scripts. Tests pass 25/25 under PS 5.1 and pwsh 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
960b3d8ed9 |
chore(deps): bump github/codeql-action from 4 to 4.37.3 (#119)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4...v4.37.3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f795fbcb4e |
feat(auth): dual app-reg parallel operation - legacy gktatooine app alongside new C4A8 app
True parallel operation, no forced re-consent: existing tenants keep signing in via the legacy app (1a400946), new tenants land on the C4A8 app (886ab5e2). Model "default legacy, funnel primary, learn in the background": - Web: am_auth_app flag -> NEXT_PUBLIC_ENTRA_DEFAULT_APP -> primary; get-started CTA hands over with ?authapp=primary; auth/me homedApp updates the flag so an operator tenant flip takes effect on each user's NEXT login; AADSTS90094/65001 one-shot cross-app retry, AADSTS65004 -> failed screen with previous-app link; am_login_attempt pins redirect returns to the app that started them. - Backend: TenantConfiguration.HomedAppClientId (null = legacy, GA-only flip via PUT config) + LastAuthClientId/Since observability; EntraAppRegistry resolves the acting app per tenant (no-op until the legacy config pair is set); Graph client-credentials, admin-consent URLs and the surfaced client id follow the homed app (app-scoped cache keys); JwtBearer [Authorize] pipeline now shares the middleware audience builders (was single-audience - would 401 legacy tokens after the swap); middleware trusts EntraId:LegacyClientId implicitly. - well-known association file now lists both app ids (publisher domain); deploy-web passes NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID / _DEFAULT_APP; mcp-server.bicep default client id -> C4A8 app. Backend xUnit 3730/3730, vitest 625/625 (13 new), tsc/ESLint clean, next build ok. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e62bac9fea |
fix(web): hooks-refs batch 2.5 — ProtectedRoute wasAuthenticated ref to state; refs category at zero
wasAuthenticated was written AND read during render (the only such case). Historical per-mount state belongs in useState, adjusted during render via the guarded official pattern — behavior-identical: the value is only consulted in !isAuthenticated renders, which can only follow a render that already set it. Auth-gate control flow (auto re-login, loading early-return, post-login return URL) untouched. react-hooks/refs: 26 -> 0. Lint ratchet 115. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1e94a7aa91 |
fix(web): hooks-refs batch 2.4 — useDashboardSessions mirror refs via useLatest
Nine render-phase mirror writes replaced: scope/filter/pagination mirrors sync post-commit via useLatest; consuming callbacks list the stable refs in their deps. Drops two write-only refs (joinGlobalAdminsRef, loadingMoreRef) that nothing read. Imperative refs (fetchGen/fetchLock/loadAllToken/submittedFilter) untouched. Lint ratchet 119. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c330e5c91a |
fix(web): hooks-refs batch 2.3 — useDashboardStats mirror refs via useLatest
Seven scope mirror refs sync post-commit via useLatest instead of render-phase writes; fetchStats lists the stable refs it reads in its deps. Lint ratchet 128. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6015548172 |
fix(web): hooks-refs batch 2.2 — useFleetHealth mirror refs via useLatest
Four scope mirror refs (routeGlobal, selectedTenantId, tenantId, days) now sync post-commit via useLatest instead of render-phase writes. fetchData lists the stable refs it reads in its deps. Lint ratchet 135. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3f661d097d |
fix(web): hooks-refs batch 2.1 — useLatest helper; useAuthenticatedFetch + presence off render-phase ref writes
Render-phase ref mirror writes tear under concurrent rendering (react-hooks/refs). New hooks/useLatest.ts syncs the mirror in a passive effect instead; refs from custom hooks are not linter-known-stable, so consumers list them in dep arrays (identity is stable — no re-creation). Lint ratchet 141 -> 139. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3392a23da6 |
feat(bootstrap): promote dev script v2.2 to stable — download.autopilotmonitor.com endpoint + OOBE-relax (#118)
The stable Install-AutopilotMonitor.ps1 is now the previously field-tested -dev variant (v2.2): agent download via the Front Door alias https://download.autopilotmonitor.com/agent/ instead of the legacy blob host, mandatory SHA-256 manifest verification (Content-MD5 fallback removed), and the OOBE-relax exemption for the Windows Backup for Organizations restore case. Test-ShouldBootstrapAgent.ps1 mirrors the same guard logic; the separate -dev.ps1 files are gone. CI dev-render anchor literals in build-agent.yml updated to the alias URLs (stable + dev channel) so the literal-substitution check keeps passing; local build.ps1 anchors updated too (gitignored). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5312216f6b |
fix(web): hooks-warnings batch 1 — static-components, purity, immutability to zero (#117)
First burn-down batch of the React-Compiler hooks warnings (163 -> 141, CI ratchet lowered to match). static-components (14, real remount bugs): SortIcon (geographic- performance), SortIndicator (VulnerabilityReportSection) and SearchIcon/ ResultsDropdown (GlobalSearch) were defined inside render — a new component type every render, so React unmounted and rebuilt their subtrees each time (GlobalSearch tore down its dropdown on every keystroke). All hoisted to module level with explicit props. purity (6): Date.now() during render replaced with mount-time clocks (useState lazy init) for the date-input caps (Maintenance sections) and trial-expiry checks (TenantManagement, SectionTenantConfigReport); FeedbackSection refreshes its clock per fetch so "Xm ago" labels track the data; DownloadProgress falls back to NaN instead of wall-clock for unparseable event timestamps (rate calc already guards via Number.isFinite). immutability (5): fetch functions were declared below the effects that call them — moved above (diagnosis, fleet-health, DistressReports). This unblocked deeper compiler analysis, surfacing 3 latent set-state-in-effect findings in those files (93 -> 96); they belong to the set-state category and stay for its own batch. Verified: eslint 0 errors / 141 warnings (gate exits 0), tsc clean, vitest 611/611, static export builds. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
72392f6f4c |
chore(web): lint findings to zero errors + CI lint gate (#116)
* feat(web): typed routes — broken internal links are now compile errors typedRoutes: true (stable in Next 16; `next typegen` already runs in CI before tsc since #114). A typo'd <Link>/router.push target now fails the build instead of 404ing at runtime — the failure class behind the recent RSC/routing incidents. - lib/routes.ts: registry helpers return `Route`; withQuery validates the base path literal. New: deviceBlockUrl (OpsEventsSection previously hand-built /admin/security/device-block against the registry rule), route() to compile-check dynamic-section literals (nav configs, section index redirects), trustedRoute() as the single named seam for runtime-data hrefs (backend-emitted notification deep links, persisted post-login return URL). - Nav types (NavItem, ExpandableSubItem, PageSectionItem) carry Route hrefs — every sidebar/navbar entry is compile-checked. - ClientRedirect is generic so dynamic-section literals infer. Verified: tsc clean; negative test (typo'd static + dynamic routes) fails compilation as intended; 611/611 vitest; static export builds with an unchanged route list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): lint findings to zero errors + CI lint gate Package deal — fixes only land together with the gate, otherwise they drift right back. CI web job now runs `eslint . --max-warnings 163`: errors fail outright, the warning cap is a ratchet (lower it when fixing warnings, never raise it). Fixed to zero (mechanical categories, behavior unchanged): - no-unused-vars (49): dead imports/locals removed; where a prop is part of a live call-site contract, only the destructured binding dropped - no-explicit-any (29): real structural types for event payloads (EnrollmentEvent.data et al -> Record<string, unknown> + per-event interfaces), SignalR passthroughs typed via the library's own signatures; five consumer files got typing-only follow-through - no-unescaped-entities (15), prefer-const, jsx-no-comment-textnodes - no-require-imports (5): scripts/**/*.js override — Node CJS scripts, require() is correct there - one stale eslint-disable directive removed Deliberately NOT fixed inline: the React-Compiler hooks rules new in eslint-plugin-react-hooks v6 (set-state-in-effect 93, refs 26, static-components 14, purity 6, immutability 5, preserve-manual-memoization 3) are demoted to warn in eslint.config.mjs — each is a per-site behavioral refactor in auth/SignalR-adjacent code, to be burned down incrementally under the ratchet. Verified: eslint 0 errors / 163 warnings (gate command exits 0), tsc clean, vitest 611/611, static export builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf45911b69 |
Web upgrade: Next 16 + React 19 + MSAL v4 (#114)
* chore(web): bump to next 16, react 19, msal-browser 4, msal-react 3 react 19.2 aligns the declared dependency with the React the app router has been running all along (Next-vendored 19 canary). msal-react 3.0.29 is the React-19-compatible bridge that only needs msal-browser 4 — deliberately NOT msal v5, whose COOP redirect bridge requires new bridge pages per origin plus app-registration changes (separate project). The react-leaflet peer overrides are gone: react 19 satisfies react-leaflet 5 natively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): react 19 / msal v4 / next 16 code adaptations - msalConfig: allowNativeBroker -> allowPlatformBroker (v4 rename); storeAuthStateInCookie stays (deprecated in v4, removed only in v5) - zero-arg useRef() removed in @types/react 19: GlobalSearch (2x), dashboard autoLoadTimerRef - React.MutableRefObject -> React.RefObject in 5 session/progress hooks (all refs are mutable in 19 types) - layout: data-scroll-behavior="smooth" opt-in so route changes keep jumping instead of animating (Next 16 stopped suppressing CSS smooth-scroll during navigation) - next.config: analyzer comment - Turbopack default build skips the webpack-only analyzer, use --webpack for ANALYZE runs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): untrack Next-managed next-env.d.ts, generate types in CI Next 16 rewrites next-env.d.ts on every dev/build run and the routes import inside flips between .next/types and .next/dev/types (vercel/ next.js#86001) - official guidance is to gitignore it. CI now runs `next typegen` before tsc so the type stubs exist on a fresh checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): migrate lint script to eslint CLI (next lint removed in 16) Minimal flat config from eslint-config-next@16 (core-web-vitals + typescript) with build-output ignores. Lint stays out of CI as before; the 264 pre-existing findings are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cc195990ff |
fix: SWA deploy — drop invalid is_static_export input, dedupe trailing-slash routes
The pinned Azure/static-web-apps-deploy@v1 has no is_static_export input (warning only — Oryx detected the export and validated out/ regardless). The real failure was config validation: with "trailingSlash": "auto" SWA normalizes /x and /x/ to the same route key, so the defensive trailing-slash duplicates were rejected as duplicate rules. One entry per route covers both forms under auto. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18b28260a7 |
feat: portal static export — eliminate the SWA SSR runtime (cold-start root fix)
output:'export' + trailingSlash: the SWA now serves plain static files; the
managed SSR runtime — the portal's last cold-start source ("Backend call
failure" when a cold start exceeded SWA's 45s limit) — no longer exists.
- Detail routes are query-string based (/sessions?id=, /sessions/inspector?id=,
/diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=,
/admin/customs-archive/detail?tenantId=&rowKey=). Single-definition builders
in lib/routes.ts; all 21 link sites migrated, duplicate sessionHref helpers
consolidated. Suspense boundaries added everywhere useSearchParams drives
page identity (hard export-build errors otherwise).
- 9 [section] families prerender via server shells (generateStaticParams from
the *NavSections registries, dynamicParams=false, client bodies extracted to
SectionClient) — 37 pages; server redirect() index pages became
ClientRedirect shells. robots/sitemap/og-image routes force-static.
- Legacy PATH-shaped deep links are preserved FOREVER (sent Teams/Slack/
webhook notifications cannot be edited): SWA wildcard rewrites serve the
family page and the globally mounted LegacyPathRedirect rewrites to the
canonical query URL, preserving query + #event hashes (mapping pinned by
lib/__tests__/legacyPathRedirect.test.ts).
- middleware.ts deleted → client-side HostRoutingGuard (prefix list moved to
lib/hostRouting.ts). Legacy /go/[code] route + bootstrapValidation deleted —
BootstrapSessions table verified free of active pre-cutover sessions.
- staticwebapp.config.json now carries production routing: 19 docs/marketing
redirects (order-sensitive), legacy rewrites (exact before wildcard), 404
override, all security headers incl. literalized prod CSP (no unsafe-eval),
Content-Type routes for the extension-less og images. Guarded by the new
utils/__tests__/swaConfig.guard.test.ts. Maintenance mode is now a
first-routes-entry insert, never a whole-file swap (README rewritten).
- Backend notification deep links go through Constants.PortalSessionUrl
(query shape, escaped, pinned); 3 producers migrated.
- deploy-web.yml uploads out/ with is_static_export.
Export build: 79 static pages, no go/ output. Web: tsc clean, vitest 53
files green (618 incl. new guards; 2 deleted legacy suites). Backend:
solution build clean, 3622/3622 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
fd2b5c2da2 |
fix(ci): drop invalid SWA input, gate deploy on token via env guard
Azure/static-web-apps-deploy@v1 has no `skip_deploy_on_missing_secrets` input (confirmed against its action.yml), so passing it did nothing but emit an "Unexpected input(s)" warning — the intended "skip green on Dependabot PRs" behaviour was never actually in effect. Replace it with the documented mechanism: surface the deploy token as a job-level env var and gate the deploy/close step with `if: env.DEPLOY_TOKEN != ''` (secrets cannot be referenced directly in an `if:`). On Dependabot PRs the token is absent, so the step skips and the job stays green; real deploys on main and internal PRs still run. Applied to both jobs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a5d12ba1d0 |
chore(ci): unify Node toolchain on 22 across CI + engines pins
Node 20 reached EOL (2026-04-30); Node 22 is the current maintenance LTS and already the runtime the MCP container ships (node:22-slim). Straighten the version skew so per-component CI major == deploy runtime == engines floor: - ci.yml: web, mcp and guardrails setup-node 20 -> 22 - combine-rules.yml: setup-node 20 -> 22 - mcp package.json: add engines.node ">=22" (matches Dockerfile) - web package.json: add engines.node ">=22" (first explicit pin; SWA/Oryx no longer picks a Node version implicitly) engine-strict stays off (no .npmrc), so the pins are advisory and cannot hard-break local installs on other Node versions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |