Skip to content

Policies

Nominal Code separates webhook event handling into two frozen Pydantic models: FilteringPolicy (which events to process) and RoutingPolicy (how to dispatch them). Both live in nominal_code.config.policies and are composed into the top-level Config.

FilteringPolicy

Controls which webhook events are accepted before any dispatch decision.

from nominal_code.config.policies import FilteringPolicy

filtering = FilteringPolicy(
    allowed_users=frozenset({"alice", "bob"}),
    allowed_repos=frozenset({"owner/repo-a"}),
    pr_title_include_tags=frozenset({"nominalbot"}),
    pr_title_exclude_tags=frozenset({"skip"}),
)
Field Type Default Description
allowed_users frozenset[str] frozenset() Usernames permitted to trigger bots via @mention. Empty means all users are allowed.
allowed_repos frozenset[str] frozenset() Repository full names (owner/repo) to process. Empty means all repositories.
pr_title_include_tags frozenset[str] frozenset() Allowlist of [tag] patterns in PR titles. Empty means no include filter.
pr_title_exclude_tags frozenset[str] frozenset() Blocklist of [tag] patterns in PR titles. Takes priority over include tags.

All fields are frozen — instances are immutable after creation.

YAML mapping

The YAML config file maps to FilteringPolicy fields under the access section:

access:
  allowed_users:
    - alice
    - bob
  allowed_repos:
    - owner/repo-a
  pr_title_include_tags:
    - nominalbot
  pr_title_exclude_tags:
    - skip

RoutingPolicy

Controls how accepted events are dispatched to bots.

from nominal_code.config.policies import RoutingPolicy
from nominal_code.models import EventType

routing = RoutingPolicy(
    reviewer_triggers=frozenset({EventType.PR_OPENED, EventType.PR_PUSH}),
    worker_bot_username="nominal-worker",
    reviewer_bot_username="nominalbot",
)
Field Type Default Description
reviewer_triggers frozenset[EventType] frozenset() PR lifecycle events that auto-trigger the reviewer bot. Empty disables auto-trigger.
worker_bot_username str "" The @mention name for the worker bot. Empty disables the worker.
reviewer_bot_username str "" The @mention name for the reviewer bot. Empty disables the reviewer.

YAML mapping

Routing fields are spread across the reviewer and worker YAML sections:

reviewer:
  bot_username: "nominalbot"
  triggers:
    - pr_opened
    - pr_push

worker:
  bot_username: "nominal-worker"

How they fit into Config

The top-level Config model holds both policies:

from nominal_code.config import Config

config: Config = load_config()

config.filtering.allowed_users      # frozenset[str]
config.routing.reviewer_triggers    # frozenset[EventType]

When load_config() reads the YAML file and environment variables, it constructs FilteringPolicy and RoutingPolicy internally and assigns them to Config.filtering and Config.routing.

Public dispatch functions

The webhook server exposes standalone dispatch functions that accept policies directly. This makes them reusable outside the built-in webhook handler — for example, in an enterprise multi-tenant wrapper that constructs per-organization policies.

filter_event

from nominal_code.commands.webhook.server import filter_event

reason: str | None = filter_event(event, filtering)

Applies allowed_repos and PR title tag filters. Returns a reason string ("filtered") if the event should be skipped, or None if it passes.

dispatch_lifecycle_event

from nominal_code.commands.webhook.server import dispatch_lifecycle_event

response = await dispatch_lifecycle_event(
    event=event,
    filtering=filtering,
    routing=routing,
    platform=platform,
    runner=runner,
    namespace="tenant-123",       # optional, for multi-tenant isolation
    extra_env={"CUSTOM": "val"},  # optional, injected into job environment
)

Dispatches a PR lifecycle event (open, push, reopen, ready-for-review) to the reviewer bot. Checks that the event type is in routing.reviewer_triggers, acknowledges it, and enqueues a reviewer job.

dispatch_comment_event

from nominal_code.commands.webhook.server import dispatch_comment_event

response = await dispatch_comment_event(
    event=event,
    filtering=filtering,
    routing=routing,
    platform=platform,
    runner=runner,
    namespace="tenant-123",
    extra_env={"CUSTOM": "val"},
)

Dispatches a comment event to the appropriate bot. Checks for @mentions of the worker and reviewer bots using usernames from routing, authorizes the comment author against filtering.allowed_users, and enqueues the job.

build_runner

from nominal_code.jobs.runner import build_runner

runner = build_runner(config, platforms)

Factory function that constructs a JobRunner from the application config. Returns a KubernetesRunner when config.kubernetes is set, otherwise a ProcessRunner.

Per-organization overrides (multi-tenant)

The policy models are designed to be constructed independently from Config. An enterprise wrapper can build per-organization policies by combining global defaults with organization-specific settings:

from nominal_code.config.policies import FilteringPolicy, RoutingPolicy
from nominal_code.models import EventType

# Global defaults from Config
global_filtering: FilteringPolicy = config.filtering
global_routing: RoutingPolicy = config.routing

# Override for a specific organization
org_filtering = FilteringPolicy(
    allowed_users=global_filtering.allowed_users,
    allowed_repos=frozenset({"org/repo-x", "org/repo-y"}),
    pr_title_include_tags=global_filtering.pr_title_include_tags,
    pr_title_exclude_tags=global_filtering.pr_title_exclude_tags,
)

org_routing = RoutingPolicy(
    reviewer_triggers=frozenset({EventType.PR_OPENED}),
    worker_bot_username="org-worker-bot",
    reviewer_bot_username=global_routing.reviewer_bot_username,
)

# Use with dispatch functions
await dispatch_lifecycle_event(
    event=event,
    filtering=org_filtering,
    routing=org_routing,
    platform=platform,
    runner=runner,
    namespace="org-123",
)

Because both models are frozen Pydantic instances, they are safe to cache, compare, and share across concurrent requests.