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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ jobs:
with:
go-version-file: godwit/go.mod
cache-dependency-path: godwit/go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
# core/plugins imports internal/i18n, whose catalog_gen.go is generated and not committed.
- name: Generate Godwit's sources
run: |
(cd godwit/web && bun install --frozen-lockfile)
cd godwit && go tool mygo generate
- name: Build registry.json from the directory and each plugin's pinned commit
run: cd godwit && go run ./cmd/godwit-registry build ../directory.json ../registry.json
# What the reviewer approves: the difference this pull request makes to what is published.
- name: Show what changes
if: github.event_name == 'pull_request'
run: |
curl -fsS https://dl.godwit.email/plugins/registry.json | jq -S 'del(.updated)' > published.json
jq -S 'del(.updated)' registry.json > built.json
{ echo '```diff'; diff -u published.json built.json || true; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
# Godwit's own validator: what passes here is what the app accepts.
- name: Check every release and download every file
run: cd godwit && go run ./cmd/godwit-registry check --fetch ../registry.json
Expand Down
55 changes: 42 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

The plugins [Godwit](https://github.com/godwit-email/godwit)'s Extensions page offers as reviewed. Godwit reads the signed
`registry.json` from `dl.godwit.email/plugins/`. The design is
[`docs/superpowers/specs/2026-10-14-native-plugins-design.md`](https://github.com/godwit-email/godwit/blob/main/docs/superpowers/specs/2026-10-14-native-plugins-design.md) in the Godwit repository.
[`docs/superpowers/specs/2026-10-14-native-plugins-design.md`](https://github.com/godwit-email/godwit/blob/main/docs/superpowers/specs/2026-10-14-native-plugins-design.md) and
[ADR 0032](https://github.com/godwit-email/godwit/blob/main/docs/adr/0032-plugin-directory-points-at-plugins.md) in the Godwit repository.

## How it works

- `registry.json` lists plugins and their releases. Each release carries its `plugin.json`
- `directory.json` says what each plugin is, in the reviewers' words, and pins by commit the
`godwit-plugin.json` its author maintains.
- `godwit-plugin.json` lists the plugin's releases. Each release carries its `plugin.json`
(as `manifest`) and, per file it ships, the URL, size and SHA-256. Godwit trusts **those
exact files**, never what a plugin says about itself.
- A merge to `main` runs `godwit-registry check --fetch` (the validator Godwit itself uses,
plus a download of every file), signs `registry.json` with the repository's ed25519 key
- `godwit-registry build` joins them into `registry.json`, which is built and never committed.
Godwit cannot check a commit SHA itself, so what it trusts is the signature over the result.
- A merge to `main` builds it, runs `godwit-registry check --fetch` (the validator Godwit itself
uses, plus a download of every file), signs `registry.json` with the repository's ed25519 key
and uploads both files to `dl.godwit.email/plugins/`. Godwit verifies the signature with the
public key built into it, and refuses a registry older than the one it has.
- Godwit installs a release by downloading its file and checking the size and SHA-256. A
Expand All @@ -25,14 +30,33 @@ In-repository script plugins live under `packages/`, including [Zen Free](packag
One **Plugins** workflow discovers and checks all packages and publishes a selected plugin
on manual dispatch. **Publish** separately validates, signs, and uploads the registry.

Open a pull request that adds to `plugins` (or to an existing plugin's `releases`):
1. Commit the new release to the plugin's `godwit-plugin.json` (a new plugin: create the file).
2. Open a pull request here that sets `source.sha` to that commit, or adds the entry. The check
shows what changes in the published `registry.json`; that difference is what gets reviewed.

An entry of `directory.json`:

```json
{
"name": "example",
"title": { "default": "Example", "zh-Hans": "示例" },
"summary": { "default": "Translates mail with an example service. Sends the text to example.com." },
"category": "translate",
"publisher": "Example",
"dataFlow": "third-party",
"homepage": "https://github.com/someone/godwit-example",
"source": { "repo": "someone/godwit-example", "path": "godwit-plugin.json", "sha": "<the full 40-character commit SHA>" }
}
```

- `category` is `translate`, `models` or `other`; `dataFlow` is `local` or `third-party`.
- `name` never changes once published: installs are keyed by it. To relabel a plugin, change `title`.

A `godwit-plugin.json`, with every release still offered or installed, not just the newest:

```json
{
"name": "example",
"releases": [
{
"version": "0.1.0",
Expand All @@ -52,7 +76,7 @@ Open a pull request that adds to `plugins` (or to an existing plugin's `releases
[`sdk/native/godwit_plugin.h`](https://github.com/godwit-email/godwit/blob/main/sdk/native/godwit_plugin.h).
- A script release has one file, `any`: the plugin bundled into the single `.js` file its
`manifest.main` names, at most 5 MiB.
- Bump the top-level `updated` (RFC 3339) in every pull request.
- A release that leaves `releases` is revoked for everyone who installed it.

## Building and releasing script plugins

Expand All @@ -69,9 +93,11 @@ without adding a workflow.
minimum released Godwit version. The workflow checks the tagged source, installs the
published SDK, and publishes `index.js`, `plugin.json`, `<name>.tar.gz`,
`registry-plugin.json`, and `SHA256SUMS` to GitHub Releases.
5. Add the released `registry-plugin.json` entry to `registry.json`, or merge its release
into the existing entry while preserving older releases. Bump `updated` and open a PR.
**Publish** validates the real downloads, signs, and uploads the registry.
5. Copy the release from the released `registry-plugin.json` into the plugin's
`packages/<name>/godwit-plugin.json`, preserving older releases, and merge it. Then open a PR
that sets the plugin's `source.sha` in `directory.json` to that commit (a new plugin: adds its
entry, whose title, summary and risk wording are the reviewers'). **Publish** builds the
registry, validates the real downloads, signs, and uploads it.

PR checks provide a `<name>-dist` artifact. Until the model API's SDK is published, they
pack it from the pinned Godwit commit used by the goja integration test. Release runs
Expand All @@ -92,13 +118,16 @@ Before approving:
answers the same name, version, ABI and capabilities, or Godwit will not start it.
For a script plugin, the permissions are no wider than what it does.
4. **Risk in plain words.** If the plugin depends on an unofficial or rate-limited service, or
sends mail content somewhere, `summary` says so.
sends mail content somewhere, `summary` says so and `dataFlow` is `third-party`.

## Revoking

Remove the release (or the whole plugin) and merge. On its next refresh (on opening
Extensions, and every 24 hours) Godwit stops every installed copy of a file the registry no
longer lists and asks the user before it runs again.
Add the version to the entry's `revoked` list, or remove the whole entry, and merge. Neither needs
the author. On its next refresh (on opening Extensions, and every 24 hours) Godwit stops every
installed copy of a file the registry no longer lists and asks the user before it runs again.

If a pinned commit can no longer be fetched (the repository was deleted or its history rewritten),
the build fails and nothing can be published until that entry is changed or removed.

## Keys

Expand Down
42 changes: 42 additions & 0 deletions directory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"plugins": [
{
"category": "translate",
"dataFlow": "third-party",
"homepage": "https://github.com/godwit-email/plugin-dlx",
"name": "dlx",
"publisher": "Godwit Community",
"source": {
"path": "godwit-plugin.json",
"repo": "godwit-email/plugin-dlx",
"sha": "717b83825d632667bffa7207813533458e10b029"
},
"summary": {
"default": "Translates mail and selected text, with nothing to set up. The text goes to a third-party translation service through an unofficial interface, which may be rate limited or stop working.",
"zh-Hans": "翻译邮件与选中文本,无需设置。文本会通过非官方接口发送到第三方翻译服务,可能被限流或失效。"
},
"title": {
"default": "DLX"
}
},
{
"category": "models",
"dataFlow": "local",
"homepage": "https://github.com/godwit-email/plugin-startlux",
"name": "startlux",
"publisher": "Godwit",
"source": {
"path": "godwit-plugin.json",
"repo": "godwit-email/plugin-startlux",
"sha": "36d8e2e879717a3f66e74fb994ad375f4dda612d"
},
"summary": {
"default": "Local judge models: label mail and answer yes/no questions on this computer, with no mail sent anywhere. Downloads a 2–2.7 GB model when you choose it; on computers without Apple silicon it is slow and best for new mail only.",
"zh-Hans": "本地判断模型:在本机给邮件打标签、回答是非题,邮件内容不发往任何地方。选用时下载 2–2.7 GB 的模型;非 Apple 芯片的电脑上较慢,建议只给新邮件打标签。"
},
"title": {
"default": "StartLux"
}
}
]
}
Loading
Loading