4 Commits

Author SHA1 Message Date
Kit OC5
5347c11f69 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>
2026-10-02 18:13:18 -04:00
Kit OC5
5fa1e652ae compute-guard: gatekeeper rules (Mind 10-02) + allow-list exemptions that expire
New kinds: voice-ai host/key (Deepgram, ElevenLabs, Cartesia, PlayHT, Resemble, HeyGen, Google
Vision/Speech/TTS, AWS Transcribe/Polly), cloudflare workers ai (REST /ai/ + wrangler [ai] binding),
talk engine port (:8791/:8788/:8794/:8099). Own kinds so they roll out WARN-first via
COMPUTE_GUARD_WARN_KINDS. Allow entries now need a named exemption (local-user-hardware |
owner-approved | compute-door | guard-self) and an expires date; expired entries stop excusing
code and are reported. ci-hygiene keeps its own (non-strict) format.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 18:11:07 -04:00
Kit OC5
6f19e23eaf lockbox-names: names-only lister (section + label + resolvable yes/no/dup), never a value
All checks were successful
check / gate (push) Successful in 14s
canary / probe (push) Successful in 8s
So lanes can discover what the lockbox holds without opening it (Super Admin 50 request).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 19:23:47 -04:00
ef96051d6f Merge pull request #2 from sneakyfree/runner-guard
All checks were successful
check / gate (push) Successful in 14s
canary / probe (push) Successful in 6s
runner-guard: no stranger code on self-hosted runners (PR check + nightly sweep)
2026-10-01 05:29:07 -04:00
9 changed files with 450 additions and 13 deletions

7
.github/CODEOWNERS vendored Normal file
View 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

View File

@@ -86,7 +86,7 @@ def test_warn_mode_never_turns_red(monkeypatch):
def test_allow_file_is_line_scoped_exceptions_only():
"""Every exception is line-scoped (`matches`), so an allowed file can't hide a
NEW floating install or docker step. Today: windy-pro's if:false deploy job."""
allow = hy.cg.load_allow(hy.ALLOW_FILE)
allow = hy.cg.load_allow(hy.ALLOW_FILE, strict=False)
assert [(e["repo"], e["paths"]) for e in allow] == [("windy-pro", [".github/workflows/ci.yml"])]
assert all(e.get("matches") for e in allow)
ok = " run: docker build -f account-server/Dockerfile -t windy-pro:${{ github.sha }} ."

View File

