1
0
forked from jason/echo

Phase 2: identity plumbing — group/member config, author: attribution

Config schema {owner,endpoint,key} -> {group, member, endpoint, key}:
- member (kebab-case slug, validated) is REQUIRED — who this machine
  writes as; group is the descriptive team name. CHORUS_GROUP/
  CHORUS_MEMBER env, config set --group/--member, doctor + config show
  both with sources, NOT-CONFIGURED (78) without a valid member.
- capture stamps author: <member> on every note (_build_note); bootstrap
  gains {{MEMBER}} substitution and stamps the seeded anchors; all 8
  scaffold templates + canonical frontmatter docs gain author:.
- New missing-author lint check: agent_written notes must carry author:
  (bootstrap marker exempt — plugin-owned).
- Lock owner is now <member>-<client>-<pid> (auto_owner); offline-queue
  records carry a member field for audit.
- build.py --bake-key bakes group/member/endpoint/key (--group/--member;
  member required + slug-validated) for per-member artifacts.
- Manifest -> 2.0.0-alpha.2; rebuilt chorus-memory.plugin.

Verified: 25/25 unit (+config/member checks), scaffold suite (+6 new
member/config assertions), routing-sync, 4 mock e2e (+capture-stamps-
author), run_eval metrics unchanged, +8 phase-2 smoke checks green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:00:59 -05:00
parent d920f8821f
commit b3bc5272d5
35 changed files with 304 additions and 144 deletions
+22 -16
View File
@@ -52,7 +52,8 @@ MAX_DESCRIPTION = 500 # plugin-marketplace cap: plugin.json "description" must
# (it has silently regressed past the limit before — fail the build now)
# field name -> the constant assigned in chorus_config.py
_CONSTS = {"owner": "DEFAULT_OWNER", "endpoint": "DEFAULT_BASE", "key": "DEFAULT_KEY"}
_CONSTS = {"group": "DEFAULT_GROUP", "member": "DEFAULT_MEMBER",
"endpoint": "DEFAULT_BASE", "key": "DEFAULT_KEY"}
def _const_re(const: str) -> re.Pattern:
@@ -113,8 +114,8 @@ def build(out: Path, files: list[Path], bake: dict | None, strip_key: bool) -> i
def bake_values(args) -> dict:
"""Resolve owner/endpoint/key for --bake-key from --owner/--endpoint/--key,
else --from <config.json>, else CHORUS_OWNER/CHORUS_BASE/CHORUS_KEY env."""
"""Resolve group/member/endpoint/key for --bake-key from the CLI flags,
else --from <config.json>, else CHORUS_GROUP/CHORUS_MEMBER/CHORUS_BASE/CHORUS_KEY env."""
src = {}
if args.from_file:
fp = Path(args.from_file).expanduser()
@@ -127,28 +128,33 @@ def bake_values(args) -> dict:
if isinstance(loaded, dict):
src = loaded
vals = {
"owner": args.owner or src.get("owner") or os.environ.get("CHORUS_OWNER") or "",
"group": args.group or src.get("group") or os.environ.get("CHORUS_GROUP") or "",
"member": args.member or src.get("member") or os.environ.get("CHORUS_MEMBER") or "",
"endpoint": (args.endpoint or src.get("endpoint") or os.environ.get("CHORUS_BASE") or "").rstrip("/"),
"key": args.key or src.get("key") or os.environ.get("CHORUS_KEY") or "",
}
missing = [f for f in ("endpoint", "key") if not vals[f]]
missing = [f for f in ("endpoint", "key", "member") if not vals[f]]
if missing:
raise RuntimeError(
"build: --bake-key needs a real endpoint and key — missing "
+ ", ".join(missing) + ". Provide --from <config.json>, the --owner/"
"--endpoint/--key flags, or set CHORUS_OWNER/CHORUS_BASE/CHORUS_KEY.")
if "your-obsidian-rest-endpoint" in vals["endpoint"] or vals["key"].startswith("<"):
"build: --bake-key needs a real endpoint, key, and member — missing "
+ ", ".join(missing) + ". Provide --from <config.json>, the --group/--member/"
"--endpoint/--key flags, or set CHORUS_GROUP/CHORUS_MEMBER/CHORUS_BASE/CHORUS_KEY.")
if "your-obsidian-rest-endpoint" in vals["endpoint"] or vals["key"].startswith("<") \
or vals["member"].startswith("<"):
raise RuntimeError("build: --bake-key got the template placeholders, not real values.")
if not re.match(r"^[a-z0-9][a-z0-9-]{0,31}$", vals["member"]):
raise RuntimeError(f"build: baked member must be a kebab-case slug, got {vals['member']!r}")
return vals
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Build the chorus-memory .plugin artifact")
ap.add_argument("--bake-key", action="store_true",
help="bake a user's owner/endpoint/key into the artifact (secret-bearing — never commit)")
help="bake a member's group/member/endpoint/key into the artifact (secret-bearing — never commit)")
ap.add_argument("--from", dest="from_file", metavar="FILE",
help="config.json to read owner/endpoint/key from when baking")
ap.add_argument("--owner", help="baked owner (overrides --from / env)")
help="config.json to read group/member/endpoint/key from when baking")
ap.add_argument("--group", help="baked group name (overrides --from / env)")
ap.add_argument("--member", help="baked member id (kebab-case; overrides --from / env)")
ap.add_argument("--endpoint", help="baked endpoint (overrides --from / env)")
ap.add_argument("--key", help="baked key (overrides --from / env)")
ap.add_argument("--label", help="suffix for the artifact name, e.g. --label alice")
@@ -206,12 +212,12 @@ def main(argv: list[str] | None = None) -> int:
print(f"built {out.name} ({len(files)} entries, {size:,} bytes)")
if bake:
who = bake.get("owner") or "(no owner)"
print(f"note: --bake-key baked credentials for {who} into {CONFIG_PY}.")
who = bake.get("member") or "(no member)"
print(f"note: --bake-key baked credentials for member '{who}' into {CONFIG_PY}.")
print("WARNING: this artifact carries a vault key — deliver it directly to that one "
"user and NEVER commit, push, or publish it (dist/ is gitignored).")
"member and NEVER commit, push, or publish it (dist/ is gitignored).")
elif args.strip_key:
print("note: --strip-key set — artifact is token-free; user configures owner/endpoint/key at runtime.")
print("note: --strip-key set — artifact is token-free; member configures group/member/endpoint/key at runtime.")
elif token_present(SRC / "skills" / "chorus-memory" / "scripts" / "chorus_config.py"):
print("WARNING: a DEFAULT_* constant in chorus_config.py is non-empty — this artifact carries a "
"secret. Do not commit it. Build from a clean source tree, or use --strip-key.")