From 4f3ea020515e73e06ab163376e1948be4dc78df2 Mon Sep 17 00:00:00 2001 From: TheStreamCode Date: Sun, 27 Sep 2026 22:10:05 +0200 Subject: [PATCH] fix(security): never persist the OAuth access token (v0.2.1) write_cache now persists only apiKey/accountId/email; the OAuth token stays in memory (nothing ever read it back). Full-login test asserts absence from the raw cache file. README documents the stored fields. --- README.md | 3 +++ muse_code_login.py | 16 ++++++++++++++-- plugin.yaml | 3 ++- tests/test_muse_code_provider.py | 6 ++++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 23745fb..95f7cac 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ python "$env:HERMES_HOME\plugins\muse-code-subscription\muse_code_login.py" This mints a stable, account-bound inference key and caches it locally (`$HERMES_HOME/muse-code-sub.json`, owner-only permissions where supported). +The cache stores exactly `apiKey`, `accountId`, and `email` — the OAuth +access token from the login flow is kept in memory only and never written +to disk. The key is never printed. Re-run only if access is revoked (401) — the mint endpoint is aggressively rate-limited, so the plugin never re-mints on its own. diff --git a/muse_code_login.py b/muse_code_login.py index 3b3f72d..e0c265a 100644 --- a/muse_code_login.py +++ b/muse_code_login.py @@ -192,11 +192,23 @@ def mint_key(access_token, onboard=True): def write_cache(credentials, path): - """Persist credentials with owner-only permissions where supported.""" + """Persist credentials with owner-only permissions where supported. + + Retention minimization: only apiKey/accountId/email touch disk. The + OAuth access token has unknown broader scope and nothing reads it back, + so it must never be persisted — sanitize at the sink, whatever the + caller passes in. + """ + cached = { + "apiKey": credentials.get("apiKey"), + "accountId": credentials.get("accountId"), + } + if credentials.get("email"): + cached["email"] = credentials["email"] directory = os.path.dirname(os.path.abspath(path)) os.makedirs(directory, exist_ok=True) with open(path, "w", encoding="utf-8") as fh: - json.dump(credentials, fh, indent=2) + json.dump(cached, fh, indent=2) try: os.chmod(path, 0o600) except Exception: diff --git a/plugin.yaml b/plugin.yaml index 532fcf0..507082f 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,9 +1,10 @@ name: muse-code-subscription kind: model-provider -version: 0.2.0 +version: 0.2.1 description: Muse Spark in Hermes billed to the Muse Code monthly login (device-code login, no API key) author: TheStreamCode license: MIT homepage: https://github.com/TheStreamCode/hermes-muse-code tags: [meta, muse-spark, subscription] requires_hermes: ">=0.21.3" + diff --git a/tests/test_muse_code_provider.py b/tests/test_muse_code_provider.py index c645b7e..98d002d 100644 --- a/tests/test_muse_code_provider.py +++ b/tests/test_muse_code_provider.py @@ -241,9 +241,11 @@ def test_full_login_flow_writes_cache(tmp_path): assert login.main(["--cache", cache]) == 0 assert "ABCD-EFGH" in out.getvalue() assert "LLM|fresh" not in out.getvalue() # secrets never printed - saved = json.loads(open(cache).read()) + raw = open(cache).read() + assert "oauthAccessToken" not in raw # never persisted + assert "dca-new" not in raw + saved = json.loads(raw) assert saved == { - "oauthAccessToken": "dca-new", "apiKey": "LLM|fresh", "accountId": "uid-1", "email": "user@example.com",