27 Commits

Author SHA1 Message Date
d609aa942a Merge pull request #6 from sneakyfree/guard-exempt-talk-client
All checks were successful
check / gate (push) Successful in 13s
canary / probe (push) Successful in 5s
compute-guard: windytalk client-side :8788 filed engine-side
2026-10-02 18:26:01 -04:00
Kit OC5
0fb2df11a6 compute-guard: windytalk client-side :8788 refs filed as engine-side (Windy Talk's classification)
run-client.sh points at an ssh -L tunnel to the dev engine on Veron; index.html lines are display-only fallbacks;
the shipped desktop client defaults to the public engine on Veron. Same dated owner-approved exemption
(approved_by windy-hub, expires 2026-12-31), NOT local-user-hardware.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 18:25:56 -04:00
44a1800eff Merge pull request #5 from sneakyfree/guard-exempt-talk-registry
All checks were successful
check / gate (push) Successful in 12s
compute-guard: dated exemptions (registry dev origin, windytalk engine-side)
2026-10-02 18:25:19 -04:00
Kit OC5
dc1cfbf044 compute-guard: dated exemptions for windy-registry dev CORS origin and windytalk engine-side ports
Hub decision 10-02 (alternative B, rule stays strict): registry tools/r2-provision.sh :8788 is a dev origin in an
R2 CORS rule; windytalk engine/server/systemd/stress ports are Talk's own engines (owner-approved, NOT compute-door,
a real bypass to be put behind Mind). approved_by windy-hub, expires 2026-12-31. Client-side files pending Talk.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 18:25:14 -04:00
61776ce021 Merge pull request #4 from sneakyfree/guard-drop-8099
compute-guard: drop :8099 (false positive) + Deepgram template exemption
2026-10-02 18:20:37 -04:00
Kit OC5
5c42b0e760 compute-guard: drop :8099 from the Talk-engine ports (false positive); Deepgram template line under the BYOK exemption
Hub classification 10-02: :8099 in windy-pro is the OLD translate-api / cloud-storage service, not Windy Talk
(verified: windytalk has no :8099, only lockfile hash fragments). DEEPGRAM_API_KEY in windy-pro .env.example is
a template line for the user-own-key Deepgram feature: owner-approved, approved_by windy-hub, expires 2026-12-31.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 18:20:28 -04:00
74ca5ac19b Merge pull request #3 from sneakyfree/compute-guard-gatekeeper
All checks were successful
check / gate (push) Successful in 20s
canary / probe (push) Successful in 5s
compute-guard: gatekeeper rules + expiring exemptions (Windy Mind 10-02)
2026-10-02 18:13:26 -04:00
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
Kit OC5
a0dc6f8568 runner-guard: block workflows that hand a self-hosted runner to strangers (Boss 10-01)
R1 pull_request_target; R2 fork pull_request on self-hosted without a same-repo/environment
gate; R3 outsider events (issue_comment, workflow_run, ...) on self-hosted; R0 unparseable.
PR mode in the 5-min sync posts windy-git/runner-guard on open PRs of public sneakyfree repos
that change a workflow (each status once). Nightly sweep (Windy 0 timer) over every public
repo: page + BOARD line on new hits + red windy-job heartbeat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 05:28:37 -04:00
Kit OC5
51e0b9d30b bridge + sync: onboard windy-calendar-site (static, no workflows; guards only)
All checks were successful
check / gate (push) Successful in 28s
canary / probe (push) Successful in 6s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 05:04:49 -04:00
Kit OC5
51777b0ad0 bridge + sync: onboard windy-hand-site (static site, no workflows yet; guards only)
All checks were successful
check / gate (push) Successful in 13s
canary / probe (push) Successful in 6s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:50:15 -04:00
Kit OC5
cbcb4c206b docs: CUTOVER-PRIMARY.md checklist (draft, nothing flipped)
All checks were successful
check / gate (push) Successful in 45s
canary / probe (push) Successful in 7s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 04:03:09 -04:00
Kit OC5
2c62e2834a secret-guard allow: windy-agent #412 synthetic Z.ai fixture (hash not in lockbox)
All checks were successful
check / gate (push) Successful in 20s
canary / probe (push) Successful in 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 03:56:48 -04:00
3a9b7ecab7 Merge pull request #1 from sneakyfree/veron-ollama-warn
All checks were successful
check / gate (push) Successful in 22s
canary / probe (push) Successful in 8s
compute-guard: WARN on new Veron Ollama (:11434) references
2026-10-01 03:36:32 -04:00
Kit OC5
cd0400c371 compute-guard: WARN (never red) on NEW references to Veron Ollama :11434
Grant via Boss 10-01: compute = Windy Mind (endpoint + key); do not call Veron Ollama directly.
Judged on lines a PR adds only (not the baseline tree), never blocks even in MODE=block,
windy-mind (the compute door) allowed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 03:36:11 -04:00
Kit OC5
4e7ab667ed backup_state: pin restic cache dir (systemd has no HOME); init only on a MISSING repo, log other errors
All checks were successful
check / gate (push) Successful in 38s
canary / probe (push) Successful in 10s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 03:23:41 -04:00
Kit OC5
f10db19316 drill_cross_host.sh: read the R2 pair + endpoint from the lockbox (drill PASSED 10-01)
All checks were successful
check / gate (push) Successful in 18s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 03:19:56 -04:00
Kit OC5
3503894a1e state backup: systemd units (NOT enabled) + cross-host drill script
All checks were successful
check / gate (push) Successful in 23s
canary / probe (push) Successful in 7s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:59:19 -04:00
Kit OC5
fbab5c4669 secret-guard/secret-scan: PyPI API token shape (pypi-AgE macaroon), WARN-first
All checks were successful
check / gate (push) Successful in 20s
canary / probe (push) Successful in 13s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:47:53 -04:00
Kit OC5
1b552c0882 docs: RESTORE-DRILL.md (state backup + restore procedure)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:46:53 -04:00
Kit OC5
8ebc18c3bc gitea 1.24.6 -> 1.24.7 (security: LFS auth bypass, symlink bypass, OAuth2 missed return)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:45:28 -04:00
Kit OC5
9566826ce9 lockbox-put: append-only lockbox PR tool (no one reads/greps the lockbox); installer updated
All checks were successful
check / gate (push) Successful in 28s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:40:31 -04:00
Kit OC5
160a3d69ba backup_state.sh: encrypted restic backup of Postgres + Gitea state to R2 (SOTU 10-01 gap: DB was not backed up)
All checks were successful
check / gate (push) Successful in 23s
canary / probe (push) Successful in 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 02:35:09 -04:00
Kit OC5
7e28a23c22 secret-scan + env-names: shared hash-only tools (Boss 10-01: lanes printed secrets while hunting them)
All checks were successful
check / gate (push) Successful in 9s
canary / probe (push) Successful in 5s
secret-scan reports file:line/commit + lockbox KEY NAME or shape, never a value or fragment;
env-names lists variable names/length/hash only. Same shapes as secret-guard. Seeded-fake tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 01:17:30 -04:00
32 changed files with 1759 additions and 15 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(): def test_allow_file_is_line_scoped_exceptions_only():
"""Every exception is line-scoped (`matches`), so an allowed file can't hide a """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.""" 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 [(e["repo"], e["paths"]) for e in allow] == [("windy-pro", [".github/workflows/ci.yml"])]
assert all(e.get("matches") for e in allow) assert all(e.get("matches") for e in allow)
ok = " run: docker build -f account-server/Dockerfile -t windy-pro:${{ github.sha }} ." 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) 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:8794/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( @pytest.mark.parametrize(
"repo, path, ok", "repo, path, ok",
[ [
@@ -229,3 +311,62 @@ def test_block_mode_never_blocks_grant_owned(monkeypatch):
assert state == "success" and desc.startswith("⚠ WARN (Grant-owned, not blocking): 1") and f is g assert state == "success" and desc.startswith("⚠ WARN (Grant-owned, not blocking): 1") and f is g
lane = cg.Finding("a.py", 3, "provider host", "x") lane = cg.Finding("a.py", 3, "provider host", "x")
assert cg.status_for([lane], whole_tree=True, grant=[g])[0] == "failure" assert cg.status_for([lane], whole_tree=True, grant=[g])[0] == "failure"
def test_veron_ollama_warns_on_added_lines_and_never_blocks(monkeypatch):
hits = cg.scan_line("app/llm.py", 'OLLAMA = "http://192.168.1.73:11434/api/generate"')
assert [k for k, _ in hits] == ["veron ollama"]
assert cg.scan_line("app/llm.py", 'port = 114345') == [] # not the port
assert cg.scan_line("app/llm.py", "# was http://x:11434 (removed)") == [] # a comment is not a call
f = cg.Finding("app/llm.py", 7, "veron ollama", ":11434")
monkeypatch.setattr(cg, "MODE", "block")
state, desc, _ = cg.status_for([f], whole_tree=False)
assert state == "success" and desc.startswith("⚠ WARN: new Veron Ollama ref app/llm.py:7")
assert "Windy Mind" in desc and len(desc) <= 140
hard = cg.Finding("app/llm.py", 1, "provider host", "api.openai.com")
assert cg.status_for([f, hard], whole_tree=False)[0] == "failure" # a real violation still blocks
def test_ollama_in_added_pr_lines_only():
diff = ("+++ b/svc/client.py\n@@ -0,0 +1,2 @@\n+import httpx\n"
"+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:8791/x"') == []
assert cg.scan_line("services/api/openapi/spec.json", '"url": "http://h:8791/x"') == []
assert [k for k, _ in cg.scan_line("deploy/docker-compose.yml", " - 8791:8791 # :8791")] == ["talk engine port"]
# :8099 is windy-pro's OLD translate-api / cloud-storage service, NOT the Talk engine (Hub 10-02): not flagged
assert cg.scan_line("deploy/docker-compose.yml", " - 8099:8099 # translate-api:8099") == []

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

@@ -0,0 +1,77 @@
"""lockbox-put against a LOCAL fake lockbox repo only (never the real one)."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FAKE = "Zk3vQ8mT1pLw7Xn2Rb5Hd9Yc" # synthetic
def sh(*a, cwd=None):
return subprocess.run(a, cwd=cwd, check=True, capture_output=True, text=True)
def make_remote(tmp_path):
work = tmp_path / "seed"
work.mkdir()
sh("git", "init", "-q", "-b", "main", cwd=work)
(work / "ACCESS_LOCKBOX.md").write_text("# LOCKBOX\n\n- **`EXISTING_KEY`**: `abcdefgh12345678`\nOLD_ENV_KEY=whatever123456\n")
sh("git", "add", "-A", cwd=work)
sh("git", "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "seed", cwd=work)
bare = tmp_path / "remote.git"
sh("git", "clone", "-q", "--bare", str(work), str(bare))
return bare
def put(tmp_path, bare, key, content, mode=0o600):
f = tmp_path / "val"
f.write_text(content)
os.chmod(f, mode)
e = {**os.environ, "LOCKBOX_PUT_REPO": str(bare), "LOCKBOX_PUT_NO_PR": "1", "HOME": str(tmp_path / "h")}
(tmp_path / "h" / ".cache").mkdir(parents=True, exist_ok=True)
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "lockbox_put.py"), key, str(f), "--lane", "test", "--note", "n"],
capture_output=True, text=True, env=e)
return r.returncode, r.stdout + r.stderr
def test_appends_one_key_by_branch_and_never_echoes_value(tmp_path):
bare = make_remote(tmp_path)
rc, out = put(tmp_path, bare, "NEW_TEST_KEY", FAKE)
assert rc == 0 and "pushed branch lockbox-put/new_test_key-" in out
assert FAKE not in out and FAKE[:6] not in out
br = [b.strip() for b in sh("git", "branch", "--list", "lockbox-put/*", cwd=bare).stdout.splitlines()]
assert len(br) == 1
diff = sh("git", "diff", "--numstat", f"main..{br[0]}", cwd=bare).stdout.split()
assert diff[1] == "0" and diff[2] == "ACCESS_LOCKBOX.md" # additions only
content = sh("git", "show", f"{br[0]}:ACCESS_LOCKBOX.md", cwd=bare).stdout
assert f"- **`NEW_TEST_KEY`**: `{FAKE}`" in content and "EXISTING_KEY" in content
# main is untouched
assert FAKE not in sh("git", "show", "main:ACCESS_LOCKBOX.md", cwd=bare).stdout
def test_refuses_existing_key_both_formats_and_bad_input(tmp_path):
bare = make_remote(tmp_path)
for k in ("EXISTING_KEY", "OLD_ENV_KEY"):
rc, out = put(tmp_path, bare, k, FAKE)
assert rc == 3 and "already exists" in out and FAKE not in out
assert put(tmp_path, bare, "lower_case", FAKE)[0] == 2
assert put(tmp_path, bare, "OK_KEY_1", FAKE, mode=0o644)[0] == 2 # not 0600
assert put(tmp_path, bare, "OK_KEY_2", "has space `tick`")[0] == 2 # unsafe value
assert not sh("git", "branch", "--list", "lockbox-put/*", cwd=bare).stdout.strip() # nothing pushed
def test_symlink_refused(tmp_path):
bare = make_remote(tmp_path)
real = tmp_path / "real"
real.write_text(FAKE)
os.chmod(real, 0o600)
link = tmp_path / "link"
link.symlink_to(real)
e = {**os.environ, "LOCKBOX_PUT_REPO": str(bare), "LOCKBOX_PUT_NO_PR": "1"}
r = subprocess.run([sys.executable, str(ROOT / "scripts" / "lockbox_put.py"), "SYM_KEY", str(link)],
capture_output=True, text=True, env=e)
assert r.returncode == 2 and FAKE not in r.stdout + r.stderr

