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 itsprofile_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
TokenAmountabove about 18.4 whole units. Consumers fall back to aTextfield, never to a truncated number. open Whether to propose anatvariant upstream is OQ-12. -
ICRC-1 timestamps are in nanoseconds. A consumer converts them with the
timestamptag and never passes the raw value as seconds; a conversion that is not exact becomes aTextfield.
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.
Consent text makes no claims about state#
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,adminanddestroycall 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#
Which calls wait for the person, and what the person reads.
Guide Generate consent messagesICRC-21 messages with provenance and, on request, the raw arguments.
Reference Guarantees and verificationWhat is proven, by which gate, and what is explicitly not claimed.