Skip to content

Commit 544fa7f

Browse files
committed
chore(release): 0.6.0
- fix(config): a missing llm.api_key is filled from the provider's own variable — DEEPSEEK_API_KEY for *.deepseek.com, OPENAI_API_KEY for every other host — so a config pointed at OpenAI never goes out with DeepSeek's key. - ci(android): attach the APK to a release only when it was signed with the release key; a debug-signed build stays a workflow artifact. - version 0.6.0 in pyproject, __init__, package.json, CITATION.cff, Android versionName/versionCode (600); Development Status :: 4 - Beta; CHANGELOG.
1 parent fa8d5f7 commit 544fa7f

12 files changed

Lines changed: 68 additions & 16 deletions

File tree

‎.github/workflows/android.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,12 @@ jobs:
7979
echo "tag v$tag does not match android versionName ${{ steps.apk.outputs.version }}"; exit 1
8080
fi
8181
82+
# Only an APK signed with the release key goes on a release: a debug-signed one would
83+
# install, but could never be updated by (or update) a properly signed build. Without
84+
# the secrets the APK is a workflow artifact and the maintainer attaches the one built
85+
# with the key kept outside the repository (docs/android.md).
8286
- name: Attach to the release
83-
if: startsWith(github.ref, 'refs/tags/v')
87+
if: startsWith(github.ref, 'refs/tags/v') && secrets.ANDROID_KEYSTORE_B64 != ''
8488
uses: softprops/action-gh-release@v2
8589
with:
8690
files: |

