ADR-0013: Live Microsoft Graph connector (read-only) validated against CDX
Status: Accepted · 2026-07-14Decision
ImplementGraphM365Connector 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,.defaultscope), cached with expiry. Secret resolved from env by reference (FMD_GRAPH_CLIENT_ SECRET_REF), never logged or committed. - Enumeration:
discoverScopeslists sites (/sites?search=*) → document-library drives.inventoryPage/changesuse thedriveItemdelta feed; the opaque@odata.nextLink/@odata.deltaLinkURLs are the cursors, preserved verbatim;410 Gonemaps to a safe resync. - Observation:
getPermissionsmaps Graph permissions to the canonical deduplicated permission-set model (link scopes → broad categories; completeness markednot_observableon 403).getContentstreams bounded bytes withcontent-lengthguarding.getLabelreturnsunknownhonestly (reading assigned sensitivity labels needs the protectedextractSensitivityLabelsAPI, not granted). - Throttling:
Retry-Afterhonored 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.
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 changedINFER_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.Selectedmigration is the least-privilege follow-up.