compute-guard: Hub conditions (90-day cap, named approver, CODEOWNERS, engine-port noise cut)
- non-structural exemptions need approved_by (windy-hub|windy-mind) and expire within 90 days; a longer amnesty simply does not apply and is reported (OVER-CAP). compute-door/guard-self: yearly. - .github/CODEOWNERS on the allow-lists + guard. - engine-port rule skips contracts/schemas/specs/openapi dirs and *.json (53 baseline hits, was 57). - tests: findings carry kind+name never the value; shipped allow file obeys its own rules. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
7
.github/CODEOWNERS
vendored
Normal file
7
.github/CODEOWNERS
vendored
Normal file
@@ -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
|
||||||
@@ -78,16 +78,18 @@ def test_allow_list_needs_a_reason_per_entry(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def _entry(**kw):
|
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)
|
base.update(kw)
|
||||||
lines = ["allow:", " - repo: x", " paths: ['*']", " reason: r"]
|
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:
|
if base.get(k) is not None:
|
||||||
lines.append(f" {k}: {base[k]}")
|
lines.append(f" {k}: {base[k]}")
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def test_allow_entries_need_a_named_exemption_and_an_expiry(tmp_path):
|
def test_allow_entries_need_a_named_exemption_and_an_expiry(tmp_path):
|
||||||
|
from datetime import date
|
||||||
f = tmp_path / "a.yml"
|
f = tmp_path / "a.yml"
|
||||||
f.write_text(_entry(exemption=None))
|
f.write_text(_entry(exemption=None))
|
||||||
with pytest.raises(ValueError):
|
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"))
|
f.write_text(_entry(expires="someday"))
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
cg.load_allow(f)
|
cg.load_allow(f)
|
||||||
f.write_text(_entry())
|
f.write_text(_entry(expires="2026-12-01"))
|
||||||
assert len(cg.load_allow(f)) == 1
|
assert len(cg.load_allow(f, today=date(2026, 10, 2))) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_expired_exemption_stops_excusing_and_is_reported(tmp_path):
|
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")
|
"+URL = 'http://veron:11434/api/chat'\n")
|
||||||
got = cg.parse_added("some-repo", diff, [])
|
got = cg.parse_added("some-repo", diff, [])
|
||||||
assert [(f.kind, f.line) for f in got] == [("veron ollama", 2)]
|
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"]
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# relative to the repo root. Owner of this file: Windy Git lane (13); changes
|
# 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 |
|
# 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);
|
# 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:
|
allow:
|
||||||
- repo: windy-mind
|
- repo: windy-mind
|
||||||
paths: ["*"]
|
paths: ["*"]
|
||||||
@@ -19,18 +20,24 @@ allow:
|
|||||||
Mind stays opt-in there, or every self-hosted user's inference lands on
|
Mind stays opt-in there, or every self-hosted user's inference lands on
|
||||||
Grant's bill (no-cloud-cost-liability rule; audit #7).
|
Grant's bill (no-cloud-cost-liability rule; audit #7).
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-code
|
- repo: windy-code
|
||||||
paths: ["extensions/windy-ai/*"]
|
paths: ["extensions/windy-ai/*"]
|
||||||
reason: "User BYOK AI extension: the user's own provider keys; Mind is one opt-in provider (audit #8)."
|
reason: "User BYOK AI extension: the user's own provider keys; Mind is one opt-in provider (audit #8)."
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-connect
|
- repo: windy-connect
|
||||||
paths: ["*writers/*"]
|
paths: ["*writers/*"]
|
||||||
reason: "Writes client configs that NAME the user's own provider env vars; makes no provider calls (audit #11)."
|
reason: "Writes client configs that NAME the user's own provider env vars; makes no provider calls (audit #11)."
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-pro
|
- repo: windy-pro
|
||||||
@@ -41,12 +48,16 @@ allow:
|
|||||||
the CSP line allows exactly those user-keyed hosts (audit #10). The
|
the CSP line allows exactly those user-keyed hosts (audit #10). The
|
||||||
account-server is NOT covered: server-side calls go through Mind.
|
account-server is NOT covered: server-side calls go through Mind.
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-pro
|
- repo: windy-pro
|
||||||
paths: ["src/client/web/src/pages/panels/MindPanel.jsx"]
|
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."
|
reason: "Validates the USER's own OpenRouter key for BYOK (audit #10); spends no house money."
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-pro
|
- repo: windy-pro
|
||||||
@@ -56,6 +67,8 @@ allow:
|
|||||||
matches: ['openrouter\.ai/auth\?', 'openrouter\.ai/api/v1/auth/keys']
|
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)."
|
reason: "BYOK key acquisition via OpenRouter OAuth PKCE; no inference; successor of MindPanel allow (ADR-064)."
|
||||||
exemption: owner-approved
|
exemption: owner-approved
|
||||||
|
approved_by: windy-hub
|
||||||
|
approved_on: 2026-10-02
|
||||||
expires: 2026-12-31
|
expires: 2026-12-31
|
||||||
|
|
||||||
- repo: windy-git
|
- repo: windy-git
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import yaml
|
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"
|
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)")
|
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 = re.compile(r"(^|/)wrangler\.(toml|jsonc?)$")
|
||||||
WRANGLER_AI = re.compile(r'^\s*\[ai\]\s*$|^\s*"ai"\s*:\s*\{')
|
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"}
|
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)
|
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]:
|
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 []
|
entries = data.get("allow") or []
|
||||||
active = []
|
active = []
|
||||||
EXPIRED.clear()
|
EXPIRED.clear()
|
||||||
|
OVERCAP.clear()
|
||||||
for e in entries:
|
for e in entries:
|
||||||
if not (e.get("repo") and e.get("paths") and str(e.get("reason", "")).strip()):
|
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}")
|
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")))
|
exp = e["expires"] if isinstance(e.get("expires"), date) else date.fromisoformat(str(e.get("expires")))
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(f"allow entry needs expires: YYYY-MM-DD: {e.get('repo')} {e.get('paths')}") from 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:
|
if exp < today:
|
||||||
EXPIRED.append({**e, "expires": exp.isoformat()})
|
EXPIRED.append({**e, "expires": exp.isoformat()})
|
||||||
else:
|
else:
|
||||||
@@ -182,6 +195,8 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]:
|
|||||||
continue
|
continue
|
||||||
if kind == "workers ai binding" and not WRANGLER.search(path):
|
if kind == "workers ai binding" and not WRANGLER.search(path):
|
||||||
continue
|
continue
|
||||||
|
if kind == "talk engine port" and PORT_SKIP.search(path):
|
||||||
|
continue
|
||||||
m = rx.search(text)
|
m = rx.search(text)
|
||||||
if m:
|
if m:
|
||||||
hits.append((kind, m.group(0).strip()[:60]))
|
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:
|
def report(repos: list[str]) -> int:
|
||||||
allow = load_allow()
|
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:
|
for e in EXPIRED:
|
||||||
print(f"## EXPIRED exemption (no longer excuses anything): {e['repo']} {e['paths']} "
|
print(f"## EXPIRED exemption (no longer excuses anything): {e['repo']} {e['paths']} "
|
||||||
f"[{e['exemption']}] expired {e['expires']}")
|
f"[{e['exemption']}] expired {e['expires']}")
|
||||||
|
|||||||
Reference in New Issue
Block a user