forked from 1b-agent/fieldcraft-upstream
Release: sign SHA256SUMS.txt with the release key; docs/updates.md (#231)
The release job signs SHA256SUMS.txt into SHA256SUMS.txt.minisig with rsign2 when the FIELDCRAFT_SIGNING_KEY secret exists, and checks the signature against the public key the app is built with; without the secret the draft is unsigned, the run warns and the notes say the app cannot install that release itself. docs/releasing.md says how the key is set up, kept and rotated; docs/updates.md describes checking, verifying and installing, the web app's Reload to update, and FIELDCRAFT_UPDATE_HOST for tests. privacy.md, control-protocol.md, web.md and the roadmap say what changed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
93b2ae343c
commit
86ba1bda5c
8 files changed
+183
-8
No files matched your search
@@ -41,7 +41,7 @@ commands (files, undo, `model.info`) leave the ribbon where it is.
|
||||
|
||||
| Method | Params | Notes |
|
||||
|---|---|---|
|
||||
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `solve` (the title bar's Solve: `enabled`, and `unavailable` as `ui.toolbar`'s entries give it, listing everything `solve.run` misses), `busy` (the background job: a file opening, a solve, a mesher run from its form by a click on OK), `prompt` (`{"unsaved": path}`, `{"design": path}` (a design of another part opened over a meshed or defined model), `"recover"`, `"samples"`), `recent` (recent files), `selected` (the browser's selected definition), `browser` (each top-level browser node's setup state: `[{node, state, tip, add}]`, `state` `done`, `next`, `blocked` (⚠: its command cannot run yet; `tip` says what it needs and where), `todo`, `optional` or null for nodes that are no setup stage, `add` its ghost rows `{stage, label, command, status}`; a ghost row's region is `layout`'s `browser/add/<stage>`), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:<action>`, `browser`, `viewport`, `nav_bar`, `axis_view`, `selection_bar`, `legend` (while results are shown), `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version |
|
||||
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `solve` (the title bar's Solve: `enabled`, and `unavailable` as `ui.toolbar`'s entries give it, listing everything `solve.run` misses), `busy` (the background job: a file opening, a solve, a mesher run from its form by a click on OK), `prompt` (`{"unsaved": path}`, `{"design": path}` (a design of another part opened over a meshed or defined model), `"recover"`, `"samples"`), `recent` (recent files), `updates` (docs/updates.md: `automatic`, `checked_at`, `skip`, `available` (a newer version the status bar offers), `busy`, `staged`, `unsupported`; null when the run does not check), `selected` (the browser's selected definition), `browser` (each top-level browser node's setup state: `[{node, state, tip, add}]`, `state` `done`, `next`, `blocked` (⚠: its command cannot run yet; `tip` says what it needs and where), `todo`, `optional` or null for nodes that are no setup stage, `add` its ghost rows `{stage, label, command, status}`; a ghost row's region is `layout`'s `browser/add/<stage>`), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:<action>`, `browser`, `viewport`, `nav_bar`, `axis_view`, `selection_bar`, `legend` (while results are shown), `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version |
|
||||
| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `show_geometry`, `show_mesh`, `show_loads` (boundary condition and load symbols), `show_interactions` (reference points, couplings, contact and tie surfaces, rigid walls), `symbol_size` (the setup symbols' size on screen in points, 6–48, at any zoom), `perspective`, `dark`, `show_tree` (the browser), `show_timeline`, `nav_preset` (the mouse navigation: `fieldcraft`, `fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`; an unknown id acts as `fieldcraft`) and `reverse_zoom` (both remembered per user), `workspace` (`prepare`, `model`, `analysis`, `results`), `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`; a tab of another workspace brings that workspace), `palette` (the command search open), `menu` (the open menu: `file`, `help`, `workspace`, `view`, `more`, or a panel's drop-down as `<tab>/<panel>` such as `mesh/Tet`; `null` closes it) | an unknown setting or value is an error; returns the settings |
|
||||
| `ui.view` | `view`: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`, `home` (iso and fit), `fit` | every view frames the model as drawn (the deformed shape included; the setup's symbols keep their size on screen and are not framed) in the part of the viewport the overlays (`layout`'s `legend`, `selection_bar`, `axis_view`, `nav_bar`) leave free; `fit` keeps the direction of view. Opening another model, sample or design frames it in the home view (the same design opened again after an edit keeps the camera), and results that come up with part of the model out of sight are framed. Returns the camera |
|
||||
| `ui.camera` | `yaw`, `pitch` (degrees), `distance?` (model units) | turns the camera to any orientation about its target: `yaw` about Z (0 looks along +Y, the front view; −45 is the iso view's, −90 the right view's), `pitch` the elevation above the XY plane; `distance` moves the eye (the zoom of an orthographic view). Angles must be finite and the distance above 0: the yaw is wrapped, the pitch clamped to ±90 and the distance to 0.001–10⁹; anything else is an error. A sequence of calls orbits smoothly. Returns the camera (`ui.inspect`'s, angles in radians) |
|
||||
@@ -50,7 +50,7 @@ commands (files, undo, `model.info`) leave the ribbon where it is.
|
||||
| `ui.select` | `kind` (`set`, `material`, `section`, `step`, `boundary_condition`, `load`, `reference_point`, `coupling`, `fluid_domain`), `name`; nothing to deselect | selects a definition as a browser click does; its properties (with Edit and Delete) show in the context panel |
|
||||
| `ui.select` (viewport) | any of `filter`, `box_mode`, `mode`, `clear`, `at`, `box`, `geometry`, `nodes`, `elements`, `faces` (and no `kind`) | changes the viewport's selection (see [Selection](#selection)); returns `ui.selection`'s reply |
|
||||
| `ui.selection` | — | the viewport's selection: `filter`, `box_mode` (box selection on), `count`, `capped`, `summary`, `items`, `params` (the selection as a set's members), `hover` (what a click would pick under the pointer) |
|
||||
| `ui.prompt` | `answer`: for `unsaved` `save`, `discard` or `cancel`; for `design` `new` (a new model with the design), `keep` (the design over the current model) or `cancel`; for `replace_mesh` `replace` (runs the form, replacing the mesh) or `cancel` (the form stays open); for `delete` (the item menu's Delete… or the Delete key: `{delete: question, kind, name, users, allowed}`) `delete` (with a datum's partitions when it has any; refused when `allowed` is false) or `cancel`; for `recover` `recover` (with `id` from `file.recovery.list`) or `close`; for `samples` `open` (with `name`) or `close`; for `crashed` (the start after a crash: `{crashed: [reports]}`) `report` (opens Report a Bug for the newest) or `close`; for `report_bug` (`{report_bug: draft}`) `set` (any of `title`, `observed`, `share_session`, `include_model`; replies with the draft) or `close` — the browser opens only from a click (docs/crash-reports.md); for `consent` (the first run's question about anonymous usage) `share` or `decline`; for `settings` (Help › Settings…) `set` (any of `usage_sharing`, `usage_automation`) or `close` (docs/privacy.md) | answers the window's question (`ui.inspect`'s `prompt`) |
|
||||
| `ui.prompt` | `answer`: for `unsaved` `save`, `discard` or `cancel`; for `design` `new` (a new model with the design), `keep` (the design over the current model) or `cancel`; for `replace_mesh` `replace` (runs the form, replacing the mesh) or `cancel` (the form stays open); for `delete` (the item menu's Delete… or the Delete key: `{delete: question, kind, name, users, allowed}`) `delete` (with a datum's partitions when it has any; refused when `allowed` is false) or `cancel`; for `recover` `recover` (with `id` from `file.recovery.list`) or `close`; for `samples` `open` (with `name`) or `close`; for `crashed` (the start after a crash: `{crashed: [reports]}`) `report` (opens Report a Bug for the newest) or `close`; for `report_bug` (`{report_bug: draft}`) `set` (any of `title`, `observed`, `share_session`, `include_model`; replies with the draft) or `close` — the browser opens only from a click (docs/crash-reports.md); for `consent` (the first run's question about anonymous usage) `share` or `decline`; for `settings` (Help › Settings…) `set` (any of `usage_sharing`, `usage_automation`; or `update_check`, `update_skip` (a version, `""` for none)) or `close` (docs/privacy.md, docs/updates.md); for `update` (a downloaded and verified update: `{update: state}`) `restart` (saves or asks about unsaved changes first, then closes the window to install) or `later` (installs when the app quits) | answers the window's question (`ui.inspect`'s `prompt`) |
|
||||
| `ui.settings` | — | opens Help › Settings… (answered with `ui.prompt`); replies with the privacy state as `ui.inspect`'s `privacy` gives it: `available` (this run may share usage), `usage_sharing` (`null` until asked), `usage_automation`, `recording`, `queued` |
|
||||
| `ui.item` | `kind` (`body`, `surface`, or a definition kind as `ui.select` takes it, `datum`, `partition`), `name`, `action`: `edit`, `hide`, `show`, `isolate`, `show_all`, `delete`, or `activate`/`deactivate` with `step` | an item menu's entry, as a right click on the item and a click on the entry: runs the same command (`delete` opens the delete prompt, answered with `ui.prompt`) |
|
||||
| `ui.dialog` | `params?` (JSON object, fills the form), `use_selection?`, `run?`, `submit?`, `cancel?` | drives the open command form, in that order; `use_selection` is the form's Target row (see [Selection](#selection)); `run` returns the command's result and closes the form on success; `submit` is the form's OK button: a mesher that would replace a body's mesh first asks (`prompt` `replace_mesh`, with the question and what else goes: sets listing the old labels, solved results), and meshers run in the background (`busy`); a form opened by another form's **Create …** button (a reference field with nothing to pick) goes back to that form on `run` (with the new item picked) or `cancel` |
|
||||
|
||||
@@ -11,6 +11,12 @@ Change it any time in **Help › Settings… › Privacy**, which also has **Inc
|
||||
agents (MCP)**: commands run by scripts, the control channel and AI agents are counted apart from
|
||||
yours (`from`), and this leaves them out.
|
||||
|
||||
Checking for updates is separate from usage sharing: at start-up, at most once a day (and from
|
||||
Help › Check for Updates), the desktop app asks GitHub for the latest release, which sends the
|
||||
request itself (FieldCraft's version in the `User-Agent`; GitHub sees the IP address) and nothing
|
||||
else. Turn it off in **Help › Settings… › Updates** (or with the first run's question): then no
|
||||
request is made ([updates.md](updates.md)).
|
||||
|
||||
Crash reports and the log never leave your computer on their own; Help › Report a Bug sends only
|
||||
what you submit yourself ([crash-reports.md](crash-reports.md)).
|
||||
|
||||
|
||||
+42
-2
@@ -14,7 +14,7 @@ are in `packaging/` and run the same way on a developer machine.
|
||||
| Linux x86_64, glibc 2.35+ | `fieldcraft-<v>-linux-x86_64.tar.gz` (`bin/`, `share/`), `fieldcraft-<v>-linux-x86_64.AppImage`, `fieldcraft_<v>_amd64.deb` (Debian, Ubuntu 22.04+), `fieldcraft-<v>.x86_64.rpm` (Fedora, RHEL, openSUSE) | `packaging/linux/package.sh` |
|
||||
| Windows x64 | `fieldcraft-<v>-windows-x64.zip` (`fieldcraft.exe`, `fieldcraft-cli.exe`) | `packaging/windows/package.ps1` |
|
||||
|
||||
Plus `SHA256SUMS.txt` and the notes from `.github/release-notes.md` followed by GitHub's
|
||||
Plus `SHA256SUMS.txt`, its signature `SHA256SUMS.txt.minisig` (below), and the notes from `.github/release-notes.md` followed by GitHub's
|
||||
generated list of changes. Every package carries `README.md`, `LICENSE-MIT`, `LICENSE-APACHE`,
|
||||
`NOTICE` and `THIRD-PARTY-LICENSES.txt` (on macOS in `FieldCraft.app/Contents/Resources/Licenses`).
|
||||
The application id is `io.github.jyviko.fieldcraft`: the macOS bundle id, the Linux `.desktop`
|
||||
@@ -56,6 +56,44 @@ three notarization secrets the app is signed and not notarized. A dmg made local
|
||||
`packaging/macos/package.sh` is ad-hoc signed unless `MACOS_SIGN_IDENTITY` names an identity
|
||||
in the keychain.
|
||||
|
||||
**The release key (updates).** The app installs a release only when `SHA256SUMS.txt` is signed
|
||||
with the FieldCraft release key ([updates.md](updates.md)): an Ed25519 key in minisign's format.
|
||||
Its public half is built into the app (`fieldcraft_update::PUBLIC_KEY` in
|
||||
`crates/update/src/lib.rs`):
|
||||
|
||||
```
|
||||
untrusted comment: minisign public key: FEF410EFEF5BAB21
|
||||
RWQhq1vv7xD0/uAOt8fqERP1hP+1UCM9U9QHTDYTYytWKiuedTkT7Lgr
|
||||
```
|
||||
|
||||
The secret half is the repository secret `FIELDCRAFT_SIGNING_KEY`: the whole secret key file
|
||||
(two lines), made without a password. With it, the release job signs `SHA256SUMS.txt` with
|
||||
[rsign2](https://github.com/jedisct1/rsign2) (pinned in `release.yml`), checks the signature
|
||||
against the public key in the source, and uploads `SHA256SUMS.txt.minisig`. Without it the
|
||||
draft is built unsigned, the run warns, and the notes say the app cannot install that release
|
||||
itself (users download it from the page). Anyone can check a release by hand with `minisign -Vm
|
||||
SHA256SUMS.txt -P RWQhq1vv7xD0/uAOt8fqERP1hP+1UCM9U9QHTDYTYytWKiuedTkT7Lgr`.
|
||||
|
||||
Setting it up, once:
|
||||
|
||||
```sh
|
||||
cargo install --locked rsign2 --version 0.6.7
|
||||
rsign generate -W -p fieldcraft-release.pub -s fieldcraft-release.key # -W: no password
|
||||
gh secret set FIELDCRAFT_SIGNING_KEY -R jyviko/fieldcraft < fieldcraft-release.key
|
||||
```
|
||||
|
||||
Keep an offline copy of the secret key file (a password manager, an encrypted backup) and
|
||||
delete it from the working machine. Never commit it.
|
||||
|
||||
Rotating it (a key that leaked, or a planned change): generate a new pair, put the new public
|
||||
key into `PUBLIC_KEY` and the block above, re-sign `crates/update/tests/data/SHA256SUMS.txt`
|
||||
with it (`rsign sign -W -s <new key> -x crates/update/tests/data/SHA256SUMS.txt.minisig
|
||||
crates/update/tests/data/SHA256SUMS.txt`, which a test checks), release that version, and only
|
||||
then replace the secret. Copies that still carry the old public key cannot verify releases
|
||||
signed with the new one: they still show the update, but Install refuses it ("not signed by the
|
||||
FieldCraft release key"), and a download from the release page brings the new key. If the old key leaked, also delete the secret at once (releases go out
|
||||
unsigned until the new one is in place) and say so in the next release notes.
|
||||
|
||||
**Linux and Windows** builds are not signed; the checksums are the integrity check. On Windows
|
||||
SmartScreen warns about an unsigned download (More info › Run anyway) until a code-signing
|
||||
certificate is added.
|
||||
@@ -100,7 +138,9 @@ Publishing is a maintainer's call; an agent preparing a release stops at the dra
|
||||
1. Check the run succeeded for the intended commit and the draft targets it
|
||||
(`gh release view v<version> -R jyviko/fieldcraft`).
|
||||
2. Download the assets into an empty folder (`gh release download v<version> -R jyviko/fieldcraft`),
|
||||
run `shasum -a 256 -c SHA256SUMS.txt`, and check every platform's files are there.
|
||||
run `shasum -a 256 -c SHA256SUMS.txt`, check the signature
|
||||
(`minisign -Vm SHA256SUMS.txt -P <the public key above>`, or `rsign verify -P …`), and check
|
||||
every platform's files are there.
|
||||
3. Smoke-test what you can run: open the dmg, drag the app to Applications, launch it with
|
||||
`--sample cantilever`; run `fieldcraft-cli --version` from the zip or tar.gz; on Linux run the
|
||||
AppImage, or install the `.deb` (`sudo apt install ./fieldcraft_<v>_amd64.deb`) or `.rpm`
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Updates
|
||||
|
||||
FieldCraft checks for new releases, verifies them and installs them itself, with your consent at
|
||||
each step. The code is `crates/update` (`fieldcraft-update`); the window's side is
|
||||
`crates/ui-egui/src/updates.rs`, the desktop app's `apps/fieldcraft/src/update.rs`.
|
||||
|
||||
## Checking
|
||||
|
||||
The app asks the GitHub Releases API for the latest published release
|
||||
(`GET https://api.github.com/repos/jyviko/fieldcraft/releases/latest`; drafts and pre-releases
|
||||
are never offered) and compares its version with the running one (Semantic Versioning order).
|
||||
|
||||
- **At start-up**, at most once a day, when **Check for updates automatically** is on (the
|
||||
default). It is shown with the first run's question and in **Help › Settings… › Updates**.
|
||||
Off means no request at all. A failed automatic check is silent (it goes to the log). Runs
|
||||
driven by automation (`--control`) and CI never check automatically.
|
||||
- **Help › Check for Updates** asks now, and says when FieldCraft is up to date or why the check
|
||||
failed.
|
||||
- **Agents and scripts**: the `app.update.check` command (in the app, and through MCP connected
|
||||
to it) and `fieldcraft-cli update --check` reply with `current`, `latest`, `newer`, `url` (the
|
||||
release page), `published`, `signed` and `asset` (the file this install would download; `null`
|
||||
when it cannot update itself). They only report.
|
||||
|
||||
The request carries the FieldCraft version in its `User-Agent`; GitHub sees the request's IP
|
||||
address, as with any download. Nothing else is sent ([privacy.md](privacy.md)).
|
||||
|
||||
A newer release shows a notice in the status bar: **FieldCraft 0.5.9 is available — What's new ·
|
||||
Install · Skip**. What's new opens the release page; Skip stops offering that version (Settings
|
||||
can offer it again).
|
||||
|
||||
## Verifying
|
||||
|
||||
Every release carries `SHA256SUMS.txt` and, signed with the FieldCraft release key,
|
||||
`SHA256SUMS.txt.minisig` (Ed25519 in minisign's format; the public key is built into the app,
|
||||
`fieldcraft_update::PUBLIC_KEY`, and printed in [releasing.md](releasing.md)). Installing:
|
||||
|
||||
1. downloads the list and its signature, and refuses unless the signature is the release key's;
|
||||
2. downloads the asset for this install and refuses unless its SHA-256 is the one the list gives;
|
||||
3. on macOS, refuses an app that fails `codesign --verify --deep --strict`, is not
|
||||
`io.github.jyviko.fieldcraft`, or is rejected by Gatekeeper (`spctl`) when the installed app
|
||||
is accepted.
|
||||
|
||||
A release without a signature cannot be installed from the app: Install opens its page instead.
|
||||
A development build (run from `target/`) checks but never installs.
|
||||
|
||||
## Installing
|
||||
|
||||
Install downloads and verifies in the background, unpacks the release beside the installed copy
|
||||
(`.fieldcraft-update-<version>`, so the swap is a rename on one file system), then asks to restart.
|
||||
**Restart Now** saves or asks about unsaved changes first, as opening another model does; the
|
||||
window closes, the autosave is closed, the swap is made and the new version starts. **Later**
|
||||
installs when you quit. A failed swap puts the previous version back; a staging folder left
|
||||
behind is removed at the next start.
|
||||
|
||||
| Installed as | Asset | Swap |
|
||||
|---|---|---|
|
||||
| `FieldCraft.app` (macOS) | `fieldcraft-<v>-macos-universal.dmg` | the bundle is replaced, then opened |
|
||||
| `fieldcraft-cli` (macOS) | `fieldcraft-cli-<v>-macos-universal.zip` | the binary is replaced |
|
||||
| A Windows folder | `fieldcraft-<v>-windows-x64.zip` | each file is replaced (a running `.exe` is renamed away, never overwritten); the app restarts |
|
||||
| An AppImage | `fieldcraft-<v>-linux-x86_64.AppImage` | the file is replaced and started |
|
||||
| A tar.gz folder | `fieldcraft-<v>-linux-x86_64.tar.gz` | the folder is replaced; when its parent is not writable the app says so and links the download |
|
||||
| `.deb` / `.rpm` | `fieldcraft_<v>_amd64.deb` / `fieldcraft-<v>.x86_64.rpm` | never replaced by the app: the verified package is downloaded to the app's data folder (`updates/`), and the app offers **Open with Installer** and the `sudo apt install …` / `sudo dnf install …` command |
|
||||
|
||||
**Help › Reinstall FieldCraft…** downloads and verifies the running version again and swaps it
|
||||
in the same way, to repair a damaged install.
|
||||
|
||||
`fieldcraft-cli update --install` does the same from the command line for the install the CLI
|
||||
belongs to (the tar.gz folder or Windows folder it is in, or the macOS CLI binary), without
|
||||
restarting anything; for `.deb`/`.rpm` it prints the package manager command.
|
||||
|
||||
## The web app
|
||||
|
||||
The browser build has no downloads to install: every half hour the page fetches its own address
|
||||
past the browser's cache, and when the deployed page loads another build than the running one
|
||||
(Trunk names each build's files by their content hash), the status bar offers **Reload to
|
||||
update**. Reloading with unsaved changes asks first, as closing the page does.
|
||||
|
||||
## Testing
|
||||
|
||||
`FIELDCRAFT_UPDATE_HOST` replaces `https://api.github.com` with another server, and allows plain
|
||||
`http` for it. It is for tests against a local server only (`fieldcraft_update::mock::Server`,
|
||||
behind the crate's `mock` feature, plays the Releases API); with it set, the app also checks
|
||||
automatically in automation runs, so the notice can be looked at:
|
||||
|
||||
```sh
|
||||
FIELDCRAFT_UPDATE_HOST=http://127.0.0.1:8899 fieldcraft-cli update --check
|
||||
```
|
||||
|
||||
The tests cover version order, the daily schedule and no request when checks are off, asset
|
||||
selection and the swap plan for every install kind, a checksum mismatch and a missing or foreign
|
||||
signature refused (without downloading the asset), archives that try to write outside their
|
||||
folder, undoing a failed swap, and the CLI against a local server. They never replace a running
|
||||
program.
|
||||
@@ -49,6 +49,7 @@ The browser has no file system the app can reach, so files live in the page
|
||||
| Autosave to the recovery folder, File › Recover… | Autosaves are kept in the browser's local storage; a page closed or crashed with unsaved changes offers them at the next visit (File › Recover…) |
|
||||
| Leaving with unsaved changes asks first | The browser's "leave site?" prompt |
|
||||
| Recent files | This visit's files only |
|
||||
| Help › Check for Updates installs a new release (docs/updates.md) | A new deployment shows **Reload to update** in the status bar |
|
||||
|
||||
A `.fieldcraft` model refers to its design and meshes by path. Paths in the model's own folder
|
||||
are stored relative to it, so picking or dropping the model with those files opens them again; a
|
||||
|
||||
Reference in new issue
Block a user