Skip to content

Replace per-handler mcp_manager plumbing with a single PolicyBus #474

@shaun0927

Description

@shaun0927

Context

Depends on #471 (control-plane RFC), the Directive vocabulary issue, and the control_directive event-category issue.

PRs #279 and #280 wire mcp_manager through ExecuteSeedHandler via explicit constructor injection. Each additional handler that needs MCP capability — evolve_step, unstuck, ralph, and any future workflow — must repeat the same injection.

Evidence from #280:

  • src/ouroboros/cli/commands/mcp.py: bridge is constructed and passed into create_ouroboros_server(..., mcp_bridge=mcp_bridge)
  • src/ouroboros/mcp/tools/definitions.py::execute_seed_handler added mcp_manager and mcp_tool_prefix parameters
  • docs/guides/mcp-bridge.md records as a Known Limitation that evolve_step does not yet receive the manager

Problem

Per-handler injection is O(handlers). It has three failure modes:

  1. A new handler silently omits the injection → runtime failure only.
  2. Future plumbing (event store, LLM backend, directive emitter) compounds the parameter list.
  3. Dynamic capability addition (also a stated limitation: "No dynamic server addition after initial connection") has no seam to plug into — each handler holds a static reference.

Proposal

Introduce a single PolicyBus object (name open — ControlBus / DirectiveBus are alternatives) that owns the dependencies currently passed à la carte:

@dataclass
class PolicyBus:
    event_store: EventStore
    mcp_manager: MCPClientManager | None   # from MCPBridge, optional
    directive_emitter: DirectiveEmitter    # emits ControlDirectiveEmitted
    llm_backend: str | None
    runtime_backend: str | None
    # ... single source of shared dependencies

Construction: once per server lifecycle, at _run_mcp_server time. Handlers receive PolicyBus by reference.

Handler contract change

Before:

ExecuteSeedHandler(
    event_store=event_store,
    mcp_manager=mcp_manager,
    mcp_tool_prefix=prefix,
    runtime_backend=rb,
    llm_backend=lb,
)

After:

ExecuteSeedHandler(bus=bus)

Dynamic capability support

Because PolicyBus holds a live reference to the manager, future dynamic add_server calls on the manager propagate to all handlers without re-injection — unlocking the second half of #280's Known Limitations.

Migration strategy

  1. Introduce PolicyBus as an additive type; existing constructors unchanged.
  2. Add a from_bus classmethod to each handler: ExecuteSeedHandler.from_bus(bus) constructs with the legacy arguments derived from the bus.
  3. Migrate call sites one at a time. Deprecate the legacy constructors.
  4. Remove legacy constructors in a subsequent release.

Acceptance Criteria

  • PolicyBus lives under src/ouroboros/orchestrator/ (or core/ — final location decided in review)
  • _run_mcp_server constructs a single bus and passes it to create_ouroboros_server
  • ExecuteSeedHandler accepts the bus and its unit tests pass unchanged
  • Deprecation warnings on legacy constructors
  • A worked example PR (separate, linked here) demonstrates adding a new handler with a 1-file change, proving O(1) plumbing

Out of Scope

  • Migrating evolve_step, unstuck, ralph — that is the next sibling issue
  • Adding new directives to the vocabulary — that is the Directive vocabulary issue
  • Reshaping the Double-Diamond execution pipeline

Risks

  • Large blast radius: handler signatures change broadly. Mitigation: classmethod bridge (from_bus) plus staged migration.
  • Hidden coupling: a "god object" bus can accumulate unrelated fields. Mitigation: keep the struct flat and reviewed on every addition; document membership criteria.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions