Skip to content

feat(providers): add optional AccountCredentialField for self-describing plugins#131

Merged
hyoshi merged 1 commit into
mainfrom
feat/provider-account-credential-fields
May 22, 2026
Merged

feat(providers): add optional AccountCredentialField for self-describing plugins#131
hyoshi merged 1 commit into
mainfrom
feat/provider-account-credential-fields

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 22, 2026

Summary

Provider plugins (built-in google_ads / meta_ads, and third-party plugins discovered via the mureo.providers entry-point group) can now declare an optional account_credential_fields: tuple[AccountCredentialField, ...] class attribute so introspection tooling — the mureo providers … CLI, configuration wizards, plugin authoring guides — can render setup prompts, validate config, and document plugins without hardcoding per-provider knowledge.

Plugins declare their per-account credential needs so tooling can render setup prompts, validate config, and document plugins without hardcoding per-provider knowledge. Existing providers without this declaration behave unchanged.

from mureo.core.providers import AccountCredentialField

class MyAdsProvider:
    name = "my_ads"
    display_name = "My Ads"
    capabilities = frozenset({...})
    account_credential_fields = (
        AccountCredentialField(
            key="advertiser_id",
            display_name="Advertiser ID",
            placeholder="adv-12345",
            required=True,
            description="From the MyAds dashboard.",
        ),
    )

Public surface added to mureo.core.providers

Name Kind Purpose
AccountCredentialField frozen dataclass One per-account credential descriptor: key, display_name, placeholder="", required=False, description="". Primitive str / bool only so dataclasses.asdict is JSON-serialisable.
get_account_credential_fields(provider) function Defensive accessor — returns () for providers that do not declare the attribute, raises TypeError when the declaration is malformed (non-tuple or wrong element type).

BaseProvider Protocol body is unchanged. The new attribute is documented as optional in the BaseProvider docstring and is read via getattr so @runtime_checkable isinstance() results do not flip for any existing plugin.

Built-in adapter updates

  • mureo.adapters.google_ads.GoogleAdsAdapter declares customer_id (placeholder "123-456-7890", required). MCC login_customer_id is intentionally excluded — it identifies the operator's manager account and is shared across every Google Ads account the operator manages (operator-shared OAuth-level credential, not per-account).
  • mureo.adapters.meta_ads.MetaAdsAdapter declares ad_account_id (placeholder "act_1234567890", required). Token / app id / app secret are operator-shared in the common Business Manager setup and live outside this declaration.

Backward compatibility

  • Plugins that do not declare account_credential_fields continue to register and discover unchanged; get_account_credential_fields() returns (), which downstream tooling treats as "no per-account configuration needed."
  • The BaseProvider Protocol body is unchanged so @runtime_checkable isinstance(obj, BaseProvider) keeps returning the same value for every existing plugin.
  • get_account_credential_fields validation raises TypeError only when a plugin actively declares a malformed attribute — pure additive footprint for everyone else.

Plugin author docs

docs/plugin-authoring.md §3 (BaseProvider) gains a Declaring per-account credential fields (optional) subsection covering the dataclass shape, default values, and the accessor's defensive-read / strict-validation semantics — plus the operator-shared / per-account credential split shown above.

Test plan

  • CI lint (ruff check mureo/, black --check mureo/) — clean locally
  • CI tests (Python 3.10 / 3.11 / 3.12 + Windows) — 11 new unit tests + 2 import-allowlist updates; 394 tests pass across tests/core/providers/ + tests/adapters/
  • CodeQL — no new findings expected; net additions are pure-Python @dataclass(frozen=True) + a defensive getattr-based accessor. No eval / subprocess / shell / network.
  • Manual: drop into a Python shell, import from mureo.core.providers import get_account_credential_fields, AccountCredentialField, run get_account_credential_fields(GoogleAdsAdapter) and confirm it returns the declared tuple.
  • Manual: install a pre-feature third-party plugin (anything that does not declare account_credential_fields); confirm discovery still loads it and get_account_credential_fields(...) returns ().

…ing plugins

Provider plugins (built-in `google_ads` / `meta_ads`, and third-party
plugins discovered via the `mureo.providers` entry-point group) can
now declare an optional `account_credential_fields:
tuple[AccountCredentialField, ...]` class attribute so introspection
tooling — the `mureo providers …` CLI, configuration wizards, plugin
authoring guides — can render setup prompts, validate config, and
document plugins without hardcoding per-provider knowledge.

