Navigate the docs

For AI agents: the documentation index is at /llms.txt, and every page here has a markdown version at its URL plus .md.

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:

DomainDeclaresLives inParams come from
Service (service.toml [requests].consent)what a brokered REQUEST meansthe service definitionrequest-bound scope_vars (P4: show ⊆ bind)
Product (src/protocol/acts.toml)what a VAULT OPERATION meanscore, next to the act implementationthe 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 bound scope_vars entry.
  • | filter is 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 (core protocol::consent + console interpOp in components/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_ACTS in 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.