Skip to content

RFC: Structured Auth Ownership Metadata for UTCP (with a Non-Normative Runtime State Pattern) #62

Description

@rnbokade

RFC: Structured Auth Ownership Metadata for UTCP (with a Non-Normative Runtime State Pattern)

  • Status: Draft — seeking community feedback before formal submission
  • Author: @rnbokade
  • Target repo: utcp-specification (schema extension only — see Appendix A for why the runtime pattern is not a repo target)

1. Summary

UTCP's auth_type enum (api_key, basic, oauth2) describes how a credential is transmitted, but not enough for a client to determine, before dispatch, what kind of credential lifecycle a tool needs. It can't distinguish a credential the connector developer provisions once and shares across every caller from one that must be provisioned per end user, and it can't distinguish which OAuth grant produced a given token. Every non-trivial implementer that needs this — multi-tenant agent hosts, anything doing per-user OAuth — ends up hand-rolling a classification on top of the spec, differently each time.

This RFC proposes a small, additive extension to the auth block — ownership and grant_type, added alongside the existing auth_type field — so a manual can describe enough about a credential's provisioning model for a client to set up the right authentication flow before attempting a call. It does not propose that UTCP track runtime credential status or session state; that remains an application concern, consistent with UTCP's stateless design. Appendix A sketches a reference pattern for that runtime layer, for implementers who want a shared starting point, but it is explicitly non-normative and not part of the schema proposal.

2. Problem Statement

UTCP's auth_type enum only describes where a credential goes on the wire. It doesn't capture:

  1. Who owns the credential (connector dev vs. end user) — which determines who provisions/rotates it, and what a client should do when it's missing.
  2. Which OAuth grant applies — the schema has no field for this today, so a manual can't distinguish a shared client-credentials integration from a per-user authorization-code or device-code flow.

Because neither is describable in a manual, every implementer that needs them ends up hand-rolling a classification on top of UTCP — informally, and inconsistently between projects. This is a metadata gap at the manual level; whether a specific tool call is currently usable for a given user or session is a separate, downstream question that an application layer decides using this metadata (see Appendix A) — it isn't something a manual can or should state on its own.

3. Proposal

3.1 Schema addition — two new optional fields, no changes to auth_type

auth_type already mixes different kinds of concepts under one enum — oauth2 is an authorization framework, basic is an HTTP scheme. This RFC doesn't attempt to normalize that taxonomy; it treats auth_type as an established compatibility surface and adds metadata around it, rather than redefining what it means or changing its enum.

"auth": {
  "auth_type": "oauth2",
  "ownership": "user",
  "grant_type": "authorization_code",
  "token_url": "...",
  "scopes": ["..."]
}
  • ownership (new, optional): the principal a given credential instance is provisioned for and whose lifecycle governs it. This is narrower than it might sound — it does not describe who owns an underlying OAuth client application, and it does not describe the OAuth resource owner. It's specifically about the credential instance's provisioning and lifecycle. Two values for v1:

    • static: one credential, provisioned by the connector/tool developer, shared across every caller.
    • user: a separate credential provisioned per end user.

    Finer-grained models — organization-owned, per-tenant, delegated or impersonated credentials — aren't covered by this two-value split. That's a deliberate v1 scope limit, not an oversight; see Open Questions.

  • grant_type (new, optional, applies when auth_type is oauth2): client_credentials | authorization_code | device_code | jwt_bearer, nested under oauth2 rather than promoted to a top-level auth_type value, so new flows don't require new top-level enum values. grant_type identifies which authorization mechanism produced the credential — it's a provisioning-model label, not a complete OAuth flow description. It doesn't carry everything needed to execute that flow: authorization endpoints, PKCE, redirect handling, and client registration are out of scope and remain resolved elsewhere (e.g. token_url plus provider docs, as today).

Field Required Applies to Default when omitted Meaning
auth_type existing, required all auth Authentication mechanism (existing field, unchanged)
ownership no all auth static Principal the credential is provisioned for and lifecycle-bound to
grant_type no auth_type: oauth2 only client_credentials OAuth flow that produced the credential

Both fields are optional and additive, and neither introduces a new auth_type value. A client that doesn't act on them can ignore them like any other unrecognized key, and nothing changes in how any client currently reads or validates auth_type.

4. Compatibility

Fully backward compatible. ownership and grant_type are new optional fields, not new values on the auth_type enum, so this proposal introduces no risk even for implementations that validate auth_type as a closed enum — that enum doesn't change. A client that ignores unrecognized keys, which is the baseline UTCP already assumes, behaves identically whether or not a manual includes these fields.

