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
title → server_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.
- 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.
- 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.
- 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.
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.titlenever reaches the one place a user reads a nametitle→server_name(enlace_connector/connector.py:78-80) reaches:app.tomldisplay_name(scaffold.py:38),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
titlethere. So that string is typed by handsomewhere else, once, and goes stale silently as connectors are added.
render_allowlist_toml(deploy.py:175-188) already solves the identical problem for accesscontrol: it renders a
platform.tomlfragment keyed byresource_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.titleis 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:
server.pyfiles isrender_server_pyoutput (scaffold.py:47-73);all three are hand-written.
render_systemd_unit(deploy.py:124-141) — theycarry hand-written comment blocks and
Description=lines._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.
connector's. The lineage is acknowledged in the source.
deploy.py:15-16says the module "generalizes the artifacts first hand-written for the[first] connector, so deploying the next one is
generate_deploy_bundle(spec)+ following therunbook." 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.
it can attach an icon — a capability
enlace_connectordoesn't expose yet."→ add
icontoConnectorSpec, passicons=throughmake_connector_app.make_connector_app(connector.py:98-120) accepts none, so they call py2mcp'smk_http_appdirectly. py2mcp already takesmiddleware=— surface it on the spec/factory.mod:fntool surfaces. One connector's tools are project-bound closures rather than"module:function"refs, so it must call its ownbuild_http_app; itsSPEC.toolsis[]with a comment explaining why. → let a spec name an app builder (or accept a callable /
a
register_extrahook) instead of only import refs.Until those three exist,
ConnectorSpecis a metadata carrier, the served app is hand-writtenper 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
generate_deploy_bundle(spec)+ the runbook, with zero hand-editedgenerated files.
is derived from its
ConnectorSpec, so there is no place left to type a name a second time.contains any identifier of the other.