Skip to content

Commit 4f3ea02

Browse files
committed
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.
1 parent 41b9f11 commit 4f3ea02

4 files changed

Lines changed: 23 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ python "$env:HERMES_HOME\plugins\muse-code-subscription\muse_code_login.py"
3232

3333
This mints a stable, account-bound inference key and caches it locally
3434
(`$HERMES_HOME/muse-code-sub.json`, owner-only permissions where supported).
35+
The cache stores exactly `apiKey`, `accountId`, and `email` — the OAuth
36+
access token from the login flow is kept in memory only and never written
37+
to disk.
3538
The key is never printed. Re-run only if access is revoked (401) — the mint
3639
endpoint is aggressively rate-limited, so the plugin never re-mints on its own.
3740

‎muse_code_login.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,23 @@ def mint_key(access_token, onboard=True):
192192

193193

194194
def write_cache(credentials, path):
195-
"""Persist credentials with owner-only permissions where supported."""
195+
"""Persist credentials with owner-only permissions where supported.
196+
197+
Retention minimization: only apiKey/accountId/email touch disk. The
198+
OAuth access token has unknown broader scope and nothing reads it back,
199+
so it must never be persisted — sanitize at the sink, whatever the
200+
caller passes in.
201+
"""
202+
cached = {
203+
"apiKey": credentials.get("apiKey"),
204+
"accountId": credentials.get("accountId"),
205+
}
206+
if credentials.get("email"):
207+
cached["email"] = credentials["email"]
196208
directory = os.path.dirname(os.path.abspath(path))
197209
os.makedirs(directory, exist_ok=True)
198210
with open(path, "w", encoding="utf-8") as fh:
199-
json.dump(credentials, fh, indent=2)
211+
json.dump(cached, fh, indent=2)
200212
try:
201213
os.chmod(path, 0o600)
202214
except Exception:

‎plugin.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
name: muse-code-subscription
22
kind: model-provider
3-
version: 0.2.0
3+
version: 0.2.1
44
description: Muse Spark in Hermes billed to the Muse Code monthly login (device-code login, no API key)
55
author: TheStreamCode
66
license: MIT
77
homepage: https://github.com/TheStreamCode/hermes-muse-code
88
tags: [meta, muse-spark, subscription]
99
requires_hermes: ">=0.21.3"
10+

‎tests/test_muse_code_provider.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,11 @@ def test_full_login_flow_writes_cache(tmp_path):
241241
assert login.main(["--cache", cache]) == 0
242242
assert "ABCD-EFGH" in out.getvalue()
243243
assert "LLM|fresh" not in out.getvalue() # secrets never printed
244-
saved = json.loads(open(cache).read())
244+
raw = open(cache).read()
245+
assert "oauthAccessToken" not in raw # never persisted
246+
assert "dca-new" not in raw
247+
saved = json.loads(raw)
245248
assert saved == {
246-
"oauthAccessToken": "dca-new",
247249
"apiKey": "LLM|fresh",
248250
"accountId": "uid-1",
249251
"email": "user@example.com",

0 commit comments

Comments
 (0)