View File

@@ -0,0 +1,70 @@
"""runner-guard: workflow shapes that hand a self-hosted runner to strangers."""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
_spec = importlib.util.spec_from_file_location("runner_guard", ROOT / "scripts" / "runner_guard.py")
rg = importlib.util.module_from_spec(_spec)
sys.modules["runner_guard"] = rg
_spec.loader.exec_module(rg)
def rules(text):
return [(r, ln) for _p, ln, r, _m in rg.lint_text("w.yml", text)]
def test_pull_request_target_always_fails():
assert rules("on: pull_request_target\njobs:\n a:\n runs-on: ubuntu-latest\n steps: []\n")[0][0] == "R1"
def test_fork_pr_on_self_hosted_fails_and_points_at_runs_on():
wf = "on:\n pull_request:\njobs:\n t:\n runs-on: [self-hosted, linux, x64]\n steps: []\n"
assert rules(wf) == [("R2", 5)]
def test_same_repo_gate_or_environment_passes():
gated = ("on: [pull_request]\njobs:\n t:\n if: github.event.pull_request.head.repo.full_name == github.repository\n"
" runs-on: [self-hosted]\n steps: []\n")
env = "on: [pull_request]\njobs:\n t:\n environment: ci\n runs-on: self-hosted\n steps: []\n"
assert rules(gated) == [] and rules(env) == []
def test_outsider_events_on_self_hosted_fail_but_writer_events_pass():
wf = "on:\n issue_comment:\n workflow_run:\n workflows: [x]\njobs:\n t:\n runs-on: self-hosted\n steps: []\n"
assert sorted(r for r, _ in rules(wf)) == ["R3", "R3"]
ok = "on:\n push:\n tags: ['v*']\n workflow_dispatch:\n schedule:\n - cron: '0 3 * * *'\njobs:\n t:\n runs-on: self-hosted\n steps: []\n"
assert rules(ok) == []
def test_expression_runs_on_is_treated_as_self_hosted_and_hosted_runner_is_fine():
expr = "on: pull_request\njobs:\n t:\n runs-on: ${{ matrix.os }}\n steps: []\n"
hosted = "on: pull_request\njobs:\n t:\n runs-on: ubuntu-latest\n steps: []\n"
assert rules(expr) == [("R2", 4)] and rules(hosted) == []
def test_broken_yaml_is_a_finding_and_non_workflows_are_ignored():
assert rules("on: [push\njobs: {")[0][0] == "R0"
assert rules("name: just a file\n") == []
def test_pr_mode_posts_each_status_once(monkeypatch, tmp_path):
calls = []
monkeypatch.setattr(rg, "STATE", str(tmp_path / "s.json"))
monkeypatch.setattr(rg, "public_repos", lambda owners: [("o/r", "main")])
monkeypatch.setattr(rg, "file_at", lambda *a: "on: push\\njobs:\\n t:\\n runs-on: self-hosted\\n steps: []\\n")
def fake_gh(*args, check=True):
if args[0].startswith("repos/o/r/pulls?"):
return "7 abc123 o/r\\n"
if args[0].endswith("/files?per_page=100"):
return ".github/workflows/ci.yml\\n"
calls.append(args[0])
return ""
monkeypatch.setattr(rg, "gh", fake_gh)
rg.cmd_pr(["o"], post=True)
rg.cmd_pr(["o"], post=True)
assert calls == ["repos/o/r/statuses/abc123"]

View File

@@ -146,3 +146,11 @@ def test_warn_kinds_do_not_block(monkeypatch):
assert sg.status_for([f], True)[0] == "success" assert sg.status_for([f], True)[0] == "success"
monkeypatch.setattr(sg, "WARN_KINDS", set()) monkeypatch.setattr(sg, "WARN_KINDS", set())
assert sg.status_for([f], True)[0] == "failure" assert sg.status_for([f], True)[0] == "failure"
def test_pypi_token_shape_hash_only():
tok = "pypi-AgE" + "Ab1_-" * 20 # synthetic
got = ss.find(f"password = {tok}")
assert [k for k, _ in got] == ["pypi token"] and got[0][1] == ss.h8(tok)
assert tok not in repr(got)
assert ss.find("pypi-AgE-too-short") == []

View File

