Skip to content

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__KEY first and then the flat KEY as a fallback, and merges them over the instance's own values, so the trace can report environment as 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 in trace.rejected_overrides, which differs from ScopedConfigService.resolve_module_settings, which raises.
  • A decision record is only appended when a decision logger is configured and trace_policy_field is set. That field must exist on the settings class, otherwise ValueError.
  • If a store is configured, session is required (ValueError otherwise). 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.
  • PolicyChangeRecord and PolicyChangeLogger only define the shape. Nothing in msflib.policy writes change records for you; emit them from your admin endpoints.
  • The in-memory loggers grow without bound. Use them in tests, and implement the append protocol over your own storage in production.
  • The service is storage-agnostic and does not check who may write tenant, workspace or user values.