3322 Commits

Author SHA1 Message Date
Oliver Kieselbach e046571350 bump pointer: internal (push deep links confirmed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:45:47 +02:00
Oliver Kieselbach 6b5c55998f bump pointer: internal (push deep links live)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:38:49 +02:00
Oliver Kieselbach 6df57459c2 bump pointer: internal (push deep links, D-360)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:19:34 +02:00
Oliver Kieselbach caeb7a3e58 feat: every alert links to its subject; ops alerts open their own event (D-360)
A push carries only category, event and severity for an ops alert, so the
portal link is the way to the details, and ops alerts had none: the event
id was minted inside the save and never reached the dispatch.

Backend: SaveOpsEventAsync sets OpsEventEntry.Id (OpsEventId,
{Category}_{RowKey}); WriteAsync hands it to the dispatch, and BuildAlert
adds "Open in the portal" -> /admin/ops/?eventId=<id> on the plain and the
payload path, so every ops channel (push, Telegram, Teams, Slack, Discord,
e-mail, JSON) gets it. GET global/ops-events?id= is a point read (400 for a
malformed id, an empty list for a deleted one). hardware_rejected links to
the hardware whitelist (one path constant, also for the bell); the session
watch link names the session's tenant; TenantSignup's portalPath keeps its
tenant query. Only a session link makes a push session-bound: tag and Topic
no longer key on any "id=" in the link.

Web: /admin/ops?eventId= opens that event's detail from the point read
(matched by id), and the detail links to the tenant and to the event's
related page (same-origin paths only) besides the session, which now
carries the tenant. CopyButton and the cursor pager footer are shared
components instead of four and three copies; the duplicate baseline shrinks
by five entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:19:22 +02:00
Oliver Kieselbach bf37fe46c1 bump pointer: internal (session block visibility RELEASED)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:43:24 +02:00
Oliver Kieselbach 3678995a9b bump pointer: internal (session block visibility, D-359)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:27:56 +02:00
Oliver Kieselbach 7a917eeaee feat: a session the platform blocked says so (D-359)
When the event limit or the platform operator blocks a session's device, the
session now carries a block record: Block* fields on the session and its index
row, a server-authored device_blocked timeline event, an interim analyze run for
that event and, on the auto path, an audit entry. The block list itself stays
platform-only.

- Portal: a card above the failure snapshot says the monitoring was paused
  (orange) or ended (red), when, until when and, for the event limit, the
  session's event count against the limit. The Blocked pill reads the record for
  every role; Global Admin mode still adds the live block list.
- A Kill is device-wide until it expires, so its record carries the end and the
  card says new enrollments on the device are not monitored until then.
- Status: the stalled and timeout reasons name the block instead of a silent
  agent; the classification is unchanged.
- Rules: ANALYZE-ENRL-006 explains the pause right after it (on_event
  device_blocked); ANALYZE-ENRL-002 no longer fires for a blocked session.
- MCP: the coverage block of get_session_summary names the block as a gap.
- The agent-facing block message no longer says "by an administrator".
- BlockCause == event_limit replaces ExcessiveEventsAutoActioned as the
  auto-action's idempotency marker.
- Privacy page: the event limit next to rate limiting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:27:55 +02:00
dependabot[bot] d5a83374d0 chore(deps): bump the web group (#186)
Bumps the web group in /src/Web/autopilot-monitor-web with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/applicationinsights-web](https://github.com/microsoft/ApplicationInsights-JS) | `3.4.4` | `3.4.5` |
| [next](https://github.com/vercel/next.js) | `16.3.8` | `16.4.0` |
| [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/HEAD/packages/next-bundle-analyzer) | `16.3.8` | `16.4.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.3.8` | `16.4.0` |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.39.0` | `6.40.0` |


Updates `@microsoft/applicationinsights-web` from 3.4.4 to 3.4.5
- [Release notes](https://github.com/microsoft/ApplicationInsights-JS/releases)
- [Changelog](https://github.com/microsoft/ApplicationInsights-JS/blob/main/RELEASES.md)
- [Commits](https://github.com/microsoft/ApplicationInsights-JS/compare/v3.4.4...v3.4.5)

Updates `next` from 16.3.8 to 16.4.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.8...v16.4.0)

Updates `@next/bundle-analyzer` from 16.3.8 to 16.4.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.4.0/packages/next-bundle-analyzer)

Updates `eslint-config-next` from 16.3.8 to 16.4.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.4.0/packages/eslint-config-next)

Updates `knip` from 6.39.0 to 6.40.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.40.0/packages/knip)

---
updated-dependencies:
- dependency-name: "@microsoft/applicationinsights-web"
  dependency-version: 3.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: next
  dependency-version: 16.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web
- dependency-name: eslint-config-next
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web
- dependency-name: knip
  dependency-version: 6.40.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 15:12:49 +02:00
dependabot[bot] 596a9ffc2b chore(deps): bump the mcp group (#185)
Bumps the mcp group in /src/McpServer/autopilot-monitor-mcp with 4 updates: [@modelcontextprotocol/server](https://github.com/modelcontextprotocol/typescript-sdk), [@modelcontextprotocol/client](https://github.com/modelcontextprotocol/typescript-sdk), [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@modelcontextprotocol/server` from 2.3.0 to 2.3.1
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/@modelcontextprotocol/server@2.3.0...@modelcontextprotocol/server@2.3.1)

Updates `@modelcontextprotocol/client` from 2.3.0 to 2.3.1
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/@modelcontextprotocol/client@2.3.0...@modelcontextprotocol/client@2.3.1)

Updates `knip` from 6.39.0 to 6.40.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.40.0/packages/knip)

