Dispatcher
Operating model
This document is the operational reference for the version-6 dispatcher. The dispatcher is a bounded router and authorization checker: it resolves one declared interface, checks the relevant blueprint policies, compiles the declared process binding, and launches the gateway. It does not repair or synchronize repository state.
For the end-to-end role of dispatch, see Getting Started. Blueprints explains the declarations Dispatcher consumes.
For each invocation, Dispatcher follows one bounded path:
resolve -> authorize -> bind -> execute
It resolves the canonical caller and target, authorizes every crossed module boundary, binds supplied arguments and stdin to the declared process grammar, then executes the selected gateway. It mechanically enforces that invocation grammar. It does not generally verify gateway output against a declared output schema; the producer, consumer, or owning adapter must perform that validation where the contract requires it. See Schemas.
Invocation
The shared famulus_dispatcher MCP server resolves the current plugin package and invokes
Dispatcher internally. Its invoke tool accepts the generated projection:
{"caller":"<top-level-skill>","interface":"<module>.interface.<name>","version":1,"arguments":{"positionals":[],"options":{},"stdin":null},"dry_run":false}
Set dry_run to true to authorize and compile without launching the gateway. The
result is JSON containing the canonical caller and target IDs, selected source
interface, compiled argv, working directory, Python entrypoint, stdin decision,
and warnings. Failures use the tool's structured dispatcher result.
The host caller must be a discoverable top-level skill. Runtime code may make
nested calls using its immediate canonical module ID through the programmatic
dispatcher API. A host cannot claim a private child such as daily-plan._rtx
as its identity.
Repository configuration
The MCP runtime supplies one exact absolute
officina.toml path from the selected plugin or checkout. The
configuration file's directory is the repository root and its ordered
modules.roots entries are the only blueprint lookup roots:
schema_version = 1
[modules]
roots = ["skills", "src/officina"]
Dispatcher does not consult AI, the current directory, parent directories,
or PYTHONPATH to discover a repository. Missing, duplicated, escaping,
symlinked, or malformed configured roots fail closed.
Direct lookup
Module identity comes from its path beneath a configured root. For example:
skills/list-manager/blueprint.yaml -> list-manager
skills/list-manager/_rtx/blueprint.yaml -> list-manager._rtx
A dotted child is reachable only when every parent registers the next segment
in children. External traversal additionally requires an explicit
namespace_exports route whose nonempty surface.only mapping contains the
exact descendant interface and version. Version 6 has no facade aliases and
no surface.all form.
For one request, dispatcher reads only:
officina.toml;- the caller and target ancestry blueprints;
- the terminal target module blueprint; and
- the selected behavioral-source blueprint.
It does not list module directories or read unrelated blueprints. Repository size therefore does not determine route-resolution work.
The lookup rule remains direct and catalog-free:
module_id -> configured root/module segments/blueprint.yaml. The MCP server
does not catalogue modules, generate an index, or add another path resolver.
Managed setup preflight
For an ordinary non-dry MCP call, Dispatcher first authorizes one exact route.
Setup classification reuses that invocation-local repository and the already
loaded target ancestry; it does not resolve or authorize the route a second
time. A public .interface.setup export is managed automatically; if the
ancestry proves no managed setup is required, MCP launches the authorized
target without calling setup-interface-manager or touching its ledger.
When a managed .interface.setup is required, the direct setup loader follows
only explicit setup_requires_setup_of references and builds the sparse fields
consumed by the existing setup evaluator. Exact managed setup and teardown
interfaces are intercepted before process-binding compilation. An ordinary
managed target still requires manager status, followed by atomic authorize
when ready, before the original target is compiled and launched.
Setup-interface-manager remains the sole authority for ledger reads, locks,
claims, recovery, and settlement. Only its status and authorize routes load
the live route-local sparse graph. begin, run/settle/recover, teardown, and
invalidate retain canonical repository-wide graph loading because those
operations need broader lifecycle state or reverse-dependent discovery.
Authorization
For a policy owned by module y, caller x is admitted when x == y, the
policy sets allow_all_modules: true, or the ancestry of x intersects the
resolved allowed_callers. Naming a module admits that module and its
registered descendants; it does not admit its parent or siblings.
Only target-side namespace boundaries below the caller/target lowest common ancestor are crossed. At every crossed boundary, dispatcher evaluates the namespace policy and any interface-specific restriction. After a gate admits the call, that namespace owner becomes the immediate caller for the next hop. The terminal child export is always an authority ceiling.
Source uses_interfaces declarations are static validation and certification
facts. They do not independently grant runtime authority.
Execution boundary
Dispatcher accepts only a process-bindable exported interface. It compiles arguments and stdin against the selected source declaration before launch. Python gateways run through the selected interpreter and a confined importer rooted at the selected module. Ambient import paths that expose configured repository modules are removed.
Gateway stdout, stderr, and exit status pass through unchanged. --dry-run
does not read stdin or launch the gateway.
Failures and warnings
Relevant malformed or unsafe state is an error, including an invalid canonical ID, missing registration, missing namespace route, ambiguous top-level module, access denial, interface/version mismatch, unsafe source path, invalid process binding, or repository-configuration error. Dispatcher reports the authored condition and does not repair it.
Unrelated malformed modules are neither errors nor warnings because they are not read. Repository-wide validators own complete topology and schema checks.
Certification currentness is advisory during dispatch. Missing, stale, expired, malformed, or unavailable status produces a warning but cannot grant or deny permission. Certification and drift tools remain authoritative for reviewing and signing repository state outside the live route.
Process and performance
Fresh-process measurements include interpreter and module startup. They are
not measurements of authorization alone. The repository performance gates
require warm in-process resolution below 50 ms median, and fresh CLI resolution
below a median that _fresh_cli_budget_ms sets per OS family: 125 ms on Linux,
150 ms on macOS, 175 ms on Windows. The three differ because hosted process
creation does, and treating that difference as a dispatcher regression would
only teach the gate to be ignored. Every gate is a median; no percentile is
enforced, since a single contended sample is what a percentile would catch and
host contention is not what these tests measure. They are regression budgets
rather than portable timing guarantees; ordinary operating-system file cache
and host-load variation is expected.
What dispatcher never does
The live routing path does not build repository-wide graphs, inventories, snapshots, catalogs, caches, or manifests; inspect Git; derive or repair certificates; synchronize blueprints; contact a network; acquire routing locks; or write routing state. The sparse setup projection described above is limited to an authorized route and its explicit setup prerequisites; it is not a repository inventory. Repository-wide operations belong to explicit validators or the non-hot-path setup-manager operations described above.