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..f83f9498 --- /dev/null +++ b/docs/rfcs/granted-scopes.md @@ -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` 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 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`.