Cross-backend parity suite for an off-grammar role on the grant verbs.
role_grant_assign and role_grant_offer_create declare role as
RoleName (lowercase letters and underscores, no leading or trailing
underscore, at most 64 characters). A role string outside that grammar is refused at input
validation — JSON-RPC invalid_params, with no data.reason — and writes
no audit row: a role is caller-chosen text until it passes the grammar, and
the grant verbs record the role they refuse in their failure rows.
- refused at validation — each off-grammar role, on each verb, answers
invalid_params with no reason, so the refusal is the input schema's and
not a grant-path or authorizer one. One of them is all lowercase letters
and one character past the length cap: the cap is part of the grammar, so
a failure row never carries an arbitrarily long caller-chosen string. - no audit row — after the refusals, no grant audit row carries an
off-grammar role. The control keeps that read from being vacuous: an
on-grammar role no registry holds is refused past validation with
role_not_web_grantable, the refusal a failure row records — as is one
exactly at the length cap, which pins the boundary.
Exists because the action-manifest parity gate captures no param schemas
while the Rust spine parses params by hand — a spine that let an off-grammar
role reach its grant path would answer with a different error and write the
caller's text into the audit log. Both verbs are on every spine's standard
surface, so the suite is ungated. Cross-process only: it reads the audit log
through _testing_drain_effects, which the spine binaries mount.