‎CHANGELOG.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ All notable changes to OpenMuse. The format follows [Keep a Changelog](https://k
44

55
## [Unreleased]
66

7+
## [0.6.0] — 2026-09-23
8+
9+
The app as Meta Muse looks, and an Android app. A plush avatar that moves while it works, one blue accent, a feed written for you, a stop button — and `openmuse.apk` on every release, so the phone gets notifications with the app closed.
10+
711
### Added
812

13+
- **An Android app.** `android/` is a native shell around the web app: connect by scanning the QR code `openmuse serve` prints (or paste the link), then the same app in a WebView — with the file picker for attachments, downloads to the phone, links opening in the real browser, and plain HTTP on the LAN. What the browser could not do: a foreground service keeps one WebSocket open to your server while the app is closed, so approvals, questions and the last word of background work arrive as notifications that open the right chat; a resolved approval takes its notification down; reconnects after a network change or a reboot. Inside the app, *Settings → Notifications* shows the phone's switch instead of Web Push, and *About* has *Disconnect from this server*. Built by `.github/workflows/android.yml` and attached to each release as `openmuse-<version>.apk` and as `openmuse.apk`, so `https://github.com/OpenMuseAgent/OpenMuse/releases/latest/download/openmuse.apk` always points at the current one (Android 8.0+). See [docs/android.md](docs/android.md).
914
- **A feed written for you.** The Feed tab opens with *Feed instructions* — one prompt in your words: topics to follow, nudges on your goals, a morning plan, the tone — and posts the agent writes from that and what it knows about you (memory, goals, today's calendar, the last conversation): a few short Markdown posts a day, in your language, each with a follow-up you can send to the chat. A new batch is written once a day while background work is on and outside quiet hours, or on demand (*New posts*); the phone gets one notification per batch. What happened while you were away stays below, as before. `GET /api/feed/posts`, `PUT /api/feed/instructions`, `POST /api/feed/posts/refresh`, `DELETE /api/feed/posts/{id}`.
1015
- **A stop button.** The status sheet (tap the avatar) shows *Stop* while a run is on: the run ends, a card waiting on you expires, what was queued behind it is dropped, and the transcript is left in a shape the model can continue from. `POST /api/threads/{id}/stop`.
1116
- **Plush avatars.** Six dolls to pick from in setup and in Settings, with an idle breath, a sway while working and a hop while waiting for you (still under *reduce motion*); tap one and it wiggles. An emoji on a colour is still there for anyone who prefers it. `profile.avatar` in the settings API; old profiles keep their emoji.
@@ -14,8 +19,13 @@ All notable changes to OpenMuse. The format follows [Keep a Changelog](https://k
1419

1520
- **The look, after Meta Muse.** Near-white and near-black surfaces, one blue accent, grey bubbles for the agent and light-blue for you with the time between groups of messages, a floating icon-only tab bar, a centred avatar header with the live status line, and a single *+ Message* composer. Type is [Figtree](https://github.com/erikdkennedy/figtree) (OFL), bundled. Goals list as *Tracking* / *Goals* rows with a check circle and one line of status; Ideas are rows grouped by area; the approval card reads "*{name} wants to … at host*" with *Deny* / *Allow*; artifact cards and the Library show live previews of pages and pictures; the status sheet has a four-way control (Approvals, Activity, Permissions, Upcoming).
1621
- **It is called OpenMuse.** The default agent name, the CLI prompt, the console, push notifications and every screen say *OpenMuse* unless you named it; "Meta Muse" is used only when comparing with the original.
22+
- **The README** is short and scannable: a start-here table, what it can do, install, a three-command quick start, the Android download, deploy, then the details. Screenshots are of the new look; links are absolute so PyPI renders them. The package is marked Beta.
23+
24+
### Fixed
1725

18-
- **An Android app.** `android/` is a native shell around the web app: connect by scanning the QR code `openmuse serve` prints (or paste the link), then the same app in a WebView — with the file picker for attachments, downloads to the phone, links opening in the real browser, and plain HTTP on the LAN. What the browser could not do: a foreground service keeps one WebSocket open to your server while the app is closed, so approvals, questions and the last word of background work arrive as notifications that open the right chat; a resolved approval takes its notification down; reconnects after a network change or a reboot. Inside the app, *Settings → Notifications* shows the phone's switch instead of Web Push, and *About* has *Disconnect from this server*. Built by `.github/workflows/android.yml` and attached to each release as `openmuse-<version>.apk` (Android 8.0+). See [docs/android.md](docs/android.md).
26+
- **Sandboxed commands failed from a relative workspace.** With the config's default `workspace = "./workspace"`, bubblewrap was told to change into `workspace` *after* pivoting to the new root, where no such directory exists — every `shell` and `python_execute` call ended in "Can't chdir to workspace" while the sandbox self-check (which uses `/`) passed. The directory is now made absolute first.
27+
- **The wrong provider's key was never sent.** With `llm.api_key` empty, `DEEPSEEK_API_KEY` used to stand in whatever the `base_url` — a config pointed at OpenAI could go out with DeepSeek's key. Now DeepSeek's variable is used for `*.deepseek.com` only and `OPENAI_API_KEY` for every other host (OpenAI, OpenRouter, a gateway, vLLM).
28+
- **The approval card said "at df".** The host line is shown for network tools only, a recipient line for mail; the chat header's status line is about the open chat, not whichever thread happens to be working.
1929

2030
## [0.5.0] — 2026-09-23
2131

@@ -118,7 +128,8 @@ First public release.
118128
- Mobile-first web app built with React, Vite and Tailwind, shipped inside the package.
119129
- Docker image and Compose file; GitHub Actions CI; PyPI publishing through Trusted Publishing.
120130

121-
[Unreleased]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.5.0...HEAD
131+
[Unreleased]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.6.0...HEAD
132+
[0.6.0]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.5.0...v0.6.0
122133
[0.5.0]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.4.0...v0.5.0
123134
[0.4.0]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.3.0...v0.4.0
124135
[0.3.0]: https://github.com/OpenMuseAgent/OpenMuse/compare/v0.2.0...v0.3.0

‎CITATION.cff‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors:
77
repository-code: "https://github.com/OpenMuseAgent/OpenMuse"
88
url: "https://github.com/OpenMuseAgent/OpenMuse"
99
license: MIT
10-
version: 0.5.0
10+
version: 0.6.0
1111
date-released: 2026-09-23
1212
keywords:
1313
- personal agent

‎android/app/build.gradle.kts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ android {
2424
applicationId = "io.github.openmuseagent.openmuse"
2525
minSdk = 26
2626
targetSdk = 36
27-
versionCode = 500
28-
versionName = "0.5.0"
27+
versionCode = 600
28+
versionName = "0.6.0"
2929
}
3030

3131
signingConfigs {

‎docs/android.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Everything else is the same web app, served by your `openmuse serve`.
1313

1414
## Install
1515

16-
Download `openmuse-<version>.apk` from the [latest release](https://github.com/OpenMuseAgent/OpenMuse/releases/latest) and open it on the phone. Android asks once to allow installs from your browser or file manager. Android 8.0 (API 26) or newer, any CPU.
16+
Download [`openmuse.apk`](https://github.com/OpenMuseAgent/OpenMuse/releases/latest/download/openmuse.apk) (always the current release; the same file is also there as `openmuse-<version>.apk`) and open it on the phone. Android asks once to allow installs from your browser or file manager. Android 8.0 (API 26) or newer, any CPU. Every release is signed with the same key, so a newer APK installs over the old one and keeps its connection.
1717

1818
Then:
1919

@@ -65,7 +65,7 @@ The same four values can come from the environment as `OPENMUSE_STOREFILE`, `OPE
6565

6666
## Releases
6767

68-
`.github/workflows/android.yml` builds the APK on every change under `android/` and, on a `v*` tag, attaches `openmuse-<version>.apk` to the GitHub release. For a properly signed release APK the repository needs four secrets: `ANDROID_KEYSTORE_B64` (the `.jks` file, base64), `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. Without them the workflow still produces an APK, signed with a throwaway debug key.
68+
`.github/workflows/android.yml` builds the APK on every change under `android/` and, on a `v*` tag, attaches `openmuse-<version>.apk` and `openmuse.apk` to the GitHub release — but only when it could sign with the release key, which the repository gets from four secrets: `ANDROID_KEYSTORE_B64` (the `.jks` file, base64), `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`. Without them the workflow still builds an APK signed with a throwaway debug key and keeps it as a workflow artifact; the maintainer then attaches the APK built locally with the real key. A debug-signed APK never goes on a release, because it could not be updated by a properly signed one.
6969

7070
The `versionName` in `android/app/build.gradle.kts` must match the tag, like the Python package's version does.
7171

‎docs/configuration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ These win over the file. They cover the settings people change most often and wh
1919
| Variable | Setting |
2020
|---|---|
2121
| `OPENMUSE_LLM_PROVIDER`, `OPENMUSE_LLM_MODEL`, `OPENMUSE_LLM_BASE_URL`, `OPENMUSE_LLM_API_KEY`, `OPENMUSE_LLM_TOOL_MODE` | `[llm]` |
22-
| `DEEPSEEK_API_KEY`, `OPENAI_API_KEY` | used as `llm.api_key` when it is empty |
22+
| `DEEPSEEK_API_KEY`, `OPENAI_API_KEY` | used as `llm.api_key` when it is empty — DeepSeek's for a `*.deepseek.com` `base_url`, OpenAI's for every other host (OpenAI itself, OpenRouter, a gateway, vLLM) |
2323
| `OPENMUSE_DATA_DIR` | `data_dir` (default `~/.openmuse`) |
2424
| `OPENMUSE_WORKSPACE` | `agent.workspace` (default `./workspace`) |
2525
| `OPENMUSE_SENTINEL_MODE` | `sentinel.mode` |

‎openmuse/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
``openmuse serve`` adds the phone app.
77
"""
88

9-
__version__ = "0.5.0"
9+
__version__ = "0.6.0"
1010
__all__ = ["__version__"]

‎openmuse/config.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import tomllib
2424
from pathlib import Path
2525
from typing import Any, Literal
26+
from urllib.parse import urlparse
2627

2728
from pydantic import BaseModel, Field, field_validator
2829

@@ -388,6 +389,20 @@ def find_config_file(explicit: str | Path | None = None) -> Path | None:
388389
return None
389390

390391

392+
def _provider_key_vars(base_url: str) -> tuple[str, ...]:
393+
"""The environment variables that may stand in for a missing ``llm.api_key``.
394+
395+
A key belongs to one provider: DeepSeek's key is only tried for ``api.deepseek.com``
396+
and OpenAI's only for ``api.openai.com``, so a config pointed at one provider never
397+
sends the other's key. Any other OpenAI-compatible host (a gateway, vLLM, OpenRouter)
398+
gets ``OPENAI_API_KEY`` alone — the convention such endpoints share.
399+
"""
400+
host = (urlparse(base_url).hostname or "").lower()
401+
if host == "deepseek.com" or host.endswith(".deepseek.com"):
402+
return ("DEEPSEEK_API_KEY",)
403+
return ("OPENAI_API_KEY",)
404+
405+
391406
def _apply_env_overrides(raw: dict[str, Any]) -> None:
392407
llm = raw.setdefault("llm", {})
393408
mapping = {
@@ -402,7 +417,8 @@ def _apply_env_overrides(raw: dict[str, Any]) -> None:
402417
if (val := os.environ.get(env)) not in (None, ""):
403418
llm[key] = val
404419
if not llm.get("api_key"):
405-
for env in ("DEEPSEEK_API_KEY", "OPENAI_API_KEY"):
420+
base_url = llm.get("base_url") or LLMSettings.model_fields["base_url"].default or ""
421+
for env in _provider_key_vars(str(base_url)):
406422
if val := os.environ.get(env):
407423
llm["api_key"] = val
408424
break

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "openmuse"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Open-source version of Meta's Muse: a personal agent with a phone app, a Sentinel gatekeeper, a credential vault, memory and goals. Bring your own model."
99
readme = "README.md"
1010
license = { text = "MIT" }
1111
requires-python = ">=3.11"
1212
authors = [{ name = "OpenMuse Contributors" }]
1313
keywords = ["agent", "ai", "llm", "personal-assistant", "muse", "meta-muse", "deepseek", "mcp"]
1414
classifiers = [
15-
"Development Status :: 3 - Alpha",
15+
"Development Status :: 4 - Beta",
1616
"Intended Audience :: Developers",
1717
"License :: OSI Approved :: MIT License",
1818
"Programming Language :: Python :: 3",

‎tests/test_config.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,24 @@ def test_defaults_without_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
6464
s = load_settings()
6565
assert s.llm.api_key == "sk-from-env"
6666
assert s.source == "defaults+env"
67+
68+
69+
def test_provider_key_fallback_follows_the_host(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
70+
"""A missing api_key is filled from the provider's own variable, never another's."""
71+
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek")
72+
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai")
73+
monkeypatch.delenv("OPENMUSE_LLM_API_KEY", raising=False)
74+
cfg = tmp_path / "config.toml"
75+
76+
def key_for(base_url: str | None) -> str | None:
77+
line = f'base_url = "{base_url}"\n' if base_url else ""
78+
cfg.write_text(f'data_dir = "{tmp_path / "data"}"\n[llm]\n{line}')
79+
return load_settings(cfg).llm.api_key
80+
81+
assert key_for(None) == "sk-deepseek" # the default endpoint is DeepSeek's
82+
assert key_for("https://api.deepseek.com") == "sk-deepseek"
83+
assert key_for("https://api.openai.com/v1") == "sk-openai"
84+
assert key_for("https://openrouter.ai/api/v1") == "sk-openai" # the shared convention
85+
monkeypatch.delenv("OPENAI_API_KEY")
86+
assert not key_for("https://api.openai.com/v1") # DeepSeek's key does not stand in
87+
assert key_for("https://api.deepseek.com") == "sk-deepseek"

0 commit comments

Comments
 (0)