utils.roles.applications package

Subpackages

Submodules

utils.roles.applications.config module

exception utils.roles.applications.config.AppConfigKeyError(message: str = '', obj: Any = None, show_content: bool = True, suppress_extended_error: bool | EllipsisType = Ellipsis, orig_exc: BaseException | None = None, help_text: str | None = None)

Bases: AnsibleTemplatePluginError, ValueError

Raised when a required application config key is missing (strict mode). Compatible with Ansible error handling and Python ValueError.

exception utils.roles.applications.config.ConfigEntryNotSetError(message: str = '', obj: Any = None, show_content: bool = True, suppress_extended_error: bool | EllipsisType = Ellipsis, orig_exc: BaseException | None = None, help_text: str | None = None)

Bases: AppConfigKeyError

Raised when a config entry is defined in schema but not set in application.

utils.roles.applications.config.get(applications, application_id, config_path, strict=True, default=None, skip_missing_app=False)

utils.roles.applications.in_group_deps module

utils.roles.applications.in_group_deps.applications_if_group_and_all_deps(applications: dict[str, Any], group_names: list[str], *, project_root: str | None = None, roles_dir: str | None = None, service_registry: dict[str, Any] | None = None, meta_deps_resolver: Callable[[str, str], list[str]] | None = None, blocked: frozenset[str] | set[str] | None = None) → dict[str, Any]
utils.roles.applications.in_group_deps.load_service_registry(project_root: str) → dict[str, Any]
utils.roles.applications.in_group_deps.meta_deps_from_disk(role: str, roles_dir: str) → list[str]
utils.roles.applications.in_group_deps.reachable_roles(applications: dict[str, Any], seeds: list[str], *, project_root: str | None = None, roles_dir: str | None = None, service_registry: dict[str, Any] | None = None, meta_deps_resolver: Callable[[str, str], list[str]] | None = None, blocked: frozenset[str] | set[str] | None = None) → set[str]

Return every role reachable from seeds through meta and service edges.

Parameters:
  • applications – the merged applications map.

  • seeds – role ids to start the traversal from.

  • project_root – repository root, used to derive roles_dir and registry.

  • roles_dir – roles directory, when it is not derived from project_root.

  • service_registry – prebuilt registry, otherwise built from the inputs.

  • meta_deps_resolver – reads a role’s meta/main.yml dependencies.

  • blocked – roles the traversal must not enter.

utils.roles.applications.mcp module

Vocabulary of the mcp block in meta/services.yml.

Every role with an application_id declares exactly one mcp: entry. The block is an audit disposition first and a deployable surface second: classification states what the role is, and only a deployable classification may also carry enabled/shared and a served contract.

Field vocabulary (see docs/contributing/design/role/services/mcp.md):

  • classification: what this role is in the MCP audit. Required.

  • reason: why a non-deployable classification cannot serve.

  • direction: whether the role exposes MCP, consumes it, or both.

  • transport: wire protocol of the endpoint.

  • exposure: who may reach the endpoint.

  • auth: authentication scheme guarding the endpoint.

  • auth_subject: identity MCP calls execute as. user and oidc additionally require a delegation block, because a rendered deployment bearer is a service account no matter what the metadata claims.

  • delegation: recorded proof, at one exact upstream version, that per-user token acquisition, refresh, audience binding and revocation exist.

  • implementation: how the surface is provided, ordered by precedence.

  • credential: which principal the provider authenticates as, and where its secret is read from. Replaces the administrator token path.

