Skip to content

ConnectorSpec.title never reaches the OAuth consent screen — and every live connector bypasses scaffold_app / generate_deploy_bundle #3

Description

@thorwhalen

A user authorizing one connector on a live enlace deployment was shown a different
connector's product name. The string itself lives in the shared authorization server
(i2mint/enlace_auth#18). This issue is the other half: why a per-connector display name was
hand-written in the first place, and why it will happen again.

A. ConnectorSpec.title never reaches the one place a user reads a name

titleserver_name (enlace_connector/connector.py:78-80) reaches:

  • the enlace app.toml display_name (scaffold.py:38),
  • the systemd unit Description= (deploy.py:128).

Both are operator-facing. The only name an end user reads during the connect flow is the
OAuth consent screen — and nothing carries title there. So that string is typed by hand
somewhere else, once, and goes stale silently as connectors are added.

render_allowlist_toml (deploy.py:175-188) already solves the identical problem for access
control: it renders a platform.toml fragment keyed by resource_url(spec)
(deploy.py:103-105). The display name needs exactly the same treatment — a sibling renderer
(or one more line in the same fragment) keyed by the same resource URL, so ConnectorSpec.title
is the single source and the platform config is derived, not authored.

Pairs with i2mint/enlace_auth#18, which adds the consuming config key on the auth-server side.

B. Why it recurs: the factory exists and is bypassed by every live connector

Verified against the three connector app dirs on the deployment:

  • None of the three server.py files is render_server_py output (scaffold.py:47-73);
    all three are hand-written.
  • The shipped systemd units do not match render_systemd_unit (deploy.py:124-141) — they
    carry hand-written comment blocks and Description= lines.
  • The shipped provision scripts do not come from _PROVISION_TEMPLATE (deploy.py:48-80).
    They are near-identical to each other, differing essentially only in the connector name
    — i.e. copy-pasted between connectors rather than rendered from a spec.
  • One connector's module docstring states outright that it is "modelled on" another
    connector's. The lineage is acknowledged in the source.

deploy.py:15-16 says the module "generalizes the artifacts first hand-written for the
[first] connector, so deploying the next one is generate_deploy_bundle(spec) + following the
runbook." That generalization shipped — and then nothing used it.

Each bypass has a real cause: a capability the factory does not expose. These are the
actual work items.

  1. Server icons. One connector's docstring says verbatim that it builds FastMCP itself "so
    it can attach an icon — a capability enlace_connector doesn't expose yet."
    → add icon to ConnectorSpec, pass icons= through make_connector_app.
  2. Middleware. Two connectors need auth/metering middleware on the ASGI app;
    make_connector_app (connector.py:98-120) accepts none, so they call py2mcp's
    mk_http_app directly. py2mcp already takes middleware= — surface it on the spec/factory.
  3. Non-mod:fn tool surfaces. One connector's tools are project-bound closures rather than
    "module:function" refs, so it must call its own build_http_app; its SPEC.tools is []
    with a comment explaining why. → let a spec name an app builder (or accept a callable /
    a register_extra hook) instead of only import refs.

Until those three exist, ConnectorSpec is a metadata carrier, the served app is hand-written
per connector, and every user-visible string is hand-maintained in N places. The consent-screen
name is simply the first one that surfaced to a user; it is not the only one that can drift.

Acceptance

  • Adding a connector is generate_deploy_bundle(spec) + the runbook, with zero hand-edited
    generated files.
  • Every user-visible string for a connector — server name, consent-screen display name, icon —
    is derived from its ConnectorSpec, so there is no place left to type a name a second time.
  • A test that renders the bundle for two different specs and asserts no artifact of one
    contains any identifier of the other.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions