1
0
forked from jason/echo

v1.4.1 bump

This commit is contained in:
Jason Stedwell
2026-06-26 15:39:17 -05:00
parent 0cd8a54649
commit 9ee1530f97
15 changed files with 162 additions and 30 deletions
@@ -16,22 +16,31 @@ committed, and is never written into a vault note. To create it, run
`python3 echo.py config init` (writes a placeholder template when absent) and edit
it, or `python3 echo.py config set --owner ... --endpoint ... --key ...`.
Per-field resolution (first hit wins):
owner env ECHO_OWNER -> config "owner" -> baked DEFAULT_OWNER
endpoint env ECHO_BASE -> config "endpoint" -> baked DEFAULT_BASE
key env ECHO_KEY -> config "key" -> baked DEFAULT_KEY
Resolution — a COMPLETE baked credential set wins unconditionally:
If the artifact carries both a baked endpoint AND key (a per-user build), those
baked values are used as-is and CANNOT be shadowed by env vars or a config file.
This is the default path: a delivered per-user plugin "just works" with no setup,
and a stale ECHO_* env var or a leftover/placeholder ~/.claude config.json can
never override or break it.
Otherwise (nothing baked — the generic published plugin), fall back per-field,
first hit wins:
owner env ECHO_OWNER -> config "owner"
endpoint env ECHO_BASE -> config "endpoint"
key env ECHO_KEY -> config "key"
The config directory honors $CLAUDE_CONFIG_DIR (defaults to ~/.claude); the config
file path itself can be overridden with $ECHO_CONFIG. Pure stdlib only.
BAKED FALLBACK (the lowest tier): the DEFAULT_* constants below are EMPTY in source
— the committed plugin ships zero credentials. `build.py --bake-key` substitutes a
specific user's owner/endpoint/key into them at package time, producing a per-user
artifact that needs no config file. This is how the plugin works in the CoWork
sandbox, where the user's ~/.claude is not bridged: the plugin itself (mounted
read-only every session) carries the values. A baked artifact is secret-bearing —
deliver it directly to that one user, NEVER commit or publish it. On a normal host
the empty defaults mean behaviour is unchanged (env/config file still win).
BAKED CREDENTIALS (the default tier): the DEFAULT_* constants below are EMPTY in
source — the committed plugin ships zero credentials. `build.py --bake-key`
substitutes a specific user's owner/endpoint/key into them at package time,
producing a per-user artifact that needs no config file. This is how the plugin
works in the CoWork sandbox, where the user's ~/.claude is not bridged: the plugin
itself (mounted read-only every session) carries the values. A baked artifact is
secret-bearing — deliver it directly to that one user, NEVER commit or publish it.
On a normal host the empty defaults mean the env/config-file path takes over.
"""
from __future__ import annotations
@@ -76,18 +85,34 @@ def _from_file() -> dict:
return data if isinstance(data, dict) else {}
def baked_complete() -> bool:
"""True when the artifact carries a usable baked endpoint AND key — i.e. a
per-user `--bake-key` build. When True, the baked values win unconditionally."""
return bool(DEFAULT_BASE and DEFAULT_KEY)
def load() -> dict:
"""Return {owner, endpoint, key} with env overriding the file. Missing -> "".
"""Return {owner, endpoint, key}. A complete baked set wins unconditionally;
otherwise env overrides the file, missing -> "".
Never raises — callers that REQUIRE a field use resolve_endpoint()/resolve_key(),
which raise a helpful error. This keeps `--help`, `config`, and `doctor` usable
even when nothing is configured yet."""
f = _from_file()
endpoint = (os.environ.get("ECHO_BASE") or f.get("endpoint") or DEFAULT_BASE or "").rstrip("/")
if baked_complete():
# Per-user artifact: baked creds are authoritative and must not be shadowed
# by a stale env var or a leftover/placeholder config file. Owner is purely
# descriptive, so it may still fall back when not baked.
return {
"owner": DEFAULT_OWNER or os.environ.get("ECHO_OWNER") or f.get("owner") or "",
"endpoint": DEFAULT_BASE.rstrip("/"),
"key": DEFAULT_KEY,
}
endpoint = (os.environ.get("ECHO_BASE") or f.get("endpoint") or "").rstrip("/")
return {
"owner": os.environ.get("ECHO_OWNER") or f.get("owner") or DEFAULT_OWNER or "",
"owner": os.environ.get("ECHO_OWNER") or f.get("owner") or "",
"endpoint": endpoint,
"key": os.environ.get("ECHO_KEY") or f.get("key") or DEFAULT_KEY or "",
"key": os.environ.get("ECHO_KEY") or f.get("key") or "",
}
@@ -133,14 +158,18 @@ def is_configured(cfg: dict | None = None) -> bool:
def source(field: str) -> str:
"""Where a field is currently coming from — for `config`/`doctor` reporting."""
baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
# A complete baked set wins unconditionally (see load()): endpoint/key always
# come from the artifact, and owner too whenever it was baked.
if baked_complete() and (field in ("endpoint", "key") or baked):
return "baked-in (plugin)"
env = {"owner": "ECHO_OWNER", "endpoint": "ECHO_BASE", "key": "ECHO_KEY"}[field]
if os.environ.get(env):
return f"{env} env"
if config_path().exists() and _from_file().get(field):
return "config file"
baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
if baked:
return "baked-in default"
return "baked-in (plugin)"
return "unset"