bots-garden/mini-mepublic Fork
⑂ 7bd25a0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-11-07T12:22:22+01:00 Browse files
7bd25a0 parent: aa3eee8
modified .gitignore +1 -0
@@ -20,3 +20,4 @@ release
2020 .release.env
2121 backup
2222 .backup
23+sidekicks
@@ -20,3 +20,4 @@ release
20 .release.env20 .release.env
21 backup21 backup
22 .backup 22 .backup
23+sidekicks
modified .tickets/issues/0195-sign-the-macos-binary-of-mini-me.yaml +37 -3
@@ -6,7 +6,7 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-07T05:28:39.000Z
9-updatedAt: 2026-11-07T08:07:23.000Z
9+updatedAt: 2026-11-07T11:16:07.000Z
1010 labels:
1111 - kind::chore
1212 epic: Release & distribution
@@ -37,20 +37,24 @@ body: |
3737 tasks:
3838 - id: 1
3939 title: Write the manual signing and upload procedure
40- state: open
40+ state: closed
4141 priority: none
4242 author:
4343 name: k33g
4444 email: ph.charriere@gmail.com
4545 createdAt: 2026-11-07T05:28:39.000Z
46+ updatedAt: 2026-11-07T10:53:47.000Z
47+ closedAt: 2026-11-07T10:53:47.000Z
4648 - id: 2
4749 title: Update SHA256SUMS when the signed binary is uploaded
48- state: open
50+ state: closed
4951 priority: none
5052 author:
5153 name: k33g
5254 email: ph.charriere@gmail.com
5355 createdAt: 2026-11-07T05:28:39.000Z
56+ updatedAt: 2026-11-07T10:53:47.000Z
57+ closedAt: 2026-11-07T10:53:47.000Z
5458 - id: 3
5559 title: Request a binary publishing API feature on Rickub
5660 state: open
@@ -69,6 +73,14 @@ tasks:
6973 createdAt: 2026-11-07T05:46:16.000Z
7074 updatedAt: 2026-11-07T08:07:23.000Z
7175 closedAt: 2026-11-07T08:07:23.000Z
76+ - id: 5
77+ title: Check the signature on a Mac (wasm under the hardened runtime, then a real release)
78+ state: open
79+ priority: high
80+ author:
81+ name: k33g
82+ email: ph.charriere@gmail.com
83+ createdAt: 2026-11-07T10:53:47.000Z
7284 comments:
7385 - id: 1
7486 author:
@@ -89,3 +101,25 @@ comments:
89101 - The binaries go through git on the temporary branch: 148 MB per run with all platforms.
90102
91103 The feature request text is in `demo-rickub-feature-request.txt`.
104+ - id: 2
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-11-07T10:53:47.000Z
109+ body: |
110+ The signature now happens before publishing, not after: `local-ci-02-build.sh` signs and notarizes `mm-<version>-darwin-arm64` on a Mac when `MM_SIGN_IDENTITY` and `MM_NOTARY_PROFILE` are set (in `.release.env`, from the Sidekicks variables). `SHA256SUMS` is computed after the signature, and `local-ci-03-create-draft.sh` adds a line to the release notes. The procedure, the one-time settings and the manual fallback are in [RELEASING.macos.fr.md](RELEASING.macos.fr.md).
111+
112+ - Nothing to register at Apple: the Developer ID certificate and the notarytool profile belong to the team, not to an app.
113+ - A bare binary cannot be stapled: Gatekeeper fetches the ticket online on first run.
114+ - Tried on Linux with stand-in `codesign`, `xcrun`, `security` and `ditto`: accepted, Invalid, missing profile, not macOS, unknown identity, unsigned. Never run on a real Mac yet.
115+ - Risk to check first: wazero compiles wasm to machine code at run time, and the hardened runtime may block it (step A4 of the document).
116+ - id: 3
117+ author:
118+ name: k33g
119+ email: ph.charriere@gmail.com
120+ createdAt: 2026-11-07T11:16:07.000Z
121+ body: |
122+ Measured on the Mac with `check-wasm-hardened-runtime.sh` and the Developer ID: with the hardened runtime alone, macOS kills the wasmtool tests (`Killed: 9`) at the first wasm module. Without the hardened runtime, 11/11 pass; with the hardened runtime and `com.apple.security.cs.allow-unsigned-executable-memory`, 11/11 pass.
123+
124+ Decision: sign with that entitlement ([macos-entitlements.plist](macos-entitlements.plist)), rather than switching wazero to its interpreter on macOS. It is invisible to the user, at install and at first run, and wasm keeps its speed. `local-ci-02-build.sh` signs with it and checks that the signature carries it. Notarization with the entitlement is not tried yet.
125+
@@ -6,7 +6,7 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-07T05:28:39.000Z8 createdAt: 2026-11-07T05:28:39.000Z
9-updatedAt: 2026-11-07T08:07:23.000Z9+updatedAt: 2026-11-07T11:16:07.000Z
10 labels:10 labels:
11 - kind::chore11 - kind::chore
12 epic: Release & distribution12 epic: Release & distribution
@@ -37,20 +37,24 @@ body: |
37 tasks:37 tasks:
38 - id: 138 - id: 1
39 title: Write the manual signing and upload procedure39 title: Write the manual signing and upload procedure
40- state: open40+ state: closed
41 priority: none41 priority: none
42 author:42 author:
43 name: k33g43 name: k33g
44 email: ph.charriere@gmail.com44 email: ph.charriere@gmail.com
45 createdAt: 2026-11-07T05:28:39.000Z45 createdAt: 2026-11-07T05:28:39.000Z
46+ updatedAt: 2026-11-07T10:53:47.000Z
47+ closedAt: 2026-11-07T10:53:47.000Z
46 - id: 248 - id: 2
47 title: Update SHA256SUMS when the signed binary is uploaded49 title: Update SHA256SUMS when the signed binary is uploaded
48- state: open50+ state: closed
49 priority: none51 priority: none
50 author:52 author:
51 name: k33g53 name: k33g
52 email: ph.charriere@gmail.com54 email: ph.charriere@gmail.com
53 createdAt: 2026-11-07T05:28:39.000Z55 createdAt: 2026-11-07T05:28:39.000Z
56+ updatedAt: 2026-11-07T10:53:47.000Z
57+ closedAt: 2026-11-07T10:53:47.000Z
54 - id: 358 - id: 3
55 title: Request a binary publishing API feature on Rickub59 title: Request a binary publishing API feature on Rickub
56 state: open60 state: open
@@ -69,6 +73,14 @@ tasks:
69 createdAt: 2026-11-07T05:46:16.000Z73 createdAt: 2026-11-07T05:46:16.000Z
70 updatedAt: 2026-11-07T08:07:23.000Z74 updatedAt: 2026-11-07T08:07:23.000Z
71 closedAt: 2026-11-07T08:07:23.000Z75 closedAt: 2026-11-07T08:07:23.000Z
76+ - id: 5
77+ title: Check the signature on a Mac (wasm under the hardened runtime, then a real release)
78+ state: open
79+ priority: high
80+ author:
81+ name: k33g
82+ email: ph.charriere@gmail.com
83+ createdAt: 2026-11-07T10:53:47.000Z
72 comments:84 comments:
73 - id: 185 - id: 1
74 author:86 author:
@@ -89,3 +101,25 @@ comments:
89 - The binaries go through git on the temporary branch: 148 MB per run with all platforms.101 - The binaries go through git on the temporary branch: 148 MB per run with all platforms.
90 102
91 The feature request text is in `demo-rickub-feature-request.txt`.103 The feature request text is in `demo-rickub-feature-request.txt`.
104+ - id: 2
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-11-07T10:53:47.000Z
109+ body: |
110+ The signature now happens before publishing, not after: `local-ci-02-build.sh` signs and notarizes `mm-<version>-darwin-arm64` on a Mac when `MM_SIGN_IDENTITY` and `MM_NOTARY_PROFILE` are set (in `.release.env`, from the Sidekicks variables). `SHA256SUMS` is computed after the signature, and `local-ci-03-create-draft.sh` adds a line to the release notes. The procedure, the one-time settings and the manual fallback are in [RELEASING.macos.fr.md](RELEASING.macos.fr.md).
111+
112+ - Nothing to register at Apple: the Developer ID certificate and the notarytool profile belong to the team, not to an app.
113+ - A bare binary cannot be stapled: Gatekeeper fetches the ticket online on first run.
114+ - Tried on Linux with stand-in `codesign`, `xcrun`, `security` and `ditto`: accepted, Invalid, missing profile, not macOS, unknown identity, unsigned. Never run on a real Mac yet.
115+ - Risk to check first: wazero compiles wasm to machine code at run time, and the hardened runtime may block it (step A4 of the document).
116+ - id: 3
117+ author:
118+ name: k33g
119+ email: ph.charriere@gmail.com
120+ createdAt: 2026-11-07T11:16:07.000Z
121+ body: |
122+ Measured on the Mac with `check-wasm-hardened-runtime.sh` and the Developer ID: with the hardened runtime alone, macOS kills the wasmtool tests (`Killed: 9`) at the first wasm module. Without the hardened runtime, 11/11 pass; with the hardened runtime and `com.apple.security.cs.allow-unsigned-executable-memory`, 11/11 pass.
123+
124+ Decision: sign with that entitlement ([macos-entitlements.plist](macos-entitlements.plist)), rather than switching wazero to its interpreter on macOS. It is invisible to the user, at install and at first run, and wasm keeps its speed. `local-ci-02-build.sh` signs with it and checks that the signature carries it. Notarization with the entitlement is not tried yet.
125+
modified .tickets/issues/0198-gardening-the-root-of-the-repository.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 198
33 title: Gardening the root of the repository
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-11-07T08:05:54.088Z
9-updatedAt: 2026-11-07T08:05:54.088Z
9+updatedAt: 2026-11-07T10:51:12.648Z
1010 labels: []
1111 milestone: v1.0.5
1212 body: ""
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 1982 id: 198
3 title: Gardening the root of the repository3 title: Gardening the root of the repository
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-11-07T08:05:54.088Z8 createdAt: 2026-11-07T08:05:54.088Z
9-updatedAt: 2026-11-07T08:05:54.088Z9+updatedAt: 2026-11-07T10:51:12.648Z
10 labels: []10 labels: []
11 milestone: v1.0.511 milestone: v1.0.5
12 body: ""12 body: ""
added RELEASING.macos.en.md +216 -0
new file mode 100644
@@ -0,0 +1,216 @@
1+# Signing and notarizing the macOS binary of mm
2+
3+*[Version française](RELEASING.macos.fr.md)*
4+
5+This document explains how to publish `mm-<version>-darwin-arm64` signed with a **Developer ID** certificate and **notarized** by Apple, so that a Mac runs it without `xattr` or **Open Anyway**. No `.dmg`: the bare binary is signed, and published as is in the release.
6+
7+The signature happens in `./local-ci-02-build.sh`, on the Mac, as soon as two variables are set. The rest of the chain (`local-ci-03` to `05`) does not change.
8+
9+| Part | When | Time |
10+| --- | --- | --- |
11+| [A. Settings](#part-a--settings-once) | once per Mac | 5 minutes if Sidekicks' certificate and profile are on this Mac, otherwise 30 minutes |
12+| [B. Every release](#part-b--every-release) | for each version | a few minutes more than usual, mostly waiting for Apple |
13+| [C. By hand](#part-c--signing-by-hand) | when the script cannot do it | 10 minutes |
14+
15+## Is anything needed at Apple for a new application?
16+
17+**No.** Nothing has to be registered at Apple for mm:
18+
19+- The **Developer ID Application** certificate belongs to the team (Team ID), not to an application. Sidekicks' certificate signs mm too.
20+- The `notarytool` profile stores the account's credentials (Apple ID, app-specific password, Team ID). It notarizes any software of the team: its name, `sidekicks-notary`, is only a label in the keychain.
21+- An application distributed outside the App Store needs no App ID, no provisioning profile and no App Store Connect record.
22+- The binary's identifier, `com.bots-garden.mm`, is chosen by the script when it signs. It is registered nowhere at Apple.
23+
24+So the `SIDEKICKS_SIGN_IDENTITY` and `SIDEKICKS_NOTARY_PROFILE` variables you already export are enough. A profile dedicated to mm (`mm-notary`) is possible but not needed.
25+
26+## What differs from Sidekicks
27+
28+| | Sidekicks (an app) | mm (a binary) |
29+| --- | --- | --- |
30+| What is signed | `sidekicks.app`, then the `.dmg` | `mm-<version>-darwin-arm64` alone |
31+| Sent for notarization | a `.zip` of the app, then the `.dmg` | a `.zip` holding the binary (`notarytool` does not take a bare binary) |
32+| Stapling (`stapler`) | on the app and on the `.dmg` | **impossible** on a bare binary: on first launch, macOS fetches the ticket from Apple, online |
33+| `spctl` check | `accepted`, `Notarized Developer ID` | does not apply: `spctl` only assesses apps, `.dmg` and `.pkg` files. The `Accepted` status from `notarytool` is the proof |
34+| Identifier | `CFBundleIdentifier` in `Info.plist` | `--identifier com.bots-garden.mm` passed to `codesign` |
35+| Entitlements | none | `allow-unsigned-executable-memory`, for the wasm engine (see A4) |
36+
37+The notarized but unstapled binary also runs offline when it was downloaded with `curl` (as in `INSTALL.MACOS.md`), because `curl` does not set the quarantine attribute. The signature matters mostly for a browser download and for managed Macs (MDM).
38+
39+## Part A — Settings (once)
40+
41+### A1. Check the signing identity
42+
43+```bash
44+security find-identity -v -p codesigning
45+```
46+
47+A line `"Developer ID Application: First Last (AB12CD34EF)"` must appear, and it must be exactly the value of `$SIDEKICKS_SIGN_IDENTITY`:
48+
49+```bash
50+echo "$SIDEKICKS_SIGN_IDENTITY"
51+```
52+
53+If the line is missing, the certificate is not on this Mac:
54+
55+- **It already exists on another Mac** (Sidekicks' one): import its `.p12` backup (double-click, then its password) and run the command again.
56+- **It does not exist yet**: you must be the **Account Holder** of the Apple Developer Program.
57+ 1. Open **Xcode → Settings… → Accounts**, add your Apple ID if needed, select the team, then **Manage Certificates… → + → Developer ID Application**.
58+ 2. Back up the certificate with its key: **Keychain Access**, **login** keychain, **My Certificates**, select **Developer ID Application: …**, **File → Export Items…**, `.p12` format, with a password. Keep it out of the repository (password manager).
59+- **The line appears with `CSSMERR_TP_NOT_TRUSTED`**: install the **Developer ID - G2** intermediate certificate from [apple.com/certificateauthority](https://www.apple.com/certificateauthority/).
60+
61+### A2. Check the notarization profile
62+
63+```bash
64+xcrun notarytool history --keychain-profile "$SIDEKICKS_NOTARY_PROFILE"
65+```
66+
67+A list appears (Sidekicks' submissions), with no authentication error. If the profile is missing on this Mac:
68+
69+1. On [account.apple.com](https://account.apple.com), **Sign-In and Security → App-Specific Passwords**, create one (for example `mm-notary`) and copy it.
70+2. Store the profile in the keychain, with your Apple ID and your Team ID (10 characters, under **Membership details** on [developer.apple.com/account](https://developer.apple.com/account)), then paste the app-specific password when asked:
71+
72+ ```bash
73+ xcrun notarytool store-credentials mm-notary --apple-id "you@example.com" --team-id "AB12CD34EF"
74+ ```
75+
76+### A3. Wire the variables into `.release.env`
77+
78+The script reads `MM_SIGN_IDENTITY` and `MM_NOTARY_PROFILE`. Add to `.release.env` (git-ignored):
79+
80+```bash
81+# macOS signature (RELEASING.macos.en.md). Empty = the macOS binary is not signed.
82+MM_SIGN_IDENTITY="${SIDEKICKS_SIGN_IDENTITY:-}"
83+MM_NOTARY_PROFILE="${SIDEKICKS_NOTARY_PROFILE:-}"
84+```
85+
86+`.release.env` is read by bash: it picks up the values exported in your `~/.zshrc`. For a profile of mm's own, put its name directly: `MM_NOTARY_PROFILE="mm-notary"`.
87+
88+### A4. Check that wasm tools work in the signed binary
89+
90+Notarization requires the **hardened runtime** (`--options runtime`), which forbids a program to make executable the memory it wrote itself. But mm's wasm engine (wazero), used by the wasm tools and hooks, compiles wasm to machine code at run time.
91+
92+**Measured on 7 November 2026** on an Apple Silicon Mac, with the Developer ID: with the hardened runtime alone, macOS kills the process (`Killed: 9`) at the first wasm module. Without the hardened runtime, or with the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement, the 11 tests pass.
93+
94+So the release signs mm with this entitlement, declared in [`macos-entitlements.plist`](macos-entitlements.plist). It is invisible to the user (no dialog, nothing different at install) and keeps wasm as fast as on the other platforms. In return, the binary loses the hardened runtime's protection against code injected in memory, a protection that matters little for an agent that runs `bash` commands by design. The other way out, wazero's interpreter on macOS, keeps that protection but makes wasm slower and makes the Mac a special case in the code.
95+
96+To check that the entitlement is still enough (after a wazero or macOS update, for example):
97+
98+```bash
99+./check-wasm-hardened-runtime.sh
100+```
101+
102+The script builds the `internal/wasmtool` tests, signs them like the release (hardened runtime + `macos-entitlements.plist`, with `MM_SIGN_IDENTITY` when it is set, otherwise an ad hoc signature) and runs them.
103+
104+- `✅ wazero works with the release signature`: nothing to do.
105+- `❌`: it runs two counter-checks (without the hardened runtime, then with the hardened runtime and no entitlement) and says whether the hardened runtime is the cause. Do not publish a signed release before fixing it.
106+
107+## Part B — Every release
108+
109+First: if the repository sits in a synced folder (kDrive, iCloud Drive…), **pause the sync** during step 02. The script signs in a temporary folder and checks the copy with `cmp`, but pausing keeps a sync client from rewriting the binary between signing and upload.
110+
111+### B1. Build, sign and notarize
112+
113+```bash
114+./local-ci-01-check.sh
115+./local-ci-02-build.sh
116+```
117+
118+With `MM_SIGN_IDENTITY` set, `local-ci-02-build.sh`:
119+
120+1. **Checks the setup first**, before building: it must run on macOS, have `MM_NOTARY_PROFILE`, find the identity in the keychain, be able to use the `notarytool` profile, and find a valid `macos-entitlements.plist`. Otherwise it stops and prints the command to run to find out why.
121+2. Builds the five platforms, as usual.
122+3. Copies `mm-<version>-darwin-arm64` to a temporary folder, signs it (`codesign --force --options runtime --timestamp --identifier com.bots-garden.mm --entitlements macos-entitlements.plist`) and checks that the signature carries the **Developer ID Application** authority, the **runtime** flag, a **Timestamp** and the `allow-unsigned-executable-memory` entitlement.
123+4. Sends it to Apple in a `.zip` (`xcrun notarytool submit --wait`) and waits for the **Accepted** status (a few minutes).
124+5. Puts the signed binary back in `release/<TAG>/` and checks the copy with `cmp`.
125+6. Runs that binary (`-h`, then `--version`): on an Apple Silicon Mac, the signed binary is the one tried.
126+7. Computes `SHA256SUMS` **after** signing: the checksums describe the signed binary.
127+
128+You should see:
129+
130+```
131+🔏 Signing and notarizing mm-1.0.5-darwin-arm64...
132+ ✅ mm-1.0.5-darwin-arm64 signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)
133+ ⏳ waiting for Apple's notary service (a few minutes)...
134+ ✅ mm-1.0.5-darwin-arm64 notarized (submission 1a2b3c4d-…)
135+ ✅ mm-1.0.5-darwin-arm64 starts and reports v1.0.5
136+```
137+
138+`release/<TAG>/macos-signature.txt` records the signature (identifier, entitlement, authority, Team ID, notarization submission). This file is not published. The folder's `README.md` then says that the macOS binary is signed, instead of giving the `xattr` command.
139+
140+Without `MM_SIGN_IDENTITY`, nothing changes: the macOS binary keeps the ad hoc signature Go gives it, and the script prints `⚠️ mm-<version>-darwin-arm64 is not signed`.
141+
142+### B2. Create the draft, upload, publish
143+
144+```bash
145+./local-ci-03-create-draft.sh
146+./local-ci-04-upload.sh
147+./local-ci-05-publish.sh
148+```
149+
150+`local-ci-03-create-draft.sh` sees `macos-signature.txt` and adds the line `macOS binary signed with a Developer ID and notarized by Apple` to the release notes. `local-ci-05-publish.sh` downloads every published file and compares it with `release/<TAG>/`: so the binary online is indeed the signed one.
151+
152+### B3. Try it on a Mac
153+
154+1. Download `mm-<version>-darwin-arm64` from the release page, **with a browser**: the file then carries the quarantine attribute of a real download.
155+2. In the Terminal:
156+
157+ ```bash
158+ cd ~/Downloads
159+ xattr -l mm-<version>-darwin-arm64 # com.apple.quarantine must appear
160+ chmod +x mm-<version>-darwin-arm64
161+ ./mm-<version>-darwin-arm64 --version # runs, no blocking dialog
162+ codesign -dv --verbose=4 mm-<version>-darwin-arm64 2>&1 | grep -E "Identifier|Authority=Developer ID Application|TeamIdentifier"
163+ ```
164+
165+3. Run a demo that uses a wasm tool (`demos/wasm-tools`) with this binary: without the entitlement, that is where macOS would kill it.
166+
167+## Part C — Signing by hand
168+
169+If `local-ci-02-build.sh` ran without the variables (or on another machine), you can sign afterwards, **before** `local-ci-03-create-draft.sh`: the release notes hold the checksums. From the root of the repository, with `TAG` set:
170+
171+```bash
172+source .release.env
173+V="${TAG#v}"
174+W="$(mktemp -d)"
175+cp "release/$TAG/mm-$V-darwin-arm64" "$W/"
176+xattr -c "$W/mm-$V-darwin-arm64"
177+codesign --force --options runtime --timestamp --identifier com.bots-garden.mm \
178+ --entitlements macos-entitlements.plist \
179+ --sign "$MM_SIGN_IDENTITY" "$W/mm-$V-darwin-arm64"
180+codesign --verify --strict --verbose=2 "$W/mm-$V-darwin-arm64"
181+ditto -c -k --keepParent "$W/mm-$V-darwin-arm64" "$W/notarize.zip"
182+xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$MM_NOTARY_PROFILE" --wait
183+```
184+
185+If `notarytool` ends with `status: Accepted`, put the binary back and compute the checksums again:
186+
187+```bash
188+cp "$W/mm-$V-darwin-arm64" "release/$TAG/"
189+(cd "release/$TAG" && shasum -a 256 mm-"$V"-* > SHA256SUMS)
190+cat "release/$TAG/SHA256SUMS"
191+```
192+
193+Then go on with `local-ci-03-create-draft.sh` (without `macos-signature.txt`, the notes will not have the signature line: add it by hand on the release page if you want it).
194+
195+**A release already published** cannot take a signed binary in place of the old one: Rickub refuses a file whose name already exists (422), and no route deletes one. Publish a new version instead.
196+
197+## Troubleshooting
198+
199+| Symptom | What to do |
200+| --- | --- |
201+| `MM_SIGN_IDENTITY is set, but signing needs macOS` | Signing happens on the Mac. Elsewhere, empty `MM_SIGN_IDENTITY` (or run on the Mac). |
202+| `MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not` | Add `MM_NOTARY_PROFILE` to `.release.env` (A3). A binary signed but not notarized is still blocked by Gatekeeper. |
203+| `no valid identity "…" in the keychain` | `security find-identity -v -p codesigning`: copy the exact value. If nothing appears, see A1. |
204+| `the notarytool profile "…" does not work` | `xcrun notarytool history --keychain-profile "…"` gives the error. A revoked app-specific password is made again (A2). |
205+| `notarization …: Invalid` | `xcrun notarytool log <id> --keychain-profile "$MM_NOTARY_PROFILE"` (the command is printed) gives the reason. |
206+| `notarytool submit failed` | Usually the network, or terms to accept on [developer.apple.com](https://developer.apple.com/account) after a program update. |
207+| `differs from the signed copy (a synced folder?)` | Pause the sync and run `local-ci-02-build.sh` again. |
208+| `codesign` refuses ("resource fork, Finder information, or similar detritus") | Metadata added by a synced folder. The script removes it with `xattr -c`; by hand, do it too (part C). |
209+| The signed binary starts, but is killed (`Killed: 9`) at the first wasm tool or hook | It was signed without `macos-entitlements.plist`: `codesign -d --entitlements - <binary>` must show `allow-unsigned-executable-memory`. Otherwise, see A4. |
210+| `macos-entitlements.plist is missing or is not a valid plist` | The file must be at the root of the repository; `plutil -lint macos-entitlements.plist` gives the error. |
211+
212+## See also
213+
214+- Sidekicks' signing guide (`docs/en/how-to/sign-and-notarize-on-macos.md` in its repository), whose settings this document reuses
215+- `local-ci-00-common.sh`: the list of the `local-ci-*` steps
216+- Apple: [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
new file mode 100644
@@ -0,0 +1,216 @@
1+# Signing and notarizing the macOS binary of mm
2+
3+*[Version française](RELEASING.macos.fr.md)*
4+
5+This document explains how to publish `mm-<version>-darwin-arm64` signed with a **Developer ID** certificate and **notarized** by Apple, so that a Mac runs it without `xattr` or **Open Anyway**. No `.dmg`: the bare binary is signed, and published as is in the release.
6+
7+The signature happens in `./local-ci-02-build.sh`, on the Mac, as soon as two variables are set. The rest of the chain (`local-ci-03` to `05`) does not change.
8+
9+| Part | When | Time |
10+| --- | --- | --- |
11+| [A. Settings](#part-a--settings-once) | once per Mac | 5 minutes if Sidekicks' certificate and profile are on this Mac, otherwise 30 minutes |
12+| [B. Every release](#part-b--every-release) | for each version | a few minutes more than usual, mostly waiting for Apple |
13+| [C. By hand](#part-c--signing-by-hand) | when the script cannot do it | 10 minutes |
14+
15+## Is anything needed at Apple for a new application?
16+
17+**No.** Nothing has to be registered at Apple for mm:
18+
19+- The **Developer ID Application** certificate belongs to the team (Team ID), not to an application. Sidekicks' certificate signs mm too.
20+- The `notarytool` profile stores the account's credentials (Apple ID, app-specific password, Team ID). It notarizes any software of the team: its name, `sidekicks-notary`, is only a label in the keychain.
21+- An application distributed outside the App Store needs no App ID, no provisioning profile and no App Store Connect record.
22+- The binary's identifier, `com.bots-garden.mm`, is chosen by the script when it signs. It is registered nowhere at Apple.
23+
24+So the `SIDEKICKS_SIGN_IDENTITY` and `SIDEKICKS_NOTARY_PROFILE` variables you already export are enough. A profile dedicated to mm (`mm-notary`) is possible but not needed.
25+
26+## What differs from Sidekicks
27+
28+| | Sidekicks (an app) | mm (a binary) |
29+| --- | --- | --- |
30+| What is signed | `sidekicks.app`, then the `.dmg` | `mm-<version>-darwin-arm64` alone |
31+| Sent for notarization | a `.zip` of the app, then the `.dmg` | a `.zip` holding the binary (`notarytool` does not take a bare binary) |
32+| Stapling (`stapler`) | on the app and on the `.dmg` | **impossible** on a bare binary: on first launch, macOS fetches the ticket from Apple, online |
33+| `spctl` check | `accepted`, `Notarized Developer ID` | does not apply: `spctl` only assesses apps, `.dmg` and `.pkg` files. The `Accepted` status from `notarytool` is the proof |
34+| Identifier | `CFBundleIdentifier` in `Info.plist` | `--identifier com.bots-garden.mm` passed to `codesign` |
35+| Entitlements | none | `allow-unsigned-executable-memory`, for the wasm engine (see A4) |
36+
37+The notarized but unstapled binary also runs offline when it was downloaded with `curl` (as in `INSTALL.MACOS.md`), because `curl` does not set the quarantine attribute. The signature matters mostly for a browser download and for managed Macs (MDM).
38+
39+## Part A — Settings (once)
40+
41+### A1. Check the signing identity
42+
43+```bash
44+security find-identity -v -p codesigning
45+```
46+
47+A line `"Developer ID Application: First Last (AB12CD34EF)"` must appear, and it must be exactly the value of `$SIDEKICKS_SIGN_IDENTITY`:
48+
49+```bash
50+echo "$SIDEKICKS_SIGN_IDENTITY"
51+```
52+
53+If the line is missing, the certificate is not on this Mac:
54+
55+- **It already exists on another Mac** (Sidekicks' one): import its `.p12` backup (double-click, then its password) and run the command again.
56+- **It does not exist yet**: you must be the **Account Holder** of the Apple Developer Program.
57+ 1. Open **Xcode → Settings… → Accounts**, add your Apple ID if needed, select the team, then **Manage Certificates… → + → Developer ID Application**.
58+ 2. Back up the certificate with its key: **Keychain Access**, **login** keychain, **My Certificates**, select **Developer ID Application: …**, **File → Export Items…**, `.p12` format, with a password. Keep it out of the repository (password manager).
59+- **The line appears with `CSSMERR_TP_NOT_TRUSTED`**: install the **Developer ID - G2** intermediate certificate from [apple.com/certificateauthority](https://www.apple.com/certificateauthority/).
60+
61+### A2. Check the notarization profile
62+
63+```bash
64+xcrun notarytool history --keychain-profile "$SIDEKICKS_NOTARY_PROFILE"
65+```
66+
67+A list appears (Sidekicks' submissions), with no authentication error. If the profile is missing on this Mac:
68+
69+1. On [account.apple.com](https://account.apple.com), **Sign-In and Security → App-Specific Passwords**, create one (for example `mm-notary`) and copy it.
70+2. Store the profile in the keychain, with your Apple ID and your Team ID (10 characters, under **Membership details** on [developer.apple.com/account](https://developer.apple.com/account)), then paste the app-specific password when asked:
71+
72+ ```bash
73+ xcrun notarytool store-credentials mm-notary --apple-id "you@example.com" --team-id "AB12CD34EF"
74+ ```
75+
76+### A3. Wire the variables into `.release.env`
77+
78+The script reads `MM_SIGN_IDENTITY` and `MM_NOTARY_PROFILE`. Add to `.release.env` (git-ignored):
79+
80+```bash
81+# macOS signature (RELEASING.macos.en.md). Empty = the macOS binary is not signed.
82+MM_SIGN_IDENTITY="${SIDEKICKS_SIGN_IDENTITY:-}"
83+MM_NOTARY_PROFILE="${SIDEKICKS_NOTARY_PROFILE:-}"
84+```
85+
86+`.release.env` is read by bash: it picks up the values exported in your `~/.zshrc`. For a profile of mm's own, put its name directly: `MM_NOTARY_PROFILE="mm-notary"`.
87+
88+### A4. Check that wasm tools work in the signed binary
89+
90+Notarization requires the **hardened runtime** (`--options runtime`), which forbids a program to make executable the memory it wrote itself. But mm's wasm engine (wazero), used by the wasm tools and hooks, compiles wasm to machine code at run time.
91+
92+**Measured on 7 November 2026** on an Apple Silicon Mac, with the Developer ID: with the hardened runtime alone, macOS kills the process (`Killed: 9`) at the first wasm module. Without the hardened runtime, or with the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement, the 11 tests pass.
93+
94+So the release signs mm with this entitlement, declared in [`macos-entitlements.plist`](macos-entitlements.plist). It is invisible to the user (no dialog, nothing different at install) and keeps wasm as fast as on the other platforms. In return, the binary loses the hardened runtime's protection against code injected in memory, a protection that matters little for an agent that runs `bash` commands by design. The other way out, wazero's interpreter on macOS, keeps that protection but makes wasm slower and makes the Mac a special case in the code.
95+
96+To check that the entitlement is still enough (after a wazero or macOS update, for example):
97+
98+```bash
99+./check-wasm-hardened-runtime.sh
100+```
101+
102+The script builds the `internal/wasmtool` tests, signs them like the release (hardened runtime + `macos-entitlements.plist`, with `MM_SIGN_IDENTITY` when it is set, otherwise an ad hoc signature) and runs them.
103+
104+- `✅ wazero works with the release signature`: nothing to do.
105+- `❌`: it runs two counter-checks (without the hardened runtime, then with the hardened runtime and no entitlement) and says whether the hardened runtime is the cause. Do not publish a signed release before fixing it.
106+
107+## Part B — Every release
108+
109+First: if the repository sits in a synced folder (kDrive, iCloud Drive…), **pause the sync** during step 02. The script signs in a temporary folder and checks the copy with `cmp`, but pausing keeps a sync client from rewriting the binary between signing and upload.
110+
111+### B1. Build, sign and notarize
112+
113+```bash
114+./local-ci-01-check.sh
115+./local-ci-02-build.sh
116+```
117+
118+With `MM_SIGN_IDENTITY` set, `local-ci-02-build.sh`:
119+
120+1. **Checks the setup first**, before building: it must run on macOS, have `MM_NOTARY_PROFILE`, find the identity in the keychain, be able to use the `notarytool` profile, and find a valid `macos-entitlements.plist`. Otherwise it stops and prints the command to run to find out why.
121+2. Builds the five platforms, as usual.
122+3. Copies `mm-<version>-darwin-arm64` to a temporary folder, signs it (`codesign --force --options runtime --timestamp --identifier com.bots-garden.mm --entitlements macos-entitlements.plist`) and checks that the signature carries the **Developer ID Application** authority, the **runtime** flag, a **Timestamp** and the `allow-unsigned-executable-memory` entitlement.
123+4. Sends it to Apple in a `.zip` (`xcrun notarytool submit --wait`) and waits for the **Accepted** status (a few minutes).
124+5. Puts the signed binary back in `release/<TAG>/` and checks the copy with `cmp`.
125+6. Runs that binary (`-h`, then `--version`): on an Apple Silicon Mac, the signed binary is the one tried.
126+7. Computes `SHA256SUMS` **after** signing: the checksums describe the signed binary.
127+
128+You should see:
129+
130+```
131+🔏 Signing and notarizing mm-1.0.5-darwin-arm64...
132+ ✅ mm-1.0.5-darwin-arm64 signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)
133+ ⏳ waiting for Apple's notary service (a few minutes)...
134+ ✅ mm-1.0.5-darwin-arm64 notarized (submission 1a2b3c4d-…)
135+ ✅ mm-1.0.5-darwin-arm64 starts and reports v1.0.5
136+```
137+
138+`release/<TAG>/macos-signature.txt` records the signature (identifier, entitlement, authority, Team ID, notarization submission). This file is not published. The folder's `README.md` then says that the macOS binary is signed, instead of giving the `xattr` command.
139+
140+Without `MM_SIGN_IDENTITY`, nothing changes: the macOS binary keeps the ad hoc signature Go gives it, and the script prints `⚠️ mm-<version>-darwin-arm64 is not signed`.
141+
142+### B2. Create the draft, upload, publish
143+
144+```bash
145+./local-ci-03-create-draft.sh
146+./local-ci-04-upload.sh
147+./local-ci-05-publish.sh
148+```
149+
150+`local-ci-03-create-draft.sh` sees `macos-signature.txt` and adds the line `macOS binary signed with a Developer ID and notarized by Apple` to the release notes. `local-ci-05-publish.sh` downloads every published file and compares it with `release/<TAG>/`: so the binary online is indeed the signed one.
151+
152+### B3. Try it on a Mac
153+
154+1. Download `mm-<version>-darwin-arm64` from the release page, **with a browser**: the file then carries the quarantine attribute of a real download.
155+2. In the Terminal:
156+
157+ ```bash
158+ cd ~/Downloads
159+ xattr -l mm-<version>-darwin-arm64 # com.apple.quarantine must appear
160+ chmod +x mm-<version>-darwin-arm64
161+ ./mm-<version>-darwin-arm64 --version # runs, no blocking dialog
162+ codesign -dv --verbose=4 mm-<version>-darwin-arm64 2>&1 | grep -E "Identifier|Authority=Developer ID Application|TeamIdentifier"
163+ ```
164+
165+3. Run a demo that uses a wasm tool (`demos/wasm-tools`) with this binary: without the entitlement, that is where macOS would kill it.
166+
167+## Part C — Signing by hand
168+
169+If `local-ci-02-build.sh` ran without the variables (or on another machine), you can sign afterwards, **before** `local-ci-03-create-draft.sh`: the release notes hold the checksums. From the root of the repository, with `TAG` set:
170+
171+```bash
172+source .release.env
173+V="${TAG#v}"
174+W="$(mktemp -d)"
175+cp "release/$TAG/mm-$V-darwin-arm64" "$W/"
176+xattr -c "$W/mm-$V-darwin-arm64"
177+codesign --force --options runtime --timestamp --identifier com.bots-garden.mm \
178+ --entitlements macos-entitlements.plist \
179+ --sign "$MM_SIGN_IDENTITY" "$W/mm-$V-darwin-arm64"
180+codesign --verify --strict --verbose=2 "$W/mm-$V-darwin-arm64"
181+ditto -c -k --keepParent "$W/mm-$V-darwin-arm64" "$W/notarize.zip"
182+xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$MM_NOTARY_PROFILE" --wait
183+```
184+
185+If `notarytool` ends with `status: Accepted`, put the binary back and compute the checksums again:
186+
187+```bash
188+cp "$W/mm-$V-darwin-arm64" "release/$TAG/"
189+(cd "release/$TAG" && shasum -a 256 mm-"$V"-* > SHA256SUMS)
190+cat "release/$TAG/SHA256SUMS"
191+```
192+
193+Then go on with `local-ci-03-create-draft.sh` (without `macos-signature.txt`, the notes will not have the signature line: add it by hand on the release page if you want it).
194+
195+**A release already published** cannot take a signed binary in place of the old one: Rickub refuses a file whose name already exists (422), and no route deletes one. Publish a new version instead.
196+
197+## Troubleshooting
198+
199+| Symptom | What to do |
200+| --- | --- |
201+| `MM_SIGN_IDENTITY is set, but signing needs macOS` | Signing happens on the Mac. Elsewhere, empty `MM_SIGN_IDENTITY` (or run on the Mac). |
202+| `MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not` | Add `MM_NOTARY_PROFILE` to `.release.env` (A3). A binary signed but not notarized is still blocked by Gatekeeper. |
203+| `no valid identity "…" in the keychain` | `security find-identity -v -p codesigning`: copy the exact value. If nothing appears, see A1. |
204+| `the notarytool profile "…" does not work` | `xcrun notarytool history --keychain-profile "…"` gives the error. A revoked app-specific password is made again (A2). |
205+| `notarization …: Invalid` | `xcrun notarytool log <id> --keychain-profile "$MM_NOTARY_PROFILE"` (the command is printed) gives the reason. |
206+| `notarytool submit failed` | Usually the network, or terms to accept on [developer.apple.com](https://developer.apple.com/account) after a program update. |
207+| `differs from the signed copy (a synced folder?)` | Pause the sync and run `local-ci-02-build.sh` again. |
208+| `codesign` refuses ("resource fork, Finder information, or similar detritus") | Metadata added by a synced folder. The script removes it with `xattr -c`; by hand, do it too (part C). |
209+| The signed binary starts, but is killed (`Killed: 9`) at the first wasm tool or hook | It was signed without `macos-entitlements.plist`: `codesign -d --entitlements - <binary>` must show `allow-unsigned-executable-memory`. Otherwise, see A4. |
210+| `macos-entitlements.plist is missing or is not a valid plist` | The file must be at the root of the repository; `plutil -lint macos-entitlements.plist` gives the error. |
211+
212+## See also
213+
214+- Sidekicks' signing guide (`docs/en/how-to/sign-and-notarize-on-macos.md` in its repository), whose settings this document reuses
215+- `local-ci-00-common.sh`: the list of the `local-ci-*` steps
216+- Apple: [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
added RELEASING.macos.fr.md +216 -0
new file mode 100644
@@ -0,0 +1,216 @@
1+# Signer et notariser le binaire macOS de mm
2+
3+*[English version](RELEASING.macos.en.md)*
4+
5+Ce document explique comment publier `mm-<version>-darwin-arm64` signé avec un certificat **Developer ID** et **notarisé** par Apple, pour qu'un Mac le lance sans passer par `xattr` ni par **Ouvrir quand même**. Pas de `.dmg` : on signe le binaire seul, qui est publié tel quel dans la release.
6+
7+La signature se fait dans `./local-ci-02-build.sh`, sur le Mac, dès que deux variables sont définies. Le reste de la chaîne (`local-ci-03` à `05`) ne change pas.
8+
9+| Partie | Quand | Durée |
10+| --- | --- | --- |
11+| [A. Réglages](#partie-a--réglages-une-seule-fois) | une seule fois par Mac | 5 minutes si le certificat et le profil de Sidekicks sont sur ce Mac, sinon 30 minutes |
12+| [B. À chaque release](#partie-b--à-chaque-release) | à chaque version | quelques minutes de plus que d'habitude, surtout l'attente chez Apple |
13+| [C. À la main](#partie-c--signer-à-la-main) | si le script ne peut pas le faire | 10 minutes |
14+
15+## Faut-il faire quelque chose chez Apple pour une nouvelle application ?
16+
17+**Non.** Rien n'est à déclarer chez Apple pour mm :
18+
19+- Le certificat **Developer ID Application** appartient à l'équipe (Team ID), pas à une application. Celui de Sidekicks signe aussi mm.
20+- Le profil `notarytool` enregistre les identifiants du compte (Apple ID, mot de passe d'application, Team ID). Il notarise n'importe quel logiciel de l'équipe : son nom, `sidekicks-notary`, n'est qu'une étiquette dans le trousseau.
21+- Une application distribuée hors de l'App Store n'a besoin ni d'un App ID, ni d'un profil de provisionnement, ni d'une fiche dans App Store Connect.
22+- L'identifiant du binaire, `com.bots-garden.mm`, est choisi par le script au moment de signer. Il n'est enregistré nulle part chez Apple.
23+
24+Les variables `SIDEKICKS_SIGN_IDENTITY` et `SIDEKICKS_NOTARY_PROFILE` que vous avez déjà exportées suffisent donc. Un profil dédié à mm (`mm-notary`) est possible mais pas nécessaire.
25+
26+## Ce qui change par rapport à Sidekicks
27+
28+| | Sidekicks (une app) | mm (un binaire) |
29+| --- | --- | --- |
30+| Ce qui est signé | `sidekicks.app` puis le `.dmg` | `mm-<version>-darwin-arm64` seul |
31+| Envoi à la notarisation | un `.zip` de l'app, puis le `.dmg` | un `.zip` qui contient le binaire (`notarytool` ne prend pas un binaire nu) |
32+| Agrafage (`stapler`) | sur l'app et sur le `.dmg` | **impossible** sur un binaire nu : au premier lancement, macOS va chercher le ticket chez Apple, en ligne |
33+| Vérification `spctl` | `accepted`, `Notarized Developer ID` | ne s'applique pas : `spctl` n'évalue que les apps, les `.dmg` et les `.pkg`. Le statut `Accepted` de `notarytool` sert de preuve |
34+| Identifiant | `CFBundleIdentifier` dans `Info.plist` | `--identifier com.bots-garden.mm` passé à `codesign` |
35+| Autorisations (entitlements) | aucune | `allow-unsigned-executable-memory`, pour le moteur wasm (voir A4) |
36+
37+Le binaire notarisé mais non agrafé se lance aussi hors ligne quand il a été téléchargé avec `curl` (comme dans `INSTALL.MACOS.md`), car `curl` ne pose pas l'attribut de quarantaine. La signature compte surtout pour un téléchargement par navigateur et pour les Mac gérés (MDM).
38+
39+## Partie A — Réglages (une seule fois)
40+
41+### A1. Vérifier l'identité de signature
42+
43+```bash
44+security find-identity -v -p codesigning
45+```
46+
47+Une ligne `"Developer ID Application: Prénom Nom (AB12CD34EF)"` doit apparaître, et elle doit être exactement la valeur de `$SIDEKICKS_SIGN_IDENTITY` :
48+
49+```bash
50+echo "$SIDEKICKS_SIGN_IDENTITY"
51+```
52+
53+Si la ligne n'apparaît pas, le certificat n'est pas sur ce Mac :
54+
55+- **Il existe déjà sur un autre Mac** (celui de Sidekicks) : importez sa sauvegarde `.p12` (double-clic, puis son mot de passe) et relancez la commande.
56+- **Il n'existe pas encore** : il faut être **titulaire du compte** (Account Holder) de l'Apple Developer Program.
57+ 1. Ouvrez **Xcode → Settings… → Accounts**, ajoutez votre Apple ID si besoin, sélectionnez l'équipe, puis **Manage Certificates… → + → Developer ID Application**.
58+ 2. Sauvegardez le certificat avec sa clé : **Trousseaux d'accès**, trousseau **session**, **Mes certificats**, sélectionnez **Developer ID Application: …**, **Fichier → Exporter les éléments…**, format `.p12`, avec un mot de passe. Rangez-le hors du dépôt (gestionnaire de mots de passe).
59+- **La ligne apparaît avec `CSSMERR_TP_NOT_TRUSTED`** : installez le certificat intermédiaire **Developer ID - G2** depuis [apple.com/certificateauthority](https://www.apple.com/certificateauthority/).
60+
61+### A2. Vérifier le profil de notarisation
62+
63+```bash
64+xcrun notarytool history --keychain-profile "$SIDEKICKS_NOTARY_PROFILE"
65+```
66+
67+Une liste s'affiche (celle des envois de Sidekicks), sans erreur d'authentification. Si le profil manque sur ce Mac :
68+
69+1. Sur [account.apple.com](https://account.apple.com), **Connexion et sécurité → Mots de passe pour les apps**, créez-en un (par exemple `mm-notary`) et copiez-le.
70+2. Enregistrez le profil dans le trousseau, avec votre Apple ID et votre Team ID (10 caractères, dans **Membership details** sur [developer.apple.com/account](https://developer.apple.com/account)), puis collez le mot de passe d'application quand il est demandé :
71+
72+ ```bash
73+ xcrun notarytool store-credentials mm-notary --apple-id "vous@example.com" --team-id "AB12CD34EF"
74+ ```
75+
76+### A3. Brancher les variables dans `.release.env`
77+
78+Le script lit `MM_SIGN_IDENTITY` et `MM_NOTARY_PROFILE`. Ajoutez à `.release.env` (ignoré par git) :
79+
80+```bash
81+# Signature macOS (RELEASING.macos.fr.md). Vides = binaire macOS non signé.
82+MM_SIGN_IDENTITY="${SIDEKICKS_SIGN_IDENTITY:-}"
83+MM_NOTARY_PROFILE="${SIDEKICKS_NOTARY_PROFILE:-}"
84+```
85+
86+`.release.env` est lu par bash : il reprend les valeurs exportées dans votre `~/.zshrc`. Pour un profil propre à mm, mettez directement son nom : `MM_NOTARY_PROFILE="mm-notary"`.
87+
88+### A4. Vérifier que les outils wasm marchent dans le binaire signé
89+
90+La notarisation exige le **runtime renforcé** (`--options runtime`), qui interdit à un programme de rendre exécutable de la mémoire qu'il a écrite lui-même. Or le moteur wasm de mm (wazero), utilisé par les outils et les hooks wasm, compile le wasm en code machine pendant l'exécution.
91+
92+**Mesuré le 7 novembre 2026** sur un Mac Apple Silicon, avec le Developer ID : avec le seul runtime renforcé, macOS tue le processus (`Killed: 9`) au premier module wasm. Sans runtime renforcé, ou avec l'autorisation (entitlement) `com.apple.security.cs.allow-unsigned-executable-memory`, les 11 tests passent.
93+
94+La release signe donc mm avec cette autorisation, déclarée dans [`macos-entitlements.plist`](macos-entitlements.plist). Elle est invisible pour l'utilisateur (aucune fenêtre, rien de changé à l'installation) et laisse wasm aussi rapide que sur les autres plateformes. En contrepartie, le binaire perd la protection du runtime renforcé contre l'injection de code en mémoire, une protection qui compte peu pour un agent qui lance des commandes `bash` par conception. L'autre solution, l'interpréteur de wazero sur macOS, garde cette protection mais rend wasm plus lent et fait du Mac un cas à part dans le code.
95+
96+Pour vérifier que l'autorisation suffit toujours (après une mise à jour de wazero ou de macOS, par exemple) :
97+
98+```bash
99+./check-wasm-hardened-runtime.sh
100+```
101+
102+Le script compile les tests de `internal/wasmtool`, les signe comme la release (runtime renforcé + `macos-entitlements.plist`, avec `MM_SIGN_IDENTITY` s'il est défini, sinon une signature ad hoc) et les lance.
103+
104+- `✅ wazero works with the release signature` : rien à faire.
105+- `❌` : il fait deux contre-essais (sans runtime renforcé, puis avec le runtime renforcé sans l'autorisation) et dit si le runtime renforcé est en cause. Ne publiez pas de release signée avant d'avoir corrigé.
106+
107+## Partie B — À chaque release
108+
109+Avant tout : si le dépôt est dans un dossier synchronisé (kDrive, iCloud Drive…), **mettez la synchronisation en pause** pendant l'étape 02. Le script signe dans un dossier temporaire et vérifie la copie avec `cmp`, mais la pause évite qu'un client de synchronisation réécrive le binaire entre la signature et l'envoi.
110+
111+### B1. Construire, signer et notariser
112+
113+```bash
114+./local-ci-01-check.sh
115+./local-ci-02-build.sh
116+```
117+
118+Avec `MM_SIGN_IDENTITY` défini, `local-ci-02-build.sh` :
119+
120+1. **Vérifie d'abord la configuration**, avant de compiler : il faut être sur macOS, avoir `MM_NOTARY_PROFILE`, trouver l'identité dans le trousseau, pouvoir utiliser le profil `notarytool` et trouver un `macos-entitlements.plist` valide. Sinon il s'arrête avec la commande à lancer pour comprendre.
121+2. Compile les cinq plateformes, comme d'habitude.
122+3. Copie `mm-<version>-darwin-arm64` dans un dossier temporaire, le signe (`codesign --force --options runtime --timestamp --identifier com.bots-garden.mm --entitlements macos-entitlements.plist`) et vérifie que la signature porte l'autorité **Developer ID Application**, le drapeau **runtime**, un **Timestamp** et l'autorisation `allow-unsigned-executable-memory`.
123+4. L'envoie à Apple dans un `.zip` (`xcrun notarytool submit --wait`) et attend le statut **Accepted** (quelques minutes).
124+5. Remet le binaire signé dans `release/<TAG>/` et vérifie la copie avec `cmp`.
125+6. Lance ce binaire (`-h`, puis `--version`) : sur un Mac Apple Silicon, c'est le binaire signé qui est essayé.
126+7. Calcule `SHA256SUMS` **après** la signature : les sommes décrivent le binaire signé.
127+
128+Vous devez voir :
129+
130+```
131+🔏 Signing and notarizing mm-1.0.5-darwin-arm64...
132+ ✅ mm-1.0.5-darwin-arm64 signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)
133+ ⏳ waiting for Apple's notary service (a few minutes)...
134+ ✅ mm-1.0.5-darwin-arm64 notarized (submission 1a2b3c4d-…)
135+ ✅ mm-1.0.5-darwin-arm64 starts and reports v1.0.5
136+```
137+
138+`release/<TAG>/macos-signature.txt` garde la trace de la signature (identifiant, autorisation, autorité, Team ID, numéro de l'envoi à la notarisation). Ce fichier n'est pas publié. Le `README.md` du dossier dit alors que le binaire macOS est signé, au lieu de donner la commande `xattr`.
139+
140+Sans `MM_SIGN_IDENTITY`, rien ne change : le binaire macOS garde la signature ad hoc que lui donne Go, et le script affiche `⚠️ mm-<version>-darwin-arm64 is not signed`.
141+
142+### B2. Créer le brouillon, envoyer, publier
143+
144+```bash
145+./local-ci-03-create-draft.sh
146+./local-ci-04-upload.sh
147+./local-ci-05-publish.sh
148+```
149+
150+`local-ci-03-create-draft.sh` voit `macos-signature.txt` et ajoute aux notes de la release la ligne `macOS binary signed with a Developer ID and notarized by Apple`. `local-ci-05-publish.sh` télécharge chaque fichier publié et le compare à `release/<TAG>/` : le binaire en ligne est donc bien le binaire signé.
151+
152+### B3. Essayer sur un Mac
153+
154+1. Téléchargez `mm-<version>-darwin-arm64` depuis la page de la release, **avec un navigateur** : le fichier porte alors l'attribut de quarantaine d'un vrai téléchargement.
155+2. Dans le Terminal :
156+
157+ ```bash
158+ cd ~/Downloads
159+ xattr -l mm-<version>-darwin-arm64 # com.apple.quarantine doit apparaître
160+ chmod +x mm-<version>-darwin-arm64
161+ ./mm-<version>-darwin-arm64 --version # se lance, sans fenêtre de blocage
162+ codesign -dv --verbose=4 mm-<version>-darwin-arm64 2>&1 | grep -E "Identifier|Authority=Developer ID Application|TeamIdentifier"
163+ ```
164+
165+3. Lancez une démo qui utilise un outil wasm (`demos/wasm-tools`) avec ce binaire : sans l'autorisation, c'est là que macOS le tuerait.
166+
167+## Partie C — Signer à la main
168+
169+Si `local-ci-02-build.sh` a été lancé sans les variables (ou sur une autre machine), vous pouvez signer après coup, **avant** `local-ci-03-create-draft.sh` : les notes de la release contiennent les sommes. Depuis la racine du dépôt, avec `TAG` réglé :
170+
171+```bash
172+source .release.env
173+V="${TAG#v}"
174+W="$(mktemp -d)"
175+cp "release/$TAG/mm-$V-darwin-arm64" "$W/"
176+xattr -c "$W/mm-$V-darwin-arm64"
177+codesign --force --options runtime --timestamp --identifier com.bots-garden.mm \
178+ --entitlements macos-entitlements.plist \
179+ --sign "$MM_SIGN_IDENTITY" "$W/mm-$V-darwin-arm64"
180+codesign --verify --strict --verbose=2 "$W/mm-$V-darwin-arm64"
181+ditto -c -k --keepParent "$W/mm-$V-darwin-arm64" "$W/notarize.zip"
182+xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$MM_NOTARY_PROFILE" --wait
183+```
184+
185+Si `notarytool` finit par `status: Accepted`, remettez le binaire en place et recalculez les sommes :
186+
187+```bash
188+cp "$W/mm-$V-darwin-arm64" "release/$TAG/"
189+(cd "release/$TAG" && shasum -a 256 mm-"$V"-* > SHA256SUMS)
190+cat "release/$TAG/SHA256SUMS"
191+```
192+
193+Puis continuez avec `local-ci-03-create-draft.sh` (sans `macos-signature.txt`, les notes n'auront pas la ligne sur la signature : ajoutez-la à la main sur la page de la release si vous le voulez).
194+
195+**Une release déjà publiée** ne peut pas recevoir un binaire signé à la place de l'ancien : Rickub refuse un fichier dont le nom existe déjà (422), et aucune route ne permet d'en supprimer un. Il faut alors publier une nouvelle version.
196+
197+## En cas de problème
198+
199+| Symptôme | Que faire |
200+| --- | --- |
201+| `MM_SIGN_IDENTITY is set, but signing needs macOS` | La signature se fait sur le Mac. Ailleurs, videz `MM_SIGN_IDENTITY` (ou lancez sur le Mac). |
202+| `MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not` | Ajoutez `MM_NOTARY_PROFILE` à `.release.env` (A3). Un binaire signé mais pas notarisé reste bloqué par Gatekeeper. |
203+| `no valid identity "…" in the keychain` | `security find-identity -v -p codesigning` : copiez la valeur exacte. Si rien n'apparaît, voir A1. |
204+| `the notarytool profile "…" does not work` | `xcrun notarytool history --keychain-profile "…"` donne l'erreur. Un mot de passe d'application révoqué se refait (A2). |
205+| `notarization …: Invalid` | `xcrun notarytool log <id> --keychain-profile "$MM_NOTARY_PROFILE"` (la commande est affichée) donne la raison. |
206+| `notarytool submit failed` | Le plus souvent le réseau, ou des conditions d'utilisation à accepter sur [developer.apple.com](https://developer.apple.com/account) après une mise à jour du programme. |
207+| `differs from the signed copy (a synced folder?)` | Mettez la synchronisation en pause et relancez `local-ci-02-build.sh`. |
208+| `codesign` refuse (« resource fork, Finder information, or similar detritus ») | Métadonnées ajoutées par un dossier synchronisé. Le script les retire avec `xattr -c` ; à la main, faites-le aussi (partie C). |
209+| Le binaire signé démarre, mais il est tué (`Killed: 9`) au premier outil ou hook wasm | Il a été signé sans `macos-entitlements.plist` : `codesign -d --entitlements - <binaire>` doit montrer `allow-unsigned-executable-memory`. Sinon, voir A4. |
210+| `macos-entitlements.plist is missing or is not a valid plist` | Le fichier doit être à la racine du dépôt ; `plutil -lint macos-entitlements.plist` donne l'erreur. |
211+
212+## Voir aussi
213+
214+- Le guide de signature de Sidekicks (`docs/fr/how-to/signer-et-notariser-sous-macos.md` dans son dépôt), dont ce document reprend les réglages
215+- `local-ci-00-common.sh` : la liste des étapes `local-ci-*`
216+- Apple : [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
new file mode 100644
@@ -0,0 +1,216 @@
1+# Signer et notariser le binaire macOS de mm
2+
3+*[English version](RELEASING.macos.en.md)*
4+
5+Ce document explique comment publier `mm-<version>-darwin-arm64` signé avec un certificat **Developer ID** et **notarisé** par Apple, pour qu'un Mac le lance sans passer par `xattr` ni par **Ouvrir quand même**. Pas de `.dmg` : on signe le binaire seul, qui est publié tel quel dans la release.
6+
7+La signature se fait dans `./local-ci-02-build.sh`, sur le Mac, dès que deux variables sont définies. Le reste de la chaîne (`local-ci-03` à `05`) ne change pas.
8+
9+| Partie | Quand | Durée |
10+| --- | --- | --- |
11+| [A. Réglages](#partie-a--réglages-une-seule-fois) | une seule fois par Mac | 5 minutes si le certificat et le profil de Sidekicks sont sur ce Mac, sinon 30 minutes |
12+| [B. À chaque release](#partie-b--à-chaque-release) | à chaque version | quelques minutes de plus que d'habitude, surtout l'attente chez Apple |
13+| [C. À la main](#partie-c--signer-à-la-main) | si le script ne peut pas le faire | 10 minutes |
14+
15+## Faut-il faire quelque chose chez Apple pour une nouvelle application ?
16+
17+**Non.** Rien n'est à déclarer chez Apple pour mm :
18+
19+- Le certificat **Developer ID Application** appartient à l'équipe (Team ID), pas à une application. Celui de Sidekicks signe aussi mm.
20+- Le profil `notarytool` enregistre les identifiants du compte (Apple ID, mot de passe d'application, Team ID). Il notarise n'importe quel logiciel de l'équipe : son nom, `sidekicks-notary`, n'est qu'une étiquette dans le trousseau.
21+- Une application distribuée hors de l'App Store n'a besoin ni d'un App ID, ni d'un profil de provisionnement, ni d'une fiche dans App Store Connect.
22+- L'identifiant du binaire, `com.bots-garden.mm`, est choisi par le script au moment de signer. Il n'est enregistré nulle part chez Apple.
23+
24+Les variables `SIDEKICKS_SIGN_IDENTITY` et `SIDEKICKS_NOTARY_PROFILE` que vous avez déjà exportées suffisent donc. Un profil dédié à mm (`mm-notary`) est possible mais pas nécessaire.
25+
26+## Ce qui change par rapport à Sidekicks
27+
28+| | Sidekicks (une app) | mm (un binaire) |
29+| --- | --- | --- |
30+| Ce qui est signé | `sidekicks.app` puis le `.dmg` | `mm-<version>-darwin-arm64` seul |
31+| Envoi à la notarisation | un `.zip` de l'app, puis le `.dmg` | un `.zip` qui contient le binaire (`notarytool` ne prend pas un binaire nu) |
32+| Agrafage (`stapler`) | sur l'app et sur le `.dmg` | **impossible** sur un binaire nu : au premier lancement, macOS va chercher le ticket chez Apple, en ligne |
33+| Vérification `spctl` | `accepted`, `Notarized Developer ID` | ne s'applique pas : `spctl` n'évalue que les apps, les `.dmg` et les `.pkg`. Le statut `Accepted` de `notarytool` sert de preuve |
34+| Identifiant | `CFBundleIdentifier` dans `Info.plist` | `--identifier com.bots-garden.mm` passé à `codesign` |
35+| Autorisations (entitlements) | aucune | `allow-unsigned-executable-memory`, pour le moteur wasm (voir A4) |
36+
37+Le binaire notarisé mais non agrafé se lance aussi hors ligne quand il a été téléchargé avec `curl` (comme dans `INSTALL.MACOS.md`), car `curl` ne pose pas l'attribut de quarantaine. La signature compte surtout pour un téléchargement par navigateur et pour les Mac gérés (MDM).
38+
39+## Partie A — Réglages (une seule fois)
40+
41+### A1. Vérifier l'identité de signature
42+
43+```bash
44+security find-identity -v -p codesigning
45+```
46+
47+Une ligne `"Developer ID Application: Prénom Nom (AB12CD34EF)"` doit apparaître, et elle doit être exactement la valeur de `$SIDEKICKS_SIGN_IDENTITY` :
48+
49+```bash
50+echo "$SIDEKICKS_SIGN_IDENTITY"
51+```
52+
53+Si la ligne n'apparaît pas, le certificat n'est pas sur ce Mac :
54+
55+- **Il existe déjà sur un autre Mac** (celui de Sidekicks) : importez sa sauvegarde `.p12` (double-clic, puis son mot de passe) et relancez la commande.
56+- **Il n'existe pas encore** : il faut être **titulaire du compte** (Account Holder) de l'Apple Developer Program.
57+ 1. Ouvrez **Xcode → Settings… → Accounts**, ajoutez votre Apple ID si besoin, sélectionnez l'équipe, puis **Manage Certificates… → + → Developer ID Application**.
58+ 2. Sauvegardez le certificat avec sa clé : **Trousseaux d'accès**, trousseau **session**, **Mes certificats**, sélectionnez **Developer ID Application: …**, **Fichier → Exporter les éléments…**, format `.p12`, avec un mot de passe. Rangez-le hors du dépôt (gestionnaire de mots de passe).
59+- **La ligne apparaît avec `CSSMERR_TP_NOT_TRUSTED`** : installez le certificat intermédiaire **Developer ID - G2** depuis [apple.com/certificateauthority](https://www.apple.com/certificateauthority/).
60+
61+### A2. Vérifier le profil de notarisation
62+
63+```bash
64+xcrun notarytool history --keychain-profile "$SIDEKICKS_NOTARY_PROFILE"
65+```
66+
67+Une liste s'affiche (celle des envois de Sidekicks), sans erreur d'authentification. Si le profil manque sur ce Mac :
68+
69+1. Sur [account.apple.com](https://account.apple.com), **Connexion et sécurité → Mots de passe pour les apps**, créez-en un (par exemple `mm-notary`) et copiez-le.
70+2. Enregistrez le profil dans le trousseau, avec votre Apple ID et votre Team ID (10 caractères, dans **Membership details** sur [developer.apple.com/account](https://developer.apple.com/account)), puis collez le mot de passe d'application quand il est demandé :
71+
72+ ```bash
73+ xcrun notarytool store-credentials mm-notary --apple-id "vous@example.com" --team-id "AB12CD34EF"
74+ ```
75+
76+### A3. Brancher les variables dans `.release.env`
77+
78+Le script lit `MM_SIGN_IDENTITY` et `MM_NOTARY_PROFILE`. Ajoutez à `.release.env` (ignoré par git) :
79+
80+```bash
81+# Signature macOS (RELEASING.macos.fr.md). Vides = binaire macOS non signé.
82+MM_SIGN_IDENTITY="${SIDEKICKS_SIGN_IDENTITY:-}"
83+MM_NOTARY_PROFILE="${SIDEKICKS_NOTARY_PROFILE:-}"
84+```
85+
86+`.release.env` est lu par bash : il reprend les valeurs exportées dans votre `~/.zshrc`. Pour un profil propre à mm, mettez directement son nom : `MM_NOTARY_PROFILE="mm-notary"`.
87+
88+### A4. Vérifier que les outils wasm marchent dans le binaire signé
89+
90+La notarisation exige le **runtime renforcé** (`--options runtime`), qui interdit à un programme de rendre exécutable de la mémoire qu'il a écrite lui-même. Or le moteur wasm de mm (wazero), utilisé par les outils et les hooks wasm, compile le wasm en code machine pendant l'exécution.
91+
92+**Mesuré le 7 novembre 2026** sur un Mac Apple Silicon, avec le Developer ID : avec le seul runtime renforcé, macOS tue le processus (`Killed: 9`) au premier module wasm. Sans runtime renforcé, ou avec l'autorisation (entitlement) `com.apple.security.cs.allow-unsigned-executable-memory`, les 11 tests passent.
93+
94+La release signe donc mm avec cette autorisation, déclarée dans [`macos-entitlements.plist`](macos-entitlements.plist). Elle est invisible pour l'utilisateur (aucune fenêtre, rien de changé à l'installation) et laisse wasm aussi rapide que sur les autres plateformes. En contrepartie, le binaire perd la protection du runtime renforcé contre l'injection de code en mémoire, une protection qui compte peu pour un agent qui lance des commandes `bash` par conception. L'autre solution, l'interpréteur de wazero sur macOS, garde cette protection mais rend wasm plus lent et fait du Mac un cas à part dans le code.
95+
96+Pour vérifier que l'autorisation suffit toujours (après une mise à jour de wazero ou de macOS, par exemple) :
97+
98+```bash
99+./check-wasm-hardened-runtime.sh
100+```
101+
102+Le script compile les tests de `internal/wasmtool`, les signe comme la release (runtime renforcé + `macos-entitlements.plist`, avec `MM_SIGN_IDENTITY` s'il est défini, sinon une signature ad hoc) et les lance.
103+
104+- `✅ wazero works with the release signature` : rien à faire.
105+- `❌` : il fait deux contre-essais (sans runtime renforcé, puis avec le runtime renforcé sans l'autorisation) et dit si le runtime renforcé est en cause. Ne publiez pas de release signée avant d'avoir corrigé.
106+
107+## Partie B — À chaque release
108+
109+Avant tout : si le dépôt est dans un dossier synchronisé (kDrive, iCloud Drive…), **mettez la synchronisation en pause** pendant l'étape 02. Le script signe dans un dossier temporaire et vérifie la copie avec `cmp`, mais la pause évite qu'un client de synchronisation réécrive le binaire entre la signature et l'envoi.
110+
111+### B1. Construire, signer et notariser
112+
113+```bash
114+./local-ci-01-check.sh
115+./local-ci-02-build.sh
116+```
117+
118+Avec `MM_SIGN_IDENTITY` défini, `local-ci-02-build.sh` :
119+
120+1. **Vérifie d'abord la configuration**, avant de compiler : il faut être sur macOS, avoir `MM_NOTARY_PROFILE`, trouver l'identité dans le trousseau, pouvoir utiliser le profil `notarytool` et trouver un `macos-entitlements.plist` valide. Sinon il s'arrête avec la commande à lancer pour comprendre.
121+2. Compile les cinq plateformes, comme d'habitude.
122+3. Copie `mm-<version>-darwin-arm64` dans un dossier temporaire, le signe (`codesign --force --options runtime --timestamp --identifier com.bots-garden.mm --entitlements macos-entitlements.plist`) et vérifie que la signature porte l'autorité **Developer ID Application**, le drapeau **runtime**, un **Timestamp** et l'autorisation `allow-unsigned-executable-memory`.
123+4. L'envoie à Apple dans un `.zip` (`xcrun notarytool submit --wait`) et attend le statut **Accepted** (quelques minutes).
124+5. Remet le binaire signé dans `release/<TAG>/` et vérifie la copie avec `cmp`.
125+6. Lance ce binaire (`-h`, puis `--version`) : sur un Mac Apple Silicon, c'est le binaire signé qui est essayé.
126+7. Calcule `SHA256SUMS` **après** la signature : les sommes décrivent le binaire signé.
127+
128+Vous devez voir :
129+
130+```
131+🔏 Signing and notarizing mm-1.0.5-darwin-arm64...
132+ ✅ mm-1.0.5-darwin-arm64 signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)
133+ ⏳ waiting for Apple's notary service (a few minutes)...
134+ ✅ mm-1.0.5-darwin-arm64 notarized (submission 1a2b3c4d-…)
135+ ✅ mm-1.0.5-darwin-arm64 starts and reports v1.0.5
136+```
137+
138+`release/<TAG>/macos-signature.txt` garde la trace de la signature (identifiant, autorisation, autorité, Team ID, numéro de l'envoi à la notarisation). Ce fichier n'est pas publié. Le `README.md` du dossier dit alors que le binaire macOS est signé, au lieu de donner la commande `xattr`.
139+
140+Sans `MM_SIGN_IDENTITY`, rien ne change : le binaire macOS garde la signature ad hoc que lui donne Go, et le script affiche `⚠️ mm-<version>-darwin-arm64 is not signed`.
141+
142+### B2. Créer le brouillon, envoyer, publier
143+
144+```bash
145+./local-ci-03-create-draft.sh
146+./local-ci-04-upload.sh
147+./local-ci-05-publish.sh
148+```
149+
150+`local-ci-03-create-draft.sh` voit `macos-signature.txt` et ajoute aux notes de la release la ligne `macOS binary signed with a Developer ID and notarized by Apple`. `local-ci-05-publish.sh` télécharge chaque fichier publié et le compare à `release/<TAG>/` : le binaire en ligne est donc bien le binaire signé.
151+
152+### B3. Essayer sur un Mac
153+
154+1. Téléchargez `mm-<version>-darwin-arm64` depuis la page de la release, **avec un navigateur** : le fichier porte alors l'attribut de quarantaine d'un vrai téléchargement.
155+2. Dans le Terminal :
156+
157+ ```bash
158+ cd ~/Downloads
159+ xattr -l mm-<version>-darwin-arm64 # com.apple.quarantine doit apparaître
160+ chmod +x mm-<version>-darwin-arm64
161+ ./mm-<version>-darwin-arm64 --version # se lance, sans fenêtre de blocage
162+ codesign -dv --verbose=4 mm-<version>-darwin-arm64 2>&1 | grep -E "Identifier|Authority=Developer ID Application|TeamIdentifier"
163+ ```
164+
165+3. Lancez une démo qui utilise un outil wasm (`demos/wasm-tools`) avec ce binaire : sans l'autorisation, c'est là que macOS le tuerait.
166+
167+## Partie C — Signer à la main
168+
169+Si `local-ci-02-build.sh` a été lancé sans les variables (ou sur une autre machine), vous pouvez signer après coup, **avant** `local-ci-03-create-draft.sh` : les notes de la release contiennent les sommes. Depuis la racine du dépôt, avec `TAG` réglé :
170+
171+```bash
172+source .release.env
173+V="${TAG#v}"
174+W="$(mktemp -d)"
175+cp "release/$TAG/mm-$V-darwin-arm64" "$W/"
176+xattr -c "$W/mm-$V-darwin-arm64"
177+codesign --force --options runtime --timestamp --identifier com.bots-garden.mm \
178+ --entitlements macos-entitlements.plist \
179+ --sign "$MM_SIGN_IDENTITY" "$W/mm-$V-darwin-arm64"
180+codesign --verify --strict --verbose=2 "$W/mm-$V-darwin-arm64"
181+ditto -c -k --keepParent "$W/mm-$V-darwin-arm64" "$W/notarize.zip"
182+xcrun notarytool submit "$W/notarize.zip" --keychain-profile "$MM_NOTARY_PROFILE" --wait
183+```
184+
185+Si `notarytool` finit par `status: Accepted`, remettez le binaire en place et recalculez les sommes :
186+
187+```bash
188+cp "$W/mm-$V-darwin-arm64" "release/$TAG/"
189+(cd "release/$TAG" && shasum -a 256 mm-"$V"-* > SHA256SUMS)
190+cat "release/$TAG/SHA256SUMS"
191+```
192+
193+Puis continuez avec `local-ci-03-create-draft.sh` (sans `macos-signature.txt`, les notes n'auront pas la ligne sur la signature : ajoutez-la à la main sur la page de la release si vous le voulez).
194+
195+**Une release déjà publiée** ne peut pas recevoir un binaire signé à la place de l'ancien : Rickub refuse un fichier dont le nom existe déjà (422), et aucune route ne permet d'en supprimer un. Il faut alors publier une nouvelle version.
196+
197+## En cas de problème
198+
199+| Symptôme | Que faire |
200+| --- | --- |
201+| `MM_SIGN_IDENTITY is set, but signing needs macOS` | La signature se fait sur le Mac. Ailleurs, videz `MM_SIGN_IDENTITY` (ou lancez sur le Mac). |
202+| `MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not` | Ajoutez `MM_NOTARY_PROFILE` à `.release.env` (A3). Un binaire signé mais pas notarisé reste bloqué par Gatekeeper. |
203+| `no valid identity "…" in the keychain` | `security find-identity -v -p codesigning` : copiez la valeur exacte. Si rien n'apparaît, voir A1. |
204+| `the notarytool profile "…" does not work` | `xcrun notarytool history --keychain-profile "…"` donne l'erreur. Un mot de passe d'application révoqué se refait (A2). |
205+| `notarization …: Invalid` | `xcrun notarytool log <id> --keychain-profile "$MM_NOTARY_PROFILE"` (la commande est affichée) donne la raison. |
206+| `notarytool submit failed` | Le plus souvent le réseau, ou des conditions d'utilisation à accepter sur [developer.apple.com](https://developer.apple.com/account) après une mise à jour du programme. |
207+| `differs from the signed copy (a synced folder?)` | Mettez la synchronisation en pause et relancez `local-ci-02-build.sh`. |
208+| `codesign` refuse (« resource fork, Finder information, or similar detritus ») | Métadonnées ajoutées par un dossier synchronisé. Le script les retire avec `xattr -c` ; à la main, faites-le aussi (partie C). |
209+| Le binaire signé démarre, mais il est tué (`Killed: 9`) au premier outil ou hook wasm | Il a été signé sans `macos-entitlements.plist` : `codesign -d --entitlements - <binaire>` doit montrer `allow-unsigned-executable-memory`. Sinon, voir A4. |
210+| `macos-entitlements.plist is missing or is not a valid plist` | Le fichier doit être à la racine du dépôt ; `plutil -lint macos-entitlements.plist` donne l'erreur. |
211+
212+## Voir aussi
213+
214+- Le guide de signature de Sidekicks (`docs/fr/how-to/signer-et-notariser-sous-macos.md` dans son dépôt), dont ce document reprend les réglages
215+- `local-ci-00-common.sh` : la liste des étapes `local-ci-*`
216+- Apple : [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
added check-wasm-hardened-runtime.sh +133 -0
new file mode 100755
@@ -0,0 +1,133 @@
1+#!/usr/bin/env bash
2+# check-wasm-hardened-runtime.sh — check A4 of RELEASING.macos.en.md.
3+#
4+# Does wazero (wasm tools and hooks) work in a binary signed the way the
5+# release signs mm: hardened runtime + macos-entitlements.plist? Runs the
6+# internal/wasmtool tests from a test binary signed that way.
7+#
8+# Signs with MM_SIGN_IDENTITY (from .release.env) when it is set, otherwise ad
9+# hoc ("-"). Mac only. Writes nothing in the repository.
10+#
11+# Exit code: 0 = wasm works with the release signature, 1 = it does not,
12+# 2 = the check could not run.
13+set -euo pipefail
14+cd "$(dirname "$0")"
15+
16+if [ "$(uname -s)" != "Darwin" ]; then
17+ echo "❌ this check needs macOS (codesign)"
18+ exit 2
19+fi
20+
21+# .release.env may hold MM_SIGN_IDENTITY; it also requires TAG, which this
22+# check does not need.
23+if [ -f ./.release.env ]; then
24+ # shellcheck disable=SC1091
25+ source ./.release.env
26+fi
27+identity="${MM_SIGN_IDENTITY:--}"
28+entitlements="macos-entitlements.plist"
29+
30+if ! plutil -lint "${entitlements}" >/dev/null; then
31+ echo "❌ ${entitlements} is missing or is not a valid plist"
32+ exit 2
33+fi
34+
35+work="$(mktemp -d)"
36+trap 'rm -rf "${work}"' EXIT
37+test_binary="${work}/wasmtool.test"
38+
39+# sign signs the test binary again, with the given extra codesign options. No
40+# --timestamp: this binary is not notarized, and the timestamp server is not
41+# what is being checked.
42+sign() {
43+ codesign --force --timestamp=none --sign "${identity}" "$@" "${test_binary}" 2>/dev/null
44+}
45+
46+# runTests runs the signed test binary from internal/wasmtool (the tests read
47+# their modules from testdata/) and prints "status passed failed".
48+runTests() {
49+ local status=0
50+ (cd internal/wasmtool && "${test_binary}" -test.v -test.count=1) >"${work}/out.txt" 2>&1 || status=$?
51+ local passed failed
52+ passed="$(grep -c -- '^--- PASS' "${work}/out.txt" || true)"
53+ failed="$(grep -c -- '^--- FAIL' "${work}/out.txt" || true)"
54+ echo "${status} ${passed} ${failed}"
55+}
56+
57+# succeeded "status passed failed" is true when every test passed.
58+succeeded() {
59+ local status passed failed
60+ read -r status passed failed <<<"$1"
61+ [ "${status}" -eq 0 ] && [ "${failed}" -eq 0 ] && [ "${passed}" -gt 0 ]
62+}
63+
64+# describe "status passed failed" prints one result line.
65+describe() {
66+ local status passed failed
67+ read -r status passed failed <<<"$1"
68+ if succeeded "$1"; then
69+ echo " ✅ ${passed} tests passed"
70+ else
71+ echo " ❌ exit code ${status}, ${passed} passed, ${failed} failed"
72+ fi
73+}
74+
75+echo "🔨 building the internal/wasmtool tests..."
76+CGO_ENABLED=0 go test -c -o "${test_binary}" ./internal/wasmtool
77+
78+if [ "${identity}" = "-" ]; then
79+ echo "🔏 signing ad hoc (MM_SIGN_IDENTITY is not set), like the release: hardened runtime + ${entitlements}"
80+else
81+ echo "🔏 signing with ${identity}, like the release: hardened runtime + ${entitlements}"
82+fi
83+sign --options runtime --entitlements "${entitlements}"
84+
85+# The whole output goes into a variable before grep reads it: with pipefail,
86+# piping codesign into grep -q fails when grep stops early and codesign dies
87+# of SIGPIPE (seen on a Mac).
88+details="$(codesign -dv --verbose=4 "${test_binary}" 2>&1)"
89+if ! grep -q "flags=.*runtime" <<<"${details}"; then
90+ echo "❌ the test binary has no hardened runtime flag: the check would prove nothing"
91+ printf '%s\n' "${details}"
92+ exit 2
93+fi
94+granted="$(codesign -d --entitlements - --xml "${test_binary}" 2>/dev/null)"
95+if ! grep -qF "com.apple.security.cs.allow-unsigned-executable-memory" <<<"${granted}"; then
96+ echo "❌ the test binary does not carry the entitlements of ${entitlements}"
97+ exit 2
98+fi
99+echo " ✅ hardened runtime flag and entitlements present"
100+
101+echo "🧪 running the wasm tests with the release signature..."
102+result="$(runTests)"
103+describe "${result}"
104+
105+if succeeded "${result}"; then
106+ echo ""
107+ echo "✅ wazero works with the release signature: the signed mm can run wasm tools and hooks."
108+ exit 0
109+fi
110+echo " End of the output:"
111+tail -15 "${work}/out.txt" | sed 's/^/ /'
112+
113+# Two counter-checks, to know why.
114+echo ""
115+echo "🔬 counter-check 1: same binary, same identity, WITHOUT the hardened runtime..."
116+sign
117+control="$(runTests)"
118+describe "${control}"
119+
120+echo "🔬 counter-check 2: hardened runtime WITHOUT ${entitlements}..."
121+sign --options runtime
122+bare="$(runTests)"
123+describe "${bare}"
124+
125+echo ""
126+echo "❌ wazero does not work with the release signature."
127+if succeeded "${control}"; then
128+ echo " The hardened runtime is the cause: without it, the same binary passes."
129+ echo " ${entitlements} no longer grants enough: the ways out are in RELEASING.macos.en.md, step A4."
130+else
131+ echo " ⚠️ The tests also fail WITHOUT the hardened runtime: the cause is elsewhere."
132+fi
133+exit 1
new file mode 100755
@@ -0,0 +1,133 @@
1+#!/usr/bin/env bash
2+# check-wasm-hardened-runtime.sh — check A4 of RELEASING.macos.en.md.
3+#
4+# Does wazero (wasm tools and hooks) work in a binary signed the way the
5+# release signs mm: hardened runtime + macos-entitlements.plist? Runs the
6+# internal/wasmtool tests from a test binary signed that way.
7+#
8+# Signs with MM_SIGN_IDENTITY (from .release.env) when it is set, otherwise ad
9+# hoc ("-"). Mac only. Writes nothing in the repository.
10+#
11+# Exit code: 0 = wasm works with the release signature, 1 = it does not,
12+# 2 = the check could not run.
13+set -euo pipefail
14+cd "$(dirname "$0")"
15+
16+if [ "$(uname -s)" != "Darwin" ]; then
17+ echo "❌ this check needs macOS (codesign)"
18+ exit 2
19+fi
20+
21+# .release.env may hold MM_SIGN_IDENTITY; it also requires TAG, which this
22+# check does not need.
23+if [ -f ./.release.env ]; then
24+ # shellcheck disable=SC1091
25+ source ./.release.env
26+fi
27+identity="${MM_SIGN_IDENTITY:--}"
28+entitlements="macos-entitlements.plist"
29+
30+if ! plutil -lint "${entitlements}" >/dev/null; then
31+ echo "❌ ${entitlements} is missing or is not a valid plist"
32+ exit 2
33+fi
34+
35+work="$(mktemp -d)"
36+trap 'rm -rf "${work}"' EXIT
37+test_binary="${work}/wasmtool.test"
38+
39+# sign signs the test binary again, with the given extra codesign options. No
40+# --timestamp: this binary is not notarized, and the timestamp server is not
41+# what is being checked.
42+sign() {
43+ codesign --force --timestamp=none --sign "${identity}" "$@" "${test_binary}" 2>/dev/null
44+}
45+
46+# runTests runs the signed test binary from internal/wasmtool (the tests read
47+# their modules from testdata/) and prints "status passed failed".
48+runTests() {
49+ local status=0
50+ (cd internal/wasmtool && "${test_binary}" -test.v -test.count=1) >"${work}/out.txt" 2>&1 || status=$?
51+ local passed failed
52+ passed="$(grep -c -- '^--- PASS' "${work}/out.txt" || true)"
53+ failed="$(grep -c -- '^--- FAIL' "${work}/out.txt" || true)"
54+ echo "${status} ${passed} ${failed}"
55+}
56+
57+# succeeded "status passed failed" is true when every test passed.
58+succeeded() {
59+ local status passed failed
60+ read -r status passed failed <<<"$1"
61+ [ "${status}" -eq 0 ] && [ "${failed}" -eq 0 ] && [ "${passed}" -gt 0 ]
62+}
63+
64+# describe "status passed failed" prints one result line.
65+describe() {
66+ local status passed failed
67+ read -r status passed failed <<<"$1"
68+ if succeeded "$1"; then
69+ echo " ✅ ${passed} tests passed"
70+ else
71+ echo " ❌ exit code ${status}, ${passed} passed, ${failed} failed"
72+ fi
73+}
74+
75+echo "🔨 building the internal/wasmtool tests..."
76+CGO_ENABLED=0 go test -c -o "${test_binary}" ./internal/wasmtool
77+
78+if [ "${identity}" = "-" ]; then
79+ echo "🔏 signing ad hoc (MM_SIGN_IDENTITY is not set), like the release: hardened runtime + ${entitlements}"
80+else
81+ echo "🔏 signing with ${identity}, like the release: hardened runtime + ${entitlements}"
82+fi
83+sign --options runtime --entitlements "${entitlements}"
84+
85+# The whole output goes into a variable before grep reads it: with pipefail,
86+# piping codesign into grep -q fails when grep stops early and codesign dies
87+# of SIGPIPE (seen on a Mac).
88+details="$(codesign -dv --verbose=4 "${test_binary}" 2>&1)"
89+if ! grep -q "flags=.*runtime" <<<"${details}"; then
90+ echo "❌ the test binary has no hardened runtime flag: the check would prove nothing"
91+ printf '%s\n' "${details}"
92+ exit 2
93+fi
94+granted="$(codesign -d --entitlements - --xml "${test_binary}" 2>/dev/null)"
95+if ! grep -qF "com.apple.security.cs.allow-unsigned-executable-memory" <<<"${granted}"; then
96+ echo "❌ the test binary does not carry the entitlements of ${entitlements}"
97+ exit 2
98+fi
99+echo " ✅ hardened runtime flag and entitlements present"
100+
101+echo "🧪 running the wasm tests with the release signature..."
102+result="$(runTests)"
103+describe "${result}"
104+
105+if succeeded "${result}"; then
106+ echo ""
107+ echo "✅ wazero works with the release signature: the signed mm can run wasm tools and hooks."
108+ exit 0
109+fi
110+echo " End of the output:"
111+tail -15 "${work}/out.txt" | sed 's/^/ /'
112+
113+# Two counter-checks, to know why.
114+echo ""
115+echo "🔬 counter-check 1: same binary, same identity, WITHOUT the hardened runtime..."
116+sign
117+control="$(runTests)"
118+describe "${control}"
119+
120+echo "🔬 counter-check 2: hardened runtime WITHOUT ${entitlements}..."
121+sign --options runtime
122+bare="$(runTests)"
123+describe "${bare}"
124+
125+echo ""
126+echo "❌ wazero does not work with the release signature."
127+if succeeded "${control}"; then
128+ echo " The hardened runtime is the cause: without it, the same binary passes."
129+ echo " ${entitlements} no longer grants enough: the ways out are in RELEASING.macos.en.md, step A4."
130+else
131+ echo " ⚠️ The tests also fail WITHOUT the hardened runtime: the cause is elsewhere."
132+fi
133+exit 1
modified local-ci-00-common.sh +5 -2
@@ -3,14 +3,17 @@
33 #
44 # The steps, in order:
55 # ./local-ci-01-check.sh gofmt, vet, tests
6-# ./local-ci-02-build.sh the binaries, in release/<TAG>/
6+# ./local-ci-02-build.sh the binaries, in release/<TAG>/; on a Mac,
7+# signs and notarizes the darwin one when
8+# MM_SIGN_IDENTITY is set (RELEASING.macos.en.md)
79 # ./local-ci-03-create-draft.sh the tag and a draft prerelease
810 # ./local-ci-04-upload.sh the binaries, through a temporary branch
911 # ./local-ci-05-publish.sh the draft becomes public; asks first
1012 #
1113 # Tests only: ./local-ci-09-remove-test-release.sh
1214 #
13-# Reads TAG, ABOUT, OWNER and REPO from .release.env, and CI_TOKEN from
15+# Reads TAG, ABOUT, OWNER, REPO, MM_SIGN_IDENTITY and MM_NOTARY_PROFILE from
16+# .release.env, and CI_TOKEN from
1417 # .ci.cd.token.env. The token never reaches argv: curl and git read it from
1518 # files of mode 600 in a temporary folder.
1619 set -euo pipefail
@@ -3,14 +3,17 @@
3 #3 #
4 # The steps, in order:4 # The steps, in order:
5 # ./local-ci-01-check.sh gofmt, vet, tests5 # ./local-ci-01-check.sh gofmt, vet, tests
6-# ./local-ci-02-build.sh the binaries, in release/<TAG>/6+# ./local-ci-02-build.sh the binaries, in release/<TAG>/; on a Mac,
7+# signs and notarizes the darwin one when
8+# MM_SIGN_IDENTITY is set (RELEASING.macos.en.md)
7 # ./local-ci-03-create-draft.sh the tag and a draft prerelease9 # ./local-ci-03-create-draft.sh the tag and a draft prerelease
8 # ./local-ci-04-upload.sh the binaries, through a temporary branch10 # ./local-ci-04-upload.sh the binaries, through a temporary branch
9 # ./local-ci-05-publish.sh the draft becomes public; asks first11 # ./local-ci-05-publish.sh the draft becomes public; asks first
10 #12 #
11 # Tests only: ./local-ci-09-remove-test-release.sh13 # Tests only: ./local-ci-09-remove-test-release.sh
12 #14 #
13-# Reads TAG, ABOUT, OWNER and REPO from .release.env, and CI_TOKEN from15+# Reads TAG, ABOUT, OWNER, REPO, MM_SIGN_IDENTITY and MM_NOTARY_PROFILE from
16+# .release.env, and CI_TOKEN from
14 # .ci.cd.token.env. The token never reaches argv: curl and git read it from17 # .ci.cd.token.env. The token never reaches argv: curl and git read it from
15 # files of mode 600 in a temporary folder.18 # files of mode 600 in a temporary folder.
16 set -euo pipefail19 set -euo pipefail
modified local-ci-02-build.sh +153 -3
@@ -7,6 +7,11 @@
77 # mm-<version>-<os>-<arch>[.exe] one binary per platform
88 # SHA256SUMS checksums of the binaries
99 # README.md download table + how to run
10+# macos-signature.txt only when the macOS binary was signed
11+#
12+# The macOS binary is signed and notarized when .release.env (or the
13+# environment) sets MM_SIGN_IDENTITY and MM_NOTARY_PROFILE. That only works on
14+# a Mac: see RELEASING.macos.en.md.
1015 source "$(dirname "$0")/local-ci-00-common.sh"
1116
1217 ABOUT="${ABOUT:-Mini Me ${TAG}}"
@@ -40,6 +45,132 @@ RELEASES_DIR="release/${TAG}"
4045 # Go embeds, and a downloaded release would not know its own name.
4146 BUILD_FLAGS=(-trimpath -ldflags "-s -w -X mm/internal/version.stamped=${TAG}")
4247
48+# The macOS signature: a Developer ID identity from the login keychain and a
49+# notarytool profile, both set up once (RELEASING.macos.en.md, part A).
50+# Without them the darwin binary ships with the ad hoc signature the Go linker
51+# gives it, and macOS quarantines it when a browser downloads it.
52+MM_SIGN_IDENTITY="${MM_SIGN_IDENTITY:-}"
53+MM_NOTARY_PROFILE="${MM_NOTARY_PROFILE:-}"
54+# The identifier macOS knows the binary by. Without it, codesign takes the
55+# file name, which changes with every version.
56+MM_SIGN_IDENTIFIER="com.bots-garden.mm"
57+# The entitlements the signed binary carries: allow-unsigned-executable-memory,
58+# without which the hardened runtime kills mm at its first wasm tool or hook
59+# (measured with check-wasm-hardened-runtime.sh). The reason is in the file.
60+MM_ENTITLEMENTS="macos-entitlements.plist"
61+MM_REQUIRED_ENTITLEMENT="com.apple.security.cs.allow-unsigned-executable-memory"
62+
63+# checkSigningSetup fails now, before minutes of building, when the signature
64+# was asked for but cannot be done on this machine.
65+checkSigningSetup() {
66+ if [ "$(uname -s)" != "Darwin" ]; then
67+ echo "❌ MM_SIGN_IDENTITY is set, but signing needs macOS (codesign, notarytool)"
68+ exit 1
69+ fi
70+ if [ -z "${MM_NOTARY_PROFILE}" ]; then
71+ echo "❌ MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not: a signed binary that is not notarized is still blocked by Gatekeeper"
72+ exit 1
73+ fi
74+ # The whole output first: with pipefail, `security … | grep -q` fails when
75+ # grep stops early and security dies of SIGPIPE.
76+ local identities
77+ identities="$(security find-identity -v -p codesigning)"
78+ if ! grep -qF -- "\"${MM_SIGN_IDENTITY}\"" <<<"${identities}"; then
79+ echo "❌ no valid identity \"${MM_SIGN_IDENTITY}\" in the keychain"
80+ echo " 💡 security find-identity -v -p codesigning"
81+ exit 1
82+ fi
83+ if ! plutil -lint "${MM_ENTITLEMENTS}" >/dev/null; then
84+ echo "❌ ${MM_ENTITLEMENTS} is missing or is not a valid plist"
85+ exit 1
86+ fi
87+ if ! xcrun notarytool history --keychain-profile "${MM_NOTARY_PROFILE}" >/dev/null 2>&1; then
88+ echo "❌ the notarytool profile \"${MM_NOTARY_PROFILE}\" does not work"
89+ echo " 💡 xcrun notarytool history --keychain-profile \"${MM_NOTARY_PROFILE}\""
90+ exit 1
91+ fi
92+}
93+
94+# signMacBinary signs ASSET with the Developer ID, has Apple notarize it, and
95+# puts it back in RELEASES_DIR. The work happens in the temporary folder: the
96+# repository may sit in a synced folder (kDrive, iCloud…) whose client writes
97+# metadata into files while codesign rewrites them.
98+#
99+# A bare binary cannot be stapled (only an app, a dmg or a pkg can): Gatekeeper
100+# fetches the notarization ticket online the first time the binary runs.
101+signMacBinary() {
102+ local asset=$1
103+ local dir="${work}/sign"
104+ local copy="${dir}/${asset}"
105+ mkdir -p "${dir}"
106+ cp "${RELEASES_DIR}/${asset}" "${copy}"
107+ xattr -c "${copy}" 2>/dev/null || true
108+
109+ # --options runtime (the hardened runtime) and --timestamp are what the
110+ # notary service requires; --force replaces the linker's ad hoc signature.
111+ codesign --force --options runtime --timestamp \
112+ --identifier "${MM_SIGN_IDENTIFIER}" \
113+ --entitlements "${MM_ENTITLEMENTS}" \
114+ --sign "${MM_SIGN_IDENTITY}" "${copy}"
115+ codesign --verify --strict --verbose=2 "${copy}"
116+
117+ local details
118+ details="$(codesign -dv --verbose=4 "${copy}" 2>&1)"
119+ local expected
120+ for expected in "Authority=Developer ID Application" "flags=.*runtime" "Timestamp="; do
121+ if ! grep -q -- "${expected}" <<<"${details}"; then
122+ echo " ❌ ${asset}: the signature has no ${expected}"
123+ printf '%s\n' "${details}"
124+ exit 1
125+ fi
126+ done
127+ local entitlements
128+ entitlements="$(codesign -d --entitlements - --xml "${copy}" 2>/dev/null)"
129+ if ! grep -qF -- "${MM_REQUIRED_ENTITLEMENT}" <<<"${entitlements}"; then
130+ echo " ❌ ${asset}: the signature has no ${MM_REQUIRED_ENTITLEMENT}: wasm tools and hooks would kill mm"
131+ exit 1
132+ fi
133+ echo " ✅ ${asset} signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)"
134+
135+ # The notary service takes a zip, a dmg or a pkg, not a bare binary.
136+ ditto -c -k --keepParent "${copy}" "${dir}/notarize.zip"
137+ echo " ⏳ waiting for Apple's notary service (a few minutes)..."
138+ if ! xcrun notarytool submit "${dir}/notarize.zip" \
139+ --keychain-profile "${MM_NOTARY_PROFILE}" --wait --output-format json >"${dir}/notary.json"; then
140+ echo " ❌ notarytool submit failed:"
141+ cat "${dir}/notary.json"
142+ exit 1
143+ fi
144+ local status submission
145+ status="$(jq -r '.status // empty' "${dir}/notary.json")"
146+ submission="$(jq -r '.id // empty' "${dir}/notary.json")"
147+ if [ "${status}" != "Accepted" ]; then
148+ echo " ❌ notarization ${submission}: ${status:-no status}"
149+ echo " 💡 xcrun notarytool log ${submission} --keychain-profile \"${MM_NOTARY_PROFILE}\""
150+ exit 1
151+ fi
152+ echo " ✅ ${asset} notarized (submission ${submission})"
153+
154+ cp "${copy}" "${RELEASES_DIR}/${asset}"
155+ if ! cmp -s "${copy}" "${RELEASES_DIR}/${asset}"; then
156+ echo " ❌ ${RELEASES_DIR}/${asset} differs from the signed copy (a synced folder?)"
157+ exit 1
158+ fi
159+
160+ {
161+ printf 'asset: %s\n' "${asset}"
162+ printf 'identifier: %s\n' "${MM_SIGN_IDENTIFIER}"
163+ printf 'entitlement: %s\n' "${MM_REQUIRED_ENTITLEMENT}"
164+ grep '^Authority=Developer ID Application' <<<"${details}"
165+ grep '^TeamIdentifier=' <<<"${details}" || true
166+ printf 'notarization: %s %s\n' "${submission}" "${status}"
167+ } >"${RELEASES_DIR}/macos-signature.txt"
168+}
169+
170+if [ -n "${MM_SIGN_IDENTITY}" ]; then
171+ checkSigningSetup
172+fi
173+
43174 echo "🚀 Building Mini Me ${TAG} — ${ABOUT}"
44175 echo "🐹 $(go version)"
45176
@@ -76,6 +207,18 @@ for platform in "${PLATFORMS[@]}"; do
76207 echo " ✅ ${asset}"
77208 done
78209
210+# Signed before the binary is run and before the checksums: signing rewrites
211+# the file, and SHA256SUMS must describe what ships.
212+MAC_ASSET="$(assetName darwin arm64)"
213+if [ -n "${MM_SIGN_IDENTITY}" ]; then
214+ echo ""
215+ echo "🔏 Signing and notarizing ${MAC_ASSET}..."
216+ signMacBinary "${MAC_ASSET}"
217+else
218+ echo ""
219+ echo "⚠️ ${MAC_ASSET} is not signed: MM_SIGN_IDENTITY is not set (RELEASING.macos.en.md)"
220+fi
221+
79222 # The binary for this machine is the only one that can run here, and running
80223 # it is the one proof that what ships starts at all: -h prints the usage and
81224 # exits 0 before any config or model server is looked for.
@@ -122,6 +265,15 @@ downloadTable() {
122265 done
123266 }
124267
268+# macosNote tells a macOS user what to expect from the download.
269+macosNote() {
270+ if [ -f "${RELEASES_DIR}/macos-signature.txt" ]; then
271+ printf 'On macOS, the binary is signed with a Developer ID and notarized by Apple:\nit runs without a quarantine step.\n'
272+ else
273+ printf 'On macOS, an unsigned download is quarantined until you say otherwise:\n\n xattr -d com.apple.quarantine mm-%s-darwin-arm64\n' "${VERSION}"
274+ fi
275+}
276+
125277 cat >"${RELEASES_DIR}/README.md" <<EOM
126278 # Mini Me ${TAG}
127279
@@ -139,9 +291,7 @@ $(downloadTable)
139291 ./mm-${VERSION}-<platform> -tui agent.yaml # window mode
140292 ./mm-${VERSION}-<platform> -acp # from an editor (Zed, …)
141293
142-On macOS, an unsigned download is quarantined until you say otherwise:
143-
144- xattr -d com.apple.quarantine mm-${VERSION}-darwin-arm64
294+$(macosNote)
145295
146296 ## Verifying the download
147297
@@ -7,6 +7,11 @@
7 # mm-<version>-<os>-<arch>[.exe] one binary per platform7 # mm-<version>-<os>-<arch>[.exe] one binary per platform
8 # SHA256SUMS checksums of the binaries8 # SHA256SUMS checksums of the binaries
9 # README.md download table + how to run9 # README.md download table + how to run
10+# macos-signature.txt only when the macOS binary was signed
11+#
12+# The macOS binary is signed and notarized when .release.env (or the
13+# environment) sets MM_SIGN_IDENTITY and MM_NOTARY_PROFILE. That only works on
14+# a Mac: see RELEASING.macos.en.md.
10 source "$(dirname "$0")/local-ci-00-common.sh"15 source "$(dirname "$0")/local-ci-00-common.sh"
11 16
12 ABOUT="${ABOUT:-Mini Me ${TAG}}"17 ABOUT="${ABOUT:-Mini Me ${TAG}}"
@@ -40,6 +45,132 @@ RELEASES_DIR="release/${TAG}"
40 # Go embeds, and a downloaded release would not know its own name.45 # Go embeds, and a downloaded release would not know its own name.
41 BUILD_FLAGS=(-trimpath -ldflags "-s -w -X mm/internal/version.stamped=${TAG}")46 BUILD_FLAGS=(-trimpath -ldflags "-s -w -X mm/internal/version.stamped=${TAG}")
42 47
48+# The macOS signature: a Developer ID identity from the login keychain and a
49+# notarytool profile, both set up once (RELEASING.macos.en.md, part A).
50+# Without them the darwin binary ships with the ad hoc signature the Go linker
51+# gives it, and macOS quarantines it when a browser downloads it.
52+MM_SIGN_IDENTITY="${MM_SIGN_IDENTITY:-}"
53+MM_NOTARY_PROFILE="${MM_NOTARY_PROFILE:-}"
54+# The identifier macOS knows the binary by. Without it, codesign takes the
55+# file name, which changes with every version.
56+MM_SIGN_IDENTIFIER="com.bots-garden.mm"
57+# The entitlements the signed binary carries: allow-unsigned-executable-memory,
58+# without which the hardened runtime kills mm at its first wasm tool or hook
59+# (measured with check-wasm-hardened-runtime.sh). The reason is in the file.
60+MM_ENTITLEMENTS="macos-entitlements.plist"
61+MM_REQUIRED_ENTITLEMENT="com.apple.security.cs.allow-unsigned-executable-memory"
62+
63+# checkSigningSetup fails now, before minutes of building, when the signature
64+# was asked for but cannot be done on this machine.
65+checkSigningSetup() {
66+ if [ "$(uname -s)" != "Darwin" ]; then
67+ echo "❌ MM_SIGN_IDENTITY is set, but signing needs macOS (codesign, notarytool)"
68+ exit 1
69+ fi
70+ if [ -z "${MM_NOTARY_PROFILE}" ]; then
71+ echo "❌ MM_SIGN_IDENTITY is set but MM_NOTARY_PROFILE is not: a signed binary that is not notarized is still blocked by Gatekeeper"
72+ exit 1
73+ fi
74+ # The whole output first: with pipefail, `security … | grep -q` fails when
75+ # grep stops early and security dies of SIGPIPE.
76+ local identities
77+ identities="$(security find-identity -v -p codesigning)"
78+ if ! grep -qF -- "\"${MM_SIGN_IDENTITY}\"" <<<"${identities}"; then
79+ echo "❌ no valid identity \"${MM_SIGN_IDENTITY}\" in the keychain"
80+ echo " 💡 security find-identity -v -p codesigning"
81+ exit 1
82+ fi
83+ if ! plutil -lint "${MM_ENTITLEMENTS}" >/dev/null; then
84+ echo "❌ ${MM_ENTITLEMENTS} is missing or is not a valid plist"
85+ exit 1
86+ fi
87+ if ! xcrun notarytool history --keychain-profile "${MM_NOTARY_PROFILE}" >/dev/null 2>&1; then
88+ echo "❌ the notarytool profile \"${MM_NOTARY_PROFILE}\" does not work"
89+ echo " 💡 xcrun notarytool history --keychain-profile \"${MM_NOTARY_PROFILE}\""
90+ exit 1
91+ fi
92+}
93+
94+# signMacBinary signs ASSET with the Developer ID, has Apple notarize it, and
95+# puts it back in RELEASES_DIR. The work happens in the temporary folder: the
96+# repository may sit in a synced folder (kDrive, iCloud…) whose client writes
97+# metadata into files while codesign rewrites them.
98+#
99+# A bare binary cannot be stapled (only an app, a dmg or a pkg can): Gatekeeper
100+# fetches the notarization ticket online the first time the binary runs.
101+signMacBinary() {
102+ local asset=$1
103+ local dir="${work}/sign"
104+ local copy="${dir}/${asset}"
105+ mkdir -p "${dir}"
106+ cp "${RELEASES_DIR}/${asset}" "${copy}"
107+ xattr -c "${copy}" 2>/dev/null || true
108+
109+ # --options runtime (the hardened runtime) and --timestamp are what the
110+ # notary service requires; --force replaces the linker's ad hoc signature.
111+ codesign --force --options runtime --timestamp \
112+ --identifier "${MM_SIGN_IDENTIFIER}" \
113+ --entitlements "${MM_ENTITLEMENTS}" \
114+ --sign "${MM_SIGN_IDENTITY}" "${copy}"
115+ codesign --verify --strict --verbose=2 "${copy}"
116+
117+ local details
118+ details="$(codesign -dv --verbose=4 "${copy}" 2>&1)"
119+ local expected
120+ for expected in "Authority=Developer ID Application" "flags=.*runtime" "Timestamp="; do
121+ if ! grep -q -- "${expected}" <<<"${details}"; then
122+ echo " ❌ ${asset}: the signature has no ${expected}"
123+ printf '%s\n' "${details}"
124+ exit 1
125+ fi
126+ done
127+ local entitlements
128+ entitlements="$(codesign -d --entitlements - --xml "${copy}" 2>/dev/null)"
129+ if ! grep -qF -- "${MM_REQUIRED_ENTITLEMENT}" <<<"${entitlements}"; then
130+ echo " ❌ ${asset}: the signature has no ${MM_REQUIRED_ENTITLEMENT}: wasm tools and hooks would kill mm"
131+ exit 1
132+ fi
133+ echo " ✅ ${asset} signed (Developer ID, hardened runtime, timestamp, allow-unsigned-executable-memory)"
134+
135+ # The notary service takes a zip, a dmg or a pkg, not a bare binary.
136+ ditto -c -k --keepParent "${copy}" "${dir}/notarize.zip"
137+ echo " ⏳ waiting for Apple's notary service (a few minutes)..."
138+ if ! xcrun notarytool submit "${dir}/notarize.zip" \
139+ --keychain-profile "${MM_NOTARY_PROFILE}" --wait --output-format json >"${dir}/notary.json"; then
140+ echo " ❌ notarytool submit failed:"
141+ cat "${dir}/notary.json"
142+ exit 1
143+ fi
144+ local status submission
145+ status="$(jq -r '.status // empty' "${dir}/notary.json")"
146+ submission="$(jq -r '.id // empty' "${dir}/notary.json")"
147+ if [ "${status}" != "Accepted" ]; then
148+ echo " ❌ notarization ${submission}: ${status:-no status}"
149+ echo " 💡 xcrun notarytool log ${submission} --keychain-profile \"${MM_NOTARY_PROFILE}\""
150+ exit 1
151+ fi
152+ echo " ✅ ${asset} notarized (submission ${submission})"
153+
154+ cp "${copy}" "${RELEASES_DIR}/${asset}"
155+ if ! cmp -s "${copy}" "${RELEASES_DIR}/${asset}"; then
156+ echo " ❌ ${RELEASES_DIR}/${asset} differs from the signed copy (a synced folder?)"
157+ exit 1
158+ fi
159+
160+ {
161+ printf 'asset: %s\n' "${asset}"
162+ printf 'identifier: %s\n' "${MM_SIGN_IDENTIFIER}"
163+ printf 'entitlement: %s\n' "${MM_REQUIRED_ENTITLEMENT}"
164+ grep '^Authority=Developer ID Application' <<<"${details}"
165+ grep '^TeamIdentifier=' <<<"${details}" || true
166+ printf 'notarization: %s %s\n' "${submission}" "${status}"
167+ } >"${RELEASES_DIR}/macos-signature.txt"
168+}
169+
170+if [ -n "${MM_SIGN_IDENTITY}" ]; then
171+ checkSigningSetup
172+fi
173+
43 echo "🚀 Building Mini Me ${TAG} — ${ABOUT}"174 echo "🚀 Building Mini Me ${TAG} — ${ABOUT}"
44 echo "🐹 $(go version)"175 echo "🐹 $(go version)"
45 176
@@ -76,6 +207,18 @@ for platform in "${PLATFORMS[@]}"; do
76 echo " ✅ ${asset}"207 echo " ✅ ${asset}"
77 done208 done
78 209
210+# Signed before the binary is run and before the checksums: signing rewrites
211+# the file, and SHA256SUMS must describe what ships.
212+MAC_ASSET="$(assetName darwin arm64)"
213+if [ -n "${MM_SIGN_IDENTITY}" ]; then
214+ echo ""
215+ echo "🔏 Signing and notarizing ${MAC_ASSET}..."
216+ signMacBinary "${MAC_ASSET}"
217+else
218+ echo ""
219+ echo "⚠️ ${MAC_ASSET} is not signed: MM_SIGN_IDENTITY is not set (RELEASING.macos.en.md)"
220+fi
221+
79 # The binary for this machine is the only one that can run here, and running222 # The binary for this machine is the only one that can run here, and running
80 # it is the one proof that what ships starts at all: -h prints the usage and223 # it is the one proof that what ships starts at all: -h prints the usage and
81 # exits 0 before any config or model server is looked for.224 # exits 0 before any config or model server is looked for.
@@ -122,6 +265,15 @@ downloadTable() {
122 done265 done
123 }266 }
124 267
268+# macosNote tells a macOS user what to expect from the download.
269+macosNote() {
270+ if [ -f "${RELEASES_DIR}/macos-signature.txt" ]; then
271+ printf 'On macOS, the binary is signed with a Developer ID and notarized by Apple:\nit runs without a quarantine step.\n'
272+ else
273+ printf 'On macOS, an unsigned download is quarantined until you say otherwise:\n\n xattr -d com.apple.quarantine mm-%s-darwin-arm64\n' "${VERSION}"
274+ fi
275+}
276+
125 cat >"${RELEASES_DIR}/README.md" <<EOM277 cat >"${RELEASES_DIR}/README.md" <<EOM
126 # Mini Me ${TAG}278 # Mini Me ${TAG}
127 279
@@ -139,9 +291,7 @@ $(downloadTable)
139 ./mm-${VERSION}-<platform> -tui agent.yaml # window mode291 ./mm-${VERSION}-<platform> -tui agent.yaml # window mode
140 ./mm-${VERSION}-<platform> -acp # from an editor (Zed, …)292 ./mm-${VERSION}-<platform> -acp # from an editor (Zed, …)
141 293
142-On macOS, an unsigned download is quarantined until you say otherwise:294+$(macosNote)
143-
144- xattr -d com.apple.quarantine mm-${VERSION}-darwin-arm64
145 295
146 ## Verifying the download296 ## Verifying the download
147 297
modified local-ci-03-create-draft.sh +3 -0
@@ -41,6 +41,9 @@ notes_file="release-notes/${TAG}.en.md"
4141 printf '%s\n\n' "${ABOUT:-Mini Me}"
4242 printf -- '- Commit: `%s`\n' "${head_sha}"
4343 printf -- '- Built on a local machine with %s\n' "$(go env GOVERSION)"
44+ if [ -f "${assets_dir}/macos-signature.txt" ]; then
45+ printf -- '- macOS binary signed with a Developer ID and notarized by Apple\n'
46+ fi
4447 if [ -f "${notes_file}" ]; then
4548 printf '\nDetails: https://rickub.com/%s/%s/blob/main/%s\n' "${OWNER}" "${REPO}" "${notes_file}"
4649 fi
@@ -41,6 +41,9 @@ notes_file="release-notes/${TAG}.en.md"
41 printf '%s\n\n' "${ABOUT:-Mini Me}"41 printf '%s\n\n' "${ABOUT:-Mini Me}"
42 printf -- '- Commit: `%s`\n' "${head_sha}"42 printf -- '- Commit: `%s`\n' "${head_sha}"
43 printf -- '- Built on a local machine with %s\n' "$(go env GOVERSION)"43 printf -- '- Built on a local machine with %s\n' "$(go env GOVERSION)"
44+ if [ -f "${assets_dir}/macos-signature.txt" ]; then
45+ printf -- '- macOS binary signed with a Developer ID and notarized by Apple\n'
46+ fi
44 if [ -f "${notes_file}" ]; then47 if [ -f "${notes_file}" ]; then
45 printf '\nDetails: https://rickub.com/%s/%s/blob/main/%s\n' "${OWNER}" "${REPO}" "${notes_file}"48 printf '\nDetails: https://rickub.com/%s/%s/blob/main/%s\n' "${OWNER}" "${REPO}" "${notes_file}"
46 fi49 fi
added macos-entitlements.plist +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+<!--
4+ Entitlements of the signed macOS mm (local-ci-02-build.sh, check-wasm-hardened-runtime.sh).
5+
6+ allow-unsigned-executable-memory: wazero, the engine of the wasm tools and
7+ hooks, compiles wasm to machine code at run time and makes that memory
8+ executable. The hardened runtime that notarization requires kills the
9+ process when it does (measured 2026-11-07: SIGKILL at the first wasm test;
10+ 11/11 pass with this entitlement). See RELEASING.macos.en.md, step A4.
11+-->
12+<plist version="1.0">
13+<dict>
14+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
15+ <true/>
16+</dict>
17+</plist>
new file mode 100644
@@ -0,0 +1,17 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+<!--
4+ Entitlements of the signed macOS mm (local-ci-02-build.sh, check-wasm-hardened-runtime.sh).
5+
6+ allow-unsigned-executable-memory: wazero, the engine of the wasm tools and
7+ hooks, compiles wasm to machine code at run time and makes that memory
8+ executable. The hardened runtime that notarization requires kills the
9+ process when it does (measured 2026-11-07: SIGKILL at the first wasm test;
10+ 11/11 pass with this entitlement). See RELEASING.macos.en.md, step A4.
11+-->
12+<plist version="1.0">
13+<dict>
14+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
15+ <true/>
16+</dict>
17+</plist>
modified release-notes/README.md +2 -1
@@ -4,7 +4,8 @@ One note per tag, in English (`.en.md`) and French (`.fr.md`). Rebuilt on 2026-1
44
55 | Version | Date | Summary | |
66 |---|---|---|---|
7-| [v1.0.4](v1.0.4.en.md) | not tagged yet | A security update: mm now builds with `golang.org/x/net` v0.62.0, which fixes two vulnerabilities in its HTML parser. | [FR](v1.0.4.fr.md) |
7+| [v1.0.5](v1.0.5.en.md) | not tagged yet | `mm dockerinit` and `mm sbxinit` write the files to run mini-me in a container or a Docker Sandbox, `mm init` gives the agent its name, two vibe-coding demos (Go and Swift), and a macOS binary signed and notarized by Apple. | [FR](v1.0.5.fr.md) |
8+| [v1.0.4](v1.0.4.en.md) | 2026-11-02 | A security update: mm now builds with `golang.org/x/net` v0.62.0, which fixes two vulnerabilities in its HTML parser. | [FR](v1.0.4.fr.md) |
89 | [v1.0.3](v1.0.3.en.md) | 2026-10-26 | `mm init` writes agents for any OpenAI-compatible service — it asks the endpoint and the API key's variable, then lists the service's models or lets you type one — and its text answers can be edited with the arrow keys. | [FR](v1.0.3.fr.md) |
910 | [v1.0.2](v1.0.2.en.md) | 2026-10-26 | Use mini-me as a library from Node.js, test what a model can do with mini-me itself, and send OpenAI models only the sampling settings you wrote. | [FR](v1.0.2.fr.md) |
1011 | [v1.0.1](v1.0.1.en.md) | 2026-10-26 | Messages typed during an answer wait their turn, the window shows when the model struggles and how long an answer takes, models can be downloaded at start-up and take turns in memory during a hand-off, and deciders need no boilerplate. | [FR](v1.0.1.fr.md) |
@@ -4,7 +4,8 @@ One note per tag, in English (`.en.md`) and French (`.fr.md`). Rebuilt on 2026-1
4 4
5 | Version | Date | Summary | |5 | Version | Date | Summary | |
6 |---|---|---|---|6 |---|---|---|---|
7-| [v1.0.4](v1.0.4.en.md) | not tagged yet | A security update: mm now builds with `golang.org/x/net` v0.62.0, which fixes two vulnerabilities in its HTML parser. | [FR](v1.0.4.fr.md) |7+| [v1.0.5](v1.0.5.en.md) | not tagged yet | `mm dockerinit` and `mm sbxinit` write the files to run mini-me in a container or a Docker Sandbox, `mm init` gives the agent its name, two vibe-coding demos (Go and Swift), and a macOS binary signed and notarized by Apple. | [FR](v1.0.5.fr.md) |
8+| [v1.0.4](v1.0.4.en.md) | 2026-11-02 | A security update: mm now builds with `golang.org/x/net` v0.62.0, which fixes two vulnerabilities in its HTML parser. | [FR](v1.0.4.fr.md) |
8 | [v1.0.3](v1.0.3.en.md) | 2026-10-26 | `mm init` writes agents for any OpenAI-compatible service — it asks the endpoint and the API key's variable, then lists the service's models or lets you type one — and its text answers can be edited with the arrow keys. | [FR](v1.0.3.fr.md) |9 | [v1.0.3](v1.0.3.en.md) | 2026-10-26 | `mm init` writes agents for any OpenAI-compatible service — it asks the endpoint and the API key's variable, then lists the service's models or lets you type one — and its text answers can be edited with the arrow keys. | [FR](v1.0.3.fr.md) |
9 | [v1.0.2](v1.0.2.en.md) | 2026-10-26 | Use mini-me as a library from Node.js, test what a model can do with mini-me itself, and send OpenAI models only the sampling settings you wrote. | [FR](v1.0.2.fr.md) |10 | [v1.0.2](v1.0.2.en.md) | 2026-10-26 | Use mini-me as a library from Node.js, test what a model can do with mini-me itself, and send OpenAI models only the sampling settings you wrote. | [FR](v1.0.2.fr.md) |
10 | [v1.0.1](v1.0.1.en.md) | 2026-10-26 | Messages typed during an answer wait their turn, the window shows when the model struggles and how long an answer takes, models can be downloaded at start-up and take turns in memory during a hand-off, and deciders need no boilerplate. | [FR](v1.0.1.fr.md) |11 | [v1.0.1](v1.0.1.en.md) | 2026-10-26 | Messages typed during an answer wait their turn, the window shows when the model struggles and how long an answer takes, models can be downloaded at start-up and take turns in memory during a hand-off, and deciders need no boilerplate. | [FR](v1.0.1.fr.md) |
added release-notes/v1.0.5.en.md +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+# mini-me v1.0.5
2+
3+*[Version française](v1.0.5.fr.md)*
4+
5+`mm dockerinit` and `mm sbxinit` write the files to run mini-me in a container or a Docker Sandbox, `mm init` gives the agent its name, two vibe-coding demos (Go and Swift), and a macOS binary signed and notarized by Apple.
6+
7+## New
8+
9+### `mm dockerinit`: run mini-me in a container (#189)
10+
11+`mm dockerinit` writes two files in the current directory:
12+
13+- a `Dockerfile` that installs the **latest release** of mm, for `amd64` and `arm64`. It finds the release through the `SHA256SUMS` of `releases/latest`, so a new release invalidates Docker's cache. The agent **never runs as root**: the image has a `mm` user.
14+- a `compose.yaml` that mounts your project and runs the container with your user and group ids (`MM_UID` and `MM_GID` override them).
15+
16+The output and the how-to show both ways of using it: a long-lived container (`docker compose up -d --wait`, then `docker compose exec … mm tui <agent>`) and a one-off run (`docker compose run --rm …`). For both architectures at once, build with `docker buildx`: putting `platforms` in `compose.yaml` fails without QEMU. An editor can talk to mm in the container over ACP through `docker exec -i`.
17+
18+If `Dockerfile` or `compose.yaml` already exists, mm asks before overwriting it (default: no). With `--yes`, or when the input ends, it writes nothing.
19+
20+Tried on linux/arm64 with Docker 29.8.1. **Not tried**: the amd64 build, reaching the model directly through `host.docker.internal`, macOS and Windows hosts.
21+
22+See [Run mini-me in a container](../docs/en/how-to/run-in-a-container.md).
23+
24+### `mm sbxinit`: a `sbxenv.yaml` for Docker Sandboxes (#191)
25+
26+`mm sbxinit` writes `sbxenv.yaml` in the current directory, for the [mini-me kit](https://rickub.com/bots-garden/mini-me-kit):
27+
28+- the kit at its **latest release**, read from the kit's releases page when the file is generated (`latest` with a warning when the page cannot be reached);
29+- `--workspace ./workspace` becomes an absolute path;
30+- `sandboxOptions.pullPolicy: always`.
31+
32+The agent file is looked for in the current directory, then in the workspace. When it is outside the workspace, `mm sbxinit` only warns: the how-to explains the two ways out (move the file, or mount its folder read-only and point `AGENT_CONFIG` at it). Existing files are handled like `mm dockerinit`'s.
33+
34+**Not tried**: `sbx env create` with the generated file.
35+
36+See [Generate a sbxenv.yaml with `mm sbxinit`](../docs/en/how-to/run-in-a-docker-sandbox.md#generate-a-sbxenvyaml-with-mm-sbxinit).
37+
38+### `mm init` gives the agent its name (#182)
39+
40+The system prompt of a file written by `mm init` now starts with `Your name is <name>.`, where the name is the one typed at `Agent name?`. Without a name, the agent takes the name of its usage (`Your name is docs.`). Asked its name on Mellum (Docker Model Runner), the agent answered with it 3 times out of 3.
41+
42+For the editors, `acp.name` is the name in lower case (`bob`) and `acp.title` the name followed by the usage (`Bob (developer)`).
43+
44+When the agent file already exists, `mm init` asks `Overwrite agent.yaml?` (default: no, and it asks for another file name). The new file is checked by loading it before it replaces the old one.
45+
46+### `mm init` links to the online documentation (#179)
47+
48+The comments of a file written by `mm init` point to the documentation as `https://` links, instead of paths that only made sense in a copy of the source code. The `Next:` hints are on four lines.
49+
50+### Point the agent at a list of pages (#181)
51+
52+No new setting: the list of URLs the agent should read goes in the system prompt. The new section of the how-to has an example. On Mellum with three URLs, the agent fetched the right page 4 times out of 4. The list **guides** the model but does not **restrict** it: asked for example.com, it fetched it 2 times out of 2 despite "Do not use any other URL". Blocking URLs would need a hook.
53+
54+See [Point the agent at a list of pages](../docs/en/how-to/fetch-a-web-page.md#point-the-agent-at-a-list-of-pages).
55+
56+## Distribution
57+
58+- **The macOS binary is signed with a Developer ID and notarized by Apple.** Downloaded with a browser, it runs without a quarantine step (macOS checks the notarization online the first time). Downloaded with `curl`, as in the install guide, nothing changes.
59+- **Releases are now built on the maintainer's machine** and published as **prereleases**. The assets and the `SHA256SUMS` file keep the same names.
60+
61+## Demos
62+
63+- [`demos/gemma-coder/`](../demos/gemma-coder/README.en.md): a Go specialist for vibe coding on Gemma 4 26B-A4B (thinking on, gopls for `lsp`, `yolo: true`). It writes the code and its tests, runs `go build` and `go test`, and fixes what fails. Measured on nine cases, two runs each: 16/18.
64+- [`demos/muse-coder/`](../demos/muse-coder/README.en.md): the Swift counterpart on Muse-Glimmer 30B (`reasoning_strength: high`, sourcekit-lsp, SwiftPM). Measured on nine cases, two runs each: 17/18 before the last prompt change, which fixed `lsp rename` (2/2 afterwards). The other cases were not measured again with the final prompt.
65+- Moved under `demos/`: `dev.team/` → `demos/dev.team/`, `learning-rust/` → `demos/learning-rust/`, `sbx/` → `demos/use-with-sbx/`.
66+
67+## Documentation
68+
69+- New: [Run mini-me in a container](../docs/en/how-to/run-in-a-container.md).
70+- [Getting started with MCP](../docs/en/tutorials/mcp-getting-started.md) is rewritten (#180): the English page was a copy of the general getting-started tutorial. It now uses `demos/dmr/agent.dmr.mcp.yaml`, shows the `🔐` confirmations of `permissions: ask`, and works without any API key through Wikipedia. The Context7 and GitHub steps were not run with real keys.
71+- The `vulcan-salute` skill is loaded only once per conversation (#190): later requests reuse what the history holds, so the recap shows `📖 0 skill(s)` while the command still runs. Expected behaviour, not a bug.
72+
73+## Upgrading
74+
75+- **Agent files do not change.** Replace the binary: see the install guides for [macOS](../INSTALL.MACOS.md), [Linux](../INSTALL.LINUX.md) and [Windows](../INSTALL.WINDOWS.md).
76+- Two new commands, `mm dockerinit` and `mm sbxinit`.
77+- Files written by `mm init` from now on start their system prompt with `Your name is …`. Files written before are left alone.
78+- If you used the demos from their old folders, they are now under `demos/` (see above).
new file mode 100644
@@ -0,0 +1,78 @@
1+# mini-me v1.0.5
2+
3+*[Version française](v1.0.5.fr.md)*
4+
5+`mm dockerinit` and `mm sbxinit` write the files to run mini-me in a container or a Docker Sandbox, `mm init` gives the agent its name, two vibe-coding demos (Go and Swift), and a macOS binary signed and notarized by Apple.
6+
7+## New
8+
9+### `mm dockerinit`: run mini-me in a container (#189)
10+
11+`mm dockerinit` writes two files in the current directory:
12+
13+- a `Dockerfile` that installs the **latest release** of mm, for `amd64` and `arm64`. It finds the release through the `SHA256SUMS` of `releases/latest`, so a new release invalidates Docker's cache. The agent **never runs as root**: the image has a `mm` user.
14+- a `compose.yaml` that mounts your project and runs the container with your user and group ids (`MM_UID` and `MM_GID` override them).
15+
16+The output and the how-to show both ways of using it: a long-lived container (`docker compose up -d --wait`, then `docker compose exec … mm tui <agent>`) and a one-off run (`docker compose run --rm …`). For both architectures at once, build with `docker buildx`: putting `platforms` in `compose.yaml` fails without QEMU. An editor can talk to mm in the container over ACP through `docker exec -i`.
17+
18+If `Dockerfile` or `compose.yaml` already exists, mm asks before overwriting it (default: no). With `--yes`, or when the input ends, it writes nothing.
19+
20+Tried on linux/arm64 with Docker 29.8.1. **Not tried**: the amd64 build, reaching the model directly through `host.docker.internal`, macOS and Windows hosts.
21+
22+See [Run mini-me in a container](../docs/en/how-to/run-in-a-container.md).
23+
24+### `mm sbxinit`: a `sbxenv.yaml` for Docker Sandboxes (#191)
25+
26+`mm sbxinit` writes `sbxenv.yaml` in the current directory, for the [mini-me kit](https://rickub.com/bots-garden/mini-me-kit):
27+
28+- the kit at its **latest release**, read from the kit's releases page when the file is generated (`latest` with a warning when the page cannot be reached);
29+- `--workspace ./workspace` becomes an absolute path;
30+- `sandboxOptions.pullPolicy: always`.
31+
32+The agent file is looked for in the current directory, then in the workspace. When it is outside the workspace, `mm sbxinit` only warns: the how-to explains the two ways out (move the file, or mount its folder read-only and point `AGENT_CONFIG` at it). Existing files are handled like `mm dockerinit`'s.
33+
34+**Not tried**: `sbx env create` with the generated file.
35+
36+See [Generate a sbxenv.yaml with `mm sbxinit`](../docs/en/how-to/run-in-a-docker-sandbox.md#generate-a-sbxenvyaml-with-mm-sbxinit).
37+
38+### `mm init` gives the agent its name (#182)
39+
40+The system prompt of a file written by `mm init` now starts with `Your name is <name>.`, where the name is the one typed at `Agent name?`. Without a name, the agent takes the name of its usage (`Your name is docs.`). Asked its name on Mellum (Docker Model Runner), the agent answered with it 3 times out of 3.
41+
42+For the editors, `acp.name` is the name in lower case (`bob`) and `acp.title` the name followed by the usage (`Bob (developer)`).
43+
44+When the agent file already exists, `mm init` asks `Overwrite agent.yaml?` (default: no, and it asks for another file name). The new file is checked by loading it before it replaces the old one.
45+
46+### `mm init` links to the online documentation (#179)
47+
48+The comments of a file written by `mm init` point to the documentation as `https://` links, instead of paths that only made sense in a copy of the source code. The `Next:` hints are on four lines.
49+
50+### Point the agent at a list of pages (#181)
51+
52+No new setting: the list of URLs the agent should read goes in the system prompt. The new section of the how-to has an example. On Mellum with three URLs, the agent fetched the right page 4 times out of 4. The list **guides** the model but does not **restrict** it: asked for example.com, it fetched it 2 times out of 2 despite "Do not use any other URL". Blocking URLs would need a hook.
53+
54+See [Point the agent at a list of pages](../docs/en/how-to/fetch-a-web-page.md#point-the-agent-at-a-list-of-pages).
55+
56+## Distribution
57+
58+- **The macOS binary is signed with a Developer ID and notarized by Apple.** Downloaded with a browser, it runs without a quarantine step (macOS checks the notarization online the first time). Downloaded with `curl`, as in the install guide, nothing changes.
59+- **Releases are now built on the maintainer's machine** and published as **prereleases**. The assets and the `SHA256SUMS` file keep the same names.
60+
61+## Demos
62+
63+- [`demos/gemma-coder/`](../demos/gemma-coder/README.en.md): a Go specialist for vibe coding on Gemma 4 26B-A4B (thinking on, gopls for `lsp`, `yolo: true`). It writes the code and its tests, runs `go build` and `go test`, and fixes what fails. Measured on nine cases, two runs each: 16/18.
64+- [`demos/muse-coder/`](../demos/muse-coder/README.en.md): the Swift counterpart on Muse-Glimmer 30B (`reasoning_strength: high`, sourcekit-lsp, SwiftPM). Measured on nine cases, two runs each: 17/18 before the last prompt change, which fixed `lsp rename` (2/2 afterwards). The other cases were not measured again with the final prompt.
65+- Moved under `demos/`: `dev.team/` → `demos/dev.team/`, `learning-rust/` → `demos/learning-rust/`, `sbx/` → `demos/use-with-sbx/`.
66+
67+## Documentation
68+
69+- New: [Run mini-me in a container](../docs/en/how-to/run-in-a-container.md).
70+- [Getting started with MCP](../docs/en/tutorials/mcp-getting-started.md) is rewritten (#180): the English page was a copy of the general getting-started tutorial. It now uses `demos/dmr/agent.dmr.mcp.yaml`, shows the `🔐` confirmations of `permissions: ask`, and works without any API key through Wikipedia. The Context7 and GitHub steps were not run with real keys.
71+- The `vulcan-salute` skill is loaded only once per conversation (#190): later requests reuse what the history holds, so the recap shows `📖 0 skill(s)` while the command still runs. Expected behaviour, not a bug.
72+
73+## Upgrading
74+
75+- **Agent files do not change.** Replace the binary: see the install guides for [macOS](../INSTALL.MACOS.md), [Linux](../INSTALL.LINUX.md) and [Windows](../INSTALL.WINDOWS.md).
76+- Two new commands, `mm dockerinit` and `mm sbxinit`.
77+- Files written by `mm init` from now on start their system prompt with `Your name is …`. Files written before are left alone.
78+- If you used the demos from their old folders, they are now under `demos/` (see above).
added release-notes/v1.0.5.fr.md +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+# mini-me v1.0.5
2+
3+*[English version](v1.0.5.en.md)*
4+
5+`mm dockerinit` et `mm sbxinit` écrivent les fichiers pour faire tourner mini-me dans un conteneur ou un Docker Sandbox, `mm init` donne son nom à l'agent, deux démos de vibe coding (Go et Swift), et un binaire macOS signé et notarisé par Apple.
6+
7+## Nouveautés
8+
9+### `mm dockerinit` : mini-me dans un conteneur (#189)
10+
11+`mm dockerinit` écrit deux fichiers dans le dossier courant :
12+
13+- un `Dockerfile` qui installe la **dernière release** de mm, pour `amd64` et `arm64`. Il trouve la release par le `SHA256SUMS` de `releases/latest` : une nouvelle release invalide donc le cache de Docker. L'agent **ne tourne jamais en root** : l'image a un utilisateur `mm`.
14+- un `compose.yaml` qui monte votre projet et lance le conteneur avec vos identifiants d'utilisateur et de groupe (`MM_UID` et `MM_GID` les remplacent).
15+
16+La sortie de la commande et le guide montrent les deux façons de s'en servir : un conteneur qui reste lancé (`docker compose up -d --wait`, puis `docker compose exec … mm tui <agent>`) et une exécution ponctuelle (`docker compose run --rm …`). Pour les deux architectures à la fois, construisez avec `docker buildx` : `platforms` dans `compose.yaml` échoue sans QEMU. Un éditeur peut parler à mm dans le conteneur par ACP, via `docker exec -i`.
17+
18+Si `Dockerfile` ou `compose.yaml` existe déjà, mm demande avant de l'écraser (non par défaut). Avec `--yes`, ou quand l'entrée se termine, il n'écrit rien.
19+
20+Essayé sur linux/arm64 avec Docker 29.8.1. **Pas essayé** : la construction amd64, l'accès direct au modèle par `host.docker.internal`, les hôtes macOS et Windows.
21+
22+Voir [Faire tourner mini-me dans un conteneur](../docs/fr/how-to/run-in-a-container.md).
23+
24+### `mm sbxinit` : un `sbxenv.yaml` pour Docker Sandboxes (#191)
25+
26+`mm sbxinit` écrit `sbxenv.yaml` dans le dossier courant, pour le [kit mini-me](https://rickub.com/bots-garden/mini-me-kit) :
27+
28+- le kit à sa **dernière release**, lue sur la page des releases du kit au moment de générer le fichier (`latest`, avec un avertissement, quand la page est injoignable) ;
29+- `--workspace ./workspace` devient un chemin absolu ;
30+- `sandboxOptions.pullPolicy: always`.
31+
32+Le fichier d'agent est cherché dans le dossier courant, puis dans le workspace. S'il est hors du workspace, `mm sbxinit` avertit seulement : le guide explique les deux solutions (déplacer le fichier, ou monter son dossier en lecture seule et y faire pointer `AGENT_CONFIG`). Les fichiers existants sont traités comme avec `mm dockerinit`.
33+
34+**Pas essayé** : `sbx env create` avec le fichier généré.
35+
36+Voir [Générer un sbxenv.yaml avec `mm sbxinit`](../docs/fr/how-to/run-in-a-docker-sandbox.md#générer-un-sbxenvyaml-avec-mm-sbxinit).
37+
38+### `mm init` donne son nom à l'agent (#182)
39+
40+Le prompt système d'un fichier écrit par `mm init` commence maintenant par `Your name is <nom>.`, avec le nom tapé à `Agent name?`. Sans nom, l'agent prend celui de son usage (`Your name is docs.`). Interrogé sur son nom avec Mellum (Docker Model Runner), l'agent l'a donné 3 fois sur 3.
41+
42+Pour les éditeurs, `acp.name` est le nom en minuscules (`bob`) et `acp.title` le nom suivi de l'usage (`Bob (developer)`).
43+
44+Quand le fichier d'agent existe déjà, `mm init` demande `Overwrite agent.yaml?` (non par défaut, et il demande alors un autre nom de fichier). Le nouveau fichier est vérifié en le chargeant avant de remplacer l'ancien.
45+
46+### `mm init` renvoie vers la documentation en ligne (#179)
47+
48+Les commentaires d'un fichier écrit par `mm init` pointent vers la documentation par des liens `https://`, au lieu de chemins qui n'avaient de sens que dans une copie du code source. Les indications `Next:` tiennent sur quatre lignes.
49+
50+### Orienter l'agent vers une liste de pages (#181)
51+
52+Pas de nouveau réglage : la liste des URL que l'agent doit lire se met dans le prompt système. La nouvelle section du guide donne un exemple. Avec Mellum et trois URL, l'agent a récupéré la bonne page 4 fois sur 4. La liste **oriente** le modèle mais ne le **limite** pas : à qui demandait example.com, il l'a récupéré 2 fois sur 2 malgré « Do not use any other URL ». Bloquer des URL demanderait un hook.
53+
54+Voir [Orienter l'agent vers une liste de pages](../docs/fr/how-to/fetch-a-web-page.md#orienter-lagent-vers-une-liste-de-pages).
55+
56+## Distribution
57+
58+- **Le binaire macOS est signé avec un Developer ID et notarisé par Apple.** Téléchargé avec un navigateur, il se lance sans étape de quarantaine (macOS vérifie la notarisation en ligne au premier lancement). Téléchargé avec `curl`, comme dans le guide d'installation, rien ne change.
59+- **Les releases sont maintenant construites sur la machine du mainteneur** et publiées en **prerelease**. Les fichiers et `SHA256SUMS` gardent les mêmes noms.
60+
61+## Démos
62+
63+- [`demos/gemma-coder/`](../demos/gemma-coder/README.fr.md) : un spécialiste Go pour le vibe coding, sur Gemma 4 26B-A4B (réflexion activée, gopls pour `lsp`, `yolo: true`). Il écrit le code et ses tests, lance `go build` et `go test`, et corrige ce qui échoue. Mesuré sur neuf cas, deux essais chacun : 16/18.
64+- [`demos/muse-coder/`](../demos/muse-coder/README.fr.md) : son équivalent Swift, sur Muse-Glimmer 30B (`reasoning_strength: high`, sourcekit-lsp, SwiftPM). Mesuré sur neuf cas, deux essais chacun : 17/18 avant la dernière modification du prompt, qui a corrigé `lsp rename` (2/2 ensuite). Les autres cas n'ont pas été remesurés avec le prompt final.
65+- Déplacées sous `demos/` : `dev.team/` → `demos/dev.team/`, `learning-rust/` → `demos/learning-rust/`, `sbx/` → `demos/use-with-sbx/`.
66+
67+## Documentation
68+
69+- Nouveau : [Faire tourner mini-me dans un conteneur](../docs/fr/how-to/run-in-a-container.md).
70+- [Premiers pas avec MCP](../docs/fr/tutorials/mcp-premiers-pas.md) est corrigé (#180), ainsi que sa version anglaise, qui était une copie du tutoriel général. Il utilise `demos/dmr/agent.dmr.mcp.yaml`, montre les confirmations `🔐` de `permissions: ask`, et marche sans aucune clé API grâce à Wikipedia. Les étapes Context7 et GitHub n'ont pas été faites avec de vraies clés.
71+- La skill `vulcan-salute` n'est chargée qu'une fois par conversation (#190) : les demandes suivantes réutilisent ce que contient l'historique, donc le récapitulatif affiche `📖 0 skill(s)` alors que la commande s'exécute toujours. C'est le comportement attendu, pas un bug.
72+
73+## Mise à jour
74+
75+- **Les fichiers d'agent ne changent pas.** Remplacez le binaire : voir les guides d'installation pour [macOS](../INSTALL.MACOS.md), [Linux](../INSTALL.LINUX.md) et [Windows](../INSTALL.WINDOWS.md).
76+- Deux nouvelles commandes, `mm dockerinit` et `mm sbxinit`.
77+- Les fichiers écrits désormais par `mm init` commencent leur prompt système par `Your name is …`. Les fichiers écrits avant ne sont pas touchés.
78+- Si vous utilisiez les démos depuis leurs anciens dossiers, elles sont maintenant sous `demos/` (voir plus haut).
new file mode 100644
@@ -0,0 +1,78 @@
1+# mini-me v1.0.5
2+
3+*[English version](v1.0.5.en.md)*
4+
5+`mm dockerinit` et `mm sbxinit` écrivent les fichiers pour faire tourner mini-me dans un conteneur ou un Docker Sandbox, `mm init` donne son nom à l'agent, deux démos de vibe coding (Go et Swift), et un binaire macOS signé et notarisé par Apple.
6+
7+## Nouveautés
8+
9+### `mm dockerinit` : mini-me dans un conteneur (#189)
10+
11+`mm dockerinit` écrit deux fichiers dans le dossier courant :
12+
13+- un `Dockerfile` qui installe la **dernière release** de mm, pour `amd64` et `arm64`. Il trouve la release par le `SHA256SUMS` de `releases/latest` : une nouvelle release invalide donc le cache de Docker. L'agent **ne tourne jamais en root** : l'image a un utilisateur `mm`.
14+- un `compose.yaml` qui monte votre projet et lance le conteneur avec vos identifiants d'utilisateur et de groupe (`MM_UID` et `MM_GID` les remplacent).
15+
16+La sortie de la commande et le guide montrent les deux façons de s'en servir : un conteneur qui reste lancé (`docker compose up -d --wait`, puis `docker compose exec … mm tui <agent>`) et une exécution ponctuelle (`docker compose run --rm …`). Pour les deux architectures à la fois, construisez avec `docker buildx` : `platforms` dans `compose.yaml` échoue sans QEMU. Un éditeur peut parler à mm dans le conteneur par ACP, via `docker exec -i`.
17+
18+Si `Dockerfile` ou `compose.yaml` existe déjà, mm demande avant de l'écraser (non par défaut). Avec `--yes`, ou quand l'entrée se termine, il n'écrit rien.
19+
20+Essayé sur linux/arm64 avec Docker 29.8.1. **Pas essayé** : la construction amd64, l'accès direct au modèle par `host.docker.internal`, les hôtes macOS et Windows.
21+
22+Voir [Faire tourner mini-me dans un conteneur](../docs/fr/how-to/run-in-a-container.md).
23+
24+### `mm sbxinit` : un `sbxenv.yaml` pour Docker Sandboxes (#191)
25+
26+`mm sbxinit` écrit `sbxenv.yaml` dans le dossier courant, pour le [kit mini-me](https://rickub.com/bots-garden/mini-me-kit) :
27+
28+- le kit à sa **dernière release**, lue sur la page des releases du kit au moment de générer le fichier (`latest`, avec un avertissement, quand la page est injoignable) ;
29+- `--workspace ./workspace` devient un chemin absolu ;
30+- `sandboxOptions.pullPolicy: always`.
31+
32+Le fichier d'agent est cherché dans le dossier courant, puis dans le workspace. S'il est hors du workspace, `mm sbxinit` avertit seulement : le guide explique les deux solutions (déplacer le fichier, ou monter son dossier en lecture seule et y faire pointer `AGENT_CONFIG`). Les fichiers existants sont traités comme avec `mm dockerinit`.
33+
34+**Pas essayé** : `sbx env create` avec le fichier généré.
35+
36+Voir [Générer un sbxenv.yaml avec `mm sbxinit`](../docs/fr/how-to/run-in-a-docker-sandbox.md#générer-un-sbxenvyaml-avec-mm-sbxinit).
37+
38+### `mm init` donne son nom à l'agent (#182)
39+
40+Le prompt système d'un fichier écrit par `mm init` commence maintenant par `Your name is <nom>.`, avec le nom tapé à `Agent name?`. Sans nom, l'agent prend celui de son usage (`Your name is docs.`). Interrogé sur son nom avec Mellum (Docker Model Runner), l'agent l'a donné 3 fois sur 3.
41+
42+Pour les éditeurs, `acp.name` est le nom en minuscules (`bob`) et `acp.title` le nom suivi de l'usage (`Bob (developer)`).
43+
44+Quand le fichier d'agent existe déjà, `mm init` demande `Overwrite agent.yaml?` (non par défaut, et il demande alors un autre nom de fichier). Le nouveau fichier est vérifié en le chargeant avant de remplacer l'ancien.
45+
46+### `mm init` renvoie vers la documentation en ligne (#179)
47+
48+Les commentaires d'un fichier écrit par `mm init` pointent vers la documentation par des liens `https://`, au lieu de chemins qui n'avaient de sens que dans une copie du code source. Les indications `Next:` tiennent sur quatre lignes.
49+
50+### Orienter l'agent vers une liste de pages (#181)
51+
52+Pas de nouveau réglage : la liste des URL que l'agent doit lire se met dans le prompt système. La nouvelle section du guide donne un exemple. Avec Mellum et trois URL, l'agent a récupéré la bonne page 4 fois sur 4. La liste **oriente** le modèle mais ne le **limite** pas : à qui demandait example.com, il l'a récupéré 2 fois sur 2 malgré « Do not use any other URL ». Bloquer des URL demanderait un hook.
53+
54+Voir [Orienter l'agent vers une liste de pages](../docs/fr/how-to/fetch-a-web-page.md#orienter-lagent-vers-une-liste-de-pages).
55+
56+## Distribution
57+
58+- **Le binaire macOS est signé avec un Developer ID et notarisé par Apple.** Téléchargé avec un navigateur, il se lance sans étape de quarantaine (macOS vérifie la notarisation en ligne au premier lancement). Téléchargé avec `curl`, comme dans le guide d'installation, rien ne change.
59+- **Les releases sont maintenant construites sur la machine du mainteneur** et publiées en **prerelease**. Les fichiers et `SHA256SUMS` gardent les mêmes noms.
60+
61+## Démos
62+
63+- [`demos/gemma-coder/`](../demos/gemma-coder/README.fr.md) : un spécialiste Go pour le vibe coding, sur Gemma 4 26B-A4B (réflexion activée, gopls pour `lsp`, `yolo: true`). Il écrit le code et ses tests, lance `go build` et `go test`, et corrige ce qui échoue. Mesuré sur neuf cas, deux essais chacun : 16/18.
64+- [`demos/muse-coder/`](../demos/muse-coder/README.fr.md) : son équivalent Swift, sur Muse-Glimmer 30B (`reasoning_strength: high`, sourcekit-lsp, SwiftPM). Mesuré sur neuf cas, deux essais chacun : 17/18 avant la dernière modification du prompt, qui a corrigé `lsp rename` (2/2 ensuite). Les autres cas n'ont pas été remesurés avec le prompt final.
65+- Déplacées sous `demos/` : `dev.team/` → `demos/dev.team/`, `learning-rust/` → `demos/learning-rust/`, `sbx/` → `demos/use-with-sbx/`.
66+
67+## Documentation
68+
69+- Nouveau : [Faire tourner mini-me dans un conteneur](../docs/fr/how-to/run-in-a-container.md).
70+- [Premiers pas avec MCP](../docs/fr/tutorials/mcp-premiers-pas.md) est corrigé (#180), ainsi que sa version anglaise, qui était une copie du tutoriel général. Il utilise `demos/dmr/agent.dmr.mcp.yaml`, montre les confirmations `🔐` de `permissions: ask`, et marche sans aucune clé API grâce à Wikipedia. Les étapes Context7 et GitHub n'ont pas été faites avec de vraies clés.
71+- La skill `vulcan-salute` n'est chargée qu'une fois par conversation (#190) : les demandes suivantes réutilisent ce que contient l'historique, donc le récapitulatif affiche `📖 0 skill(s)` alors que la commande s'exécute toujours. C'est le comportement attendu, pas un bug.
72+
73+## Mise à jour
74+
75+- **Les fichiers d'agent ne changent pas.** Remplacez le binaire : voir les guides d'installation pour [macOS](../INSTALL.MACOS.md), [Linux](../INSTALL.LINUX.md) et [Windows](../INSTALL.WINDOWS.md).
76+- Deux nouvelles commandes, `mm dockerinit` et `mm sbxinit`.
77+- Les fichiers écrits désormais par `mm init` commencent leur prompt système par `Your name is …`. Les fichiers écrits avant ne sont pas touchés.
78+- Si vous utilisiez les démos depuis leurs anciens dossiers, elles sont maintenant sous `demos/` (voir plus haut).