Updates `typescript-eslint` from 8.71.0 to 8.71.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/server"
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mcp
- dependency-name: "@modelcontextprotocol/client"
  dependency-version: 2.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: mcp
- dependency-name: knip
  dependency-version: 6.40.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: typescript-eslint
  dependency-version: 8.71.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: mcp
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 15:12:31 +02:00
Oliver Kieselbach 0e1428922f bump pointer: internal (diagnosis plan RELEASED)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:36:43 +02:00
Oliver Kieselbach 10314cd8f2 bump pointer: internal (diagnosis plan: live, visual check open)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:35:28 +02:00
Oliver Kieselbach 9ee882ab68 bump pointer: internal (diagnosis plan: customer docs approved and committed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:55:13 +02:00
Oliver Kieselbach eefa365b6c bump pointer: internal (diagnosis plan: docs drafted)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:30:35 +02:00
Oliver Kieselbach 21ca9eb068 bump pointer: internal (only an InProgress session stalls)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:28:53 +02:00
Oliver Kieselbach af86e8f7cc Sessions: only an In Progress session stalls
A stall report never overwrote a guard: the ingest branch for session_stalled
runs after the completion and failure chain, and the transition gate let
Stalled through over every status but Pending. An offline batch carrying
session_stalled next to enrollment_complete left a finished enrollment
Stalled until the timeout sweep reclassified it, and a sweep that lost the
race against a completion could do the same.

IsTerminalTransitionAllowed now allows Stalled only over InProgress, on the
normal and the ETag-retry path alike; the separate Pending guard is folded
into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:28:52 +02:00
Oliver Kieselbach 8c5fbd5739 bump pointer: internal (diagnosis headline concept doc, backlog side findings, plan phase 6 internal), .claude/commands (rule-builder presentation)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:19:30 +02:00
Oliver Kieselbach 55b667ad2c bump pointer: internal (D-358, diagnosis plan phase 5), .claude/commands (GetAppFleetContext marker, open_fleet_sessions)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:14:56 +02:00
Oliver Kieselbach 325632e780 Diagnosis: the culprit app in the tenant's other enrollments, and the rule line beside each finding (D-358)
GET apps/fleet-context (MemberRead, QueryParam) counts one app over the
tenant's last 7 days: installed, skipped, failed, incomplete, in progress,
collision rows left out, the three most frequent failure codes. It reads
the apps dashboard's partition query, leaves out the asking session and,
with the culprit's app id, another app of the same name; no session point
reads. AppFleetContextService caches the app's rows per tenant, channel,
name and minute for 60 s (single flight).

The classification is the analytics view's: MetricsMath.IsInstalled
replaces six inline copies, AppsAnalyticsHelper.IsSameApp and
TopFailureCodes are shared by analytics, sessions and the fleet context.

The diagnosis page shows the result as a tile in "Around the failure"
only when other enrollments exist (the read fails soft), with "Show
sessions" on the app page where that page shows the viewed tenant. An
expanded finding shows "Fired in N enrollments in the last 14 days"
through useRuleFireStats and RecentFiresLine, both extracted from the
session page, whose cross-tenant check now ignores case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:14:55 +02:00
Oliver Kieselbach 9ad3cb2df1 bump pointer: internal (diagnosis plan phase 4, D-356/D-357 amended, drift guards), .claude/commands (diagnosis telemetry rows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:30:53 +02:00
Oliver Kieselbach 127d5c1173 Diagnosis: the page answers what happened, for every status with a verdict (D-356, D-357)
The diagnosis page is rebuilt on the server headline instead of rule
confidence: a verdict card per status and cause (culprit tiles, ESP
phases, what an unfinished enrollment still waits for), the
circumstances around it, the steps of the finding that explains it,
where the time went, all open findings grouped by role, team notes and
the deduplicated errors and warnings. It reuses the session page's hook
set, refetches the headline on live activity and opens for Failed,
Stalled, AwaitingUser, Incomplete and Succeeded with open findings, also
from read-only and cross-tenant views.

Rule text: lib/ruleText.ts prunes a finding's explanation and steps like
the notifications do (shared cases in tests/fixtures/rule-text-display,
also run by RuleTemplateInterpolator.InterpolateForNotification), then
parses the markdown and fills values into text nodes only. The session
page's analysis section, the rule card and the dry run render through it;
components/findings/EvidenceChips replaces the two evidence-link copies
and covers correlation pairs.

Vocabularies: DiagnosisPrerequisites names the engine's completion
prerequisites (manifest diagnosis.prerequisites, parity fact against
DecisionEngine.CompletionPrerequisites); the page's texts for every
diagnosis vocabulary are exhaustive over the manifest types.

Fixed on the way: useSessionSignalR held the first render's callbacks,
so ruleResultsReady was lost on an already connected hub; a deep link to
an event dropped its hash before the event loaded; the global analyze
rule update answered 500 for an invalid presentation (now 400); the
rule editor kept the presentation block on create and in JSON mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:30:29 +02:00
Oliver Kieselbach 6df88abdb3 bump pointer: internal (D-357, diagnosis plan phase 3), .claude/commands (GetSessionDiagnosis marker)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:48:07 +02:00
Oliver Kieselbach 5c3d158d85 Sessions: GET sessions/{id}/diagnosis returns the session's verdict (D-357)
A typed headline built on demand from the session row and every event:
outcome = status; cause from the verdict path (derived for older rows,
flagged as a caveat), and for an explicit failure from the ESP evidence
(failed step, error code, culprit apps, AppX candidate, stuck policy
providers). Plus the ESP phase rollup, the ESP profile, what an unfinished
session still waited for, and observation caveats. Never from rule
confidence: Stalled, AwaitingUser and reconciled sessions get no terminal
rule run.

- App facts come from the session's own app events (the app table has no
  intent or targeting); only required apps count, a later install clears
  an app, a zero error code is none, and a 0x87d1041c ESP failure ranks
  the NotDetected app first (real IME payload shape).
- The event read is strict: a storage error is a 500, never a verdict
  built from no evidence. 400 for a non-GUID id, 404 for an unknown session.
- Helpers/EventDataReader replaces three private Data-string copies
  (classifier, failure snapshot, time attribution) and the list parsing in
  EspBlockingSets; EspBlockingSets.FromLatest replaces two backward walks.
- Six vocabularies in the shared manifest section `diagnosis`; parity facts
  pin the full and the minimal shape; route in the policy catalog.
- MCP get_session_summary carries it as `headline` right after coverage,
  null when the call fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:48:05 +02:00
Oliver Kieselbach 3bedc65ed5 bump pointer: internal (D-356 amended, diagnosis plan phase 2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:36:33 +02:00
Oliver Kieselbach e5cb3c4f5e Rules: every analyze rule carries a presentation block (D-356)
All 54 shipped analyze rules get role, summary, subject and key facts as
{{signal.key}} templates, each with a minor version bump so the self-seed
picks them up. Values were checked against stored findings, the rows that
receive the block at read time.

- ENRL-001 repeats the outcome only: the 0x87D1041C paragraph and the
  placeholder note are gone, labels read "ESP step that failed" / "ESP
  error code", remediation is "Find the cause". This changes its alert text.
- ENRL-002 says the sweep "closed" the session (it can end Incomplete).
- ID-002 and ID-004 gain evidence-only conditions (unexpected account
  names; IME token failure count and codes); firing and confidence are
  unchanged.
- Resolution: facts sharing a label are alternatives, an exact repeat of
  label and value is left out (new shared fixture case, web module and
  its generated MCP copy).
- Ratchet: every built-in presentation passes the save validator, uses
  qualified tokens only, and each token names a condition of the rule and
  a key its source records (RuleTemplateInterpolator.TokenRegex internal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:36:21 +02:00
Oliver Kieselbach d7b094852d bump pointer: internal (D-356, diagnosis plan phase 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:49:00 +02:00
Oliver Kieselbach 516cdc71a7 Rules: optional presentation block, {{signal.key}} tokens, one resolution for web and MCP (D-356)
Analyze rules can describe how a finding is shown: a role (outcome, cause, context,
advisory), a one-line summary, the subject it is about and up to six key facts. Like the
explanation, the block is stored raw on the result and resolved where it is rendered.

- Templates: a qualified token {{signal.key}} reads one key of one condition's evidence,
  so counts, durations and correlation fields are reachable. RuleEvidenceKeys (Shared,
  exported to the manifest) lists the keys each condition source records; tests pin it
  against the evaluators.
- Backend: PresentationJson on AnalyzeRules and RuleResults, RuleVersion on results,
  ValidatePresentation on create/update, and results stored without a presentation get
  the current rule's at read time.
- Web + MCP: lib/rulePresentation.ts resolves the block (unrecorded facts left out, summary
  falls back to the explanation). The MCP now runs generated copies of the web modules
  instead of a hand-kept mirror (npm run generate:mcp-logic, freshness tests in both
  suites); the duplicate baseline shrinks by one.
- MCP output: get_session and get_session_summary return the resolved block (issues gain
  ruleId, role, summary, subject, facts); test_analyze_rule previews it; validate_rule
  checks qualified tokens against the key catalog and lints the subject.
- Shared case files: rule-template-interpolation gains the qualified-token cases,
  rule-presentation is new.

No rule carries a presentation yet; renderers behave as before until rules gain one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:48:46 +02:00
Oliver Kieselbach 4a822767d4 bump pointer: internal (catalog check opens files exclusively, B-ute)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:33:02 +02:00
Oliver Kieselbach f82eabd6eb bump pointer: internal (payload catalog released, B-5q2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:21 +02:00
Oliver Kieselbach 1892da30e4 Agent: the bootstrap provenance also reads the log names from before 2026-08-18
The first device on 2.0.1486 reported no bootstrap log at all. Installers up to v2.3 write
bootstrap_agent.log, and the MSI's own downloader (before the loader of 2026-09-08)
msi-bootstrap.log; the reader knew only the names from the rename of 2026-08-18 on, so the
old directly assigned installers that agent_started is meant to count read as "no bootstrap".

- The installer log is the more recently written of bootstrap-script.log and
  bootstrap_agent.log; the MSI channel also covers msi-bootstrap.log, which has no version.
- Tests: the old installer name, both installer logs (the newer one counts), the old MSI
  downloader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:04:06 +02:00
Oliver Kieselbach fd44518e14 Bootstrap: installer v2.6 and the OOBE go-script verify the payload catalog (D-354, phase B)
- Install-AutopilotMonitor.ps1 v2.6: Get-AgentPayloadRejections (Test-FileCatalog
  -Detailed; signature Valid, signer -like the pin, files exactly as catalogued) and
  Install-VerifiedAgentPayload (extract into Agent-Install, verify there, then move into
  the agent folder; a rejection deletes the staging folder, so the next run starts clean).
  Replaces the name-filtered Get-UnverifiedAgentBinaries; the dead "already installed"
  branch behind guard 5 is gone.
- OOBE go-script: embeds the installer's marked block verbatim (EmbeddedResource) and
  verifies before --install; pin and catalog name come from the Shared constants; the
  golden is updated, new tests pin the embedding and the order check-before-install.
- build-agent.yml: the release verify step dot-sources the installer and runs its own
  function on the final ZIP.
- Pester: every decision branch, a real unsigned catalog, the staging flow (64/64).
- Privacy page: the catalog claim; LAST_UPDATED.

Merge only after a catalog ZIP is live on stable (plan step V0): pushing this to main
publishes the installer to every loader on its next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:43:25 +02:00
Oliver Kieselbach d95640c597 Agent: protected DACL on the agent data directory (D-355)
%ProgramData%\AutopilotMonitor inherited ProgramData's "Users may create files and
folders" entry, so a standard user could add files to the folder the SYSTEM agent and its
boot task start from. The agent outlives the device phase of OOBE (user phase, desktop,
reboots until the enrollment ends), and the payload catalog is checked at install time
only.

- AgentDirectoryAcl.EnsureProtected: protected DACL, SYSTEM and Administrators full
  control, Users read and execute, inherited by every subfolder; idempotent, never blocks
  the agent.
- Install mode applies it after the agent directories exist; every runtime start
  re-checks it, which also reaches agents that arrived by self-update.
- Tests: protected shape, apply once then leave alone, existing subfolders take it over,
  a widened or unprotected DACL is not taken as protected, a missing folder is not created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v2.0.1486
2026-10-13 19:26:44 +02:00
Oliver Kieselbach b5e9f45a46 bump pointer: internal (payload catalog lab VM result)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:57:58 +02:00
Oliver Kieselbach b4dabcce94 bump pointer: internal (payload catalog dev rehearsal)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:32:25 +02:00
Oliver Kieselbach 6bbdcc0313 bump pointer: internal (agent payload catalog plan status)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:09:51 +02:00
Oliver Kieselbach 283c094986 Agent payload: signed catalog, vendor check, dev channel in CI (D-354, phase A)
- build-agent.yml: input channel (release | dev; dev writes the lab blobs only and
  refuses stable/release). Every third-party PE needs its vendor's embedded Authenticode
  signature (signtool verify /pa, read-only). AutopilotMonitor.Agent.cat, a signed
  SHA-256 catalog over every payload file, is created after Authenticode and before the
  ZIP; the device check runs under Windows PowerShell 5.1 on the final ZIP.
- Agent: SelfUpdater step 5b verifies the staged payload against the catalog before any
  file is swapped (PayloadCatalogVerifier, Test-FileCatalog in-process; a rejection means
  no update, skip reason catalog_rejected). agent_started reports which bootstrap
  installed the agent, read from the bootstrap logs (BootstrapProvenanceReader).
- Payload: System.Management.Automation.dll (never loaded, the GAC copy binds first) and
  AutopilotMonitor.Shared.xml no longer ship; PDBs stay.
- Shared: CodeSigningPublisherPattern, AgentPayloadCatalogFileName;
  CodeSigningParityTests pins them against installer, loader and workflow.
- Submodules: infra (build.ps1 local only), internal (D-354, plan, docs), commands.

Installer v2.6 and the OOBE script follow once a catalog ZIP is on stable: a merged
installer change is live on the next loader run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:48:29 +02:00
Oliver Kieselbach 47728fbcd4 bump pointer: internal (external identities released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:44:18 +02:00
Oliver Kieselbach d5f885c4b4 bump pointer: internal (external identities never claim a tenant, D-353)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:21:38 +02:00
Oliver Kieselbach dd4975b73e Auth: an external identity never claims a tenant (D-353)
A caller whose token shows it is not a member of the token's tenant (acct = 1, a #EXT# user name,
or an idp naming another tenant, a personal Microsoft account or an external sign-in provider) no
longer seeds DomainName/OnboardedBy or queues the activation, is never the first-user Tenant Admin,
and cannot set the activation-page address in the roleless signup window. Positive evidence only:
a token without these signals stays a member's, and tid/idp are read under their inbound-mapped
names as well. Activation promotes OnboardedBy only; the UpdatedBy fallback is gone and a stored
#EXT# UPN is refused. Explicit grants (TenantAdmins row, app role) still apply.

The once-per-interval log gate of the client-app binding becomes Helpers/LogThrottle and also
throttles the new warning, which the polling activation page would otherwise repeat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:19:56 +02:00
Oliver Kieselbach 18310b05aa bump pointer: internal (MCP input bounds released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:29:24 +02:00
Oliver Kieselbach 40894ed5e7 bump pointer: internal (MCP input bounds, D-352)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:10:02 +02:00
Oliver Kieselbach 6894e71668 MCP: bound caller-sized search input; scoring cost no longer grows with the keyword count
search_events, search_knowledge and search_docs take a query of at most 1,000
characters; search_events takes at most 20 keywords of 100 characters and
scores at most 32 distinct keywords, caller keywords first, reporting the rest
as keywordsDropped with a note. The error-code fallback takes at most 10 codes,
search_docs.section at most 100 characters.

The event scorer compiles the query once (compileConcepts): each event field is
split into words once and its words are looked up by the key of their first
three characters, so scoring an event costs the same whatever the number of
keywords. The matching rule is unchanged; a seeded differential test pins it
against the per-keyword rule. scoreEvents yields to the event loop every 256
events and stops once the caller has gone.

Every paginated tool's continuation goes through one ContinuationSchema
(at most 4,096 characters), and buildQuery appends instead of set: both
URLSearchParams.set paths were quadratic in caller-sized input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:07:28 +02:00
Oliver Kieselbach a28fdc874f bump pointer: internal (watch at the seal + app badge released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:45:24 +02:00
Oliver Kieselbach 7480a2e218 Push receiver: an app badge counts unread alerts; an open history reads them
Every stored history entry starts unread. After a new entry the worker sets the
app badge to the unread count through applyBadge, fire and forget: inside
Safari's declarative push event the setAppBadge promise never settles, so it is
never awaited and never handed to waitUntil (pushSw.test.ts pins it). The
history page marks everything read on every visible load, clears the badge,
closes the notifications whose entries the history holds and shows "New" on
what arrived since the last visit. Unpair clears the badge; the wipe push
leaves it to the next open. The payload repeats mutable inside notification,
where Safari 18.4-26.1 reads it, and carries no app_badge.

Bumps internal (plan, D-350, D-351, concept doc).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:22:56 +02:00
Oliver Kieselbach 96fc88462d Push: the session watch fires at the WhiteGlove seal
"Notify me when done" now also fires on the first genuine pre-provisioning
seal ("Pre-provisioning finished") on the gate of the Pre-Provisioning
Completed alert, and the watch is consumed there; watching the user phase takes
a new click. A watch placed on a sealed session lives 30 days instead of 48
hours. EventIngestProcessor.WatchOutcomeFor is the one gate, the watch text
comes from PushSessionWatchService.WatchMessage. The portal button re-reads the
watch on every status change and names what it waits for. The Success hint
says it covers a finished pre-provisioning; the receiver labels the
preprovisioning_* alerts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:22:44 +02:00
Oliver Kieselbach 6b86549fd0 bump pointers: internal, .claude/commands (client-cert interception released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:30:15 +02:00
Oliver Kieselbach e8e350bf14 bump pointer: internal (B-k87 done)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v2.0.1484
2026-10-12 17:55:27 +02:00
Oliver Kieselbach 4e519fa4b7 Backend: the correlation-id gate anchors with \z
.NET's $ also matches before a final newline, so "7c9e6679\n" passed the allow-list that guards
the inbound X-Correlation-ID against CRLF and log forgery and was echoed. Such a value does not
arrive over HTTP, but the gate now holds what its comment promises; a new test case pins it
(red with $).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:55:26 +02:00
Oliver Kieselbach 52f6a177f9 bump pointers: internal, .claude/commands (client-cert interception, D-349)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:45:02 +02:00
Oliver Kieselbach caafaeafd4 Tests: the push recording handler survives the parallel fan-out
PushDeliveryService sends with Parallel.ForEachAsync, and RecordingHandler appended to plain
lists without a lock: two concurrent sends could lose a request (Delivered 2, recorded 1), so
Tenant_alert_also_reaches_platform_devices_whose_owners_home_tenant_it_is failed now and then
(once in six full runs while it was traced). A request and its body are now recorded together
under the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:43:52 +02:00
Oliver Kieselbach bfb6c12384 Agent: an intercepted connection is no longer an auth failure
A security client that inspects TLS strips the device certificate on the way to the backend;
the platform then answers 403 "Client Certificate Required" itself, or a proxy answers with
its own page. The agent counted these like backend rejections: five in a row shut it down,
the scheduled task restarted it at every boot, and only the 48-hour emergency break removed it.

- Origin of every 401/403: only a response that echoes the request's X-Correlation-ID is the
  backend's verdict and counts toward the auth shutdown; the stopped-app page still counts;
  anything else is foreign (client_certificate_required or foreign_response) and never counts.
- Telemetry: a foreign answer keeps the batch and pauses uploads with a backoff from 30 s up to
  15 min, without a timer; collectors and the decision engine keep running, the terminal phase
  gets one upload attempt, and the agent still leaves the device when the enrollment ends.
- Distress type ClientCertificateNotReceived (9), once per process, with operation, status,
  page title and the issuer of the server certificate; both handlers observe it through a
  validation callback that returns exactly sslPolicyErrors == None.
- Registration: an intercepted start retries on at most InterceptedRegistrationRetryBoots
  further boots (config channel, default 1, agent clamp 0..5, ConfigVersion 41), then ends
  monitoring like the emergency break. The break, this give-up and the config kill signal now
  share Program.EndMonitoringAtStart.
- Evidence: telemetry_upload_blocked carries reason, operation, statusCode, pageTitle,
  serverCertIssuer and firstBlockedAtUtc; agent_started carries priorRegistration* after failed
  starts (a local network fingerprint; only whether it changed leaves the device);
  agent_metrics_snapshot counts 2xx responses without the echo.
- Backend: distress type 9, the buffer default, InterceptedRuns on the registration-failure
  summary; tests pin the correlation echo on 401/403 and the middleware's place in the pipeline.
- Web: distress labels for TPM Incompatible and Cert Not Received; config report row.
- MCP: session coverage names intercepted uploads and the failed registrations before the start.

D-349

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:42:59 +02:00