Files
AutopilotMonitor/docs/backend/session-annotations.md
T
Oliver Kieselbach ade6c4948f annotations: tenant switcher on the overview page + GA-lane guard on the global list
/annotations now carries the standard cross-tenant scope (GA/Reader: "All
tenants" aggregate + per-tenant drill-down via GET global/session-annotations,
delegated: managed subset), with a tenant column and tenant-qualified session
deep links. The global list endpoint now derives excludeGlobalAdminLane from
HasGlobalScope so delegated-rescue callers never receive the platform-internal
globaladmin lane (endpoint-test-pinned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:27:31 +02:00

8.2 KiB
Raw Blame History

type, title, description, resource, tags, timestamp
type title description resource tags timestamp
Concept Session Annotations — Role-Separated Human Verdicts The SessionAnnotations table (PK=tenantId, RK={sessionId}_{lane}) storing per-session human verdicts + notes in three role lanes (operator / tenantadmin / globaladmin), the per-lane write matrix with own-tenant binding, the platform-internal GA lane, the fired-rule-id snapshot that makes annotations rule-quality labels, and the portal/MCP surfaces. /src/Backend/AutopilotMonitor.Functions/Functions/Annotations/UpsertSessionAnnotationFunction.cs
backend
web
mcp
annotations
authorization
rule-quality
2026-08-11T00:00:00+02:00

Schema

Table. SessionAnnotations: PartitionKey = tenantId, RowKey = {sessionId}_{lane} with lane ∈ operator | tenantadmin | globaladmin (a closed set — AnnotationLanes.All). One row per session + lane, edited in place; per-session reads use the RK range ge '{sid}_' / lt '{sid}_~'. Serialization lives only in TableSessionAnnotationRepository.StoreAnnotation/MapAnnotation (round-trip-pinned; corrupt RuleIdsJson degrades to an empty list on read).

Row. Verdict (nullable: root_cause_confirmed | analysis_wrong | different_problem | inconclusive), Note (nullable, ≤ 4096 chars), AuthorUpn/AuthorDisplayName (last editor), CreatedByUpn/CreatedAtUtc (first writer, immutable across edits), UpdatedAtUtc, and RuleIdsJson — a snapshot of the rule ids that had fired for the session at write time (from IRuleRepository.GetRuleResultsAsync, fail-soft to empty). The snapshot is what turns an annotation into a rule-quality label without a join: filtering annotations by ruleId + verdict yields confirmed vs false-positive counts per rule even after results are re-analyzed or deleted.

Verdict + note are independently optional; a PUT with both null deletes the lane row (there is no DELETE route). Author identity is always stamped server-side from the JWT (TenantHelper.GetUserDisplayName + RequestContext.UserPrincipalName) — a body-supplied author never wins (same anti-spoof contract as the gather-rule PUT-upserts).

Authorization

Routes (all in EndpointAccessPolicyCatalog):

Route Policy Notes
GET sessions/{sessionId}/annotations MemberRead + QueryParam handler filters the GA lane
PUT sessions/{sessionId}/annotations/{lane} TenantAdminOrOperator + QueryParam re-gated per lane in-function
GET global/session-annotations GlobalReadOrAdmin + QueryParam evaluation stream; delegated ("MSP") readers are rescue-admitted on a managed ?tenantId= only

Per-lane write matrix (UpsertSessionAnnotationFunction.IsLaneWritableByCaller, an internal static pure function like QueueSessionActionFunction.IsTypeAllowedForCaller): operator → tenant Operator or tenant Admin; tenantadmin → tenant Admin; globaladmin → Global Admin only. Own-tenant binding: the session's tenant is resolved before the gate (GA cross-tenant fallback via FindSessionTenantIdAsync), and tenant-role inputs are zeroed when the resolved tenant differs from the caller's JWT tenant — so a GA who is also an admin of their home tenant can never write another tenant's operator/tenantadmin lanes. Cross-tenant, a GA writes exactly the globaladmin lane (the platform labeling flow).

GA lane is platform-internal. GetSessionAnnotationsFunction.FilterLanesForCaller drops the globaladmin lane for every caller without HasGlobalScope (tenant members and delegated admins). Both list endpoints enforce the same rule server-side via the repository's excludeGlobalAdminLane OData clause — on the global route this matters exactly for the delegated-rescue path, which reaches it without global scope. Tenant lanes are readable by all members of that tenant.

Audit. Non-GA writes/clears log LogAuditEntryAsync(..., "SessionAnnotation", "{sessionId}/{lane}", ...); GA writes are skipped (platform-internal labeling must not surface in the tenant-visible audit log — same convention as GA session-report submissions).

Lifecycle

  • Per-session cascade delete: three exact-RK steps (one per lane) in DeletionManifestBuilder (orders 17–19; inventory 20/21, tombstone 22). ComputePreflightCounts sums repeated-table steps so the sessionAnnotations count covers all lanes.
  • Tenant offboarding: SessionAnnotations is in the PK=tenantId wipe list — deliberately wiped, unlike Feedback (annotations are customer session data, not product feedback).
  • Backup: member of Constants.CriticalBackupTables.All — hand-labeled data is the most expensive data on the platform to lose.
  • Being in Constants.TableNames.All auto-creates the table at startup and exposes it to the raw list_tables/query_table tools.

Surfaces

  • Portal: SessionAnnotationsCard on the session detail page (#section-annotations, after Analysis; registered in BOTH the section div and the sessionSections sidebar registry). The card mirrors the matrix via the pure module sessionAnnotationLogic.ts (vitest-pinned) and uses the explicit-Save pattern; the backend re-gates every save. Collapsed by default with a count + verdict-pill summary header.
  • Portal overview page /annotations (Monitoring sidebar group — deliberately NO new "Insights" group; Fleet Health & co. are insights too and a second analytics group would dilute both): tenant-scoped list backed by GET sessions/annotations/list (ListTenantSessionAnnotationsFunction, MemberRead + QueryParam). The path sits under the /api/sessions client-cert exclusion prefix (rename-over-exclusion convention; the Azure exclusion list is portal-only config) and uses THREE segments because a two-segment literal would be eaten by the sessions/{sessionId} route template. The tenant is always the middleware-validated TargetTenantId, and for callers without global scope the repository adds a server-side Lane ne 'globaladmin' OData clause — hidden rows never consume page budget, and an explicit lane=globaladmin filter self-contradicts to an empty page. Rows deep-link to sessions?id=…#section-annotations. The page carries the standard tenant switcher (useAggregatedAdminScope + TenantScopeSelector): a GA/Reader defaults to the "All tenants" aggregate (the portal twin of MCP's list_session_annotations) with per-tenant drill-down, a delegated ("MSP") admin gets the managed subset; cross-tenant scopes route to GET global/session-annotations, a tenant column appears, and deep links carry the row's tenantId.
  • MCP read: get_session_summary carries an annotations key (4th parallel leg, fail-soft null); list_session_annotations (ga-gated) is the evaluation stream with tenantId / lane / verdict / ruleId / dateFrom / dateTo filters and nextLink pagination. The ruleId filter is applied server-side but client-of-Azure (substring on the JSON column), so the backend back-fills short pages by looping the Azure continuation (bounded rounds) — filtered-out rows never consume page budget.
  • MCP write: annotate_session (strictGa-gated, MUTATING) always PUTs the globaladmin lane — no lane argument by design. This is the frictionless labeling path after a session-debug.
  • Session reports: SessionReportService.SubmitReportAsync snapshots ALL lanes into annotations.json inside the report ZIP (annotationCount in report-metadata.json; fail-soft). Safe because report blobs are readable only through the GA-only session-reports routes, and valuable because the ZIP outlives the session's retention/delete.

Citations

  • src/Shared/AutopilotMonitor.Shared/Models/SessionAnnotation.cs — model, lanes, verdicts.
  • src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionAnnotationRepository.cs — Store/Map + query back-fill.
  • src/Backend/AutopilotMonitor.Functions/Functions/Annotations/ — the three endpoints.
  • src/Backend/AutopilotMonitor.Functions/Pagination/SessionAnnotationsPagination.cs — filter-bound continuation tokens.
  • src/Web/autopilot-monitor-web/app/sessions/components/sessionAnnotationLogic.ts — portal matrix mirror.
  • src/McpServer/autopilot-monitor-mcp/src/tools/admin.ts — list_session_annotations, annotate_session.