Permissioning
Almanac mirrors source-side permissions per connector at ingest time. The mapping shape is the same regardless of source.
Principal kinds
user, group, workspace, public. workspace and public short-circuit the ACL function and always match a caller in the workspace.
Identity mappings
At OAuth-completion time, the connector populates identity_mappings (almanac_user_id, source_kind, source_principal_id, source_principal_kind). Drive bootstraps user primary email + group memberships via the Admin SDK (cached 24h). Notion reads the workspace user ID. Slack reads user ID + channel memberships, refreshed by the member_joined_channel / member_left_channelEvents API webhooks where wired.
Retrieval
AclAwareRetriever estimates selectivity, over-fetches by target_K / max(selectivity, 0.01) clamped to [50, 500], sets hnsw.iterative_scan = strict_order and hnsw.max_scan_tuples = k_request * 4, then runs a SQL query that applies the ACL filter via user_can_read(principal_set, document_id) inside the iterative scan — not post-filtered on top.
If the post-filter result count falls below min_results, a second pass runs with a hard cap of 2000 tuples before declaring acl_thin. Acl-thin queries surface as confidence: low with reason acl_thin in the audit log and count toward the gap report.
ACL drift
Source-side permission changes are picked up by the 10-minute ingest cron. ACL rows naming a principal that no Almanac user is mapped to are stored (so the drift is auditable) but never match — fail-closed. Slack channel membership changes are also picked up live via the Events API where wired.