From 43d685ee5c98e31c330fe61ef486b57274a0cfe5 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Wed, 19 Aug 2026 15:36:37 +0100 Subject: [PATCH 1/3] docs(rfc): scope trustedProducts grants to storage and context --- docs/design/product-manifest.md | 25 ++++++-- docs/rfcs/granted-scopes.md | 100 ++++++++++++++++++++++++++++++++ docs/rfcs/product-manifest.md | 10 +++- 3 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 docs/rfcs/granted-scopes.md diff --git a/docs/design/product-manifest.md b/docs/design/product-manifest.md index d59df8fa..66f1e874 100644 --- a/docs/design/product-manifest.md +++ b/docs/design/product-manifest.md @@ -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 @@ -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: @@ -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". @@ -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 | diff --git a/docs/rfcs/granted-scopes.md b/docs/rfcs/granted-scopes.md new file mode 100644 index 00000000..3d5782b0 --- /dev/null +++ b/docs/rfcs/granted-scopes.md @@ -0,0 +1,100 @@ +--- +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 the `all` wildcard: `storage` (read the granting product's host-local storage) and `context` (the cross-product account and identity interactions the Host mediates). `trustedProducts` keeps its existing `Record` shape, so a publisher pre-approves a scope list per product instead of choosing between everything and nothing. + +## Motivation + +[RFC — Product Manifest Format][manifest] defines one grant value: + +```typescript +type Granted = 'all'; +``` + +`all` is deliberately unenumerated: it resolves against the complete set of cross-product interactions the Host mediates at the moment the grant is used, so it also covers interactions added after the manifest was published. That is the right default for a product's own companion apps and the wrong one for everything else. + +A wallet that wants a portfolio tracker to read its holdings has to grant `all`, which also pre-approves every account and signing interaction the Host mediates now or later. The publisher's actual intent — "read my stored data, prompt for anything else" — is not expressible. The consequence is not a missing feature but a pressure to over-grant: `all` is the only value, so `all` is what gets published. + +The manifest RFC reserves this change in its Future Directions, and `trustedProducts` values are already an array, so the narrower values need no new field, no new shape, and no `$v` bump. + +## Detailed Design + +`Granted` becomes: + +```typescript +type Granted = 'all' | 'storage' | 'context'; +``` + +`trustedProducts` is unchanged: + +```typescript +trustedProducts?: Record; +``` + +| 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. | + +### `all` stays the wildcard + +`all` is a superset, not a peer: `["all"]` implies `storage` and `context`. Two rules follow. + +- `["all", "storage"]` is `["all"]`. A Host MUST NOT read a narrower value as a restriction on `all`. There is no way to subtract from a wildcard, and treating one as a subtraction would silently narrow grants that are already published. +- `["storage", "context"]` covers the same interactions as `["all"]` today, but does **not** widen when a fourth value is defined. That difference is the whole point of enumerating: an enumerated grant is a statement about a fixed set, a wildcard is a standing delegation. + +### Values are a set + +Order is not significant and duplicates collapse — `["storage", "storage"]` is `["storage"]`. Hosts MUST NOT attach meaning to position. + +### Scopes are independent + +`["storage"]` waives the prompt for storage reads and leaves account interactions prompting as usual. `["context"]` is the mirror image. Neither scope implies the other. + +### Unchanged rules + +Everything the manifest RFC already says about grants holds with three values in play: + +- Hosts MUST ignore unrecognised values, keep the recognised ones in the same entry, and MUST NOT fail validation over them. A Host that knows only `all` therefore reads `["storage"]` as an empty grant and prompts — the correct degradation, since it cannot honour a scope it does not implement. +- Publishers MUST NOT emit a value outside `Granted`. +- A grant waives the publisher's prompt, never a denial the user already gave. +- Absence, an empty record, and an empty array remain equivalent. + +### Runtime mechanics stay out of the manifest + +The manifest RFC defines how grants are published and read, and defers which interactions a Host mediates to the Host runtime contracts. That split is unchanged: `storage` and `context` name what a grant covers, while the calls each one gates belong to the Host API surface. Addressing another product's local storage is its own runtime contract — a `storage` grant is what makes such a read promptless, not what makes it possible. + +## Drawbacks + +- **`storage` is read-only, so "read and write, nothing else" stays inexpressible.** A publisher who wants a trusted product to write has to grant `all`. This is the same over-granting pressure the RFC reduces, narrowed to writes rather than removed. +- **`all` still widens silently.** A publisher who wants to stay narrow has to revisit the manifest whenever a new scope is defined. Enumerating buys precision at the cost of maintenance and the wildcard buys the reverse; there is no third option that is both precise and maintenance-free. +- **Three values do not partition the mediated set.** Signing is mediated but has no scope of its own, so it stays reachable only through `all`. Until it gets one, "let this product sign on my behalf but touch nothing else" cannot be published. +- **A narrow grant is not portable across Hosts.** Ignore-unrecognised means a Host implementing only `all` degrades a scoped grant to a prompt, so `["storage"]` is a weaker guarantee to the publisher than `["all"]`. Correct, but it makes the effect of a grant depend on Host version. + +## Alternatives + +- **A separate field per scope**, for example a foreign-storage record beside `trustedProducts`. Discarded because it splits one question — what may this product do to me — across fields that must be read together, and every future scope then costs another top-level field and another branch in every validator. The array already exists to carry this. +- **Per-scope operations**, `{ storage: ["read", "write"] }`. Discarded for v1: it adds a second dimension to the manifest's only unbounded field, and read-versus-write is the sole place that dimension currently pays off. A `storage-write` value can land later under the same ignore-unrecognised rule. +- **Leave `all` alone and narrow at the Host prompt instead.** Discarded because it turns a publisher declaration into a per-Host UX decision: the same manifest would mean different things on different Hosts, and the publisher's intent would never be recorded anywhere durable. + +## Unresolved Questions + +1. **Is `context` the right name?** It covers account and identity reads, which `account` would say more directly. `context` also sits awkwardly beside the `context` parameter [RFC 0020][0020] removed from `create_transaction`. +2. **Does `storage` need a write counterpart here?** Deferring it keeps this change small but leaves writes on the wildcard, which is where the over-granting pressure is strongest. +3. **Should a Host distinguish a scoped grant from a wildcard one in its permission UI**, so a user can see that a product was pre-approved narrowly rather than completely? + +[manifest]: product-manifest.md +[0020]: 0020-create-transaction.md diff --git a/docs/rfcs/product-manifest.md b/docs/rfcs/product-manifest.md index 8e227966..ab3c9de5 100644 --- a/docs/rfcs/product-manifest.md +++ b/docs/rfcs/product-manifest.md @@ -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 @@ -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 `` 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. @@ -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`. From daae4f4f01a406d6a049eb2e22d35fad4b935314 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Wed, 19 Aug 2026 15:53:57 +0100 Subject: [PATCH 2/3] docs(rfc): trim granted-scopes to the definition --- docs/rfcs/granted-scopes.md | 67 +++++++------------------------------ 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/docs/rfcs/granted-scopes.md b/docs/rfcs/granted-scopes.md index 3d5782b0..4df5659a 100644 --- a/docs/rfcs/granted-scopes.md +++ b/docs/rfcs/granted-scopes.md @@ -13,88 +13,47 @@ owner: "@filippovecchiato" ## Summary -`Granted` gains two narrow values alongside the `all` wildcard: `storage` (read the granting product's host-local storage) and `context` (the cross-product account and identity interactions the Host mediates). `trustedProducts` keeps its existing `Record` shape, so a publisher pre-approves a scope list per product instead of choosing between everything and nothing. +`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 -[RFC — Product Manifest Format][manifest] defines one grant value: - -```typescript -type Granted = 'all'; -``` - -`all` is deliberately unenumerated: it resolves against the complete set of cross-product interactions the Host mediates at the moment the grant is used, so it also covers interactions added after the manifest was published. That is the right default for a product's own companion apps and the wrong one for everything else. - -A wallet that wants a portfolio tracker to read its holdings has to grant `all`, which also pre-approves every account and signing interaction the Host mediates now or later. The publisher's actual intent — "read my stored data, prompt for anything else" — is not expressible. The consequence is not a missing feature but a pressure to over-grant: `all` is the only value, so `all` is what gets published. - -The manifest RFC reserves this change in its Future Directions, and `trustedProducts` values are already an array, so the narrower values need no new field, no new shape, and no `$v` bump. +`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 -`Granted` becomes: +[RFC — Product Manifest Format][manifest] gains two `Granted` values: ```typescript type Granted = 'all' | 'storage' | 'context'; ``` -`trustedProducts` is unchanged: - -```typescript -trustedProducts?: Record; -``` - | 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. | -### `all` stays the wildcard - -`all` is a superset, not a peer: `["all"]` implies `storage` and `context`. Two rules follow. - -- `["all", "storage"]` is `["all"]`. A Host MUST NOT read a narrower value as a restriction on `all`. There is no way to subtract from a wildcard, and treating one as a subtraction would silently narrow grants that are already published. -- `["storage", "context"]` covers the same interactions as `["all"]` today, but does **not** widen when a fourth value is defined. That difference is the whole point of enumerating: an enumerated grant is a statement about a fixed set, a wildcard is a standing delegation. - -### Values are a set - -Order is not significant and duplicates collapse — `["storage", "storage"]` is `["storage"]`. Hosts MUST NOT attach meaning to position. - -### Scopes are independent - -`["storage"]` waives the prompt for storage reads and leaves account interactions prompting as usual. `["context"]` is the mirror image. Neither scope implies the other. - -### Unchanged rules - -Everything the manifest RFC already says about grants holds with three values in play: - -- Hosts MUST ignore unrecognised values, keep the recognised ones in the same entry, and MUST NOT fail validation over them. A Host that knows only `all` therefore reads `["storage"]` as an empty grant and prompts — the correct degradation, since it cannot honour a scope it does not implement. -- Publishers MUST NOT emit a value outside `Granted`. -- A grant waives the publisher's prompt, never a denial the user already gave. -- Absence, an empty record, and an empty array remain equivalent. +`trustedProducts` keeps its `Record` shape, so this needs no new field and no `$v` bump. -### Runtime mechanics stay out of the manifest +- **`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. -The manifest RFC defines how grants are published and read, and defers which interactions a Host mediates to the Host runtime contracts. That split is unchanged: `storage` and `context` name what a grant covers, while the calls each one gates belong to the Host API surface. Addressing another product's local storage is its own runtime contract — a `storage` grant is what makes such a read promptless, not what makes it possible. +Which calls each scope gates remains a Host runtime contract, as it already is for `all`. ## Drawbacks -- **`storage` is read-only, so "read and write, nothing else" stays inexpressible.** A publisher who wants a trusted product to write has to grant `all`. This is the same over-granting pressure the RFC reduces, narrowed to writes rather than removed. -- **`all` still widens silently.** A publisher who wants to stay narrow has to revisit the manifest whenever a new scope is defined. Enumerating buys precision at the cost of maintenance and the wildcard buys the reverse; there is no third option that is both precise and maintenance-free. -- **Three values do not partition the mediated set.** Signing is mediated but has no scope of its own, so it stays reachable only through `all`. Until it gets one, "let this product sign on my behalf but touch nothing else" cannot be published. -- **A narrow grant is not portable across Hosts.** Ignore-unrecognised means a Host implementing only `all` degrades a scoped grant to a prompt, so `["storage"]` is a weaker guarantee to the publisher than `["all"]`. Correct, but it makes the effect of a grant depend on Host version. +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**, for example a foreign-storage record beside `trustedProducts`. Discarded because it splits one question — what may this product do to me — across fields that must be read together, and every future scope then costs another top-level field and another branch in every validator. The array already exists to carry this. -- **Per-scope operations**, `{ storage: ["read", "write"] }`. Discarded for v1: it adds a second dimension to the manifest's only unbounded field, and read-versus-write is the sole place that dimension currently pays off. A `storage-write` value can land later under the same ignore-unrecognised rule. -- **Leave `all` alone and narrow at the Host prompt instead.** Discarded because it turns a publisher declaration into a per-Host UX decision: the same manifest would mean different things on different Hosts, and the publisher's intent would never be recorded anywhere durable. +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?** It covers account and identity reads, which `account` would say more directly. `context` also sits awkwardly beside the `context` parameter [RFC 0020][0020] removed from `create_transaction`. -2. **Does `storage` need a write counterpart here?** Deferring it keeps this change small but leaves writes on the wildcard, which is where the over-granting pressure is strongest. -3. **Should a Host distinguish a scoped grant from a wildcard one in its permission UI**, so a user can see that a product was pre-approved narrowly rather than completely? +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`. +2. Should `storage` get a write counterpart here rather than leaving writes on `all`? [manifest]: product-manifest.md [0020]: 0020-create-transaction.md From 0369bd218286df5ec3256a3b26169adba9e357c0 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Wed, 19 Aug 2026 15:59:51 +0100 Subject: [PATCH 3/3] docs(rfc): drop the storage-write open question --- docs/rfcs/granted-scopes.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/rfcs/granted-scopes.md b/docs/rfcs/granted-scopes.md index 4df5659a..f83f9498 100644 --- a/docs/rfcs/granted-scopes.md +++ b/docs/rfcs/granted-scopes.md @@ -53,7 +53,6 @@ A separate field per scope (a foreign-storage record beside `trustedProducts`) s ## 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`. -2. Should `storage` get a write counterpart here rather than leaving writes on `all`? [manifest]: product-manifest.md [0020]: 0020-create-transaction.md