Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions build-an-oracle/develop/identity-and-auth.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Identity and auth"
description: "An oracle has a persistent blockchain identity (entity DID + Matrix bot) and per-user auth via UCAN delegation. This guide covers both."

Check warning on line 3 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L3

Did you really mean 'blockchain'?
icon: "shield-halved"
---

Expand Down Expand Up @@ -35,7 +35,7 @@
<Step title="Run the CLI to create the oracle's on-chain entity and Matrix account">
```sh
qiforge-cli create-entity --no-interactive \
--network devnet \

Check warning on line 38 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L38

Did you really mean 'devnet'?
--oracle-name "My Oracle" \
--org-name "My Org" \
--api-url http://localhost:3000 \
Expand Down Expand Up @@ -119,7 +119,7 @@
| `x-ucan-delegation` | fallback / downstream-authz | The user→oracle delegation. Carries the capabilities plugins use to mint downstream invocations. Also accepted as the auth artifact on its own for pre-invocation clients. |
| `x-did` | no | The user's IXO DID (set by SDK; runtime derives the authenticated DID from the invocation/delegation regardless). Not used for authentication. |
| `x-matrix-access-token` | no | For clients that already have a Matrix session. |
| `x-matrix-homeserver` | no | Matrix homeserver for the user. |

Check warning on line 122 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L122

Did you really mean 'homeserver'?
| `x-timezone` | no | Propagates to `rtCtx.user.timezone`. |
| `x-request-id` | no | Correlation ID for logs and traces. |

Expand All @@ -135,7 +135,7 @@

<Steps>
<Step title="Validates the invocation (primary)">
When `X-Auth-Type: ucan` + `Authorization: Bearer …` are present, [`validateUcanInvocation`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/modules/auth/validate-ucan-invocation.ts) verifies the signature and audience (the oracle's `ORACLE_DID`) and rejects any invocation whose lifetime exceeds `UCAN_AUTH_MAX_TTL_SECONDS` (default 900s). Results are cached by the token's SHA-256 hash with **TTL = the invocation's own expiry**, so reusing the same token until it expires (JWT-style) doesn't re-hit Blocksync. The invocation's signer becomes `req.authData.did`.

Check warning on line 138 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L138

Did you really mean 'Blocksync'?
</Step>

<Step title="Falls back to the delegation (migration only)">
Expand All @@ -143,7 +143,7 @@
</Step>

<Step title="Trusts the delegation downstream only when it's the caller's own">
A delegation is public and shareable, so the middleware acts on it downstream **only when its issuer DID equals the authenticated DID**. When they differ, the delegation is ignored downstream — `req.authData.ucanDelegation.raw` is set to `''`, and plugins branch on `raw.length === 0`. This stops a client from pairing their own invocation with someone else's delegation to make the oracle act on that person's behalf.

Check warning on line 146 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L146

Did you really mean 'else's'?
</Step>

<Step title="Attaches RuntimeUserContext to the request">
Expand Down Expand Up @@ -207,10 +207,11 @@
return JSON.stringify({ error: 'Could not resolve downstream service DID.' });
}

const invocation = await rtCtx.ucan.mintInvocation({
did: serviceDid,
capability: 'ixo:downstream',
});
const invocation = await rtCtx.ucan.mintInvocation(
{ did: serviceDid, capability: 'ixo:downstream' },
// Claim the ability the user's delegation grants — see below.
{ can: 'downstream/*' },
);

const resp = await fetch('https://downstream-service.example/data', {
headers: {
Expand All @@ -226,7 +227,7 @@

| Method | Purpose |
| --- | --- |
| `mintInvocation(target, opts?)` | Mint a service-targeted invocation from the user's cached delegation. `target` = `{ did, capability }`; `opts.skipCache` forces a fresh signature. |
| `mintInvocation(target, opts?)` | Mint a service-targeted invocation from the user's cached delegation. `target` = `{ did, capability }`; `opts.can` is the ability claimed (default `'*'`); `opts.skipCache` forces a fresh signature. |
| `requireCapability(resource, action)` | Throws if the user's delegation doesn't include the capability. |
| `hasCapability(resource, action)` | Returns a boolean — non-throwing variant. |
| `resolveServiceDid(serviceUrl)` | Resolves a service URL to its `did:web:...` identifier. Returns `null` when the DID document is missing or has no `id`. |
Expand All @@ -235,13 +236,46 @@

See [`packages/oracle-runtime/src/modules/ucan/`](https://github.com/ixoworld/ixo-oracles-boilerplate/tree/main/packages/oracle-runtime/src/modules/ucan) for the service implementation.

### Claim only what you were granted

An invocation says what the oracle is *doing right now*; the delegation says what the user *permitted*. The service accepts the invocation only if the delegation covers it — and coverage is narrower than it looks. A granted ability covers a claim when it is:

- `'*'` — covers everything, or
- **exactly equal** to the claim, or
- a `prefix/*` pattern matching it (`memory/*` covers `memory/read`).

Nothing else. In particular **`'*'` is not a wildcard when you *claim* it** — a `'*'` claim is satisfiable only by a `'*'` grant, because `'*'` does not start with `memory/`:

```ts
// delegation grants { can: 'memory/*', with: 'ixo:memory' }

// ❌ over-claim — refused: "Delegated capability not found"
await rtCtx.ucan.mintInvocation({ did, capability: 'ixo:memory' });

// ✅ claims exactly what was granted
await rtCtx.ucan.mintInvocation(
{ did, capability: 'ixo:memory' },
{ can: 'memory/*' },
);
```

<Warning>
The service must **register** the ability you claim. It matches an invocation's `can` by strict string equality, so a service that only defines `'*'` rejects a `memory/*` invocation as an unknown capability *before* authorization is considered. When narrowing a claim, roll out in this order:

1. service accepts the narrow ability **and** `'*'`
2. oracle switches to claiming the narrow ability
3. service tightens or upgrades

Reversing steps 1 and 2 produces a 401 on every call.
</Warning>

## What plugins can and cannot do

- **Can:** read `rtCtx.user.did`, `rtCtx.user.matrixUserId`, `rtCtx.user.ucanDelegation`, `rtCtx.user.timezone`.
- **Can:** call `rtCtx.secrets.getIndex()` / `getValues()` to read per-room secrets.
- **Can:** mint downstream invocations via `rtCtx.ucan.mintInvocation`.
- **Cannot:** override the oracle's identity per request.
- **Cannot:** issue UCANs as anyone other than the oracle itself.

Check warning on line 278 in build-an-oracle/develop/identity-and-auth.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/develop/identity-and-auth.mdx#L278

Did you really mean 'UCANs'?

## Where to read next

Expand Down
18 changes: 16 additions & 2 deletions build-an-oracle/reference/runtime-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
ucan: {
requireCapability: (resource: string, action: string) => void;
hasCapability: (resource: string, action: string) => boolean;
mintInvocation: (target: { did: string; capability: string }, opts?: { skipCache?: boolean }) => Promise<string>;
mintInvocation: (target: { did: string; capability: string }, opts?: { skipCache?: boolean; can?: string }) => Promise<string>;
resolveServiceDid: (serviceUrl: string) => Promise<string | null>;
hasSigningKey: () => boolean;
createInvocationFromDelegation: (
Expand Down Expand Up @@ -107,7 +107,7 @@
</Accordion>

<Accordion title="history" icon="clock">
- `messages` — readonly array of `BaseMessage` (LangChain). The full thread history loaded by the checkpointer.

Check warning on line 110 in build-an-oracle/reference/runtime-context.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/reference/runtime-context.mdx#L110

Did you really mean 'readonly'?

Check warning on line 110 in build-an-oracle/reference/runtime-context.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/reference/runtime-context.mdx#L110

Did you really mean 'checkpointer'?
- `recent(n)` — convenience method returning the most recent `n` messages.
- `userContext` — enrichment object from `state.userContext` (typically populated by the Memory plugin).
- `state` — `ReadonlyState`, a typed view over the LangGraph annotation state. See [State schema](/build-an-oracle/reference/state-schema).
Expand Down Expand Up @@ -139,7 +139,7 @@
</Accordion>

<Accordion title="blobStore" icon="box-archive">
Short-TTL, user-namespaced store for content the LLM must **never relay verbatim** — UCAN invocation CARs, JWTs, signed envelopes. A producing tool stores the value and returns a short opaque ID; a consuming tool looks it up server-side and forwards it on. The model only ever sees the ID.

Check warning on line 142 in build-an-oracle/reference/runtime-context.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/reference/runtime-context.mdx#L142

Did you really mean 'JWTs'?

- `put({ userDid, name, value, ttlSeconds? })` — store a value, returns a fresh `blob_<16 hex>` ID. TTL defaults to 1h and is clamped to the service max (24h). Pass `userDid` from a **trusted source** (e.g. `rtCtx.user.did`) — never from LLM-supplied tool args.
- `get({ userDid, blobId })` — retrieve a blob scoped to the requesting user. Returns `null` if it doesn't exist, has expired, or belongs to a different user (cross-user reads always miss).
Expand Down Expand Up @@ -171,7 +171,21 @@

- `requireCapability(resource, action)` — throws if the user's delegation doesn't include this capability.
- `hasCapability(resource, action)` — boolean check.
- `mintInvocation({ did, capability }, opts?)` — mint a downstream invocation signed by the oracle's signing mnemonic.
- `mintInvocation({ did, capability }, opts?)` — mint a downstream invocation signed by the oracle's signing mnemonic. `opts.can` is the **ability** the invocation claims (default `'*'`); `opts.skipCache` bypasses the invocation cache, required for services that enforce single-use replay protection per invocation CID.

<Warning>
**Claim the ability the user's delegation actually grants.** A claim resolves against a delegation only when the granted ability is `'*'`, equals the claim, or is a `prefix/*` covering it. So the default `'*'` claim is satisfiable **only** by a `'*'` grant — if the user granted `memory/*`, a `'*'` claim is an over-claim and the service refuses it:

```ts
// ✅ delegation grants { can: 'memory/*', with: 'ixo:memory' }
await rtCtx.ucan.mintInvocation(
{ did: memoryDid, capability: 'ixo:memory' },
{ can: 'memory/*' },
);
```

The service must also register that ability: it matches an invocation's `can` by **strict equality**, so one that only defines `'*'` rejects a `memory/*` invocation as an unknown capability before authorization is considered. Roll out the service side first.
</Warning>
- `resolveServiceDid(serviceUrl)` — look up a downstream service's DID document; returns `id` or `null`.
- `hasSigningKey()` — `true` once the oracle has loaded its Ed25519 signing mnemonic. **Gate registration of mint-capable tools on this**: without a key, minting is a no-op, so the tool should surface an error rather than pretend it worked.
- `createInvocationFromDelegation(delegationCar, serviceUrl, capability, options?)` — mint an invocation from a **directly-supplied** delegation CAR (rather than the user's cached one), targeted at a specific service route. Returns `{ invocation }` on success or `{ error }` with a surfaced-verbatim reason (missing signing key, audience mismatch, did:web unreachable, …).
Expand Down Expand Up @@ -217,7 +231,7 @@
</Accordion>

<Accordion title="shared" icon="share-nodes">
Read accessors for state owned by other plugins (registered via `getSharedState()`). Typed via declaration merging on the `SharedAccessors` interface — see [Plugin shared state guide](/build-an-oracle/develop/plugin-recipes/share-state).

Check warning on line 234 in build-an-oracle/reference/runtime-context.mdx

View check run for this annotation

Mintlify / Mintlify Validation (ixoworld) - vale-spellcheck

build-an-oracle/reference/runtime-context.mdx#L234

Did you really mean 'accessors'?
</Accordion>

<Accordion title="toolCallId" icon="hashtag">
Expand Down
Loading