Admission is not declared here. A client marks itself once, in its own meta/services.yml self-entry, as mcp_consumer: true; a provider deviates by carrying mcp_consumer: false on that client’s entry in its own meta/services.yml. derive_allowed_consumers resolves the pair.

  • supported_transports / supported_auths: what a client can present.

  • endpoint: connection metadata for clients (server roles only).

  • adapter: immutable source contract of a repository-owned adapter. category_allowlist suits an upstream that switches whole categories and offers no per-tool filter, where a tool allowlist would be unenforceable; mcp_passthrough fronts an upstream that already speaks MCP instead of translating REST. upstream_path names that upstream where it lives on the application’s own vhost, so the proxy can hide it: reaching it directly walks past the allowlist the adapter exists to enforce. A provider whose upstream is a separate sidecar declares upstream_network instead and has nothing to hide there.

  • limits: the request, response, timeout, concurrency, pagination and stream ceilings the surface enforces.

  • tools: allowlist is what a reader may reach and writer_allowlist what a writer may reach; the deployment renders one contract per role, each with its own bearer, because the gateway tells callers apart by credential and by nothing else. allowlist is policy, what the platform lets a consumer reach, and is only a constraint where a gateway enforces it. upstream_serves is the observation: every tool the upstream offers at the deployed version. State it only where it DIFFERS from the allowlist; absence means the two are identical, which is the normal case for a provider that serves exactly what is allowed. The difference between them measures unenforced exposure, and a provider whose difference is non-empty needs a gateway in front. Plus schema hash and mutation policy.

  • mutating_proofs: required once tools.mutating_tools_enabled is true. Names the artifact proving each of confirmation, authorization, idempotency, audit and reversal for the mutating variant, separately from the read-only one.

  • source_url: upstream documentation or source of the MCP surface.

utils.roles.applications.mcp.admits_mcp_consumer(provider_services: object, consumer_id: str) → bool

Return whether a provider admits one client.

An omitted flag inherits the client’s own declaration; only an explicit mcp_consumer: false excludes, so a deliberate exclusion is distinguishable from a forgotten entry.

Parameters:
  • provider_services – the provider’s services mapping.

  • consumer_id – the client’s application_id.

utils.roles.applications.mcp.declares_delegation(mcp: object) → bool

Return whether this block claims to act as the requesting end user.

Parameters:

mcp – the role’s mcp mapping, or anything else.

utils.roles.applications.mcp.declares_mcp_consumer(role_id: str, services: object) → bool

Return whether a role marks itself an MCP client in its own services.

Parameters:
  • role_id – the role’s application_id.

  • services – that role’s services mapping, or anything else.

utils.roles.applications.mcp.delegation_is_proven(mcp: object) → bool

Return whether a recorded per-version audit backs the delegation claim.

Parameters:

mcp – the role’s mcp mapping, or anything else.

utils.roles.applications.mcp.derive_allowed_consumers(provider_id: str, services_by_role: dict[str, object]) → list[str]

Return the client application ids a provider admits, sorted.

Parameters:
  • provider_id – the provider’s application_id.

  • services_by_role – every role’s services mapping, keyed by role id.

utils.roles.applications.mcp.derive_mcp_presence(applications: dict[str, Any]) → dict[str, Any]

Materialise every mcp topic’s enable state from its declared peers.

Parameters:

applications – {application_id: config}, mutated in place.

Returns:

The same mapping.

A surface is reachable only while a peer that uses it is deployed, so both keys resolve to the in group_names form the rest of the tree uses, and to False when a role has no peer at all. An explicitly configured enabled/shared always wins, so config stays the override channel. Keying on peer PRESENCE rather than on a peer’s own value is required: a re-entrant read under _RENDER_GUARD returns the unrendered dict.

The resolved block replaces the mapping rather than being written into it. _build_role_base_config parks the YAML cache’s own object under mcp, so an in-place write would inject these keys into every later raw read of meta/mcp.yml.

utils.roles.applications.mcp.is_deployable(mcp: object) → bool

Return whether this block may carry a served surface and be discovered.

Parameters:

mcp – the role’s mcp mapping, or anything else.

utils.roles.applications.mcp.resolve_credential(server: Mapping[str, Any], users: Mapping[str, Any], role_credentials: Mapping[str, Any]) → tuple[str, str]

Return the provider’s declared secret and the owner it belongs to.

Parameters:
  • server – a discovered direction=server entry.

  • users – the merged users mapping, carrying each principal’s tokens.

  • role_credentials – the provider role’s own secrets.credentials mapping, which is where every other consumer addresses them.

Returns an empty token when the declaration is incomplete or the principal holds nothing; the caller turns that into credential_missing.

utils.roles.applications.mcp.value_is_templated(value: object) → bool

utils.roles.applications.mounts module

Normalize and validate the role-level mount declaration in roles/<role>/meta/volumes.yml.

Canonical shape (dict-of-dicts; the YAML key is the semantic short name):

