#26594·cosmos-sdk

Declarative internal-only messages (governance/module dispatch only, reject direct submission)

Author: aljo242Created Jul 13, 2026Updated Jul 13, 2026

Summary

Add a first-class, declarative way to mark a Msg as internal-only: dispatchable through governance or module routing (x/gov, x/group, inter-module calls) but rejected when submitted as a top-level message in a user-signed transaction.

Motivation

Many messages are meant to be executed only by governance or a module, never by a direct user tx (MsgUpdateParams, software-upgrade scheduling, and gov-to-EVM execution like https://github.com/cosmos/group-evm-demo). Today each handler hand-rolls the same guard (msg.Authority == k.authority). It is repetitive, and forgetting it silently exposes a privileged handler. A declarative marker enforced by the framework removes the boilerplate and closes that bug class.

Proposal

A build-time proto annotation, for example (cosmos.msg.v1.internal) = true. Default is external, so the change is additive and backward compatible. Enforcement lives at the external transaction boundary (baseapp's top-level runMsgs, or a distinct HandlerExternal entry on the router), not in the shared MsgServiceRouter, since gov/group and user txs currently reach the same router.Handler.

Key subtlety: authority origin, not call site

"Internal" must mean the authorizing principal is not a user key, not merely "not the top level of a tx." Delegation paths (x/authz MsgExec, and any user-initiated dispatch) must also refuse internal messages, or they become a bypass. This mirrors what keyless authority accounts (module accounts, group policies) already enforce implicitly through ante signature rejection; this proposal makes it explicit.

Static vs configurable

Recommend static (build-time). Runtime or governance-toggling of the flag is a foot-gun: one bad vote flips a security boundary to externally callable.

Sketch

proto
message MsgUpdateParams {
  option (cosmos.msg.v1.internal) = true;   // additive; default is external
  string authority = 1;
}

Enforcement: reject internal messages at the external tx boundary (baseapp runMsgs or a HandlerExternal), and in user-initiated dispatch (x/authz MsgExec). Internal routing (gov/group/module) is allowed. Load-bearing point: "internal" means the authority origin is non-user, not "not top-level."

Open questions

  • Annotation vs registry flag; where the check should live (baseapp vs router).
  • Exact policy for x/authz and future inter-module dispatch.
  • Whether to surface the flag in reflection and tooling so clients and explorers can see which messages are internal.