diff --git a/docs/access-control-policy-gateway.md b/docs/access-control-policy-gateway.md
index 7cfe8b4..7bcf43f 100644
--- a/docs/access-control-policy-gateway.md
+++ b/docs/access-control-policy-gateway.md
@@ -139,6 +139,22 @@ to future backend storage.
## Adapter Boundaries
+Enterprise IAM integration is covered in
+`docs/enterprise-access-control-integration.md`. In that architecture,
+Markitect is the policy enforcement point for Markdown knowledge results, while
+NetKingdom/key-cape-compatible OIDC supplies identity claims and external
+policy engines can act as policy decision points.
+
+Identity and directory integration use these provider-neutral boundaries:
+
+- `IdentityClaimsAdapter` validates OIDC/JWT/SAML material and returns
+ normalized `EnterpriseIdentity`.
+- `DirectoryGroupResolver` resolves group overage or stale directory claims
+ through SCIM/Graph/LDAP/Keycloak-style adapters.
+- `EnterprisePolicyMapper` maps canonical enterprise roles, scopes, and groups
+ to `PolicySubject` labels, trust zones, and allowed actions.
+- `DecisionLogStore` persists durable audit records for policy decisions.
+
Relationship policies use `RelationshipPolicyAdapter`:
```text
diff --git a/docs/enterprise-access-control-integration.md b/docs/enterprise-access-control-integration.md
new file mode 100644
index 0000000..1c06d16
--- /dev/null
+++ b/docs/enterprise-access-control-integration.md
@@ -0,0 +1,335 @@
+# Enterprise Access-Control Integration
+
+Date: 2026-05-04
+
+## Purpose
+
+This note explains how Markitect's access-control gateway should integrate with
+enterprise IAM while respecting the local NetKingdom/key-cape direction.
+
+The answer is yes: trust zones and access groups can map to canonical directory
+group membership, but Markitect should not trust raw AD group names directly.
+The enterprise shape should be:
+
+```text
+OIDC/SAML/SCIM identity and directory plane
+ -> canonical subject claims
+ -> policy map and/or policy decision point
+ -> Markitect policy gateway
+ -> filtered document/query/context results
+```
+
+Markitect is the policy enforcement point for Markdown knowledge results. It
+should validate and normalize identity data through adapters, then call a policy
+decision point through the existing policy interfaces.
+
+## Canonical Enterprise Shape
+
+The canonical enterprise pattern is the PEP/PDP/PIP/PAP split:
+
+| Component | Markitect fit |
+| --- | --- |
+| PEP: policy enforcement point | `AccessPolicyGateway` at query, search, context, workflow, export, and assisted-prompt boundaries. |
+| PDP: policy decision point | Local label policy for development; OpenFGA/SpiceDB, OPA/Rego, Cedar, or enterprise authorization service through adapters. |
+| PIP: policy information point | NetKingdom/key-cape OIDC claims, directory group resolution, object labels, backend metadata, workflow context, and environment attributes. |
+| PAP: policy administration point | Enterprise IAM/policy administration, plus versioned Markitect mapping files for labels, actions, trust zones, and emergency rules. |
+
+This keeps Markitect small and auditable. It enforces decisions where Markdown
+knowledge leaves a boundary, but it does not become the enterprise directory,
+SSO provider, or policy administration system.
+
+## Local Infrastructure Fit
+
+The local canon already points the right way:
+
+- NetKingdom SSO is the reference identity provider.
+- Keycloak is the reference OIDC provider, with privacyIDEA-backed MFA.
+- key-cape is the lightweight OIDC/profile-enforcement path for local,
+ sandbox, and bootstrap scenarios.
+- Services should trust OIDC tokens, validate issuer/audience/signature/expiry,
+ and authorize from explicit roles/scopes.
+
+Markitect should therefore consume the NetKingdom IAM profile rather than
+create its own identity standard.
+
+## Enterprise Reference Architecture
+
+```mermaid
+flowchart LR
+ AD["AD / LDAP / HRIS"] --> SCIM["SCIM / Directory Sync
(PIP)"]
+ SCIM --> IdP["NetKingdom SSO
Keycloak / key-cape-compatible OIDC"]
+ IdP --> Token["OIDC Access Token
roles, scopes, groups, org, assurance"]
+ Token --> Mapper["Identity Claim Mapper"]
+ Mapper --> Subject["PolicySubject"]
+ Source["Markdown objects
labels, paths, trust zones"] --> Object["PolicyObject"]
+ Subject --> Gateway["Markitect AccessPolicyGateway
(PEP)"]
+ Object --> Gateway
+ Gateway --> PDP["Policy Decision Point
local / OpenFGA / OPA / Cedar"]
+ PDP --> Decision["PolicyDecision + audit metadata"]
+ Decision --> Boundary["Filtered query/search/context results"]
+```
+
+### Identity Plane
+
+OIDC should be the default authentication path for users and services. SAML may
+remain relevant for enterprise federation, but Markitect should normalize both
+into the same subject shape.
+
+The canonical accepted claims should follow the NetKingdom IAM profile:
+
+- `iss`
+- `sub`
+- `aud`
+- `exp`
+- `iat`
+- `scope` or `scp`
+- `preferred_username`
+- `roles` or `realm_access.roles`
+- recommended `groups`, `azp`, `email`, `name`
+
+For humans, Authorization Code + PKCE is the right login flow. For services,
+client credentials or workload identity should produce short-lived service
+tokens. OAuth token exchange can support hub-to-hub delegation where a service
+acts on behalf of a user.
+
+### Directory And Group Plane
+
+Enterprise directories remain authoritative for group membership. Provisioning
+should use a standard such as SCIM where possible; AD/LDAP synchronization into
+Keycloak is also reasonable when NetKingdom owns the identity plane.
+
+Important design rule:
+
+```text
+directory groups -> canonical roles/scopes/trust labels -> Markitect subject
+```
+
+Avoid:
+
+```text
+raw AD group name -> direct Markitect privilege
+```
+
+Reasons:
+
+- AD group names change and often encode organizational accidents.
+- Token group claims can be too large for large organizations.
+- Privilege should be expressed as app roles/scopes or an explicit mapping file.
+- Group membership freshness must be visible in the decision trail.
+
+For Microsoft Entra-style group claims, large tenants may hit token group
+overage. Markitect should therefore support a group-resolution adapter rather
+than assuming all groups are always present in the token.
+
+### Policy Plane
+
+The existing Markitect interfaces already provide the main policy boundary:
+
+```text
+AccessPolicyGateway.authorize(subject, action, object_id, context)
+AccessPolicyGateway.filter_results(subject, action, results, context)
+AccessPolicyGateway.explain_decision(decision_id)
+```
+
+The existing data models also fit enterprise needs:
+
+- `PolicySubject`: identity, roles, groups/labels, allowed actions, attributes.
+- `PolicyObject`: path, labels, trust zone, attributes.
+- `PolicyDecision`: stable decision id, effect, reason, labels, trust zone,
+ metadata.
+- `PolicyFilterResult`: filtered results, decisions, diagnostics, summary.
+
+The adapter seams are also correct:
+
+- `RelationshipPolicyAdapter` for OpenFGA/SpiceDB/Zanzibar-style checks.
+- `RulePolicyAdapter` for OPA/Rego, Cedar, or other ABAC engines.
+
+This follow-up adds the missing interface layer:
+
+```text
+OIDC token or SAML assertion -> verified EnterpriseIdentity -> PolicySubject
+```
+
+Concrete adapters must validate issuer, audience, expiry, signature, and
+assurance metadata before any authorization decision is made. The core package
+now exposes protocol/data boundaries for this without taking a dependency on
+Keycloak, Entra, LDAP, SCIM, OpenFGA, OPA, or Cedar client libraries.
+
+## Canonical Subject Mapping
+
+Recommended normalized shape:
+
+```yaml
+subject:
+ id: "oidc:#"
+ display_name: "Ada Lovelace"
+ principal_type: human | service
+ issuer: "https://sso.example.org/realms/netkingdom"
+ audience: ["markitect-tool"]
+ authorized_party: "markitect-cli"
+ roles: [viewer, operator]
+ scopes: [openid, profile, hub:read]
+ groups:
+ - /markitect/readers
+ - /platform/architecture
+ allowed_labels: [public, internal]
+ trust_zones: [public, internal]
+ assurance:
+ mfa: true
+ acr: "..."
+ amr: ["pwd", "otp"]
+ directory:
+ source: keycloak | entra | ldap | scim
+ refreshed_at: "2026-05-04T10:00:00Z"
+```
+
+The mapping file should translate enterprise groups and app roles to Markitect
+labels and actions:
+
+```yaml
+id: markitect-enterprise-policy-map
+issuer: https://sso.example.org/realms/netkingdom
+audiences: [markitect-tool]
+groups:
+ /markitect/readers:
+ allowed_labels: [public, internal]
+ actions: [query, search, read]
+ /markitect/stewards:
+ allowed_labels: [public, internal, restricted]
+ actions: [query, search, read, package, export]
+roles:
+ admin:
+ allowed_labels: [public, internal, restricted]
+ actions: [query, search, read, package, export, policy-admin]
+scopes:
+ markitect:read:
+ actions: [query, search, read]
+trust_zones:
+ internal:
+ required_groups: [/markitect/readers]
+```
+
+## Data/Object Mapping
+
+Markdown remains the source-friendly object labeling layer:
+
+```yaml
+---
+policy:
+ labels: [internal]
+ trust_zone: internal
+ owner: team:platform-architecture
+---
+```
+
+For enterprise environments, object metadata should eventually include:
+
+- content labels/classification
+- repository/path
+- owning team or project
+- business domain
+- tenancy/org
+- retention or export constraints
+- provenance of label assignment
+
+Path rules remain useful as a safety net, but document labels and backend
+metadata should become authoritative for cached/context-package retrieval.
+
+## Enforcement Points
+
+Markitect should enforce policy at every point where knowledge leaves a
+boundary:
+
+- `mkt cache query`
+- `mkt search`
+- context package creation and activation
+- workflow outputs
+- assisted/LLM prompt assembly
+- export/render steps
+- backend APIs or future MCP resources
+
+The highest-risk point is context assembly for agents. Before WP-0008 turns
+context caches into agent memory, policy must be able to answer:
+
+- who is activating the context package?
+- which token or service account is acting?
+- which labels/trust zones are included?
+- was anything denied or redacted?
+- can this package be reactivated later under the same policy assumptions?
+
+## Interface Confirmation
+
+The WP-0009 infrastructure is a good foundation. It already has:
+
+- policy gateway protocol
+- local label gateway
+- explainable decisions
+- filter-before-return behavior
+- query/search integration
+- relationship and rule adapter boundaries
+
+This follow-up adds these provider-neutral interfaces in
+`markitect_tool.policy.adapters`:
+
+1. `IdentityClaimsAdapter`
+ - validates OIDC/JWT/SAML assertions
+ - normalizes NetKingdom/key-cape-compatible claims into
+ `EnterpriseIdentity`
+
+2. `DirectoryGroupResolver`
+ - resolves group overage or stale claims
+ - supports SCIM/Graph/LDAP/Keycloak admin APIs behind adapters
+ - records freshness and source provenance
+
+3. `EnterprisePolicyMapper`
+ - maps canonical groups, roles, scopes, and tenants to Markitect labels,
+ trust zones, actions, and object constraints
+
+4. `DecisionLogStore`
+ - persists policy decisions for query/context/workflow runs
+ - records token hash, subject id, action, object id, policy version, result,
+ reason, and redaction status
+
+5. workflow/runtime policy injection
+ - `subject_from_token`
+ - `policy_map`
+ - `required_assurance`
+ - `emergency_justification`
+
+Concrete implementations remain future work. This is deliberate: key-cape and
+NetKingdom should own identity issuance and profile compliance, while
+Markitect owns normalization, policy envelopes, diagnostics, and enforcement at
+knowledge boundaries.
+
+## Recommended Direction
+
+Do not make AD/LDAP/Entra groups a Markitect core dependency.
+
+Instead:
+
+1. Accept NetKingdom/key-cape-compatible OIDC tokens.
+2. Normalize claims into `PolicySubject`.
+3. Map enterprise groups/roles/scopes to Markitect labels/trust zones/actions.
+4. Use the existing `AccessPolicyGateway` as the enforcement point.
+5. Let OpenFGA/SpiceDB/OPA/Cedar attach through adapter protocols where a
+ deployment needs stronger central policy.
+6. Persist decisions before using this for production agent memory or exports.
+
+## Sources
+
+- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
+- OAuth 2.0 Token Exchange, RFC 8693: https://www.rfc-editor.org/rfc/rfc8693
+- SCIM Protocol, RFC 7644: https://datatracker.ietf.org/doc/rfc7644
+- NIST SP 800-162 ABAC guide: https://csrc.nist.gov/pubs/sp/800/162/upd2/final
+- NIST glossary, Policy Enforcement Point:
+ https://csrc.nist.gov/glossary/term/policy_enforcement_point
+- NIST glossary, Policy Decision Point:
+ https://csrc.nist.gov/glossary/term/policy_decision_point
+- NIST glossary, Policy Information Point:
+ https://csrc.nist.gov/glossary/term/policy_information_point
+- Keycloak Server Administration Guide: https://www.keycloak.org/docs/latest/server_admin/
+- Microsoft Entra group claims: https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles
+- OpenFGA concepts: https://openfga.dev/docs/concepts
+- Open Policy Agent policy language: https://www.openpolicyagent.org/docs/policy-language
+- Cedar policy language: https://docs.cedarpolicy.com/
+- Local canon: `/home/worsch/the-custodian/canon/standards/iam-profile_v0.1.md`
diff --git a/docs/workplan-planning-map.md b/docs/workplan-planning-map.md
index e58a7d3..39ba3e9 100644
--- a/docs/workplan-planning-map.md
+++ b/docs/workplan-planning-map.md
@@ -38,6 +38,7 @@ and descriptions mirror the operational view.
| `MKTT-WP-0005` | complete | done | `MKTT-WP-0003`, `MKTT-WP-0004` | Runtime context, form state, dynamic rules, workflow integration, and provider-neutral assessment boundary are complete. |
| `MKTT-WP-0011` | complete | done | `MKTT-WP-0003`; task-level triggers: `MKTT-WP-0010-T001`, `MKTT-WP-0010-T005` | Markdown dataflow workflow layer is complete: workflow standard, source collectors, binding model, deterministic steps, assisted boundary, safe outputs, CLI, docs, and examples. |
| `MKTT-WP-0009` | complete | done | `MKTT-WP-0006` | Access-controlled knowledge gateway is complete: local labels, trust zones, path rules, policy-aware cache query/search, decisions, diagnostics, and external adapter boundaries. |
+| `MKTT-WP-0014` | P2 | todo | `MKTT-WP-0009` | Enterprise IAM access-control integration: NetKingdom/key-cape-compatible identity claims, directory group resolution, policy maps, durable decision logs, and external PDP examples. |
| `MKTT-WP-0012` | P3 | todo | `MKTT-WP-0004`, `MKTT-WP-0010`, `MKTT-WP-0011` | Future Quarkdown-inspired document function layer: reusable Markdown-native function calls over processors, references, contracts, workflows, and later assisted steps. |
| `MKTT-WP-0008` | P3 | todo | `MKTT-WP-0006`, `MKTT-WP-0007`, `MKTT-WP-0009` | Agent working-memory cache after backend and policy floor are available. |
@@ -74,6 +75,12 @@ operations deserve author-facing function syntax. It should remain optional and
capability-gated, especially before assisted, external, file, or network
functions are allowed.
+`MKTT-WP-0014` captures enterprise IAM integration for the access-control
+gateway. It should follow `MKTT-WP-0009` and can run before or alongside
+security-sensitive context memory work. It does not block local `MKTT-WP-0008`
+research, but it should gate production deployment of reactivatable agent
+context packages in enterprise environments.
+
## State Hub Mirror
Native State Hub dependency edges should mirror the whole-workstream
@@ -100,6 +107,7 @@ dependencies:
- `MKTT-WP-0012 -> MKTT-WP-0004`
- `MKTT-WP-0012 -> MKTT-WP-0010`
- `MKTT-WP-0012 -> MKTT-WP-0011`
+- `MKTT-WP-0014 -> MKTT-WP-0009`
- `MKTT-WP-0008 -> MKTT-WP-0006`
- `MKTT-WP-0008 -> MKTT-WP-0007`
- `MKTT-WP-0008 -> MKTT-WP-0009`
diff --git a/src/markitect_tool/extension/builtins.py b/src/markitect_tool/extension/builtins.py
index cfde167..6ac537d 100644
--- a/src/markitect_tool/extension/builtins.py
+++ b/src/markitect_tool/extension/builtins.py
@@ -219,8 +219,12 @@ def _local_label_policy_descriptor() -> ExtensionDescriptor:
examples=["examples/policy/local-label-policy.yaml"],
metadata={
"external_adapters": [
+ "IdentityClaimsAdapter",
+ "DirectoryGroupResolver",
+ "EnterprisePolicyMapper",
"RelationshipPolicyAdapter",
"RulePolicyAdapter",
+ "DecisionLogStore",
]
},
)
diff --git a/src/markitect_tool/policy/__init__.py b/src/markitect_tool/policy/__init__.py
index 139f27f..86c08ac 100644
--- a/src/markitect_tool/policy/__init__.py
+++ b/src/markitect_tool/policy/__init__.py
@@ -1,6 +1,14 @@
"""Access policy gateways and adapter protocols."""
from markitect_tool.policy.adapters import (
+ DecisionLogStore,
+ DirectoryGroupResolution,
+ DirectoryGroupResolutionRequest,
+ DirectoryGroupResolver,
+ EnterpriseIdentity,
+ EnterprisePolicyMapRequest,
+ EnterprisePolicyMapper,
+ IdentityClaimsAdapter,
RelationshipPolicyAdapter,
RelationshipPolicyRequest,
RulePolicyAdapter,
@@ -23,6 +31,14 @@ __all__ = [
"LocalLabelPolicy",
"LocalLabelPolicyGateway",
"LocalPathPolicyRule",
+ "DecisionLogStore",
+ "DirectoryGroupResolution",
+ "DirectoryGroupResolutionRequest",
+ "DirectoryGroupResolver",
+ "EnterpriseIdentity",
+ "EnterprisePolicyMapRequest",
+ "EnterprisePolicyMapper",
+ "IdentityClaimsAdapter",
"PolicyDecision",
"PolicyFilterResult",
"PolicyObject",
diff --git a/src/markitect_tool/policy/adapters.py b/src/markitect_tool/policy/adapters.py
index 8f9a3a3..16fe7e7 100644
--- a/src/markitect_tool/policy/adapters.py
+++ b/src/markitect_tool/policy/adapters.py
@@ -1,11 +1,168 @@
-"""Protocol boundaries for external authorization engines."""
+"""Protocol boundaries for external identity and authorization engines."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any, Protocol
-from markitect_tool.policy.models import PolicyDecision
+from markitect_tool.policy.models import PolicyDecision, PolicySubject
+
+
+@dataclass(frozen=True)
+class EnterpriseIdentity:
+ """Verified enterprise identity claims normalized for policy mapping.
+
+ Concrete adapters are responsible for validating tokens or assertions before
+ returning this shape. Core Markitect only consumes the normalized identity.
+ """
+
+ issuer: str
+ subject: str
+ identity_scheme: str = "oidc"
+ principal_type: str = "human"
+ audience: list[str] = field(default_factory=list)
+ authorized_party: str | None = None
+ preferred_username: str | None = None
+ roles: list[str] = field(default_factory=list)
+ scopes: list[str] = field(default_factory=list)
+ groups: list[str] = field(default_factory=list)
+ assurance: dict[str, Any] = field(default_factory=dict)
+ directory: dict[str, Any] = field(default_factory=dict)
+ claims: dict[str, Any] = field(default_factory=dict)
+ provenance: dict[str, Any] = field(default_factory=dict)
+
+ @property
+ def canonical_id(self) -> str:
+ return f"{self.identity_scheme}:{self.issuer}#{self.subject}"
+
+ def to_policy_subject(
+ self,
+ *,
+ allowed_labels: list[str] | None = None,
+ trust_zones: list[str] | None = None,
+ allowed_actions: list[str] | None = None,
+ attributes: dict[str, Any] | None = None,
+ ) -> PolicySubject:
+ """Convert verified claims into the subject shape used by gateways."""
+
+ return PolicySubject(
+ id=self.canonical_id,
+ allowed_labels=list(allowed_labels or []),
+ trust_zones=list(trust_zones or []),
+ roles=list(self.roles),
+ allowed_actions=list(allowed_actions or []),
+ attributes={
+ "issuer": self.issuer,
+ "subject": self.subject,
+ "identity_scheme": self.identity_scheme,
+ "principal_type": self.principal_type,
+ "audience": self.audience,
+ "authorized_party": self.authorized_party,
+ "preferred_username": self.preferred_username,
+ "scopes": self.scopes,
+ "groups": self.groups,
+ "assurance": self.assurance,
+ "directory": self.directory,
+ "identity_provenance": self.provenance,
+ }
+ | dict(attributes or {}),
+ )
+
+ def to_dict(self) -> dict[str, Any]:
+ data = asdict(self)
+ data["canonical_id"] = self.canonical_id
+ return _drop_empty(data)
+
+
+class IdentityClaimsAdapter(Protocol):
+ """Adapter boundary for OIDC/JWT/SAML verification and normalization."""
+
+ def verify(
+ self,
+ token_or_assertion: str | dict[str, Any],
+ *,
+ context: dict[str, Any] | None = None,
+ ) -> EnterpriseIdentity | dict[str, Any]:
+ """Validate identity material and return normalized enterprise claims."""
+
+
+@dataclass(frozen=True)
+class DirectoryGroupResolutionRequest:
+ """Request for resolving group claims that are stale, partial, or overlarge."""
+
+ subject_id: str
+ issuer: str
+ groups: list[str] = field(default_factory=list)
+ claims: dict[str, Any] = field(default_factory=dict)
+ context: dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ return _drop_empty(asdict(self))
+
+
+@dataclass(frozen=True)
+class DirectoryGroupResolution:
+ """Directory group resolution result with freshness and source provenance."""
+
+ groups: list[str] = field(default_factory=list)
+ source: str | None = None
+ refreshed_at: str | None = None
+ overage: bool = False
+ metadata: dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ return _drop_empty(asdict(self))
+
+
+class DirectoryGroupResolver(Protocol):
+ """Adapter boundary for SCIM/LDAP/Graph/Keycloak group resolution."""
+
+ def resolve(
+ self,
+ request: DirectoryGroupResolutionRequest,
+ ) -> DirectoryGroupResolution | dict[str, Any]:
+ """Return groups plus freshness/provenance metadata."""
+
+
+@dataclass(frozen=True)
+class EnterprisePolicyMapRequest:
+ """Request to map enterprise claims onto Markitect policy vocabulary."""
+
+ identity: EnterpriseIdentity | dict[str, Any]
+ policy_map: dict[str, Any] = field(default_factory=dict)
+ groups: list[str] = field(default_factory=list)
+ context: dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ data = asdict(self)
+ if isinstance(self.identity, EnterpriseIdentity):
+ data["identity"] = self.identity.to_dict()
+ return _drop_empty(data)
+
+
+class EnterprisePolicyMapper(Protocol):
+ """Adapter boundary for mapping IAM roles/groups/scopes to policy subjects."""
+
+ def map_subject(
+ self,
+ request: EnterprisePolicyMapRequest,
+ ) -> PolicySubject | dict[str, Any]:
+ """Return a gateway-ready subject with labels, zones, and actions."""
+
+
+class DecisionLogStore(Protocol):
+ """Persistent audit boundary for policy decisions."""
+
+ def record(
+ self,
+ decision: PolicyDecision,
+ *,
+ context: dict[str, Any] | None = None,
+ ) -> str:
+ """Persist a decision and return its durable audit id."""
+
+ def get(self, decision_id: str) -> dict[str, Any] | None:
+ """Return one recorded decision when available."""
@dataclass(frozen=True)
diff --git a/tests/test_builtin_extension_catalog.py b/tests/test_builtin_extension_catalog.py
index 1c38405..568b900 100644
--- a/tests/test_builtin_extension_catalog.py
+++ b/tests/test_builtin_extension_catalog.py
@@ -96,4 +96,5 @@ def test_builtin_policy_descriptor_exposes_cli_and_adapter_boundary():
"policy_filter",
}
assert "mkt policy check" in descriptor.cli["commands"]
+ assert "IdentityClaimsAdapter" in descriptor.metadata["external_adapters"]
assert "RelationshipPolicyAdapter" in descriptor.metadata["external_adapters"]
diff --git a/tests/test_policy_gateway.py b/tests/test_policy_gateway.py
index d16010b..c25ff98 100644
--- a/tests/test_policy_gateway.py
+++ b/tests/test_policy_gateway.py
@@ -4,7 +4,14 @@ from pathlib import Path
from click.testing import CliRunner
from markitect_tool.cli import main
-from markitect_tool.policy import LocalLabelPolicy, LocalLabelPolicyGateway
+from markitect_tool.policy import (
+ DirectoryGroupResolution,
+ DirectoryGroupResolutionRequest,
+ EnterpriseIdentity,
+ EnterprisePolicyMapRequest,
+ LocalLabelPolicy,
+ LocalLabelPolicyGateway,
+)
POLICY_TEXT = """id: example-policy
@@ -187,6 +194,59 @@ def test_mkt_cache_query_filters_indexed_documents_by_policy(tmp_path: Path):
assert data["policy"]["denied"] == 1
+def test_enterprise_identity_maps_to_policy_subject():
+ identity = EnterpriseIdentity(
+ issuer="https://sso.example.test/realms/netkingdom",
+ subject="user-123",
+ preferred_username="ada",
+ roles=["viewer"],
+ scopes=["markitect:read"],
+ groups=["/markitect/readers"],
+ assurance={"mfa": True},
+ directory={"source": "keycloak"},
+ )
+
+ subject = identity.to_policy_subject(
+ allowed_labels=["public", "internal"],
+ trust_zones=["public", "internal"],
+ allowed_actions=["query", "search"],
+ )
+
+ assert subject.id == "oidc:https://sso.example.test/realms/netkingdom#user-123"
+ assert subject.roles == ["viewer"]
+ assert subject.allowed_labels == ["public", "internal"]
+ assert subject.allowed_actions == ["query", "search"]
+ assert subject.attributes["issuer"] == "https://sso.example.test/realms/netkingdom"
+ assert subject.attributes["groups"] == ["/markitect/readers"]
+ assert subject.attributes["assurance"]["mfa"] is True
+
+
+def test_enterprise_policy_adapter_requests_serialize_cleanly():
+ group_request = DirectoryGroupResolutionRequest(
+ subject_id="oidc:https://sso.example.test/realms/netkingdom#user-123",
+ issuer="https://sso.example.test/realms/netkingdom",
+ claims={"hasgroups": True},
+ )
+ group_result = DirectoryGroupResolution(
+ groups=["/markitect/readers"],
+ source="keycloak",
+ refreshed_at="2026-05-04T10:00:00Z",
+ overage=True,
+ )
+ map_request = EnterprisePolicyMapRequest(
+ identity=EnterpriseIdentity(
+ issuer="https://sso.example.test/realms/netkingdom",
+ subject="user-123",
+ ),
+ policy_map={"groups": {"/markitect/readers": {"allowed_labels": ["internal"]}}},
+ groups=group_result.groups,
+ )
+
+ assert group_request.to_dict()["claims"] == {"hasgroups": True}
+ assert group_result.to_dict()["overage"] is True
+ assert map_request.to_dict()["identity"]["canonical_id"].endswith("#user-123")
+
+
def _policy_mapping() -> dict:
return {
"id": "example-policy",
diff --git a/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md b/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md
new file mode 100644
index 0000000..f05a400
--- /dev/null
+++ b/workplans/MKTT-WP-0014-enterprise-iam-access-control-integration.md
@@ -0,0 +1,238 @@
+---
+id: MKTT-WP-0014
+type: workplan
+title: "Enterprise IAM Access-Control Integration"
+domain: markitect
+status: todo
+owner: markitect-tool
+topic_slug: markitect
+planning_priority: P2
+planning_order: 82
+depends_on_workplans:
+ - MKTT-WP-0009
+related_workplans:
+ - MKTT-WP-0006
+ - MKTT-WP-0007
+ - MKTT-WP-0008
+ - MKTT-WP-0011
+ - MKTT-WP-0013
+created: "2026-05-04"
+updated: "2026-05-04"
+state_hub_workstream_id: "86c22ccc-5f5a-4650-8495-76fe6c08e411"
+---
+
+# MKTT-WP-0014: Enterprise IAM Access-Control Integration
+
+## Purpose
+
+Turn the local access-control gateway into an enterprise-ready integration
+surface without making Markitect an identity provider or hard-coding one
+directory vendor.
+
+Markitect should act as the policy enforcement point for Markdown knowledge
+results. NetKingdom/key-cape-compatible SSO should supply identity claims.
+External policy engines and enterprise directories should attach through
+provider-neutral adapters.
+
+## Background
+
+`MKTT-WP-0009` implemented local labels, trust zones, path rules, query/search
+filtering, explainable decisions, and relationship/rule policy adapter
+boundaries. The enterprise follow-up research showed a clear canonical shape:
+
+- OIDC/SAML for authentication and signed identity assertions.
+- SCIM/LDAP/Graph/Keycloak admin APIs for directory and group information.
+- PEP/PDP/PIP/PAP separation for authorization architecture.
+- RBAC/ABAC/ReBAC policy models through mappable policy decision points.
+- NetKingdom IAM profile as the local identity contract, with key-cape as the
+ preferred lightweight/bootstrap path.
+
+Initial provider-neutral interfaces now exist in
+`markitect_tool.policy.adapters`:
+
+- `EnterpriseIdentity`
+- `IdentityClaimsAdapter`
+- `DirectoryGroupResolver`
+- `EnterprisePolicyMapper`
+- `DecisionLogStore`
+
+Documentation: `docs/enterprise-access-control-integration.md`.
+
+## Decision
+
+Implement concrete enterprise integration as an optional extension track. Core
+Markitect should keep accepting normalized `PolicySubject` and `PolicyObject`
+models, while enterprise adapters handle token verification, group freshness,
+claim mapping, durable decision logs, and external PDP calls.
+
+Do not map raw AD/LDAP/Entra group names directly to Markitect privileges.
+Always map:
+
+```text
+directory groups -> canonical roles/scopes/trust labels -> PolicySubject
+```
+
+## P14.1 - Define enterprise policy map schema
+
+```task
+id: MKTT-WP-0014-T001
+status: todo
+priority: high
+state_hub_task_id: "1894c50f-95c3-4e1a-bd4f-388f7624ebd7"
+```
+
+Define the mapping file that translates enterprise groups, roles, scopes,
+tenants, assurance levels, and emergency rules into Markitect labels, trust
+zones, allowed actions, and object constraints.
+
+Output: schema, examples, diagnostics, and tests.
+
+## P14.2 - Implement NetKingdom/key-cape identity claims adapter
+
+```task
+id: MKTT-WP-0014-T002
+status: todo
+priority: high
+state_hub_task_id: "8a177375-09b3-4898-a053-7601f82fcb29"
+```
+
+Implement an optional `IdentityClaimsAdapter` that consumes
+NetKingdom/key-cape-compatible OIDC discovery and JWTs.
+
+It must validate:
+
+- issuer
+- audience
+- expiry and issued-at
+- signature through JWKS
+- authorized party/client id where required
+- MFA/assurance claims for privileged actions
+
+Output: adapter, fixtures, negative tests, and clear diagnostics.
+
+## P14.3 - Implement enterprise subject mapper
+
+```task
+id: MKTT-WP-0014-T003
+status: todo
+priority: high
+state_hub_task_id: "6861d4bc-1bb8-440d-bb9e-33e20c7feb55"
+```
+
+Implement `EnterprisePolicyMapper` over the policy map schema. It should map
+verified identity claims and resolved groups into gateway-ready
+`PolicySubject` objects.
+
+Output: mapper, examples, and tests for roles, scopes, groups, trust zones,
+tenancy, and emergency access.
+
+## P14.4 - Add directory group resolution boundary
+
+```task
+id: MKTT-WP-0014-T004
+status: todo
+priority: medium
+state_hub_task_id: "56d6bad6-d706-47b3-b321-1f0e870ecc0d"
+```
+
+Implement a provider-neutral group-resolution layer for claims that are stale,
+partial, or too large for tokens. Start with a fake/test resolver and specify
+adapter hooks for SCIM, Microsoft Graph, LDAP, and Keycloak.
+
+Output: resolver contract, freshness metadata, overage handling, and tests.
+
+## P14.5 - Persist decision logs
+
+```task
+id: MKTT-WP-0014-T005
+status: todo
+priority: high
+state_hub_task_id: "f212662c-4ffc-4cac-ace2-a43777f4960c"
+```
+
+Implement a durable `DecisionLogStore` for policy decisions from query, search,
+context packages, workflows, exports, and assisted prompt assembly.
+
+Decision logs should record subject id, token hash, action, object id, policy
+version, decision effect, reason, redaction status, and provenance.
+
+Output: storage adapter, CLI inspection path, and tests.
+
+## P14.6 - Add external PDP examples
+
+```task
+id: MKTT-WP-0014-T006
+status: todo
+priority: medium
+state_hub_task_id: "573a198f-df0b-470a-b11c-9ac839c0845e"
+```
+
+Provide reference adapters or documented examples for:
+
+- OpenFGA/SpiceDB-style relationship checks through
+ `RelationshipPolicyAdapter`
+- OPA/Rego or Cedar-style rule checks through `RulePolicyAdapter`
+
+Output: examples, adapter stubs, and policy request/decision fixtures.
+
+## P14.7 - Integrate policy identity into workflows and context packages
+
+```task
+id: MKTT-WP-0014-T007
+status: todo
+priority: high
+state_hub_task_id: "c4650304-0e2b-49c5-8569-e69907c08ccc"
+```
+
+Make workflow and future context-package execution accept explicit enterprise
+identity and policy mapping configuration.
+
+Required concepts:
+
+- `subject_from_token`
+- `policy_map`
+- `required_assurance`
+- `emergency_justification`
+- decision-log sink
+
+Output: workflow/context integration design, examples, and tests.
+
+## P14.8 - Validate against NetKingdom IAM profile
+
+```task
+id: MKTT-WP-0014-T008
+status: todo
+priority: medium
+state_hub_task_id: "0486e0c2-2cb9-4902-9a09-9ec729e9e79f"
+```
+
+Build conformance tests against the local IAM profile:
+
+- required claims
+- human Authorization Code + PKCE expectations
+- service account claims
+- local development issuer rejection in production mode
+- emergency access audit requirements
+
+Output: test fixtures and conformance checklist.
+
+## Exit Criteria
+
+- A NetKingdom/key-cape-compatible OIDC identity can be validated and mapped to
+ a `PolicySubject`.
+- Enterprise groups, roles, scopes, trust zones, and labels are mapped through
+ a versioned policy map rather than raw directory names.
+- Query, search, workflow, and context-package boundaries can enforce policy
+ and emit durable decision logs.
+- Directory group overage and freshness are represented explicitly.
+- OpenFGA/SpiceDB and OPA/Cedar-style PDP integrations can attach without
+ replacing Markitect's local policy gateway.
+- The implementation remains optional and does not add enterprise IAM
+ dependencies to core Markdown parsing or deterministic processing.
+
+## Notes
+
+This workplan should be picked up before using Markitect context caches for
+production agent memory in enterprise settings. It does not need to block local
+research on `MKTT-WP-0008`, but it should gate production deployment of
+reactivatable cross-document context packages.