@@ -0,0 +1,106 @@
"""secret-scan + env-names: findings carry label/location/hash, NEVER a value or fragment."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
# Synthetic, random-looking, never real. FAKE_LB is in the fake lockbox; TWI matches a shape.
FAKE_LB = "k7Xv2QpLm9RtZw4HnB8dYc3S"
TWI = "9f3a7c1e5b2d48806a1f4e7d2c9b0835"
def run(script, *args, env=None):
e = {**os.environ, **(env or {})}
r = subprocess.run([sys.executable, str(SCRIPTS / script), *args], capture_output=True, text=True, env=e)
return r.returncode, r.stdout + r.stderr
def no_fragment(out: str, value: str, n: int = 6):
assert value not in out
for i in range(len(value) - n + 1):
assert value[i:i + n] not in out, f"fragment of the value leaked at {i}"
def seeded(tmp_path):
lb = tmp_path / "lockbox.md"
lb.write_text(f"FAKE_VENDOR_API_KEY={FAKE_LB}\nNOTE: nothing here\n")
repo = tmp_path / "repo"
repo.mkdir()
g = lambda *a: subprocess.run(["git", "-C", str(repo), *a], check=True, capture_output=True) # noqa: E731
g("init", "-q", "-b", "main")
g("config", "user.email", "t@t")
g("config", "user.name", "t")
(repo / "app.py").write_text(f'KEY = "{FAKE_LB}"\nTWILIO_AUTH_TOKEN = "{TWI}"\n')
g("add", "-A")
g("commit", "-qm", "add secrets")
(repo / "app.py").write_text("KEY = None\n") # removed from HEAD, still in history
g("commit", "-qam", "remove")
return lb, repo
def test_tree_scan_labels_locations_no_value(tmp_path):
lb, repo = seeded(tmp_path)
(repo / "live.py").write_text(f'x = "{FAKE_LB}"\n')
rc, out = run("secret_scan.py", str(repo / "live.py"), env={"SECRET_SCAN_LOCKBOX_PATHS": str(lb)})
assert rc == 1
assert "live.py:1" in out and "lockbox:FAKE_VENDOR_API_KEY" in out
no_fragment(out, FAKE_LB)
def test_history_finds_removed_secret_with_commit(tmp_path):
lb, repo = seeded(tmp_path)
rc, out = run("secret_scan.py", str(repo), "--history", env={"SECRET_SCAN_LOCKBOX_PATHS": str(lb)})
assert rc == 1
assert "app.py:1" in out and "commit=" in out and "lockbox:FAKE_VENDOR_API_KEY" in out
assert "app.py:2" in out and "32-hex secret assignment" in out
no_fragment(out, FAKE_LB)
no_fragment(out, TWI)
def test_repo_flag_clones_scans_and_cleans_up(tmp_path):
lb, repo = seeded(tmp_path)
cache = tmp_path / "home"
(cache / ".cache").mkdir(parents=True)
rc, out = run("secret_scan.py", "--repo", str(repo),
env={"SECRET_SCAN_LOCKBOX_PATHS": str(lb), "HOME": str(cache)})
assert rc == 1 and "app.py:1" in out
no_fragment(out, FAKE_LB)
assert not [p for p in (cache / ".cache").iterdir() if p.name.startswith("secret-scan-")]
def test_clean_tree_and_bad_input(tmp_path):
(tmp_path / "ok.txt").write_text("hello world\n")
rc, out = run("secret_scan.py", str(tmp_path / "ok.txt"), "--no-lockbox")
assert rc == 0 and "0 finding(s)" in out
rc, out = run("secret_scan.py", str(tmp_path), "--history", "--no-lockbox")
assert rc == 2 and "needs a git repo" in out
def test_env_names_never_prints_values(tmp_path):
a = tmp_path / "a.env"
b = tmp_path / "b.env"
a.write_text(f"# c\nexport DB_PASSWORD={FAKE_LB}\nTOKEN=\"{TWI}\"\nEMPTY=\nONLY_A=1\n")
b.write_text(f"DB_PASSWORD={FAKE_LB}\nTOKEN=different-value-here\nONLY_B=2\n")
rc, out = run("env_names.py", str(a), "--hash")
assert rc == 0 and "DB_PASSWORD" in out and "set" in out and "empty" in out and "len=24" in out
assert "sha256:" in out
no_fragment(out, FAKE_LB)
no_fragment(out, TWI, 7)
rc, out = run("env_names.py", str(a), "--compare", str(b))
assert "DB_PASSWORD" in out and "SAME" in out and "DIFFERENT" in out
assert "only-in-A" in out and "only-in-B" in out
no_fragment(out, FAKE_LB)
rc, out = run("env_names.py", str(tmp_path / "missing.env"))
assert rc == 2
def test_shapes_are_the_guards_shapes():
sys.path.insert(0, str(SCRIPTS))
import secret_scan as sc
import secret_shapes as ss
assert sc.ss is ss # one source of shapes: a new guard shape is automatically a scan shape

View File

@@ -2,11 +2,16 @@
# Windy Mind is the ONLY door to AI compute (Grant, 2026-09-23). Every entry # 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 # 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 # 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: allow:
- repo: windy-mind - repo: windy-mind
paths: ["*"] paths: ["*"]
reason: "Windy Mind IS the door: provider clients belong here by definition." reason: "Windy Mind IS the door: provider clients belong here by definition."
exemption: compute-door
expires: 2027-10-02
- repo: windy-agent - repo: windy-agent
paths: ["*"] paths: ["*"]
@@ -14,14 +19,26 @@ allow:
User BYOK: self-hosted agents call providers on the USER's own keys. 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 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
approved_by: windy-hub
approved_on: 2026-10-02
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
approved_by: windy-hub
approved_on: 2026-10-02
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
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windy-pro - repo: windy-pro
paths: ["src/client/desktop/*"] paths: ["src/client/desktop/*"]
@@ -30,10 +47,18 @@ allow:
enters (renderer localStorage -> electron-store; env var only for dev), and enters (renderer localStorage -> electron-store; env var only for dev), and
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
approved_by: windy-hub
approved_on: 2026-10-02
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
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windy-pro - repo: windy-pro
paths: ["src/client/web/src/pages/panels/MindKeychain.jsx"] paths: ["src/client/web/src/pages/panels/MindKeychain.jsx"]
@@ -41,7 +66,65 @@ allow:
# /api/v1/chat, i.e. inference) still flags. Orchestrator-approved 09-23. # /api/v1/chat, i.e. inference) still flags. Orchestrator-approved 09-23.
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
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windy-git - repo: windy-git
paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"] paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"]
reason: "The guard's own pattern list and this file." 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
- repo: windy-pro
paths: [".env.example"]
matches: ['DEEPGRAM_API_KEY']
reason: "Template line (name only, no value) for the existing user-own-key Deepgram feature in Word's Settings. Mind's migration removes the feature and then this line (Hub classification 10-02)."
exemption: owner-approved
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windy-registry
paths: ["tools/r2-provision.sh"]
matches: ['http://localhost:8788']
reason: "Dev origin in an R2 bucket CORS rule, not a call to the Talk engine (Hub 10-02)."
exemption: owner-approved
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windytalk
paths: ["engine/*", "scripts/stress/*", "scripts/veron/*"]
matches: [':(8791|8788|8794)']
reason: "Talk's own voice engines (server, systemd unit, stress scripts); to be placed behind Mind per Mind's audit plan. NOT compute-door: a real bypass being fixed (Hub 10-02)."
exemption: owner-approved
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windytalk
paths: ["scripts/run-client.sh"]
matches: [':(8791|8788|8794)']
reason: "Dev client launcher: 127.0.0.1:8788 is an ssh -L tunnel to the dev engine ON VERON (not the user's machine), so engine-side; dev-only, not shipped. To be placed behind Mind per Mind's audit plan (Windy Talk, Hub 10-02)."
exemption: owner-approved
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31
- repo: windytalk
paths: ["apps/desktop/renderer/index.html"]
matches: [':(8791|8788|8794)']
reason: "Display-only fallback label in the settings panel (cfg.engineUrl || default); connects to nothing. The shipped client defaults to the public engine on Veron, never local inference (Windy Talk, Hub 10-02)."
exemption: owner-approved
approved_by: windy-hub
approved_on: 2026-10-02
expires: 2026-12-31

View File

@@ -57,3 +57,6 @@ allow:
- repo: windytalk - repo: windytalk
hashes: ["c4189d79"] hashes: ["c4189d79"]
reason: "apps/desktop/test/diagnostics.test.ts redaction fixture (hexSecret beside a fake sk-ant token); hash checked against the lockbox 10-01: not present." reason: "apps/desktop/test/diagnostics.test.ts redaction fixture (hexSecret beside a fake sk-ant token); hash checked against the lockbox 10-01: not present."
- repo: windy-agent
hashes: ["84e0c0ea"]
reason: "tests/test_log_redaction.py:56 Z.ai redaction fixture REPLACED by windy-agent #412 with a synthetic value; hash checked against the lockbox 10-01: not present."

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Windy Git nightly STATE backup (Postgres + Gitea config + repos -> encrypted restic in R2)
After=network-online.target docker.service
[Service]
Type=oneshot
WorkingDirectory=/srv/windygit/src
# R2 credentials come from the .env; the restic password from /etc/windygit/restic.pass
# (root 600; the same value lives in the lockbox as RESTIC_WINDYGIT_PASSWORD).
EnvironmentFile=/srv/windygit/src/.env
ExecStart=/bin/bash /srv/windygit/src/scripts/backup_state.sh
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=3h

View File

@@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=/usr/local/bin/windy-job windygit-state-backup 26h --expect "ok — state backed up" --owner 13 -- /bin/bash /srv/windygit/src/scripts/backup_state.sh

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Nightly Windy Git state backup
[Timer]
# 03:07 local, clear of the git-bundle backup at 04:17.
OnCalendar=*-*-* 03:07:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Nightly (Boss 10-01): runner-guard over the default branch of EVERY public repo in Grant's
# 5 GitHub accounts (runs on Veron: windy-git scripts/runner_guard.py report). Writes
# ~/windy-orchestra/RUNNER_GUARD.md (repo, file:line, rule; no file content), appends ONE
# BOARD line per NEW hit vs the last run, and prints "runner-guard sweep: N hit(s)" last, so
# the windy-job heartbeat (--expect "runner-guard sweep: 0 hit") goes red while any hit exists.
set -euo pipefail
page=~/windy-orchestra/RUNNER_GUARD.md
state=~/.local/state/runner-guard-sweep.txt
mkdir -p "$(dirname "$state")"
out=$(timeout 900 ssh -o BatchMode=yes -o ConnectTimeout=15 ts-veron \
'cd /srv/windygit/src && timeout 850 python3 scripts/runner_guard.py report' || true)
summary=$(grep '^# runner-guard sweep:' <<<"$out" || echo "# runner-guard sweep: ERROR (no summary)")
hits=$(grep -v '^#' <<<"$out" | grep . || true)
{
echo "# Runner guard: stranger-code paths to self-hosted runners ($(date -u '+%Y-%m-%d %H:%MZ'))"
echo "_Nightly; windy-git scripts/runner_guard.py. R1 pull_request_target · R2 fork PR on self-hosted without a same-repo/environment gate · R3 outsider events (issue_comment, workflow_run, ...) on self-hosted · R0 unparseable._"
echo; echo "${summary#\# }"; echo
echo "| repo | file:line | rule | fix |"; echo "|---|---|---|---|"
while IFS=$'\t' read -r repo loc rule msg; do [[ -n $repo ]] && echo "| $repo | $loc | $rule | $msg |"; done <<<"$hits"
} > "$page"
new=$(comm -13 <(sort -u "$state" 2>/dev/null || true) <(cut -f1-3 <<<"$hits" | sort -u))
cut -f1-3 <<<"$hits" | sort -u > "$state"
if [[ -n "$new" ]]; then
n=$(grep -c . <<<"$new")
echo "$(date -u +%Y-%m-%dT%H:%MZ) Windy Git: 🚨 runner-guard: $n NEW stranger-code path(s) to a self-hosted runner in public repos; see ~/windy-orchestra/RUNNER_GUARD.md" >> ~/windy-orchestra/BOARD.md
fi
echo "${summary#\# }"

View File

@@ -0,0 +1,7 @@
[Unit]
Description=Nightly runner-guard sweep of every PUBLIC repo's workflows (Windy Git lane)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/windy-job windy-runner-guard-sweep 26h --expect "runner-guard sweep: 0 hit" --owner 13 -- %h/bin/nightly-runner-guard-sweep.sh
TimeoutStartSec=1200

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Nightly runner-guard sweep
[Timer]
OnCalendar=*-*-* 09:40:00 UTC
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -32,7 +32,7 @@ services:
gitea: gitea:
# G2.1 — PIN AN EXACT VERSION. Never `latest`. Record it in SUBSTRATE.md. # G2.1 — PIN AN EXACT VERSION. Never `latest`. Record it in SUBSTRATE.md.
image: docker.io/gitea/gitea:1.24.6 image: docker.io/gitea/gitea:1.24.7
environment: environment:
GITEA__database__DB_TYPE: postgres GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432 GITEA__database__HOST: db:5432

31
docs/CUTOVER-PRIMARY.md Normal file
View File

@@ -0,0 +1,31 @@
# Checklist: making Windy Git the PRIMARY home of one repo (after launch)
Status 2026-10-01: DRAFT, nothing flipped. GitHub stays the source of truth until Grant says otherwise.
First candidate: `windy-git` itself (public, low blast radius). GitHub becomes the free off-site push-mirror.
## Preconditions (all must be true)
- [x] Encrypted state backup (Postgres + Gitea config + repos) nightly, restore drill passed cross-host (10-01).
- [x] Gitea on a patched release (1.24.7); upgrade path = snapshot first (docs/RESTORE-DRILL.md).
- [ ] Heartbeat `windygit-state-backup` in heartbeats-veron expected list (asked Cloud/Super Admin 10-01).
- [ ] A missed/failed backup PAGES (heartbeat 26h), tested once by skipping a night in a drill.
- [ ] Boss/Grant capacity call on Veron (dedicated non-SMR volume for DB + git storage; root disk < 80%).
- [ ] Post-launch freeze lifted (Hub). No cutover during store review or a launch window.
## Cutover for ONE repo (reversible at every step)
1. Announce on BOARD; tell every consuming lane (no schema drift rule). Pause the sync for that repo only:
remove it from `REPOS` in `scripts/sync_from_github.sh` FIRST (the sync force-overwrites Windy Git).
2. Verify Windy Git main == GitHub main (sha equal), all branches/tags present, LFS (if any) in R2.
3. Add a PUSH-mirror on the Windy Git repo -> GitHub (deploy key or scoped token, write on that repo only,
interval 8h + on-commit), so GitHub keeps a current copy. Test with a throwaway branch.
4. Flip the lanes' remote: `origin` = Windy Git (SSH/HTTPS via Windy SSO token), `github` = secondary.
Lanes push to Windy Git only; the mirror carries it to GitHub.
5. CI keeps running here (no change); remove the `pr_status_bridge` mirror-PR duplication for that repo
(PRs are now native here; the bridge's GitHub status posting for it can stay for any open GitHub PRs).
6. Watch 3 days: mirror lag, backup includes the new writes, no push rejected, no lane still pushing to GitHub.
7. Rollback at any time: re-add the repo to `REPOS` (sync resumes GitHub->Windy Git, GitHub is intact via the
push-mirror) and point lanes' remote back. Nothing is destroyed in either direction.
## NOT in scope for a first cutover
Deploy workflows (stay disabled; deploys remain manual until a separate runner + scoped deploy keys exist),
credential repos (soul/anima/kit-army-config), windy-pro (importer refuses by name), opening the forge to
other members (Grant 09-23: registration closed; Hub 10-01: jit false, 4 conditions before any change).

33
docs/RESTORE-DRILL.md Normal file
View File

@@ -0,0 +1,33 @@
# Restoring Windy Git from the encrypted state backup
What is backed up (`scripts/backup_state.sh`, restic repo `s3:…/windy-git-backups/restic`, tag `windygit-state`):
both Postgres databases (`gitea`, `windygit`, custom-format dumps + globals), the whole Gitea data root
(`/srv/windygit/git`: config, jwt, attachments, avatars, templates AND the bare repositories),
`/srv/windygit/src/.env`, `deploy/runner/.env`, `/etc/cloudflared`, the windygit systemd drop-ins.
NOT in it: the restic password itself (lockbox `RESTIC_WINDYGIT_PASSWORD`) and the R2 access key
(scoped token `windy-git-r2-scoped`, lockbox). Never print either: use `lockbox-get KEY FILE`.
## Drill (any machine with restic + docker; proven on Veron 2026-10-01, counts identical)
export RESTIC_PASSWORD_FILE=<0600 file from lockbox-get RESTIC_WINDYGIT_PASSWORD>
export AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=… # from lockbox-get, into env, not echoed
export RESTIC_REPOSITORY=s3:https://<R2 account>.r2.cloudflarestorage.com/windy-git-backups/restic
restic snapshots --tag windygit-state
restic restore latest --tag windygit-state --target /var/tmp/wg-drill
docker run -d --name wg-drill-pg -e POSTGRES_PASSWORD=<random> -e POSTGRES_USER=drill postgres:16-alpine
for db in gitea windygit; do
docker exec wg-drill-pg psql -U drill -d postgres -c "create database $db"
docker exec -i wg-drill-pg pg_restore -U drill -d $db --no-owner --no-privileges \
< /var/tmp/wg-drill/var/backups/windygit-state/$db.dump
done
# compare row counts with live (or with the last known): repository, issue, pull_request, "user",
# external_login_user, access_token, action_run, action_run_job
docker rm -f wg-drill-pg; rm -rf /var/tmp/wg-drill
## Real disaster (Veron lost)
1. New Linux host with Docker, a Cloudflare tunnel connector, the repo (`git clone` from GitHub: windy-git).
2. `restic restore latest --tag windygit-state --target /` (puts /srv/windygit/git, the .env files, /etc/cloudflared back).
3. `docker compose -p windy-git up -d db`, then pg_restore both dumps into it (as above, into the real db names/owner from `.env`).
4. `docker compose -p windy-git up -d` + `deploy/runner` runners; re-register runners if the token changed.
5. Verify: `/api/healthz`, Windy SSO login, `git ls-remote`, one CI run. GitHub is still the source of truth for code,
so repo content can also be re-synced from there; the database is what only this backup holds.
Retention: 14 daily / 8 weekly / 6 monthly (prune on Sundays). Integrity: every run does `restic check --read-data-subset=2%`.

66
scripts/backup_state.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Nightly STATE backup: everything git bundles do NOT hold (SOTU 10-01: 625 issues/PRs, users,
# SSO links, CI history, settings lived on one unbacked-up host). Encrypted restic repo in R2.
# - Postgres: every database (custom-format dump, restore-listable) + globals
# - Gitea config/data (app.ini, jwt, attachments, avatars, templates) + the bare repositories
# - the deploy .env files, systemd drop-ins and the cloudflared tunnel config (needed to rebuild)
# The restic password lives in /etc/windygit/restic.pass (root 600) AND the lockbox
# (RESTIC_WINDYGIT_PASSWORD): a lost Veron must not lose the backups. NEVER echo env/values here.
# Restore: docs/RESTORE-DRILL.md. Bounded: every docker exec runs under `timeout` (a hung
# runc exec in the IO stall wedged the sync on 09-23).
set -euo pipefail
log() { echo "[backup_state $(date -u +%FT%TZ)] $*"; }
: "${R2_ACCOUNT_ID:?}" "${R2_ACCESS_KEY_ID:?}" "${R2_SECRET_ACCESS_KEY:?}"
PASSFILE="${RESTIC_PASSWORD_FILE:-/etc/windygit/restic.pass}"
[[ -s "$PASSFILE" ]] || { log "FATAL: $PASSFILE missing/empty: refusing to report a backup that did not happen"; exit 1; }
export RESTIC_PASSWORD_FILE="$PASSFILE"
export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY"
export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-s3:https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${R2_BUCKET_BACKUPS:-windy-git-backups}/restic}"
DB="${WG_DB_CONTAINER:-windy-git-db-1}"
STAGE="${WG_STAGE:-/var/backups/windygit-state}"
GIT_ROOT="${GIT_DATA_ROOT:-/srv/windygit/git}"
umask 077
mkdir -p "$STAGE"; chmod 700 "$STAGE"; rm -f "$STAGE"/*.dump "$STAGE"/globals.sql
# systemd gives units no $HOME, and restic wants a cache dir: pin one.
export RESTIC_CACHE_DIR="${RESTIC_CACHE_DIR:-/var/cache/windygit-restic}"; mkdir -p "$RESTIC_CACHE_DIR"
if ! err=$(restic cat config 2>&1 >/dev/null); then
# only a MISSING repo may be initialised; any other error (auth, network, wrong password) must stop here
if grep -qiE "does not exist|is there a repository|unable to open config file" <<<"$err"; then
log "initialising restic repo"; restic init >/dev/null
else
log "FATAL: restic cannot open the repository: $(head -c 300 <<<"$err" | tr '\n' ' ')"; exit 1
fi
fi
PGU=$(timeout 30 docker exec "$DB" printenv POSTGRES_USER)
[[ -n "$PGU" ]] || { log "FATAL: no POSTGRES_USER in $DB"; exit 1; }
dbs=$(timeout 60 docker exec "$DB" psql -U "$PGU" -Atc "select datname from pg_database where not datistemplate and datname<>'postgres' order by 1")
n=0
for d in $dbs; do
timeout 600 docker exec "$DB" pg_dump -U "$PGU" -Fc "$d" > "$STAGE/$d.dump"
# a dump that cannot be listed is not a backup
timeout 120 docker exec -i "$DB" pg_restore -l < "$STAGE/$d.dump" >/dev/null
[[ $(stat -c%s "$STAGE/$d.dump") -gt 1000 ]] || { log "FATAL: $d dump suspiciously small"; exit 1; }
n=$((n+1)); log "dumped $d ($(stat -c%s "$STAGE/$d.dump") bytes)"
done
[[ $n -ge 1 ]] || { log "FATAL: no databases dumped"; exit 1; }
timeout 120 docker exec "$DB" pg_dumpall -U "$PGU" --globals-only > "$STAGE/globals.sql"
paths=("$STAGE" "$GIT_ROOT" /srv/windygit/src/.env /srv/windygit/src/deploy/runner/.env /etc/cloudflared)
for p in /etc/systemd/system/windygit-*.service.d /etc/windygit; do [[ -e $p ]] && paths+=("$p"); done
# restic.pass itself is excluded: the password never rides in its own backup
snap=$(restic backup --tag windygit-state --host windygit-veron --quiet --json \
--exclude "$GIT_ROOT/gitea/log" --exclude "$GIT_ROOT/gitea/queues" --exclude "$GIT_ROOT/gitea/tmp" \
--exclude "$GIT_ROOT/gitea/indexers" --exclude "$GIT_ROOT/gitea/actions_log" --exclude /etc/windygit/restic.pass \
"${paths[@]}" | python3 -c 'import sys,json
for l in sys.stdin:
d=json.loads(l)
if d.get("message_type")=="summary": print(d["snapshot_id"][:8])')
[[ -n "$snap" ]] || { log "FATAL: restic produced no snapshot"; exit 1; }
rm -f "$STAGE"/*.dump "$STAGE"/globals.sql
restic check --read-data-subset=2% --quiet >/dev/null || { log "FATAL: restic check failed"; exit 1; }
if [[ $(date +%u) == 7 ]]; then
restic forget --tag windygit-state --keep-daily 14 --keep-weekly 8 --keep-monthly 6 --prune --quiet >/dev/null
fi
echo "ok — state backed up ($n dbs, snapshot $snap)"

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" bare = cg.WORK / f"{repo}.git"
if not bare.is_dir() or not cg.fetched(bare, sha): # pushed after the fetch: next cycle if not bare.is_dir() or not cg.fetched(bare, sha): # pushed after the fetch: next cycle
return None 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] 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 fp = cg._fingerprint(allow) + ":" + rules # hashlib, not hash(): hash() is per-process random
kw = dict(line_fn=scan_line, path_ok=path_ok) 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: def report(repos: list[str]) -> int:
allow = cg.load_allow(ALLOW_FILE) allow = cg.load_allow(ALLOW_FILE, strict=False)
total = 0 total = 0
for repo in repos: for repo in repos:
bare = cg.WORK / f"{repo}.git" bare = cg.WORK / f"{repo}.git"

View File

@@ -31,6 +31,7 @@ import re
import subprocess import subprocess
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, timedelta
from pathlib import Path from pathlib import Path
import yaml import yaml
@@ -49,6 +50,17 @@ HOSTS = [
"api.cohere.com", "api.fireworks.ai", "api.replicate.com", "api.cohere.com", "api.fireworks.ai", "api.replicate.com",
"api-inference.huggingface.co", "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 = [ KEYS = [
"ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN",
"OPENAI_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY", "OPENAI_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY",
@@ -61,8 +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" 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_AI = re.compile(r'^\s*\[ai\]\s*$|^\s*"ai"\s*:\s*\{')
RULES: list[tuple[str, re.Pattern]] = [ RULES: list[tuple[str, re.Pattern]] = [
("provider host", re.compile("|".join(re.escape(h) for h in HOSTS))), ("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)(?![0-9])")),
("workers ai binding", WRANGLER_AI),
("provider key", re.compile(r"\b(?:" + "|".join(KEYS) + r")\b")), ("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"^\s*(?:from|import)\s+(?:{PY_SDKS})(?:\s|\.|$|,)")),
("provider SDK", re.compile(rf"""(?:from\s+|require\(\s*|import\(\s*)['"](?:{JS_SDKS})(?:/[^'"]*)?['"]""")), ("provider SDK", re.compile(rf"""(?:from\s+|require\(\s*|import\(\s*)['"](?:{JS_SDKS})(?:/[^'"]*)?['"]""")),
@@ -70,6 +97,12 @@ RULES: list[tuple[str, re.Pattern]] = [
("provider SDK dep", re.compile(rf'''^\s*"(?:{JS_SDKS})"\s*:''')), ("provider SDK dep", re.compile(rf'''^\s*"(?:{JS_SDKS})"\s*:''')),
("provider SDK dep", re.compile(rf'''^\s*["']?(?:{PY_SDKS.replace(chr(92) + ".", "-")})(?:\[[^\]]*\])?\s*(?:[<>=~!]=?|["',]|$)''')), ("provider SDK dep", re.compile(rf'''^\s*["']?(?:{PY_SDKS.replace(chr(92) + ".", "-")})(?:\[[^\]]*\])?\s*(?:[<>=~!]=?|["',]|$)''')),
] ]
# 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)$") DEP_FILES = re.compile(r"(^|/)(package\.json|requirements[^/]*\.txt|pyproject\.toml|setup\.cfg|Pipfile)$")
# Never scanned: tests, docs, lockfiles, vendored/built code, CI config. # Never scanned: tests, docs, lockfiles, vendored/built code, CI config.
@@ -89,13 +122,47 @@ class Finding:
match: str 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 {} data = yaml.safe_load(path.read_text()) or {}
entries = data.get("allow") 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()): 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}")
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: def allowed(repo: str, path: str, allow: list[dict], text: str | None = None) -> bool:
@@ -126,6 +193,10 @@ def scan_line(path: str, text: str) -> list[tuple[str, str]]:
for kind, rx in RULES: for kind, rx in RULES:
if kind == "provider SDK dep" and not DEP_FILES.search(path): if kind == "provider SDK dep" and not DEP_FILES.search(path):
continue 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) m = rx.search(text)
if m: if m:
hits.append((kind, m.group(0).strip()[:60])) hits.append((kind, m.group(0).strip()[:60]))
@@ -150,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. # Other guards (ci_hygiene) reuse this walker with their own line rules.
line_fn = line_fn or scan_line line_fn = line_fn or scan_line
path_ok = path_ok or _default_path_ok 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", "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", "%3[aA]87", r"^\s*\[ai\]", '"ai"'])
try: try:
out = _git(bare, "grep", "-nIE", "-e", pre, sha, "--", ".") out = _git(bare, "grep", "-nIE", "-e", pre, sha, "--", ".")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@@ -209,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) -------------- # ---- cache: a tree scan runs once per (repo, sha, rules+allow) --------------
def _fingerprint(allow: list[dict]) -> str: def _fingerprint(allow: list[dict]) -> str:
return hashlib.sha256( 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] ).hexdigest()[:16]
@@ -253,7 +327,8 @@ def check(repo: str, sha: str, default_branch: str, is_default_head: bool) -> li
allow = load_allow() allow = load_allow()
fp = _fingerprint(allow) fp = _fingerprint(allow)
if is_default_head: if is_default_head:
return cached_scan(f"tree:{repo}:{sha}:{fp}", lambda: scan_tree(repo, bare, sha, allow)) return cached_scan(f"tree:{repo}:{sha}:{fp}",
lambda: [f for f in scan_tree(repo, bare, sha, allow) if f.kind not in WARN_ONLY_KINDS])
return cached_scan( return cached_scan(
f"pr:{repo}:{sha}:{fp}", f"pr:{repo}:{sha}:{fp}",
lambda: scan_added(repo, bare, f"refs/heads/{default_branch}", sha, allow), lambda: scan_added(repo, bare, f"refs/heads/{default_branch}", sha, allow),
@@ -268,6 +343,18 @@ def status_for(findings: list[Finding], whole_tree: bool,
Grant-owned code (ci/grant-owned.yml): always WARN, never red (orchestrator Grant-owned code (ci/grant-owned.yml): always WARN, never red (orchestrator
09-23: his desktop work is never blocked by us).""" 09-23: his desktop work is never blocked by us)."""
scope = "in tree" if whole_tree else "added" scope = "in tree" if whole_tree else "added"
soft = [f for f in findings if f.kind in WARN_ONLY_KINDS]
findings = [f for f in findings if f.kind not in WARN_ONLY_KINDS]
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: if not findings and grant:
g, n = grant[0], len(grant) g, n = grant[0], len(grant)
desc = (f"⚠ WARN (Grant-owned, not blocking): {n} direct AI-provider use{'s' if n > 1 else ''} " desc = (f"⚠ WARN (Grant-owned, not blocking): {n} direct AI-provider use{'s' if n > 1 else ''} "
@@ -286,6 +373,12 @@ 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:
print(f"## EXPIRED exemption (no longer excuses anything): {e['repo']} {e['paths']} "
f"[{e['exemption']}] expired {e['expires']}")
total = 0 total = 0
for repo in repos: for repo in repos:
bare = WORK / f"{repo}.git" bare = WORK / f"{repo}.git"

23
scripts/drill_cross_host.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Cross-host restore drill on Windy 0: ONLY lockbox + R2, nothing from Veron. Values never printed.
# Usage: bash drill_cross_host.sh (needs lockbox keys RESTIC_WINDYGIT_PASSWORD, WINDYGIT_R2_ACCESS_KEY_ID, WINDYGIT_R2_SECRET_ACCESS_KEY, WINDYGIT_R2_ENDPOINT)
set -euo pipefail
umask 077; W=$(mktemp -d ~/.cache/wg-xdrill.XXXXXX)
trap 'docker rm -f wg-xdrill-pg >/dev/null 2>&1 || true; rm -rf "$W"' EXIT
lockbox-get RESTIC_WINDYGIT_PASSWORD "$W/pw" >/dev/null
lockbox-get WINDYGIT_R2_ACCESS_KEY_ID "$W/ak" >/dev/null; lockbox-get WINDYGIT_R2_SECRET_ACCESS_KEY "$W/sk" >/dev/null; lockbox-get WINDYGIT_R2_ENDPOINT "$W/ep" >/dev/null
export RESTIC_PASSWORD_FILE="$W/pw" AWS_ACCESS_KEY_ID="$(cat "$W/ak")" AWS_SECRET_ACCESS_KEY="$(cat "$W/sk")"
export RESTIC_REPOSITORY="s3:$(cat "$W/ep")/windy-git-backups/restic"
restic snapshots --tag windygit-state --compact | tail -3
restic restore latest --tag windygit-state --target "$W/r" --include /var/backups/windygit-state --include /srv/windygit/git/gitea/conf --quiet
ls -l "$W/r/var/backups/windygit-state" | awk 'NR>1{print $5, $NF}'
docker run -d --name wg-xdrill-pg -e POSTGRES_PASSWORD="$(python3 -c 'import secrets;print(secrets.token_hex(12))')" -e POSTGRES_USER=drill postgres:16-alpine >/dev/null
for i in $(seq 1 30); do docker exec wg-xdrill-pg pg_isready -U drill >/dev/null 2>&1 && break; sleep 2; done
for db in gitea windygit; do
docker exec wg-xdrill-pg psql -U drill -d postgres -qc "create database $db"
docker exec -i wg-xdrill-pg pg_restore -U drill -d $db --no-owner --no-privileges < "$W/r/var/backups/windygit-state/$db.dump" 2>&1 | grep -v "already exists" | head -2 || true
done
for t in repository issue pull_request '"user"' external_login_user action_run action_run_job; do
echo "$t restored=$(docker exec wg-xdrill-pg psql -U drill -d gitea -Atc "select count(*) from $t")"
done
echo "cross-host drill OK (cleaned up)"

153
scripts/env_names.py Normal file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""env-names: list environment variable NAMES only (Boss ruling 10-01, house rule 10).
env-names <docker container | systemd unit | env file> [--host H] [--sudo] [--hash]
env-names A --compare B [--host H] [--host2 H2]
Prints NAME, set|empty, and value LENGTH. Never a value or fragment. --hash adds sha256[:8]
(compare two places for equality; a hash of a weak value can be guessed, so use it for
real secrets only). --compare prints SAME / DIFFERENT / only-in-A / only-in-B per name
(equality by full-value hash, nothing else shown). Values live in memory only.
Targets: an existing file (dotenv style) | a docker container name | a systemd unit
(Environment= + EnvironmentFile=; --sudo to read root-only files). --host runs the docker /
systemctl / file read over ssh (alias from ~/.ssh/config).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shlex
import subprocess
import sys
KV = ("=",)
def run(cmd: list[str], host: str | None, sudo: bool = False) -> tuple[int, str]:
if sudo:
cmd = ["sudo", "-n", *cmd]
if host:
cmd = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", host, shlex.join(cmd)]
r = subprocess.run(cmd, capture_output=True, text=True, errors="ignore", timeout=60)
return r.returncode, r.stdout
def parse_dotenv(text: str) -> dict[str, str]:
out: dict[str, str] = {}
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
if line.startswith("export "):
line = line[7:].lstrip()
k, v = line.split("=", 1)
k = k.strip()
v = v.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1]
if k.replace("_", "").isalnum() and not k[0].isdigit():
out[k] = v
return out
def from_file(path: str, host: str | None, sudo: bool) -> dict[str, str] | None:
if host or sudo:
rc, out = run(["cat", path], host, sudo)
return parse_dotenv(out) if rc == 0 else None
try:
with open(path, errors="ignore") as fh:
return parse_dotenv(fh.read())
except OSError:
return None
def from_docker(name: str, host: str | None, sudo: bool) -> dict[str, str] | None:
rc, out = run(["docker", "inspect", "-f", "{{json .Config.Env}}", name], host, sudo)
if rc != 0 or not out.strip():
return None
try:
items = json.loads(out)
except ValueError:
return None
return {k: v for k, _, v in (i.partition("=") for i in (items or []))}
def from_systemd(unit: str, host: str | None, sudo: bool) -> dict[str, str] | None:
rc, out = run(["systemctl", "show", unit, "-p", "Environment", "-p", "EnvironmentFiles"], host)
if rc != 0 or "LoadState=not-found" in out:
return None
env: dict[str, str] = {}
files: list[str] = []
for line in out.splitlines():
if line.startswith("Environment="):
for tok in shlex.split(line[len("Environment="):]):
k, _, v = tok.partition("=")
env[k] = v
elif line.startswith("EnvironmentFiles="):
f = line[len("EnvironmentFiles="):].split(" (")[0].strip().lstrip("-")
if f:
files.append(f)
for f in files: # later files override earlier, like systemd
d = from_file(f, host, sudo)
if d is None:
print(f"# note: EnvironmentFile {f} unreadable (try --sudo)", file=sys.stderr)
else:
env.update(d)
return env
def load(target: str, host: str | None, sudo: bool) -> dict[str, str] | None:
if (not host and os.path.isfile(target)) or target.startswith(("/", "./", "~")):
return from_file(os.path.expanduser(target), host, sudo)
if target.endswith((".service", ".timer", ".socket")):
return from_systemd(target, host, sudo)
return from_docker(target, host, sudo) or from_systemd(target, host, sudo)
def sh(v: str) -> str:
return hashlib.sha256(v.encode()).hexdigest()
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="env-names", description="env var NAMES only")
ap.add_argument("target")
ap.add_argument("--host")
ap.add_argument("--host2", help="ssh host for the --compare target")
ap.add_argument("--sudo", action="store_true")
ap.add_argument("--hash", action="store_true", help="add sha256[:8] per variable")
ap.add_argument("--compare", metavar="TARGET2")
a = ap.parse_args(argv)
env = load(a.target, a.host, a.sudo)
if env is None:
print(f"error: could not read {a.target!r} (file, docker container or systemd unit)")
return 2
if a.compare:
env2 = load(a.compare, a.host2 or a.host, a.sudo)
if env2 is None:
print(f"error: could not read {a.compare!r}")
return 2
for k in sorted(set(env) | set(env2)):
if k not in env2:
print(f"{k:<40} only-in-A")
elif k not in env:
print(f"{k:<40} only-in-B")
else:
print(f"{k:<40} {'SAME' if sh(env[k]) == sh(env2[k]) else 'DIFFERENT'}"
f" (len {len(env[k])} vs {len(env2[k])})")
return 0
for k in sorted(env):
v = env[k]
extra = f" sha256:{sh(v)[:8]}" if a.hash and v else ""
print(f"{k:<40} {'set ' if v else 'empty'} len={len(v)}{extra}")
print(f"# {len(env)} variable(s); values never printed")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e: # never a traceback
print(f"error: {type(e).__name__}")
sys.exit(2)

14
scripts/install_secret_tools.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Install the shared hash-only secret tools on THIS machine (Windy 0): secret-scan + env-names.
# Source of truth is this repo (scripts/); re-run after a pull to update.
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" "$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, 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)

141
scripts/lockbox_put.py Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""lockbox-put: add ONE secret to the lockbox by PR, without anyone reading, printing or
grepping the lockbox (Boss rule 10-01; Windy Hub ruling).
lockbox-put KEY FILE [--lane NAME] [--note TEXT]
KEY exact name, ^[A-Z][A-Z0-9_]{2,63}$ . FILE a 0600 file you own (not a symlink) whose
content is the value (one line). Appends ONE line `- **`KEY`**: `<value>`` (the format
lockbox-get reads) under a new heading at the END of ACCESS_LOCKBOX.md in a fresh temp clone,
on a new branch, and opens a kit-army-config PR. Append-only: the diff is verified to be one
file, additions only, before pushing. REFUSES if KEY already exists (a bool computed in
memory; no line, value or location is ever printed). Reviewers see the KEY NAME + lane only
if they look at the diff; the PR body never carries the value. Never echoes the value.
Env (tests): LOCKBOX_PUT_REPO=<clone url/path>, LOCKBOX_PUT_NO_PR=1.
"""
from __future__ import annotations
import argparse
import datetime as dt
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = os.environ.get("LOCKBOX_PUT_REPO", "https://github.com/sneakyfree/kit-army-config.git")
SLUG = "sneakyfree/kit-army-config"
KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]{2,63}$")
VAL_RE = re.compile(r"^[A-Za-z0-9._~+/=:@%,-]{8,512}$") # no backtick, quote, space or newline
def die(msg: str, code: int = 2):
print(f"lockbox-put: {msg}")
sys.exit(code)
def git(cwd: str, *a: str, quiet=True) -> subprocess.CompletedProcess:
# stderr is dropped: git/gh errors can echo URLs; stdout only when we need it.
return subprocess.run(["git", "-C", cwd, *a], capture_output=True, text=True, errors="ignore")
def key_exists(clone: str, key: str) -> bool:
"""True if KEY is already defined anywhere lockbox-get reads. Bool only, nothing printed."""
pat_env = re.compile(r"^" + re.escape(key) + r"=")
pat_md = re.compile(r"^\s*[-*]?\s*\*\*`" + re.escape(key) + r"`\*\*\s*:")
paths = [Path(clone, "ACCESS_LOCKBOX.md")] + [
Path(dp, f) for dp, _d, fs in os.walk(Path(clone, "secrets")) for f in fs if f.endswith(".env")]
for p in paths:
try:
with open(p, errors="ignore") as fh:
for line in fh:
if pat_env.match(line) or pat_md.match(line):
return True
except OSError:
continue
return False
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="lockbox-put")
ap.add_argument("key")
ap.add_argument("file")
ap.add_argument("--lane", default=os.environ.get("LOCKBOX_LANE", "a lane"))
ap.add_argument("--note", default="")
a = ap.parse_args(argv)
if not KEY_RE.match(a.key):
die("KEY must match ^[A-Z][A-Z0-9_]{2,63}$")
try:
st = os.lstat(a.file)
except OSError:
die("FILE not found")
if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode):
die("FILE must be a regular file (not a symlink)")
if st.st_uid != os.getuid() or (st.st_mode & 0o077):
die("FILE must be owned by you and mode 0600")
with open(a.file) as fh:
value = fh.read().strip()
if not VAL_RE.match(value):
die("value must be one line of 8-512 chars from [A-Za-z0-9._~+/=:@%,-] (no spaces, quotes, backticks)")
note = re.sub(r"[`\n\r]", " ", a.note)[:160]
lane = re.sub(r"[^A-Za-z0-9 ._-]", "", a.lane)[:40]
tmp = tempfile.mkdtemp(prefix="lockbox-put-", dir=str(Path.home() / ".cache") if (Path.home() / ".cache").is_dir() else None)
os.chmod(tmp, 0o700)
clone = os.path.join(tmp, "k")
try:
if subprocess.run(["git", "clone", "-q", "--depth", "1", REPO, clone],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
die("clone failed (details withheld: URLs can carry tokens)")
if key_exists(clone, a.key):
die(f"{a.key} already exists: refusing to overwrite (append-only; pick a new KEY)", 3)
lb = Path(clone, "ACCESS_LOCKBOX.md")
if not lb.is_file():
die("ACCESS_LOCKBOX.md not found in the repo")
today = dt.date.today().isoformat()
stamp = dt.datetime.now(dt.UTC).strftime("%Y%m%d%H%M")
branch = f"lockbox-put/{a.key.lower()}-{stamp}"
with open(lb, "a") as fh:
fh.write(f"\n## 🗝️ {a.key} (added {today} by {lane} via lockbox-put)\n")
fh.write(f"- **`{a.key}`**: `{value}`\n")
if note:
fh.write(f"- **Note:** {note}\n")
git(clone, "checkout", "-q", "-b", branch)
git(clone, "add", "ACCESS_LOCKBOX.md")
ns = git(clone, "diff", "--cached", "--numstat").stdout.split()
# numstat: <added> <deleted> <path>; exactly one file, no deletions
if len(ns) != 3 or ns[1] != "0" or ns[2] != "ACCESS_LOCKBOX.md":
die("diff is not a pure append to ACCESS_LOCKBOX.md: aborting, nothing pushed")
msg = f"lockbox: add {a.key} (via lockbox-put, {lane})\n\nCo-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>"
if git(clone, "-c", "user.name=lockbox-put", "-c", "user.email=lockbox-put@windy.invalid",
"commit", "-q", "-m", msg).returncode != 0:
die("commit failed")
if git(clone, "push", "-q", "origin", branch).returncode != 0:
die("push failed (details withheld)")
if os.environ.get("LOCKBOX_PUT_NO_PR"):
print(f"ok: pushed branch {branch} ({a.key}); PR skipped")
return 0
body = (f"Adds exactly one key: `{a.key}` (by {lane}). Append-only, one file, no deletions "
f"(verified before push). Review by KEY NAME only; do not paste the value anywhere.\n\n"
f"{note}\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)")
r = subprocess.run(["gh", "pr", "create", "-R", SLUG, "--head", branch, "--base", "main",
"--title", f"lockbox: add {a.key} ({lane})", "--body", body],
capture_output=True, text=True)
if r.returncode != 0:
die("branch pushed but `gh pr create` failed; open the PR for the branch by hand")
print(f"ok: {a.key} added via PR {r.stdout.strip().splitlines()[-1]}")
return 0
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
try:
sys.exit(main())
except SystemExit:
raise
except Exception as e: # never a traceback: it could carry data
print(f"lockbox-put: error: {type(e).__name__}")
sys.exit(2)

View File

@@ -54,7 +54,7 @@ REPOS = os.environ.get(
" windy-drops windy-code-web windy-code windy-traveler windy-registry eternitas" " windy-drops windy-code-web windy-code windy-traveler windy-registry eternitas"
" windy-translate windytranslate-site windytraveler-site windy-hand" " windy-translate windytranslate-site windytraveler-site windy-hand"
" windy-cloud-sites windy-cloud-domains windy-cloud-vps windytalk windy-pro windy-mind" " windy-cloud-sites windy-cloud-domains windy-cloud-vps windytalk windy-pro windy-mind"
" windy-inbox windy-text windy-call windy-cell", " windy-inbox windy-text windy-call windy-cell windy-hand-site windy-calendar-site",
).split() ).split()
# Gitea run status -> GitHub status state. `skipped` is deliberately absent: a # Gitea run status -> GitHub status state. `skipped` is deliberately absent: a

