diff --git a/rfcs/0009-hosted-feeds-for-plugins-and-skills.md b/rfcs/0009-hosted-feeds-for-plugins-and-skills.md index af54b4f3..d06eaf38 100644 --- a/rfcs/0009-hosted-feeds-for-plugins-and-skills.md +++ b/rfcs/0009-hosted-feeds-for-plugins-and-skills.md @@ -5,7 +5,7 @@ authors: - Patrick - Gio created: 2026-06-18 -last_updated: 2026-07-08 +last_updated: 2026-07-18 status: draft issue: rfc_pr: https://github.com/openclaw/rfcs/pull/19 @@ -30,7 +30,7 @@ uses. External plugins can continue to install from npm or ClawHub, with Git available for immutable source installs. The feed provides package selection, version, and checksum data; local configuration supplies the source endpoint and credentials. ClawHub can publish the default public feed and other -ClawHub-hosted public, named, account, organization, or composed feeds. Those +ClawHub-hosted public, named, publisher, organization, or composed feeds. Those ClawHub-hosted feeds should be signed by the ClawHub platform feed-signing key, with the matching public key bundled in OpenClaw so ordinary ClawHub feed use is zero-config. Organizations can publish effective feeds by subsetting, filtering, @@ -136,19 +136,34 @@ points are: The first feed version should preserve those semantics while moving the catalog source from bundled-only JSON to hosted JSON with bundled fallback. -The implementer-facing v1 core contract is captured in -[`0009/hosted-feed-v1-spec.md`](0009/hosted-feed-v1-spec.md). Trust and account -feed addenda are captured separately in -[`0009/signed-feed-trust-v1-spec.md`](0009/signed-feed-trust-v1-spec.md) and -[`0009/clawhub-account-feeds-v1-spec.md`](0009/clawhub-account-feeds-v1-spec.md). -This RFC remains the design rationale and rollout plan; the sidecar specs are -the concise schema, example, verification, refresh, and conformance references -for feed publishers and OpenClaw clients. +This RFC contains the rationale and rollout plan. Implementers should use the +sidecar specification matching their task: + +- [`0009/hosted-feed-v1-spec.md`](0009/hosted-feed-v1-spec.md) defines the + strict atomic install-catalog document and baseline client behavior. +- [`0009/signed-feed-trust-v1-spec.md`](0009/signed-feed-trust-v1-spec.md) + defines DSSE transport, trusted public keys, verification, and rotation. +- [`0009/clawhub-account-feeds-v1-spec.md`](0009/clawhub-account-feeds-v1-spec.md) + defines publisher discovery feeds. The historical filename is retained for + link stability; the contract does not define a separate account identity. +- [`0009/hosted-feed-distribution-v1-spec.md`](0009/hosted-feed-distribution-v1-spec.md) + defines sharded snapshots, signed queries, changed-since retrieval, and + client-first watches. + +An install-catalog producer starts with the core and trust specs. A publisher +feed producer or consumer also uses the publisher and distribution specs. ### Feed document A feed document should be a deterministic JSON document with a schema version, feed id, generated timestamp, monotonic sequence number, expiry, and entries. +The complete atomic document is the simplest representation, not a permanent +assumption that catalogs remain small. Main catalogs and high-volume publisher +feeds may exceed response limits. Those feeds use a signed root manifest over +immutable content-addressed shards for complete synchronization, and signed +revision-bound query or change projections for server-side search and +incremental refresh. Changed-since responses include tombstones so removals and +blocks cannot survive indefinitely in client state. Entry ids must be stable registry identities, not mutable display slugs. If a registry allows user-editable slugs, the feed should either carry an immutable package id or use the canonical package coordinate as the stable id. Display @@ -223,11 +238,12 @@ installer behavior already trusted by that deployment. The default ClawHub feed profile is special only in its bootstrap trust. OpenClaw can ship a bundled ClawHub platform public key and use it to verify any ClawHub-hosted feed URL whose identity is inside the signed payload. That covers -`clawhub-public`, ClawHub named feeds, account feeds, organization feeds, and +`clawhub-public`, ClawHub named feeds, publisher feeds, organization feeds, and ClawHub-served composed feeds without requiring users to paste keys for each -feed. The feed document still carries its `feedId`, owner or organization scope, -visibility, sequence, expiry, and entries; ClawHub still enforces ACLs before it -serves private account or organization feeds. Non-ClawHub feed URLs do not +feed. The feed document still carries its `feedId`, publisher, organization, or +collection scope, visibility, sequence, expiry, and entries; ClawHub still +enforces ACLs before it serves private named, organization, or composed feeds. +Non-ClawHub feed URLs do not inherit this trust and must either use an explicitly configured trust root or an explicit unsigned opt-in. @@ -471,138 +487,179 @@ hosted feed payload according to local configuration. Later PRs can consume that verified state when they wire trusted feed entries into install eligibility, search filtering, regional variants, or tenant-composed effective feeds. -### ClawHub account feeds and following - -ClawHub can also publish account-backed feeds as a discovery feature. A ClawHub -account feed is produced by ClawHub for a stable ClawHub account or publisher id, -not by an arbitrary third-party endpoint. If OpenClaw verifies the feed as -ClawHub-authored, the client can trust that the feed reflects ClawHub's view of -that account's published plugins or skills and account metadata. - -This is different from treating every account feed entry as official or safe to +### Implementation status (July 2026) + +This status is informative rather than normative. Open PRs remain subject to +review and may change without changing the v1 contract. + +- **Hosted catalog foundation (merged):** OpenClaw + [#95846](https://github.com/openclaw/openclaw/pull/95846), + [#95868](https://github.com/openclaw/openclaw/pull/95868), + [#95877](https://github.com/openclaw/openclaw/pull/95877), + [#95964](https://github.com/openclaw/openclaw/pull/95964), + [#95969](https://github.com/openclaw/openclaw/pull/95969), + [#95981](https://github.com/openclaw/openclaw/pull/95981), + [#96155](https://github.com/openclaw/openclaw/pull/96155), + [#96158](https://github.com/openclaw/openclaw/pull/96158), and + [#96194](https://github.com/openclaw/openclaw/pull/96194) provide hosted + fetch, durable fallback, source/config profiles, refresh, entries, and + bounded telemetry for the external plugin catalog. +- **Signed-feed trust (merged):** OpenClaw + [#98299](https://github.com/openclaw/openclaw/pull/98299), + [#98316](https://github.com/openclaw/openclaw/pull/98316), + [#98338](https://github.com/openclaw/openclaw/pull/98338), and + [#98350](https://github.com/openclaw/openclaw/pull/98350) provide envelope + verification, trusted source-profile configuration, verified snapshot state, + rollback protection, and operator trust visibility. +- **ClawHub signing and bootstrap trust (active):** ClawHub + [#3005](https://github.com/openclaw/clawhub/pull/3005) signs stored catalog + publications with a dedicated feed key. OpenClaw + [#101981](https://github.com/openclaw/openclaw/pull/101981) binds the built-in + `clawhub-public` profile to the matching bundled or environment-provided + public trust anchor. OpenClaw + [#110037](https://github.com/openclaw/openclaw/pull/110037) aligns the client + with standard DSSE while retaining the shipped beta format as an isolated + compatibility path. OpenClaw + [#108342](https://github.com/openclaw/openclaw/pull/108342) preserves + monotonic rollback comparison across trusted-key rotation. +- **Publisher-feed producer (active, first product lane):** ClawHub + [#2948](https://github.com/openclaw/clawhub/pull/2948) defines stable + publisher identity, coherent snapshots, and bounded pagination. ClawHub + [#3116](https://github.com/openclaw/clawhub/pull/3116) and + [#3117](https://github.com/openclaw/clawhub/pull/3117) add revisioned query + and change state plus signed snapshot, query, change, and reset routes. +- **Optional ClawHub publisher discovery (active):** ClawHub + [#2949](https://github.com/openclaw/clawhub/pull/2949), + [#2950](https://github.com/openclaw/clawhub/pull/2950), + and [#2958](https://github.com/openclaw/clawhub/pull/2958) cover publisher + state and labels, the public follow graph, follow controls, and a pull-based + activity timeline. This social lane is not required for signed publisher + feed consumption. ClawHub + [#2953](https://github.com/openclaw/clawhub/pull/2953) is parked until a + concrete registry-export consumer exists. +- **Publisher-feed consumer (active):** The OpenClaw stack is + [#109305](https://github.com/openclaw/openclaw/pull/109305), + [#109340](https://github.com/openclaw/openclaw/pull/109340), + [#109378](https://github.com/openclaw/openclaw/pull/109378), + [#109397](https://github.com/openclaw/openclaw/pull/109397), + [#109461](https://github.com/openclaw/openclaw/pull/109461), + and [#109584](https://github.com/openclaw/openclaw/pull/109584). Together they + cover strict verification and transport, durable refresh/follows, gateway + scheduling and RPC, Control UI following, and signed-profile discovery. +- **Main-catalog scale and incremental refresh (active):** ClawHub + [#3149](https://github.com/openclaw/clawhub/pull/3149), + [#3160](https://github.com/openclaw/clawhub/pull/3160), and + [#3163](https://github.com/openclaw/clawhub/pull/3163) define and produce + strict query/change/reset schemas, retained change history, signed delta and + query pages, and signed sharded snapshots. OpenClaw + [#110250](https://github.com/openclaw/openclaw/pull/110250) consumes complete + signed shard roots and immutable digest-addressed shards. +- **Client-first item watches (active):** OpenClaw + [#110438](https://github.com/openclaw/openclaw/pull/110438) is the first + watch-series slice: durable local plugin watches, accepted per-watch + baselines, bounded update history, source isolation, and CLI read, dismiss, + and mute operations evaluated only after signed refresh. A bounded Control + UI updates surface is deliberately not authored until this storage and CLI + boundary is accepted. ClawHub + [#3171](https://github.com/openclaw/clawhub/pull/3171) is a parked + server-hosted watch/inbox prototype, not a v1 dependency. + +### ClawHub publisher feeds and following + +ClawHub can also publish publisher-backed feeds as a discovery feature. +Publishers are ClawHub's generic public identity for both people and +organizations. Internal user accounts and memberships remain authentication and +ownership details; they are not parallel public feed identities. A publisher +feed uses a stable publisher id rather than a mutable handle or display name. + +This is different from treating every publisher feed entry as official or safe to install. The trust layers stay separate: - Feed source trust means OpenClaw verified that ClawHub produced the feed. - Publisher identity means the feed entry is attributed to a stable ClawHub - account or publisher id, not a mutable display name. -- Official status means the publisher or package has passed ClawHub's official - account or package process. + publisher id, not a mutable display name. - Registry inclusion means a downstream registry, such as a Microsoft registry, has accepted the publisher or package under its own rules. - Install eligibility still depends on package-source artifact verification, ClawHub/OpenClaw release trust, and any downstream security scans or policy checks required by the consuming deployment. -Following an account should therefore be a discovery and notification signal. A -user can follow a ClawHub account to see that account's new skills or plugins, -filter search to followed accounts, or receive update notifications. Following -must not by itself make a package official, bypass security scans, bypass -tenant-admin approval, or allow a feed to introduce new source profiles or -credentials. - -For Microsoft or another enterprise registry, the expected flow is: a publisher -creates or claims a ClawHub account, ClawHub publishes account-backed feed state, -the enterprise registry selects the relevant subset, runs its own scans and -approval checks, and then publishes an approved effective feed for its clients. - -The account-feed work should move on two tracks: - -1. ClawHub product track: define the account or publisher feed model, ownership - and claim flow, follow graph, notifications, profile surfaces, and search - filters such as "people I follow" or "new from followed publishers". This - track owns the user experience for following publishers and discovering new - plugins or skills. -2. OpenClaw trust and runtime track: verify ClawHub-authored feed envelopes, - record source-profile trust state, expose bounded CLI and diagnostics - visibility, and then consume verified account-feed state for discovery. This - track owns what the client can safely display, cache, refresh, and use for - search or notification surfaces. +Following a publisher is therefore a social-discovery signal. ClawHub should +show new work from followed publishers in a pull-based activity timeline rather +than send one notification for every publication. First-class alerts should +start as local OpenClaw item watches anchored to verified signed feed revisions. +ClawHub may later synchronize watches or host a durable user inbox for +cross-device and offline delivery. Synchronizing installed-item watches requires +an explicit account setting because that discloses local inventory. +Following must not make a package official, bypass security scans or approval, +or allow a feed to introduce new source profiles or credentials. + +For an enterprise registry, the expected flow is: a publisher publishes through +ClawHub, ClawHub exposes publisher-feed discovery state, the registry selects +the relevant subset, runs its own scans and approval checks, and publishes an +approved effective feed for its clients. + +The publisher-feed work separates its required protocol/runtime path from +optional ClawHub social discovery: + +1. Core producer/consumer path: ClawHub defines coherent publisher projections; + OpenClaw verifies their envelopes, records source-profile trust, and exposes + bounded follow, refresh, diagnostics, and discovery state. +2. Optional social path: ClawHub may add public follows, profile controls, + activity timelines, and discovery filters such as "people I follow" or + "new from followed publishers". This path does not block signed publisher + feed consumption. These tracks intentionally meet at discovery first. Install authority, official status, Microsoft registry inclusion, tenant approval, security-scan results, and package artifact verification remain separate gates that later PRs must wire explicitly. -Implementation update: the ClawHub account-feed discovery work in this RFC is -now backed by the current ClawHub PR stack. The code-backed slices are -`#2948` through `#2959`: account-feed model/API (`#2948`), claim and -official-state facts (`#2949`), follow graph API (`#2950`), profile and -discovery surfaces (`#2951`), registry and scan bridge (`#2953`), follow -controls and followed-publisher discovery (`#2957`), follow notification -delivery (`#2958`), and public feed/profile routes (`#2959`). Those PRs keep -the same boundary described above: following and discovery do not imply -official status, registry inclusion, install eligibility, or security-scan -bypass. - -The likely PR stacks are: - -ClawHub product stack: - -1. Account-feed model and API: define stable account or publisher ids, account - feed URLs, feed ownership metadata, and whether a feed represents a person, - organization, or curated publisher collection. -2. Account claim and official-state flow: let publishers create or claim a - ClawHub account, record verification state, and keep official account or - package status separate from feed publication. -3. Follow graph and notifications: add follow and unfollow state, notification - events for new or updated skills and plugins, and user preferences for those - notifications. -4. Profile and discovery surfaces: expose publisher profile pages, followed - publisher lists, and search filters such as "people I follow" or "new from - followed publishers". -5. Registry and scan bridge: expose the subset of ClawHub account and feed state - that downstream registries can consume, while preserving their own scans, - approval workflows, and registry-inclusion decisions. - -The next ClawHub implementation stack should build on those foundations without -collapsing the trust gates: - -1. Follow controls and followed-publisher discovery: add follow and unfollow - controls on publisher surfaces, followed-publisher lists, and search filters - such as "people I follow" or "new from followed publishers", with clear - reasons for why entries appear. -2. Follow notification delivery: emit notification events when followed - publishers publish or update skills and plugins, with user preferences, - mute or unsubscribe controls, replay/backfill rules, rate limits, and audit - records. -3. Public feed and profile pages: expose account and publisher feed pages backed - by the account-feed API, including empty, restricted, official, review, and - scan states that reflect only recorded facts. -4. Registry submission workflow: let eligible ClawHub publisher entries be - idempotently queued, retried, withdrawn, or resubmitted for downstream - registry review and scanning while keeping submission separate from approval. -5. Reflected registry and scan state: persist and display downstream review, - scan, inclusion, and local approval states, including status history, only - when those systems report them back to ClawHub. - -OpenClaw trust and runtime stack: - -1. Feed envelope and verifier primitives: parse ClawHub-authored feed envelopes, - verify configured trust anchors, fail closed on invalid signatures, and keep - unsigned or failed-verification bodies inert. -2. Source-profile trust config: define which ClawHub feed profiles and account - feed endpoints are locally trusted, including directly configured publisher - public keys and explicit unsigned opt-in behavior for self-hosted feeds. -3. Refresh and snapshot trust state: persist verification result, sequence, - freshness, and fallback state with the cached feed snapshot without granting - install or search authority by persistence alone. -4. CLI and operator visibility: show feed trust, source profile, account-feed, - stale, fallback, and verification-failure states in refresh, entries, and - diagnostics using bounded fields. -5. Discovery consumption: add followed-account and publisher-specific discovery - filters only after verified account-feed state exists, while keeping install - eligibility and package-source artifact verification on their existing gates. - -After both tracks land, a joint ecosystem phase can build on the verified -account-feed foundation: - -1. Install and policy integration: let verified feed, account, and publisher +The implementation-status section above tracks the current ClawHub publisher +and OpenClaw consumer PRs. Those slices keep the same boundary described here: +following and discovery do not imply official status, registry inclusion, +install eligibility, or security-scan bypass. + +The implementation is organized into independent merge lanes. + +Publisher-feed core is the first product lane: + +1. ClawHub publisher identity, coherent snapshot, and bounded feed API. +2. ClawHub durable revisions, query state, and changed-since state. +3. ClawHub signed snapshot, query, change, and reset routes. +4. OpenClaw verification, bounded transport, durable refresh, and local follow + CLI. Gateway automation and Control UI are a second publisher-feed wave. + +Main-catalog scale is the second product lane: + +1. Shared strict query/change/reset schemas and durable change history. +2. Signed changed-since and indexed query routes. +3. Signed sharded full snapshots. +4. OpenClaw sharded-root and immutable-shard consumption. + +Optional ClawHub social discovery remains independent: + +1. Public publisher follow graph. +2. Follow controls, publisher discovery, and pull-based activity timeline. +3. Publisher state and labels. Registry export remains parked until a concrete + consumer exists. + +Client-first item watches are a third, independent lane. OpenClaw +[#110438](https://github.com/openclaw/openclaw/pull/110438) implements the +durable local store and CLI boundary. A Control UI slice follows only after +that boundary is accepted. Hosted synchronization and inbox delivery remain +optional future work. + +After the publisher and catalog-scale lanes land, a joint ecosystem phase can +build on the verified publisher-feed foundation: + +1. Install and policy integration: let verified feed and publisher state participate in install eligibility, tenant policy, allow lists, block lists, and admin-approved effective feeds. 2. Security and scanning maturity: carry ClawHub/OpenClaw scan results, vulnerability signals, malware checks, provenance attestations, and review - status without treating account follows as scan bypasses. -3. Ranking and recommendations: use followed accounts, official status, scan + status without treating publisher follows as scan bypasses. +3. Ranking and recommendations: use followed publishers, official status, scan state, popularity, recency, and enterprise approval to shape search ranking and recommendations. 4. Enterprise composition: let Microsoft/MOS3 or another registry compose @@ -611,7 +668,7 @@ account-feed foundation: 5. Regional and mirror strategy: support regional ClawHub mirrors, partner mirrors, enterprise mirrors, key rotation, and failover rules. 6. Publishing workflow: define the path from local skill or plugin authoring to - ClawHub account-feed publication, scanning, signing, follower notification, + ClawHub publisher-feed publication, scanning, signing, timeline discovery, and discovery. 7. Admin and audit surfaces: add audit logs, install provenance, visibility explanations, drift reports, stale-feed reports, and tenant admin controls. @@ -898,7 +955,7 @@ activated. of platform release-signing identities. 5. Publish the first ClawHub-hosted feed for the current external plugin catalog. The same ClawHub platform signing path should sign `clawhub-public`, named - feeds, account feeds, organization feeds, and ClawHub-served composed feeds. + feeds, publisher feeds, organization feeds, and ClawHub-served composed feeds. The first public feed may include all current external entries; ClawHub can narrow future default feeds to official packages as the official catalog grows. @@ -909,9 +966,9 @@ activated. 9. Extend the same discovery contract to skills after the skill installer accepts catalog candidates, including GitHub-indexed skills that do not have ClawHub-hosted artifacts. -10. Add ClawHub account-backed feeds and following as a discovery layer for +10. Add ClawHub publisher-backed feeds and following as a discovery layer for publisher-specific skill and plugin updates. These feeds should be - ClawHub-authored and verified, but following an account should not imply + ClawHub-authored and verified, but following a publisher should not imply official status, registry inclusion, install eligibility, or security-scan bypass. 11. Add composition guidance and examples for Microsoft/MOS3 and other @@ -933,9 +990,9 @@ activated. "feed" for the protocol and propagation artifact? - Which runtime policy metadata should feed entries be allowed to reference without turning the feed into a policy engine? -- Which ClawHub account-feed states should OpenClaw expose for following, - notifications, and search filters without turning followed accounts into - install authority? +- Which ClawHub publisher-feed states should OpenClaw expose for following, + timelines, local installed-content notifications, and search filters without + turning followed publishers into install authority? - How many stable OpenClaw releases should ship hosted feed fallback before Scout, Microsoft, and other clients depend on the contract? - Should a later LTS release define stronger compatibility guarantees for feed diff --git a/rfcs/0009/clawhub-account-feeds-v1-spec.md b/rfcs/0009/clawhub-account-feeds-v1-spec.md index 014cb44b..1931d059 100644 --- a/rfcs/0009/clawhub-account-feeds-v1-spec.md +++ b/rfcs/0009/clawhub-account-feeds-v1-spec.md @@ -1,290 +1,323 @@ -# ClawHub Account Feeds v1 Addendum Specification +# ClawHub Publisher Feeds v1 Addendum Specification -This document is the implementer-facing account-feed addendum for RFC 0009 -hosted feeds. It builds on the core feed specification and the signed-feed trust -addendum. +This document defines the ClawHub publisher discovery-feed contract associated +with RFC 0009. The historical filename is retained to avoid breaking links +while the addendum is under review. Status: draft addendum, tied to RFC 0009. +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are normative. + ## Scope This addendum defines: -- ClawHub account and publisher feed identity; -- account-feed metadata; -- account feed entries for plugins and skills; -- the boundary between following and install authority; -- user-facing account feed discovery states; -- downstream registry consumption of ClawHub account-feed facts; -- diagnostics and provenance requirements. - -This addendum does not define: +- stable publisher feed identity; +- the strict publisher-feed document and entry shapes; +- bounded pagination and refresh semantics; +- public publisher-follow discovery; +- local item-watch semantics and optional hosted synchronization; +- the boundary between following, discovery, trust, and installation; +- downstream registry provenance and diagnostics. -- the core feed document shape; -- signed envelope mechanics; -- package artifact trust; -- security scanning implementation; -- self-service claim workflows; -- organization-admin approval workflows; -- notification delivery protocols beyond feed/follow semantics. +It does not define a separate account feed, installable catalog entries, +package candidates, concrete push or channel transports, organization +approval, artifact trust, or security scanning. -## Model +Publisher feeds are the first rollout target for the reusable query and change +contracts in `hosted-feed-distribution-v1-spec.md`. A publisher with hundreds of +skills or plugins must remain discoverable without requiring one unbounded +response. -ClawHub can publish feeds for stable account or publisher identities. These -feeds let OpenClaw and downstream registries discover the packages associated -with a publisher without treating account following as a security or install -grant. +Signed complete publisher snapshots use payload type +`openclaw.clawhub-publisher-feed-snapshot.v1`. Signed publisher query pages use +payload type +`openclaw.clawhub-publisher-feed-query-results.v1`. Signed publisher change +pages use `openclaw.clawhub-publisher-feed-changes.v1`. These payload types MUST +NOT be accepted as install catalogs. A sharded complete publisher-feed root uses +`openclaw.clawhub-publisher-feed-shard-root.v1`. -An account feed is still a normal hosted feed. It should use the core v1 feed -shape for entries and the signed-feed trust addendum for authenticity. The -account-feed addendum defines the feed identity and metadata conventions around -that core payload. +## Publisher Identity -## Identity +ClawHub publishers are the public identity for both people and organizations. +Internal user or membership records are authentication and ownership details; +they are not separate public feed identities. -Account feeds must use stable ClawHub identity, not mutable display names. - -Recommended feed ids: +A publisher feed MUST use the stable publisher id, not a mutable handle, +display name, profile URL, linked user id, or organization membership id. ```text -clawhub-account: -clawhub-publisher: -clawhub-org: +clawhub.publisher. ``` -Display handles, names, avatars, bios, and URLs are metadata. They may change -without changing feed identity. +Changing a handle, display name, members, or owners MUST NOT change the feed id. + +## Relationship To Hosted Catalog Feeds + +A publisher feed is a ClawHub discovery projection. It is not the Hosted Feed +v1 install catalog defined by `hosted-feed-v1-spec.md`. + +Publisher-feed entries identify public skills and plugins associated with a +publisher. To install one, a client MUST resolve it through an accepted install +catalog or another explicit package resolver and MUST still apply source, +integrity, scan, approval, and runtime policy checks. + +## Wire Contract -## Feed Metadata +The normative v1 schema is the generated `PublisherFeedSchema` in the ClawHub +schema package. The schema is strict. Conforming publishers MUST NOT add unknown +fields to a v1 document or entry. -Account feeds should carry bounded account metadata in `metadata`. +## Feed Document ```json { "schemaVersion": 1, - "id": "clawhub-publisher:openclaw", - "generatedAt": "2026-07-15T00:00:00.000Z", + "feedId": "clawhub.publisher.publishers:01JEXAMPLE", + "publisherId": "publishers:01JEXAMPLE", + "handle": "openclaw", + "displayName": "OpenClaw", + "generatedAt": "2026-07-16T00:00:00.000Z", "sequence": 17, - "expiresAt": "2026-07-22T00:00:00.000Z", - "metadata": { - "kind": "clawhubPublisherFeed", - "publisherId": "openclaw", - "handle": "openclaw", - "displayName": "OpenClaw", - "profileUrl": "https://clawhub.ai/creators/openclaw", - "officialState": "official" - }, - "entries": [] + "entries": [], + "nextCursor": null } ``` -Recommended metadata fields: +| Field | Type | Required | Semantics | +| --- | --- | --- | --- | +| `schemaVersion` | integer | Yes | MUST be `1`. | +| `feedId` | string | Yes | `clawhub.publisher.`, 1 to 256 UTF-8 bytes. | +| `publisherId` | string | Yes | Stable ClawHub publisher id, 1 to 200 UTF-8 bytes. | +| `handle` | string or null | Yes | Current mutable public handle, 1 to 64 UTF-8 bytes when present. | +| `displayName` | string | Yes | Current public display name, 1 to 256 UTF-8 bytes. | +| `generatedAt` | RFC 3339 string | Yes | Generation time for this logical revision. | +| `sequence` | non-negative safe integer | Yes | Monotonic revision for this `feedId`. | +| `entries` | array | Yes | At most 200 ordered public discovery entries. | +| `nextCursor` | string or null | Yes | Opaque continuation cursor, at most 4096 UTF-8 bytes. | + +Every page for one coherent revision SHOULD report the same `feedId`, +`generatedAt`, and `sequence`. A publisher MUST NOT derive `sequence` from the +newest visible entry timestamp because deletion or demotion could make it +decrease. + +Each new accepted publisher-feed revision MUST increment `sequence` by exactly +one. Re-serving the same revision MUST preserve the same sequence and logical +content. + +## Entry Object + +Every publisher-feed entry has exactly these fields: | Field | Type | Required | Semantics | | --- | --- | --- | --- | -| `kind` | string | Yes | `clawhubAccountFeed`, `clawhubPublisherFeed`, or `clawhubOrganizationFeed`. | -| `accountId` | string | Conditional | Stable account id for account feeds. | -| `publisherId` | string | Conditional | Stable publisher id for publisher feeds. | -| `organizationId` | string | Conditional | Stable organization id for organization feeds. | -| `handle` | string | Optional | Current display handle. | -| `displayName` | string | Optional | Current display name. | -| `profileUrl` | string | Optional | Human-facing ClawHub profile URL. | -| `claimState` | string | Optional | Publisher claim state when known. | -| `officialState` | string | Optional | ClawHub official-state fact when known. | -| `restrictionState` | string | Optional | Suspended, restricted, or normal state when known. | +| `kind` | string | Yes | `skill` or `plugin`. | +| `id` | string | Yes | Stable ClawHub object or package identity, 1 to 256 UTF-8 bytes. | +| `name` | string | Yes | Current package name or slug, 1 to 256 UTF-8 bytes. | +| `displayName` | string | Yes | User-facing title, 1 to 256 UTF-8 bytes. | +| `summary` | string or null | Yes | Public summary, at most 1024 UTF-8 bytes. | +| `url` | string | Yes | Canonical public ClawHub page URL or safe origin-relative reference, at most 2048 UTF-8 bytes. | +| `updatedAt` | number | Yes | Finite, non-negative Unix epoch time in milliseconds. | -Metadata fields are facts from ClawHub. They do not by themselves grant install -authority. +An entry intentionally has no install candidate, artifact URL, integrity claim, +official status, approval state, or scan result. Those facts have separate +owners. -## Entry Requirements +Clients MUST resolve an origin-relative `url` against the origin of the +publisher-feed request. Publishers MUST reject backslashes, control characters, +protocol-relative references, and any reference that resolves away from that +origin. -Entries in account feeds must use the core feed entry shape. Account-feed -entries should preserve the account or publisher provenance in bounded metadata: +Publishers MUST order entries by descending `updatedAt` and MUST use stable +kind and object-identity tie-breakers when timestamps are equal. -```json -{ - "type": "plugin", - "id": "acpx", - "title": "ACP-X", - "version": "1.2.3", - "state": "available", - "publisher": { - "id": "openclaw", - "trust": "official" - }, - "metadata": { - "clawhub": { - "publisherId": "openclaw", - "packageId": "acpx", - "packageUrl": "https://clawhub.ai/packages/acpx" - } - }, - "install": { - "candidates": [ - { - "sourceRef": "public-clawhub", - "package": "acpx", - "version": "1.2.3", - "integrity": "sha512-..." - } - ] - } -} -``` +## Pagination And Revision -Account feeds may include plugins, skills, or both. Clients must not route a -skill through the plugin installer merely because both appear in one account -feed. +`nextCursor` is opaque and MUST NOT be constructed or interpreted by clients. A +client follows it until it is null or a local page or entry bound is reached. -## Following Semantics +ClawHub MUST preserve deterministic ordering across page boundaries and MUST +NOT report `nextCursor: null` when a bounded scan stopped before all eligible +entries were considered. Cursor errors SHOULD return a bounded client error +rather than silently restarting at the first page. -Following a ClawHub account, publisher, or organization is a discovery and -notification preference. It means: +ClawHub MUST bound requested and effective page size, indexed scan work, +response bytes, and cursor lifetime. A page contains at most 200 entries and is +at most 1 MiB; a cursor is at most 4096 UTF-8 bytes and expires within 15 +minutes. Cursor expiry MUST return a bounded client error rather than restart. -- show the followed identity in the user's followed-publisher list; -- optionally include matching entries in discovery filters; -- optionally notify the user when the publisher adds or updates packages; -- preserve provenance so the user can see why an entry appeared. +If ClawHub cannot yet provide coherent monotonic revisions and deterministic +continuation, it MUST label the route experimental rather than publish it as a +stable v1 feed contract. -Following does not mean: +### Search And Changed-Since Retrieval -- the publisher is official; -- the package is OpenClaw-reviewed; -- the package passed security scanning; -- the package is locally approved; -- the package is organization-approved; -- the package is installable; -- the package bypasses source-profile, artifact-integrity, or policy checks. +ClawHub SHOULD expose signed publisher-feed query projections for bounded text +and kind filters. Query cursors MUST bind the stable publisher id, feed +revision, normalized query, and authorization context. -Clients should label followed-account results as followed or from a followed -publisher, not as trusted or approved. +ClawHub SHOULD also expose signed publisher-feed change projections from a +caller's accepted sequence. The change stream MUST include complete upserts and +tombstones for content that is deleted, blocked, made private, or transferred +away from the publisher. All pages MUST be pinned to one `toSequence`, and a +client MUST apply them atomically. -## Claim, Official, And Scan State +If the requested publisher-feed revision is older than retained change history, +ClawHub MUST require a full publisher-feed refresh. It MUST NOT silently return +only the retained tail. These projections remain discovery data; installation +still resolves entries through an accepted install catalog. -ClawHub may expose publisher facts such as claim state, official state, -restriction state, scan state, or registry review state. These facts should be -explicit fields or metadata; clients and downstream registries must not infer -them from following state. +### Signed Complete Snapshot -Recommended separation: +`GET /api/v1/publishers/{publisherId}/feed/snapshot` returns a DSSE envelope +whose payload type is `openclaw.clawhub-publisher-feed-snapshot.v1`. Its decoded +payload is a strict complete publisher feed with exactly `schemaVersion`, +`feedId`, `publisherId`, `handle`, `displayName`, `generatedAt`, `expiresAt`, +`sequence`, and `entries`. It has no cursor and MUST NOT be truncated. -| Fact | Meaning | -| --- | --- | -| `claimState` | Whether the publisher identity is claimed or claimable. | -| `officialState` | Whether ClawHub marks the publisher as official. | -| `restrictionState` | Whether the publisher is restricted, suspended, or normal. | -| `scanState` | Whether a package has scan metadata, if ClawHub exposes it. | -| `registryReviewState` | Whether a downstream registry has reflected review state. | -| `followState` | Whether the current user follows the publisher. | +The payload repeats the identity and entry bounds above, contains at most 400 +entries, and is at most 1 MiB. `expiresAt` is an RFC 3339 instant later than +`generatedAt`. A client MUST verify the envelope, expected payload type, +publisher-derived feed id, expiry, response bounds, and entry uniqueness before +atomically replacing accepted state. This discovery snapshot does not grant +install authority. -Each fact has a separate source of authority. Clients should show labels and -diagnostics that preserve the distinction. +## Public API -## Follow Privacy And Abuse Controls +The initial publisher routes are: -Follow state is user-specific preference data. ClawHub should treat follower -lists as private by default unless a user or organization explicitly publishes a -public list. Account-feed publication must not expose who follows an account. +```text +GET /api/v1/publishers/{publisherId} +GET /api/v1/publishers/{publisherId}/feed +GET /api/v1/publishers/{publisherId}/feed/snapshot +GET /api/v1/publishers/{publisherId}/feed/query?q=&kind=&limit= +GET /api/v1/publishers/{publisherId}/feed/changes?fromSequence=&limit= +``` -Follow and unfollow operations should be reversible, idempotent, and bounded. -Implementations should rate-limit follow changes, suppress self-follow -notifications, and provide a way to mute or disable notifications without -unfollowing the publisher. +The first query or change request carries its selectors and optional bounded +limit. A continuation request carries only the opaque `cursor`; clients MUST +NOT repeat or alter initial selectors alongside it. ClawHub MUST reject a +cursor combined with initial selectors rather than guess which values win. -Blocked, suspended, deleted, or private publishers should not silently remain in -normal followed-discovery results. Clients should show an actionable status such -as unavailable, restricted, or private rather than treating the feed as a -successful empty feed. +The unsigned paginated `/feed` route is machine-readable public discovery, not +signed synchronization state. It SHOULD use `Cache-Control: private, no-store` +so a shared cache cannot bypass cursor freshness or publisher visibility. A +signed-feed client follows `/feed/snapshot`, `/feed/query`, and +`/feed/changes`; it MUST NOT upgrade the unsigned route into signed authority. -## Downstream Registry Consumption +ClawHub does not expose parallel `/accounts` feed routes. Publisher detail may +include public profile facts and a canonical feed URL, but MUST NOT expose +private owner, member, linked-user, moderation, or authentication records. + +Public APIs SHOULD return the same not-found shape for missing, non-public, +restricted, and suspended publishers. Operator-only diagnostics MAY distinguish +those conditions. A producer that cannot prove a coherent complete revision or +cannot sign a required projection MUST return a non-cacheable service-unavailable +response; it MUST NOT return a truncated or unsigned substitute. -Downstream registries can consume ClawHub account feeds as source input. They -may subset, block, pin, scan, or re-publish account-feed entries into their own -effective feeds. +## Following And Timeline -When a downstream registry emits an effective feed, it should preserve bounded -provenance: +Following a publisher is a reversible discovery preference. It MAY: -- ClawHub feed id; -- ClawHub feed sequence or checksum; -- ClawHub publisher id; -- ClawHub package id; -- selected version or revision; -- downstream policy state. +- include the publisher in public follower and following lists; +- include matching entries in discovery filters; +- populate a pull-based activity timeline; +- explain why an entry appeared. -Downstream registries should not treat account following as approval. They must -run their own review, scan, allow-list, block-list, or organization policy before -calling an entry approved. +Follower and following lists are public social-discovery data. Notification and +mute preferences remain private. Follow and unfollow operations SHOULD be +authenticated, idempotent, rate-limited, and based on stable publisher ids. -## Discovery And UI Requirements +ClawHub SHOULD prefer a timeline over per-publication notification fanout. A +high-volume publisher can publish hundreds of entries, and following that +publisher does not imply that every publish deserves an alert. -Clients and ClawHub surfaces should make account-feed state explainable: +OpenClaw SHOULD support local item watches using the distribution addendum's +watch contract. An item watch is explicit alert intent and is evaluated against +verified signed publisher or catalog changes. Following a publisher MUST NOT +automatically create one item alert per publication. -- profile pages should show package entries without implying install approval; -- follow buttons should say follow or following, not trust; -- official labels should be separate from scan or review labels; -- unavailable, blocked, or restricted publishers should have actionable status; -- discovery results should say when an entry appears because of a followed - publisher. +ClawHub MAY later synchronize authenticated item watches or provide a durable +hosted inbox for cross-device and offline delivery. That hosted capability is +not required for publisher-feed v1. Synchronizing watches derived from installed +content requires an explicit account setting because it discloses installed-item +identities; without it, OpenClaw keeps installed-item watches local. -Empty or restricted account feeds should be explicit. A missing account feed, -private feed, deleted publisher, or blocked publisher should not silently look -like an empty successful feed. +Hosted inbox delivery is a hint. OpenClaw or Control UI MUST verify the +referenced signed publisher or catalog change before presenting an actionable +update, and an alert MUST NOT install, update, enable, approve, or remove +content. -## API Shape +Following MUST NOT mean that a publisher is official, reviewed, trusted, +scanned, approved, or installable. UI and APIs MUST use follow/following +language rather than trust or approval language. -The exact ClawHub HTTP routes can evolve, but account feed APIs should preserve -these capabilities: +## Client Consumption + +A client MAY expose a command such as: ```text -GET /v1/feeds/accounts/{accountId} -GET /v1/feeds/publishers/{publisherId} -GET /v1/feeds/organizations/{organizationId} -POST /v1/publisher-follows -DELETE /v1/publisher-follows/{publisherId} -GET /v1/publisher-follows +openclaw publisher follow ``` -Feed routes should return hosted feed payloads or signed envelopes. Follow -routes are user-specific application APIs and should not be confused with the -feed payload itself. +The handle is a lookup convenience only. The client MUST resolve it through +ClawHub and persist or transmit the stable publisher id. Feed URLs remain +machine-readable API discovery, not a required profile-page control. + +## Trust And Signing Boundary + +The atomic publisher-feed snapshot MUST use +`openclaw.clawhub-publisher-feed-snapshot.v1` and MUST NOT reuse +`openclaw.official-external-plugin-catalog-feed.v1`. Its complete sharded +representation, plus signed query and change projections, use the distinct +payload types defined above and the strict distribution-addendum schemas. Each +requires expected publisher-feed identity binding, test vectors, and explicit +client verifier registration. + +All publisher-feed representations MAY use the same dedicated ClawHub platform +feed-signing key and bundled public trust anchor as other ClawHub-operated +feeds, but each payload type remains distinct from install-catalog authority. + +Until the corresponding producer and verifier for a representation land, HTTPS +transport and ClawHub origin identify the API source, but publisher-feed content +still grants discovery only and never install authority. + +## Downstream Registry Consumption -## Diagnostics And Audit +A downstream registry MAY consume publisher feeds as discovery input, resolve +selected entries through an install catalog, and apply its own review, scan, +allow-list, block-list, and organization policy. -Clients and ClawHub should emit bounded diagnostics for: +Derived records SHOULD preserve the publisher feed id and revision, publisher +id, entry kind and stable id, separately resolved catalog provenance, and the +downstream decision. Following state MUST NOT be translated into approval. -- account-feed fetch result; -- account-feed verification result; -- missing, private, deleted, restricted, or blocked publisher state; -- follow and unfollow actions; -- discovery results sourced from followed publishers; -- downstream registry import of account-feed entries. +## Diagnostics -Diagnostics must not include raw tokens, private profile data, unbounded URLs, -prompt contents, tool arguments, package payload bytes, or raw user identifiers -unless the local deployment explicitly permits them. +ClawHub and clients SHOULD distinguish malformed feeds, non-public publishers, +invalid cursors, bounded-scan exhaustion, follow outcomes, timeline query +limits, and catalog resolution failures. -## Publisher Checklist +Diagnostics MUST NOT expose tokens, private profile or membership data, +moderation evidence, package payload bytes, or private notification settings. +Public diagnostics MUST NOT distinguish a nonexistent publisher from a hidden, +restricted, or suspended publisher. -ClawHub account-feed publication is compatible with this addendum when it: +## Conformance -- uses stable account, publisher, or organization ids; -- keeps mutable display fields in metadata; -- signs account feeds through the ClawHub platform feed-signing path; -- preserves entry provenance for publisher and package identity; -- separates follow, official, scan, registry review, and install state; -- makes empty, private, restricted, and blocked feed states distinguishable. +A v1 publisher-feed producer: -## Client Checklist +- uses stable publisher identity only; +- exposes only public publisher and entry facts; +- emits deterministic bounded pages and monotonic revisions; +- keeps following separate from trust and install authority; +- does not expose internal account, ownership, or moderation state. -An OpenClaw client is compatible with this addendum when it: +A client: -- treats account feeds as discovery input, not install authority; -- verifies ClawHub-hosted account feeds according to the signed-feed trust - addendum when configured; -- preserves followed-publisher provenance in discovery results; -- keeps source-profile and artifact-integrity checks on the install path; -- displays follow state separately from official, scan, review, or approval - state; -- produces bounded diagnostics for account-feed failures and followed-publisher - discovery. +- treats the feed as discovery rather than install authority; +- resolves entries through an accepted catalog before installation; +- stores stable publisher ids rather than mutable handles; +- bounds pagination and reports unavailable or malformed state; +- keeps local item-watch alerts separate from ClawHub publisher following; +- verifies referenced signed feed state before presenting actionable alerts. diff --git a/rfcs/0009/hosted-feed-distribution-v1-spec.md b/rfcs/0009/hosted-feed-distribution-v1-spec.md new file mode 100644 index 00000000..84a075ff --- /dev/null +++ b/rfcs/0009/hosted-feed-distribution-v1-spec.md @@ -0,0 +1,463 @@ +# Hosted Feed Distribution and Query v1 Addendum Specification + +This document defines scalable snapshot, query, and incremental-refresh +transport for Hosted Feed v1. It builds on `hosted-feed-v1-spec.md` and the +signed transport in `signed-feed-trust-v1-spec.md`. + +Status: draft addendum, tied to RFC 0009. + +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are normative. + +## Scope + +This addendum defines: + +- signed root manifests and immutable catalog shards; +- signed, revision-bound query result pages; +- signed incremental change pages, including tombstones; +- cursor, consistency, retention-gap, and atomic-application rules. + +It does not define ranking algorithms, access-control policy, package +installation, or artifact trust. + +## Distribution Modes + +A feed publisher MAY expose any combination of these representations: + +1. **Atomic snapshot:** the complete Hosted Feed v1 document when it fits + publisher and client limits. +2. **Sharded snapshot:** a signed root manifest referencing immutable, + digest-addressed shards for complete synchronization. +3. **Query projection:** signed pages containing the ordered subset matching a + bounded query at one feed revision. +4. **Change projection:** signed pages describing every change after a known + sequence through one fixed target sequence. + +Publishers MUST NOT silently truncate any representation. Query and change +projections do not prove that a client possesses the complete feed. A client +that requires complete offline state MUST accept an atomic or sharded snapshot. + +## Payload-Type Binding + +This addendum defines reusable representation shapes, not one universal signed +payload type. Each concrete feed contract MUST assign a distinct versioned +payload type to each representation it accepts. For example, ClawHub publisher +feeds use `openclaw.clawhub-publisher-feed-snapshot.v1`, +`openclaw.clawhub-publisher-feed-query-results.v1`, and +`openclaw.clawhub-publisher-feed-changes.v1`. Its sharded root uses +`openclaw.clawhub-publisher-feed-shard-root.v1`. + +The official external plugin install catalog uses these exact scalable +representation types: + +- `openclaw.official-external-plugin-catalog-shard-root.v1`; +- `openclaw.official-external-plugin-catalog-query-results.v1`; +- `openclaw.official-external-plugin-catalog-changes.v1`. + +The official ClawHub skills catalog shard root uses +`openclaw.official-skills-catalog-shard-root.v1`. Any query or change +representations for that catalog, and all other install catalogs, MUST assign +their own distinct types before use. A verifier MUST reject a valid signature +whose payload type does not match the selected feed class and representation. + +This prevents a signed discovery projection from being replayed as an install +catalog, or one feed operator's response from being interpreted under another +schema. + +All v1 documents and nested objects in this addendum are strict and MUST reject +unknown fields. `feedId` is 1 to 256 UTF-8 bytes. Cursor strings are at most +4096 UTF-8 bytes. URLs are at most 2048 UTF-8 bytes. Timestamp strings are at +most 64 UTF-8 bytes and MUST be valid RFC 3339 instants. + +## Sharded Snapshot Root + +```json +{ + "schemaVersion": 1, + "feedId": "clawhub-official", + "sequence": 42, + "generatedAt": "2026-07-16T00:00:00.000Z", + "expiresAt": "2026-07-23T00:00:00.000Z", + "metadata": { + "description": "Official OpenClaw plugins published on ClawHub." + }, + "entryCount": 2450, + "shards": [ + { + "index": 0, + "url": "https://clawhub.ai/v1/feeds/plugins/shards/sha256-abc.json", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "byteLength": 524288, + "entryCount": 500 + } + ] +} +``` + +The root fields are strict: + +| Field | Type | Semantics | +| --- | --- | --- | +| `schemaVersion` | integer | MUST be `1`. | +| `feedId` | string | Stable feed identity. | +| `sequence` | non-negative safe integer | Complete snapshot revision. | +| `generatedAt` | RFC 3339 string | Root publication time. | +| `expiresAt` | RFC 3339 string | Root and shard-set expiry. | +| `metadata` | object | Complete feed-level metadata defined by the concrete payload type. | +| `entryCount` | non-negative safe integer | Exact entries across all shards. | +| `shards` | array | Ordered immutable shard descriptors. Empty only when `entryCount` is zero. | + +Each shard descriptor has exactly these fields: + +| Field | Type | Semantics | +| --- | --- | --- | +| `index` | non-negative safe integer | Position in the root's shard array. | +| `url` | string | Immutable HTTPS shard URL. | +| `sha256` | string | Exact lowercase hexadecimal digest described below. | +| `byteLength` | positive safe integer | Exact response-byte length, at most 1 MiB. | +| `entryCount` | positive safe integer | Exact shard entry count, at most 10,000. | + +When `entryCount` is zero, `shards` MUST be empty. Otherwise `shards` MUST be +non-empty and indexes MUST be contiguous from zero. URLs MUST use HTTPS and +satisfy the selected feed profile's origin policy. `sha256` is exactly 64 +lowercase hexadecimal characters encoding the 32-byte SHA-256 digest of the +decoded representation-data bytes after HTTP content decoding. `byteLength` +is the length of those same decoded bytes, independent of gzip, Brotli, or +another transfer/content encoding. `entryCount` is exact. Publishers MUST serve +the same decoded representation bytes for a shard URL regardless of negotiated +HTTP encoding. + +Descriptor URLs MUST be unique within a root, and descriptor digests MUST be +unique within a root. The sum of descriptor `entryCount` values MUST equal the +root `entryCount`; the sum of descriptor `byteLength` values is the aggregate +shard-set size used for the limit below. + +A shard document has exactly these fields: + +| Field | Type | Semantics | +| --- | --- | --- | +| `schemaVersion` | integer | MUST be `1`. | +| `feedId` | string | MUST match the signed root. | +| `sequence` | non-negative safe integer | MUST match the signed root. | +| `index` | non-negative safe integer | MUST match the descriptor. | +| `entries` | array | Entries validated by the concrete feed contract. | + +Entries across the shard set MUST have unique identities and deterministic +global ordering. A root contains at most 1024 shards and 1,000,000 entries and +is at most 1 MiB. The aggregate shard set is at most 256 MiB. A concrete feed +contract MAY impose lower limits but MUST NOT raise these v1 maxima. + +The official external plugin catalog root metadata has exactly `description`, +which is a string of at most 1024 UTF-8 bytes or null. Publisher-feed root +metadata has exactly `publisherId`, `handle`, and `displayName`, with the types +and bounds in the publisher-feed addendum. Other root payload types MUST define +their complete strict metadata object before use. + +Publishers MUST write every immutable shard before publishing the signed root. +They MUST NOT mutate bytes at a published shard URL. Clients MUST verify the +signed root, enforce aggregate byte, shard, and entry limits, fetch every shard, +verify lengths and digests, validate all entries, and atomically replace the +prior root and shard set only after the entire snapshot succeeds. + +Shards are authenticated by their exact byte length and digest in the signed +root and are not independently DSSE-signed. A client that persists a sharded +snapshot MUST retain the exact accepted root payload and exact shard bytes, or +an equivalent lossless representation. Before using that snapshot as fallback, +it MUST reverify the root under the currently selected trust policy, reject an +expired root using the current load time, and reverify every retained shard's +length and digest before parsing or exposing entries. A partial or expired +cached shard set MUST fail closed; it MUST NOT replace or extend the root's +validity window. + +## Signed Query Projection + +```json +{ + "schemaVersion": 1, + "feedId": "clawhub.publisher.publishers:01JEXAMPLE", + "sequence": 17, + "generatedAt": "2026-07-16T00:00:00.000Z", + "expiresAt": "2026-07-16T00:05:00.000Z", + "query": { + "text": "cuda", + "kinds": ["plugin", "skill"] + }, + "requestCursor": null, + "pageIndex": 0, + "startIndex": 0, + "resultCount": 0, + "entries": [], + "nextCursor": null +} +``` + +The query object is a strict normalized representation of the request. A +concrete feed contract defines its allowed filters. ClawHub publisher-feed v1 +allows `text` and `kinds`; the install catalog may additionally allow entry +states and publisher ids. Arrays MUST be sorted and deduplicated. Omitted +filters are absent from the normalized query object. Publisher-defined ranking is +allowed, but ordering MUST be deterministic for a given query and sequence. + +Text normalization is exact: decode valid UTF-8, normalize to Unicode NFC, +trim leading and trailing ASCII whitespace characters (`U+0009` through +`U+000D` and `U+0020`), and replace each remaining run of those characters with +one `U+0020`. Case is preserved; matching and ranking may be case-insensitive +but do not change the signed query value. Enum values use the lowercase +spellings in their defining schema. Arrays are deduplicated and sorted by +lexicographic UTF-8 byte order. An omitted filter is absent from the query +object. Empty text and empty arrays are invalid and MUST NOT be treated as +omitted. Clients compare the parsed normalized query structurally, not by JSON +object member order. + +Every page MUST repeat the same normalized query, feed id, sequence, and expiry. +It MUST include `requestCursor`, exactly matching the cursor sent for that +request or null for the first page, plus a zero-based `pageIndex`. The opaque +`nextCursor` MUST be integrity-protected and bound to those values plus any +access-control context needed to prevent replay across principals. A client +MUST verify `requestCursor` and the expected page index before accepting a page. +A cursor MUST NOT continue against a newer feed revision. Publishers SHOULD +return a bounded conflict or reset response when the pinned revision is gone. + +A valid signature authenticates the returned projection and the publisher's +claim that it matches the bound query at that revision. It does not make an +entry installable or prove that the client has synchronized the complete feed. + +A query page has exactly these fields: + +| Field | Type | Semantics | +| --- | --- | --- | +| `schemaVersion` | integer | MUST be `1`. | +| `feedId` | string | Bound feed identity. | +| `sequence` | non-negative safe integer | Pinned feed revision. | +| `generatedAt` | RFC 3339 string | Page generation time. | +| `expiresAt` | RFC 3339 string | MUST be later than `generatedAt`. | +| `query` | object | Strict normalized filters defined by the concrete payload type. | +| `requestCursor` | string or null | Exact request cursor; null on page zero. | +| `pageIndex` | non-negative safe integer | Zero-based page position. | +| `startIndex` | non-negative safe integer | Global result offset of the first entry. | +| `resultCount` | non-negative safe integer | Exact matching entries across all pages. | +| `entries` | array | At most 200 entries using the concrete feed entry schema. | +| `nextCursor` | string or null | Integrity-protected continuation. | + +A query page is at most 1 MiB. For publisher-feed query payloads, `query` has +only optional `text` and `kinds` fields. `text` is 1 to 256 UTF-8 bytes after +normalization. `kinds` is a sorted, deduplicated, non-empty subset of `plugin` +and `skill`. At least one query field MUST be present. The official install +catalog query has only optional `text`, `types`, `states`, and `publisherIds`. +`text` has the same bound; `types` is a non-empty subset of `plugin` and +`skill`; `states` is a non-empty subset of the Hosted Feed v1 entry states; and +`publisherIds` is a sorted, deduplicated array of 1 to 100 publisher ids, each +1 to 256 UTF-8 bytes. At least one query field MUST be present. + +The first query page MUST have `startIndex: 0`. Every continuation page's +`startIndex` MUST equal the prior page's `startIndex + entries.length`. +`resultCount` MUST be identical on every page, and a terminal page MUST end +exactly at `resultCount`. Clients MUST reject a gap, overlap, or premature +terminal page. + +## Signed Change Projection + +```json +{ + "schemaVersion": 1, + "feedId": "clawhub.publisher.publishers:01JEXAMPLE", + "fromSequence": 14, + "toSequence": 16, + "generatedAt": "2026-07-16T00:00:00.000Z", + "expiresAt": "2026-07-16T00:05:00.000Z", + "requestCursor": null, + "pageIndex": 0, + "startIndex": 0, + "changeCount": 2, + "changes": [ + { + "sequence": 15, + "operation": "upsert", + "entry": { + "kind": "skill", + "id": "skills:01JEXAMPLE", + "name": "cuda-helper", + "displayName": "CUDA Helper", + "summary": "Tools for CUDA development.", + "url": "/example/cuda-helper", + "updatedAt": 1784160000000 + } + }, + { + "sequence": 16, + "operation": "remove", + "entryId": "skills:01JRETIRED", + "entryKind": "skill" + } + ], + "nextCursor": null +} +``` + +`fromSequence` is the client's accepted revision and is exclusive. +`toSequence` is the fixed target revision and is inclusive. Every continuation +page MUST remain pinned to this range and carry the exact request cursor and +expected zero-based page index as defined for query projections. Changes MUST +be ordered by sequence and a stable within-sequence order. An `upsert` carries +the complete current entry. A `remove` tombstone carries the stable entry id and +type. + +A concrete change payload MAY also carry a metadata record with exactly +`sequence`, `operation: "metadata"`, and `metadata`. The metadata object is the +same complete strict object used by that feed's sharded root. A publisher-feed +metadata record therefore replaces `publisherId`, `handle`, and `displayName` +together; an install-catalog metadata record replaces `description`. + +Publishers MUST emit tombstones when an entry is deleted, made private, or +otherwise leaves the requesting principal's effective feed. In a discovery +feed, a blocked entry that is no longer publicly discoverable uses a tombstone. +In an install catalog, `blocked` is an explicit deny with composition precedence +and MUST be emitted as a complete upsert carrying that state; it MUST NOT be +converted to a removal. A changed-items endpoint that omits required removals +or explicit deny-state upserts is not conformant. + +A change page has exactly these fields: + +| Field | Type | Semantics | +| --- | --- | --- | +| `schemaVersion` | integer | MUST be `1`. | +| `feedId` | string | Bound feed identity. | +| `fromSequence` | non-negative safe integer | Exclusive accepted revision. | +| `toSequence` | non-negative safe integer | Inclusive pinned target; not below `fromSequence`. | +| `generatedAt` | RFC 3339 string | Page generation time. | +| `expiresAt` | RFC 3339 string | MUST be later than `generatedAt`. | +| `requestCursor` | string or null | Exact request cursor; null on page zero. | +| `pageIndex` | non-negative safe integer | Zero-based page position. | +| `startIndex` | non-negative safe integer | Global change-record offset of the first record. | +| `changeCount` | non-negative safe integer | Exact records across all pages. | +| `changes` | array | At most 500 ordered change records. | +| `nextCursor` | string or null | Integrity-protected continuation. | + +An upsert record has exactly `sequence`, `operation: "upsert"`, and `entry`. +The entry uses the concrete feed entry schema. A remove record has exactly +`sequence`, `operation: "remove"`, `entryId`, and the concrete schema's entry +discriminator: `entryKind` for publisher feeds or `entryType` for install +catalogs. Sequence values are +non-negative safe integers in `(fromSequence, toSequence]`; entry ids are 1 to +256 UTF-8 bytes. A change page is at most 1 MiB. + +Every feed revision in `(fromSequence, toSequence]` MUST have at least one +change record, including a metadata record for a metadata-only revision. The +first page MUST have `startIndex: 0`. Every continuation page's `startIndex` +MUST equal the prior page's `startIndex + changes.length`; `changeCount` MUST be +identical on every page; and a terminal page MUST end exactly at `changeCount`. +These offsets, page indexes, cursor chaining, and per-revision records make +gaps and overlaps detectable. + +Clients MUST verify and collect every page through `nextCursor: null`, reject a +missing revision, offset gap, overlap, identity mismatch, or range change, then +apply the full change set atomically. They MUST NOT advance their accepted +sequence after only part of a paginated change set. + +Publishers MAY retain a bounded change history. If `fromSequence` predates that +history, the endpoint MUST return an explicit reset-required response and the +current snapshot/root location or revision. It MUST NOT return a partial delta +as if it were complete. Clients then perform a full atomic or sharded refresh. + +The reset response is signed with the same concrete change payload type and is +a strict alternative to a change page. It contains exactly `schemaVersion: 1`, +`feedId`, `fromSequence`, `currentSequence`, `generatedAt`, `expiresAt`, +`resetRequired: true`, and `snapshotUrl`. The sequence values are non-negative +safe integers, `currentSequence` is greater than `fromSequence`, and +`snapshotUrl` is an HTTPS URL for the feed's atomic snapshot or signed shard +root. It MUST satisfy the selected feed profile's origin policy before any +request is made. Clients MUST NOT forward credentials across origins and MUST +verify this response before changing accepted state. + +## Watches And Notifications + +Watches are a first-class client capability built on signed feed changes. They +are not a substitute for the signed change or snapshot representations and do +not add install authority. + +A client MAY persist local watches for a feed, publisher, or stable item +identity. Item watches are the baseline alerting scope. A publisher or +whole-feed watch SHOULD populate a pull-based timeline or bounded digest by +default rather than emit one alert per changed item. + +For each watched feed representation, the client SHOULD persist its last +accepted sequence or snapshot digest and enough local history to deduplicate, +read, dismiss, or coalesce observed changes. It MUST evaluate a watch only after +verifying and atomically accepting the corresponding signed change range or +snapshot. The first accepted baseline MUST NOT report every existing item as a +new change. A client that was offline catches up on its next verified refresh. + +Clients MUST NOT issue one network refresh per watched item. One bounded source +refresh SHOULD fan out locally across all watches bound to that accepted feed +state. Automatic refresh MUST bound concurrency, use backoff and jitter after +failures, and avoid overlapping runs for the same source. An unavailable watched +source MUST NOT block runtime readiness or discard the last accepted state; +clients report the failure and retry according to the source lifecycle policy. + +A feed service MAY expose authenticated watch synchronization or a durable +notification inbox for cross-device and offline delivery. If it does, the inbox +SHOULD provide: + +- stable event ids and idempotent acknowledgement; +- the feed id, representation class, item identity, and observed feed sequence; +- a bounded reason such as `updated`, `removed`, `blocked`, or + `security-state-changed`; +- created, read, and dismissal state suitable for multiple clients; +- a reference sufficient to retrieve the signed change range or current signed + snapshot that produced the event. + +Hosted inbox and subscription state are account data, not signed feed content. +A push, email, webhook, or channel delivery is only a wake-up hint. Before +presenting an actionable update from a hosted inbox, a client MUST retrieve and +verify the referenced signed feed state, confirm the expected feed and item +identity, and apply local policy. Rejected, expired, rolled-back, or incomplete +feed state MUST NOT produce an actionable item update. + +Services MUST deduplicate retries and SHOULD coalesce repeated updates to the +same item. They MUST bound fanout, retention, page size, and delivery retries. +Removal, blocked-state, and security-state events SHOULD remain distinguishable +from ordinary version or metadata updates. A baseline sync MUST NOT report every +existing item as newly published, and a reset-required refresh SHOULD compare +the newly accepted complete snapshot with the prior accepted snapshot when one +is available. + +Local watch state is the v1 baseline and does not require an account service. +Explicit server-side watch synchronization is optional. A client MAY derive +watches from locally installed content, but sending installed-item identities +or local watch history to a service requires an explicit account setting +because it discloses local inventory and interests. Hosted subscription, mute, +delivery-channel, and read state MUST remain private even when publisher follows +are public. + +A notification MUST NOT install, update, enable, remove, approve, or otherwise +activate content. Any such action uses the normal install or update path and +re-evaluates artifact integrity, trust, and policy. + +## Access Control + +For private publisher, named, organization, or composed feeds, the publisher +MUST authorize the request before selecting or signing entries. Cursors MUST be +bound to the authorization context without exposing user identifiers or ACLs. +A signed response authenticates what the feed operator served; it does not +grant access to another principal or bypass downstream policy. + +## Conformance + +A conforming publisher: + +- never silently truncates snapshots, query results, or change history; +- pins every paginated operation to one immutable revision or range; +- emits removal tombstones and explicit reset-required responses; +- publishes immutable shards before the signed root; +- signs each atomic snapshot, root, query page, and change page with the + concrete feed contract's distinct payload type. + +A conforming client: + +- enforces per-response and aggregate bounds; +- binds query and change continuations to the initial request and revision; +- verifies all shard digests and all signed projection pages; +- reverifies current root trust, expiry, and retained shard bytes before using + a cached sharded snapshot; +- applies complete snapshots and complete change ranges atomically; +- falls back to a full snapshot after an explicit retention gap. diff --git a/rfcs/0009/hosted-feed-v1-spec.md b/rfcs/0009/hosted-feed-v1-spec.md index 98243fd4..1ac21cae 100644 --- a/rfcs/0009/hosted-feed-v1-spec.md +++ b/rfcs/0009/hosted-feed-v1-spec.md @@ -1,198 +1,125 @@ # Hosted Feed v1 Core Specification -This document is the implementer-facing core specification for RFC 0009, +This document defines the interoperable v1 catalog feed contract for RFC 0009, Hosted Feeds for Plugins and Skills. The RFC explains the motivation and -rollout plan. This file defines the v1 feed document, entry, source-reference, -candidate, refresh, fallback, and validation contract that feed publishers and -OpenClaw clients can build against. +rollout plan; this sidecar defines the wire shape publishers and clients use. Status: draft, tied to RFC 0009. -## Scope - -This core specification covers the feed contract from the accepted RFC: - -- feed document shape; -- plugin and skill entry shape; -- source references through locally configured source profiles; -- npm, ClawHub, and Git install candidates; -- entry states; -- refresh, cache, and bundled fallback behavior; -- client validation and rejection behavior; -- basic publisher and client conformance checklists. - -This core specification intentionally does not define: - -- signed feed envelopes; -- publisher public-key configuration; -- ClawHub account or publisher feeds; -- account following, notifications, or profile feeds; -- enterprise feed composition semantics beyond final entry state precedence; -- tenant-admin policy, Microsoft MOS3, or other enterprise registry behavior; -- runtime tool policy or connector/agent activation. - -Those belong in separate addendum specs once the corresponding RFC addenda are -accepted. - -## What This Enables Today - -The core v1 spec is enough for a company, registry, or community publisher to -start hosting an OpenClaw-compatible plugin and skill catalog. - -A publisher can: - -- generate a v1 JSON feed; -- host it on an internal or public HTTPS endpoint; -- point entries at package artifacts through `sourceRef` values; -- use local OpenClaw source profiles to resolve those `sourceRef` values to - npm, ClawHub-compatible, Git, or private artifact sources; -- mark entries as `available`, `recommended`, `disabled`, `blocked`, or - `deprecated`; -- publish a final effective feed generated by its own policy, review, or - registry system, as long as the emitted feed follows this core shape. - -An OpenClaw client can then cache, inspect, search, and install from that feed -using the configured source profiles and existing plugin or skill install paths. - -The core spec does not require the publisher to expose how its internal policy -system produced the feed. For example, a company can combine internal review, -private package metadata, and local approval rules in its own registry, then -emit the final OpenClaw-compatible feed as the client-facing catalog. Later -addendum specs can standardize signed trust, account feeds, following, -enterprise composition, audit, and key management without changing the basic -feed contract. - -## Design Lineage - -Hosted feeds use the same broad pattern as APT, Homebrew formula/cask metadata, -npm package metadata, and other package ecosystems: - -- catalog metadata is separate from artifact storage; -- clients cache catalog state and can fall back when the network is unavailable; -- entries carry stable identity, selected version, source, integrity, and state; -- package or artifact installation remains a separate client/runtime action. - -OpenClaw adds one important constraint in the core v1 contract: feed entries -select locally configured source profiles by `sourceRef`. Feeds do not smuggle -registry domains, credentials, SSH material, or client trust roots into clients. +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are normative. -## Terminology - -- **Feed**: a deterministic JSON catalog document. -- **Entry**: one plugin or skill record inside a feed. -- **Source profile**: local client configuration that maps a `sourceRef` to npm, - ClawHub, Git, or another approved artifact source. -- **Bundled fallback**: a feed shipped with OpenClaw for offline and recovery - behavior. -- **Snapshot**: a locally accepted and cached feed payload plus metadata such as - sequence, expiry, checksum, and HTTP validators. +## Scope -## Media Type +This specification defines: -Publishers should use this content type where possible: +- the catalog feed document and entry shapes; +- plugin and skill install candidates; +- locally configured source profiles; +- entry state, refresh, validation, and fallback behavior; +- publisher and client conformance requirements. -```text -application/vnd.openclaw.catalog-feed+json;v=1 -``` +It does not define signed envelopes, publisher following, publisher-feed discovery, +organization approval, artifact signing, malware scanning, or runtime policy. +Those are separate contracts. -Clients must not rely only on the HTTP content type. They must validate the JSON -shape and schema version before accepting a feed. +Large-feed sharding, signed server-side queries, and incremental change +projections are defined in `hosted-feed-distribution-v1-spec.md`. A publisher +MUST NOT truncate this atomic document to remain within a local limit; it must +fail publication or expose the scalable representation. -## Compatibility And Evolution +## Wire Contract -Feed v1 uses a conservative compatibility model: +The normative v1 publisher schema is the generated `CatalogFeedSchema` in the +ClawHub `clawhub-schema` package. The matching OpenClaw consumer is the hosted +official external plugin catalog parser. If prose or examples in this document +disagree with those shipped artifacts, that disagreement is a specification bug +and must be corrected before adding a second interpretation. -- clients must reject unsupported major `schemaVersion` values; -- publishers may add optional fields without changing `schemaVersion`; -- clients must ignore unknown optional fields unless local policy says to fail - closed; -- new required fields require a new schema version; -- enum expansions must be treated as unsupported by old clients until the - client has an explicit fallback rule; -- publishers should keep removed or renamed fields readable for at least one - transition release when practical. +Publishers SHOULD serve unsigned feeds as `application/json`. Signed transport +uses the payload type defined by the signed-feed trust addendum. Clients MUST +validate the body and MUST NOT infer compatibility from the HTTP content type. -The compatibility goal is that a publisher can add metadata without breaking old -clients, while old clients do not accidentally treat a new required governance -or install meaning as safe. +The v1 publisher schema is strict. Conforming publishers MUST NOT add fields +that are not defined here. A client MAY retain unknown fields for forward +compatibility, but MUST NOT assign install, trust, approval, or policy meaning +to them. ## Feed Document -A v1 feed document is a JSON object with these top-level fields: - | Field | Type | Required | Semantics | | --- | --- | --- | --- | -| `schemaVersion` | integer | Yes | Must be `1` for this specification. | -| `id` | string | Yes | Stable feed identity, for example `clawhub-public`. | -| `generatedAt` | RFC 3339 string | Yes | Publisher generation timestamp. | -| `sequence` | integer | Yes | Monotonic publisher sequence for this feed id. | -| `expiresAt` | RFC 3339 string | Recommended | Expiry after which clients should report stale state. | -| `entries` | array | Yes | Feed entries. | -| `metadata` | object | Optional | Human-readable or publisher-specific metadata. | - -Feed JSON should be deterministic at generation time. Field order is not a -verification primitive in this core spec. - -Publishers should keep feed payloads bounded. OpenClaw clients should enforce -local limits for payload byte size, entry count, metadata object size, string -length, and nested object depth before accepting a feed. Exact limits are -implementation policy, but an unbounded feed must not block the gateway event -loop, exhaust memory, or make search and install paths perform request-time -network polling. - -Minimal feed: +| `schemaVersion` | integer | Yes | MUST be `1`. | +| `id` | string | Yes | Stable feed identity. | +| `generatedAt` | RFC 3339 string | Yes | Publication generation time. | +| `sequence` | non-negative safe integer | Yes | Monotonically increasing for this `id`. | +| `expiresAt` | RFC 3339 string | Yes | MUST be later than `generatedAt`. | +| `description` | string | No | Bounded human-readable feed description. | +| `entries` | array | Yes | Catalog entries. | + +Example: ```json { "schemaVersion": 1, - "id": "clawhub-public", + "id": "clawhub-official", "generatedAt": "2026-07-15T00:00:00.000Z", "sequence": 42, "expiresAt": "2026-07-22T00:00:00.000Z", + "description": "Official OpenClaw plugins published on ClawHub.", "entries": [] } ``` +Publishers MUST increment `sequence` by exactly one when publishing a new +accepted revision. +Deleting or demoting an entry MUST NOT make the sequence decrease. Republished +bytes at the same sequence MUST be identical. + +The atomic document is intentionally not cursor-paginated. Pagination without a +pinned revision can combine different catalog states and cannot be accepted as +one signed snapshot. Large complete feeds use a signed root plus immutable +shards; interactive search and changed-since retrieval use signed projections. + ## Entry Object -Every entry must have stable identity, type, display metadata, selected version -or revision when available, state, publisher, and install candidates when the -entry is installable. +Every v1 entry has exactly these fields: | Field | Type | Required | Semantics | | --- | --- | --- | --- | -| `type` | string | Yes | `plugin` or `skill` for v1. Clients must ignore or reject unsupported types according to local policy. | -| `id` | string | Yes | Stable entry identity. Must not be a mutable display slug. | -| `title` | string | Yes | User-facing display title. | -| `version` | string | Type-specific | Selected package version when the artifact model has one. Skills may use `revision` instead. | -| `revision` | string | Optional | Immutable source revision for Git-backed or non-semver skills. | -| `state` | string | Yes | One of the v1 entry states below. | -| `publisher` | object | Yes | Publisher id and trust label. | -| `install` | object | Required for installable entries | Candidate artifact selectors. | -| `metadata` | object | Optional | Tags, summaries, icons, links, or publisher-specific metadata. | +| `type` | string | Yes | `plugin` or `skill`. | +| `id` | string | Yes | Stable entry identity. | +| `title` | string | Yes | User-facing title. | +| `version` | string | Yes | Exact package version or immutable source revision. | +| `state` | string | Yes | One of the states below. | +| `publisher` | object | Yes | Publisher identity and catalog trust label. | +| `install` | object | Yes | One or more artifact candidates. | + +The v1 entry schema does not define arbitrary entry `metadata`, `revision`, or +`description` fields. A future schema version may add bounded presentation or +provenance fields. ### Entry States | State | Meaning | | --- | --- | -| `available` | Entry can be shown and installed when source and policy checks pass. | -| `recommended` | Entry can be highlighted; it is still subject to source and policy checks. | -| `disabled` | Entry is known but intentionally unavailable from this feed. | -| `blocked` | Entry is explicitly denied by this feed. | -| `deprecated` | Entry may remain visible for migration but should not be selected for new installs. | +| `available` | May be offered when source and policy checks pass. | +| `recommended` | May be highlighted; no additional install authority. | +| `disabled` | Known but intentionally unavailable from this feed. | +| `blocked` | Explicitly denied by this feed. | +| `deprecated` | Retained for migration but not preferred for new installs. | -State precedence for final effective entry state should be: +When a publisher composes multiple inputs, the final state precedence SHOULD be: ```text blocked > disabled > deprecated > recommended > available ``` -The core spec defines only the final state emitted in the feed. How a publisher -or enterprise registry computes that state is outside this spec. +The feed communicates final state only. It does not standardize the policy or +review process that produced that state. ## Publisher Object -Publisher trust is intentionally narrow in v1. +The publisher object has exactly two fields: ```json { @@ -201,77 +128,68 @@ Publisher trust is intentionally narrow in v1. } ``` -`trust` may be: +`trust` MUST be `official` or `community`. It describes the publisher's catalog +classification. It does not assert package safety, organization approval, +artifact integrity, runtime permission, or account-follow state. -- `official` -- `unofficial` +## Install Candidates -Feeds must not invent broad "safe" labels that imply package security, -enterprise approval, or runtime permission. Those are separate policy layers. +`install.candidates` MUST contain at least one candidate. Every candidate has: -## Install Candidates +| Field | Type | Required | Semantics | +| --- | --- | --- | --- | +| `sourceRef` | string | Yes | Name of a locally configured source profile. | +| `package` | string | Yes | Package name or stable package coordinate. | +| `version` | string | Yes | Exact version or immutable revision. | +| `integrity` | string | Yes | Artifact or selected-content integrity value. | +| `github` | object | No | Immutable GitHub source details. | -An installable entry has an `install.candidates` array. Each candidate names a -local source profile through `sourceRef`. +Candidates MUST NOT contain credentials, source base URLs, bearer tokens, SSH +material, or trust roots. + +### Package Candidate ```json { - "install": { - "candidates": [ - { - "sourceRef": "public-npm", - "package": "@openclaw/acpx", - "version": "1.2.3", - "integrity": "sha512-..." - } - ] - } + "sourceRef": "public-clawhub", + "package": "@openclaw/acpx", + "version": "1.2.3", + "integrity": "sha256:0123456789abcdef" } ``` -Candidate fields vary by source type. - -### npm Candidate - -| Field | Required | Semantics | -| --- | --- | --- | -| `sourceRef` | Yes | Local source profile with `type: "npm"`. | -| `package` | Yes | npm package name. | -| `version` | Yes | Exact selected package version. | -| `integrity` | Recommended | npm integrity string or equivalent artifact integrity. | +### GitHub Candidate -### ClawHub Candidate +A GitHub candidate retains the common candidate fields and adds a strict nested +`github` object: -| Field | Required | Semantics | -| --- | --- | --- | -| `sourceRef` | Yes | Local source profile with `type: "clawhub"`. | -| `package` | Yes | ClawHub package id or package coordinate. | -| `version` | Yes | Exact selected version. | -| `integrity` | Recommended | Artifact integrity when available. | - -### Git Candidate - -| Field | Required | Semantics | -| --- | --- | --- | -| `sourceRef` | Yes | Local source profile with `type: "git"`. | -| `repo` | Yes | Repository path relative to the source profile. | -| `commit` | Yes | Full immutable commit hash. Branches and tags are not allowed. | -| `path` | Optional | Path inside the repository, such as `skills/review/SKILL.md`. | -| `sha256` | Recommended | Content hash for the selected path or package payload. | +```json +{ + "sourceRef": "public-github", + "package": "@openclaw/review-pr", + "version": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", + "integrity": "sha256:0123456789abcdef", + "github": { + "repo": "openclaw/skills", + "path": "skills/review-pr", + "commit": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", + "contentHash": "0123456789abcdef" + } +} +``` -A feed candidate must not include registry credentials, bearer tokens, SSH -private keys, npm auth tokens, source base URLs, or trust roots. Unknown -`sourceRef` values make the candidate invalid. If all candidates are invalid, -the entry is not installable. +`github.repo`, `github.path`, `github.commit`, and `github.contentHash` are all +required when `github` is present. `commit` MUST be immutable; branch names, +tags, and shortened commit hashes are invalid. -## Examples +## Complete Entry Examples -### Plugin From npm +Plugin: ```json { "type": "plugin", - "id": "acpx", + "id": "@openclaw/acpx", "title": "ACP-X", "version": "1.2.3", "state": "recommended", @@ -282,172 +200,138 @@ the entry is not installable. "install": { "candidates": [ { - "sourceRef": "public-npm", + "sourceRef": "public-clawhub", "package": "@openclaw/acpx", "version": "1.2.3", - "integrity": "sha512-..." + "integrity": "sha256:0123456789abcdef" } ] } } ``` -### Skill From Git +Community skill from GitHub: ```json { "type": "skill", - "id": "review-pr", + "id": "@example/review-pr", "title": "Review Pull Request", - "revision": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", + "version": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", "state": "available", "publisher": { - "id": "openclaw", - "trust": "official" + "id": "example", + "trust": "community" }, "install": { "candidates": [ { "sourceRef": "public-github", - "repo": "openclaw/skills", - "commit": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", - "path": "skills/review-pr/SKILL.md", - "sha256": "..." + "package": "@example/review-pr", + "version": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", + "integrity": "sha256:0123456789abcdef", + "github": { + "repo": "example/skills", + "path": "skills/review-pr", + "commit": "8d6b6f7f0d9d4d1e2e6d3d1234567890abcdef12", + "contentHash": "0123456789abcdef" + } } ] } } ``` -### Blocked Entry - -```json -{ - "type": "plugin", - "id": "risky-plugin", - "title": "Risky Plugin", - "version": "1.0.0", - "state": "blocked", - "publisher": { - "id": "example", - "trust": "unofficial" - }, - "metadata": { - "reason": "Blocked by feed policy" - } -} -``` - ## Source Profile Configuration -Source profiles are local configuration. A feed entry names `sourceRef`; the -client maps that name to the actual source endpoint and credentials. +Feed and source profiles are local OpenClaw configuration under +`marketplaces`, not data supplied by a feed: ```json { - "catalog": { + "marketplaces": { "feeds": { - "clawhub-public": { - "url": "https://clawhub.ai/v1/feeds/plugins", - "refresh": { - "onStartup": "if-stale", - "interval": "6h", - "jitter": "10m", - "timeout": "10s", - "maxStale": "7d" - } + "partner-catalog": { + "url": "https://packages.example.com/openclaw/feed.json" } }, "sources": { - "public-npm": { - "type": "npm", - "registry": "https://registry.npmjs.org/" + "partner-packages": { + "type": "npm" }, - "public-github": { - "type": "git", - "baseUrl": "https://github.com/" + "partner-github": { + "type": "git" } } } } ``` -Configuration errors should be precise and actionable. For example, if a -candidate references an unknown `sourceRef`, diagnostics should name the feed -id, entry id, and source ref without printing credentials, raw bearer tokens, -local private paths, or unbounded URLs. +Source profile `type` MUST be `npm`, `clawhub`, or `git`. Endpoint and +credential selection are resolver concerns and are not fields in the v1 feed +wire contract. Unknown `sourceRef` values make that candidate unusable. If an +entry has no usable candidate, the client MUST NOT offer it for installation. ## Refresh And Fallback -Clients should: - -1. Load the bundled fallback feed. -2. Start feed refresh after the gateway/runtime is ready. -3. Fetch remote feeds on the configured lifecycle schedule, not on every search - or install request. -4. Use conditional HTTP requests with `ETag` or `Last-Modified` when available. -5. Validate transport response limits, schema version, entries, source refs, and - candidate fields before accepting a new snapshot. -6. Compute a local `sha256` over the accepted payload bytes. -7. Store accepted snapshots atomically with sequence, expiry, payload checksum, - and HTTP validators. -8. Use the latest accepted snapshot during transient failures. -9. Report stale state when a snapshot exceeds `maxStale`. -10. Fall back to the bundled feed when no usable snapshot exists. - -Refresh runs on a lifecycle schedule. It must not happen synchronously inside -ordinary search, onboarding, or install rendering. Search and install should -consume the current accepted snapshot plus local source profile state. This -keeps feed governance from adding unpredictable latency to the gateway runtime -or user-facing marketplace views. - -## Client Rejection Rules - -Clients must reject or ignore unsafe feed content before it affects install or -search authority. At minimum, clients must reject: - -- unsupported `schemaVersion`; -- missing or malformed top-level feed identity; -- duplicate entry ids inside a feed; -- entries with unsupported `type` when local policy says fail closed; -- install candidates with unknown `sourceRef`; -- Git candidates that use branches, tags, or shortened commit hashes; -- feed documents that contain credentials, source base URLs, or trust roots in - entry candidates; -- malformed payloads or payloads that exceed local size and shape limits. - -Rejections should be diagnosable. The diagnostics record should identify the -feed profile, feed id when available, sequence when available, failure category, -and whether the client used hosted, cached, local, or bundled content. It must -not include secrets, raw credential-bearing URLs, prompt contents, tool -arguments, local private paths, or full package payload bytes. - -## Publisher Checklist - -A feed publisher is compatible with v1 core when it: - -- emits `schemaVersion: 1`; -- uses stable feed and entry ids; -- uses only supported entry states; -- keeps artifact credentials, source endpoints, and trust roots out of the feed; -- emits exact package versions, immutable Git commits, and integrity metadata - where available; -- publishes examples and negative test payloads for client compatibility. - -## Client Checklist - -An OpenClaw client is compatible with v1 core when it: - -- validates feed shape before accepting entries; -- keeps source profile credentials local; -- rejects unknown source refs and unsafe Git selectors; -- caches accepted snapshots atomically; -- preserves bundled fallback behavior; -- treats search, install, package integrity, and runtime policy as separate - decisions; -- emits bounded diagnostics for fetch, validation, fallback, and rejection - outcomes; -- preserves feed id, sequence or payload checksum, entry id, and source ref in +Clients SHOULD: + +1. Load a bundled fallback when one exists. +2. Refresh after runtime startup or through an explicit refresh command. +3. Use conditional HTTP requests with `ETag` or `Last-Modified`. +4. Bound URL, response size, read time, entry count, and string sizes. +5. Validate the complete feed before accepting a new snapshot. +6. Store accepted payload bytes and metadata atomically. +7. Reject signed rollback attempts with a lower sequence. +8. Use the last accepted snapshot during transient failures. +9. Report expired or stale content without silently granting new authority. + +If a complete atomic feed exceeds configured limits, clients SHOULD discover a +sharded root representation when the selected profile supports it. Clients MAY +use signed query projections for interactive discovery and signed change +projections for incremental refresh, but MUST NOT mistake either for possession +of a complete snapshot. + +Search and install rendering MUST consume accepted local state rather than +performing an unbounded request-time refresh. + +## Rejection And Diagnostics + +Clients MUST reject a feed before it affects install or search authority when: + +- `schemaVersion` is unsupported; +- feed identity, timestamps, sequence, entries, or candidates are malformed; +- duplicate entry identities create ambiguity; +- no candidate resolves to a configured `sourceRef`; +- an immutable selector or required integrity value is missing; +- configured transport, payload, or shape limits are exceeded; +- signed verification required by the profile fails. + +Diagnostics SHOULD identify the local profile, payload feed id when available, +sequence, failure category, and fallback source. They MUST NOT expose secrets, +credential-bearing URLs, local private paths, or package payload bytes. + +## Publisher Conformance + +A v1 publisher: + +- emits the strict schema above with stable feed and entry identities; +- increments sequence monotonically; +- emits exact versions and required integrity values; +- keeps source endpoints, credentials, and trust roots out of entries; +- publishes positive and negative compatibility fixtures; +- serves stored publication bytes rather than rebuilding a different document + for every read. + +## Client Conformance + +A v1 client: + +- validates shape before accepting entries; +- keeps source and trust configuration local; +- separates discovery, install resolution, artifact integrity, and runtime + policy; +- stores accepted snapshots atomically and preserves last-known-good fallback; +- preserves feed id, sequence, entry id, source ref, version, and integrity in install provenance; -- reports blocked, disabled, deprecated, stale, and configuration-error states - with actionable messages. +- provides bounded refresh, verification, rejection, and fallback diagnostics. diff --git a/rfcs/0009/signed-feed-trust-v1-spec.md b/rfcs/0009/signed-feed-trust-v1-spec.md index 8a214cf9..36ac9c6b 100644 --- a/rfcs/0009/signed-feed-trust-v1-spec.md +++ b/rfcs/0009/signed-feed-trust-v1-spec.md @@ -1,67 +1,60 @@ # Signed Feed Trust v1 Addendum Specification -This document is the implementer-facing trust addendum for RFC 0009 hosted -feeds. It builds on `hosted-feed-v1-spec.md`, which defines the core feed -document, entry, source-reference, refresh, fallback, and validation contract. +This document defines signed transport and trust-anchor behavior for Hosted Feed +v1. It builds on `hosted-feed-v1-spec.md`. Status: draft addendum, tied to RFC 0009. +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are normative. + ## Scope This addendum defines: -- signed feed envelopes; -- payload media type binding; -- Ed25519 signature verification; -- configured and bundled publisher public-key trust; -- fail-closed signed feed behavior; -- key id and threshold rules; -- ClawHub platform feed-signing bootstrap; -- optional signed key-rotation documents; -- diagnostics requirements for feed verification failures. - -This addendum does not define: - -- package artifact signing or package malware scanning; -- runtime tool policy; -- account following or notifications; -- organization-admin approval workflows; -- a remote public-key bootstrap endpoint; -- reuse of OpenClaw release-signing identities as feed-signing identities. +- the signed feed envelope; +- exact DSSE pre-authentication encoding; +- Ed25519 verification and signature thresholds; +- local and bundled publisher public-key trust; +- expected feed-identity binding; +- fail-closed refresh, snapshot, and key-rotation behavior; +- bounded verification diagnostics. + +It does not define package signing, malware scanning, runtime policy, account +following, organization approval, a remote trust bootstrap endpoint, or reuse +of release-signing identities as feed-signing identities. ## Trust Model -A signed feed proves that the exact feed payload bytes were signed by a -publisher key already trusted by the client or deployment. It does not prove -that package code is safe, reviewed, organization-approved, or installable. +A valid signature proves that trusted key material signed the exact decoded feed +payload bytes for the declared payload type. It does not prove package safety, +organization approval, artifact integrity, or runtime permission. + +An initial trust anchor MUST arrive through one of these channels: -Trust anchors come from one of these channels: +- a public key bundled with OpenClaw for a built-in platform feed; +- operator-managed local configuration; +- another authenticated software or configuration distribution channel. -- a public key bundled with OpenClaw for the default ClawHub platform - feed-signing identity; -- operator-managed local configuration for private, third-party, or development - feeds; -- a signed key-rotation document that chains from an already trusted key. +A feed endpoint MUST NOT bootstrap its own trust by advertising a key from an +ordinary endpoint such as `/v1/feeds/public-key`. Compromise of that endpoint +would otherwise compromise both content and trust. -A feed host must not bootstrap its own initial trust by serving a key from an -ordinary endpoint such as `/public-key`. Such endpoints may be informational for -operators, but clients must not trust them unless the key is already bundled, -locally configured, or delivered through a signed rotation chain. +Publisher private keys MUST remain in publisher-owned secret storage. Feed keys +MUST be distinct from OpenClaw release, package, TLS, account, and platform +signing identities even when the same secret-management system operates them. ## Signed Envelope -Signed feeds are served as an envelope containing the exact UTF-8 JSON feed -payload bytes encoded as base64url and one or more signatures. +The v1 envelope uses the standard DSSE JSON shape: ```json { - "type": "openclaw.signed-envelope.v1", - "payloadType": "application/vnd.openclaw.catalog-feed+json;v=1", - "payload": "base64url(exact UTF-8 feed JSON bytes)", + "payloadType": "openclaw.official-external-plugin-catalog-feed.v1", + "payload": "eyJzY2hlbWFWZXJzaW9uIjoxLC4uLn0", "signatures": [ { "keyid": "clawhub-feed-2026-q3", - "sig": "base64:..." + "sig": "base64url-signature" } ] } @@ -71,76 +64,76 @@ Envelope fields: | Field | Type | Required | Semantics | | --- | --- | --- | --- | -| `type` | string | Yes | Must be `openclaw.signed-envelope.v1`. | -| `payloadType` | string | Yes | Must identify the signed payload media type. | -| `payload` | string | Yes | Base64url encoded exact payload bytes. | -| `signatures` | array | Yes | One or more signatures over the envelope pre-authentication encoding. | +| `payloadType` | string | Yes | MUST equal the exact type selected by the concrete feed contract and representation. The baseline catalog type is `openclaw.official-external-plugin-catalog-feed.v1`. | +| `payload` | string | Yes | Base64 or unpadded base64url encoding of the exact UTF-8 feed bytes. | +| `signatures` | array | Yes | Between 1 and 16 signature records. | Signature fields: | Field | Type | Required | Semantics | | --- | --- | --- | --- | -| `keyid` | string | Yes | Publisher key id selected from trusted keys. | -| `sig` | string | Yes | Base64 encoded signature bytes. | - -Clients must verify the envelope before decoding and accepting the payload as a -feed. Signing exact payload bytes avoids a second JSON canonicalization contract -for feed documents. - -## Signature Algorithm - -The initial signature algorithm is Ed25519. - -OpenClaw's verifier uses DSSE-style pre-authentication encoding for signed -payloads. Publishers should use the OpenClaw verifier, test vectors, or a -compatibility harness before treating a signature as compatible. - -## Verification Rules - -Clients must: +| `keyid` | string | Yes for Hosted Feed v1 | Non-empty configured publisher key id. DSSE permits omission, but this profile requires it to resolve an out-of-band trusted key. | +| `sig` | string | Yes | Base64 or unpadded base64url Ed25519 signature over the DSSE PAE bytes. | + +Ed25519 is selected by the trusted feed profile; the envelope does not carry an +algorithm field. Duplicate `keyid` values make the envelope invalid. Producers +MAY add fields and consumers MUST ignore unrecognized envelope and signature +fields, as required by DSSE. Unknown fields MUST NOT be assigned trust meaning. + +Signed HTTP responses MUST use `Content-Type: application/vnd.dsse+json`. +After parsing optional media-type parameters, clients MUST reject another media +type on an operation that requires a signed envelope. A producer SHOULD +calculate `ETag` over the exact envelope bytes. +`Last-Modified` MUST NOT be the sole validator for a signed representation: +signing-key rotation can change envelope bytes without changing the stored +payload publication time. After a `304 Not Modified`, a client MUST still +reverify its retained envelope under the current trust policy before use. + +## DSSE Pre-Authentication Encoding + +Signatures MUST be calculated over the DSSE v1 pre-authentication encoding of +the decoded payload bytes, not over the base64 text in the envelope. + +For UTF-8 `payloadType` bytes `T` and decoded payload bytes `P`, construct: + +```text +PAE("DSSEv1", T, P) = + "DSSEv1" || SP || decimal(byte_length(T)) || SP || T || + SP || decimal(byte_length(P)) || SP || P +``` -- reject unsupported envelope `type`; -- reject unsupported `payloadType`; -- reject malformed base64/base64url fields; -- reject empty `signatures`; -- bound the maximum signature count; -- reject duplicate `keyid` values in one envelope; -- resolve `keyid` only against locally trusted keys for the feed profile; -- require the configured threshold to be met by distinct trusted keys; -- reject signed feeds when only part of required key configuration is present; -- decode and validate the feed payload only after signature verification; -- compute and persist a local payload checksum after verification. +For a selected payload type this is equivalent to: -`verification.mode: "signed"` fails closed. A failure to fetch keys, malformed -local key config, missing key id, invalid signature, unsupported envelope, or -unsupported payload type means the hosted feed must not become install or search -authority. +```text +"DSSEv1 " + utf8ByteLength(payloadType) + " " + payloadType + +" " + payloadByteLength + " " + payloadBytes +``` -This addendum is a verification layer, not a packaging requirement. Publishers -can still host ordinary core feeds, and clients can still use local or unsigned -development feeds through explicit local configuration. Signed verification -becomes a requirement only for profiles configured with `mode: "signed"` or for -default ClawHub-hosted feeds that OpenClaw treats as signed by platform policy. +Lengths are unsigned decimal ASCII without leading signs or padding. `SP` is +one ASCII space (`0x20`). The final component is the exact decoded payload byte +sequence and may contain arbitrary bytes. Publishers SHOULD validate against +OpenClaw's signed-envelope test vectors before production use. -## Source Profile Verification Configuration +## Local Configuration -A feed profile can require signed verification. +Custom signed feeds are configured under `marketplaces.feeds`: ```json { - "catalog": { + "marketplaces": { "feeds": { - "acme": { - "url": "https://packages.acme.example/openclaw/feed", + "partner-catalog": { + "url": "https://packages.example.com/openclaw/feed.json", + "feedId": "partner-official", "verification": { "mode": "signed", - "rootKeys": [ + "keys": [ { - "id": "acme-feed-2026-q3", - "publicKey": "base64:..." + "keyId": "partner-feed-2026-q3", + "publicKey": "base64url-raw-ed25519-key-or-pem" } ], - "rootThreshold": 1 + "threshold": 1 } } } @@ -148,117 +141,150 @@ A feed profile can require signed verification. } ``` -`rootKeys` are public keys. They are not secrets. Private signing keys must stay -outside OpenClaw client configuration. - -The default ClawHub platform feed should not require ordinary users to paste -`rootKeys`. OpenClaw can ship the ClawHub public feed-signing key as part of the -OpenClaw release. Explicit `rootKeys` remain useful for private ClawHub -deployments, third-party publishers, development, emergency override, and -operator-managed deployments. - -## ClawHub Platform Signing - -ClawHub-hosted feeds can use a ClawHub platform feed-signing key. The same -platform signing path can sign: - -- `clawhub-public`; -- ClawHub named feeds; -- ClawHub account feeds; -- ClawHub organization feeds; -- ClawHub-served composed feeds. - -OpenClaw verifies these feeds with the bundled ClawHub public key when the feed -identity in the signed payload matches a ClawHub-hosted feed profile. ClawHub -enforces ACLs before serving private or organization-scoped feeds; feed -verification proves payload authenticity, not viewer authorization. - -ClawHub private signing material should live in ClawHub secret storage or a -signing service. It must not be checked into feed documents, OpenClaw config, or -public source. - -The feed-signing key is a feed integrity key. It should not reuse OpenClaw -release-signing identities, Apple Developer ID certificates, notarization -credentials, npm tokens, GitHub tokens, or package-publishing credentials. -Keeping these identities separate limits blast radius when a feed-signing key or -package-publishing path needs emergency rotation. - -## Key Rotation - -If a publisher needs remote key rotation, it should publish a signed rotation -document verified by an already trusted key. A rotation document is separate -from an ordinary feed. - -```json -{ - "type": "openclaw.feed-key-rotation.v1", - "feedId": "clawhub-public", - "sequence": 3, - "expiresAt": "2026-09-01T00:00:00.000Z", - "threshold": 1, - "feedKeys": [ - { - "id": "clawhub-feed-2026-q4", - "publicKey": "base64:..." - } - ] -} -``` - -Clients that support rotation should persist accepted rotation sequence and -expiry metadata to prevent rollback and freeze attacks. Emergency root -replacement remains a local operator or OpenClaw release action. - -## Refresh And Snapshot Trust State - -When a signed hosted feed is accepted, clients should persist bounded trust -state with the cached snapshot: - -- feed profile name; -- feed id from the verified payload; -- payload checksum; -- feed sequence; -- expiry; -- verification mode; -- signed/unsigned outcome; -- signature count; -- threshold; -- accepted key ids or bounded key fingerprints; -- verification failure category when rejected. - -Diagnostics must not include private keys, raw bearer tokens, credential-bearing -URLs, full payload bytes, or unbounded identity values. - -## Unsigned Feeds - -Unsigned remote feeds require explicit local opt-in through -`verification.mode: "unsigned"`. Unsigned remote feeds should still require -HTTPS unless a local development profile explicitly allows loopback or local -file input. - -Clients must make unsigned state visible in diagnostics so operators can tell -that a feed is accepted without signature verification. - -## Publisher Checklist - -A signed-feed publisher is compatible with this addendum when it: - -- signs exact feed payload bytes through the v1 envelope; -- uses Ed25519 keys with stable key ids; -- keeps private signing material outside feed documents and client config; -- publishes test vectors for valid signatures and expected failures; -- documents who owns key rotation and emergency revocation; -- provides a rotation story before retiring old keys; -- avoids using ordinary `/public-key` endpoints as trust bootstrap. - -## Client Checklist - -An OpenClaw client is compatible with this addendum when it: - -- verifies signed envelopes before decoding feed payloads; -- fails closed for configured signed feeds; -- rejects malformed, duplicate, or unbounded signature inputs; -- verifies thresholds against distinct trusted keys; -- supports bundled ClawHub public-key trust for default ClawHub feeds; -- keeps direct public-key config as an operator override path; -- records bounded verification diagnostics with accepted snapshots. +`feedId` is the expected signed payload identity. Signed profiles MUST bind the +decoded payload's `id` to this configured or built-in expected value. The local +profile name and payload feed id may differ; for example the built-in profile +`clawhub-public` expects payload id `clawhub-official`. + +`keys` MUST contain distinct key ids and distinct normalized Ed25519 public key +material. `threshold` defaults to 1, MUST be positive, and MUST NOT exceed the +number of configured distinct keys. Private signing keys MUST NOT appear in +OpenClaw configuration. + +## Verification + +Before accepting a signed refresh, a client MUST: + +1. Parse and validate the bounded envelope. +2. Require the exact `payloadType` selected by the concrete feed contract and + requested representation. +3. Decode `payload` and each candidate signature. +4. Construct the exact DSSE PAE bytes above. +5. Resolve signature key ids only against the selected profile's trusted keys. +6. Count only valid signatures from distinct trusted public key material. +7. Require the configured threshold. +8. Dispatch only to the schema validator registered for that exact payload + type. The baseline catalog type parses as Hosted Feed v1; distribution + addendum types parse as their strict root, query, or change schema. +9. Require the payload's `id` or `feedId`, as defined by that schema, to equal + the selected profile or endpoint's expected feed identity. +10. Apply schema, source-profile, expiry, and monotonic sequence checks. + +Failure at any step MUST reject the new payload. A client MUST NOT retry the +same bytes as unsigned content or silently replace the configured signed profile +with an unsigned feed. + +Supporting the envelope does not make every payload type acceptable. A client +MUST maintain an explicit allowlist from endpoint or operation to exact payload +type and validator. It MUST NOT select a validator from untrusted payload +contents or treat an unknown but validly signed type as a catalog. + +## Snapshots And Rollback Protection + +An accepted signed snapshot SHOULD persist: + +- exact envelope bytes; +- exact decoded payload bytes or their checksum; +- payload feed id and sequence; +- accepted signature key ids, count, and threshold; +- verification time; +- HTTP validators and fetch metadata. + +Clients MUST reverify a stored signed snapshot before using it. A newly fetched +valid feed with a lower sequence than the previously accepted feed id MUST be +rejected as rollback. A verification-policy change MUST NOT make it impossible +to compare a newly valid feed against metadata from the previously accepted +snapshot; the old snapshot remains the rollback baseline even when it no longer +satisfies the new key set. + +The old snapshot may be served as fallback only if it satisfies the currently +configured verification policy. Otherwise the client MUST fail closed with a +clear diagnostic. + +## Bundled ClawHub Trust + +The official ClawHub feed uses a dedicated ClawHub feed-signing identity: + +- ClawHub stores and uses the private Ed25519 key; +- OpenClaw bundles the matching public key and stable key id; +- the built-in `clawhub-public` profile expects feed id `clawhub-official`; +- normal users do not configure the official key manually; +- an environment override MAY exist for development or staged rollout, but + MUST fail closed when only part of the key pair configuration is present. + +Bundling a public key is source-controlled trust distribution. It is not secret +storage and it does not put the private key in the OpenClaw release. + +The same dedicated ClawHub feed-signing identity MAY sign multiple +ClawHub-operated feed classes, including public catalog, publisher, named, +organization, and composed feeds. Each feed class MUST define its own versioned +payload type, expected identity binding, schema, and verifier before clients +accept it. Sharing the platform feed-signing key does not permit one payload +type or feed identity to be replayed as another, and it does not grant access, +approval, or install authority. + +## Rotation And Revocation + +Signed Feed v1 does not define an in-band key-rotation document. Rotation uses +the same authenticated channel that established the trust anchor. + +A normal rotation SHOULD proceed as follows: + +1. Generate a new dedicated feed-signing key. +2. Add the new public key to bundled or operator-managed trust configuration. +3. Publish envelopes signed by enough old trusted keys to satisfy the current + threshold, optionally also signed by the new key. +4. Distribute the updated trust configuration. +5. After the update window, sign with the new active set and remove retired keys + in a later configuration or release update. + +Merely including a signature from one old key is insufficient when the current +threshold is greater than one. Emergency revocation may intentionally make the +last snapshot unusable; clients must fail closed and identify the revoked or +missing trust state without revealing key material beyond public key ids. + +Future in-band rotation requires a separately versioned payload type, exact wire +schema, replay rules, and threshold transition semantics. Implementations MUST +NOT invent incompatible rotation documents under this v1 payload type. + +## Diagnostics + +Clients SHOULD distinguish: + +- malformed envelope; +- unsupported payload type; +- malformed feed payload; +- expected feed-id mismatch; +- unknown key id; +- invalid signature; +- duplicate key id or duplicate public key material; +- threshold not met; +- expired feed; +- rollback sequence; +- snapshot rejected under the current policy. + +Diagnostics MAY expose feed profile, expected and actual feed ids, sequence, +public key ids, threshold, verification time, and fallback source. They MUST NOT +expose private keys, bearer tokens, credential-bearing URLs, or payload bytes. + +## Publisher Conformance + +A signed v1 publisher: + +- signs the exact stored feed payload bytes using the DSSE PAE above; +- uses the required envelope and payload type; +- keeps sequence monotonic across deletion and demotion; +- keeps private keys in publisher-owned secret storage; +- publishes stable public key ids through an authenticated trust channel; +- provides positive, tampered-payload, wrong-key, and rollback test vectors. + +## Client Conformance + +A signed v1 client: + +- obtains initial trust independently of the feed endpoint; +- binds signed payload identity to the selected profile; +- enforces distinct-key thresholds and fail-closed behavior; +- stores and reverifies last-known-good signed snapshots; +- preserves rollback metadata across verification-policy changes; +- reports bounded, actionable verification diagnostics.