Home-Mandate Specification v0 (Draft)
Auf GitHub ansehen(öffnet eine andere Website in einem neuen Tab)Status: Working draft. From v0.1.0 on, the v0 formats are frozen; incompatible changes
then require a new major version (Section 14).
License of this document: CC BY 4.0 (LICENSE-docs). Schema, examples, conformance cases and
code: Apache 2.0 (LICENSE).
Reference evaluator: the code in this repository. Changes are listed in the Changelog at the end.
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT" and "MAY" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
1 Goal
#A vendor-neutral format that describes what a software agent may do on behalf of a household, and an evaluation rule that yields the same result in every implementation. The specification builds on existing standards and defines only what is missing:
| Purpose | Standard |
|---|---|
| Decision interface | OpenID AuthZEN Authorization API 1.0 |
| Transport of the mandate in the OAuth flow | Rich Authorization Requests (RFC 9396), authorization_details |
| Signatures | JSON Web Signature (RFC 7515) with EdDSA (RFC 8037) or ES256, keys as JWK (RFC 7517) |
| Agent identity | OAuth 2.1 client ID (for MCP: Client ID Metadata Document) |
| Device vocabulary | own categories (vocabulary/v0.json); informative mappings to platforms in profiles/ |
Newly defined are: the mandate data model, the third decision ask, conditions,
protection classes and the evaluation rule.
1.1 Overview (informative)
#An agent never talks to a device directly. It sends its request to the implementation, which decides with the household's mandate and only then passes a command on to the smart-home platform. A human sets the mandate and confirms what the mandate leaves to a human.
Solid lines are interfaces this specification defines. Dashed lines are left to the implementation: the specification states what must hold there, not how it is achieved (Section 11).
- Agent → PEP. This specification does not prescribe the protocol in which an agent
sends its requests. It fixes what the agent may contribute (only the resource and the
action, Section 4) and how the agent is named (
agent.client_id, Section 3.3). The reference implementation for Home Assistant offers the Model Context Protocol (MCP): the agent calls tools over HTTPS and signs in with OAuth 2.1; its OAuth client ID is the agent identifier. Other implementations may offer a different interface. - PEP → PDP. The PEP asks the PDP for a decision in the form of AuthZEN (Section 6).
Both can run in one process; an implementation of the class
pdp(Section 8.1) offers the interface over HTTP. - PEP → platform. The PEP talks to the platform through the platform's own API. It
takes category, areas and critical marking from its own resource directory, never from
the agent (Section 4). The profiles in
profiles/relate platform devices to the vocabulary (informative). - Human. The household chooses the mandate (Sections 3 and 3.5), confirms
askdecisions (Section 11.1) and can revoke a mandate or stop all agents at once (Section 11.3). How a confirmation reaches a human (an app, a notification, a dashboard) is left open. - Audit log. Every decision and every change to a mandate is recorded (Section 9).
A mandate that leaves the implementation, for example between an issuer and a separate PDP, travels signed (Section 7).
2 Terminology
#- Principal: the household or person on whose behalf the agent acts.
- Agent: software that requests actions, identified by its OAuth client ID.
- Resource: a single device or entity, named by an identifier from the resource directory of the implementation.
- Resource directory: what the implementation knows about the resources of the household: for each identifier its category and, optionally, its areas (Section 3.4).
- Action: what is to be done with the resource, taken from the vocabulary of its category.
- Decision:
allow(execute immediately),ask(a human must confirm),deny(reject). - PDP (policy decision point): the part of an implementation that evaluates a request against the mandates (Section 4) and returns the decision.
- PEP (policy enforcement point): the part of an implementation that receives the agent's requests, asks the PDP and enforces the decision: it passes allowed commands on to the platform, obtains approvals and keeps the audit log (Sections 9 and 11).
- Audit mode: a mandate with
enforce: falseis tried, not enforced: its rules are evaluated and recorded, but a denial by the rules of an action that is not critical does not stop the agent; where a rule asks for a confirmation, a human still confirms (Section 4.6). - Template: a mandate draft that an implementation stores and from which it creates mandates. This version defines no format for templates; the audit log records changes to them (Section 9.2).
3 Data model
#Machine-readable: schema/mandate-v0.schema.json. The schemas in schema/ are normative;
this text and the schemas MUST agree, and any contradiction is an error in the
specification.
Patterns. A pattern in the schemas applies to the entire string: a string matches only
if the pattern matches from its first to its last character. In particular, a trailing line
feed is not ignored. Regular expression dialects differ (ECMA-262, RE2, PCRE and others), so
the schemas use only constructs that mean the same in all of them: literal ASCII characters,
explicit character classes and ranges, groups, alternation, quantifiers, and ^ and $ as
the first and last character. They do not use \s, \d, \w, \b, a bare ., flags or
lookarounds. An implementation MAY use any regular expression engine or none, as long as it
accepts exactly the strings the patterns describe.
Summary:
Mandate
├── type "https://home-mandate.org/mandate/v0"
├── id unique ID
├── principal "household:<id>" or "person:<id>"
├── agent { client_id, display_name } client_id: see Section 3.3
├── enforce? only false: audit mode (Section 4.6); absent: enforced
├── rules[] rule
│ ├── id
│ ├── resource selector: entity_id | category | area (at least one) or any: true alone
│ │ entity_id and area are opaque identifiers, see Section 3.4
│ ├── actions[] actions from the vocabulary or "*"
│ ├── decision allow | ask | deny
│ ├── conditions? { time_window, weekdays }
│ ├── constraints? { <parameter>: { min, max } or { one_of } } only with allow, see Section 4.5
│ ├── approval? { timeout, approvers } only with ask
│ └── allow_critical? true, required for allow on critical actions; not together with "*"
├── default always "deny" in v0
├── approval default for ask: { timeout, approvers }
├── limits { max_actions_per_hour }
├── valid_from, expires?
├── issuer?, version? only together, see Section 3.5
└── created_by, created_at3.1 Validity of a mandate
#A mandate is valid if it conforms to the schema and additionally:
all fields with
format: date-timeare valid RFC 3339 timestamps with an offset (implementations MUST validateformat, not merely treat it as an annotation), written with an upper-caseTand eitherZor a numeric offset. Not permitted are: leap seconds (:60), more than 9 fractional digits, the year0000and the offset-00:00. Timestamps are compared as points in time with nanosecond precision;it is I-JSON as defined in RFC 7493: valid UTF-8, no lone surrogates (e.g. a standalone
\ud800), no JSON object with a duplicate key, exactly one JSON value;all rule
ids within the mandate are distinct;for every
time_window, start and end are distinct;every action of a rule is
"*"or belongs to the vocabulary:- if the rule's
resourcenames a category from Section 5, to that category's vocabulary (e.g.unlockwithcategory: lightis invalid); - if it names no category, to the vocabulary of at least one category from Section 5.
A misspelled action (
unlokc) therefore never yields a valid rule that silently matches nothing. Actions of an extension can only be used together with the extension category; - if it names an extension category, the actions are not checked: whether a mandate is valid MUST NOT depend on which extensions an implementation knows. Rules for an extension that the implementation does not know never match (Section 4);
- if the rule's
it is at most 262 144 bytes (256 KiB) in size. Lengths of strings in the schemas (
maxLength,minLength) count Unicode code points, not bytes and not UTF-16 code units;expires, if present, is later thanvalid_from;every
approval.timeoutis between 10 seconds and 1 hour (both inclusive). A timeout has the formPT[nH][nM][nS]with at least one component, in this order, eachna decimal number of 1 to 5 digits (PT2M,PT1M30S,PT1H). Its value is the sum of the components. Different spellings of the same duration are different mandates with different digests;agent.display_name,created_byand allapproversare displayed text: they are shown to humans, for example in an approval request, and MUST NOT be able to mislead them. A displayed text- contains no code point listed under
forbiddenindata/forbidden-codepoints-v0.json. The list is normative and fixed; it was derived from Unicode 17.0.0 and contains the general categories Cc, Cf, Zl, Zp, Co and Cs (control and format characters, bidirectional controls, line and paragraph separators, private use, surrogates), the properties Default_Ignorable_Code_Point (invisible characters), White_Space except U+0020, and Noncharacter_Code_Point, and the blank characters U+2800 and U+FFFC; not on it are variation selectors and the two joiners; - does not begin or end with a space (U+0020), the only white space permitted;
- does not begin with a code point listed under
not_first(combining marks and variation selectors, which need a character before them); - contains the joiners U+200C (zero width non-joiner) and U+200D (zero width joiner) only between two other code points: not first, not last, and not directly after another joiner. Persian, Indic scripts and emoji sequences need them.
Implementations MUST use the list, not the character tables of their runtime, so that validity does not change with a Unicode version. The rule cannot prevent look-alike letters from different scripts (homoglyphs); user interfaces SHOULD therefore show the
client_idtogether with thedisplay_name.- contains no code point listed under
a rule with
allow_criticallists its actions; it does not contain"*". Whoever permits critical actions without an approval request names them, so that a later vocabulary cannot add to them silently.constraintsappear only in rules withdecision: allowthat list their actions (no"*"). For every constraint,minis not greater thanmax, and for every action of the rule, the named parameter is a parameter of the action that the constraint fits: in the vocabulary of the rule's category or, if the rule names no category, in at least one category from Section 5. A constraint fits a parameter if it givesminormaxonly for an integer parameter, and if every value of itsone_ofhas the type of the parameter and, for a string parameter with permitted values, is one of them, compared exactly and case-sensitive (Section 4.5). Rules for an extension category are not checked. A constraint on adenyoraskrule would fail open, because a request without the parameter would not match the rule; therefore it is invalid.enforce, if present, isfalse(audit mode, Section 4.6). A mandate that enforces has noenforcemember:truewould be a second form of the same mandate with a different digest, and is invalid.enforceis a member of the mandate, not of a rule.
All numbers in a mandate are integers whose magnitude is at most 2^53 − 1; the schema permits nothing else.
Implementations MAY reject JSON input with a nesting depth greater than 32 before validating it against the schema; valid mandates never reach this depth.
Implementations MUST reject invalid mandates when they are stored. If an invalid mandate is
nevertheless evaluated, the result MUST be deny.
Revocation of a mandate is not a field of the mandate but a state that the implementation maintains and passes to the evaluation (Section 11.3).
Requirements in this document are written with the key words of BCP 14. Where a rule of
the data model, the evaluation or the audit log is stated as a fact ("the result is
deny", "seconds are truncated"), it is part of the definition and every conforming
implementation behaves that way.
3.2 Digest of a mandate
#Each version of a mandate is uniquely identified by its digest:
digest = "sha256:" + hex(SHA-256(JCS(mandate)))JCSis the canonical JSON form defined in RFC 8785: sorted keys, no whitespace, a fixed representation of strings and numbers.- The digest is computed over the valid mandate (Section 3.1), not over the stored bytes. Key order and whitespace therefore do not change the digest; any change in content does.
hexproduces lowercase letters, 64 characters.
In the audit log (Section 9), the digest refers to the version on which the decision was based, without writing the content of the mandate to the audit log.
enforce is part of the content: a mandate in audit mode and the same mandate without
enforce have different digests, so that switching audit mode on or off is a new version
(Section 4.6) and every audit entry names the version it was decided under.
Numbers are canonicalized by their value: 10, 10.0 and 1e1 are the same number and
yield the same digest. Because the only numbers in mandates and audit log entries are
integers, an implementation needs no floating-point serialization for the canonical form:
an integer is written in decimal notation without fraction and exponent.
3.3 Agent identifier
#agent.client_id identifies the agent. It is printable ASCII, at most 512 characters, and
has one of two forms:
- an https URL with a lower-case host, optionally a port, and a path; without userinfo
(
user@) and without a fragment (#…). This is the form of an OAuth Client ID Metadata Document URL; - any other URI: a lower-case scheme,
:, and 1 to 256 characters fromA–Z a–z 0–9 . _ ~ : / -, for example an identifier assigned by the implementation (<namespace>:<id>), a DID (did:web:agent.example), a SPIFFE ID (spiffe://example.org/agents/voice) or a URN. The schemehttpis not permitted.
Neither form contains a dot segment (/./ or /../). Identifiers are compared exactly per
character, without normalization: two identifiers that differ in any character identify
different agents. How an implementation authenticates the agent behind an identifier is
outside this specification.
3.4 Resource and area identifiers
#entity_id and area are opaque identifiers: 1 to 255 (entity_id) or 1 to 64 (area)
printable ASCII characters without space (U+0021 to U+007E). The specification gives them no
structure. Whatever names a platform uses can be used directly, for example
light.living_room, Kitchen_Light, 1/2/3, 0x00000000000004D2/1, a UUID or a URN; a
platform whose names contain other characters maps them, for example by percent-encoding.
Identifiers are compared exactly per character, without normalization and case-sensitive:
Kitchen_Light and kitchen_light are different resources.
Implementations SHOULD use identifiers that do not change when a human renames a device.
If the identifier of a resource changes, rules that name the old identifier no longer
match: an allow rule stops allowing, but a deny or ask rule stops restricting, and a
broader allow rule may then apply. The same holds when an area of a resource changes
or is removed, or when a resource moves to another area. Implementations MUST therefore
tell the household which mandates are affected when an identifier or an area that a rule
names changes or disappears, or when a resource leaves or enters such an area; this
covers every area in the resource's set of areas. They SHOULD express restrictions by
category or by identifier, not by area alone.
Areas are the grouping this version knows. The resource directory gives each resource
a set of areas: 0 to 16 area identifiers, each following the rules for area above,
without duplicates and without order. The set holds the room of the resource and every
location above it (for example garage, ground_floor, home), or several rooms side
by side where the platform places a resource in more than one. What a platform calls
room, zone, floor or group can be mapped to areas. A resource without an area has an
empty set. A rule still names exactly one area; it matches if that area is in the
resource's set (Section 4, step 2). approvers and created_by are identifiers from
the user management of the implementation and have no meaning outside it; a mandate
that moves to another implementation needs them replaced.
3.5 Issuer and version
#A mandate MAY name who issued it and which version it is:
issuer: a URI with the same form asagent.client_id(Section 3.3), compared exactly.version: an integer of at least 1. Among the mandates with the sameidfrom the same issuer, every change gets a higher version than every earlier one.
Both appear together or not at all. The digest (Section 3.2) says which version a document is; it does not say which of two documents is newer. Without a version, an older and more generous mandate with a valid digest cannot be told from the current one.
An implementation that stores a mandate and is offered a replacement with the same id
accepts it only if (succession) the offered mandate names the same agent
(agent.client_id) and the same principal, and
- the stored mandate has no
version, or - the offered mandate has the same
issuerand aversiongreater than the stored one.
A mandate without version therefore never replaces one that has a version, and an older
version is not accepted while the newer one is known (rollback). This protection is local
state: an implementation MUST keep the highest version it has accepted for an issuer
and id, also after the mandate was revoked, removed (Section 11.3) or deleted, for as
long as an older version could still be valid, and it MUST treat a restore from a backup
like the arrival of mandates from outside. A revocation is local as well: a signed mandate
stays verifiable until it expires, so a revoked mandate MUST NOT be accepted again by way
of its signature.
Audit mode. A mandate that switches audit mode on MUST NOT take effect until a human
of the household has confirmed it explicitly (Section 11.6 item 4). It switches audit
mode on if it has enforce: false (Section 4.6) and no mandate with the same id in
audit mode is in force: the stored mandate enforces, or it is not in force because it is
revoked, expired, not yet valid or invalid, or there is none. A mandate that arrives
without a predecessor in force, a restore from a backup included, therefore counts as
arriving from outside. The confirmation is bound to the digest (Section 3.2) of exactly
the version that is adopted; any other version needs its own. This holds for a signed
successor as well: succession and the signature make it acceptable, not adopted, and
until the confirmation the stored mandate stays as it is. Every approver of the mandate
MUST be notified when such a mandate is proposed and when it takes effect. A replacement
that switches audit mode off needs no confirmation.
An issuer can end the sequence of an id by issuing the highest possible version; the
mandate then needs a new id. A mandate that leaves the implementation that
created it, signed per Section 7, MUST carry issuer and version.
4 Evaluation rule
#Input: mandate, resource (entity ID, category, set of areas, and whether the directory marks it as
critical), action, optionally parameters of the action (Section 4.5), point in time,
household time zone, status of the mandate (active or revoked). Which mandate is evaluated is determined by Section 4.3.
Origin of inputs: The PEP MUST determine the category, the areas and the critical marking from its own resource directory, the point in time from its own clock, the time zone from the household configuration and the status from its own mandate management. It MUST NOT take any of these from the agent; only the requested resource and action originate from the agent.
The PEP MUST resolve the resource the agent names to the identifier under which its
directory knows it, and pass that identifier to the evaluation. Because identifiers are
compared exactly (Section 3.4), passing the agent's spelling unchanged would let a
different spelling of the same device slip past a rule. A resource the directory does not
contain has no category; the result is deny with unknown_resource.
Pre-check, each case →
deny:- the mandate is invalid (Section 3.1);
- the request is invalid: the entity ID is missing or does not match the pattern of
entity_idin the schema, an area of the resource does not match the pattern ofarea, the resource has more than 16 areas or the same area twice, a parameter is neither an integer of magnitude at most 2^53 − 1 nor a string of 1 to 64 printable ASCII characters without space, a parameter that the action declares in the vocabulary of the resource's category has another type, lies outside the range of an integer parameter or, for a string with permitted values, is another value (Section 4.5), the point in time or the time zone is invalid or unknown, or the status is neitheractivenorrevoked; - the category is missing: the resource is not in the directory of the PEP;
- the category is neither listed in Section 5 nor an extension whose vocabulary the implementation knows;
- the action does not belong to the vocabulary of the resource's category.
If the mandate is revoked, not yet valid or expired →
deny. It is valid for points in timetwithvalid_from ≤ t < expires(withoutexpires:valid_from ≤ t). Points in time are compared, not clock times.Collect all rules whose
resourcematches the resource and whoseactionscontain the action and whoseconditionsare satisfied at the point in time and whoseconstraintsare satisfied by the parameters (Section 4.5).resourcematches if all specified fields match (e.g.category: lightandarea: living_room= lights in the living room).areamatches if it is in the resource's set of areas; a resource without areas matches no rule that names anarea.any: truematches every resource. Comparison is exact per character, case-sensitive and without normalization."*"inactionscontains every action, including critical ones.readis an action in its own right. Write permissions do not include read permissions.
No rule found →
default(deny).Otherwise the most restrictive decision wins:
denyoveraskoverallow.Protection class: If the action is critical and the result is
allow, but not every one of the matchingallowrules carriesallow_critical: true→ the result becomesask. For non-critical actions,allow_criticalhas no effect. An action is critical if- the vocabulary marks it as critical (Section 5), or
- the resource directory marks the resource as critical and the action is not
read.
The marking of a resource lets a household protect what the vocabulary cannot know: a switch that drives a door opener, a ground-floor window, a heater. It can only add protection. An implementation SHOULD let the household mark resources as critical.
Audit mode: if the mandate carries
enforce: false, the result of step 3 or 4 isdenyand the action is not critical (step 5), the result is not enforced (Section 4.6). It staysdeny. If one of the matching rules of step 2 decidesask, the request still needs a confirmation before it is executed: the result requires a confirmation and carries the approval settings per Section 4.1.
The rule is deliberately simple: whoever allows something broadly and denies individual items
gets the denial. Whoever sets something broadly to ask and allows individual items gets ask.
When in doubt, the safer side always wins, in audit mode as well: a deny that is not
enforced never takes away a confirmation that a matching ask rule asks for.
limits are not part of the evaluation. The PEP enforces the rate limit (Section 11.2).
4.1 Result
#In addition to the decision, the evaluation returns:
reason: exactly one reason code. If several reasons apply, the first one in the following order applies:Code Decision Meaning no_mandatedeny no mandate for the agent and the principal (Section 4.3) ambiguous_mandatedeny several mandates for the agent and the principal (Section 4.3) invalid_mandatedeny mandate invalid (Section 3.1) invalid_requestdeny request invalid (step 0, second item) unknown_resourcedeny resource not in the directory of the PEP (no category) unknown_categorydeny category neither in Section 5 nor a known extension unknown_actiondeny action not in the category's vocabulary revokeddeny mandate revoked not_yet_validdeny point in time before valid_fromexpireddeny point in time at or after expiresno_matchdeny no rule matches, defaultcritical_demotionask allowbecameaskper step 5ruleallow, ask, deny decision of a rule per step 4 mandate_digest: digest of the evaluated mandate (Section 3.2); absent forno_mandate,ambiguous_mandateandinvalid_mandate.rule_id: the first rule in document order that carries the final decision. For step 5, this is the first matchingallowrule withoutallow_critical. For the pre-check, step 1 and step 3, there is norule_id.Approval settings (only for
askand for a result that requires a confirmation): theapprovalof the first matchingaskrule in document order that has its ownapproval, otherwise theapprovalof the mandate.enforced:falseif the result is adenythat is not enforced (step 6, Section 4.6); thenreasonisno_matchorrule. Every other result is enforced and has noenforced.confirm:trueif the result is adenythat is not enforced and requires a confirmation (step 6, Section 4.6); thenenforcedisfalseandreasonisrule. Every other result has noconfirm; for anaskthe confirmation follows from the decision.
4.2 Conditions
#All conditions of a rule MUST be satisfied. They are checked in household local time: the point in time is converted to the household's time zone.
The time zone is an identifier from the IANA time zone database, exact per character and case-sensitive:
UTCor the formArea/Location(e.g.Europe/Berlin,America/Argentina/Buenos_Aires,Etc/GMT+9), where each part begins with an uppercase letter and contains onlyA–Z,a–z,0–9,_,-,+; at most 64 characters. Other names (includingLocal,localtime,GMTor abbreviations such asCET) and names that the implementation does not know result ininvalid_request. Some time zone libraries match names without regard to case; an implementation that uses one MUST compare the spelling itself (Europe/BERLINis not a name of the database). Names of this form that the database defines as links to another zone (such asUS/Eastern) are permitted. The rules of a zone change with the release of the database; implementations SHOULD keep it current, and two implementations can differ for points in time that a newer release treats differently.If no time zone is given, the offset with which the point in time is expressed applies. A PEP SHOULD always pass the household time zone; without it, whoever chooses the offset of the point in time chooses the local time.
For the comparison, the hour and minute of local time are used; seconds and fractions are truncated (23:58:59 is 23:58).
time_window:"HH:MM-HH:MM", minute precision, start inclusive, end exclusive."06:00-22:00"matches 06:00 through 21:59. If the start is greater than the end, the window spans midnight:"22:00-06:00"matches from 22:00 and before 06:00. During daylight saving time transitions, the clock time displayed in the household counts; an hour that occurs twice matches both times.weekdays: list ofmon…sun. The weekday of the point in time in local time is decisive, including for windows spanning midnight (Friday 22:00 to Saturday 02:00 withweekdays: ["fri"]matches only until midnight).
4.3 Selection of the mandate
#An implementation can store several mandates. Which one is evaluated for a request is
determined by the agent (agent.client_id) and the principal of the request, both compared
exactly. household:x and person:x are different principals; nothing is inherited from
one to the other. The PEP determines the principal; it does not take it from the agent.
- Candidates are the stored mandates of the agent and the principal that are not revoked. A stored document that is not a valid mandate (Section 3.1) is a candidate as well.
- No candidate →
denywithno_mandate. - Of the candidates, those are current that are valid at the point in time
(
valid_from ≤ t < expires); an invalid document always counts as current.- exactly one current candidate → it is evaluated (Section 4);
- several current candidates →
denywithambiguous_mandate. No mandate wins, and mandates are never combined; - no current candidate: if there is exactly one candidate, it is evaluated and yields
not_yet_validorexpired; otherwisedenywithno_mandate.
An agent can therefore have mandates that follow one another in time, but never two at the same time. Implementations SHOULD refuse to store a mandate whose validity period overlaps with another mandate of the same agent and principal that is not revoked.
4.4 Requests for several resources
#The evaluation always concerns exactly one resource and one action. If an agent addresses
several resources at once (an area, a group, a list), the PEP resolves them with its
directory and evaluates each resource separately. Unless the PEP documents otherwise, the
request is executed only if every single evaluation permits it: one deny denies the
whole request, and one ask requires a confirmation that names all resources concerned. A
deny that is not enforced (Section 4.6) counts as permitting here, and as an ask if it
requires a confirmation; it is recorded as such. The agent is never told which of the
resources were not enforced (Section 11.6).
The PEP MUST execute such a request on exactly the resources it evaluated, naming each of
them to the platform (for example as a list of identifiers), never the area, group or list
it resolved them from: the platform would resolve that again on its own, and a resource
added in the meantime would be acted on without an evaluation. This is the same as for
parameters, whose evaluated values are the ones executed (Section 4.5).
A confirmation for such a request covers exactly the set of resources the PEP resolved and showed (Section 11.1 item 2). Before the execution the PEP resolves the request again; if the set differs in any way (a resource added, removed or replaced), nothing is executed, not even for the resources that were confirmed, and executing the new set needs a new request and a new confirmation (Section 11.1 item 6).
A resource whose activation acts on other resources (a scene, a script, a group that the
platform executes itself) is evaluated as the resource it is; this is why scene.activate
and script.run are critical.
4.5 Parameters and constraints
#Some actions carry a value: a temperature, a position, a volume, a mode. The vocabulary names these parameters per action, each with its type (Section 5). A parameter is
- an integer in the unit the vocabulary gives it. The units are chosen fine enough
that no fraction is needed (
temperatureis in hundredths of a degree Celsius, 21.5 °C is2150); there are no fractions anywhere, and the magnitude is at most 2^53 − 1. The vocabulary can give an integer parameter a range (brightness0 to 100, both inclusive); no value outside it is valid then.temperaturehas no range; - or a string of 1 to 64 printable ASCII characters without space (U+0021 to U+007E),
the rules of an area (Section 3.4). Strings are compared exactly per character,
case-sensitive and without normalization:
heatandHeatare different values. The vocabulary can list the permitted values of a string parameter, as it does formode; no other value is valid then.
The PEP derives the parameters from the request of the agent and expresses each in the
type and unit of its parameter in the vocabulary. It MUST execute exactly the values that were evaluated: whatever
it sends to the platform MUST NOT set a constrained quantity to another value, by rounding
or through a second, platform-specific parameter for the same quantity. A value that
cannot be expressed as an integer in the unit, or as one of the permitted values of a
string parameter, or that lies outside the range of an integer parameter, makes the
request invalid: the PEP refuses the request, and never leaves the parameter out or
evaluates without it. The type is part of the value: a
request whose parameter has another type than the vocabulary declares for the action
(the integer 4 for mode, the string "50" for brightness) is invalid as well
(Section 4, step 0). A parameter that the action does not declare is checked only for
being an integer or a string.
A quantity can have a parameter per unit: a temperature is temperature in hundredths of
a degree Celsius or temperature_f in hundredths of a degree Fahrenheit (70 °F is 7000;
it is no integer in temperature, 21.111… °C). The PEP derives exactly one of them for a
request, the one in the unit in which the platform expresses the value; it never converts
into the other unit. A rule that constrains the other parameter does not match the
request, and a rule that constrains both matches no request a PEP derives this way.
A constraint has one of two forms:
- a range for an integer parameter:
min,maxor both. A limit that is not given does not restrict; - a list of permitted values for an integer or a string parameter:
one_ofwith 1 to 32 distinct values of the parameter's type. It does not appear together withminormax.
A rule with constraints matches only if the request carries every named parameter
and each value v satisfies its constraint: min ≤ v ≤ max for a range; for a list, v
equals one of the values exactly (an integer never equals a string). A request without
the parameter does not match the rule. Parameters that no constraint names do not
influence the evaluation.
{ "id": "r-heating", "resource": { "category": "climate" }, "actions": ["set_temperature"],
"decision": "allow", "constraints": { "temperature": { "min": 1600, "max": 2300 } } }
{ "id": "r-heating-modes", "resource": { "category": "climate" }, "actions": ["set_mode"],
"decision": "allow", "constraints": { "mode": { "one_of": ["heat", "auto"] } } }The second rule lets the agent switch the heating to heat or auto, but never off.
A constraint may name values outside the range that the vocabulary gives an integer
parameter: a one_of value, a min or a max beyond it is valid in a mandate. Such a
value never matches, because a request with it is invalid; {"min": 0, "max": 500} on
brightness permits the same requests as no limits at all.
Constraints narrow an allow. Whoever wants a confirmation for values outside the limits
adds an ask rule for the same action without constraints; ask then wins inside the
limits as well, so the usual form is: allow with constraints, and nothing else, which
denies everything outside.
4.6 Audit mode
#A household that tries a new mandate can first let it run in audit mode: the rules are
evaluated and every decision is recorded, but a denial by the rules does not stop the
agent. The audit log then shows what the mandate would deny before it does. A mandate is
in audit mode if it carries "enforce": false; without the member, it enforces. enforce
has no other value (Section 3.1 item 11).
In audit mode exactly one kind of result is not enforced: a deny that results from the
rules of the mandate, that is no_match (step 3) or a matching deny rule
(reason: rule, step 4), for an action that is not critical (step 5). The evaluation
returns it as deny with enforced: false (Section 4.1). The decision stays deny, so
that the audit log records truthfully what the mandate says. Everything else is enforced
as in a mandate without enforce:
- a
denyof a critical action, whether no rule matches or adenyrule does: audit mode never unlocks a door, opens a gate or takes a snapshot that the mandate does not permit, and a resource the directory marks as critical stays protected in every action exceptread; ask, includingcritical_demotion: a human still confirms (Section 11.1), also when adenyrule matches the same request. Thedenywins and is not enforced, but the result requires a confirmation (confirm: true, step 6): without thedenyrules the request would be anask, and a stricter rule must not leave it with less protection. The confirmation also shows whether the household wants thedeny;allow: unchanged;- every
denybefore the rules: an invalid mandate, an invalid request, an unknown resource, category or action, a revoked mandate and one that is not yet valid or has expired (steps 0 and 1), as well asno_mandateandambiguous_mandate(Section 4.3); - every denial outside the evaluation: the rate limit, the emergency stop, the authentication of the agent, and an approval that was not given (Section 11).
The PEP executes a request whose result is not enforced as it executes an allow, and
one that requires a confirmation only after a confirmation, as it executes an ask
(Section 11.6). What the evaluation does not decide stays as it is: the PEP executes
exactly the parameters that were evaluated (Section 4.5), the request counts towards the
rate limit (Section 11.2), and a confirmed request is evaluated again before it is
executed: a deny prevents the execution of a confirmed ask in audit mode as well, and
of a confirmed deny with confirm: true every result except allow, ask and another
deny with confirm: true (Section 11.1 item 6).
Audit mode belongs to the mandate, not to the implementation: one mandate can be tried
while the others enforce. Switching it on or off is a change to the mandate with a new
digest (Section 3.2), recorded with mandate.updated (Section 9.2). Switching it on
weakens the mandate: it takes effect only after a human of the household has confirmed it
explicitly, and every approver is notified (Sections 3.5 and 11.6). Switching it off
strengthens the mandate and needs no confirmation.
5 Vocabulary v0
#Machine-readable and normative: vocabulary/v0.json (format:
schema/vocabulary-v0.schema.json). The table shows its content.
| Category | Actions | Critical |
|---|---|---|
light | read, turn_on, turn_off, set | – |
switch | read, turn_on, turn_off | – |
climate | read, set_temperature, set_mode | – |
cover | read, open, close, stop, set_position | – |
gate | read, open, close | open |
lock | read, lock, unlock, open | unlock, open |
alarm | read, arm, disarm | disarm |
camera | read, snapshot | snapshot |
media | read, turn_on, turn_off, play, pause, set_volume | – |
sensor | read | – |
scene | read, activate | activate |
script | read, run | run |
other | read, set | set |
Parameters (Section 4.5):
| Action | Parameter | Type | Unit | Range or permitted values |
|---|---|---|---|---|
light.set | brightness | integer | percent | 0 to 100 |
climate.set_temperature | temperature | integer | 0.01 degree Celsius | – |
climate.set_temperature | temperature_f | integer | 0.01 degree Fahrenheit | – |
climate.set_mode | mode | string | – | off, auto, cool, heat, emergency_heat, precooling, fan_only, dry, sleep |
cover.set_position | position | integer | percent open | 0 to 100 |
media.set_volume | volume | integer | percent | 0 to 100 |
The permitted values of mode are the values of the attribute SystemMode of the Matter
Thermostat cluster, in lower case with underscores: what the system as a whole does, not
a preset such as eco or away. How the modes of a platform map to them is described in
the profiles (informative). A value outside the list makes the request invalid
(Section 4.5).
scene.activate, script.run and other.set are critical because scenes, scripts and
unknown entities can have arbitrary consequences, including opening doors. What else is
critical in a particular household (a switch on a door opener, a ground-floor window) the
household marks in the resource directory (Section 4, step 5).
How an implementation assigns its resources to categories is its own matter and part of
its resource directory. A resource that fits no other category belongs to other.
profiles/ contains informative mappings for individual platforms; they are not part of
the specification, and a platform without a profile is not at a disadvantage.
5.1 Extensions
#Extensions for other kinds of resources receive their own namespaces, e.g.
paperless:document with read, tag, delete. An extension defines its vocabulary and
its critical actions in a file of the same format as vocabulary/v0.json, with its own
id and version. It declares the type of every parameter: integer with a unit,
or string, optionally with 1 to 32 permitted values, as many as a one_of can list.
A vocabulary never changes under the same id and version. If an
implementation does not know the vocabulary of an extension, every request for a resource
of that category is deny. A registry of extensions does not exist yet.
6 AuthZEN mapping
#The PEP queries the PDP in accordance with AuthZEN Authorization API 1.0:
POST /access/v1/evaluation
{
"subject": { "type": "agent", "id": "https://example-agent.local/client.json",
"properties": { "principal": "household:hm-7f3a" } },
"action": { "name": "unlock" },
"resource": { "type": "lock", "id": "lock.front_door",
"properties": { "areas": ["hallway", "ground_floor"] } },
"context": { "time": "2026-10-12T21:14:03+02:00" }
}AuthZEN only knows true or false. The third decision is conveyed in the response
context:
{ "decision": false,
"context": { "outcome": "ask", "reason": "rule", "rule_id": "r-locks",
"approval_timeout": "PT2M", "approvers": ["user-1"],
"mandate_digest": "sha256:9f2c…" } }reason is REQUIRED, as is mandate_digest unless Section 4.1 says it is absent; rule_id is
included when Section 4.1 provides for it. For outcome: ask, approval_timeout and
approvers (the approval settings per Section 4.1) are REQUIRED, so that a PEP can obtain
the confirmation without knowing the mandate. enforced: false is REQUIRED for a deny
that is not enforced (audit mode, Section 4.6) and absent otherwise:
{ "decision": false,
"context": { "outcome": "deny", "reason": "no_match", "enforced": false,
"mandate_digest": "sha256:83d0…" } }confirm: true is REQUIRED for a deny that is not enforced and requires a confirmation
(Section 4.1), together with approval_timeout and approvers, and absent otherwise:
{ "decision": false,
"context": { "outcome": "deny", "reason": "rule", "rule_id": "r-no-kids-room",
"enforced": false, "confirm": true,
"approval_timeout": "PT1M", "approvers": ["parent-1"],
"mandate_digest": "sha256:5b1e…" } }Field mapping:
| AuthZEN | Input per Section 4 |
|---|---|
subject.id | agent.client_id; together with subject.properties.principal, selects the mandate |
resource.id | entity ID |
resource.type | category |
resource.properties.areas | set of areas, as an array (Section 3.4) |
action.name | action |
action.properties | parameters (Section 4.5): an object of parameter names and values, each a JSON integer or string |
context.time | point in time |
The PDP does not use resource.type, resource.properties.areas and context.time without
verification, but in accordance with Section 4 "Origin of inputs"; the critical marking of a
resource likewise comes from the directory, never from the request. The mandate is selected
per Section 4.3; if there is none for the agent and principal, the result is deny with
reason: no_mandate.
outcome: allow→decision: trueoutcome: ask→decision: false; the PEP MUST obtain a confirmation per Section 11.1 and MUST NOT execute the action unless the response is positiveoutcome: deny→decision: false; withenforced: false, the PEP executes the action as in Section 11.6, and withconfirm: trueas well only after a confirmation per Section 11.1
A PEP that does not know ask automatically treats the response as a denial, and a PEP
that does not know enforced enforces the denial. The extension is thus
backward-compatible and safe. A PEP that executes a deny with enforced: false MUST also
know confirm, because it may only execute one with confirm: true after a confirmation.
subject.type MUST be agent; any other value is deny with invalid_request.
Trust between PEP and PDP. The PDP decides on the basis of the resource directory, the
clock, the household time zone and the mandate status. Whoever holds these is part of the
trusted side. If PEP and PDP are separate components, exactly one of them holds the
directory, and both MUST know which. The connection between them MUST be authenticated in
both directions and protected against modification; a PDP MUST NOT answer callers it has
not authenticated, and it MUST NOT be reachable by agents. A PDP that does not hold the
directory itself takes resource.type, resource.properties and context.time from the
authenticated PEP, and only from it.
7 Signed mandates and transport
#As long as a mandate stays inside the implementation that stores and evaluates it, it needs no signature. A mandate that travels (between an issuer and a PDP, between implementations, or in an OAuth flow) is signed, so that the receiver can tell who issued it and that it was not changed.
7.1 Signed mandate
#A signed mandate is a JSON Web Signature in compact serialization (RFC 7515):
BASE64URL(header) "." BASE64URL(JCS(mandate)) "." BASE64URL(signature)- The payload is the canonical form of the mandate (Section 3.2), byte for byte. A verifier MUST reject a payload that is not canonical; one mandate thus has exactly one payload.
- The mandate carries
issuerandversion(Section 3.5). - The protected header has exactly two members:
algandkid.algisEdDSAwith Ed25519 (RFC 8037) orES256. Every implementation that verifies signed mandates MUST supportEdDSAand MAY supportES256. Any other algorithm (in particularnone) and any other header member (such ascrit,b64,jwk,jku,x5c) make the signed mandate invalid: a key is never taken from the signed object itself. kidis 1 to 64 characters fromA–Z a–z 0–9 . _ -and names a key of the issuer.- A signed object has exactly one spelling. The three segments are base64url without
padding and without any other character (no line breaks); the names of the header
members are case-sensitive; and of the two values of
sthat verify for an ES256 signature, only the lower one (s≤ n/2) is accepted, and signers produce it. A verifier MUST reject everything else, even if the signature would verify. - Keys that sign mandates MUST NOT be used to sign checkpoints (Section 9.5), and vice versa.
A verifier accepts a signed mandate only if
- it holds public keys that it trusts for the issuer the mandate names, and the
signature verifies with the key
kidamong them; it has also decided, by its own configuration, that this issuer may issue mandates for theprincipaland the agent the mandate names. A key that is trusted for one household MUST NOT be accepted for another; - the payload is canonical and a valid mandate (Section 3.1);
- succession holds against what the verifier has stored (Section 3.5).
How a verifier comes to trust the keys of an issuer is outside this specification
(configuration, pairing, a JWK Set under the issuer's URL, …). Keys are exchanged as JWK or
JWK Set (RFC 7517): {"kty":"OKP","crv":"Ed25519","x":…} or
{"kty":"EC","crv":"P-256","x":…,"y":…}, each with a kid.
The algorithms are fixed per version of the specification. The prefix of a digest
(sha256:) and the alg of a signature name the algorithm, so that a later version can
add others without changing the format.
7.2 OAuth flow (informative)
#A mandate can be represented as an authorization_details object (RFC 9396) with
type: "https://home-mandate.org/mandate/v0", e.g. in token introspection or when an agent
proposes a desired mandate at sign-in. The human always chooses the mandate; a proposal by
the agent is only a pre-filled default and is never evaluated. Where the object leaves the
party that issued it, the signed form of Section 7.1 is used.
8 Conformance and certification
#conformance/cases-v0.json contains the cases: mandate + request + expected decision.
Paths to mandates are relative to the root directory of this repository. The collection grows
with every version; every bug found is first added as a case.
Fields of a case:
| Field | Required | Meaning |
|---|---|---|
id | yes | unique identifier |
mandate / mandate_inline | one of them | path to the mandate, or the mandate inline in the case |
resource | yes | entity_id, category, areas (an array, Section 3.4) of the resource as the PEP resolved it, and optionally critical: true if the directory marks it as critical; without category the directory does not contain it |
action | yes | requested action |
parameters | no | parameters of the action (Section 4.5), each an integer or a string; a value that is neither (a fraction, for example) stands for a request the PEP cannot express, the expected result is then invalid_request |
time | yes | point in time per RFC 3339 |
timezone | no | household time zone (IANA); if absent, the offset in time applies |
revoked | no | true: status revoked; if the field is absent or false: status active |
expected | yes | allow, ask or deny |
reason | yes | expected reason code per Section 4.1 |
rule_id | no | expected rule_id per Section 4.1; if the field is absent, it is not checked; null means: none |
approval_timeout | no | expected timeout of the approval settings for ask and for a result that requires a confirmation |
enforced | no | false: the expected result is a deny that is not enforced (Section 4.6); absent or true: the result is enforced, and an implementation that reports enforced: false fails the case |
confirm | no | true: the expected result is a deny that is not enforced and requires a confirmation (Section 4.1); absent or false: it requires none, and an implementation that reports confirm: true fails the case |
why | no | explanation for humans |
conformance/invalid-v0.json contains mandates that are invalid per Section 3.1 and
MUST be rejected (mandate_inline, or mandate_raw as a string if the
error cannot be represented as a JSON object, such as duplicate keys).
conformance/selection-v0.json contains cases for Section 4.3: mandates (the stored
mandates, each as mandate_inline with an optional revoked), subject (client_id and
principal of the request), the request as in cases-v0.json, selected (the id of the
mandate that is evaluated, or null) and the expected expected, reason and optionally
rule_id, approval_timeout, enforced and confirm, as in cases-v0.json.
conformance/succession-v0.json contains cases for Section 3.5: stored and offered
(two valid mandates) and expected (accept or reject).
conformance/signed-v0.json contains cases for Section 7.1: jws (the signed mandate),
keys (path to the JWK Set the verifier trusts), issuer (the issuer the keys are trusted
for), expected (valid or invalid) and, for valid, the digest of the mandate. The
keys in conformance/keys/ are test keys whose private parts are public.
conformance/digest-v0.json contains, under cases, mandates (mandate, mandate_inline or
mandate_raw) with their expected digest digest (Section 3.2).
conformance/audit-v0.json contains, under logs, audit logs: entries (the entries in
file order), expected (valid or invalid), for invalid the expected position
broken_at (Section 9.4) and optionally entry_digests (the digest of each entry, for
debugging). Instead of entries, a log can be given as jsonl: the exchange format
(Section 9.4) as one string, for cases about line separators. With keys (path to a JWK
Set) the signatures of checkpoints are verified (Section 9.5), optionally against the
expected log_id; anchored is then the expected seq up to which a verified checkpoint
covers the log.
In addition to cases or logs respectively, every file has a description; every case has a
unique id and optionally why.
The format of each conformance file is itself described by a JSON Schema in
conformance/schema/, so that the files can be read and checked without the code in this
repository. conformance/manifest.json lists every machine-readable file of the
specification (schemas, examples, conformance files) with the SHA-256 of its bytes and, for
conformance files, the number of cases. A test report SHOULD name the manifest it was
produced with, so that it states exactly which cases were run.
8.1 Conformance classes
#Not every implementation does everything. An evaluator library has no HTTP endpoint; a verification tool for audit logs evaluates nothing. An implementation therefore conforms to one or more classes, and it conforms to a class of a version if it passes all cases of that class in that version.
| Class | What it does | Cases |
|---|---|---|
evaluator | validates mandates, computes digests, evaluates requests (Sections 3 and 4) | cases-v0.json, invalid-v0.json, digest-v0.json |
selection | selects the mandate among several (Section 4.3) | selection-v0.json |
signatures | verifies signed mandates and succession (Sections 3.5 and 7) | signed-v0.json, succession-v0.json |
audit | verifies the hash chain of audit logs and computes entry digests (Section 9.4) | logs in audit-v0.json without keys |
audit-anchored | additionally verifies checkpoints (Section 9.5) | logs in audit-v0.json with keys |
pdp | answers AuthZEN requests from its own mandate store and resource directory (Sections 4.3 and 6) | the cases of cases-v0.json and selection-v0.json whose mandates are valid, over HTTP |
evaluator is the basis: an implementation that claims any conformance to the
evaluation conforms to it. The obligations of the PEP (Section 11: approval, rate limit,
revocation) cannot be tested from outside in this version; an implementation states for
each item of Section 11 how it meets it.
A claim of conformance names the version (the tag of this repository), the classes and
the manifest (conformance/manifest.json, by its SHA-256). The report of the test tool
contains all three and can be published. A formal certification program will follow only
once the specification is frozen.
8.2 Ways of testing
#- Library: Implementations in Go can embed the reference code of this repository or test their own code against the cases directly.
- Test tool:
mandate-conformance(incmd/) plays the cases against any implementation through the test interface of Section 10, independent of language and vendor, and writes a machine-readable report.
9 Audit log
#Every implementation MUST maintain an audit log that shows which agent did what, when, on the
basis of which configuration. The format and the hash chain are fixed so that
audit logs of different implementations can be verified with the same tools.
Machine-readable: schema/audit-v0.schema.json.
9.1 Entries
#Each entry is a JSON object with type: "https://home-mandate.org/audit/v0" and:
| Field | Contents |
|---|---|
id | UUIDv7 (RFC 9562) |
seq | sequential number in the audit log, starting at 1, without gaps. Read by its value: 1, 1.0 and 1e0 are the same number; writers SHOULD use the plain form |
recorded_at | point in time of the entry (RFC 3339) |
event | event type (Section 9.2) |
principal | principal |
prev | digest of the previous entry (Section 9.4); null for the first entry |
Depending on the event, the following are added:
| Field | Contents |
|---|---|
actor | who triggered a change: kind (user, agent, system) and id |
agent | client_id, optionally display_name |
request | input to the evaluation: resource (entity_id, optionally category, areas as in Section 3.4 and critical), action, time, optionally parameters (each an integer or a string, Section 4.5), timezone and revoked |
mandate | id, digest, and for mandate.updated additionally previous_digest; for mandate.removed, digest is that of the last version the implementation held |
evaluation | result per Section 4.1: decision, reason, rule_id, optionally approval_timeout, enforced (only false, Section 4.6) and confirm (only true, with enforced) |
approval | outcome of an approval request: outcome (approved, rejected, timeout, invalid_response, cancelled), at, by (who responded; required for approved, rejected and invalid_response, optional for timeout, absent for cancelled), optionally via (the channel the answer came through, an implementation-defined lowercase code such as push or ui; only together with by); cause (why the request ended without an answer: withdrawn, revoked, emergency_stop, interrupted, Section 11.1) only with cancelled and required there |
result | status: executed, denied (with denied_by: mandate, approval, rate_limit, emergency_stop, authentication) or failed (with error, a code consisting of lowercase letters, digits and _); denied_by only with denied; error is required with failed, MAY give the cause of a denied and never appears with executed; optionally duration_ms; optionally count (Section 11.2) |
truncated | up_to_seq, last_digest (Section 9.4) |
checkpoint | log_id, signature (Section 9.5) |
directory | change to the resource directory (Section 11.4): change (critical_marked, critical_unmarked, renamed, rename_applied, rename_dismissed), entity_id and, except for critical_marked and critical_unmarked, previous_entity_id, both identifiers per Section 3.4 and different from each other. For renamed, entity_id is the new and previous_entity_id the former identifier; for rename_applied (a human took the rename over into the mandates) and rename_dismissed, entity_id is the current identifier and previous_entity_id the former one that was resolved, one entry per former identifier. Only with directory.changed |
template | change to a template (Section 2): change (stored: created or changed, removed, hidden: no longer offered or accepted for creating mandates without being removed, shown: offered and accepted again), name (1 to 64 characters) and digest; for stored the digest of the new content, for removed that of the last content, for hidden and shown optionally that of the current content. previous_digest only for stored and only if the template had content before, different from digest. Digests are computed as in Section 3.2 over the template as the implementation stores it. Only with template.changed |
approver | change to the humans whom the implementation lets answer approval requests (Section 11.1): change (added, removed) and id (1 to 64 characters), the identifier of the approver in the user management, as in approval.by. Only with approver.changed |
request records the input to the evaluation as the PEP determined it, also when the
evaluation found it invalid (invalid_request). Identifiers in it are therefore limited only
in length and number, not in their characters; Section 3.4 applies to mandates and to valid
requests. Input that the log cannot record (a longer identifier, more than 16 areas or areas
that repeat, more than 16 parameters, a parameter value that is neither an integer nor a
string of the form of Section 4.5) is not a request: the PEP rejects it at its interface
before any evaluation (with MCP, for example, as invalid parameters).
For decision, the following additionally applies:
executedonly withevaluationandmandate, and only ifevaluation.decisionisallow,askwithapproval.outcome: approved, ordenywithevaluation.enforced: false, and then withevaluation.confirm: trueonly withapproval.outcome: approved;- if
evaluation.decisionisdeny, thenresultisdeniedwithdenied_by: mandate, unlessevaluation.enforcedisfalse: thenresultisexecutedorfailed(audit mode, Section 11.6), and withevaluation.confirm: truetheresultcan be anything anaskcan have; evaluation.enforcedappears only indecisionentries withmandateandrequest, only asfalse, and only withevaluation.decision: denyandreasonno_matchorrule. Anallowor anaskis never marked: an allow is executed anyway, and anaskis asked in audit mode as well;evaluation.confirmappears only astrueand only withevaluation.enforced: falseandreason: rule: thedenywas not enforced, and a human was asked first because a matching rule decidesask(Section 4.6). Anapprovalobject appears only withevaluation.decision: askor withevaluation.confirm: true;- with
evaluation.enforced: false,request.resourcenames thecategoryandcritical(trueorfalse, so that the marking cannot be left out), and the action is not critical: the resource is not markedcriticalunless the action isread, and the action is in the vocabulary of the category and not critical there (Section 5). The schema cannot see the vocabulary; a verifier MUST check it for the categories of Section 5 and for every extension whose vocabulary it knows, and an entry that violates it is invalid. A verifier without the vocabulary of an extension cannot tell which actions of that extension are critical, and cannot reject such an entry; a relying party that needs the check gives the verifier the vocabularies of the extensions the implementation uses; - a confirmed
askthat is not executed because the evaluation before the execution gave adeny, enforced or not (Section 11.1 item 6), is recorded with theevaluationthat was confirmed (ask, withoutenforced),approval.outcome: approvedandresultdeniedwithdenied_by: mandate; the same holds for a confirmeddenywithconfirm: truewhose evaluation before the execution gave adenywithoutconfirm: true, recorded with theevaluationthat was confirmed (deny,enforced: false,confirm: true). Withconfirm: true,denied_by: mandateappears only so, or withapproval.outcome: cancelledandcause: revoked(Section 11.6 item 1); evaluation.reasonbelongs toevaluation.decisionas in the table of Section 4.1 (allowonly withrule,askonly withruleorcritical_demotion);- a denial with
denied_by: approvalcarries theapprovalobject if a request was made: answered, timed out or cancelled. It has none if no request was made: no approver could be reached, or the agent already had too many requests waiting (Section 11.1); approval.outcome: cancelledonly withresult.status: denied, and with thedenied_bythat matches itscause:withdrawnandinterruptedwithapproval,revokedwithauthentication(the agent was revoked) ormandate(only its mandate was), andemergency_stopwithemergency_stop. A request that a revocation or an emergency stop ended while it was waiting is recorded with this outcome, not without anapprovalobject.
A log.truncated entry has an actor of kind user or system; an agent never deletes
entries. The same holds for directory.changed, template.changed, approver.changed,
mandate.removed and agent.removed: an agent never changes the directory, a template or
who may answer approval requests, and never removes a mandate or an agent. An
agent.reconnected entry has an actor of kind user: only a human reconnects an agent
(Section 11.3).
actor.id, agent.display_name, approval.by, template.name and approver.id are
displayed text; Section 3.1 item 8
applies to them, and an entry that violates it is invalid.
Entries MUST NOT contain tokens, nonces, credentials or the content of a mandate or of a template.
9.2 Events
#event | Required | When |
|---|---|---|
decision | yes | every request from an authenticated agent, even if it is rejected before evaluation (e.g. rate limit, emergency stop) |
mandate.created, mandate.updated, mandate.revoked | yes | every change to a mandate, with old and new digest |
agent.registered, agent.revoked | yes | admission and revocation of an agent |
emergency_stop.activated, emergency_stop.released | yes | emergency stop, if the implementation has one |
log.truncated | yes | before deleting old entries (Section 9.4) |
log.checkpoint | no | signed statement about the log so far (Section 9.5) |
directory.changed | yes, if the implementation lets humans change the critical marking or tracks renamed resources | every such change to the directory, and every rename a human resolves (Section 11.4) |
template.changed | yes, if the implementation lets humans change templates | every template stored, removed, hidden or shown again |
approver.changed | yes, if the implementation keeps its own list of humans who may answer approval requests | every approver added to or removed from that list (Section 11.1) |
mandate.removed, agent.removed | yes, if the implementation removes revoked mandates or agents | every revoked mandate or agent removed from the implementation's lists (Section 11.3) |
agent.reconnected | yes, if the implementation lets humans reconnect agents | every existing agent that receives new credentials instead of being admitted anew (Section 11.3) |
auth.rejected | no | rejected sign-in or invalid token |
For decision, evaluation matches the result that the evaluation (Section 4)
returns for request and the version mandate.digest, including enforced, confirm
and approval_timeout. This makes
every decision reproducible.
The rate limit and the emergency stop are defined in Sections 11.2 and 11.3. Denials are
logged with denied_by: rate_limit or emergency_stop respectively, and the emergency stop
with the emergency_stop.* events.
9.3 Retention
#- Each mandate version MUST be retained at least as long as entries that refer to its digest.
- How long entries are retained is determined by the implementation. Only the oldest entries MAY be deleted (Section 9.4).
9.4 Hash chain and verification
#- Digest of an entry:
"sha256:" + hex(SHA-256(JCS(entry))), computed over the complete entry includingprev. - The
prevof each entry is the digest of its predecessor; for the entry withseq1,previsnull. - Before the entries up to and including
seqn are deleted, alog.truncatedentry withtruncated: { up_to_seq: n, last_digest: <digest of entry n> }is appended. - Exchange format: JSON Lines, UTF-8, in ascending
seqorder. Lines are separated by U+000A (line feed) and by nothing else: readers MUST NOT split at any other character, such as U+2028 inside a string. Every line is exactly one entry; white space that JSON permits around a value, including a carriage return before the line feed, is ignored. A line feed after the last entry is optional. A byte order mark and empty lines are not permitted; such a line counts as an entry that violates the schema.
An audit log is valid if, in file order:
- every entry conforms to the schema and to the rules of Section 9.1 that the schema
cannot express (displayed text, a former value that differs from the current one, and
enforcedagainst the vocabulary); - the first entry either has
seq1, or a laterlog.truncatedentry withup_to_seq=seq− 1 andlast_digest=prevof the first entry is present; - every subsequent entry has the
seqof its predecessor plus 1 and itsprevis the predecessor's digest.
If an audit log is invalid, verification reports broken_at: the seq of the first entry in
file order that violates one of the conditions. The conditions are checked in the order
given: first the schema of all entries, then the start, then the chain.
An audit log without entries is valid. Verification reports the number of entries, because an empty log proves nothing: a log that was emptied looks the same.
What the chain proves, and what not. The chain is neither keyed nor signed. It shows that the entries of a log are consistent with one another: a change, a gap or a reordering in the middle breaks it. It does not show that the log is the one the implementation wrote. Whoever can write the log can
- remove the most recent entries or change the last one;
- recompute every digest from any entry onwards;
- replace the whole log with a new one that starts at
seq1; - delete a prefix and append a matching
log.truncatedentry.
A valid chain is therefore evidence only together with an anchor outside the log: a checkpoint (Section 9.5), or a copy of the log or of its latest digest kept where the writer of the log cannot change it.
9.5 Checkpoints
#A checkpoint anchors the log: a signed statement that the log with a certain identifier had a certain entry at a certain position. The key that signs it MUST NOT be available to whoever can merely write the log file; the public key is kept outside the log.
A checkpoint is an entry with event: log.checkpoint and
checkpoint: { "log_id": <UUID of the log>, "signature": <JWS> }It covers the log up to and including its predecessor: the entry with seq − 1, whose
digest is the checkpoint's prev. A checkpoint therefore never has seq 1.
log_idis a UUID in lower case that the implementation chooses once per audit log. Every checkpoint of a log carries the samelog_id; two logs never share one.signatureis a JWS in compact serialization with detached payload (RFC 7515 appendix F,header..signature), with the header and algorithms of Section 7.1. The payload is the canonical form (RFC 8785) ofjson { "type": "https://home-mandate.org/audit-checkpoint/v0", "log_id": <log_id>, "seq": <seq of the checkpoint entry − 1>, "digest": <prev of the checkpoint entry> }
Verification (Section 9.4) gains a fourth condition, checked after the chain:
- all checkpoints carry the same
log_id; and, if the verifier was given the public keys of the log, thelog_idis the expected one (if the verifier expects one) and every checkpoint's signature verifies with one of these keys.broken_atis theseqof the first checkpoint that violates this.
A verifier that was given keys additionally reports
- up to which
seqthe log is anchored: the position covered by the last checkpoint. Entries after it are consistent but not anchored; - the
seqof the first entry present, and, if the beginning of the log was deleted, whether thelog.truncatedentry that accounts for it is itself anchored (itsseqis not greater than the anchoredseq). Alog.truncatedentry after the last checkpoint can have been written by anyone who can write the log.
Without keys, the signatures are not checked and nothing is anchored. An implementation
MUST therefore write a checkpoint directly after every log.truncated entry, and a
relying party SHOULD give the verifier the log_id it expects: without it, the log of
another household signed with the same key would verify.
What a checkpoint adds: a log that was rewritten or replaced no longer matches the
checkpoints signed before, and nobody without the key can sign new ones. What it does not
add: it does not prevent the removal of entries after the last checkpoint, and a verifier
that does not know how far the log should reach cannot tell that the log and its last
checkpoints were removed together. Implementations SHOULD write a checkpoint at regular
intervals, and a party that relies on the log SHOULD keep the log_id and the highest
anchored seq it has seen.
10 Test interface
#So that an implementation in any language and of any shape can be tested, the test interface has two bindings. They are equivalent in what a passed case means; an implementation offers the one that fits it. Neither is needed in operation.
10.1 Safety
#The test interface lets its caller choose the mandates, the directory and the clock. An implementation MUST NOT offer it in operation: not in a release build, or only after an explicit start option that is off by default and that the implementation reports clearly. State set through it MUST NOT mix with the household's real mandates and audit log.
10.2 Process binding
#The implementation provides a program (a harness) that reads requests from its standard input and writes responses to its standard output: one JSON object per line (UTF-8, separated by line feed), one response for every request, in order. It needs no network and no server. The harness ends when its input ends.
Mandates, audit entries and logs are passed as JSON text inside a string, so that they arrive byte for byte (duplicate keys, number spellings and line separators are part of the cases).
op | Request members | Response members |
|---|---|---|
capabilities | – | name, version, ops (the operations offered) |
validate | mandate | valid; digest if valid |
evaluate | mandate, request | decision, reason, rule_id if Section 4.1 provides one, approval_timeout and approvers for ask and for a result that requires a confirmation, mandate_digest unless absent per Section 4.1, enforced: false for a deny that is not enforced (Section 4.6), confirm: true for one that requires a confirmation |
select | mandates (each mandate, optional revoked), subject (client_id, principal), request | as evaluate, plus selected (the id of the selected mandate) if one was selected |
succession | stored, offered | accept |
verify_signed | jws, issuer, keys (a JWK Set trusted for the issuer) | valid; digest if valid |
verify_audit | entries (array of entry texts) or jsonl (the exchange format); optionally keys and log_id | valid, entries (their number); broken_at if invalid; anchored if valid and keys were given |
entry_digest | entry | digest |
request is the input of the evaluation as the PEP determines it: resource
(entity_id, optionally category, areas (an array), critical), action, optionally parameters
(an object of names and values, each a JSON number or string), time, optionally timezone
and revoked. A time that is no RFC 3339 timestamp or a parameter that is neither an
integer nor a string yields deny with invalid_request. A string is never read as a
number: "50" is the string 50.
A request with a member that this section does not define is malformed and is answered with
an error, so that a former member name (such as area) is never evaluated as if it were
absent. For an operation it does not offer, the harness answers {"error": "unsupported"}; the
cases of that operation then count as not passed for their class. Any other error is a
failed case. Every request can carry id, the identifier of the case, for diagnostics.
→ {"op":"evaluate","id":"c02","mandate":"{…}","request":{"resource":{"entity_id":"lock.front_door","category":"lock","areas":["hallway","ground_floor"]},"action":"unlock","time":"2026-10-12T19:00:00+02:00"}}
← {"decision":"ask","reason":"rule","rule_id":"r-locks","approval_timeout":"PT2M","approvers":["user-1"],"mandate_digest":"sha256:…"}10.3 HTTP binding
#For the class pdp. The implementation offers its AuthZEN endpoint (Section 6) and one
additional control resource at a URL of its choice. Before every case the test tool
sets the complete state with
PUT <control>
{ "mandates": [ { "mandate": "<JSON text>", "revoked": false } ],
"directory": [ { "entity_id": "…", "category": "…", "areas": [ "…" ], "critical": false } ],
"time": "2026-10-12T19:00:00+02:00", "timezone": "Europe/Berlin" }which replaces everything set before: the stored mandates with their status, the resource
directory, the point in time the PDP uses as its clock, and the household time zone
(absent: the offset of time applies). The answer is a 2xx status. Then the tool sends the
request of the case to POST <authzen>/access/v1/evaluation with subject (type agent,
id, properties.principal), action (name, properties for parameters) and
resource.id only: category, areas and the critical marking come from the directory. It
compares decision and, in the response context, outcome, reason, rule_id,
approval_timeout, enforced and confirm.
The tool sends the HTTP headers it was given (for example Authorization) with every
request; the control resource SHOULD require authentication even in a test build.
10.4 Test tool
#mandate-conformance -exec <command> uses the process binding,
mandate-conformance -authzen <url> -control <url> the HTTP binding. The tool contains the
cases of its version. Its report (-report) is a JSON object with spec, manifest (the
SHA-256 of conformance/manifest.json), binding, implementation, per class the number
of cases passed, failed and skipped and whether the implementation conforms, and the list
of failed cases with what was expected and what was received. cmd/mandate-harness is the
harness of the reference code and an example for implementers.
11 Obligations of the PEP
#The evaluation (Section 4) is a pure function. This section defines what an implementation does around it. It states properties, not mechanisms: how a confirmation reaches a human, which algorithm limits the rate and how the parts of an implementation talk to each other is left open.
11.1 Approval
#If the result of the evaluation is ask, or a deny that is not enforced and requires a
confirmation (confirm: true, Section 4.6), the PEP obtains a confirmation from a human
before it executes the action. What follows holds for both.
- Who. A confirmation counts only if it comes from one of the
approversof the approval settings (Section 4.1), authenticated by the implementation. One approver suffices. An approver that the user management no longer knows does not count. If no approver can be reached, the result isdeny. - What is confirmed. A confirmation is bound to exactly one request: the agent, the
resource (for a request for several resources, the set the PEP resolved, Section 4.4),
the action, its parameters and the
mandate_digestof the evaluation. It MUST NOT be used for any other request, and the human MUST be shown what they confirm: the agent (display_nameandclient_id), the resource or every resource of the set, the action and the parameters. For adenythat is not enforced, the human SHOULD also be shown that the mandate in audit mode would deny the request, and by which rule: their answer then also says whether the household wants thedeny. The agent is not told (Section 11.6 item 2). Text supplied by the agent (a reason, for example) MUST be recognizable as such. - Once. A confirmation permits exactly one execution. A second use, a replay or a forged confirmation MUST be rejected; implementations use a value that cannot be guessed and accept it once.
- Independent of the agent. The confirmation MUST reach the PEP on a path that the requesting agent does not control. An agent MUST NOT be able to deliver, relay or answer a confirmation, its own or another agent's. A voice assistant that asks "shall I unlock?" and reports the answer itself is not a confirmation.
- In time. No answer within the
timeoutof the approval settings →deny(timeout). A rejection or an answer that is invalid (wrong person, wrong value, malformed) →deny. A request that ends earlier without an answer: item 8. The execution follows the confirmation without delay; a confirmation that is older thantimeoutwhen the action would be executed has expired. - Evaluated again. After the confirmation and immediately before the execution, the
PEP evaluates the request again. The action is executed only if the result is not
denyand themandate_digestis the one that was confirmed; adenythat is not enforced (Section 4.6) prevents the execution as well, and the request is recorded as denied by the mandate with theaskthat was confirmed (Section 9.1). If what was confirmed was itself adenythat is not enforced (confirm: true), the action is executed only if the result isallow,askor again adenywithconfirm: true; any otherdeny, enforced or not, prevents the execution, and the request is recorded as denied by the mandate with the evaluation that was confirmed. A mandate that was revoked, changed or has expired in the meantime, a time window that has closed and an emergency stop all prevail over the confirmation. For a request for several resources, the PEP resolves it again and executes only if the set is the one that was confirmed (Section 4.4); otherwise nothing is executed and the entry isfailed, with the RECOMMENDEDerrorresources_changed. - Limited. The number of approval requests of one agent that wait for an answer at
the same time MUST be limited (RECOMMENDED: 2). A request beyond the limit is
denywithout asking anyone. Requests that lead toaskcount towards the rate limit (Section 11.2). Both protect the approvers from being worn down by repeated requests. - Ended without an answer. A request also ends before its timeout when the agent
withdraws it, when the agent or its mandate is revoked, when the emergency stop is
activated, or when the process that made it stops. The action is then not executed, and
the outcome is
cancelledwith thatcause(withdrawn,revoked,emergency_stop,interrupted). An agent MAY withdraw its own request; withdrawing is no answer and cannot lead to an execution (item 4).cancelledis only for a request nobody had answered: if an answer was accepted before the end, the outcome is that answer (approved,rejected,invalid_response), and a confirmed action that is then not executed is denied as in item 6. A mandate that is changed or expires while a request waits does not end the request; item 6 decides at the execution. - Not across a restart. An approval request does not survive the end of the process
that made it. After a restart the implementation MUST NOT execute an action for a
request that was waiting, and MUST NOT reopen such a request or accept an answer to
it; it records every such request with
cancelledandcause: interrupted, as soon as it can, withapproval.atthe time it determined that the request had ended. If the execution of a confirmed action was under way when the process ended and its effect is unknown, the action MUST NOT be repeated; the entry isfailedwitherror: outcome_unknown(RECOMMENDED code). An implementation therefore keeps enough state outside the process to tell these cases apart. - The state that was shown. An implementation that shows the human the state of the
resource together with the request SHOULD execute only if the state is still the one
shown. Otherwise the action is not executed and the entry is
failed, with the RECOMMENDEDerroralready_in_stateif the state the action leads to has already been reached, andstate_changedin any other case. This keeps a confirmation from acting twice, for example when an agent repeated a request because its own client gave up waiting and both requests were confirmed. - What the resource can do. An implementation that knows that the resource cannot
carry out the action (for example because the platform reports the features of a
device) SHOULD NOT ask anyone for it: a confirmation of an action that cannot happen
only teaches people to confirm without reading. It then executes nothing, and the
entry is
failed, with the RECOMMENDEDerrornot_supportedand without anapproval. It SHOULD tell an agent whose mandate allows it toreadthe resource which of the actions its mandate does not deny the resource can carry out, and SHOULD NOT tell any other agent more than that the resource cannot carry out this one (Section 13). An implementation that does not know what the resource can do asks as before.
MCP elicitation (informative). Item 4 names no mechanism. For an agent that uses MCP:
- URL mode elicitation (MCP
2025-11-25and later) can be such a path. The person confirms on a page the PEP serves, the client opens it so that neither the client nor the model can see the page, and the PEP verifies who confirms. It satisfies item 4 when the PEP checks that the person who confirms is one of the approvers (item 1) and binds the page to exactly one request (items 2 and 3). - Form mode elicitation is not a confirmation in the sense of item 4. The answer passes through the agent's client, and the PEP cannot tell whether a person gave it.
- In both cases the person asked is whoever sits at the client, not necessarily an approver. A PEP still needs a path to the approvers, for agents without a person at the client.
Every outcome is recorded in the audit log (Section 9.1) with approval.outcome, who
answered and, optionally, through which channel; a cancelled request with its cause.
An implementation MAY keep a list of the humans who can answer approval requests, for
example to offer them as approvers of a new mandate. Adding someone to that list or
removing them from it is recorded with approver.changed (Section 9.2). A change to the
approvers of a mandate is a change to the mandate (mandate.updated).
11.2 Rate limit
#limits.max_actions_per_hour (N) bounds how often an agent can make the implementation
act or decide.
- In every period of 3600 seconds, at most N requests of the agent under the mandate
reach the evaluation. The count belongs to the mandate (
id), not to a version of it: a new version does not reset it. Further requests are denied without evaluation (denied_by: rate_limit). - Every request of the authenticated agent counts, whatever the action and whatever the
result:
read, requests that are denied and requests that lead toask. Requests denied by the rate limit itself or by the emergency stop do not count. - The bound is normative, the algorithm is not. A sliding window meets it; a token bucket that is full after an idle period does not, because it lets up to 2N requests pass within one hour.
- The count SHOULD survive a restart of the implementation; otherwise a crash resets the limit.
- A change of N applies to the next request.
So that an agent cannot fill the audit log with denials, consecutive requests of one agent
that the rate limit denies MAY be recorded as a single decision entry whose
result.count is the number of requests it stands for; request then describes the first
of them.
11.3 Revocation, emergency stop, removal and reconnection
#- A revocation of a mandate or of an agent takes effect with the next evaluation: no request that is evaluated after the revocation was recorded is permitted. An implementation MUST NOT cache decisions beyond a request.
- Approval requests of the agent that are waiting end with
denywhen the mandate or the agent is revoked, recorded withapproval.outcome: cancelledandcause: revoked(Section 11.1 item 8); Section 11.1 item 6 covers a confirmation that arrives at the same moment. - An emergency stop is optional. If an implementation has one, then while it is active
every request of every agent is denied without evaluation
(
denied_by: emergency_stop), waiting approval requests end withdeny(cancelled,cause: emergency_stop, Section 11.1 item 8), and activating and releasing it are recorded (emergency_stop.*). Only a human can release it. - An implementation MAY let a human remove a revoked mandate or a revoked agent from its
lists, and MAY remove them on its own under a rule the household configured, for example
once the audit log no longer holds entries about them. Only what is revoked is removed: an implementation
MUST NOT remove a mandate or an agent that is not revoked. An implementation that offers
revoking and removing in one step records
mandate.revokedoragent.revokedbeforemandate.removedoragent.removed. Removing an agent does not remove its mandates; an implementation that removes them together with the agent records amandate.removedfor each, after revoking those that are not yet revoked. Every removal is recorded (mandate.removed,agent.removed; Section 9.2). A removed mandate or agent stays revoked and never becomes active again. - Removal does not delete audit entries and does not weaken the protection against
rollback: mandate versions are kept as long as entries refer to their digest
(Section 9.3), and the highest
versionaccepted for anissuerandidis kept as Section 3.5 requires, so that an older version of a removed mandate is never accepted again. Beyond that, an implementation MAY delete the data of a removed mandate or agent. - An implementation MAY let a human reconnect an agent whose credentials it withdrew
without revoking the agent, for example when an emergency stop invalidated every token:
it issues new credentials to the existing agent instead of admitting it as a new one. The
agent keeps its
agent.client_id, its mandates and its history; withdrawn credentials stay invalid. Only an agent that is not revoked is reconnected, and only by a human: an implementation MUST NOT reconnect an agent on its own, for example because a new sign-in comes from the same OAuth client, which several agents can share. The reconnection is recorded withagent.reconnectedand the human who chose it asactor(Section 9.2); a change to the agent's mandates made at the same time is amandate.updated.
11.4 Clock and directory
#The validity period, time windows and the audit log depend on the clock of the PEP, the
evaluation on its resource directory. An implementation SHOULD synchronize its clock and
SHOULD deny requests while it has reason to believe the clock is wrong (for example a time
before the newest entry of its audit log). Changes to the directory are covered by
Section 3.4. Changes that affect the evaluation (the critical marking, renamed resources)
are recorded with directory.changed (Section 9.2).
When a resource is renamed, an implementation SHOULD keep applying the rules that name its former identifier to it, together with the rules that name the new one, and take the stricter decision (Section 4, most restrictive wins) until a human resolves the rename: takes it over into the mandates or dismisses it. A rename then fails closed.
11.5 Telling the agent its limits
#An implementation MAY tell an agent the limits of its own mandate, so that it can choose
a value that will be allowed instead of trying: for a resource and an action, the
constraints (Section 4.5) of the allow rules that match apart from their constraints,
for example after a request denied with no_match or in a list of what the agent may do.
It states them in the unit in which the agent gives the value. When it does:
- It MUST NOT name limits for a resource whose
readthe agent's mandate denies, limits of another agent's mandate, or anything of adenyoraskrule. Limits never reveal more than the values the agent could try anyway. - It MUST NOT name limits that cannot apply: a constraint on a parameter the PEP never derives for the resource (a temperature in the other unit, Section 4.5).
- It SHOULD NOT name limits where rules on a former identifier of a renamed resource (Section 11.4) still apply: they can decide more strictly than the limits say.
The limits inform; they decide nothing. Every request is evaluated as in Section 4.
Audit mode (Section 4.6) is not part of what an agent is told. An implementation MUST NOT
tell an agent that its mandate is in audit mode or that a request was not enforced, and
the limits it names are those of the allow rules, as without audit mode (Section 11.6).
11.6 Audit mode
#An implementation that offers audit mode (Section 4.6):
- Executes a request whose result is not enforced (
enforced: false) as it executes anallow, with every obligation that applies to anallow, and records adecisionentry whoseevaluationis the result as returned, includingenforced: false, withresult.statusexecutedorfailed(Section 9.1). If the result requires a confirmation (confirm: true), it executes the request only as it executes a confirmedask, with every obligation of Section 11.1, and the entry records the outcome of the approval request as for anask. It never records a request whose result is not enforced as denied by the mandate, except a confirmed one that the evaluation before the execution denied (Section 11.1 item 6) or one whose mandate was revoked while it waited (Section 11.1 item 8), and it never executes adenythat is enforced. - Tells the agent nothing. The agent receives the same response as for an
allowof the same request, or, if the result requires a confirmation, as for anask: no hint of the audit mode, of the denial or of the rule. An agent that learned which of its requests the mandate will deny could use the trial to act before audit mode ends, or adapt its requests to the trial. - Shows audit mode to the household. Wherever an implementation shows a mandate or a
list of mandates, it MUST show clearly that a mandate in audit mode does not enforce
its denials, and it SHOULD say so before such a mandate is stored or audit mode is
switched on. It SHOULD let the household see the requests that were not enforced (the
entries with
enforced: false), so that the trial ends with a decision. Switching audit mode on or off is a change to the mandate and is recorded withmandate.updatedand the actor (Section 9.2); no event of its own is needed, because the digest names the version, and the implementation keeps every version an entry refers to (Section 9.3). It SHOULD warn wherever it shows such a mandate if a resource the directory marks as critical can be read under it:readis never critical, so audit mode does not protect what such a resource shows (Section 13). - Lets audit mode be switched on only by a human. A mandate that switches audit mode
on (Section 3.5: no mandate with its
idin audit mode is in force, which includes a mandate arriving from outside or restored from a backup) MUST NOT take effect before a human of the household has confirmed it explicitly, in the user interface of the implementation and on a path the agent does not control (Section 11.1 item 4). The confirmation names the digest of exactly the version that is adopted and counts for no other; a signed successor is not adopted automatically. The implementation MUST notify every approver of the mandate when such a mandate is proposed, while it waits for the confirmation, and again when audit mode takes effect. Switching audit mode off takes effect without a confirmation.
An implementation is not required to offer audit mode. One that does not MUST refuse to
store a mandate with enforce: false instead of storing it and enforcing it, so that the
household never believes a mandate is being tried while it enforces, or the reverse.
12 Security considerations
#12.1 What the specification protects
#Assets: the physical security of the home (locks, gates, alarm), the privacy of its inhabitants (cameras, presence, habits) and the integrity of the record of what agents did.
12.2 Attackers
#| Attacker | What the specification does | What remains |
|---|---|---|
| Agent that is malicious or was manipulated (prompt injection through a website, an e-mail, a document) | The decision is made outside the agent. Default deny, most restrictive rule wins, critical actions need allow_critical or a human (Section 4). The agent supplies only the resource and the action; category, areas, time and status come from the PEP. Rate limit and a limit on waiting approval requests (Section 11) | Everything the mandate allows, the agent can do. A mandate with a broad allow is a broad permission. An automation or a rule on the platform that reacts to an allowed action on a harmless-looking resource (a switch, a virtual datapoint) does what the household would not let the agent do directly; the PEP sees only the switch. Such a resource is marked critical (Section 12.4) |
| Agent that tries to obtain a confirmation | The confirmation is bound to one request, used once, comes from a named approver on a path the agent does not control, and the request is evaluated again afterwards (Section 11.1) | A human who confirms without reading. The implementation MUST show what is confirmed; it cannot make the human read |
| Agent that imitates a trusted name | Displayed text carries no invisible, blank or direction-changing characters and no white space other than the space (Section 3.1 item 8); identifiers are ASCII and compared exactly (Sections 3.3, 3.4) | Look-alike letters and names, stacked combining marks, characters that a font does not have. Interfaces SHOULD show the client_id next to the display_name |
| Member of the household who may change the resource directory but not the mandates | Implementations MUST report mandates affected by a renamed resource or a changed area (Section 3.4); rules that name a former identifier keep applying to a renamed resource until a human resolves the rename (Section 11.4); changes to the critical marking and renames are recorded (Section 9.2) | Rules by area stop restricting when a resource leaves the area. Restrictions SHOULD name the category or the resource. Setting a critical mark only adds protection, removing one lowers it; the audit log shows who did it, it does not prevent it |
| Attacker who can write the audit log | Hash chain; checkpoints signed with a key the attacker does not have; verification reports how far the log is anchored and whether a deleted beginning is covered by an anchored log.truncated (Sections 9.4, 9.5) | Entries after the last checkpoint, and a beginning deleted with a log.truncated written after it. Removal of the end of the log together with its last checkpoints, unless the relying party remembers how far the log reached. Without checkpoints, the chain proves consistency only |
| Attacker who replays an old mandate | version and succession (Section 3.5); signature of the issuer (Section 7) | Mandates without version are not ordered. The protection is the receiver's memory of the highest version; a receiver that lost it (new installation, restored backup) accepts an old signed mandate until it expires |
| Issuer that is trusted for something else | A verifier decides per issuer for which principals and agents it may issue mandates (Section 7.1); a replacement keeps agent and principal (Section 3.5) | The specification does not define how this trust is configured |
| Attacker between PEP and PDP | The connection MUST be authenticated in both directions; the PDP MUST NOT be reachable by agents (Section 6) | The specification does not define the mechanism |
| Whoever can change a mandate and switches it to audit mode (a member of the household, a manipulated user interface, an issuer) | Critical actions, ask and every denial outside the rules stay enforced (Section 4.6). Switching audit mode on takes effect only after a human of the household confirmed it explicitly, also for a signed successor, and every approver is notified (Sections 3.5, 11.6). The switch is a new version with a new digest, recorded with mandate.updated and its actor (Sections 3.2, 9.2), and implementations MUST show audit mode wherever they show mandates (Section 11.6). In a signed mandate, enforce is covered by the signature (Section 7); a verifier rejects an audit entry that claims an unenforced deny of a critical action (Section 9.1) | In audit mode the agent can do every action that is not critical, including read of cameras and presence sensors, without asking unless a rule sets it to ask. Whoever can change mandates could also write a broad allow; audit mode gives no more power, but it looks harmless. A human who confirms without reading; the confirmation and the notice make the switch visible, they do not prevent it |
| Agent with a path past the PEP (another credential for the platform, for example an MCP tool that talks to the platform with its own API token, or the right to create or change scripts, automations or rules) | Nothing. The evaluation covers only the requests that reach the PEP (Section 12.3, the only path) | Whatever the agent can do on that path, no mandate limits. A script or an automation it writes can scan the network, load programs or unlock the front door |
| Compromised PEP | Nothing. The PEP executes; a PEP that ignores a deny cannot be stopped by a format | – |
12.3 Assumptions
#The evaluation is only as trustworthy as its inputs. A conforming implementation relies on:
- the resource directory: it assigns category, areas and the critical marking. A wrong
category makes the wrong vocabulary and the wrong rules apply, a wrong set of areas the
wrong rules by
area; - the clock: validity periods and time windows depend on it (Section 11.4);
- the user management: it decides who an approver is and authenticates the person;
- the approval channel: it reaches the approver and nobody else can answer on it;
- the authentication of the agent: the specification assumes that the agent behind
a
client_idwas authenticated; how is outside its scope; - the only path: the agent reaches the platform only through the PEP. It holds no other credential for the platform (an API token, a user account) and cannot create or change what executes on the platform: scripts, automations, rules, scenes, adapters, integrations, code. Whatever the agent can do past the PEP, no mandate limits;
- the keys: of issuers (Section 7) and of the audit log (Section 9.5).
12.4 Advice for implementers
#- Broad mandates.
{"any": true}with"*"and noexpiresis valid. Interfaces SHOULD warn before such a mandate is stored, SHOULD require a separate confirmation for every rule withallow_critical, and SHOULD suggest anexpiresfor mandates that allow critical actions without approval. - Indirect effects. A resource can act on others: a switch on a door opener, a scene that unlocks. The vocabulary cannot know this; the critical marking of the resource (Section 4, step 5) is the means for it.
- Automations that react. An automation or a rule on the platform (a Home Assistant automation, a Node-RED flow, an n8n workflow) can react to a harmless-looking resource, a switch or a virtual datapoint, and unlock a door. An allowed command to the switch then starts what the household would not let the agent run directly; the PEP sees only the switch. A resource that automations react to SHOULD be marked critical. The critical marking relies on the household knowing which resources these are, so where the platform can tell which automations or rules react to a resource, implementations SHOULD show it to the household and suggest marking the resource critical. openHAB, for example, returns the triggers of every rule through its REST API, among them a command to an Item and a change of its state.
- Platform configuration. Writing scripts, automations, rules, scenes or integrations
is not an action on a resource, and no mandate limits what such code does once it runs
(Section 12.3, the only path). Implementations SHOULD NOT offer tools that write platform
configuration through the PEP. One that does MUST keep them outside the mandate model and
MUST NOT let a mandate
allowthem. A request for configuration is an action outside the vocabulary and thereforedeny(Sections 4 and 5.1). - The PEP's own credential. The credential with which the PEP acts on the platform SHOULD be as restricted as the platform allows (a restricted user or profile), so that the platform itself refuses configuration changes and the mandate narrows down what remains. The agent MUST NOT receive this credential.
- Reading is not harmless.
readis never critical, but presence sensors, door contacts and cameras reveal who is at home. Mandates SHOULD allowreadper category, not forany. - Parameters. Only parameters of the vocabulary can be constrained. Whatever else a platform accepts with a command passes unchecked; a PEP SHOULD forward only what it has evaluated (Section 4.5).
- Mapped values. Where a profile maps several values of a platform to one permitted
value (Home Assistant
heat_coolandautoboth toauto), a rule that permits the value permits all of them. Interfaces SHOULD show the household which platform values a permitted value stands for. - Renamed resources. A
denyoraskrule that names the former identifier of a renamed resource no longer protects it, and a broaderallowrule may apply instead. Applying the rules of both identifiers until a human resolves the rename keeps the protection (Section 11.4). - Audit mode. A mandate in audit mode permits every action that is not critical,
without a confirmation unless a matching rule decides
ask(Section 4.6). Interfaces SHOULD suggest a shortexpiresfor it, SHOULD remind the household of a mandate that stays in audit mode for long, and SHOULD show the requests that were not enforced, so that they become rules or audit mode is switched off. What must stay protected during a trial is marked critical in the directory (Section 4, step 5). - Size and time. Implementations SHOULD bound the size of inputs before parsing them (Section 3.1 item 5, nesting depth) and SHOULD verify signatures and schemas before any further processing.
13 Privacy considerations
#The audit log records who caused which device to do what and when. It is a record of the behavior of the people in a household and MUST be protected like the devices themselves.
- Minimization. Entries contain identifiers, not content: no tokens, no mandate text
(Section 9.1), and no state that a
readreturned. Implementations SHOULD NOT add free text. - Retention. How long entries are kept is up to the implementation and SHOULD be
configurable by the household. The chain permits deletion from the oldest end
(
log.truncated); a checkpoint SHOULD precede it. - Persons.
actor.id,approval.byandapprover.idname people. Implementations SHOULD use identifiers from their user management rather than names, so that a person's name does not have to be removed from a chained log. Removing the data of a single person from the middle of a log is not possible without breaking the chain; this version offers no remedy other than retention limits. - Export. An exported log leaves the protection of the implementation. Exports SHOULD be created only by a human and SHOULD be recorded.
- Agents. What an agent learns through
readleaves the household with the agent. A mandate is also a decision about which data an agent may see. - Features. What a resource can carry out is information about it, often about its
model. An agent learns it only as far as it may
readthe resource (Section 11.1 item 11); an agent that probes actions on resources it may not read learns little, and every probe counts against its rate limit, is recorded, and is executed or asked for whenever the resource can carry it out. - Audit mode.
readis never critical, so a mandate in audit mode lets the agent read whatever it asks for, unless a rule sets thereadtoask(Section 4.6). A household that tries a mandate SHOULD be told that the agent can see more during the trial than the mandate will permit afterwards.
14 Versions and compatibility
#- Formats. This section covers three formats: the mandate
(
https://home-mandate.org/mandate/v0), the audit entry (https://home-mandate.org/audit/v0) and the audit checkpoint (https://home-mandate.org/audit-checkpoint/v0, the signed payload of Section 9.5). Thetypeof each names its major version. The identifiers are identifiers, not addresses that must resolve. - Releases. Releases of this repository are tagged (
v0.1.0, …). An implementation states which tag it conforms to, andconformance/manifest.jsonidentifies the exact cases. - Freeze. The v0 formats are frozen from
v0.1.0, the first release of this specification without a pre-release suffix. From then on, an incompatible change to a format MUST NOT be made under its existingtype; it requires a newtypewith the next major version (e.g.https://home-mandate.org/mandate/v1). A document that is valid under a released format stays valid, and is evaluated the same way, in every later release. - Incompatible. A change is incompatible if a document that was valid becomes invalid,
or if a valid document is evaluated differently: a different result of the evaluation
rule (Section 4) or of succession (Section 3.5), a different digest (Sections 3.2, 9.4),
or a different outcome of the verification of an audit log (Sections 9.4, 9.5). A change
that only makes documents valid that were invalid before, such as a new optional member
or a new audit event, is compatible; it is listed in the Changelog and keeps the
type. - Mixed data. Once a newer major of a format exists, data of both majors exist side by
side:
- An implementation that supports a newer major MUST still read the documents of the older majors it supported before, and MUST write new audit entries and checkpoints in the newest major it supports. An audit log is not rewritten; it continues in the newer major.
- A verifier MUST accept an audit log whose earlier entries carry an older major than
its later entries, provided every entry conforms to the schema of its own
typeand the chain (Section 9.4) holds across the change. An entry of an older major after an entry of a newer major makes the log invalid. A checkpoint is verified with the payloadtypethat belongs to the major of the checkpoint entry. - A mandate moves to a newer format only by succession (Section 3.5): its issuer issues
the same
idwith a higherversionin the newer format. A stored or signed mandate MUST NOT be rewritten into another format, because its digest, its signature and the audit entries that refer to it would no longer match.
- Until v0.1.0. The pre-releases (
v0.1.0-alpha.1, …) are not covered by the freeze: untilv0.1.0, the v0 formats can still change incompatibly under the sametype. Every such change is listed in the Changelog together with what happens to documents that were valid before. - Unknown members. Mandates and audit entries have no extension points: a member that the schema does not define makes the document invalid. This is deliberate. An implementation that silently ignored a restriction it does not know would allow more than the household intended.
- Consequence. A newer mandate that uses a member an older implementation does not know, or a newer major, is invalid there and denies everything. Implementations SHOULD therefore state which version they support before a mandate is transferred to them, and issuers SHOULD NOT use members or majors the receiver does not support.
- Independent versions. The vocabulary and extensions carry their own
idandversion(Section 5.1), are versioned independently of the formats and never change under the same pair; a new vocabulary version does not require a newtype. The$ids of the conformance schemas (https://home-mandate.org/conformance/v0/…) identify test material, not formats, and change independently of this section.
Changelog
#Unreleased
#Renamed: the specification formerly published as mandate-spec is now the Home-Mandate
Specification; repository github.com/home-mandate/spec, Go module
github.com/home-mandate/spec, root package spec. Versions start again at
v0.1.0-alpha.1. The releases of mandate-spec (up to v0.2.0-alpha.5) remain available
from the Go module proxy under the module path github.com/mandate-spec/mandate-spec; their
changes are listed below.
Audit mode (Sections 2, 3, 3.1, 3.2, 3.5, 4, 4.1, 4.4, 4.6, 6, 8, 9.1, 9.2, 9.4, 10, 11, 12, 13):
- New optional member
enforceof the mandate, only everfalse(true, a string, a number ornullare invalid, and a rule has noenforce). A mandate withenforce: falseis in audit mode: adenythat results from its rules (no_matchor adenyrule) for an action that is not critical is evaluated and recorded but not enforced; the PEP executes the request as if it were allowed. Critical actions,ask,allow, every denial before the rules (invalid mandate or request, unknown resource, category or action, revoked, not yet valid, expired) and every denial outside the evaluation (rate limit, emergency stop, authentication, approval) are enforced as before. The mandatetypestayshttps://home-mandate.org/mandate/v0. Compatible: every mandate that was valid stays valid, is evaluated the same way and keeps its digest. - The result of the evaluation carries
enforced: falsefor such adeny; the decision staysdeny. In AuthZEN it is a member of the response context (Section 6); a PEP that does not know it enforces the denial. In the test interface it is a member of theevaluateandselectresponse and compared over HTTP (Section 10); the conformance cases have the new fieldenforced. - Audit entry:
evaluation.enforced(onlyfalse, only for adecisionwithevaluation.decision: denyand reasonno_matchorrule); only with it may adenybeexecutedorfailed, and it is neverdeniedunless it also carriesconfirm(see "Audit mode andask" below). The audittypestayshttps://home-mandate.org/audit/v0. Compatible for logs: every entry that was valid stays valid. A verifier that does not know the member rejects a log that contains it (Section 14, unknown members). - With
evaluation.enforced: false, an entry namesrequestwith thecategoryof the resource; it is invalid for a resource markedcritical(except forread), and a verifier MUST reject it for an action that is critical in the vocabulary or not in it (Sections 9.1, 9.4);request.resource.criticalis then required,trueorfalse, so that the marking cannot be left out. The reference verifier (audit) checks this against vocabulary v0 and against extension vocabularies it is given (audit.Options.Vocabularies,audit.VerifyWith,audit.VerifyJSONLinesWith); without the vocabulary of an extension, a verifier cannot check its critical actions. - Obligations of the PEP (Section 11.6): the agent is told nothing about audit mode, nor
which resources of a request for several were not enforced (Section 4.4);
implementations MUST show audit mode wherever they show mandates and SHOULD warn when a
resource marked critical can be read under it; switching it is a
mandate.updated. An implementation that does not offer audit mode MUST refuse to store a mandate withenforce: false. - Switching audit mode on (a mandate with
enforce: falsewhile no mandate with itsidin audit mode is valid, active and in force, which includes a predecessor that is revoked, expired or not yet valid, a mandate from outside and a restore from a backup) MUST be confirmed explicitly by a human of the household before it takes effect, also for a signed successor. The confirmation is bound to the digest of the version that is adopted. Every approver MUST be notified when the switch is proposed and when it takes effect (Sections 3.5, 4.6, 11.6). Switching it off needs no confirmation. - A confirmed
askis not executed if the evaluation before the execution yields anydeny, enforced or not; it is recorded with the confirmedask,approvedanddenied_by: mandate(Sections 9.1, 11.1 item 6). - Conformance cases
c174–c192(new mandateconformance/mandates/audit-mode.json),i144–i148,d43–d44,a111–a125. - Reference evaluator (Go):
Result.NotEnforced,Mandate.EnforceandEntersAuditMode(whether adopting a mandate switches audit mode on, compared with the stored mandate only if it is valid, active and in force).
Permitted values and string parameters (Sections 3, 3.1, 3.2, 4, 4.5, 5, 6, 8, 9.1, 10, 12):
- A parameter is an integer or a string. The vocabulary declares the type of every
parameter (
type:integerwith aunit, orstring, optionally with permittedvalues); strings follow the rules of an area: 1 to 64 printable ASCII characters without space, compared exactly and case-sensitive. There are still no fractions. Incompatible for extension vocabularies (beforev0.1.0): they must declare thetypeof every parameter and list at most 32 permitted values; mandates stay valid. - New constraint
one_of: a list of 1 to 32 distinct permitted values of the parameter's type, for integer and string parameters; not together withminormax, which exist only for integer parameters. A rule withone_ofmatches only if the request carries the parameter with one of the values. A constraint that does not fit the declared type, or a string value outside the permitted values of the vocabulary, makes the mandate invalid (Section 3.1 item 10). Every mandate that was valid stays valid: a constraint withminormaxon a parameter of the vocabulary was on an integer before as well. climate.set_modegets the string parametermodewith the permitted values of the Matter Thermostat SystemMode:off,auto,cool,heat,emergency_heat,precooling,fan_only,dry,sleep. A household can now allow heating modes but neveroff. The vocabulary keepsv0: it changes before the formats are frozen withv0.1.0.other.setstays critical and without parameters.- A request is invalid (
invalid_request) if a parameter is neither an integer nor such a string, if a parameter the action declares has another type ("50"forbrightness,4formode), if a string is not among the permitted values, or if an integer lies outside the range of the vocabulary (brightness,positionandvolume0 to 100;temperatureandtemperature_fhave none). Before, the range was informative, andbrightness: 101was evaluated like any other value. Constraints beyond the range stay valid but never match. Before,set_modehad no parameters, and a PEP could pass any value without effect. - In the test interface (Section 10), in AuthZEN (
action.properties) and in the audit entry (request.parameters), a parameter value is a JSON integer or, new, a JSON string. Incompatible: a string is never read as a number; the harness of the reference code formerly accepted"50"as the integer 50. The audittypestayshttps://home-mandate.org/audit/v0: the change comes before the formats are frozen. The audit loga37carries a boolean instead of the string"40"(a string is now a valid value) and is chained anew; its expected result is unchanged. - Numbers are read by their exact value: a fraction is never rounded to an integer, however
small it is (
50.00000000000000000001,1e-400). The reference code formerly read request parameters through floating point and took such values for integers. - Conformance cases
c152–c173(new mandateconformance/mandates/one-of.json),i129–i143,d41–d42,a109–a110. - Profiles: Home Assistant and Matter map their climate modes to the values of
mode; Home Assistantheat_coolandautoboth map toauto(Section 12.4). - Reference evaluator (Go), before
v0.1.0:Request.Parametersis amap[string]Value;Valueis an integer or a string (IntegerValue,StringValue) and reads and writes itself as JSON;Limit.OneOfholds the permitted values of a constraint.
Sets of areas (Sections 1.1, 2, 3.4, 4, 6, 8, 9.1, 10, 12):
- A resource has a set of 0 to 16 areas instead of at most one: its room and every
location above it (for example garage, ground floor, home), or several rooms side by
side. A rule still names exactly one
areaand matches if that area is in the set; the mandate format does not change, and every mandate that was valid stays valid. A request with more than 16 areas, the same area twice or an area that is no identifier is invalid (invalid_request). The duty to report affected mandates covers every area in the set. - Incompatible: in the request of the test interface, in the directory of the HTTP binding,
in AuthZEN (
resource.properties.areas) and in the audit entry (request.resource.areas),area(a string) becameareas(an array). An audit entry withrequest.resource.areais invalid. The audittypestayshttps://home-mandate.org/audit/v0: the change comes before the formats are frozen withv0.1.0. The audit logs inconformance/audit-v0.jsoncarryareasand are chained anew; every expected result is unchanged. - Conformance cases
c140–c151(new mandateconformance/mandates/areas.json) anda106–a108. - Test interface (Section 10): a request with a member the section does not define is
malformed, so that a former
areais not evaluated as if it were absent.
Temperatures in degrees Fahrenheit (Sections 4.5, 5):
- New parameter
temperature_fofclimate.set_temperaturein hundredths of a degree Fahrenheit, next totemperaturein hundredths of a degree Celsius, so that households whose platform uses °F can constrain temperatures exactly. The PEP derives exactly one of the two, in the platform's unit, and never converts. Compatible for implementations that load the vocabulary: a constraint ontemperature_fwas invalid before. The vocabulary keepsv0: the parameter is added before the formats are frozen withv0.1.0; from then on Section 5.1 holds without exception. Conformance casesc133–c139,i128. - Section 4.5 says plainly that a value that cannot be expressed as an integer in the unit makes the PEP refuse the request; it never leaves the parameter out.
- Section 11.5 (new): a PEP MAY tell an agent the limits of its own mandate (the
constraints of the
allowrules that match apart from them, in the agent's units), but never for a resource the agent may not read, of another mandate, ofdenyoraskrules, in a unit it never derives, or while rules on a former identifier still apply. The reference evaluator (Go) offersMandate.Limitsfor it; it decides nothing.
Overview (Sections 1.1 and 2, informative, no change to any rule):
- Section 1.1 (new) shows in a diagram how agent, implementation (PEP, PDP, audit log), smart-home platform and human work together, which of these interfaces the specification defines and which it leaves open. It names MCP as the agent interface of the reference implementation, as an example.
- Section 2 defines PDP and PEP.
The only path to the platform (Sections 12.2, 12.3, 12.4, text only, no change to any rule or format):
- Section 12.3: new assumption that the agent reaches the platform only through the PEP, holds no other credential for it and cannot create or change scripts, automations, rules or other code that executes on it. Section 12.2: new row for an agent with a path past the PEP.
- Sections 12.2 and 12.4: automations and rules that react to a harmless-looking resource; such a resource is marked critical, and interfaces show the household which resources automations react to where the platform can tell (openHAB as the example).
- Section 12.4: no tools that write platform configuration through the PEP (a request for
configuration is
deny); the PEP's own platform credential is as restricted as the platform allows and never given to the agent.
MCP elicitation (Section 11.1, informative, no change to any rule):
- Note after the list of Section 11.1: URL mode elicitation (MCP
2025-11-25and later) can be a confirmation path in the sense of item 4 when the PEP checks that the person who confirms is an approver and binds the page to exactly one request; form mode elicitation cannot, because the answer passes through the agent's client. In both cases a PEP still needs a path to the approvers.
Confirmations for several resources (Sections 4.4, 11.1):
- A confirmation for a request that the PEP resolves into several resources covers exactly the resolved set; the human is shown every resource of it (Section 11.1 item 2).
- Before the execution the PEP resolves the request again and executes only if the set is
unchanged; a resource added, removed or replaced in the meantime prevents the execution
for the whole set, also when the mandate is unchanged and every resource still evaluates
to
askorallow(Section 11.1 item 6). New RECOMMENDED coderesources_changed(failed). No change to any format. - The PEP executes a request for several resources on exactly the resources it evaluated and names each of them to the platform, never the area, group or list it resolved them from, so that the platform cannot add a resource that was not evaluated (Section 4.4).
Versions (Section 14, rewritten):
- The v0 formats (mandate, audit entry, audit checkpoint) are frozen from
v0.1.0, the first release without a pre-release suffix. After that, every incompatible change requires a newtypewith the next major version; a document that was valid stays valid and is evaluated the same way. Untilv0.1.0, incompatible changes remain possible and are listed here. - Defined what is incompatible: a valid document becomes invalid, or a valid document is evaluated differently (result, succession, digest, verification of an audit log).
- Rule for mixed data: implementations read the older major and write the newer one; a verifier accepts an audit log whose earlier entries carry an older major; mandates move to a newer format only by succession and are never rewritten.
- The vocabulary (Section 5.1) and the
$ids of the conformance schemas are versioned independently of the formats.
Incompatible:
typeof mandates, audit entries and checkpoints and the schema$idmoved fromhttps://mandate-spec.org/…tohttps://home-mandate.org/…, e.g.https://home-mandate.org/mandate/v0. Mandates with the old identifier are invalid (conformance casei127). Data model and evaluation are otherwise unchanged.- Because the
typeis part of every digest and signed payload, all digests, audit chains, checkpoint signatures and signed mandates inconformance/are new. Cases that are invalid on purpose keep their defect.
New:
- Audit log: event
template.changedwith the membertemplaterecords changes to templates, the stored mandate drafts from which an implementation creates mandates (Section 2): stored, removed, hidden and shown again, with the name and digests but never the content. Eventapprover.changedwith the memberapproverrecords humans added to or removed from the implementation's list of approvers (Section 11.1). Both have an actor of kinduserorsystem;template.nameandapprover.idare displayed text, and aprevious_digestequal todigestmakes an entry invalid (Sections 9.1, 9.2). Compatible for logs: every entry that was valid remains valid. A verifier that does not know the events rejects a log that contains them (Section 14, unknown members). Conformance casesa58–a76inconformance/audit-v0.json. - Audit log: events
mandate.removedandagent.removedrecord that a revoked mandate or a revoked agent was removed from the implementation's lists, with the existing membersmandate(idand the digest of the last version) andagent; theiractoris of kinduserorsystem. Eventagent.reconnectedwith the memberagentrecords that a human gave new credentials to an existing agent that is not revoked, for example after an emergency stop; itsactoris of kinduser(Sections 9.1, 9.2, 11.3). Only revoked mandates and agents are removed; removal deletes no audit entries and keeps the protection against rollback (Section 3.5). Compatible for logs: every entry that was valid remains valid. A verifier that does not know the events rejects a log that contains them (Section 14, unknown members). Conformance casesa77–a88inconformance/audit-v0.json. - Approval requests that end without an answer before their timeout: new outcome
cancelledwith the memberapproval.cause(withdrawnby the agent,revoked,emergency_stop,interruptedby the end of the process), only withresult.status: deniedand thedenied_bymatching the cause; nobody answered, so noby(Sections 9.1, 11.1 items 8 and 9). A request that a revocation or an emergency stop ends is recorded with this outcome instead of without anapprovalobject. After a restart no waiting request is executed, reopened or answered; an execution whose effect is unknown is not repeated (failed, RECOMMENDEDerror: outcome_unknown). New RECOMMENDED codesalready_in_stateandstate_changedfor a confirmed action that is not executed because the state shown to the human no longer holds (Section 11.1 item 10). Compatible for logs: every entry that was valid remains valid. A verifier that does not know the outcome rejects a log that contains it (Section 14, unknown members). Conformance casesa89–a105inconformance/audit-v0.json.
Actions the resource cannot carry out (Section 11.1 item 11): an implementation that knows
that a resource cannot carry out the action SHOULD NOT ask anyone for it; the entry is
failed with the new RECOMMENDED code not_supported and without an approval. Which
actions the resource can carry out is told only to an agent that may read it (Section 13,
new item on features). No change
to the evaluation, to the vocabulary or to the format of mandates and logs. The profile for
Home Assistant names where the features of an entity come from.
Conformance cases (no change to any rule or format):
- Areas:
c193–c199(an area of 64 characters, the edge characters!and~, U+007F, non-ASCII, an empty area,Homenext tohome, a set in another order); two rules added toconformance/mandates/areas.json. - Parameters:
c200–c209,c212,c213(2^53 and 2^53 − 1, the spelling5e1, the values ofmodethatone_ofdoes not list, a constraint beyond the range of the vocabulary),d45–d48andi149(one_ofwith 32 values, a string value of 64 characters,min/maxbeyond the range,one_offormodein a rule without category). - Audit mode:
c210,c211(new mandateconformance/mandates/audit-mode-critical.json: adenyrule on a resource marked critical),s20(selection; the selection cases acceptenforcedas the evaluation cases do),v13,v14(succession switching audit mode on and off),a127–a129. - Selection with a set of areas:
s21. Audit log:a126(not_supported, Section 11.1 item 11),a130–a133(16 and 0 areas, a fraction and an empty string as parameter).
What request records (Section 9.1, text only, no change to any rule or format):
requestrecords the input to the evaluation as the PEP determined it, also when the evaluation found it invalid (invalid_request); identifiers in it are limited in length and number, not in their characters, as the schema has always had it.- Input that the log cannot record (a longer identifier, more than 16 areas or areas that
repeat, more than 16 parameters, a parameter value that is neither an integer nor a
string of the form of Section 4.5) is not a request: the PEP rejects it at its interface
before any evaluation. Conformance cases
a134(valid, an identifier with a space) anda135(invalid, anentity_idof 256 characters).
Audit mode and ask (Sections 4, 4.1, 4.4, 4.6, 6, 8, 9.1, 10, 11.1, 11.6, 12, 13; issue #59):
- In audit mode, a
denyrule that wins over a matchingaskrule no longer takes the confirmation away. Thedenystays the decision and is not enforced, but the result requires a confirmation: it carriesconfirm: trueand the approval settings of theask(Section 4.1), and the PEP executes it only after a confirmation, as anask(Sections 4.6, 11.1, 11.6). Before, such a request was executed without asking anyone, while the same request without thedenyrule needed a confirmation. Adenywithout a matchingaskrule is executed without a confirmation as before; critical actions are enforced as before. - New member
confirm(onlytrue, only withenforced: falseandreason: rule) of the result of the evaluation, of the AuthZEN response context (Section 6, REQUIRED there together withapproval_timeoutandapprovers), of theevaluateandselectresponses of the test interface and of the conformance cases (Sections 8, 10), and ofevaluationin the audit log (Section 9.1). A PEP that executes adenywithenforced: falseMUST knowconfirm; one that does not knowenforcedstill enforces everydeny. Implementations that already offer audit mode MUST addconfirm, or they execute such a request without asking anyone. - Audit entry: an
approvalobject appears withevaluation.decision: askor withevaluation.confirm: true. Withconfirm: true,executedrequiresapproval.outcome: approved, and theresultcan be anything anaskcan have; without it, adenywithenforced: falseis still onlyexecutedorfailed. Every entry that was valid stays valid. - Before the execution (Section 11.1 item 6), a confirmed request with
confirm: trueis executed only if the evaluation then givesallow,askor again adenywithconfirm: true, so that a time window that has closed prevails as for anask; a confirmedaskis still not executed after anydeny. In the audit log,denied_by: mandatewithconfirm: truerequires an approved or a revoked request. The human who confirms SHOULD be shown that the mandate in audit mode would deny the request, and by which rule (Section 11.1 item 2); the agent is not told. - Conformance cases
c214–c223(new mandateconformance/mandates/audit-mode-ask.json),s22(the selection cases acceptapproval_timeoutandconfirm),a136(valid: every outcome of an approval request withconfirm: true) anda137–a145(invalid). - Reference evaluator (Go):
Result.Confirm;Result.Approvalis also set with it.
History as mandate-spec
#mandate-spec v0.2.0-alpha.1 to v0.2.0-alpha.5
#Incompatible; mandates that were valid before can become invalid, and an invalid mandate denies every request. Implementations SHOULD check their stored mandates before they update:
agent.client_id(new Section 3.3): https URLs need a lower-case host and a path and must not contain userinfo, a fragment, white space or non-ASCII characters; no dot segments. Newly permitted: other URIs such asdid:…,spiffe://…,urn:…. Identifiers of the form<namespace>:<id>remain valid.- Timestamps: at most 9 fractional digits; the year
0000and the offset-00:00are invalid (Section 3.1 item 0). entity_idandareaare opaque identifiers (new Section 3.4): printable ASCII without space, up to 255 and 64 characters, compared exactly. Every identifier that was valid before remains valid. The evaluation no longer rejects a request because of the form of an identifier; the PEP MUST resolve the requested resource in its directory (Section 4).- A resource without category is
denywith the new reason codeunknown_resourceinstead ofinvalid_request(Section 4.1). - A rule without category may only use actions that some category of the vocabulary has (Section 3.1 item 4); before, such actions were not checked.
scene.activateis critical (Section 5): anallowrule withoutallow_criticalnow yieldsask.allow_criticaltogether with"*"is invalid (Section 3.1 item 9).- "No mandate for the agent and principal" is
denywith the new reason codeno_mandateinstead ofinvalid_mandate; several mandates at the same time areambiguous_mandate(Sections 4.1, 4.3 and 6). - Audit log:
denied_byonly withdenied, noerrorwithexecuted;evaluation.reasonmust belong toevaluation.decision;log.truncatedonly by a user or the system (Section 9.1). - Displayed text (Section 3.1 item 8): fixed code point list
data/forbidden-codepoints-v0.jsoninstead of the Unicode categories of the runtime; additionally forbidden are private-use and invisible (default ignorable) characters and characters, blank characters, noncharacters, every white space other than U+0020, a space at the start or end and a combining mark at the start; newly permitted are ZWNJ and ZWJ between other characters. The rule now also applies toactor.id,agent.display_nameandapproval.byin the audit log.
Clarified, each with new conformance cases:
- Patterns apply to the entire string and use only a portable subset of regular expressions (Section 3).
approval.timeout: hours are permitted (PT1H); at most 5 digits per component (Section 3.1 item 7).- Numbers are read by their value: digest of a mandate (Section 3.2) and
seqin the audit log (Section 9.1). - Time zones: links of the time zone database such as
US/Easternare permitted (Section 4.2). - Exchange format of the audit log: line feed as the only separator, no byte order mark, no
empty lines (Section 9.4).
conformance/audit-v0.jsonhas the new fieldjsonlfor it.
New:
- Conformance classes (Section 8.1):
evaluator,selection,signatures,audit,audit-anchored,pdp. - Test interface (Section 10) with a process binding (JSON lines over standard input and
output, no network) and an HTTP binding for PDPs; test tool
cmd/mandate-conformancewith a machine-readable report andcmd/mandate-harnessas the harness of the reference code. - Sections 12 to 14: security considerations with the attackers and assumptions, privacy considerations, versions and compatibility. More requirements are stated with the key words of BCP 14.
- Mandates can carry
issuerandversion; succession protects against rollback (Section 3.5,conformance/succession-v0.json). - Signed mandates: compact JWS over the canonical form with EdDSA or ES256, with exactly
one accepted spelling; Section 7 is normative now (
conformance/signed-v0.json). - Checkpoints anchor the audit log: event
log.checkpointwith alog_idand a detached JWS over the position and digest of the log (Section 9.5); verification with keys reports up to whichseqthe log is anchored. - Section 11, obligations of the PEP: approval (binding, single use, independence from the agent, re-evaluation, limits), rate limit (at most N requests in every period of 3600 seconds, every request counts), revocation and emergency stop, clock.
- AuthZEN:
approversin the response context forask; trust between PEP and PDP (Section 6). - Audit log:
result.countfor combined rate limit denials; verification reports the number of entries; Section 9.4 states what the hash chain proves and what not. - Critical resources: the resource directory can mark a resource as critical; then every
action except
readis critical (Section 4, step 5). New optional inputcriticalof the evaluation, in the conformance cases and inrequest.resourceof the audit log. - Parameters and constraints (Section 4.5, Section 3.1 item 10): an
allowrule can limit integer parameters of an action (temperature,position,volume,brightness); the vocabulary names the parameters and their units. New optional inputparametersof the evaluation, in the conformance cases, inaction.propertiesof the AuthZEN request and inrequest.parametersof the audit log. - Selection of the mandate (Section 4.3) with
conformance/selection-v0.json; requests for several resources (Section 4.4). vocabulary/v0.json: the vocabulary of Section 5 as a normative file, withschema/vocabulary-v0.schema.json; the same format for extensions (Section 5.1).profiles/: informative mappings of platforms to the vocabulary. The Home Assistant and Matter columns left the table in Section 5.- Conformance cases with identifiers in the style of several platforms
(
conformance/mandates/identifiers.json). - Reference code:
displaytext.Check,evaluator.Approval.Duration,evaluator.SelectAndEvaluate,evaluator.Resource.Critical,evaluator.Request.Parameters, packageratelimit(a limiter that meets the bound of Section 11.2),audit.VerifyJSONLinesreads the log as a stream, packagejws,evaluator.Sign,evaluator.ParseSigned,evaluator.CheckSuccessor,audit.SignCheckpoint,audit.VerifyAnchored. conformance/schema/: JSON Schemas of the conformance files;conformance/manifest.json: all machine-readable files with SHA-256 and number of cases (Section 8).LICENSE(Apache 2.0),LICENSE-docs(CC BY 4.0),SECURITY.md,CONTRIBUTING.md.- Audit log: optional
approval.via, the channel an answer came through (Section 9.1), so that a log shows whether a person confirmed on a phone or in a user interface. Conformance casesa11–a13inconformance/audit-v0.json. - Reference evaluator:
IsCriticalexposes the critical actions of the vocabulary (Section 5); anything outside the vocabulary counts as critical. - Audit log: event
directory.changedwith the memberdirectoryrecords changes to the resource directory that affect the evaluation: the critical marking, renamed resources and how a human resolved a rename (Sections 9.1, 9.2 and 11.4). Required for implementations that let humans change the critical marking or track renamed resources. Compatible for logs: every entry that was valid remains valid. A verifier that does not know the event rejects a log that contains it (Section 14, unknown members). Conformance casesa50–a57inconformance/audit-v0.json. - Examples in English:
examples/*.jsonuse English display names, device identifiers and areas, and so do the conformance cases on them. Their digests change:d01–d03,d06andd08inconformance/digest-v0.json. The audit vectors keep their mandate digest, which is only data there.
mandate-spec v0.1.0-alpha.1
#Incompatible:
typeand schema$idmoved to the neutral domain:https://mandate-spec.org/mandate/v0. Name of the specification: mandate-spec.- New validity rules for mandates (Section 3.1):
format: date-timeis validated, no leap seconds, I-JSON, unique rule IDs,time_windowwith distinct start and end, actions matching the category, at most 256 KiB,expiresaftervalid_from, approval timeout 10 s to 1 h, no control or format characters in displayed texts. agent.client_id: https URL or<namespace>:<id>instead of the product-specific prefixhm-client:(existinghm-client:IDs remain valid).- Request: the entity ID is required and must match the pattern, as must the area; the status
of the mandate is required (
activeorrevoked). - The schemas are normative.
Clarified (Section 4), each with new conformance cases:
- Pre-check: unknown category, action outside the vocabulary, invalid time specification
or invalid mandate →
deny. - Validity period
valid_from ≤ t < expires; revocation as an input to the evaluation. - Local time via the household time zone; time window start inclusive, end exclusive; weekday of the point in time; behavior during daylight saving time transitions.
rule_idand approval settings in the result (Section 4.1).limitsare not part of the evaluation.- Extensions without a known vocabulary →
deny. - Origin of inputs: category, area, time, time zone and status come from the PEP, never from the agent; comparison exact per character and case-sensitive.
- Time zones: only
UTCor IANA identifiers of the formArea/Location; seconds are truncated. - AuthZEN mapping of the fields (Section 6); the Home Assistant and Matter columns in the vocabulary are informative.
- Audit log: required fields per event,
executedonly after a permitting decision,broken_atalso on schema violations (Section 9).
New:
- Digest of a mandate (Section 3.2), reason codes (Section 4.1), both required in the AuthZEN response context (Section 6).
- Audit log with a fixed format and hash chain (Section 9,
schema/audit-v0.schema.json). - Schema: time fields additionally with
pattern.
Conformance cases: new fields reason (required), timezone, revoked, rule_id,
approval_timeout; new files conformance/invalid-v0.json, conformance/digest-v0.json,
conformance/audit-v0.json; test mandates under conformance/mandates/.