@@ -77,6 +77,88 @@ def test_allow_list_needs_a_reason_per_entry(tmp_path):
cg.load_allow(bad)
def _entry(**kw):
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", "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):
cg.load_allow(f)
f.write_text(_entry(exemption="because-i-said-so"))
with pytest.raises(ValueError):
cg.load_allow(f)
f.write_text(_entry(expires=None))
with pytest.raises(ValueError):
cg.load_allow(f)
f.write_text(_entry(expires="someday"))
with pytest.raises(ValueError):
cg.load_allow(f)
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):
from datetime import date
f = tmp_path / "a.yml"
f.write_text(_entry(expires="2026-10-01"))
assert cg.load_allow(f, today=date(2026, 10, 1)) # the expiry day is still valid
assert cg.load_allow(f, today=date(2026, 10, 2)) == [] # the next day it no longer excuses anything
assert cg.EXPIRED and cg.EXPIRED[0]["repo"] == "x" and cg.EXPIRED[0]["expires"] == "2026-10-01"
def test_shipped_allow_file_is_valid_today():
assert cg.load_allow() and not cg.EXPIRED # nothing in the repo's own file may already be expired
@pytest.mark.parametrize("text, kind", [
('u = "https://api.deepgram.com/v1/listen"', "voice-ai host"),
("fetch(`https://api.elevenlabs.io/v1/tts`)", "voice-ai host"),
('h = "api.cartesia.ai"', "voice-ai host"),
('h = "app.resemble.ai"', "voice-ai host"),
('h = "speech.googleapis.com"', "voice-ai host"),
('h = "transcribe.us-east-1.amazonaws.com"', "voice-ai host"),
('h = "polly.eu-west-1.amazonaws.com"', "voice-ai host"),
("DEEPGRAM_API_KEY=abc", "voice-ai key"),
("ELEVENLABS_API_KEY = x", "voice-ai key"),
('u = f"https://api.cloudflare.com/client/v4/accounts/{a}/ai/run/@cf/m"', "cloudflare workers ai"),
('ENGINE = "http://10.0.0.5:8791/v1"', "talk engine port"),
('ENGINE = "http://h:8788/ws"', "talk engine port"),
('x = "http://h:8099/health"', "talk engine port"),
])
def test_gatekeeper_rules_fire(text, kind):
assert kind in [k for k, _ in cg.scan_line("app/x.py", text)]
def test_workers_ai_binding_only_in_wrangler_and_near_misses_are_quiet():
assert [k for k, _ in cg.scan_line("wrangler.toml", "[ai]")] == ["workers ai binding"]
assert [k for k, _ in cg.scan_line("apps/x/wrangler.jsonc", ' "ai": {')] == ["workers ai binding"]
assert cg.scan_line("other.toml", "[ai]") == []
assert cg.scan_line("app/x.py", "port = 87912") == []
assert cg.scan_line("app/x.py", "# talk engine was :8791 (removed)") == []
def test_rolling_out_kinds_warn_but_dont_block_and_hard_kinds_still_do(monkeypatch):
monkeypatch.setattr(cg, "MODE", "block")
monkeypatch.setattr(cg, "SOFT_KINDS", {"voice-ai host", "voice-ai key"})
soft = cg.Finding("a.py", 1, "voice-ai host", "api.deepgram.com")
hard = cg.Finding("a.py", 2, "provider host", "api.openai.com")
state, desc, _ = cg.status_for([soft], whole_tree=True)
assert state == "success" and "rolling out" in desc and len(desc) <= 140
assert cg.status_for([soft, hard], whole_tree=True)[0] == "failure"
monkeypatch.setattr(cg, "SOFT_KINDS", set())
assert cg.status_for([soft], whole_tree=True)[0] == "failure" # rollout over: it blocks
@pytest.mark.parametrize(
"repo, path, ok",
[
@@ -250,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"]

View File

@@ -0,0 +1,64 @@
"""lockbox-names: headings + labels + resolvable, NEVER a value or prose after a label."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
V1 = "Qm7xT2vLp9RkZw4HnB8dYc3S" # synthetic values
V2 = "other-fake-value-1234567890"
V3 = "dup-one-aaaaaaaaaaaaaaaa"
V4 = "dup-two-bbbbbbbbbbbbbbbb"
PROSE = "ZZPROSEZZ-not-for-printing"
def make(tmp_path):
r = tmp_path / "kit"
(r / "secrets" / "x").mkdir(parents=True)
(r / "ACCESS_LOCKBOX.md").write_text(
"# LOCKBOX\n\n## 🔷 AZURE signing (added 10-01)\n"
f"- **Tenant:** {PROSE} lives in the portal\n"
f"- **`AZURE_CLIENT_ID`**: `{V1}`\n"
f"- **Secret (AZURE_CLIENT_SECRET):** `{V2}`\n"
"## GOOGLE oauth\n"
f"GOOGLE_OAUTH_CLIENT_ID={V2}\n"
f"- **`DUP_KEY`**: `{V3}`\n- **`DUP_KEY`**: `{V4}`\n"
f"## stray\n**{V1}** is a heading-like bold that is secret shaped? no, just label\n")
(r / "secrets" / "x" / "a.env").write_text(f"FILE_KEY={V1}\n")
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=r, check=True)
return r
def run(r, *args):
e = {**os.environ, "LOCKBOX_REPO": str(r), "LOCKBOX_REF": "WORKTREE"}
p = subprocess.run([sys.executable, str(ROOT / "scripts" / "lockbox_names.py"), *args],
capture_output=True, text=True, env=e)
return p.returncode, p.stdout + p.stderr
def test_lists_labels_and_resolvable_without_values_or_prose(tmp_path):
r = make(tmp_path)
rc, out = run(r, "AZURE|GOOGLE|FILE|DUP")
assert rc == 0
assert "AZURE signing (added 10-01) | AZURE_CLIENT_ID | md | yes" in out
assert "| GOOGLE_OAUTH_CLIENT_ID | env | yes" in out
assert "| FILE_KEY | file | yes" in out
assert "| DUP_KEY | md | dup" in out
# a prose label is listed but never resolvable, and nothing after the label leaks
assert "| Tenant: " not in out or "| Tenant" in out
assert "| label | no" in out
for secret in (V1, V2, V3, V4, PROSE, "lives in the portal"):
assert secret not in out
for i in range(0, len(secret) - 7):
assert secret[i:i + 8] not in out
def test_filter_and_bad_regex(tmp_path):
r = make(tmp_path)
rc, out = run(r, "NOSUCHTHING")
assert rc == 0 and "0 entries" in out
rc, out = run(r, "(")
assert rc == 2 and "bad regex" in out

View File

@@ -2,11 +2,16 @@
# Windy Mind is the ONLY door to AI compute (Grant, 2026-09-23). Every entry
# here is an exception to that rule and MUST say why. Paths are fnmatch globs
# relative to the repo root. Owner of this file: Windy Git lane (13); changes
# go through the orchestrator. Source of the first entries: COMPUTE_BYPASS_AUDIT.md.
# 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);
# 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: ["*"]
reason: "Windy Mind IS the door: provider clients belong here by definition."
exemption: compute-door
expires: 2027-10-02
- repo: windy-agent
paths: ["*"]
@@ -14,14 +19,26 @@ allow:
User BYOK: self-hosted agents call providers on the USER's own keys.
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
paths: ["src/client/desktop/*"]
@@ -30,10 +47,18 @@ allow:
enters (renderer localStorage -> electron-store; env var only for dev), and
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
paths: ["src/client/web/src/pages/panels/MindKeychain.jsx"]
@@ -41,12 +66,20 @@ allow:
# /api/v1/chat, i.e. inference) still flags. Orchestrator-approved 09-23.
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
paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"]
reason: "The guard's own pattern list and this file."
exemption: guard-self
expires: 2027-10-02
- repo: windy-mind
paths: ["*"]
matches: [':11434']
reason: "Windy Mind IS the compute door (endpoint + key); it may call Ollama. Only the Ollama port is allowed here, any provider host/SDK in Mind still flags."
exemption: compute-door
expires: 2027-10-02

View File

@@ -204,7 +204,7 @@ def _check(repo: str, sha: str, default_branch: str, is_default_head: bool):
bare = cg.WORK / f"{repo}.git"
if not bare.is_dir() or not cg.fetched(bare, sha): # pushed after the fetch: next cycle
return None
allow = cg.load_allow(ALLOW_FILE)
allow = cg.load_allow(ALLOW_FILE, strict=False)
rules = hashlib.sha256((PREFILTER + INCLUDE.pattern + EXACT_PY.pattern + EXACT_NPM.pattern).encode()).hexdigest()[:8]
fp = cg._fingerprint(allow) + ":" + rules # hashlib, not hash(): hash() is per-process random
kw = dict(line_fn=scan_line, path_ok=path_ok)
@@ -232,7 +232,7 @@ def status_for(findings, whole_tree: bool, grant=()):
def report(repos: list[str]) -> int:
allow = cg.load_allow(ALLOW_FILE)
allow = cg.load_allow(ALLOW_FILE, strict=False)
total = 0
for repo in repos:
bare = cg.WORK / f"{repo}.git"

View File

@@ -31,6 +31,7 @@ import re
import subprocess
import sys
from dataclasses import dataclass
from datetime import date, timedelta
from pathlib import Path
import yaml
@@ -49,6 +50,17 @@ HOSTS = [
"api.cohere.com", "api.fireworks.ai", "api.replicate.com",
"api-inference.huggingface.co",
]
# Mind's 10-02 gatekeeper list (speech / voice / avatar / vision / cloud ML): own kinds, so a rollout
# can be WARN-first (COMPUTE_GUARD_WARN_KINDS) without softening the original provider rules.
VOICE_HOSTS = [
"api.deepgram.com", "api.elevenlabs.io", "api.cartesia.ai", "api.play.ht", "api.playht.com",
"app.resemble.ai", "f.cluster.resemble.ai", "api.heygen.com",
"vision.googleapis.com", "speech.googleapis.com", "texttospeech.googleapis.com",
]
VOICE_KEYS = [
"DEEPGRAM_API_KEY", "ELEVENLABS_API_KEY", "ELEVEN_API_KEY", "CARTESIA_API_KEY", "PLAYHT_API_KEY",
"PLAY_HT_API_KEY", "PLAYHT_USER_ID", "RESEMBLE_API_KEY", "HEYGEN_API_KEY",
]
KEYS = [
"ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN",
"OPENAI_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY",
@@ -61,11 +73,23 @@ 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*\{')
RULES: list[tuple[str, re.Pattern]] = [
("provider host", re.compile("|".join(re.escape(h) for h in HOSTS))),
# Grant via Boss 10-01: compute = Windy Mind. A NEW reference to an Ollama port (Veron's :11434) is a
# direct call around Mind's metering/caps. WARN-only, never red, and only for lines a PR ADDS.
("veron ollama", re.compile(r"(?::|%3[aA])11434(?![0-9])")),
("voice-ai host", re.compile("|".join(re.escape(h) for h in VOICE_HOSTS))),
("voice-ai key", re.compile(r"\b(?:" + "|".join(VOICE_KEYS) + r")\b")),
("voice-ai host", re.compile(r"(?:transcribe|polly)\.[a-z0-9-]+\.amazonaws\.com")),
("provider host", re.compile(r"(?:bedrock-runtime|bedrock)\.[a-z0-9-]+\.amazonaws\.com")),
("cloudflare workers ai", re.compile(r"api\.cloudflare\.com/client/v4/accounts/[^\s'\"/]+/ai/")),
("talk engine port", re.compile(r"(?::|%3[aA])(?:8791|8788|8794|8099)(?![0-9])")),
("workers ai binding", WRANGLER_AI),
("provider key", re.compile(r"\b(?:" + "|".join(KEYS) + r")\b")),
("provider SDK", re.compile(rf"^\s*(?:from|import)\s+(?:{PY_SDKS})(?:\s|\.|$|,)")),
("provider SDK", re.compile(rf"""(?:from\s+|require\(\s*|import\(\s*)['"](?:{JS_SDKS})(?:/[^'"]*)?['"]""")),
@@ -75,6 +99,9 @@ RULES: list[tuple[str, re.Pattern]] = [
]
# Kinds that never block (even in MODE=block) and are only judged on ADDED lines, never the baseline tree.
WARN_ONLY_KINDS = {"veron ollama"}
# Rolled out WARN-first: these kinds still show (tree + PRs) but never block, until the env var
# (a systemd drop-in on the sync, like SECRET_GUARD_WARN_KINDS) is removed.
SOFT_KINDS = {k for k in os.environ.get("COMPUTE_GUARD_WARN_KINDS", "").split(",") if k}
OLLAMA_MSG = "compute = Windy Mind (endpoint + key); do not call Veron's Ollama directly"
DEP_FILES = re.compile(r"(^|/)(package\.json|requirements[^/]*\.txt|pyproject\.toml|setup\.cfg|Pipfile)$")
@@ -95,13 +122,47 @@ class Finding:
match: str
def load_allow(path: Path = ALLOW_FILE) -> list[dict]:
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]:
"""Active entries only. Every entry needs repo, paths, a reason, a NAMED exemption and an expiry
date (Mind 10-02: nothing gets permanent amnesty). An expired entry stops excusing code at once.
`strict=False` is for OTHER guards reusing this loader (ci-hygiene) with their own file format."""
today = today or date.today()
data = yaml.safe_load(path.read_text()) or {}
entries = data.get("allow") or []
for e in entries: # a reason per entry is the whole point of the file
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}")
return entries
if not strict:
active.append(e)
continue
if e.get("exemption") not in EXEMPTIONS:
raise ValueError(f"allow entry needs exemption in {sorted(EXEMPTIONS)}: {e.get('repo')} {e.get('paths')}")
try:
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:
active.append(e)
return active
def allowed(repo: str, path: str, allow: list[dict], text: str | None = None) -> bool:
@@ -132,6 +193,10 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]:
for kind, rx in RULES:
if kind == "provider SDK dep" and not DEP_FILES.search(path):
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]))
@@ -156,9 +221,11 @@ def scan_tree(repo: str, bare: Path, sha: str, allow: list[dict], *, line_fn=Non
# Other guards (ci_hygiene) reuse this walker with their own line rules.
line_fn = line_fn or scan_line
path_ok = path_ok or _default_path_ok
pre = prefilter or "|".join([re.escape(h) for h in HOSTS] + KEYS + [
pre = prefilter or "|".join([re.escape(h) for h in HOSTS + VOICE_HOSTS] + KEYS + VOICE_KEYS + [
"anthropic", "openai", "groq", "mistral", "generativeai", "genai", "cohere",
"together", "cerebras", "litellm"])
"together", "cerebras", "litellm", "deepgram", "elevenlabs", "cartesia", "play\\.ht", "resemble",
"heygen", "googleapis\\.com", "amazonaws\\.com", "api\\.cloudflare\\.com", ":8791", ":8788",
":8794", ":8099", "%3[aA]87", "%3[aA]8099", r"^\s*\[ai\]", '"ai"'])
try:
out = _git(bare, "grep", "-nIE", "-e", pre, sha, "--", ".")
except subprocess.CalledProcessError as e:
@@ -215,7 +282,8 @@ def parse_added(repo: str, diff: str, allow: list[dict], *, line_fn=None, path_o
# ---- cache: a tree scan runs once per (repo, sha, rules+allow) --------------
def _fingerprint(allow: list[dict]) -> str:
return hashlib.sha256(
json.dumps([HOSTS, KEYS, PY_SDKS, JS_SDKS, SKIP.pattern, allow], sort_keys=True).encode()
json.dumps([HOSTS, KEYS, VOICE_HOSTS, VOICE_KEYS, [r.pattern for _, r in RULES], PY_SDKS, JS_SDKS,
SKIP.pattern, allow], sort_keys=True, default=str).encode()
).hexdigest()[:16]
@@ -280,6 +348,13 @@ def status_for(findings: list[Finding], whole_tree: bool,
if not findings and not grant and soft:
f = soft[0]
return "success", f"⚠ WARN: new Veron Ollama ref {f.path}:{f.line}. {OLLAMA_MSG}"[:140], f
rolling = [f for f in findings if f.kind in SOFT_KINDS]
if findings and len(rolling) == len(findings) and not grant:
f, n = rolling[0], len(rolling)
return "success", (f"⚠ WARN (rolling out, not blocking): {n} direct AI-provider use{'s' if n > 1 else ''} "
f"{scope}, e.g. {f.path}:{f.line} {f.match}")[:140], f
if rolling:
findings = [f for f in findings if f.kind not in SOFT_KINDS]
if not findings and grant:
g, n = grant[0], len(grant)
desc = (f"⚠ WARN (Grant-owned, not blocking): {n} direct AI-provider use{'s' if n > 1 else ''} "
@@ -298,6 +373,12 @@ 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']}")
total = 0
for repo in repos:
bare = WORK / f"{repo}.git"

View File

@@ -5,10 +5,10 @@ set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
dest="$HOME/.local/share/secret-tools"
mkdir -p "$dest" "$HOME/.local/bin"
cp "$here/secret_shapes.py" "$here/secret_scan.py" "$here/env_names.py" "$here/lockbox_put.py" "$dest/"
for pair in "secret-scan:secret_scan.py" "env-names:env_names.py" "lockbox-put:lockbox_put.py"; do
cp "$here/secret_shapes.py" "$here/secret_scan.py" "$here/env_names.py" "$here/lockbox_put.py" "$here/lockbox_names.py" "$dest/"
for pair in "secret-scan:secret_scan.py" "env-names:env_names.py" "lockbox-put:lockbox_put.py" "lockbox-names:lockbox_names.py"; do
n=${pair%%:*}; f=${pair##*:}
printf '#!/usr/bin/env bash\nexec python3 "%s/%s" "$@"\n' "$dest" "$f" > "$HOME/.local/bin/$n"
chmod 755 "$HOME/.local/bin/$n"
done
echo "installed secret-scan, env-names and lockbox-put (shapes from secret_shapes.py, same as secret-guard)"
echo "installed secret-scan, env-names, lockbox-put and lockbox-names (shapes from secret_shapes.py, same as secret-guard)"

134
scripts/lockbox_names.py Normal file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""lockbox-names: DISCOVER what the lockbox holds without reading it (Boss rule 10-01).
lockbox-names [REGEX] (case-insensitive; matches the section heading or the label)
Prints one row per entry: SECTION HEADING | LABEL | kind | resolvable
kind env = `KEY=value` line md = `- **`KEY`**: `value`` line
label = a bold prose label (`**Password (X):** ...`) file = secrets/**/*.env key
resolvable yes = `lockbox-get LABEL FILE` returns exactly one value
dup = defined with 2+ different values (lockbox-get refuses)
no = a prose-only label, or not an exact key
NEVER prints a value or any prose after a label. A label or heading that itself looks
like a secret (secret_shapes) is replaced by <secret-shaped>. Reads the COMMITTED lockbox at
origin/main (like lockbox-get; LOCKBOX_REF=<ref> or WORKTREE overrides). Memory only, stdout only.
"""
from __future__ import annotations
import hashlib
import os
import re
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import secret_shapes as ss # noqa: E402
REPO = os.environ.get("LOCKBOX_REPO", os.path.expanduser("~/kit-army-config"))
REF = os.environ.get("LOCKBOX_REF", "origin/main")
HEAD = re.compile(r"^#{1,6}\s+(.*\S)\s*$")
ENV = re.compile(r"^([A-Z][A-Z0-9_]{2,})=(.*)$")
MD = re.compile(r"^\s*[-*]?\s*\*\*`([A-Za-z0-9_]+)`\*\*\s*:\s*`([^`]+)`")
LABEL = re.compile(r"\*\*([^*`]{2,70}?)\*\*")
def git(*a: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", "-C", REPO, *a], capture_output=True, text=True, errors="ignore")
def read_sources() -> dict[str, str]:
"""{path: text} for ACCESS_LOCKBOX.md and secrets/**/*.env."""
if REF == "WORKTREE":
out = {}
for p in [Path(REPO, "ACCESS_LOCKBOX.md"), *Path(REPO, "secrets").rglob("*.env")]:
if p.is_file():
out[str(p.relative_to(REPO))] = p.read_text(errors="ignore")
return out
if REF.startswith("origin/"):
git("fetch", "-q", "origin", REF.split("/", 1)[1])
names = ["ACCESS_LOCKBOX.md"] + [
p for p in git("ls-tree", "-r", "--name-only", REF, "--", "secrets").stdout.splitlines() if p.endswith(".env")]
out = {}
for n in names:
r = git("show", f"{REF}:{n}")
if r.returncode == 0:
out[n] = r.stdout
return out
def safe(text: str, limit: int = 70) -> str:
text = re.sub(r"\s+", " ", text).strip()
return "<secret-shaped>" if ss.find(text) else text[:limit]
def h(v: str) -> str:
return hashlib.sha256(v.strip().strip('"').strip("'").encode()).hexdigest()[:16]
def collect(src: dict[str, str]):
"""(rows, values) where values[KEY] = {hash,...} for resolvability; nothing printed from it."""
rows, values = [], {}
for path, text in src.items():
section = path
for line in text.splitlines():
m = HEAD.match(line) if path.endswith(".md") else None
if m:
section = safe(m.group(1), 90)
continue
m = ENV.match(line)
if m:
values.setdefault(m.group(1), set()).add(h(m.group(2)))
rows.append((section, m.group(1), "file" if path.startswith("secrets/") else "env"))
continue
m = MD.match(line)
if m:
values.setdefault(m.group(1), set()).add(h(m.group(2)))
rows.append((section, m.group(1), "md"))
continue
if path.endswith(".md"):
mm = LABEL.search(line)
if mm and not mm.group(1).startswith("http"):
rows.append((section, safe(mm.group(1)), "label"))
return rows, values
def resolvable(label: str, kind: str, values) -> str:
if kind not in ("env", "md", "file"):
return "no"
n = len(values.get(label, ()))
return "yes" if n == 1 else "dup" if n > 1 else "no"
def main(argv=None) -> int:
argv = list(sys.argv[1:] if argv is None else argv)
rx = re.compile(argv[0], re.I) if argv else None
src = read_sources()
if not src:
print("lockbox-names: cannot read the lockbox")
return 2
rows, values = collect(src)
seen, n = set(), 0
for section, label, kind in rows:
if rx and not (rx.search(section) or rx.search(label)):
continue
key = (section, label, kind)
if key in seen:
continue
seen.add(key)
n += 1
print(f"{section} | {label} | {kind} | {resolvable(label, kind, values)}")
print(f"# {n} entr{'y' if n == 1 else 'ies'}; names only, values never printed")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except re.error:
print("lockbox-names: bad regex")
sys.exit(2)
except Exception as e: # never a traceback
print(f"lockbox-names: error: {type(e).__name__}")
sys.exit(2)