84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""echo_secrets.py — M1: resolve the API bearer token without shipping it in source.
|
||
|
|
[v1.0 SCAFFOLD — resolve_key() implemented; wiring into echo.py is the integration step]
|
||
|
|
|
||
|
|
Problem: the token is hardcoded at echo.py:70 and repeated through api-reference.md /
|
||
|
|
README — committed to git and shipped inside every .plugin zip, rotatable only by a
|
||
|
|
rebuild. The plugin's own safety rule is "never store secrets". This resolves the key
|
||
|
|
from progressively less-trusted sources, with a loud warning if the legacy baked
|
||
|
|
default is ever reached.
|
||
|
|
|
||
|
|
RESOLUTION ORDER (first hit wins):
|
||
|
|
1. env ECHO_KEY — preferred; CI / per-session override.
|
||
|
|
2. ECHO_STATE_DIR/credentials — local file, one `key=...` line, chmod 0600.
|
||
|
|
3. legacy baked default (deprecated) — warns to stderr; remove before 1.0 ship.
|
||
|
|
|
||
|
|
Pure stdlib only (no `keyring`); the local file is the cross-platform stand-in for an
|
||
|
|
OS keychain and keeps the secret OUT of the tracked tree.
|
||
|
|
|
||
|
|
INTEGRATION (do at merge time):
|
||
|
|
echo.py: KEY = echo_secrets.resolve_key()
|
||
|
|
docs: replace the literal token with `<ECHO_KEY>` placeholders
|
||
|
|
rotate: document `echo write-key` (writes the credentials file) — see TODO below
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# Kept ONLY so a fresh install still works during the migration window; delete for 1.0.
|
||
|
|
_LEGACY_DEFAULT = "" # intentionally blank in the scaffold — do not re-introduce the literal
|
||
|
|
|
||
|
|
|
||
|
|
def _state_dir() -> Path:
|
||
|
|
return Path(os.environ.get("ECHO_STATE_DIR") or (Path.home() / ".echo-memory"))
|
||
|
|
|
||
|
|
|
||
|
|
def _from_file() -> str | None:
|
||
|
|
p = _state_dir() / "credentials"
|
||
|
|
if not p.exists():
|
||
|
|
return None
|
||
|
|
for line in p.read_text(encoding="utf-8").splitlines():
|
||
|
|
line = line.strip()
|
||
|
|
if line.startswith("key=") or line.startswith("ECHO_KEY="):
|
||
|
|
return line.split("=", 1)[1].strip()
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def resolve_key(legacy: str | None = None) -> str:
|
||
|
|
"""Return the bearer token from the most trusted available source:
|
||
|
|
ECHO_KEY env -> credentials file -> `legacy` baked-in fallback (with a loud, once-per-
|
||
|
|
process warning that can be silenced via ECHO_KEY_LEGACY_OK=1). Raises if none exist."""
|
||
|
|
env = os.environ.get("ECHO_KEY")
|
||
|
|
if env:
|
||
|
|
return env
|
||
|
|
from_file = _from_file()
|
||
|
|
if from_file:
|
||
|
|
return from_file
|
||
|
|
legacy = legacy or _LEGACY_DEFAULT
|
||
|
|
if legacy:
|
||
|
|
if os.environ.get("ECHO_KEY_LEGACY_OK") != "1":
|
||
|
|
print("echo_secrets: WARNING — using the deprecated baked-in API key. Run "
|
||
|
|
"`echo.py write-key <token>` (stores ~/.echo-memory/credentials) or set "
|
||
|
|
"ECHO_KEY; set ECHO_KEY_LEGACY_OK=1 to silence.", file=sys.stderr)
|
||
|
|
return legacy
|
||
|
|
raise RuntimeError(
|
||
|
|
"echo_secrets: no API key found. Set ECHO_KEY or create "
|
||
|
|
f"{_state_dir() / 'credentials'} with a 'key=<token>' line."
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def write_key(token: str) -> Path:
|
||
|
|
"""Persist the token to the local credentials file (one `key=...` line, chmod 0600).
|
||
|
|
Best-effort perms — Windows has no POSIX mode bits, so the chmod is advisory there."""
|
||
|
|
d = _state_dir()
|
||
|
|
d.mkdir(parents=True, exist_ok=True)
|
||
|
|
path = d / "credentials"
|
||
|
|
path.write_text(f"key={token.strip()}\n", encoding="utf-8")
|
||
|
|
try:
|
||
|
|
os.chmod(path, 0o600)
|
||
|
|
except OSError:
|
||
|
|
pass # platform without POSIX perms (e.g. some Windows filesystems)
|
||
|
|
return path
|