The benefit for OSS-only operators is concrete: after installing a
new plugin, an introspection tool can show "this plugin requires
these per-account fields" without the operator (or the tool itself)
needing to know what the plugin needs ahead of time.

Public surface:

* `mureo.core.providers.AccountCredentialField` — frozen dataclass
  with `key`, `display_name`, `placeholder=""`, `required=False`,
  `description=""`. Primitive `str` / `bool` only so
  `dataclasses.asdict` is JSON-serialisable for tooling that surfaces
  the descriptor over an HTTP boundary.
* `mureo.core.providers.get_account_credential_fields(provider)` —
  defensive accessor. Returns `()` when the optional attribute is
  absent (pre-feature plugins keep loading unchanged) and raises
  `TypeError` when present-but-malformed (non-tuple, or any element
  that is not an `AccountCredentialField`) so the failure surfaces
  near the plugin, not deep inside the consuming UI.

`BaseProvider` Protocol body is unchanged. The new attribute is
documented as optional in the `BaseProvider` docstring; the Protocol
itself stays stable so `@runtime_checkable` `isinstance()` results
do not flip for any existing plugin.

Built-in adapters updated:

* `mureo.adapters.google_ads.GoogleAdsAdapter` declares
  `customer_id` (placeholder `"123-456-7890"`, required). The MCC
  `login_customer_id` is intentionally excluded — it identifies the
  operator's manager account and is shared across every Google Ads
  account the operator manages (operator-shared OAuth-level
  credential, not per-account).
* `mureo.adapters.meta_ads.MetaAdsAdapter` declares `ad_account_id`
  (placeholder `"act_1234567890"`, required). Token / app id / app
  secret are operator-shared in the common Business Manager setup
  and live outside this declaration.

Docs:

* `docs/plugin-authoring.md` §3 (`BaseProvider`) gains a *Declaring
  per-account credential fields (optional)* subsection covering the
  dataclass shape, default values, and the accessor's defensive-read
  / strict-validation semantics — plus the operator-shared /
  per-account credential split shown above.

Tests:

* `tests/core/providers/test_account_credential_field.py` (4 tests)
  — construction, frozen-ness, defaults, `asdict` is JSON-friendly.
* `tests/core/providers/test_registry.py` (5 new tests) — empty
  return when attr absent, declared tuple read-through, `TypeError`
  on non-tuple, `TypeError` on wrong element type, public re-export
  from `mureo.core.providers`.
* `tests/adapters/google_ads/test_adapter.py` +1 — `customer_id`
  declaration shape.
* `tests/adapters/meta_ads/test_adapter.py` +1 — `ad_account_id`
  declaration shape.
* `tests/adapters/{google_ads,meta_ads}/test_imports.py` — adapter
  import allowlist gains `mureo.core.providers.credentials`.

Backward compatibility: 394 tests pass across `tests/core/providers/`
and `tests/adapters/`, including the `base.py` foundation-rule AST
scan and the adapter import allowlist. Existing plugins that do not
declare `account_credential_fields` continue to register and discover
unchanged. ruff and black clean on `mureo/`.
@hyoshi hyoshi marked this pull request as ready for review May 22, 2026 05:43
@hyoshi hyoshi merged commit 1414901 into main May 22, 2026
9 checks passed
@hyoshi hyoshi deleted the feat/provider-account-credential-fields branch May 22, 2026 05:43
@hyoshi hyoshi mentioned this pull request May 22, 2026
3 tasks
hyoshi added a commit that referenced this pull request May 22, 2026
…tial_fields (#131) (#132)

See CHANGELOG.md for details.

Provider plugins can now declare an optional
`account_credential_fields: tuple[AccountCredentialField, ...]`
class attribute so introspection tooling — the `mureo providers …`
CLI, configuration wizards, plugin authoring guides — can render
setup prompts, validate config, and document plugins without
hardcoding per-provider knowledge.

`BaseProvider` Protocol body is unchanged (`getattr`-based defensive
read), and built-in adapters now self-describe: `GoogleAdsAdapter`
declares `customer_id`, `MetaAdsAdapter` declares `ad_account_id`.

Backward compatibility: plugins that do not declare
`account_credential_fields` continue to register and discover
unchanged; `get_account_credential_fields()` returns `()` for them.

Version bumped to 0.9.7 across .claude-plugin/plugin.json,
mureo/__init__.py, and pyproject.toml.
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.

1 participant