Profiles

Security model

What a profile can and cannot vouch for, and the rules that keep a claim from becoming a loss.

Every word in that preview came from a profile, and a profile can lie. Its author can call a transfer to their own account "Claim reward", and the consent message will say so in the canister's own format. Meant does not try to make a dishonest profile impossible; no description format can. It makes sure a profile cannot turn a lie into a loss quietly: the values a call carries are exact and shown, a profile is bound to one interface, every tag says whose it is, and the calls that move value wait for the person. This page is the spec's list of what a profile cannot vouch for (spec §11), and the rule behind each limit.

A profile is a claim, not a proof#

An author profile can describe a drain as "claim reward". Ethereum's clear-signing format, ERC-7730, has the same limit, and adds ERC-8176 attestations, in which a reviewer vouches for a file, to address it. v0 has two mitigations of its own:

  • Provenance on every tag. Each tag in the resolved form carries the origin (author, third_party, pack, inferred) and the name of the profile it came from, and the resolved form lists every applied profile with its profile_id. The consent generator returns the same provenance beside each message, so a signer can warn when a profile, not the canister, wrote the words.
  • Binding to interface_id. A canister profile describes one callable interface. When the interface changes and the profile no longer fits, it is stale and loses every tag that converts a value.

Consumers should show the decoded raw arguments next to the rendered intent for every call whose effect is transfer, approve, admin or destroy, as in the preview above. The signer that displays a consent message is the consumer that should show them; @meantjs/consent includes them on request. A profile's effect is a claim too. The resolver checks what it can: a query cannot be declared anything but a read (EE_EFFECT_MODE), and a read on an update method is warned (WW_EFFECT_MODE). It cannot check that an update changes nothing, so the read-only update form, which gets no warning, rests on the profile's provenance alone.

Dynamic values come from one replica#

fact A plain query's reply comes from a single replica and is signed only by that node, while an update's reply comes with a certificate of the subnet's state (IC interface specification, HTTPS interface). So a malicious replica can answer icrc1_decimals with a wrong number, and every amount shown for that ledger is then wrong by a power of ten.

Consumers should resolve the dynamic values used to render a transfer or approve intent through an update call or another certified path, or cross-check them with icrc1_metadata. A consent message generated inside the canister avoids the problem, because the canister knows its own decimals. open Whether the resolved form should carry resolved dynamic values with their provenance, certified or not, is OQ-10.

Nothing is rounded or truncated#

No value conversion anywhere rounds or truncates. Excess precision is refused, out-of-range values are refused rather than wrapped or clamped, and a time that does not fit its unit is refused (Values and exact conversion). The same rule holds where a message would be long: a consent message for a call without an intent, one row per argument leaf, is refused above 64 rows rather than cut short, and a formatter that cannot be built (an unresolved dynamic value, a bad tag argument) produces no message at all, so a number is never shown without its decimals.

Rendered values are escaped#

A template is the author's text; the values in it come from the call. Rendered values are escaped for the target format, so a memo of [Approve now](https://evil.example/drain) is shown as those characters, not as a link (Intent, effects and confirmation).

ICRC-21 field limits#

fact ICRC-21's TokenAmount.amount is a nat64, and TimestampSeconds counts seconds (spec §14, verified 2026-10-08). Two consequences:

  • A token with 18 decimals does not fit TokenAmount above about 18.4 whole units. Consumers fall back to a Text field, never to a truncated number. open Whether to propose a nat variant upstream is OQ-12.
  • ICRC-1 timestamps are in nanoseconds. A consumer converts them with the timestamp tag and never passes the raw value as seconds; a conversion that is not exact becomes a Text field.

Work is bounded#

Profiles and interfaces can come from anyone, so resolving them should run under a budget, as compiling an untrusted .did does (Limits, budgets and diagnostics). The reference packages bound their own work: the subtype check behind pack matching stops at depth 256 and 2,000,000 pair evaluations and answers "not a subtype" beyond them, the format library reads at most 1024 characters of input (4 MiB for bytes), and a served profile is at most 1 MiB.

Names are untrusted display text#

Whoever may name an account, or join with a label, chooses the text. It can look like an address, mimic another entry, or press the reader ("approved by the CEO"). The rules of Names keep a name from ever replacing a value: a value always wins over a name, an ambiguous name fails rather than picks one, and the value is shown beside the name. Names read through a plain query come from one replica, as dynamic values do.

A served profile is certified, not vouched for#

A canister can serve its own profile in the public custom section icp:public candid:profile (Serve a profile from a canister). fact The section's content is in the canister's certified state, and a read_state reply for it is a certificate of the subnet's state, unlike a query's reply (spec §8.1, from the IC interface specification). The certificate proves that the canister serves these bytes, so no replica or boundary node can change them.

It proves nothing about the words. A canister can serve a profile that calls a drain "claim reward", and the controller that installs the module chooses it. A served profile is an author profile, with the trust of one.

An EVM pack matches any contract with the right functions#

EVM profiles are experimental (EVM contracts). An EVM pack applies to any contract whose functions match it, so a hostile contract can match ERC-20, have its drain described as "Send tokens", and answer "USDT" to symbol(). A consumer must show the contract's address, with its EIP-55 checksum, whenever it asks a person to confirm a call matched by a pack; the consent text @meantjs/consent/evm writes ends with it. fact ERC-7730 names the same risk in its security considerations, as "parameter injection using legitimate, high-profile contract formats" (ERC-7730). A query: value on an EVM contract is an eth_call answered by one RPC node, as one on a canister is answered by one replica. open Whether EVM packs should apply only to deployments a consumer lists is item O4 of the EVM binding.

Confirmed terms authorize in advance#

A person can confirm terms for one task, once, and a request whose grant fits them may then be signed or sent without a second confirmation (Confirm terms for a task). Whoever confirms terms authorizes every request that fits them, so the confirmation must come from the person, on the wallet's own screen, never through a tool the agent can call. The terms must be shown exactly: the full address, the amount without rounding, and "unlimited" or "never ends" written out when the terms say so. termsText of @meantjs/consent/terms writes them that way:

fact Observed 2026-10-29, in the everyday-wording test of Meant's signing guard (spec §11 item 10): small models that wrote the terms from a user's everyday words filled a term the user had left out with the loosest value ("unlimited", "never"), and a check against those terms passed requests the user never asked for; the check itself held every time. The terms the agent writes are the weak point, not the comparison, which is why the person confirms their exact text.

A grant is the profile's claim, like the intent in the first section, and terms cover what a grant says. A request outside its grant's words, another call or another chain, is checked by the rest of the consumer, as without terms.

A consent message describes what the call does with its arguments, never what the canister's state will be: no "your balance after this is…". ICRC-21 requires the message to describe every possible outcome, because time passes between fetching the message and submitting the call. Templates are written under that rule, and @meantjs/consent has no network access and states nothing about ledger state (Generate consent messages).

What v0 does not do yet#

These are open, and the spec says so. Each is on Open questions with its owner.

  • open Third-party attestations for profiles (OQ-7), so that trust can rest on more than the author's claim, and where a registry of them lives. Planned for v1. The ERC-7730 export already gives a reviewer the file's ERC-8176 hash and the unsigned attestation to sign; checking attestations is a later phase.
  • open Spending policies and budgets (OQ-8) that would let an agent commit some calls without a confirmation each time. Until then every transfer, approve, admin and destroy call waits for the person, except the one request of a task whose grant fits terms the person confirmed for it.
  • open Resolved dynamic values with provenance (OQ-10), so a consumer can tell a certified decimals value from a single replica's.

Next#