5. Alternatives Considered

  • Do nothing, leave it application-specific. The status quo, and precisely what produces the fragmentation this RFC responds to — every implementer handling per-user OAuth converges on some version of this split anyway, just inconsistently.
  • Put the full manager interfaces into the spec as required behavior. Rejected — this would turn UTCP into a stateful platform spec rather than a calling-convention spec, undermining the design goal that differentiates it.
  • Skip the schema too, keep everything external. Viable, but forgoes the low-cost, high-leverage part of the fix — ownership/grant_type are cheap to standardize and immediately useful to any client building the Appendix A pattern instead of guessing at conventions.
  • Rename auth_type to credential_shape and demote it to a pure wire-mechanics field. The conceptually cleanest version of a two-axes split. Rejected because it breaks every existing reader of auth_type — contradicting the additive, backward-compatible goal this RFC is built around. Adding sibling fields gets most of the same descriptive power without the breakage.
  • Keep ownership/grant metadata entirely outside the manual, in a policy overlay, with a resolver wrapping existing UTCP calls. A policy file keyed by provider/tool ID carries ownership/grant_type; a resolver reads manual and overlay together, running the Appendix A cascade before delegating to an unmodified UTCP client. Zero spec changes, works against any manual today — a fast way to prototype and validate the idea. Set aside as the main proposal because policy and manual can drift out of sync with nothing enforcing they match, every deployment maintains its own overlay copy, and the ecosystem never converges on shared vocabulary. Still a reasonable way to validate ownership/grant_type before or instead of a schema change; the resolver/overlay pattern is reusable regardless of whether §3.1 is adopted.
  • Nest credential metadata under auth_type — e.g. {"auth_type": "oauth2", "credential": {"ownership": "user", "grant_type": "authorization_code"}}. Separates wire mechanics from credential metadata more cleanly than flat sibling fields, and is arguably the better long-term shape given this RFC's own case that ownership and wire mechanics are different concerns. Not proposed here because it restructures the existing auth object for every consumer rather than just adding keys — a larger compatibility footprint for a comparable descriptive gain. Worth revisiting if the ecosystem wants a deeper auth-schema normalization later.
  • Extend auth_type itself (e.g. mtls, signature) alongside the ownership/grant metadata. Considered, since a broader wire-mechanics taxonomy was part of earlier framing of this proposal. Deferred here: it doesn't serve the ownership/grant-type problem this RFC targets, and bundling an enum extension with an additive metadata proposal adds review surface — and a closed-enum compatibility question — for no benefit to the core ask. Left as a separate, future proposal if there's appetite for it.

6. Open Questions for Discussion

  1. Does ownership: static | user cover real cases, or are there ownership models this misses (e.g. org-owned-but-not-shared, delegated/impersonation credentials)?
  2. Is defaulting omitted ownership to static right, or should omission mean "unspecified" and force explicitness once user-owned flows are in play?
  3. Should ownership generalize to non-OAuth mechanisms UTCP might describe in the future (e.g. per-user vs. shared client certificates under a hypothetical mtls auth type)?
  4. Is there appetite to converge on the Appendix A pattern even informally, or does each project's internal version differ enough that a shared pattern wouldn't reduce duplicated effort?

Appendix A: Reference Pattern for Runtime State (Non-Normative)

Not part of the proposal in §3 and not intended for inclusion in utcp-specification. This documents one way ownership and grant_type can be consumed at runtime — a shared starting point for implementers, not a requirement, since the fragmentation in §2 already exists in practice regardless of whether this RFC is adopted.

  • CredentialManager — account-level, persists across sessions, keyed by (provider_id, user_id). Exposes get, status (Valid | Missing | Expired | Revoked | Error), refresh, revoke. Error means the manager attempted to determine validity and couldn't reach a reliable conclusion — e.g. the introspection or refresh call itself failed — distinct from Missing (no credential on record) or Revoked (a definite negative answer).

  • ProviderStateManager — session-level, ephemeral, keyed by (provider_id, session_id, tool_id?). Tool-level granularity so partial-scope providers (e.g. Gmail-read enabled, Gmail-send disabled) aren't forced into one rollup state. Where provider- and tool-level settings both exist, the tool-level setting wins for enabling a tool; a provider disabled at the provider level disables all its tools regardless of individual settings.

  • Effective tool state, derived rather than stored directly:

    resolve(provider, tool, session, user):
      if CredentialManager.status(provider, user) != Valid:
          return AuthRequired(reason=status)   # Missing / Expired / Revoked / Error, distinct UX for each
      if not ProviderStateManager.get_enabled(provider, session, tool):
          return Disabled
      return Enabled
    

    This keeps "never connected," "token expired," "revoked," and "toggled off for this session" distinguishable — a flat Enabled/Disabled/AuthRequired result collapses all of them.

This pattern directly consumes the fields proposed in §3.1: knowing a credential is user-owned via authorization_code is what tells a CredentialManager which provisioning flow to trigger when status comes back Missing.

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