data: # YAML key = semantic short name

type: volume # bind | volume | config | secret | tmpfs name: matrix_synapse_data # OPTIONAL docker volume name; defaults to the key nfs: true # only meaningful for type: volume mounts:

  • service: synapse target: /data read_only: false # per-mount override of volume-level when: “{{ FLAG | bool }}”

config:

type: bind # bind | config | secret may carry source source: “{{ FOO }}” read_only: true mode: “0440” # only for config/secret mounts:

  • service: synapse target: /etc/synapse/config

scratch:

type: tmpfs mounts:

  • service: synapse target: /var/run/synapse size: 64m # per-mount tmpfs size

The legacy list-of-dicts shape is no longer accepted.

exception utils.roles.applications.mounts.VolumesSchemaError

Bases: ValueError

Raised when a meta/volumes.yml fails schema validation.

utils.roles.applications.mounts.content_hash(text: str, length: int = 8) → str

Stable short hex digest of a string (used for swarm config/secret name rotation).

utils.roles.applications.mounts.mount_default_read_only(volume: dict[str, Any]) → bool

Default read-only policy per type.

bind/volume default to rw (False); config/secret are conceptually read-only at the docker primitive level; tmpfs is rw.

utils.roles.applications.mounts.mount_when_passes(mount: dict[str, Any], render_jinja: Callable[[str], str] | None) → bool

Evaluate the optional when: gate of a single mount entry.

Parameters:
  • mount – one dict from volume['mounts'].

  • render_jinja – renders a Jinja expression string; None disables rendering.

Returns:

True when the mount is active.

utils.roles.applications.mounts.mounts_for_service(volume: dict[str, Any], service: str) → Iterable[dict[str, Any]]

Yield mount dicts in volume['mounts'] that target service.

utils.roles.applications.mounts.normalize_volumes_meta(meta_volumes: Any) → dict[str, dict[str, Any]]

Return a dict[semantic_name -> canonical entry].

The YAML key is the semantic short name. Each entry value MAY carry name (the docker volume name); when absent and type is volume, callers default the docker name to the semantic key.

utils.roles.applications.mounts.validate_volumes_meta(meta_volumes: Any, role_id: str) → list[str]

Return a list of human-readable violation strings (empty = valid).

utils.roles.applications.security_headers module

Response security headers the front proxy adds for one application.

WEBSERVER_SECURITY_HEADERS holds the platform values. An application overrides any of them under security_headers in its meta/server.yml.

utils.roles.applications.security_headers.security_headers(applications: Mapping[str, Any], application_id: str, platform: Mapping[str, Any]) → dict[str, Any]

Return the security header values the proxy sends for application_id.

Parameters:
  • applications – the applications SPOT.

  • application_id – the role whose vhost is rendered; a redirect vhost passes its domain, which owns no override.

  • platform – WEBSERVER_SECURITY_HEADERS.

Returns:

nosniff as a bool, referrer_policy and permissions_policy as header values (empty when not sent) and hsts as the complete Strict-Transport-Security value.

utils.roles.applications.status_codes module

HTTP status codes for a deployed domain.

roles/<app>/meta/server.yml declares status_codes keyed like domains.canonical; it is the single source of truth for every prober.

utils.roles.applications.status_codes.accepted_status_codes(applications, app_id: str, domain: str) → list[int]

Return the codes that prove domain is being served at all.

Most declarations are narrower than DEFAULT_OK, so this gate adds to the default instead of replacing it; narrowing it would escalate vhosts that answer perfectly well. Contract checking is a different question and belongs to sys-ctl-hlth-webserver.

Parameters:
  • applications – the applications SPOT.

  • app_id – the role the domain belongs to.

  • domain – the fully qualified domain being probed.

Returns:

DEFAULT_OK plus whatever the app declares for this domain.

utils.roles.applications.status_codes.codes_by_key(raw) → dict[str, list[int]]

Normalize a raw server.status_codes declaration.

Parameters:

raw – the declaration as read from the app config.

Returns:

The declared keys mapped to their codes, dropping keys that declare nothing usable.

utils.roles.applications.status_codes.declared_status_codes(applications, app_id: str, domain: str) → list[int]

