Skip to content
Open
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
25 changes: 21 additions & 4 deletions docs/design/product-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ type Icon = {
format: "jpeg" | "png"; // v1 formats; an unrecognised value is tolerated, not fatal
};

type Granted = "all"; // only v1 grant; unrecognised values are tolerated, not fatal
type Granted = // v1 grants; unrecognised values are tolerated, not fatal
| "all" // wildcard: every mediated interaction, present and future
| "storage" // read this product's host-local storage
| "context"; // read this product's account and the identity behind it
```

### Executable Manifest
Expand Down Expand Up @@ -145,8 +148,8 @@ directory, so its root CID is `dag-pb` — a raw block has no links and cannot b
## Cross-Product Trust

Running products interact through the host — reading another product's account, asking it to
sign. Normally each is a consent prompt; `trustedProducts` skips the prompt for products the
publisher pre-approved.
sign. Normally each is a consent prompt; `trustedProducts` skips the prompt for the scopes the
publisher pre-approved, per product.

Grants point inward:

Expand All @@ -157,6 +160,20 @@ A's manifest: trustedProducts: { "wallet": ["all"] }
→ products wallet trusts get nothing on A
```

Each entry's value list scopes the grant:

```
trustedProducts: {
"wallet": ["all"], → every mediated interaction, now and later
"tracker": ["storage"], → storage reads promptless; account reads still prompt
"hub": ["storage", "context"] → both, and no more when a fourth scope is defined
}
```

`all` is a superset, not a peer, so `["all", "storage"]` is just `["all"]` — never treat a narrow
value as carving something out of the wildcard. The list is a set: ignore order, collapse
duplicates. A scope you do not implement is an unrecognised value, so prompt for it.

Keys carry no TLD: `wallet`, not `wallet.dot`. Append the TLD of the network you resolve against
before matching. Missing field, empty record, empty array all mean "prompt as usual".

Expand All @@ -174,7 +191,7 @@ Two rules the host owes the user:
| Unknown `$v` | Undiscoverable; skip, surface diagnostic |
| Malformed JSON / schema validation fail | Do not launch; surface diagnostic |
| Unknown `icon.format` | Placeholder; never sniff or auto-correct |
| Unknown `Granted` value | Ignore it; manifest stays valid |
| Unknown `Granted` value | Ignore it; prompt. Manifest stays valid |
| `trustedProducts` key does not resolve | Entry inert; manifest stays valid |
| `trustedProducts` key carries a TLD | Does not resolve; entry inert |
| Icon CID unreachable, or bytes undecodable | Render placeholder; product launchable |
Expand Down
58 changes: 58 additions & 0 deletions docs/rfcs/granted-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: "Scoped grants in trustedProducts"
owner: "@filippovecchiato"
---

# RFC — Scoped grants in `trustedProducts`

| | |
| --------------- | ---------------------------------------------------------------------------------- |
| **Start Date** | 2026-08-19 |
| **Description** | Widen `Granted` from the single `all` wildcard to `all`, `storage`, and `context`. |
| **Authors** | Filippo Vecchiato |

## Summary

`Granted` gains two narrow values alongside `all`, so a publisher pre-approves a scope list per product instead of choosing between everything and nothing.

## Motivation

`all` resolves against every cross-product interaction the Host mediates at the moment the grant is used, including interactions added after publication. A wallet that wants a portfolio tracker to read its holdings has to grant `all`, which also pre-approves every account and signing interaction. "Read my stored data, prompt for anything else" is not expressible, so `all` is what gets published.

## Detailed Design

[RFC — Product Manifest Format][manifest] gains two `Granted` values:

```typescript
type Granted = 'all' | 'storage' | 'context';
```

| Value | Pre-approves |
| --------- | ------------------------------------------------------------------------------------------------------- |
| `all` | Every cross-product interaction the Host mediates on the granting product's behalf, present and future. |
| `storage` | Reading the granting product's host-local storage. Read-only. |
| `context` | Reading the granting product's account and the identity that follows from it. |

`trustedProducts` keeps its `Record<string, Granted[]>` shape, so this needs no new field and no `$v` bump.

- **`all` is a superset, not a peer.** `["all"]` implies `storage` and `context`, so `["all", "storage"]` is `["all"]`. A Host MUST NOT read a narrower value as a restriction on `all`. Enumerating the narrow values covers the same interactions today but does not widen when a further value is defined — that difference is the point of enumerating.
- **Values are a set.** Order is not significant, duplicates collapse.
- **Scopes are independent.** `["storage"]` leaves account interactions prompting as usual, and vice versa.
- **Existing rules are unchanged.** Hosts MUST ignore unrecognised values and MUST NOT fail validation over them, so a Host implementing only `all` reads `["storage"]` as an empty grant and prompts. Publishers MUST NOT emit a value outside `Granted`. A grant never overrides a denial the user already gave.

