Semantic Context — Role Mapping (P5-006)¶
Scheme identifiers¶
The legacy context endpoint, vocabulary graph export, and extraction run accept either a concept scheme key or its UUID string. Both forms resolve to the same scheme on SQLite and Postgres; an unknown key or UUID returns 404. This corrects a UUID lookup that previously failed during SQL binding. No client migration is needed, and existing key-based requests keep their behavior. Governed context routes continue to apply the role and workspace rules below.
The four governed context-pack endpoints under
/api/v1/semantic/context/* apply policy filtering based on the
caller's role and workspace. The role + workspace are resolved into
a RequesterContext by
pycharter.api.dependencies.requester_context.get_requester_context.
Update (2026-05-20). Role + workspace come exclusively from the
verified JWT (Authorization: Bearer ...). The legacy X-Role /
X-Workspace header fallback was retired. See
role-permissions-matrix.md for the
JWT claim shape, the full route × minimum-role table, and the
operator + client migration outcome. When PYCHARTER_AUTH_DISABLED=true
every caller is a superuser for dev / smoke convenience.
Roles¶
The role enum (pycharter.api.dependencies.requester_context.Role)
widens visibility as the role escalates:
| Role | Visible policy states | Workspace scope |
|---|---|---|
viewer |
approved |
Workspace-scoped |
reviewer |
approved + under_review |
Workspace-scoped |
admin |
approved + under_review + deprecated + rejected |
Workspace-scoped |
superuser |
same as admin |
Cross-workspace (bypass) |
How visibility is enforced¶
There are two filter layers (defense in depth):
- Query layer — the loader helpers in
pycharter.semantic.context._helpersapplyWHEREclauses onMappingProjectionModel.stateandConceptModel.scheme_idbefore returning rows. The pure filter primitives live inpycharter.semantic.context._policy. - Response layer —
_filter_pack()inpycharter.api.routes.v1.semantic_contextre-applies the same allowlists on the already-builtContextPackbody. Any row that slipped through the builder is stripped before serialization.
Workspace scoping¶
Workspace identity in this codebase is the concept scheme — there
is no separate workspace table today. Non-superuser callers must
provide a concept-scheme UUID via X-Workspace; the filter layer
checks ConceptModel.scheme_id == X-Workspace (and
ConceptRelationshipModel.scheme_id for edges).
MappingProjectionModel has no native workspace column, so contract
bindings are workspace-scoped via a JOIN against ConceptModel on
concept_key. Bindings whose concept_key is null cannot be
attributed to a workspace and are therefore excluded for non-superuser
callers.
Binding-state allowlist¶
| Role | Allowed MappingProjectionModel.state values |
|---|---|
viewer |
approved |
reviewer |
approved, mapped, suggested |
admin |
all states (no filter) |
superuser |
all states (no filter) |
Note: a viewer looking at a contract that bindings to a
deprecated=True concept still has those bindings hidden — the
response-layer filter drops deprecated concept refs, and the
viewer cannot reach a deprecated concept's id directly (the concept
route 404s for them).
Defaults¶
get_requester_context defaults to viewer with no workspace when
the headers are absent. A non-superuser without a workspace sees
nothing: that is the safe default for local development and
keeps a misconfigured client from accidentally reading content
across schemes.
Enforcement on writes (P6-001)¶
Visibility filtering on reads (this document) is paired with the
P6-001 require_role(min_role) dependency for write endpoints
on the suggestion / binding / proposal / ingest surfaces. See
role-permissions-matrix.md for the
full route table.