Consent templates: the ONE grammar for approval copy
This page defines the reviewed consent templates used by SafeClaw approval surfaces.
Two authorship domains, one grammar, one renderer contract:
| Domain | Declares | Lives in | Params come from |
|---|---|---|---|
Service (service.toml [requests].consent) | what a brokered REQUEST means | the service definition | request-bound scope_vars (P4: show ⊆ bind) |
Product (src/protocol/acts.toml) | what a VAULT OPERATION means | core, next to the act implementation | the signed op's target / scope fields |
Every approval surface (grant page, CLI prompt, audit row) renders static reviewed template ⊕ signed op bytes: requester-authored prose can never reach an approval surface (ERC-7730 / RFC 9396 shape).
Grammar
{{ vars.<path> | <filter> }}
- acts.toml paths:
vars.target→op.act.target;vars.scope.<key>→op.act.scope[<key>](one level, no deep paths). - requests paths:
vars.<name>→ the boundscope_varsentry. | filteris display-only. Renderers MUST accept and MAY ignore any filter. Currently defined:basic(requests only, git credential preview). Adding a filter requires updating BOTH renderers (coreprotocol::consent+ consoleinterpOpincomponents/approvals/approval-view.tsx) and the shared vectors below.
Value rendering (P3 rules: both renderers MUST match)
- undefined / missing / null → empty string
- string → verbatim; number → decimal
true→yes;false→ empty- array → string elements joined with
", " - object → its distinct string values, in key order, joined with
", "(the connection-add card's slot → KEY maps render as their KEYs) - a FACT row whose value renders empty is omitted entirely
- values are truncated (core: 120 chars +
…)
acts.toml descriptor fields
action (+action_zh): verb-object title, doubles as the approve button
label. explain (+explain_zh): one sentence, ALWAYS rendered inline.
tone: neutral | info | danger; advisory upward-only (clients keep a
built-in danger floor per act kind; a catalog can raise tone, never lower).
facts: {label, label_zh?, value} rows; the Connection label maps onto
the console's "through" line. A fact renders only daemon-stamped or
daemon-checked fields, never a requester-supplied list: connection-add's
facts read the card (written, referenced, overwrites, destinations,
replaces), not secrets.
service-add's facts read its card (ids, from, destinations, oauth,
new_destinations, loosens, tightens, replaces, removes) and
service-rm's (ids, from, connections), not the definitions. from
states the source as a fact only when the daemon fetched it again
(source_check = fetched); otherwise it says the requester stated it.
Drift gates
consent::tests::dispatch_and_table_agree: every dispatched custom act has a descriptor and vice versa (DISPATCHED_CUSTOM_ACTSin approve.rs).- Shared vectors:
consent::tests(renders_action_facts_and_p3_rules,bool_and_missing_vars_render_per_p3,filters_accepted_and_ignored) are the normative examples; a console renderer change must keep them true.
Distribution
sc registry --json (the published SSoT catalog, opt-in like policy rules) →
CI registry.json → backend /registry passthrough → console. Version skew
falls back: no descriptor → humanized slug card, never a raw debug dump.