forked from jason/echo
ver 1.0
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user