209
scripts/runner_guard.py Normal file
View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""Runner guard: no workflow may let a STRANGER's code reach a self-hosted runner (Boss 10-01).
Our self-hosted GitHub runners run on Veron as `github-runner`, which is in the docker group
(= root on Veron). Until ephemeral containerised runners exist (after launch), the cheap
guard is to refuse the workflow shapes that hand a self-hosted runner to outsiders:
R1 pull_request_target : runs with secrets/write token in the BASE repo context
R2 pull_request on self-hosted : fork PRs run their own code, unless the job is gated to
same-repo heads (`if:` on head.repo.full_name == github.repository
or head.repo.fork == false) or an `environment:`
R3 issue_comment / workflow_run / issues / discussion* / pull_request_review* / fork / watch
: anyone can fire these; never on self-hosted without an environment gate
(push, tags, schedule, workflow_dispatch, repository_dispatch, workflow_call: writers only, fine)
A job counts as self-hosted when its runs-on names `self-hosted`, or is an expression we
can't resolve (conservative). Findings carry file:line, never file content beyond that.
python3 scripts/runner_guard.py lint FILE... # local files
python3 scripts/runner_guard.py report [--owners a,b] # default branch of every PUBLIC repo
python3 scripts/runner_guard.py pr [--owners a,b] [--post] # open PRs on public repos: changed workflows
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import subprocess
import sys
import yaml
OWNERS = ["sneakyfree", "VERONTECH", "Windstorm-Institute", "Windstorm-Labs", "Public-Streamer"]
CTX = "windy-git/runner-guard"
OUTSIDE = {"issue_comment", "workflow_run", "issues", "discussion", "discussion_comment",
"pull_request_review", "pull_request_review_comment", "fork", "watch"}
SAME_REPO_GATES = ("head.repo.full_name == github.repository", "github.repository == github.event.pull_request.head.repo.full_name",
"head.repo.fork == false", "!github.event.pull_request.head.repo.fork")
def _node_map(node):
"""{key: (value_node, line)} for a YAML mapping node."""
if not isinstance(node, yaml.MappingNode):
return {}
return {k.value: (v, k.start_mark.line + 1) for k, v in node.value if isinstance(k, yaml.ScalarNode)}
def _triggers(on_node) -> dict[str, int]:
"""{event: line}."""
if isinstance(on_node, yaml.ScalarNode):
return {on_node.value: on_node.start_mark.line + 1}
if isinstance(on_node, yaml.SequenceNode):
return {n.value: n.start_mark.line + 1 for n in on_node.value if isinstance(n, yaml.ScalarNode)}
return {k: line for k, (_v, line) in _node_map(on_node).items()}
def _self_hosted(runs_on) -> bool:
if runs_on is None:
return False
text = yaml.serialize(runs_on) if isinstance(runs_on, yaml.Node) else str(runs_on)
return "self-hosted" in text or "${{" in text
def lint_text(path: str, text: str) -> list[tuple[str, int, str, str]]:
"""[(path, line, rule, message)]. Unparseable YAML is a finding (it cannot be reviewed)."""
try:
root = yaml.compose(text)
except yaml.YAMLError as e:
line = getattr(getattr(e, "problem_mark", None), "line", 0) + 1
return [(path, line, "R0", "workflow YAML does not parse; cannot be checked")]
top = _node_map(root)
if "on" not in top or "jobs" not in top:
return []
trig = _triggers(top["on"][0])
jobs = _node_map(top["jobs"][0])
out = []
if "pull_request_target" in trig:
out.append((path, trig["pull_request_target"], "R1",
"pull_request_target runs fork code with base-repo secrets; not allowed"))
for name, (jnode, jline) in jobs.items():
j = _node_map(jnode)
ro = j.get("runs-on", (None, jline))
if not _self_hosted(ro[0]):
continue
has_env = "environment" in j
cond = j["if"][0].value if "if" in j and isinstance(j["if"][0], yaml.ScalarNode) else ""
same_repo = any(g in cond.replace(" ", " ") for g in SAME_REPO_GATES)
if "pull_request" in trig and not (has_env or same_repo):
out.append((path, ro[1], "R2", f"job '{name}' runs fork PR code on a self-hosted runner "
"(gate it: if: github.event.pull_request.head.repo.full_name == github.repository, or an environment)"))
for ev in sorted(OUTSIDE & trig.keys()):
if not has_env:
out.append((path, trig[ev], "R3", f"'{ev}' can be fired by anyone and job '{name}' is self-hosted "
"without an environment gate"))
return out
# ---------------------------------------------------------------- GitHub side
def gh(*args: str, check=True) -> str:
r = subprocess.run(["gh", "api", *args], capture_output=True, text=True, timeout=60)
if check and r.returncode != 0:
raise RuntimeError(f"gh api {args[0]} failed")
return r.stdout
def public_repos(owners) -> list[tuple[str, str]]:
out = []
for o in owners:
txt = gh(f"users/{o}/repos?per_page=100&type=owner", "--paginate",
"--jq", '.[]|select(.private==false and .archived==false)|.full_name+" "+.default_branch', check=False)
out += [tuple(row.split()) for row in txt.splitlines() if row.strip()]
return out
def workflows_at(full: str, ref: str) -> list[tuple[str, str]]:
txt = gh(f"repos/{full}/contents/.github/workflows?ref={ref}", "--jq",
'.[]|select(.type=="file")|.path', check=False)
files = [p for p in txt.splitlines() if p.endswith((".yml", ".yaml"))]
return [(p, file_at(full, p, ref)) for p in files]
def file_at(full: str, path: str, ref: str) -> str:
raw = gh(f"repos/{full}/contents/{path}?ref={ref}", "--jq", ".content", check=False).strip()
return base64.b64decode(raw).decode("utf-8", "replace") if raw else ""
def cmd_report(owners) -> int:
hits = 0
repos = public_repos(owners)
for full, branch in repos:
for path, text in workflows_at(full, branch):
for p, line, rule, msg in lint_text(path, text):
hits += 1
print(f"{full}\t{p}:{line}\t{rule}\t{msg}")
print(f"# runner-guard sweep: {hits} hit(s) in {len(repos)} public repos")
return 1 if hits else 0
STATE = os.environ.get("RUNNER_GUARD_STATE", "/var/lib/windy-git/runner-guard-posted.json")
def cmd_pr(owners, post: bool) -> int:
# Post each (repo, sha, state, description) ONCE: the sync runs every 5 min and GitHub caps
# statuses per sha+context at 1000.
try:
with open(STATE) as fh:
posted = set(json.load(fh))
except (OSError, ValueError):
posted = set()
seen = set()
for full, _branch in public_repos(owners):
prs = gh(f"repos/{full}/pulls?state=open&per_page=50", "--jq",
'.[]|(.number|tostring)+" "+.head.sha+" "+.head.repo.full_name', check=False)
for line in prs.splitlines():
num, sha, head_repo = line.split(" ", 2)
files = gh(f"repos/{full}/pulls/{num}/files?per_page=100", "--jq",
'.[]|select(.status!="removed")|.filename', check=False).split()
wf = [f for f in files if f.startswith(".github/workflows/") and f.endswith((".yml", ".yaml"))]
# a fork's own content is read from the head repo at the head sha
src = head_repo if head_repo and head_repo != "null" else full
found = [h for f in wf for h in lint_text(f, file_at(src, f, sha))]
if found:
p, ln, rule, msg = found[0]
state, desc = "failure", f"BLOCKED: {rule} {p}:{ln}: {msg}"[:140]
else:
state, desc = "success", ("OK: no workflow changes" if not wf else
"OK: no stranger-code path to a self-hosted runner")
key = f"{full}@{sha}:{state}:{desc}"
seen.add(key)
if key in posted:
continue
print(f"{full}#{num}@{sha[:7]} {state} {desc}")
if post:
gh(f"repos/{full}/statuses/{sha}", "-f", f"state={state}", "-f", f"context={CTX}",
"-f", f"description={desc}", check=False)
posted.add(key)
if post: # keep only keys for PRs still open, so the file never grows without bound
os.makedirs(os.path.dirname(STATE), exist_ok=True)
with open(STATE, "w") as fh:
json.dump(sorted(posted & seen), fh)
return 0
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="runner_guard")
sub = ap.add_subparsers(dest="cmd", required=True)
lint_p = sub.add_parser("lint")
lint_p.add_argument("files", nargs="+")
rep = sub.add_parser("report")
rep.add_argument("--owners", default=",".join(OWNERS))
prp = sub.add_parser("pr")
prp.add_argument("--owners", default="sneakyfree") # self-hosted runners exist only there
prp.add_argument("--post", action="store_true")
a = ap.parse_args(argv)
if a.cmd == "lint":
hits = []
for f in a.files:
with open(f, errors="replace") as fh:
hits += lint_text(f, fh.read())
for p_, ln, rule, msg in hits:
print(f"{p_}:{ln}\t{rule}\t{msg}")
return 1 if hits else 0
owners = a.owners.split(",")
return cmd_report(owners) if a.cmd == "report" else cmd_pr(owners, a.post)
if __name__ == "__main__":
sys.exit(main())

