diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..23c98ad --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Changes to the guard allow-lists / exemptions need review by the account owner on GitHub, AND an +# approved_by (windy-hub | windy-mind) on every non-structural entry, which the guard enforces itself +# (scripts/compute_guard.py: load_allow). A lane never approves its own exemption (Hub 10-02). +/ci/compute-guard-allow.yml @sneakyfree +/ci/secret-guard-allow.yml @sneakyfree +/ci/ci-hygiene-allow.yml @sneakyfree +/scripts/compute_guard.py @sneakyfree diff --git a/api/tests/test_compute_guard.py b/api/tests/test_compute_guard.py index 8397a58..8d7de8a 100644 --- a/api/tests/test_compute_guard.py +++ b/api/tests/test_compute_guard.py @@ -78,16 +78,18 @@ def test_allow_list_needs_a_reason_per_entry(tmp_path): def _entry(**kw): - base = dict(repo="x", paths=["*"], reason="r", exemption="owner-approved", expires="2099-01-01") + base = dict(repo="x", paths=["*"], reason="r", exemption="owner-approved", expires="2099-01-01", + approved_by="windy-hub") base.update(kw) lines = ["allow:", " - repo: x", " paths: ['*']", " reason: r"] - for k in ("exemption", "expires"): + for k in ("exemption", "expires", "approved_by"): if base.get(k) is not None: lines.append(f" {k}: {base[k]}") return "\n".join(lines) + "\n" def test_allow_entries_need_a_named_exemption_and_an_expiry(tmp_path): + from datetime import date f = tmp_path / "a.yml" f.write_text(_entry(exemption=None)) with pytest.raises(ValueError): @@ -101,8 +103,8 @@ def test_allow_entries_need_a_named_exemption_and_an_expiry(tmp_path): f.write_text(_entry(expires="someday")) with pytest.raises(ValueError): cg.load_allow(f) - f.write_text(_entry()) - assert len(cg.load_allow(f)) == 1 + f.write_text(_entry(expires="2026-12-01")) + assert len(cg.load_allow(f, today=date(2026, 10, 2))) == 1 def test_expired_exemption_stops_excusing_and_is_reported(tmp_path): @@ -330,3 +332,39 @@ def test_ollama_in_added_pr_lines_only(): "+URL = 'http://veron:11434/api/chat'\n") got = cg.parse_added("some-repo", diff, []) assert [(f.kind, f.line) for f in got] == [("veron ollama", 2)] + + +def test_non_structural_exemptions_need_an_independent_approver_and_a_90_day_cap(tmp_path): + from datetime import date + f = tmp_path / "a.yml" + f.write_text(_entry(approved_by=None)) + with pytest.raises(ValueError): + cg.load_allow(f, today=date(2026, 10, 2)) + f.write_text(_entry(approved_by="windy-chat")) # a lane may not approve itself/another lane + with pytest.raises(ValueError): + cg.load_allow(f, today=date(2026, 10, 2)) + f.write_text(_entry(expires="2026-12-31")) # exactly 90 days: fine + assert len(cg.load_allow(f, today=date(2026, 10, 2))) == 1 + f.write_text(_entry(expires="2027-01-01")) # 91 days: does NOT apply, and is reported + assert cg.load_allow(f, today=date(2026, 10, 2)) == [] and cg.OVERCAP + f.write_text(_entry(exemption="compute-door", approved_by=None, expires="2027-10-02")) + assert len(cg.load_allow(f, today=date(2026, 10, 2))) == 1 # structural: yearly, no approver field + + +def test_shipped_allow_file_obeys_its_own_rules(): + allow = cg.load_allow() + assert allow and not cg.EXPIRED and not cg.OVERCAP + for e in allow: + if e["exemption"] not in cg.STRUCTURAL: + assert e["approved_by"] in cg.APPROVERS + + +def test_findings_carry_kind_and_name_never_the_value_and_ports_skip_contracts(): + for line, kind in [("ELEVENLABS_API_KEY=sk_live_SUPERSECRET123456789", "voice-ai key"), + ('DEEPGRAM_API_KEY = "dg-VALUE-0123456789abcdef"', "voice-ai key")]: + hits = cg.scan_line("app/x.py", line) + assert [k for k, _ in hits] == [kind] + assert all("SUPERSECRET" not in m and "VALUE" not in m for _, m in hits) + assert cg.scan_line("engine/contracts/ops.mcp.v1.json", '"url": "http://h:8099/x"') == [] + assert cg.scan_line("services/api/openapi/spec.json", '"url": "http://h:8099/x"') == [] + assert [k for k, _ in cg.scan_line("deploy/docker-compose.yml", " - 8099:8099 # :8099")] == ["talk engine port"] diff --git a/ci/compute-guard-allow.yml b/ci/compute-guard-allow.yml index 589258a..d71a890 100644 --- a/ci/compute-guard-allow.yml +++ b/ci/compute-guard-allow.yml @@ -4,7 +4,8 @@ # relative to the repo root. Owner of this file: Windy Git lane (13); changes # go through the orchestrator. EVERY entry needs `exemption` (local-user-hardware | owner-approved | # compute-door | guard-self) and `expires` (YYYY-MM-DD): nothing gets permanent amnesty (Mind 10-02); -# an expired entry stops excusing code on that date and shows in the guard report. Source of the first entries: COMPUTE_BYPASS_AUDIT.md. +# non-structural exemptions also need approved_by (windy-hub | windy-mind; a lane never approves its own) +# and expire within 90 days; an expired entry stops excusing code on that date and shows in the guard report. Source of the first entries: COMPUTE_BYPASS_AUDIT.md. allow: - repo: windy-mind paths: ["*"] @@ -19,18 +20,24 @@ allow: Mind stays opt-in there, or every self-hosted user's inference lands on Grant's bill (no-cloud-cost-liability rule; audit #7). exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-code paths: ["extensions/windy-ai/*"] reason: "User BYOK AI extension: the user's own provider keys; Mind is one opt-in provider (audit #8)." exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-connect paths: ["*writers/*"] reason: "Writes client configs that NAME the user's own provider env vars; makes no provider calls (audit #11)." exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-pro @@ -41,12 +48,16 @@ allow: the CSP line allows exactly those user-keyed hosts (audit #10). The account-server is NOT covered: server-side calls go through Mind. exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-pro paths: ["src/client/web/src/pages/panels/MindPanel.jsx"] reason: "Validates the USER's own OpenRouter key for BYOK (audit #10); spends no house money." exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-pro @@ -56,6 +67,8 @@ allow: matches: ['openrouter\.ai/auth\?', 'openrouter\.ai/api/v1/auth/keys'] reason: "BYOK key acquisition via OpenRouter OAuth PKCE; no inference; successor of MindPanel allow (ADR-064)." exemption: owner-approved + approved_by: windy-hub + approved_on: 2026-10-02 expires: 2026-12-31 - repo: windy-git diff --git a/scripts/compute_guard.py b/scripts/compute_guard.py index 04a9af0..e01cae6 100644 --- a/scripts/compute_guard.py +++ b/scripts/compute_guard.py @@ -31,7 +31,7 @@ import re import subprocess import sys from dataclasses import dataclass -from datetime import date +from datetime import date, timedelta from pathlib import Path import yaml @@ -73,6 +73,8 @@ PY_SDKS = r"anthropic|openai|groq|mistralai|cohere|google\.generativeai|google\. JS_SDKS = (r"@anthropic-ai/sdk|openai|groq-sdk|@google/generative-ai|@google/genai|@mistralai/mistralai" r"|cohere-ai|together-ai|@ai-sdk/(?:anthropic|openai|groq|google|mistral)") +# The engine-port rule is about CODE/CONFIG that calls the engine, not API contracts, schemas or specs. +PORT_SKIP = re.compile(r"(^|/)(contracts?|schemas?|specs?|openapi)/|\.json$", re.I) WRANGLER = re.compile(r"(^|/)wrangler\.(toml|jsonc?)$") WRANGLER_AI = re.compile(r'^\s*\[ai\]\s*$|^\s*"ai"\s*:\s*\{') @@ -121,7 +123,11 @@ class Finding: EXEMPTIONS = {"local-user-hardware", "owner-approved", "compute-door", "guard-self"} +STRUCTURAL = {"compute-door", "guard-self"} # the door itself and the guard's own files: yearly review +APPROVERS = {"windy-hub", "windy-mind"} # a lane never approves its own exemption (Hub 10-02) +MAX_DAYS = 90 # every other exemption: 90 days max, then re-approve EXPIRED: list[dict] = [] # entries dropped as expired on the last load_allow (reported, never silent) +OVERCAP: list[dict] = [] # entries dropped because their expiry is further out than MAX_DAYS def load_allow(path: Path = ALLOW_FILE, today: date | None = None, strict: bool = True) -> list[dict]: @@ -133,6 +139,7 @@ def load_allow(path: Path = ALLOW_FILE, today: date | None = None, strict: bool entries = data.get("allow") or [] active = [] EXPIRED.clear() + OVERCAP.clear() for e in entries: if not (e.get("repo") and e.get("paths") and str(e.get("reason", "")).strip()): raise ValueError(f"allow entry needs repo, paths and a reason: {e}") @@ -145,6 +152,12 @@ def load_allow(path: Path = ALLOW_FILE, today: date | None = None, strict: bool exp = e["expires"] if isinstance(e.get("expires"), date) else date.fromisoformat(str(e.get("expires"))) except ValueError as err: raise ValueError(f"allow entry needs expires: YYYY-MM-DD: {e.get('repo')} {e.get('paths')}") from err + if e["exemption"] not in STRUCTURAL: + if e.get("approved_by") not in APPROVERS: + raise ValueError(f"allow entry needs approved_by in {sorted(APPROVERS)}: {e.get('repo')} {e.get('paths')}") + if exp > today + timedelta(days=MAX_DAYS): + OVERCAP.append({**e, "expires": exp.isoformat()}) # a longer amnesty simply does not apply + continue if exp < today: EXPIRED.append({**e, "expires": exp.isoformat()}) else: @@ -182,6 +195,8 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]: continue if kind == "workers ai binding" and not WRANGLER.search(path): continue + if kind == "talk engine port" and PORT_SKIP.search(path): + continue m = rx.search(text) if m: hits.append((kind, m.group(0).strip()[:60])) @@ -358,6 +373,9 @@ def status_for(findings: list[Finding], whole_tree: bool, def report(repos: list[str]) -> int: allow = load_allow() + for e in OVERCAP: + print(f"## OVER-CAP exemption (> {MAX_DAYS} days, NOT applied): {e['repo']} {e['paths']} " + f"[{e['exemption']}] expires {e['expires']}") for e in EXPIRED: print(f"## EXPIRED exemption (no longer excuses anything): {e['repo']} {e['paths']} " f"[{e['exemption']}] expired {e['expires']}")