Invocation
An invocation is the act of exercising a capability: a UCAN token, signed by the invoker, that names an ability over a resource and cites the delegation that authorizes it. Every read and write in TinyCloud is an invocation — the node admits it only if its signature is valid, it is time-valid, and the capability it claims is extends-covered by a parent delegation whose chain traces back to the space's owner DID. Where a delegation grants authority, an invocation uses it.
Actors
- Invoker — the principal exercising the capability; a DID (
invocation.invoker), typically a session key or an agent DID. - Parent delegation(s) — the delegations (referenced by CID in the UCAN
proof) that must have granted the invoked ability to the invoker. - The host node — receives the invocation at
/invoke, runsinvocation::process, and on success performs the side-effect (KV write, SQL query, …).
Sequence
process() (tinycloud-core/src/models/invocation.rs:86) runs verify() → validate() → save():
- Decode —
TinyCloudInvocationis a bare UCAN (type TinyCloudInvocation = Ucan).TryFrom<TinyCloudInvocation>buildsInvocationInfo { capabilities, invoker, parents }by readingpayload().attenuation(the invoked caps),payload().issuer(the invoker, fragment-stripped), andpayload().proof(parent CIDs). verify()— UCAN signature + own time-validity (see crypto).validate()— the authorization check against the delegation chain (below).save()— records the invocation (content-hash PK, idempotent), its invoked abilities, parent-edges, and any resultingkv_write/kv_deleterows; then enqueues matching webhook deliveries.
The authorization check (validate)
validate() (invocation.rs:115) mirrors delegation's check but is invoker-centric:
- Caps where
is_root_authority(cap, invoker)(the invoker owns the target space, or owns the network URN) are self-authorized and need no parent. - For dependent caps:
- With no parents →
MissingParents. - Parents are loaded by CID (with their abilities). Each parent must identify the invoker as its delegatee:
did_principal_matches(&p.delegatee, &invocation.invoker), elseUnauthorizedInvoker. - Parents are filtered to those valid at the invocation time
now:now < parent.expiryandnow ≥ parent.not_before(aNonebound is unbounded). - Every dependent cap must be covered by a surviving parent ability:
c.resource.extends(&pc.resource) && c.ability == pc.ability. OtherwiseUnauthorizedAction(resource, ability).
- With no parents →
Crypto
verify() (invocation.rs:101) calls invocation.verify_signature(&AnyDidMethod::default()) (Ed25519 session-key signature over the JWT) then payload().validate_time(None). Each invocation carries a fresh nonce (urn:uuid:…) minted at creation (make_invocation, tinycloud-auth/src/authorization.rs:154), but the general /invoke path has no nonce-dedup table — replay is bounded by validate_time + content-hash idempotency + the per-space epoch ordering. (The encryption decrypt path does add an encryption_nonce replay table; see consistency-model.)
Edge cases
- Invoker ≠ delegatee. A UCAN proving a delegation granted to a different DID is rejected (
UnauthorizedInvoker) — the proof must have been issued to the signer. - Expired parent. Even a syntactically valid invocation fails if its supporting delegation is no longer time-valid at
now(the parent is filtered out, leaving the cap uncovered →UnauthorizedAction). - Scope overreach. Invoking an ability/resource outside the granted prefix fails via
extends(see attenuation). - Missing KV target. A
tinycloud.kv/delagainst a key with no prior write yieldsMissingKvWriteduringsave. - Idempotent re-submission. The same signed invocation re-posted hits the content-hash PK and is a no-op insert (
RecordNotInserted→ returns the existing hash) — for writes this is harmless; the lack of dedup matters only for replayable read side-effects (see consistency-model).
Trace
An agent holds a UCAN delegation for tinycloud.kv/get over tinycloud:pkh:eip155:1:0xf39f…2266:applications/kv/com.listen.app/transcript/ (issued to its DID, see delegation). To read today's transcript it mints an invocation: attenuation = { "tinycloud:…:applications/kv/com.listen.app/transcript/2026-06-23.json": ["tinycloud.kv/get"] }, proof = the delegation CID, signed by the agent's session key. /invoke → verify() OK; validate() loads the parent by CID, confirms delegatee == invoker, confirms it's time-valid, and confirms …/transcript/2026-06-23.json extends …/transcript/ with tinycloud.kv/get ✓; the node reads and returns the value. (See example-listen.)
Status & drift
Shipped. The general /invoke path's absence of a nonce-dedup store is a known property (writes are idempotent; reads can be re-driven before exp) — flagged in capabilities and consistency-model, not a docs gap. DID method resolution is AnyDidMethod::default() (// TODO, invocation.rs:103).
Sources
tinycloud-node:tinycloud-core/src/models/invocation.rs:86-204(process/verify/validate/is_root_authority),tinycloud-core/src/util.rs:188-198(InvocationInfo),tinycloud-auth/src/authorization.rs:74-170(TinyCloudInvocation,make_invocation, nonce)