210
scripts/secret_scan.py Normal file
View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""secret-scan: hash-only secret finder. Boss ruling 10-01 after three lanes printed secrets
into their own transcripts while hunting secrets (house rule 10).
secret-scan <path> [--history] [--repo <git url or path>] [--no-lockbox]
Reports `file:line` (and the commit with --history), WHICH lockbox entry matched (the KEY
NAME only) or which secret SHAPE matched (twilio, zai, aws, ...), plus a sha256[:8] of the
token for allow-listing. It NEVER prints, logs or writes a value or any fragment of one
(no context line, no masking). The lockbox is loaded in memory only. stdout only.
Exit 0 = clean, 1 = findings, 2 = usage/error.
"""
from __future__ import annotations
import argparse
import hashlib
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import secret_shapes as ss # noqa: E402 (the SAME shapes as secret-guard)
HOME = Path.home()
LOCKBOX_PATHS = [p for p in os.environ.get("SECRET_SCAN_LOCKBOX_PATHS", "").split(":") if p] or [
str(HOME / "kit-army-config" / "secrets"), str(HOME / "kit-army-config" / "ACCESS_LOCKBOX.md")]
# Extra shapes that are scan-only (not in the blocking guard): label, regex.
EXTRA = [("zai key", re.compile(r"(?<![0-9a-f])[0-9a-f]{32}\.[A-Za-z0-9]{16}(?![A-Za-z0-9])"))]
SKIP_DIRS = {".git", "node_modules", "vendor", "third_party", "__pycache__", ".venv"}
MAX_BYTES = 5_000_000
RUN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_\-]{15,199}")
UUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
NAME = re.compile(r"[\s>*`|-]*([A-Za-z][A-Za-z0-9_]{2,60})\s*[=:|]")
def h16(t: str) -> str:
return hashlib.sha256(t.encode()).hexdigest()[:16]
def cands(line: str):
"""Token candidates: runs >=16 chars with a digit and a letter; git shas/uuids excluded."""
for chunk in re.split(r"[^A-Za-z0-9_\-.]+", line):
parts = [chunk, *chunk.split(".")] if "." in chunk else [chunk]
for p in parts:
for m in RUN.finditer(p):
t = m.group(0)
if not (re.search(r"\d", t) and re.search(r"[A-Za-z]", t)):
continue
if re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", t) or UUID.match(t.lower()):
continue
yield t
def load_lockbox() -> dict[str, set[str]]:
"""{hash16: {key-name labels}}; values never leave this dict."""
out: dict[str, set[str]] = {}
files: list[str] = []
for p in LOCKBOX_PATHS:
if os.path.isdir(p):
for root, _d, fs in os.walk(p):
files += [os.path.join(root, f) for f in fs]
elif os.path.isfile(p):
files.append(p)
for f in files:
try:
with open(f, errors="ignore") as fh:
for line in fh:
m = NAME.match(line)
label = m.group(1) if m else "?"
for t in cands(line):
out.setdefault(h16(t), set()).add(label)
except OSError:
continue
return out
def scan_line(line: str, lockbox: dict[str, set[str]]) -> list[tuple[str, str]]:
"""[(label, hash8)]: `shape:<kind>` and/or `lockbox:<NAMES>`. No value escapes."""
res: list[tuple[str, str]] = []
for kind, h in ss.find(line):
res.append((f"shape:{kind}", h))
for kind, rx in EXTRA:
for m in rx.finditer(line):
res.append((f"shape:{kind}", ss.h8(m.group(0))))
for t in cands(line):
k = h16(t)
if k in lockbox:
names = sorted(n for n in lockbox[k])
res.append(("lockbox:" + ",".join(names)[:70], k[:8]))
return sorted(set(res))
def is_text(path: Path) -> bool:
try:
with open(path, "rb") as fh:
return b"\0" not in fh.read(4096)
except OSError:
return False
def scan_tree(root: Path, lockbox):
files = [root] if root.is_file() else [
Path(dp) / f for dp, dn, fn in os.walk(root) for f in fn
if not set(Path(dp).relative_to(root).parts) & SKIP_DIRS]
for p in sorted(files):
try:
if p.stat().st_size > MAX_BYTES or not is_text(p):
continue
with open(p, errors="ignore") as fh:
for n, line in enumerate(fh, 1):
for label, h in scan_line(line, lockbox):
yield (str(p), n, None, label, h)
except OSError:
continue
def git(repo: str, *a: str) -> subprocess.Popen:
return subprocess.Popen(["git", "--git-dir", repo, *a], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, errors="ignore")
def scan_history(gitdir: str, lockbox):
"""Every ADDED line on every ref (incl. PR refs). Oldest commit per (hash, file, line)."""
seen: dict[tuple, str] = {}
p = git(gitdir, "log", "--all", "-p", "-U0", "--no-color", "--format=@@C %h", "-a")
commit = path = None
ln = 0
for row in p.stdout: # type: ignore[union-attr]
if row.startswith("@@C "):
commit = row[4:].strip()
elif row.startswith("+++ "):
path = row[6:].strip() if row.startswith("+++ b/") else None
elif row.startswith("@@ "):
m = re.search(r"\+(\d+)", row)
ln = int(m.group(1)) - 1 if m else 0
elif row.startswith("+") and path:
ln += 1
for label, h in scan_line(row[1:], lockbox):
seen[(label, h, path, ln)] = commit or "?"
p.wait()
for (label, h, path, ln), c in sorted(seen.items(), key=lambda x: (x[0][2], x[0][3])):
yield (path, ln, c, label, h)
def resolve_gitdir(p: Path) -> str | None:
for cand in (p / ".git", p):
if (cand / "HEAD").exists() and ((cand / "objects").exists()):
return str(cand)
return None
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="secret-scan", description=__doc__.split("\n\n")[1] if __doc__ else "")
ap.add_argument("path", nargs="?", help="file, directory, or git repo (with --history)")
ap.add_argument("--history", action="store_true", help="scan every added line in all git history")
ap.add_argument("--repo", help="git URL or path to scan (mirror-cloned to a temp dir, then deleted)")
ap.add_argument("--no-lockbox", action="store_true", help="shapes only")
a = ap.parse_args(argv)
if not (a.path or a.repo):
ap.print_usage()
return 2
lockbox = {} if a.no_lockbox else load_lockbox()
print(f"# secret-scan: {len(lockbox)} lockbox tokens in memory, values never printed", flush=True)
tmp = None
findings = 0
try:
if a.repo:
tmp = tempfile.mkdtemp(prefix="secret-scan-", dir=str(HOME / ".cache") if (HOME / ".cache").is_dir() else None)
os.chmod(tmp, 0o700)
target = os.path.join(tmp, "r.git")
rc = subprocess.run(["git", "clone", "-q", "--mirror", a.repo, target],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
if rc != 0:
print("error: clone failed (details withheld: URLs can carry tokens)")
return 2
a.history = True
gitdir = target
elif a.history:
gitdir = resolve_gitdir(Path(a.path))
if not gitdir:
print("error: --history needs a git repo path")
return 2
if a.history:
for path, ln, c, label, h in scan_history(gitdir, lockbox):
findings += 1
print(f"{path}:{ln} commit={c} {label} #{h}")
else:
for path, ln, _c, label, h in scan_tree(Path(a.path), lockbox):
findings += 1
print(f"{path}:{ln} {label} #{h}")
finally:
if tmp:
shutil.rmtree(tmp, ignore_errors=True)
print(f"# {findings} finding(s)")
return 1 if findings else 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)
except Exception as e: # never a traceback: it could carry data
print(f"error: {type(e).__name__}")
sys.exit(2)

View File

@@ -23,13 +23,14 @@ PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("twilio sid/api key", re.compile(r"\b(?:AC|SK)[0-9a-f]{32}\b")), ("twilio sid/api key", re.compile(r"\b(?:AC|SK)[0-9a-f]{32}\b")),
("32-hex secret assignment", re.compile( ("32-hex secret assignment", re.compile(
r"(?i)\b[a-z0-9_.-]*(?:token|secret|key|password)[a-z0-9_.-]*[\"']?\s*[:=]\s*[\"']?(?P<v>(?<![0-9a-f])[0-9a-f]{32}(?![0-9a-f]))")), r"(?i)\b[a-z0-9_.-]*(?:token|secret|key|password)[a-z0-9_.-]*[\"']?\s*[:=]\s*[\"']?(?P<v>(?<![0-9a-f])[0-9a-f]{32}(?![0-9a-f]))")),
("pypi token", re.compile(r"\bpypi-AgE[A-Za-z0-9_-]{50,}")),
("private key block", re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----")), ("private key block", re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----")),
] ]
# git grep -E (POSIX ERE) prefilter: cheap superset of PATTERNS. # git grep -E (POSIX ERE) prefilter: cheap superset of PATTERNS.
PREFILTER = ("[0-9]{8,10}:[A-Za-z0-9_-]{35}|gh[pousr]_[A-Za-z0-9]{36}|github_pat_|(AKIA|ASIA)[0-9A-Z]{16}" PREFILTER = ("[0-9]{8,10}:[A-Za-z0-9_-]{35}|gh[pousr]_[A-Za-z0-9]{36}|github_pat_|(AKIA|ASIA)[0-9A-Z]{16}"
"|xox[abprs]-|sk-ant-|sk-[A-Za-z0-9_-]{32}|sk-proj-|[rs]k_live_|AIza[0-9A-Za-z_-]{35}" "|xox[abprs]-|sk-ant-|sk-[A-Za-z0-9_-]{32}|sk-proj-|[rs]k_live_|AIza[0-9A-Za-z_-]{35}"
"|-----BEGIN [A-Z ]*PRIVATE KEY-----|(AC|SK)[0-9a-f]{32}|[0-9a-fA-F]{32}") "|-----BEGIN [A-Z ]*PRIVATE KEY-----|(AC|SK)[0-9a-f]{32}|[0-9a-fA-F]{32}|pypi-AgE")
def h8(value: str | bytes) -> str: def h8(value: str | bytes) -> str:

View File

@@ -38,7 +38,7 @@ FAILED=0
# Repos Windy Git tracks FROM GitHub. Remove a repo from this list at the moment # Repos Windy Git tracks FROM GitHub. Remove a repo from this list at the moment
# it flips to Windy-Git-first, or the sync will fight its authors and win. # it flips to Windy-Git-first, or the sync will fight its authors and win.
REPOS="${SYNC_REPOS:-windy-calendar windy-search windy-registry Windy-Clone WindyCloud windy-cloud-sites windy-mind eternitas windy-agent windy-git windy-chat windy-mail windy-connect windy-drops windy-code-web windy-code windy-traveler windy-translate windytranslate-site windytraveler-site windy-hand windy-cloud-domains windy-cloud-vps windytalk windy-pro windy-inbox windy-text windy-call windy-cell}" REPOS="${SYNC_REPOS:-windy-calendar windy-search windy-registry Windy-Clone WindyCloud windy-cloud-sites windy-mind eternitas windy-agent windy-git windy-chat windy-mail windy-connect windy-drops windy-code-web windy-code windy-traveler windy-translate windytranslate-site windytraveler-site windy-hand windy-cloud-domains windy-cloud-vps windytalk windy-pro windy-inbox windy-text windy-call windy-cell windy-hand-site windy-calendar-site}"
# Repos whose TAGS must not reach Windy Git. A tag push fires `on: push: tags` # Repos whose TAGS must not reach Windy Git. A tag push fires `on: push: tags`
# workflows; windy-pro's build-electron is a matrix over ubuntu/macos/windows- # workflows; windy-pro's build-electron is a matrix over ubuntu/macos/windows-
@@ -94,6 +94,11 @@ if ! python3 "$(dirname "$0")/pr_status_bridge.py"; then
log "FAILED pr status bridge"; FAILED=1 log "FAILED pr status bridge"; FAILED=1
fi fi
# Runner guard (Boss 10-01): PUBLIC sneakyfree repos have self-hosted GitHub runners on Veron.
# A PR that changes a workflow so a stranger's code could reach one gets a red
# windy-git/runner-guard status. Each status is posted once; never fails the sync.
timeout -k 10 120 python3 "$(dirname "$0")/runner_guard.py" pr --post || log "runner-guard failed or timed out (non-fatal)"
# CI telemetry -> admin.windyword.ai (shapes declared with Windy Telemetry 40). # CI telemetry -> admin.windyword.ai (shapes declared with Windy Telemetry 40).
# Sends nothing until WINDYGIT_TELEMETRY_TOKEN is set; never fails the sync. # Sends nothing until WINDYGIT_TELEMETRY_TOKEN is set; never fails the sync.
timeout -k 10 180 python3 "$(dirname "$0")/telemetry_emit.py" || log "telemetry emit failed or timed out (non-fatal)" timeout -k 10 180 python3 "$(dirname "$0")/telemetry_emit.py" || log "telemetry emit failed or timed out (non-fatal)"