Skip to main content
A grant carries a list of scope entries. Each entry is operation:scope, where the operation prefix is optional. An entry with no prefix means read. Read and write never cross. This is a property of the encoding rather than a separate check: the read policy matches entries literally, so write:x can never satisfy a read of x, and the write policy only accepts entries that carry the write operation, so a plain entry can never satisfy a write.

Why read is the implicit default

  • Every grant issued before this convention is a read grant. Making read implicit keeps all of them valid with no migration and no change to the signed payload.
  • It fails closed on clients that predate the grammar. An older Personal Server sees write:notes.entries as a scope name it does not hold, matches nothing, and denies. The failure mode of a client that does not understand a new operation is refusal, not accidental access.

Matching rules

  • The operation is matched exactly. Comparison is case sensitive and the token is lowercase ASCII.
  • Wildcards apply to the scope part only, using the existing scope matcher. write:chatgpt.* is a wildcard over chatgpt scopes; there is no wildcard over operations.
  • An entry whose operation is not recognised is ignored for authorization, which means it denies. This is what makes the grammar safe to extend.
  • An entry is a single operation over a single scope. To grant read and write over the same scope, the grant carries two entries.

Operations

Operations that cannot be expressed as a single token do not belong in this grammar. Running a transformation over a user’s data and returning a derivative is one of those, and it is expected to be modelled as its own request type rather than as a scope prefix. See Operation-scoped grants.

Scope names

The prefix is the only part of the string this grammar defines. The scope name itself is declared by the schema and stays dot separated, source first, lowercase.

What builders see

You should not construct or parse these strings by hand. The string form is a wire and storage detail: it is what the user signs and what the DP RPC stores. The Vana SDK exposes a grant as scope and actions instead, and consent screens render a table of scope against what the app may do.
  • Every grant the SDK reads back from the DP RPC (getGrant, listGrantsByUser) carries a derived permissions field: one { scope, actions } row per scope pattern, with actions drawn from "read" and "write". The signed scopes array is left untouched.
  • grantPermissions(scopes) produces that grouped view from any scope list, and permissionsToScopes(permissions) is its inverse, so you can build a grant request from checkboxes without writing a prefix.
  • hasAction(scopes, scope, action) answers “does this grant let me write notes.entries?” using the same matcher the Personal Server uses.
  • parseScopeEntry(entry) and formatScopeEntry({ scope, action }) handle a single entry when you need one.
An entry the SDK does not recognise is never treated as read: parseScopeEntry and grantPermissions throw, hasAction skips it, and permissions is left absent on the grant so a consent screen cannot show such a grant as narrower than it is.

Not covered here

  • Fees, which may differ per operation.
  • Delete semantics. The prefix is reserved, the behaviour is not specified yet.