Return what app_id declares for domain, or an empty list.

The vhost is identified by the domains.canonical key that carries the domain, which is the same key server.status_codes uses. A domain that is not canonical, or a key that declares nothing, falls back to the declared default.

Parameters:
  • applications – the applications SPOT.

  • app_id – the role the domain belongs to.

  • domain – the fully qualified domain being probed.

Returns:

The declared codes, empty when the app declares none for this domain.

utils.roles.applications.status_codes.normalize_codes(value) → list[int]

Return value as a list of plausible HTTP status codes.

Parameters:

value – a scalar, list or mapping straight out of the declaration.

Returns:

The codes as de-duplicated integers, dropping anything outside 100-599.

utils.roles.applications.topics module

The config topics a variant may dictate for the providers it pulls in.

A round deploys a provider once, so the role that pulls it in is the only place that can say how lean it has to be. A variant entry may therefore carry any application config topic under the service it depends on:

services:
openwebui:

enabled: true shared: true services: null # brings nothing along addons:

whisper:

enabled: false # everything else in addons survives

A mapping deep-merges, so naming one key changes that key and nothing else. null empties the topic, which is the only way to say “bring nothing”: merged, an empty mapping is a no-op and would leave exactly the services the round was trying to shed.

YAML writes null three ways - null, ~, and a key with nothing after it - so a half-typed line empties a topic rather than being ignored.

utils.roles.applications.topics.apply_topic(base_value: Any, override_value: Any) → Any

Return a provider’s topic after one override is applied.

Parameters:
  • base_value – what the provider declares for the topic today.

  • override_value – what the pulling role demands.

None empties the topic and is normalised to {}: the closure resolver skips a non-mapping payload, so leaving the bare None would silently keep the services it was meant to drop.

utils.roles.applications.topics.overridden_providers(roles_dir: str | Path) → dict[tuple[str, int], set[str]]

Return {(role, variant index): {provider role, ...}} across every variant.

Parameters:

roles_dir – roles directory the variants and service registry are read from.

A variant that dictates a provider’s config does not deploy that provider as the provider declares it, so nothing about that run states what the provider does on its own. Callers that reason about coverage need to know which pairs those are before they credit one role’s deploy to another.

utils.roles.applications.topics.provider_of(service_key: str, service_registry: Mapping) → str

Return the role providing service_key, or “” when unregistered.

Parameters:
  • service_key – a first-level key of a role’s services map.

  • service_registry – the project-wide service registry.

utils.roles.applications.variants module

What one round’s variants mean for the roles that round pulls in.

A round picks one variant per role, and that choice decides two things no single role’s meta/services.yml can state on its own: which providers the role still pulls in, and how lean those providers have to be (utils.roles.applications.topics). Both are resolved here, so the matrix planner’s include set, the inventory bake and the CI selection read one answer instead of three.

The module is deliberately free of deploy-runtime imports: the CI selection resolves its closure on a runner that has no inventory, no container and no INFINITO_SRC_DIR.

exception utils.roles.applications.variants.NestedOverrideConflictError

Bases: RuntimeError

Two roles in one round demand different config for one provider.

utils.roles.applications.variants.collect_provider_overrides(payloads: Mapping[str, Any], *, roles_dir: str) → dict[str, dict[str, Any]]

Return {provider: {topic: value}} demanded by the round’s payloads.

Parameters:
  • payloads – {role: variant payload} for one round.

  • roles_dir – roles directory the service registry is built from.

Raises:

NestedOverrideConflictError – two roles demand different values for the same topic of the same provider.

utils.roles.applications.variants.services_overrides_for_round(*, roles_dir: str, round_index: int, primary_app_variants: Mapping[str, int]) → dict[str, dict]

For every role with meta/variants.yml, return the services map that results from merging the round’s variant payload onto the role’s on-disk meta/services.yml.

Apps in primary_app_variants use the supplied (already clamped) index. Other roles with their own variants clamp round_index to their own variant count. Roles without variants are absent from the result so the resolver falls through to its disk-read path.

The merged map is what the inventory ALSO bakes into host_vars, so feeding the same dict into CombinedResolver(services_overrides=…) eliminates the topology-vs-host_vars drift that variant-blind resolution produced.

Module contents