Which calls each scope gates remains a Host runtime contract, as it already is for `all`.

## Drawbacks

Writes stay on the wildcard: `storage` is read-only, so "read and write, nothing else" is still inexpressible. Signing has no scope of its own either. And `all` still widens silently, so staying narrow means revisiting the manifest as scopes are added.

## Alternatives

A separate field per scope (a foreign-storage record beside `trustedProducts`) splits one question — what may this product do to me — across fields that must be read together, and costs a top-level field per future scope. Per-scope operations (`{ storage: ["read", "write"] }`) add a second dimension to the manifest's only unbounded field; a `storage-write` value can land later under the ignore-unrecognised rule.

## Unresolved Questions

1. Is `context` the right name? `account` says it more directly, and `context` sits awkwardly beside the `context` parameter [RFC 0020][0020] removed from `create_transaction`.

[manifest]: product-manifest.md
[0020]: 0020-create-transaction.md
10 changes: 7 additions & 3 deletions docs/rfcs/product-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ type Icon = {
format: 'jpeg' | 'png'; // Formats defined by v1. An unrecognised value is tolerated, not fatal.
};

type Granted = 'all'; // The only grant v1 defines. Unrecognised values are ignored, not fatal.
type Granted = // Grants v1 defines. Unrecognised values are ignored, not fatal.
| 'all' // Wildcard: every mediated interaction, present and future.
| 'storage' // Read this product's host-local storage.
| 'context'; // Read this product's account and the identity behind it.
```

#### Icons
Expand All @@ -134,7 +137,8 @@ Each such interaction is normally a consent decision; `trustedProducts` pre-appr
**The grant is issued by the product being accessed.** An entry in A's manifest states what B may do *to A* — the only direction A's name can authenticate. It says nothing about what A may do to B, nor about the products B in turn trusts.

- **Keys** are bare `<product_id>` labels, lowercase, with no TLD suffix: `"wallet"`, never `"wallet.dot"`. The Host appends the TLD of the network it resolves against. A key that does not resolve there is inert, not a validation error.
- **Values** are that product's grants. v1 defines one, `all` — a wildcard for the complete set of cross-product permissions the Host mediates on this product's behalf. It is resolved against that set when the grant is used, not enumerated here, so a grant of `all` covers permissions added after it was published. Hosts MUST ignore unrecognised values, keep the recognised ones, and MUST NOT fail validation over them.
- **Values** are that product's grants. v1 defines three. `all` is a wildcard for the complete set of cross-product permissions the Host mediates on this product's behalf: it is resolved against that set when the grant is used, not enumerated here, so a grant of `all` covers permissions added after it was published. `storage` covers reading this product's host-local storage, read-only. `context` covers reading this product's account and the identity that follows from it. Hosts MUST ignore unrecognised values, keep the recognised ones, and MUST NOT fail validation over them.
- **`all` is a superset, not a peer.** `["all"]` implies `storage` and `context`, so `["all", "storage"]` is `["all"]` and a Host MUST NOT read a narrower value as a restriction on `all`. Enumerating the narrow values instead of granting `all` covers the same interactions today but does not widen when a further value is defined. Values are a set: order is not significant and duplicates collapse. Scopes are independent — a grant of `["storage"]` leaves account interactions prompting as usual.
- **Absence means no grants.** Missing field, empty record, and empty array are equivalent: prompt as usual. A product listing itself is ignored.

Which interactions a Host mediates, and what the prompt looks like, are Host runtime contracts; this RFC defines only how the grants are published and read.
Expand Down Expand Up @@ -524,6 +528,6 @@ A conforming Host implementation should produce well-defined behaviour for each

## Future Directions

- `Granted` will gain per-capability values (account read, signing, …) alongside `all` once the Host runtime contracts name those capabilities; `all` stays the wildcard, and the array shape and the ignore-unrecognised-values rule let the narrower values land without a new `$v`.
- `Granted` covers the capabilities the Host runtime contracts name today. Further values — write access to storage, a scope of its own for signing — fit the same way: `all` stays the wildcard, and the array shape and the ignore-unrecognised-values rule let them land without a new `$v`.
- A manifest-aggregation RPC could eliminate the N+1 lookup pattern (one round-trip per subname) without changing the schema.
- A companion spec will pin down the dashboard grid (cell size, bounds, responsive behaviour) referenced by `WidgetManifest.dimensions`.
Loading