From 276abdca06f780a54394515a69cddcdf441fa612 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Thu, 17 Sep 2026 18:14:34 +0300 Subject: [PATCH 1/8] Uninstall left a plain-SQL copy of the database behind. The pre-upgrade dump lives outside the volume on purpose, so removing the volumes never touched it, and the screen said everything was removed. Co-Authored-By: Claude Opus 5 (1M context) --- .../XBotRuntime/RuntimeController.swift | 6 +++++ .../Tests/XBotRuntimeTests/RuntimeTests.swift | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/apps/mac/Sources/XBotRuntime/RuntimeController.swift b/apps/mac/Sources/XBotRuntime/RuntimeController.swift index e9d4549..3b1f4d3 100644 --- a/apps/mac/Sources/XBotRuntime/RuntimeController.swift +++ b/apps/mac/Sources/XBotRuntime/RuntimeController.swift @@ -586,6 +586,12 @@ public actor RuntimeController { for volume in [Self.dataVolume, Self.workspaceVolume, Self.profilesVolume] { try? await driver.removeVolume(volume) } + // The pre-upgrade dump is a plain-SQL copy of the database, kept outside the volume so a + // rollback can use it — which is exactly why removing the volumes never removed it. + try? FileManager.default.removeItem(at: dumpURL) + try? FileManager.default.removeItem( + at: dumpURL.deletingLastPathComponent().appendingPathComponent("restore.log") + ) // Back to stopped, not notDetected: the runtime is still installed and still working — it // is only xBot's own data that is gone. state = .stopped diff --git a/apps/mac/Tests/XBotRuntimeTests/RuntimeTests.swift b/apps/mac/Tests/XBotRuntimeTests/RuntimeTests.swift index c53821e..6c88d86 100644 --- a/apps/mac/Tests/XBotRuntimeTests/RuntimeTests.swift +++ b/apps/mac/Tests/XBotRuntimeTests/RuntimeTests.swift @@ -790,6 +790,31 @@ extension RuntimeControllerTests { #expect(await controller.state == .stopped) } + /// The pre-upgrade dump is the database in plain SQL — agents, settings, everything the volume + /// held. It lives outside the volume on purpose, so removing the volumes never touched it, and + /// the screen said "Everything xBot stored has been removed" while a copy sat in Application + /// Support. The restore log beside it goes too. + @Test func uninstallRemovesThePreUpgradeDump() async throws { + let dumpURL = isolatedDumpURL() + let directory = dumpURL.deletingLastPathComponent() + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try Data("-- dump".utf8).write(to: dumpURL) + let logURL = directory.appendingPathComponent("restore.log") + try Data("log".utf8).write(to: logURL) + let controller = RuntimeController( + driver: FakeDriver(), + image: ImageReference(repository: "xbot/engine", tag: "1"), + health: { _ in EngineHealth(engineVersion: "0.0.5", schemaVersion: "0000") }, + ports: isolatedPortStore(), + dumpURL: dumpURL + ) + + await controller.uninstall() + + #expect(!FileManager.default.fileExists(atPath: dumpURL.path)) + #expect(!FileManager.default.fileExists(atPath: logURL.path)) + } + /// Uninstall runs once and cannot ask the person to try again, so a step whose work is already /// done must not stop the rest. Without this, a half-uninstalled machine keeps its volumes. @Test func uninstallSurvivesAnythingAlreadyGone() async { From c1a0537fd60220ce0922a408842d33a5b7c2f0d1 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Thu, 17 Sep 2026 18:14:34 +0300 Subject: [PATCH 2/8] Ship an uninstaller in the DMG, for an app already in the Trash. Co-Authored-By: Claude Opus 5 (1M context) --- docs/11-packaging-and-updates.md | 8 +++- docs/12-roadmap.md | 4 +- scripts/create-dmg.sh | 2 + scripts/uninstall-xbot.command | 69 ++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100755 scripts/uninstall-xbot.command diff --git a/docs/11-packaging-and-updates.md b/docs/11-packaging-and-updates.md index c32d9e0..bf526f8 100644 --- a/docs/11-packaging-and-updates.md +++ b/docs/11-packaging-and-updates.md @@ -300,13 +300,17 @@ is a bad citizen with a reputation problem. logins" 4. Remove Keychain items 5. Remove `UserDefaults` -6. Offer to move the app to the Trash +6. Remove the pre-upgrade database dump in Application Support — a plain-SQL copy of the database, + kept outside the volume so rollback can use it, and therefore untouched by step 3 +7. Tell the person they can now drag the app to the Trash. **Not done for them**: moving a running + app's own bundle is not something an app should try, so the screen says it instead **Does not remove the container runtime**, because the user may have installed it for something else. It says so. **Also ship a standalone uninstaller script** in the DMG for the user who already dragged the app to -the Trash and then found the volumes. ⚠️ Yes, this is a terminal — it is the one place the no-terminal +the Trash and then found the volumes. It is `Uninstall xBot.command`, copied from +`scripts/uninstall-xbot.command`, and it mirrors `AppState.uninstall()` step for step. ⚠️ Yes, this is a terminal — it is the one place the no-terminal promise yields, because the alternative is orphaned data with no way to remove it. It is documented on the website, not in the app. diff --git a/docs/12-roadmap.md b/docs/12-roadmap.md index 7804bcc..9918759 100644 --- a/docs/12-roadmap.md +++ b/docs/12-roadmap.md @@ -258,7 +258,9 @@ it.** still open.** - Engine update flow including rollback, and the migration-rollback decision. **Done** — the dump, per docs/11's recommendation: taken before the new image can migrate, restored on rollback. -- Uninstall, complete. +- Uninstall, complete. **Done** — Settings → Advanced removes the container, volumes, Keychain items, + preferences and the pre-upgrade dump (which it used to leave behind); `Uninstall xBot.command` ships + in the DMG for somebody who already trashed the app. - Admin surfaces embedded (webview). **Plugins admin ships; the audit trail is native rather than embedded, per ADR-0004's exception; credentials, playground, etc. still open.** - Settings: General, Models, Agents, Computer, Advanced, Updates. **Built** — all seven panes, in the diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index db3b7b1..23a5f11 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -27,6 +27,8 @@ mkdir -p "${STAGE}" cp -R "${APP}" "${STAGE}/" ln -s /Applications "${STAGE}/Applications" +cp "${ROOT}/scripts/uninstall-xbot.command" "${STAGE}/Uninstall xBot.command" +chmod +x "${STAGE}/Uninstall xBot.command" hdiutil create -volname "${VOLUME}" -srcfolder "${STAGE}" -ov -format UDZO "${DMG}" rm -rf "${STAGE}" diff --git a/scripts/uninstall-xbot.command b/scripts/uninstall-xbot.command new file mode 100755 index 0000000..12fbe93 --- /dev/null +++ b/scripts/uninstall-xbot.command @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Remove everything xBot stored on this Mac, for somebody who already dragged the app to the Trash. +# +# Ships in the DMG. docs/11-packaging-and-updates.md: this is the one place the no-terminal promise +# yields, because the alternative is gigabytes of orphaned volumes with no way to remove them. The +# in-app path (Settings → Advanced → Uninstall) does the same, and is what everybody else should use. +# +# Mirrors AppState.uninstall() and RuntimeController.uninstall(). Change one, change the other. +# +# Like the app, it leaves the container runtime alone — Docker or Colima may be in use for something +# else. +set -uo pipefail + +SUPPORT="${HOME}/Library/Application Support/xBot/runtime" + +cat <<'EOF' +This removes everything xBot stored on this Mac: + + - your conversations and your agents + - the websites your agents are signed in to + - the keys xBot saved in your Keychain + - xBot's preferences + +It does not remove Docker or Colima. It cannot be undone. +EOF +printf '\nType "remove" to continue: ' +read -r answer +if [[ "${answer}" != "remove" ]]; then + echo "Nothing was removed." + exit 0 +fi + +if pgrep -xq XBot; then + echo "Quit xBot first, then run this again." + exit 1 +fi + +# The Docker CLI xBot installed, or whichever one is on the PATH. +DOCKER="${SUPPORT}/bin/docker" +[[ -x "${DOCKER}" ]] || DOCKER="$(command -v docker || true)" + +if [[ -n "${DOCKER}" ]] && "${DOCKER}" info >/dev/null 2>&1; then + "${DOCKER}" rm -f xbot-engine >/dev/null 2>&1 + for volume in xbot-data xbot-workspace xbot-profiles; do + "${DOCKER}" volume rm -f "${volume}" >/dev/null 2>&1 + done + echo "Removed the engine and its data." +else + # Said, not skipped quietly: the volumes are the gigabytes this script exists for. + echo "Docker is not running, so the engine's data could not be removed." + echo "Start Docker Desktop or Colima, then run this again." +fi + +# Every service xBot writes, by name — never a wildcard sweep of the login Keychain. One service can +# hold several accounts (a key per provider), so each is deleted until none is left. +for service in dev.xbot.provider-key dev.xbot.engine-token dev.xbot.key-encryption-key \ + dev.xbot.intelligence-key dev.xbot.copilotkit-license; do + while security delete-generic-password -s "${service}" >/dev/null 2>&1; do :; done +done +echo "Removed xBot's Keychain items." + +# The pre-upgrade dump is a plain-SQL copy of the database, kept outside the volume on purpose. +rm -f "${SUPPORT}/engine-pre-upgrade.sql" "${SUPPORT}/restore.log" + +defaults delete dev.xbot.app >/dev/null 2>&1 +echo "Removed xBot's preferences." + +echo +echo "Done." From 1d76e72ade6fea3952d354db07ef5fb4d5da1f3d Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:38:49 +0300 Subject: [PATCH 3/8] Every admin surface was already one webview away, and unreachable. Upstream's admin has its own sidebar, so opening it at /admin reaches credentials, computers, the playground and the rest. Only the door was missing. Co-Authored-By: Claude Opus 5 (1M context) --- apps/mac/Sources/XBotApp/XBotApp.swift | 2 +- apps/mac/Sources/XBotUI/Settings/SettingsRootView.swift | 8 +++++++- docs/03-openbot-fork.md | 2 +- docs/09-ui-spec.md | 4 ++-- docs/12-roadmap.md | 5 +++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/mac/Sources/XBotApp/XBotApp.swift b/apps/mac/Sources/XBotApp/XBotApp.swift index 6a74b36..c229415 100644 --- a/apps/mac/Sources/XBotApp/XBotApp.swift +++ b/apps/mac/Sources/XBotApp/XBotApp.swift @@ -124,7 +124,7 @@ struct XBotApp: App { } } - Window(String(localized: "Plugins"), id: "plugins-admin") { + Window(String(localized: "Engine Admin"), id: "plugins-admin") { PluginsAdminView() .environment(state) } diff --git a/apps/mac/Sources/XBotUI/Settings/SettingsRootView.swift b/apps/mac/Sources/XBotUI/Settings/SettingsRootView.swift index 8c02665..f96ecad 100644 --- a/apps/mac/Sources/XBotUI/Settings/SettingsRootView.swift +++ b/apps/mac/Sources/XBotUI/Settings/SettingsRootView.swift @@ -49,13 +49,19 @@ public struct AdvancedSettingsView: View { state.preparePluginsAdmin() openWindow(id: "plugins-admin") } + // One webview for every admin surface: upstream's admin has its own sidebar, so its index + // reaches credentials, computers, the playground and the rest without wiring each. + Button(String(localized: "Engine admin…")) { + state.preparePluginsAdmin(path: "admin") + openWindow(id: "plugins-admin") + } } header: { Text(String(localized: "Admin")) } footer: { Text( String( localized: - "Connect third-party services and choose which agents may use their tools. Opens the engine's plugins manager." + "Plugins connects third-party services and chooses which agents may use their tools. Engine admin opens the engine's own tools: credentials, computers, the playground, and people." ) ) } diff --git a/docs/03-openbot-fork.md b/docs/03-openbot-fork.md index 2ecc1c3..afeec77 100644 --- a/docs/03-openbot-fork.md +++ b/docs/03-openbot-fork.md @@ -334,7 +334,7 @@ security-sensitive codebase within a year. | Local-token auth | 3–4 days | **Done** — `EngineTokenStore`, bearer on loopback | | Config surface → app settings | 1–2 weeks | **Done** — `EngineEnvironment` + [`env-mapping.md`](env-mapping.md) | | Port negotiation + runtime driver | 1–2 weeks | **Done** — `DockerDriver`, adoption, `RuntimeController` | -| Admin webview embedding | 3–4 days | **Partially done** — plugins admin webview ships; other admin surfaces still open | +| Admin webview embedding | 3–4 days | **Done** — one webview; Plugins opens at `/admin/plugins`, Engine admin at `/admin`, whose sidebar reaches the rest | | **Engine total** | **~9–14 weeks** | | The Mac client is estimated separately in [12-roadmap.md](12-roadmap.md). The two streams can run in diff --git a/docs/09-ui-spec.md b/docs/09-ui-spec.md index 58a98fb..1f24cd6 100644 --- a/docs/09-ui-spec.md +++ b/docs/09-ui-spec.md @@ -26,7 +26,7 @@ The app drives a `RuntimeController` and `HTTPEngineClient` in production, and s | Design system | Built — tokens, aurora field, frosted glass, Reduce Motion and Reduce Transparency inside tokens | | Onboarding | **Built (M6 in progress)** — five steps, Colima install-for-me, engine adoption, provider keys, handoff to main window. VM clean-machine validation still open | | Settings | **In the main window, not a separate scene** — the gear at the foot of the rail, ⌘, and Escape. General, Models, **Agents** (defaults), **Computer** (policy presets + boundaries admin), **Usage** (placeholder), Updates, Advanced | -| Plugins & admin | **Partial** — native grant toggles in agent settings; Plugins admin window (`WKWebView` at `/admin/plugins` with bearer injection). **The audit trail is native** (Settings → Audit), which is ADR-0004's one exception to the webview rule. Credentials, playground and the rest **not embedded** | +| Plugins & admin | **Partial** — native grant toggles in agent settings; Plugins admin window (`WKWebView` at `/admin/plugins` with bearer injection). **The audit trail is native** (Settings → Audit), which is ADR-0004's one exception to the webview rule. Credentials, playground and the rest reached from **Engine admin…** in the same window | ## The main window @@ -423,7 +423,7 @@ from Settings → Advanced, not inside the main window. | --- | --- | | **Plugins (full manager)** | `WKWebView` at the engine's `/admin/plugins` — OAuth setup, catalogue, per-tool config | | **Plugin grants (per agent)** | Native — **What it can reach** and **Handoff grants** in the panel's Agent settings | -| Everything else in the table above | **Not embedded yet** — same webview pattern when added | +| **Everything else** — credentials, boundaries, computers, skills, components, playground, people, identity providers | Same webview, opened at `/admin` from Settings → Advanced → **Engine admin…**. Upstream's admin sidebar reaches each one, so no per-surface wiring | The webview injects the loopback bearer token at document start so the upstream React admin can call `/api` without a sign-in flow. The token never appears in page-visible UI. diff --git a/docs/12-roadmap.md b/docs/12-roadmap.md index 9918759..30d20dc 100644 --- a/docs/12-roadmap.md +++ b/docs/12-roadmap.md @@ -221,7 +221,7 @@ digest from `manifests/engine-stable.json`. it works against a live engine and not only the stub. It used to reach the message bubble and nowhere else, and `activity(for:)` returns an empty list on HTTP by design, so the panel promised "commands, files, and pages will show up here" and nothing could deliver it. -- Plugins admin webview + native grant toggles (partial — other admin surfaces still open). +- Plugins admin webview + native grant toggles. Other admin surfaces via **Engine admin…** (same webview). **Done when:** create an agent in the app, send a message, watch it browse, take control, hand it back — all native. `scripts/verify-m5-handoff.sh` covers the smoke path against a running engine. @@ -262,7 +262,8 @@ it.** preferences and the pre-upgrade dump (which it used to leave behind); `Uninstall xBot.command` ships in the DMG for somebody who already trashed the app. - Admin surfaces embedded (webview). **Plugins admin ships; the audit trail is native rather than - embedded, per ADR-0004's exception; credentials, playground, etc. still open.** + embedded, per ADR-0004's exception; every other surface opens in the same window at `/admin`, whose own sidebar reaches credentials, + computers, playground and the rest.** - Settings: General, Models, Agents, Computer, Advanced, Updates. **Built** — all seven panes, in the main window rather than a separate scene. - The honest v1 limitations stated in the UI: shared browser, shared workspace. **Done** — Settings → From bd66d8f34e1e80520af13e3adfae25e679b92a17 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:40:07 +0300 Subject: [PATCH 4/8] A website, so the download has somewhere to live. One file, no build step. Says where conversation history goes before anybody downloads anything, and documents the uninstaller that docs/11 keeps out of the app. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/pages.yml | 34 ++++++++ docs/12-roadmap.md | 3 +- docs/13-launch-checklist.md | 14 +-- site/index.html | 166 ++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 site/index.html diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ad5da87 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,34 @@ +# Publish site/ to GitHub Pages. Needs Pages enabled with "Source: GitHub Actions" once, by hand. +name: Website + +on: + push: + branches: [master] + paths: ["site/**", ".github/workflows/pages.yml"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + # The banner lives in assets/ and is not duplicated into site/. + - run: cp assets/banner.png site/banner.png + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: deploy + uses: actions/deploy-pages@v4 diff --git a/docs/12-roadmap.md b/docs/12-roadmap.md index 30d20dc..7f89759 100644 --- a/docs/12-roadmap.md +++ b/docs/12-roadmap.md @@ -268,7 +268,8 @@ it.** main window rather than a separate scene. - The honest v1 limitations stated in the UI: shared browser, shared workspace. **Done** — Settings → Computer, in the wording docs/10-security.md specifies. -- Website with the download and the security explanation. +- Website with the download and the security explanation. **Built** — `site/index.html`, published by + `.github/workflows/pages.yml`; enabling Pages is a one-time repository setting. **The first-run supply chain is verified anonymously**, which is the part of "installs from the website and uses it" that does not need a person. From a shell holding no credentials: the manifest diff --git a/docs/13-launch-checklist.md b/docs/13-launch-checklist.md index dd426cc..b8e61fb 100644 --- a/docs/13-launch-checklist.md +++ b/docs/13-launch-checklist.md @@ -163,12 +163,16 @@ bugs, and they are only visible on the first run. --- -## 7. The website +## 7. The website — **built; needs Pages turned on** -The last item in M7 and the only one with no code in this repository. It needs the download, the -security explanation, and — per ADR-0007 — the fact that conversation history is stored by -CopilotKit, said plainly rather than buried. The README already carries that wording; reuse it -rather than writing a second version that can drift. +`site/index.html` is the page: one file, no build step. It carries the download (pointing at +`releases/latest`), the security explanation, the uninstall instructions — per docs/11 the standalone +uninstaller is documented here and not in the app — and, per ADR-0007, the fact that conversation +history is stored by CopilotKit, in the README's wording rather than a second version that can drift. + +`.github/workflows/pages.yml` publishes it on every push to master that touches `site/`. **It needs +you once:** repository → Settings → Pages → Source: **GitHub Actions**. Until then the workflow fails +at the deploy step. The download link is only useful once step 4 has published a release. --- diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..40e9143 --- /dev/null +++ b/site/index.html @@ -0,0 +1,166 @@ + + + + + + xBot — your own AI coworkers, on your own Mac + + + + + + + +
+ xBot +

Your own AI coworkers, on your own Mac

+

+ Create agents, give them a computer, watch them work, and take the wheel when you want to. +

+ Download for Mac +

Free and open source. macOS 14 Sonoma or later, Apple Silicon or Intel.

+
+ +
+

What it is

+

+ xBot is a native Mac app. You download it, drag it to Applications, and open it. It walks you + through everything else — there is no terminal, no configuration file to edit, and no log to + read. +

+

+ Behind the app, a full agent platform runs in containers on your Mac. Each agent gets its own + computer with its own browser, its own files, and only the tools you grant it. Every action an + agent takes is checked against a policy before it happens, and recorded after. +

+

+ Bring any model: OpenAI, Anthropic, Google, xAI, or one running locally through + Ollama. xBot supplies no intelligence of its own, so there is + nothing to lock you in. +

+ +

Where your things live

+

+ Your agents, their files, and the websites they are signed in to stay on your Mac. Your model + keys are held in the macOS Keychain — never in a file, never in a log. The engine listens on + loopback only, so nothing outside your machine can reach it, and a bearer token is required + even from inside it. +

+
+

+ One exception, and we would rather you read it here than find it later. In + version 1, your conversation history is stored by + CopilotKit, the service xBot's engine is built on. Your + agents' files and browsers are not — only the transcript. Onboarding says so before you type + a key, and + ADR-0007 + records why and what replaces it. +

+
+ +

Removing it

+

+ Settings → Advanced → Remove all xBot data deletes the containers, the + volumes, the Keychain items and the preferences, then tells you to drag the app to the Trash. + It leaves Docker or Colima alone, since you may be using those for something else. +

+

+ If you already moved the app to the Trash and want the rest gone, the disk image contains + Uninstall xBot.command. Open the .dmg and double-click it. That is + the one place xBot asks you to use a terminal, and it exists so that orphaned data is never + unremovable. +

+ +

Built on OpenBot

+

+ xBot's engine is a fork of OpenBot by + CopilotKit, used under the MIT licence. Copyright © 2026 + CopilotKit. +

+
+ + + + From 3f92d690e350d3eb5d4c4f3f1a13793fb6adc4ee Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:41:07 +0300 Subject: [PATCH 5/8] Docs: Usage shows real counts, and Advanced has a second door. Co-Authored-By: Claude Opus 5 (1M context) --- docs/09-ui-spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/09-ui-spec.md b/docs/09-ui-spec.md index 1f24cd6..c5b43cc 100644 --- a/docs/09-ui-spec.md +++ b/docs/09-ui-spec.md @@ -362,7 +362,7 @@ A separate floating panel was tried first and removed: it put settings somewhere and find, and it hid the rail — so which agent was selected stopped being visible while its model was being changed. -**Shipped today:** General, Models, **Agents** (default model + description; shared preamble deferred), **Computer** (auto-review + preset deny rules + boundaries admin webview), **Usage** (honest placeholder — engine accounting still open), Updates (Sparkle scaffold + engine install with health rollback), Advanced (Plugins, uninstall). +**Shipped today:** General, Models, **Agents** (default model + description; shared preamble deferred), **Computer** (auto-review + preset deny rules + boundaries admin webview), **Usage** (real per-agent token counts; the agent sums `usage_metadata` across a turn and emits `xbot.usage`), Updates (Sparkle scaffold + engine install with health rollback), Advanced (Plugins, Engine admin, uninstall). | Tab | Contents | | --- | --- | From e4adccf0f9f6ccfc1891174303f40bbebb0dc956 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:42:39 +0300 Subject: [PATCH 6/8] Sparkle keys without downloading Sparkle. Co-Authored-By: Claude Opus 5 (1M context) --- docs/13-launch-checklist.md | 6 ++++-- scripts/generate-sparkle-keys.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 scripts/generate-sparkle-keys.sh diff --git a/docs/13-launch-checklist.md b/docs/13-launch-checklist.md index b8e61fb..02e14ef 100644 --- a/docs/13-launch-checklist.md +++ b/docs/13-launch-checklist.md @@ -68,8 +68,10 @@ wrong identity string and is a missing certificate. Updates are dead until this exists, and it cannot be retrofitted: an app shipped without the public key baked in can never verify an update, so v1.0 users would be stranded on v1.0 forever. -1. Generate the EdDSA key pair with Sparkle's tool (`generate_keys` from the Sparkle release - archive). It writes the private key to your login Keychain and prints the public key. +1. Run `scripts/generate-sparkle-keys.sh`. It writes the private key to your login Keychain and + prints the public key. There is nothing to download — Sparkle's own `generate_keys` is already in + the SwiftPM artifacts, and the script only finds it. Running it twice is safe: it prints the + existing public key rather than replacing a key your shipped builds were signed against. 2. Add two more secrets: `SPARKLE_EDDSA_PRIVATE_KEY` (the private key) and `XBOT_SPARKLE_PUBLIC_KEY` (the public one, which gets baked into the bundle). 3. Decide where the appcast lives and set `XBOT_APPCAST_URL` to it. GitHub Releases plus a raw file diff --git a/scripts/generate-sparkle-keys.sh b/scripts/generate-sparkle-keys.sh new file mode 100755 index 0000000..c97eca9 --- /dev/null +++ b/scripts/generate-sparkle-keys.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Print the Sparkle EdDSA public key, generating the pair on first run. +# +# docs/13-launch-checklist.md step 3. Sparkle's own tool is already in the SwiftPM artifacts, so +# there is nothing to download: this only finds it and runs it. +# +# The private key goes into your login Keychain, which is where Sparkle's tool puts it. Back it up — +# losing it strands every shipped version, because an app cannot verify an update signed by a key it +# has never seen. +set -euo pipefail + +MAC="$(cd "$(dirname "$0")/../apps/mac" && pwd)" + +GENERATE_KEYS="$(find "${MAC}/.build/artifacts/sparkle" -name generate_keys -type f 2>/dev/null | head -1)" +if [[ -z "${GENERATE_KEYS}" ]]; then + echo "Sparkle's generate_keys is not built yet — run: cd apps/mac && swift build -c release" >&2 + exit 1 +fi + +# -p prints the public key for a pair that already exists, and says so if there is none, so a second +# run never quietly replaces the key every shipped build was signed against. +if "${GENERATE_KEYS}" -p 2>/dev/null; then + echo + echo "That is XBOT_SPARKLE_PUBLIC_KEY. The private key is already in your login Keychain." + exit 0 +fi + +"${GENERATE_KEYS}" From bb303f4330ccd4e5bac365e5a9910d4155350291 Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:42:51 +0300 Subject: [PATCH 7/8] The Sparkle private key needs exporting before it can be a secret. Co-Authored-By: Claude Opus 5 (1M context) --- docs/13-launch-checklist.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/13-launch-checklist.md b/docs/13-launch-checklist.md index 02e14ef..c7a93a1 100644 --- a/docs/13-launch-checklist.md +++ b/docs/13-launch-checklist.md @@ -72,8 +72,11 @@ key baked in can never verify an update, so v1.0 users would be stranded on v1.0 prints the public key. There is nothing to download — Sparkle's own `generate_keys` is already in the SwiftPM artifacts, and the script only finds it. Running it twice is safe: it prints the existing public key rather than replacing a key your shipped builds were signed against. -2. Add two more secrets: `SPARKLE_EDDSA_PRIVATE_KEY` (the private key) and `XBOT_SPARKLE_PUBLIC_KEY` - (the public one, which gets baked into the bundle). +2. Add two more secrets: `SPARKLE_EDDSA_PRIVATE_KEY` and `XBOT_SPARKLE_PUBLIC_KEY` (the public one, + which gets baked into the bundle). The private key is in the Keychain, not on disk, so export it + first — `apps/mac/.build/artifacts/sparkle/Sparkle/bin/generate_keys -x sparkle-private.key` — + paste the file's contents into the secret, and then delete the file. That export is the key + itself: anything holding it can sign an update your users' apps will install. 3. Decide where the appcast lives and set `XBOT_APPCAST_URL` to it. GitHub Releases plus a raw file in the repo is enough to start; `XBOT_RELEASE_DOWNLOAD_PREFIX` is optional and only needed if the DMG is served from somewhere other than the appcast's own host. From 53c6f5a4a44ac74b01f2ee225eb9ac838805b61b Mon Sep 17 00:00:00 2001 From: MasterYoav Date: Fri, 18 Sep 2026 09:43:48 +0300 Subject: [PATCH 8/8] The admin window is not only Plugins now, so it should not say so. Co-Authored-By: Claude Opus 5 (1M context) --- apps/mac/Sources/XBotUI/Admin/PluginsAdminView.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/mac/Sources/XBotUI/Admin/PluginsAdminView.swift b/apps/mac/Sources/XBotUI/Admin/PluginsAdminView.swift index 1be9876..0cdd865 100644 --- a/apps/mac/Sources/XBotUI/Admin/PluginsAdminView.swift +++ b/apps/mac/Sources/XBotUI/Admin/PluginsAdminView.swift @@ -2,7 +2,8 @@ import SwiftUI import XBotCore import XBotEngine -/// The upstream plugins admin UI, embedded per ADR-0004. +/// The upstream admin UI, embedded per ADR-0004. One window for every admin surface: the deep link +/// decides which, so this opens at plugins or at the admin index. public struct PluginsAdminView: View { @Environment(AppState.self) private var state @@ -26,9 +27,14 @@ public struct PluginsAdminView: View { Image(systemName: "puzzlepiece.extension") .font(.system(size: 36, weight: .light)) .foregroundStyle(Palette.textSecondary) - Text(String(localized: "Plugins need a running engine")) + Text(String(localized: "These tools need a running engine")) .sectionTitle() - Text(String(localized: "Start the engine from onboarding or the main window, then open Plugins again.")) + Text( + String( + localized: + "The engine's admin tools are served by the engine itself. Start it from the main window, then open this again." + ) + ) .bodyText() .foregroundStyle(Palette.textSecondary) .multilineTextAlignment(.center)