Policy¶
msflib.policy builds on tiered configuration. Where tiered config answers "what is the effective value", policy adds "which tier decided it, which request overrides were refused, and can I log that". It is meant for structured settings such as model routing, chunking or retrieval rules, where an operator needs to audit why a request behaved as it did.
Everything below is exported from msflib.policy.
What it provides¶
| Name | Purpose |
|---|---|
PolicyResolutionService |
Resolves a ModuleSettingsBase across all tiers and returns the settings plus a trace. |
PolicyResolutionTrace |
Per key, which PolicyLayer won, plus the request overrides that were rejected. |
PolicyLayer |
Enum: default, environment, tenant, workspace, user, request. |
PolicyScopedConfigStore |
Protocol the service uses to read tenant, workspace and user values. |
PolicyEnvelope |
Optional pydantic base model with a common policy shape (enabled, version, defaults, constraints, overrides, selection, fallbacks, observability). Extra fields are allowed. |
PolicyDecisionRecord, PolicyChangeRecord |
Dataclasses for decision and change audit entries. |
PolicyDecisionLogger, PolicyChangeLogger |
Protocols with one method, append(record). |
InMemoryPolicyDecisionLogger, InMemoryPolicyChangeLogger |
List-backed loggers for tests and local use (.records). |
validate_policy_mapping |
Parse and validate a policy mapping against a schema. |
Resolving¶
The service takes a store for the persisted tiers and an optional decision logger. ScopedConfigPolicyStoreAdapter from msflib.workspace_config.service is the store backed by workspace-config. Any object with the three get_*_namespace_values methods satisfies the protocol, so you can also back it with something else.
from msflib.policy import InMemoryPolicyDecisionLogger, PolicyResolutionService
from msflib.workspace_config.service import ScopedConfigPolicyStoreAdapter, ScopedConfigService
decisions = InMemoryPolicyDecisionLogger()
policy_service = PolicyResolutionService(
scoped_config_store=ScopedConfigPolicyStoreAdapter(ScopedConfigService()),
decision_logger=decisions,
)
resolved, trace = policy_service.resolve_module_settings(
ChatSettings(), # a ModuleSettingsBase subclass instance
session=session,
tenant_id=1,
workspace_id=10,
account_id=25,
request_override={"MAX_TOKENS": 10, "MODEL": "other"},
trace_policy_field="MODEL",
request_id="req-1",
)
trace.winning_layers["MAX_TOKENS"] # PolicyLayer.request
trace.rejected_overrides # [RejectedOverride(... key='MODEL', reason='Not in allowlist')]
decisions.records[0].winning_layers # {'MODEL': 'tenant'}
(ChatSettings is the class from tiered configuration, with MAX_TOKENS in its allowlist and MODEL not.)
Behavior to rely on:
- Resolution order is default, environment, tenant, workspace, user, request. The service reads the namespace's environment variables itself on each call, trying
NS__KEYfirst and then the flatKEYas a fallback, and merges them over the instance's own values, so the trace can reportenvironmentas a winner. - Request overrides are normalized and checked against
request_override_allowlist. Rejected keys do not raise. They are dropped from the merge and listed intrace.rejected_overrides, which differs fromScopedConfigService.resolve_module_settings, which raises. - A decision record is only appended when a decision logger is configured and
trace_policy_fieldis set. That field must exist on the settings class, otherwiseValueError. - If a store is configured,
sessionis required (ValueErrorotherwise). With no store, only defaults, environment and request override apply. - Stored values are deep-merged into the current settings, so a tenant can override one key of a nested policy and keep the rest.
Policy fields with a schema¶
A policy field is an ordinary settings field whose value is a structured mapping. Annotate it with a pydantic model, typically a PolicyEnvelope subclass:
from typing import ClassVar
from msflib.core.config import ModuleSettingsBase
from msflib.policy import PolicyEnvelope
class RoutingPolicy(PolicyEnvelope):
default_model: str = "small"
class LlmSettings(ModuleSettingsBase):
namespace: ClassVar[str] = "LLM"
ROUTING_POLICY: RoutingPolicy = RoutingPolicy()
LlmSettings.get_policy_field_schema("ROUTING_POLICY") returns RoutingPolicy. If the field is annotated as a plain mapping instead, the lookup falls back to a sibling policy.py module next to the settings module, looking for a class named RoutingPolicySchema, then RoutingSchema, RoutingPolicy or RoutingModel (the base name comes from the field name minus _POLICY), or an entry in a POLICY_FIELDS dict there. It raises ValueError if nothing is found.
resolve_policy_field resolves the whole module and returns just one field as a validated dict, with its trace:
policy, trace = policy_service.resolve_policy_field(
LlmSettings(),
field_name="ROUTING_POLICY",
schema=RoutingPolicy,
session=session,
tenant_id=1,
request_id="req-1",
)
policy["default_model"]
validate_policy_mapping(value, field_name=..., schema=...) is the same parsing step on its own. It accepts a dict, a pydantic model, or a string containing JSON or a Python literal, and raises ValueError("<field> failed schema validation: ...") on failure.
In a host app¶
Resolve per request, then hand the resolved settings to your own code (a "bridge" that maps the policy to behavior). Modules that support policy, such as ai_core, accept a get_policy_resolver callable in their deps.py factory and fall back to base settings when none is supplied. See dependency injection.
Pitfalls¶
- Use the resolved settings, not the base instance, for runtime decisions. The base instance never reflects tenant or user values.
PolicyChangeRecordandPolicyChangeLoggeronly define the shape. Nothing inmsflib.policywrites change records for you; emit them from your admin endpoints.- The in-memory loggers grow without bound. Use them in tests, and implement the
appendprotocol over your own storage in production. - The service is storage-agnostic and does not check who may write tenant, workspace or user values.