Declarative internal-only messages (governance/module dispatch only, reject direct submission)
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
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/authzand future inter-module dispatch. - Whether to surface the flag in reflection and tooling so clients and explorers can see which messages are internal.
Source: cosmos/cosmos-sdk