Skip to main content

ADR-0013: Live Microsoft Graph connector (read-only) validated against CDX

Status: Accepted · 2026-07-14

Decision

Implement GraphM365Connector behind the existing Connector contract and wire it through ConnectorRegistry for FMD_CONNECTOR_MODE=graph. Scope of this build: read-only.
  • Auth: app-only client-credentials (/oauth2/v2.0/token, .default scope), cached with expiry. Secret resolved from env by reference (FMD_GRAPH_CLIENT_ SECRET_REF), never logged or committed.
  • Enumeration: discoverScopes lists sites (/sites?search=*) → document-library drives. inventoryPage/changes use the driveItem delta feed; the opaque @odata.nextLink/@odata.deltaLink URLs are the cursors, preserved verbatim; 410 Gone maps to a safe resync.
  • Observation: getPermissions maps Graph permissions to the canonical deduplicated permission-set model (link scopes → broad categories; completeness marked not_observable on 403). getContent streams bounded bytes with content-length guarding. getLabel returns unknown honestly (reading assigned sensitivity labels needs the protected extractSensitivityLabels API, not granted).
  • Throttling: Retry-After honored with bounded retries.
  • Writes: label preview/execute return unsupported — the read-only app has no write permission, matching the documented posture (ADR-0010, ADR-0011).
  • Least privilege chosen for the run: Sites.Read.All + Files.Read.All. Sites.Selected (per-site grant) remains the production recommendation (permissions-manifest.md); Read.All was used to get a working read-only pilot.
A dependency-free, bounded DOCX text extractor (intelligence/docx.ts) was added so real Word documents classify on content: parse the ZIP central directory, inflate word/document.xml (node:zlib raw inflate), strip WordprocessingML to text. Other binary formats remain honest unsupported states.

Context

The MVP prioritized a deep fixture-validated slice (ADR-0012) with the live path gated. A follow-up session had authenticated CDX access, so the live adapter was built and validated read-only against the Contoso CDX tenant (run record: cdx-test-runbook.md §0). Live data is the only way to surface issues that deterministic fixtures cannot.

Consequences

  • The mock connector stays the default and the CI-tested path; live is opt-in by config and constructed only when credentials resolve (else an explicit BlockedError, never a silent mock fallback).
  • Live validation surfaced and fixed a real over-classification defect (generic evidence must not create type-specific candidates — see inference.ts hasDiscriminating). This changed INFER_IMPL_VERSION; retained features re-score without source I/O.
  • Still to validate live: change-sequence deltas, Purview label read/write, Teams/Exchange, and write actions. Sites.Selected migration is the least-privilege follow-up.