Skip to content

feat(core): accept several bootstrap modules in OTARI_BOOTSTRAP - #1061

Closed
daavoo wants to merge 1 commit into
mainfrom
feat/multiple-bootstrap-selectors
Closed

daavoo wants to merge 1 commit into
mainfrom
feat/multiple-bootstrap-selectors

Conversation

@daavoo

@daavoo daavoo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Description

Otari can be extended by pointing OTARI_BOOTSTRAP at a trusted module that rebinds ports and adds routes at startup. Until now that setting took exactly one module, which fits one overlay per deployment but not a deployment that installs two independent extensions from different authors: one of them had to wrap the other.

OTARI_BOOTSTRAP (and the bootstrap config field) now accepts a comma-separated list of module:callable selectors, applied in the order written after the core defaults are bound. A later bind wins, as it did already. A blank entry anywhere in the list, a trailing comma included, refuses to start and names the position, and a second selector that cannot be loaded fails startup rather than quietly running with only the first. The startup summary names each selector with the ports it rebound and the routers it contributed.

The list is explicit and ordered, so the whole wiring is still readable from one config value and nothing is discovered from installed packages, which keeps the no-auto-discovery rule in ARCHITECTURE.md intact. A single selector behaves exactly as before, including every error message.

This is one of the four seams needed so that budget alerts (#973) can ship as a plugin rather than in core. Refs #973.

How to test it locally

Automated:

  • make lint, make typecheck, make test-unit: the new cases live in tests/unit/test_container.py (two selectors applied in order with a later bind winning and both routers recorded, blank middle entry, trailing comma, leading comma, and a failing second selector).
  • uv run --frozen --no-dev python scripts/oss_edition_smoke.py: boots with no bootstrap at all, confirming the plain build is untouched.

The integration suite (make test-integration, which includes tests/integration/test_bootstrap_overlay.py) was not run locally: the machine this was developed on has neither Docker nor a PostgreSQL server, so Testcontainers cannot start. CI runs it on this PR.

By hand, from a checkout:

mkdir -p /tmp/boot && cat > /tmp/boot/one.py <<'EOF'
from gateway.ports.billing_port import BillingPort
class OneBilling:
    def __init__(self, session): ...
def register(container):
    container.bind(BillingPort, OneBilling)
EOF
cat > /tmp/boot/two.py <<'EOF'
from gateway.ports.entitlement_port import EntitlementPort
class TwoEntitlements:
    def __init__(self, session): ...
def register(container):
    container.bind(EntitlementPort, TwoEntitlements)
EOF
PYTHONPATH=/tmp/boot OTARI_BOOTSTRAP="one:register, two:register" uv run otari serve

The startup log reads Composition root: one:register rebound BillingPort; two:register rebound EntitlementPort. Then try OTARI_BOOTSTRAP="one:register,": the gateway refuses to start with OTARI_BOOTSTRAP entry 2 of 2 is blank; remove the extra comma.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #1060
Refs #973

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test). make test-unit and the smoke gate ran locally; make test-integration could not (no Docker or PostgreSQL on the machine) and is left to CI.
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py). Not applicable: no route or schema changed.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used: Claude Fable 5.1 via Claude Code

Any additional AI details you'd like to share: Implementation, tests, docs and this description were written by the agent following the repo's pr-cycle, backend-standards and review skills. A human requested the change and set its scope.

  • I am an AI Agent filling out this form (check box if true)

🤖 Generated with Claude Code

`build_container` now takes a comma-separated list of `module:callable`
selectors and applies them in order after the core defaults are bound,
so two independent extensions (a budget-alerts plugin and a custom
identity adapter, say) coexist without one wrapping the other. A later
bind still wins; a blank entry, a trailing comma included, refuses to
start naming its position, and a selector that cannot be loaded fails
startup as before. The container summary names each selector with what
it rebound and contributed, computed against a snapshot taken before
that selector ran.

Single-selector behavior and messages are unchanged. Docs and the
config field description say a list is accepted; auto-discovery is
still off the table because the wiring stays readable from one value.

Fixes #1060
Refs #973

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@daavoo
daavoo temporarily deployed to integration-tests September 10, 2026 14:15 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@daavoo
daavoo requested review from a team, khaledosman and njbrake and removed request for a team September 10, 2026 14:15
@daavoo

daavoo commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #1087, which is open as a draft with all CI green.

The four seam PRs all touched src/gateway/container.py, so landing them separately meant a four way rebase. #1087 carries them as one change, with the commits from this branch cherry-picked rather than rewritten.

What changed on the way, after reading the real consumer of this seam (the enterprise overlay on otari-ai main, which selects overlay.gateway_bootstrap:register):

  • Migration contributions now pass the database URL as config.attributes["database_url"] as well as sqlalchemy.url, because the overlay's own env.py deliberately avoids the latter: it goes through configparser interpolation and a password containing a percent sign breaks it.
  • The declared version_table is documented as declarative. A contributed chain may hardcode its own constant, as the overlay does; what otari requires is that the declared value is the table the chain stamps, since otari uses it only for collision detection.
  • Granting contributed capabilities and publishing them on /v1/bootstrap is dropped. The overlay already serves the entitlement axis over its own contributed GET /v1/entitlements, which its dashboard resolver reads, so a bootstrap field would have been a second source of truth. Instead RouterContribution.capability becomes optional, and None mounts a router ungated. The overlay had invented a capability named entitlements purely to satisfy the mandatory gate, which is the evidence that the gate should be optional.
  • Several bootstrap selectors in one OTARI_BOOTSTRAP is dropped entirely; see OTARI_BOOTSTRAP loads exactly one module, so two independent extensions cannot coexist #1060 for why.

The branch is left in place rather than deleted.

@daavoo daavoo closed this Sep 11, 2026

This branch was previously deployed

1 inactive deployment
integration-tests — a06e416c Deployed Sep 10, 2026 by daavoo via test-integration #1707
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OTARI_BOOTSTRAP loads exactly one module, so two independent extensions cannot coexist

1 participant