1 Commits

Author SHA1 Message Date
Kit OC5
b438b6a053 ci: run dind under Sysbox, not privileged (rollback override kept)
dind was privileged: true, so a job that escaped into dind was root on
Veron 1, which is Grant's workstation. Under sysbox-runc (sysbox-ce 0.7.1,
installed 09-23 with no docker restart) dind root is an unprivileged host
uid. Smoke-tested standalone: nested containers, internet, a services-style
postgres on a private network and a python image all pass unprivileged.
Fresh volume dind-storage-sysbox; the old dind-storage stays for
docker-compose.privileged.yml, the one-command rollback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:41:42 -04:00
16 changed files with 13 additions and 1178 deletions

View File

@@ -1,92 +0,0 @@
"""The canary's login probe must end the session it opens (journey cleanup rule)."""
from __future__ import annotations
import importlib.util
import io
import sys
import urllib.error
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
_spec = importlib.util.spec_from_file_location("canary", ROOT / "scripts" / "canary.py")
canary = importlib.util.module_from_spec(_spec)
sys.modules["canary"] = canary # dataclasses resolve their module by name
_spec.loader.exec_module(canary)
class _Resp:
def __init__(self, status=200, body=b"{}"):
self.status, self._body = status, body
def read(self):
return self._body
def __enter__(self):
return self
def __exit__(self, *a):
return False
def _err(code):
return urllib.error.HTTPError(canary.LOGOUT_URL, code, "x", {}, io.BytesIO(b""))
def _script(monkeypatch, outcomes):
calls = []
def fake(req, timeout=None):
calls.append((req.get_method(), req.full_url, req.get_header("Authorization")))
o = outcomes.pop(0)
if isinstance(o, Exception):
raise o
return o
monkeypatch.setattr(canary.urllib.request, "urlopen", fake)
return calls
def test_logout_ends_the_session(monkeypatch):
calls = _script(monkeypatch, [_Resp(200)])
r = canary.logout("tok", sleep=lambda s: None)
assert r.status == "ok"
assert calls == [("POST", canary.LOGOUT_URL, "Bearer tok")]
def test_5xx_and_no_response_are_retried_then_succeed(monkeypatch):
calls = _script(monkeypatch, [_err(502), OSError("reset"), _Resp(200)])
assert canary.logout("tok", sleep=lambda s: None).status == "ok"
assert len(calls) == 3
def test_already_over_counts_as_done(monkeypatch):
_script(monkeypatch, [_err(401)])
assert canary.logout("tok", sleep=lambda s: None).status == "ok"
def test_other_4xx_fails_fast_and_honestly(monkeypatch):
calls = _script(monkeypatch, [_err(400)])
r = canary.logout("tok", sleep=lambda s: None)
assert r.status == "down" and r.detail.startswith("CLEANUP FAILED") and len(calls) == 1
def test_retries_are_bounded_and_reported(monkeypatch):
calls = _script(monkeypatch, [_err(503)] * 8)
r = canary.logout("tok", attempts=8, sleep=lambda s: None)
assert r.status == "down" and "CLEANUP FAILED after 8 tries" in r.detail and len(calls) == 8
def test_login_probe_logs_out_with_the_token_it_got(monkeypatch):
calls = _script(monkeypatch, [_Resp(200, b'{"token": "abc"}'), _Resp(200)])
c = canary.Check("identity.login", "https://account.windyword.ai/api/v1/auth/login", "x",
method="POST", body={"email": "e", "password": "p"},
after=canary._logout_after_login)
r = canary._probe(c)
assert r.status == "ok" and [f.status for f in r.followups] == ["ok"]
assert calls[1] == ("POST", canary.LOGOUT_URL, "Bearer abc")
def test_login_without_token_is_a_cleanup_failure_not_a_pass():
[f] = canary._logout_after_login(b"{}")
assert f.status == "down" and "CLEANUP FAILED" in f.detail

View File

@@ -1,184 +0,0 @@
"""Compute guard: Windy Mind is the only door to AI compute (warn-only today)."""
from __future__ import annotations
import importlib.util
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
_spec = importlib.util.spec_from_file_location("compute_guard", ROOT / "scripts" / "compute_guard.py")
cg = importlib.util.module_from_spec(_spec)
sys.modules["compute_guard"] = cg
_spec.loader.exec_module(cg)
ALLOW = cg.load_allow(ROOT / "ci" / "compute-guard-allow.yml")
@pytest.mark.parametrize(
"path, text, kind",
[
# audit #1 (windy-search, closed) and #2 (windy-chat, live): the shapes they had
("service/app/anthropic_client.py", 'URL = "https://api.anthropic.com/v1/messages"', "provider host"),
("service/app/config.py", 'token = os.environ["ANTHROPIC_OAUTH_TOKEN"]', "provider key"),
("services/agent-roster/lib/llm.js", "const url = 'https://api.groq.com/openai/v1/chat/completions'", "provider host"),
("docker-compose.yml", " GROQ_API_KEY: ${GROQ_API_KEY}", "provider key"),
# audit #3/#4 (windy-pro account-server)
("account-server/src/routes/transcription.ts", "const r = await fetch('https://api.openai.com/v1/audio/transcriptions'", "provider host"),
("account-server/src/config.ts", "openaiKey: process.env.OPENAI_API_KEY,", "provider key"),
# SDKs and deps
("app/llm.py", "from anthropic import Anthropic", "provider SDK"),
("app/llm.py", "import openai", "provider SDK"),
("app/llm.py", "import google.generativeai as genai", "provider SDK"),
("src/ai.ts", 'import Anthropic from "@anthropic-ai/sdk";', "provider SDK"),
("src/ai.js", "const Groq = require('groq-sdk')", "provider SDK"),
("package.json", ' "openai": "^4.52.0",', "provider SDK dep"),
("requirements.txt", "anthropic>=0.40", "provider SDK dep"),
("pyproject.toml", ' "google-generativeai>=0.8",', "provider SDK dep"),
],
)
def test_audit_shapes_are_flagged(path, text, kind):
assert kind in [k for k, _ in cg.scan_line(path, text)]
@pytest.mark.parametrize(
"path, text",
[
("app/mind.py", 'MIND = "https://mind.windyword.ai/v1/chat/completions"'), # the door itself
("app/models.py", "openai_compatible = True # Mind speaks the OpenAI wire format"),
("app/x.py", "from app.openai_shim import x"), # a local module, not the SDK
("package.json", ' "openai-types-lite": "1.0.0",'), # a different package
("README.txt", "set OPENAI_API_KEY"), # scanned-by-rule, excluded by SKIP separately
],
)
def test_near_misses_are_not_flagged(path, text):
if cg.SKIP.search(path):
return
assert cg.scan_line(path, text) == []
@pytest.mark.parametrize(
"path",
["tests/test_llm.py", "api/tests/x.py", "src/ai.test.ts", "web/foo.spec.js", "docs/setup.md",
"README.md", "package-lock.json", "uv.lock", "node_modules/openai/index.js", ".github/workflows/ci.yml",
"conftest.py", "app/llm_test.py"],
)
def test_tests_docs_lockfiles_vendored_ci_are_never_scanned(path):
assert cg.SKIP.search(path)
def test_allow_list_needs_a_reason_per_entry(tmp_path):
bad = tmp_path / "a.yml"
bad.write_text("allow:\n - repo: x\n paths: ['*']\n")
with pytest.raises(ValueError):
cg.load_allow(bad)
@pytest.mark.parametrize(
"repo, path, ok",
[
("windy-mind", "app/providers/anthropic.py", True),
("windy-agent", "agent/providers.py", True),
("windy-code", "extensions/windy-ai/src/aiProvider.ts", True),
("windy-code", "web/server/llm.ts", False), # BYOK is the extension only
("windy-connect", "backend/src/writers/claude_code.py", True),
("windy-chat", "services/agent-roster/lib/llm.js", False), # audit #2: must be flagged
("windy-pro", "account-server/src/routes/translations.ts", False),
],
)
def test_allow_list_entries(repo, path, ok):
assert cg.allowed(repo, path, ALLOW) is ok
DIFF = """diff --git a/app/llm.py b/app/llm.py
--- a/app/llm.py
+++ b/app/llm.py
@@ -10,0 +11,2 @@
+import anthropic
+client = anthropic.Anthropic()
diff --git a/tests/test_llm.py b/tests/test_llm.py
--- /dev/null
+++ b/tests/test_llm.py
@@ -0,0 +1 @@
+import anthropic
@@ -40 +42 @@
-x = 1
+x = 2
"""
def test_only_added_non_test_lines_are_findings():
fs = cg.parse_added("windy-chat", DIFF, ALLOW)
assert [(f.path, f.line, f.kind) for f in fs] == [("app/llm.py", 11, "provider SDK")]
def _repo(tmp_path, files: dict[str, str]) -> tuple[Path, str]:
work = tmp_path / "w"
work.mkdir()
run = lambda *a: subprocess.run(["git", *a], cwd=work, check=True, capture_output=True) # noqa: E731
run("init", "-q", "-b", "main")
for p, text in files.items():
(work / p).parent.mkdir(parents=True, exist_ok=True)
(work / p).write_text(text)
run("add", "-A")
run("-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "x")
bare = tmp_path / "r.git"
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
sha = subprocess.run(["git", "--git-dir", str(bare), "rev-parse", "main"],
capture_output=True, text=True, check=True).stdout.strip()
return bare, sha
def test_tree_scan_on_a_real_git_repo(tmp_path):
bare, sha = _repo(tmp_path, {
"app/llm.py": "import os\nKEY = os.environ['OPENAI_API_KEY']\n",
"app/ok.py": "MIND = 'https://mind.windyword.ai'\n",
"tests/test_llm.py": "import anthropic\n",
"docs/x.md": "api.anthropic.com\n",
})
fs = cg.scan_tree("windy-chat", bare, sha, ALLOW)
assert [(f.path, f.line, f.kind) for f in fs] == [("app/llm.py", 2, "provider key")]
def test_warn_mode_never_turns_red(monkeypatch):
monkeypatch.setattr(cg, "MODE", "warn")
state, desc, f = cg.status_for([cg.Finding("a.py", 3, "provider host", "api.openai.com")], whole_tree=False)
assert state == "success" and desc.startswith("⚠ WARN (not blocking): 1 direct AI-provider use added")
assert "a.py:3" in desc and f.path == "a.py"
def test_block_mode_fails(monkeypatch):
monkeypatch.setattr(cg, "MODE", "block")
state, desc, _ = cg.status_for([cg.Finding("a.py", 3, "provider host", "x")], whole_tree=True)
assert state == "failure" and desc.startswith("BLOCKED")
def test_clean_is_ok():
assert cg.status_for([], whole_tree=True)[:2] == (
"success", "OK: no direct AI-provider use in tree (Windy Mind is the only door)")
@pytest.mark.parametrize(
"text",
[
" # The ANTHROPIC_OAUTH_TOKEN setting was removed on 2026-09-23 ON PURPOSE", # windy-search
"# ANTHROPIC_API_KEY=",
" // fallback used to call https://api.groq.com directly",
" * @see https://api.openai.com/v1/audio",
"<!-- api.anthropic.com -->",
],
)
def test_comments_are_not_calls(text):
assert cg.scan_line("service/app/config.py", text) == []
def test_code_with_a_trailing_comment_still_counts():
assert cg.scan_line("a.js", "fetch('https://api.openai.com/v1') // TODO move to Mind")
def test_windy_pro_desktop_is_byok_but_the_account_server_is_not():
assert cg.allowed("windy-pro", "src/client/desktop/main.js", ALLOW)
assert not cg.allowed("windy-pro", "account-server/src/routes/translations.ts", ALLOW)

View File

@@ -425,28 +425,9 @@ def test_i05_jobs_get_a_network_per_job_not_a_shared_bridge():
def test_i05_jobs_cannot_bind_mount_from_the_daemon_host(): def test_i05_jobs_cannot_bind_mount_from_the_daemon_host():
"""Narrowed 2026-09-23 (orchestrator-approved): a job may bind-mount EXACTLY cfg = (ROOT / "deploy" / "runner" / "config.yaml").read_text()
one daemon path, windy-pro's non-secret build inputs, and only because dind assert "valid_volumes: []" in cfg
itself has that path READ-ONLY. Anything more (a second path, a writable assert 'docker_host: "-"' in cfg
one, a glob) reopens the host to CI code. Still no docker socket for jobs."""
import re
import yaml
rd = ROOT / "deploy" / "runner"
cfg = yaml.safe_load((rd / "config.yaml").read_text())
allowed = cfg["container"]["valid_volumes"]
assert allowed in ([], ["/ci-inputs/windy-pro"]), f"I-5: jobs may mount nothing else: {allowed}"
assert cfg["container"]["docker_host"] == "-"
if allowed:
compose = yaml.safe_load((rd / "docker-compose.yml").read_text())
binds = [v for v in compose["services"]["dind"]["volumes"] if v.startswith("/")]
assert binds == ["/home/user1-gpu/ci-inputs/windy-pro:/ci-inputs/windy-pro:ro"], (
f"I-5: dind's only host bind must be the ci-inputs path, READ-ONLY: {binds}")
for name, svc in compose["services"].items():
if name != "dind":
for v in svc.get("volumes") or []:
assert not re.match(r"^/home/user1-gpu/ci-inputs", v), f"I-5: {name} mounts ci-inputs"
def test_i05_no_ci_container_can_reach_the_forge_network(): def test_i05_no_ci_container_can_reach_the_forge_network():

View File

@@ -10,7 +10,6 @@ from __future__ import annotations
import base64 import base64
import importlib.util import importlib.util
import sys
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -80,11 +79,10 @@ class Fake:
@pytest.fixture @pytest.fixture
def fake(monkeypatch): def fake(monkeypatch):
def make(queued=(), **kw): def make(**kw):
f = Fake(**kw) f = Fake(**kw)
monkeypatch.setattr(bridge, "gitea", f.gitea) monkeypatch.setattr(bridge, "gitea", f.gitea)
monkeypatch.setattr(bridge, "github", f.github) monkeypatch.setattr(bridge, "github", f.github)
monkeypatch.setattr(bridge, "queued_jobs", lambda repo, sha: list(queued))
return f return f
return make return make
@@ -274,111 +272,3 @@ def test_gitea_dir_wins_over_github_dir(fake):
) )
def test_workflow_problem(text, problem): def test_workflow_problem(text, problem):
assert bridge.workflow_problem(text) == problem assert bridge.workflow_problem(text) == problem
def _q(n, wf, job):
return {"run_number": n, "workflow_id": wf, "name": job}
def test_queued_job_shows_pending_instead_of_nothing(fake):
f = fake(queued=[_q(5, "ci.yml", "test")])
bridge.post_statuses("windy-chat", SHA)
assert [(p["context"], p["state"]) for p in f.posted] == [("windy-git/ci/test", "pending")]
assert f.posted[0]["target_url"].endswith("/actions/runs/5")
def test_queued_rerun_supersedes_the_stale_failure(fake):
f = fake(runs=[_run(1, "ci.yml", "test", "failure", n=4)], queued=[_q(7, "ci.yml", "test")])
bridge.post_statuses("windy-chat", SHA)
assert [(p["context"], p["state"]) for p in f.posted] == [("windy-git/ci/test", "pending")]
def test_older_queued_job_never_overrides_a_newer_verdict(fake):
f = fake(runs=[_run(1, "ci.yml", "test", "success", n=9)], queued=[_q(3, "ci.yml", "test")])
bridge.post_statuses("windy-chat", SHA)
assert [(p["context"], p["state"]) for p in f.posted] == [("windy-git/ci/test", "success")]
def test_queued_docker_and_non_blocking_jobs_stay_unposted(fake):
f = fake(queued=[_q(2, "ci.yml", "docker-build"), _q(2, "ci.yml", "build-desktop")])
bridge.post_statuses("windy-pro", SHA)
assert f.posted == []
def test_queued_lookup_refuses_unsafe_input():
assert bridge.queued_jobs("x'; drop table t;--", SHA) == []
assert bridge.queued_jobs("windy-chat", "not-a-sha") == []
def _db(monkeypatch, jobs, labels):
import json as _json
import subprocess as _sp
payload = _json.dumps({"jobs": jobs, "labels": [_json.dumps(x) for x in labels]})
monkeypatch.setattr(
bridge.subprocess, "run",
lambda *a, **k: _sp.CompletedProcess(a, 0, stdout=payload, stderr=""),
)
RUNNER = ["veron-1", "linux-x64", "self-hosted", "linux", "x64"]
def test_only_jobs_a_runner_can_take_are_pending(monkeypatch):
# macos-latest is cancelled unpicked by the janitor: pending would never resolve.
_db(monkeypatch, [
{"run_number": 3, "workflow_id": "ci.yml", "name": "test", "runs_on": '["self-hosted","linux","x64"]'},
{"run_number": 3, "workflow_id": "ci.yml", "name": "mac", "runs_on": '["macos-latest"]'},
], [RUNNER])
assert [j["name"] for j in bridge.queued_jobs("windy-chat", SHA)] == ["test"]
def test_lookup_failure_is_non_fatal(monkeypatch):
import subprocess as _sp
def boom(*a, **k):
raise _sp.TimeoutExpired("docker", 30)
monkeypatch.setattr(bridge.subprocess, "run", boom)
assert bridge.queued_jobs("windy-chat", SHA) == []
class _Guard:
def __init__(self, findings):
self.findings = findings
def check(self, repo, sha, default_branch, is_default_head):
return self.findings
@staticmethod
def status_for(findings, whole_tree):
if not findings:
return "success", "OK: clean", None
return "success", f"WARN {len(findings)}", findings[0]
class _F:
path, line = "app/llm.py", 7
def test_guard_posts_warn_with_a_link_to_the_first_finding(fake, monkeypatch):
f = fake()
monkeypatch.setitem(sys.modules, "compute_guard", _Guard([_F()]))
bridge.post_compute_guard("windy-chat", SHA, "main", False)
assert [(p["context"], p["state"], p["description"]) for p in f.posted] == [
("windy-git/compute-guard", "success", "WARN 1")]
assert f.posted[0]["target_url"].endswith(f"/src/commit/{SHA}/app/llm.py#L7")
def test_guard_same_status_is_not_reposted(fake, monkeypatch):
f = fake(statuses=[{"context": "windy-git/compute-guard", "state": "success", "description": "WARN 1"}])
monkeypatch.setitem(sys.modules, "compute_guard", _Guard([_F()]))
bridge.post_compute_guard("windy-chat", SHA, "main", False)
assert f.posted == []
def test_guard_that_cannot_run_posts_nothing(fake, monkeypatch):
f = fake()
monkeypatch.setitem(sys.modules, "compute_guard", _Guard(None))
bridge.post_compute_guard("windy-chat", SHA, "main", True)
assert f.posted == []

View File

@@ -1,82 +0,0 @@
"""Push-velocity detection (scripts/telemetry_emit.py): detect + alert only.
Driven through the real function with rows shaped like the Gitea query's.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
_spec = importlib.util.spec_from_file_location("telemetry_emit", ROOT / "scripts" / "telemetry_emit.py")
te = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(te)
NOW = 1_800_000_000.0
def row(login="agent-et26abcd1234", uid=7, p1h=0, p24h=0, d24h=0, repos=1, wid=None):
return {"uid": uid, "login": login, "wid": wid, "p1h": p1h, "p24h": p24h, "d24h": d24h, "repos": repos}
def test_under_every_threshold_emits_nothing():
ev, keep = te.push_velocity_events([row(p1h=60, p24h=500, d24h=10)], NOW, {})
assert ev == [] and keep == {}
def test_burst_emits_one_declared_row_with_the_passport():
ev, keep = te.push_velocity_events([row(p1h=61, p24h=61, repos=3)], NOW, {})
assert len(ev) == 1
e = ev[0]
assert e["event_type"] == "forge.push_velocity" and e["service"] == "forge"
assert e["actor_type"] == "agent" and e["actor_id"] == "ET26-ABCD-1234"
assert e["metadata"] == {
"rule": "pushes_1h", "window_s": 3600, "count": 61, "threshold": 60,
"repos": 3, "gitea_user_id": 7,
}
assert keep == {"7:pushes_1h": NOW}
def test_still_over_is_reported_once_per_window_not_every_run():
_, keep = te.push_velocity_events([row(p1h=90)], NOW, {})
ev, keep = te.push_velocity_events([row(p1h=95)], NOW + 300, keep)
assert ev == [] and keep == {"7:pushes_1h": NOW}
ev, _ = te.push_velocity_events([row(p1h=95)], NOW + 3601, keep)
assert len(ev) == 1
def test_dropping_back_under_rearms():
_, keep = te.push_velocity_events([row(p1h=90)], NOW, {})
_, keep = te.push_velocity_events([row(p1h=5)], NOW + 300, keep)
assert keep == {}
ev, _ = te.push_velocity_events([row(p1h=90)], NOW + 600, keep)
assert len(ev) == 1
def test_the_sync_account_is_exempt():
ev, _ = te.push_velocity_events([row(login="windyadmin", uid=1, p1h=9999, p24h=9999)], NOW, {})
assert ev == []
def test_sso_human_is_keyed_on_windy_identity_id():
ev, _ = te.push_velocity_events([row(login="u-5e1b9569abc", wid="5e1b9569-full-id", d24h=11)], NOW, {})
assert [(e["actor_type"], e["actor_id"], e["metadata"]["rule"]) for e in ev] == [
("human", "5e1b9569-full-id", "ref_deletes_24h")
]
assert "caller" not in ev[0]["metadata"]
def test_no_provable_id_is_system_plus_caller_never_an_invented_id():
# UPDATE 2 actor rule: agent/human rows without an actor_id are quarantined.
for login in ("u-nolink", "agent-weird"):
ev, _ = te.push_velocity_events([row(login=login, p24h=501)], NOW, {})
assert ev[0]["actor_type"] == "system" and "actor_id" not in ev[0]
assert ev[0]["metadata"]["caller"] == "unknown"
def test_passport_round_trip():
assert te.passport_from_login("agent-et26p1zgttp8") == "ET26-P1ZG-TTP8"
assert te.passport_from_login("u-abc") is None

View File

@@ -1,40 +0,0 @@
# Compute guard allow-list: code that MAY talk to an AI provider directly.
# Windy Mind is the ONLY door to AI compute (Grant, 2026-09-23). Every entry
# here is an exception to that rule and MUST say why. Paths are fnmatch globs
# relative to the repo root. Owner of this file: Windy Git lane (13); changes
# go through the orchestrator. Source of the first entries: COMPUTE_BYPASS_AUDIT.md.
allow:
- repo: windy-mind
paths: ["*"]
reason: "Windy Mind IS the door: provider clients belong here by definition."
- repo: windy-agent
paths: ["*"]
reason: >-
User BYOK: self-hosted agents call providers on the USER's own keys.
Mind stays opt-in there, or every self-hosted user's inference lands on
Grant's bill (no-cloud-cost-liability rule; audit #7).
- repo: windy-code
paths: ["extensions/windy-ai/*"]
reason: "User BYOK AI extension: the user's own provider keys; Mind is one opt-in provider (audit #8)."
- repo: windy-connect
paths: ["*writers/*"]
reason: "Writes client configs that NAME the user's own provider env vars; makes no provider calls (audit #11)."
- repo: windy-pro
paths: ["src/client/desktop/*"]
reason: >-
User BYOK desktop client: cloud STT/translate keys come from what the USER
enters (renderer localStorage -> electron-store; env var only for dev), and
the CSP line allows exactly those user-keyed hosts (audit #10). The
account-server is NOT covered: server-side calls go through Mind.
- repo: windy-pro
paths: ["src/client/web/src/pages/panels/MindPanel.jsx"]
reason: "Validates the USER's own OpenRouter key for BYOK (audit #10); spends no house money."
- repo: windy-git
paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"]
reason: "The guard's own pattern list and this file."

View File

@@ -54,9 +54,6 @@ container:
privileged: false privileged: false
options: options:
workdir_parent: /workspace workdir_parent: /workspace
# A job may bind-mount exactly ONE daemon path: the read-only windy-pro build valid_volumes: [] # a job cannot bind-mount anything from the daemon host
# inputs (mounted :ro into dind itself). Per-runner, not per-repo (act_runner
# limit): any windyadmin repo could mount it; it is non-secret and read-only.
valid_volumes: ["/ci-inputs/windy-pro"]
docker_host: "-" # do NOT expose the runner's own docker socket to jobs docker_host: "-" # do NOT expose the runner's own docker socket to jobs
force_pull: false force_pull: false

View File

@@ -49,11 +49,6 @@ services:
# A fresh volume: Sysbox shifts ownership to its own uid range. The old # A fresh volume: Sysbox shifts ownership to its own uid range. The old
# `dind-storage` is kept untouched for the privileged rollback. # `dind-storage` is kept untouched for the privileged rollback.
- dind-storage-sysbox:/var/lib/docker - dind-storage-sysbox:/var/lib/docker
# READ-ONLY, non-secret build inputs for windy-pro's desktop jobs (models,
# linux-x64 portable bundle, enter-monitor build), copied from the frozen
# release clone by deploy/runner/refresh-ci-inputs.sh. Jobs may mount ONLY
# this path (config.yaml valid_volumes). Orchestrator-approved 09-23.
- /home/user1-gpu/ci-inputs/windy-pro:/ci-inputs/windy-pro:ro
# G1.5 — bounded so a fork-bomb workflow cannot starve Grant's interactive # G1.5 — bounded so a fork-bomb workflow cannot starve Grant's interactive
# session. Veron 1 is his workstation, not a dedicated build box. # session. Veron 1 is his workstation, not a dedicated build box.
cpus: 12.0 # 12 of 24 cores cpus: 12.0 # 12 of 24 cores

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# Refresh the READ-ONLY CI input cache for windy-pro desktop jobs from the FROZEN
# release clone on Veron. Reads ~/windy-pro-release only; never writes to it.
# Run when the release lane says engines / wheels / the portable bundle changed.
# Linux inputs only: 3 models + requirements-bundle.txt, bundled-portable/linux-x64,
# native/enter-monitor/build. Result is chmod a-w and mounted :ro into dind.
set -euo pipefail
SRC=/home/user1-gpu/windy-pro-release
DST=/home/user1-gpu/ci-inputs/windy-pro
models=$(ls "$SRC/extraResources/model" | grep -E '^windy-(nano|lite|core)-ct2$')
[ "$(wc -w <<<"$models")" = 3 ] || { echo "expected 3 models, got: $models"; exit 1; }
mkdir -p "$DST/extraResources/model" "$DST/bundled-portable" "$DST/native-enter-monitor-build"
chmod -R u+w "$DST"
R="ionice -c3 nice -n 19 rsync -a --delete"
for m in $models; do $R "$SRC/extraResources/model/$m/" "$DST/extraResources/model/$m/"; done
$R "$SRC/extraResources/requirements-bundle.txt" "$DST/extraResources/requirements-bundle.txt"
$R "$SRC/bundled-portable/linux-x64/" "$DST/bundled-portable/linux-x64/"
$R "$SRC/native/enter-monitor/build/" "$DST/native-enter-monitor-build/"
date -u +%FT%TZ > "$DST/.refreshed-from-windy-pro-release"
chmod -R a-w "$DST"
du -sh --apparent-size "$DST"

View File

@@ -152,16 +152,3 @@ disqualifying the moment a stranger depends on it. **The trigger is not a date
it is the first external push.** Move the control plane to a dedicated VPS (not it is the first external push.** Move the control plane to a dedicated VPS (not
Kit 0), keep Veron 1 as the runner. It is an rsync, a Postgres dump and three Kit 0), keep Veron 1 as the runner. It is an rsync, a Postgres dump and three
DNS record edits. DNS record edits.
## Re-run a PR's CI (Gitea 1.24 has no rerun API)
```bash
ssh wg-veron
cd /srv/windygit/src && bash scripts/rerun_ci.sh <repo> <branch> <github-head-sha-prefix>
```
Moves the Windy Git branch back one commit; the next sync force-pushes the
GitHub head again and Gitea re-fires every workflow for that event on the same
commit. Guarded: refuses unless the branch is at the given sha, waits for a
sync that starts AFTER the rewind, restores the branch itself on timeout.
Don't use the web "Re-run" button: it needs a hub-SSO session as windyadmin,
which is Grant's identity.

View File

@@ -33,7 +33,6 @@ import sys
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
STATE_PATH = os.environ.get("CANARY_STATE", "canary-state.json") STATE_PATH = os.environ.get("CANARY_STATE", "canary-state.json")
@@ -47,16 +46,6 @@ ALERT_FROM = os.environ.get("CANARY_ALERT_FROM", "office@thewindstorm.uk")
LOGIN_WARN_SECONDS = float(os.environ.get("CANARY_LOGIN_WARN_S", "35")) LOGIN_WARN_SECONDS = float(os.environ.get("CANARY_LOGIN_WARN_S", "35"))
TIMEOUT = float(os.environ.get("CANARY_TIMEOUT_S", "60")) TIMEOUT = float(os.environ.get("CANARY_TIMEOUT_S", "60"))
# Journey cleanup rule (orchestrator, 2026-09-23). The login probe creates a hub
# session (access + refresh token) every run, so it must end it. The hub's
# /auth/logout revokes the token AND every refresh token of the account
# (verified live: access 401, refresh 401 after it). So the next successful
# logout also heals anything a failed run left behind; no ledger needed.
LOGOUT_URL = "https://account.windyword.ai/api/v1/auth/logout"
LOGOUT_ATTEMPTS = 8 # retried on 5xx / no response only
LOGOUT_GAP_S = 15.0
LOGOUT_GONE = (401, 404, 410) # the session is already over = done
@dataclass @dataclass
class Result: class Result:
@@ -65,7 +54,6 @@ class Result:
detail: str detail: str
seconds: float = 0.0 seconds: float = 0.0
user_visible: str = "" user_visible: str = ""
followups: list[Result] = field(default_factory=list)
@dataclass @dataclass
@@ -80,8 +68,6 @@ class Check:
# When True this check INVERTS: a 2xx is a critical failure (a security # When True this check INVERTS: a 2xx is a critical failure (a security
# control opened) and a 401/403/503 is the healthy, expected outcome. # control opened) and a 401/403/503 is the healthy, expected outcome.
must_refuse: bool = False must_refuse: bool = False
# Runs on a 2xx with the response body; returns follow-up results (cleanup).
after: Callable[[bytes], list[Result]] | None = None
def _probe(c: Check) -> Result: def _probe(c: Check) -> Result:
@@ -105,18 +91,13 @@ def _probe(c: Check) -> Result:
elapsed, c.what_it_proves) elapsed, c.what_it_proves)
if r.status >= 400: if r.status >= 400:
return Result(c.name, "down", f"HTTP {r.status}", elapsed, c.what_it_proves) return Result(c.name, "down", f"HTTP {r.status}", elapsed, c.what_it_proves)
raw = r.read()
warn = c.warn_seconds warn = c.warn_seconds
if warn and elapsed > warn: if warn and elapsed > warn:
res = Result( return Result(
c.name, "slow", f"HTTP {r.status} in {elapsed:.1f}s (warn >{warn:.0f}s)", c.name, "slow", f"HTTP {r.status} in {elapsed:.1f}s (warn >{warn:.0f}s)",
elapsed, c.what_it_proves, elapsed, c.what_it_proves,
) )
else: return Result(c.name, "ok", f"HTTP {r.status} in {elapsed:.1f}s", elapsed, c.what_it_proves)
res = Result(c.name, "ok", f"HTTP {r.status} in {elapsed:.1f}s", elapsed, c.what_it_proves)
if c.after:
res.followups = c.after(raw)
return res
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
if c.must_refuse and e.code in (401, 403, 503): if c.must_refuse and e.code in (401, 403, 503):
return Result(c.name, "ok", f"correctly refused (HTTP {e.code})", return Result(c.name, "ok", f"correctly refused (HTTP {e.code})",
@@ -129,52 +110,6 @@ def _probe(c: Check) -> Result:
) )
def logout(token: str, *, attempts: int = LOGOUT_ATTEMPTS, gap: float = LOGOUT_GAP_S,
sleep: Callable[[float], None] = time.sleep) -> Result:
"""End the session the login probe opened. Honest: never ok unless proven."""
what = "the canary leaves no live session behind (journey cleanup rule)"
headers = {
"User-Agent": "windy-git-canary/1.0",
"X-Windy-Synthetic": "1",
"Authorization": f"Bearer {token}",
}
start = time.monotonic()
last = "no attempt"
for i in range(attempts):
if i:
sleep(gap)
req = urllib.request.Request(LOGOUT_URL, data=b"", method="POST", headers=headers)
try:
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
return Result("identity.logout", "ok", f"session ended (HTTP {r.status})",
time.monotonic() - start, what)
except urllib.error.HTTPError as e:
if e.code in LOGOUT_GONE:
return Result("identity.logout", "ok", f"session already over (HTTP {e.code})",
time.monotonic() - start, what)
if e.code < 500: # a 4xx won't change on retry: fail fast
return Result("identity.logout", "down", f"CLEANUP FAILED: HTTP {e.code}",
time.monotonic() - start, what)
last = f"HTTP {e.code}"
except Exception as e: # noqa: BLE001 — no response / timeout: retry
last = f"{type(e).__name__}"
return Result("identity.logout", "down",
f"CLEANUP FAILED after {attempts} tries: {last} (next run's logout heals it)",
time.monotonic() - start, what)
def _logout_after_login(raw: bytes) -> list[Result]:
try:
token = (json.loads(raw or b"{}") or {}).get("token")
except ValueError:
token = None
if not token:
return [Result("identity.logout", "down",
"CLEANUP FAILED: login returned no token to log out with",
0.0, "the canary leaves no live session behind (journey cleanup rule)")]
return [logout(token)]
def build_checks() -> list[Check]: def build_checks() -> list[Check]:
checks = [ checks = [
Check( Check(
@@ -252,7 +187,6 @@ def build_checks() -> list[Check]:
method="POST", method="POST",
body={"email": email, "password": pw}, body={"email": email, "password": pw},
warn_seconds=LOGIN_WARN_SECONDS, warn_seconds=LOGIN_WARN_SECONDS,
after=_logout_after_login,
) )
) )
return checks return checks
@@ -329,10 +263,7 @@ def main() -> int:
args = ap.parse_args() args = ap.parse_args()
previous = load_state() previous = load_state()
results = [] results = [_probe(c) for c in build_checks()]
for c in build_checks():
r = _probe(c)
results += [r, *r.followups]
print(f"windy canary — {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}\n") print(f"windy canary — {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}\n")
for r in results: for r in results:

View File

@@ -1,270 +0,0 @@
#!/usr/bin/env python3
"""Compute guard: Windy Mind is the ONLY door to AI compute (Grant, 2026-09-23).
Flags code that talks to an AI provider directly instead of through Windy Mind:
a provider API host, a provider SDK import or dependency, or a raw provider key
name. Direct calls skip Mind's metering, caps and live-model routing, and they
spend whichever key happens to be lying around (the audit found Grant's personal
Max OAuth token inside a platform container).
WARN-ONLY for now: the bridge posts `windy-git/compute-guard` as success with a
"⚠ WARN" description, so nothing turns red. `COMPUTE_GUARD_MODE=block` flips
findings to failure once the repos are clean (orchestrator's call).
- PR heads: only lines the PR ADDS (vs its merge-base with the default branch).
- Default-branch head: the whole tree (the baseline, and what `report` prints).
Exceptions live in ONE file, ci/compute-guard-allow.yml, each with a reason.
Tests, docs, lockfiles, vendored code and CI config are never scanned.
Reads the sync's bare GitHub clones on Veron (no docker exec: IO-stall lesson).
python3 scripts/compute_guard.py report [repo ...] # whole-tree findings on each default branch
"""
from __future__ import annotations
import fnmatch
import hashlib
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
ALLOW_FILE = Path(os.environ.get("COMPUTE_GUARD_ALLOW", ROOT / "ci" / "compute-guard-allow.yml"))
WORK = Path(os.environ.get("SYNC_WORK", "/srv/windygit/sync"))
CACHE = Path(os.environ.get("COMPUTE_GUARD_CACHE", "/var/lib/windy-git/compute-guard-cache.json"))
MODE = os.environ.get("COMPUTE_GUARD_MODE", "warn") # warn | block
HOSTS = [
"api.anthropic.com", "api.openai.com", "api.groq.com",
"generativelanguage.googleapis.com", "api.mistral.ai", "api.perplexity.ai",
"openrouter.ai", "api.together.xyz", "api.together.ai", "api.cerebras.ai",
"api.sambanova.ai", "api.deepseek.com", "api.x.ai", "api.cohere.ai",
"api.cohere.com", "api.fireworks.ai", "api.replicate.com",
"api-inference.huggingface.co",
]
KEYS = [
"ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN",
"OPENAI_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY",
"GOOGLE_AI_API_KEY", "MISTRAL_API_KEY", "PERPLEXITY_API_KEY", "PPLX_API_KEY",
"OPENROUTER_API_KEY", "TOGETHER_API_KEY", "CEREBRAS_API_KEY", "SAMBANOVA_API_KEY",
"DEEPSEEK_API_KEY", "XAI_API_KEY", "COHERE_API_KEY", "FIREWORKS_API_KEY",
"REPLICATE_API_TOKEN",
]
PY_SDKS = r"anthropic|openai|groq|mistralai|cohere|google\.generativeai|google\.genai|together|cerebras|litellm"
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)")
RULES: list[tuple[str, re.Pattern]] = [
("provider host", re.compile("|".join(re.escape(h) for h in HOSTS))),
("provider key", re.compile(r"\b(?:" + "|".join(KEYS) + r")\b")),
("provider SDK", re.compile(rf"^\s*(?:from|import)\s+(?:{PY_SDKS})(?:\s|\.|$|,)")),
("provider SDK", re.compile(rf"""(?:from\s+|require\(\s*|import\(\s*)['"](?:{JS_SDKS})(?:/[^'"]*)?['"]""")),
# dependency manifests: package.json keys, requirements / pyproject lines
("provider SDK dep", re.compile(rf'''^\s*"(?:{JS_SDKS})"\s*:''')),
("provider SDK dep", re.compile(rf'''^\s*["']?(?:{PY_SDKS.replace(chr(92) + ".", "-")})(?:\[[^\]]*\])?\s*(?:[<>=~!]=?|["',]|$)''')),
]
DEP_FILES = re.compile(r"(^|/)(package\.json|requirements[^/]*\.txt|pyproject\.toml|setup\.cfg|Pipfile)$")
# Never scanned: tests, docs, lockfiles, vendored/built code, CI config.
SKIP = re.compile(
r"(^|/)(tests?|__tests__|spec|docs?|node_modules|vendor|dist|build|\.github|\.gitea)/"
r"|(^|/)(test_[^/]*|[^/]*_test\.py|conftest\.py|[^/]*\.(test|spec)\.[cm]?[jt]sx?)$"
r"|\.(md|mdx|rst|txt|lock|snap|svg|png|jpg|pdf)$"
r"|(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|uv\.lock|poetry\.lock|Cargo\.lock)$"
)
@dataclass(frozen=True)
class Finding:
path: str
line: int
kind: str
match: str
def load_allow(path: Path = ALLOW_FILE) -> list[dict]:
data = yaml.safe_load(path.read_text()) or {}
entries = data.get("allow") or []
for e in entries: # a reason per entry is the whole point of the file
if not (e.get("repo") and e.get("paths") and str(e.get("reason", "")).strip()):
raise ValueError(f"allow entry needs repo, paths and a reason: {e}")
return entries
def allowed(repo: str, path: str, allow: list[dict]) -> bool:
for e in allow:
if e["repo"] == repo and any(fnmatch.fnmatch(path, g) for g in e["paths"]):
return True
return False
COMMENT = re.compile(r"^\s*(?:#|//|/\*|\*|<!--)")
def scan_line(path: str, text: str) -> list[tuple[str, str]]:
# A comment is not a call: "the ANTHROPIC_OAUTH_TOKEN setting was removed"
# (windy-search) must not count, nor a commented-out `# OPENAI_API_KEY=`.
if COMMENT.match(text):
return []
hits = []
for kind, rx in RULES:
if kind == "provider SDK dep" and not DEP_FILES.search(path):
continue
m = rx.search(text)
if m:
hits.append((kind, m.group(0).strip()[:60]))
return hits
def _git(bare: Path, *args: str) -> str:
return subprocess.run(
["git", "--git-dir", str(bare), *args],
capture_output=True, text=True, check=True, timeout=120,
).stdout
def scan_tree(repo: str, bare: Path, sha: str, allow: list[dict]) -> list[Finding]:
"""Every line in the tree at `sha` (default branch: the baseline)."""
# A cheap prefilter by git, then the real rules in Python.
pre = "|".join([re.escape(h) for h in HOSTS] + KEYS + ["anthropic", "openai", "groq", "mistral",
"generativeai", "genai", "cohere", "together", "cerebras", "litellm"])
try:
out = _git(bare, "grep", "-nIE", "-e", pre, sha, "--", ".")
except subprocess.CalledProcessError as e:
if e.returncode == 1: # no matches
return []
raise
found = []
for raw in out.splitlines():
# <sha>:<path>:<line>:<text>
try:
_, path, line, text = raw.split(":", 3)
except ValueError:
continue
if SKIP.search(path) or allowed(repo, path, allow):
continue
for kind, match in scan_line(path, text):
found.append(Finding(path, int(line), kind, match))
return found
def scan_added(repo: str, bare: Path, base_ref: str, sha: str, allow: list[dict]) -> list[Finding]:
"""Only the lines a PR adds, vs its merge-base with the default branch."""
mb = _git(bare, "merge-base", base_ref, sha).strip()
diff = _git(bare, "diff", "-U0", "--no-color", "--no-ext-diff", mb, sha)
return parse_added(repo, diff, allow)
HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
def parse_added(repo: str, diff: str, allow: list[dict]) -> list[Finding]:
found, path, line = [], None, 0
for raw in diff.splitlines():
if raw.startswith("+++ "):
p = raw[4:]
path = None if p == "/dev/null" else p[2:] if p.startswith("b/") else p
continue
m = HUNK.match(raw)
if m:
line = int(m.group(1))
continue
if path is None or raw.startswith("--- "):
continue
if raw.startswith("+"):
if not (SKIP.search(path) or allowed(repo, path, allow)):
for kind, match in scan_line(path, raw[1:]):
found.append(Finding(path, line, kind, match))
line += 1
return found
# ---- cache: a tree scan runs once per (repo, sha, rules+allow) --------------
def _fingerprint(allow: list[dict]) -> str:
return hashlib.sha256(
json.dumps([HOSTS, KEYS, PY_SDKS, JS_SDKS, SKIP.pattern, allow], sort_keys=True).encode()
).hexdigest()[:16]
def cached_scan(key: str, fn) -> list[Finding]:
try:
cache = json.loads(CACHE.read_text())
except (OSError, ValueError):
cache = {}
if key in cache:
return [Finding(**f) for f in cache[key]]
result = fn()
cache[key] = [f.__dict__ for f in result]
if len(cache) > 2000: # keep it small: newest entries win
cache = dict(list(cache.items())[-1000:])
try:
CACHE.parent.mkdir(parents=True, exist_ok=True)
tmp = CACHE.with_suffix(".tmp")
tmp.write_text(json.dumps(cache))
tmp.replace(CACHE)
except OSError:
pass
return result
def check(repo: str, sha: str, default_branch: str, is_default_head: bool) -> list[Finding] | None:
"""Findings for one commit, or None when the guard can't run (never a fake OK)."""
bare = WORK / f"{repo}.git"
if not bare.is_dir():
return None
allow = load_allow()
fp = _fingerprint(allow)
if is_default_head:
return cached_scan(f"tree:{repo}:{sha}:{fp}", lambda: scan_tree(repo, bare, sha, allow))
return cached_scan(
f"pr:{repo}:{sha}:{fp}",
lambda: scan_added(repo, bare, f"refs/heads/{default_branch}", sha, allow),
)
def status_for(findings: list[Finding], whole_tree: bool) -> tuple[str, str, Finding | None]:
"""(state, description, first finding) for the GitHub commit status."""
scope = "in tree" if whole_tree else "added"
if not findings:
what = "no direct AI-provider use in tree" if whole_tree else "no direct AI-provider use added"
return "success", f"OK: {what} (Windy Mind is the only door)", None
f = findings[0]
n = len(findings)
state = "failure" if MODE == "block" else "success"
lead = "BLOCKED" if MODE == "block" else "⚠ WARN (not blocking)"
desc = f"{lead}: {n} direct AI-provider use{'s' if n > 1 else ''} {scope}, e.g. {f.path}:{f.line} {f.match}"
return state, desc[:140], f
def report(repos: list[str]) -> int:
allow = load_allow()
total = 0
for repo in repos:
bare = WORK / f"{repo}.git"
if not bare.is_dir():
print(f"## {repo}: no sync clone, skipped")
continue
head = _git(bare, "symbolic-ref", "--short", "HEAD").strip()
sha = _git(bare, "rev-parse", head).strip()
fs = scan_tree(repo, bare, sha, allow)
total += len(fs)
print(f"## {repo} ({head} {sha[:7]}): {len(fs)} finding(s)")
for f in fs:
print(f" {f.path}:{f.line} [{f.kind}] {f.match}")
print(f"TOTAL {total}")
return 0
if __name__ == "__main__":
if len(sys.argv) >= 2 and sys.argv[1] == "report":
default = os.environ.get("BRIDGE_REPOS", "").split() or sorted(
p.name.removesuffix(".git") for p in WORK.glob("*.git"))
sys.exit(report(sys.argv[2:] or default))
sys.exit(__doc__)

View File

@@ -32,7 +32,6 @@ import base64
import json import json
import os import os
import re import re
import subprocess
import sys import sys
import time import time
import urllib.error import urllib.error
@@ -223,54 +222,6 @@ def sync_prs(repo: str) -> list[str]:
return heads return heads
SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
SAFE_SHA = re.compile(r"^[0-9a-f]{40}$")
def queued_jobs(repo: str, sha: str) -> list[dict]:
"""Jobs at `sha` that are waiting for a runner and that a runner CAN take.
Gitea 1.24's API lists only PICKED-UP jobs (/actions/tasks), so a queued PR
showed nothing on GitHub and people asked whether the push was lost. The
truth is in the gitea DB. Bounded + non-fatal: during the 09-23 IO stall
`docker exec` hung for an hour and must never wedge the bridge again.
Only status 5 (waiting) with labels some live runner has. A `pending` we
post must end in a verdict we will also see, or it sits yellow forever:
blocked jobs (7) often end SKIPPED, and jobs for labels no runner has
(macos-/windows-/ubuntu-latest) are cancelled by the janitor unpicked;
neither ever appears in /actions/tasks.
"""
if not (SAFE_NAME.match(repo) and SAFE_NAME.match(WG_OWNER) and SAFE_SHA.match(sha)):
return []
query = (
"select json_build_object("
" 'jobs', (select coalesce(json_agg(t), '[]'::json) from ("
" select ar.index as run_number, ar.workflow_id, j.name, j.runs_on"
" from action_run_job j join action_run ar on ar.id = j.run_id"
" join repository r on r.id = j.repo_id join \"user\" o on o.id = r.owner_id"
f" where o.lower_name = '{WG_OWNER.lower()}' and r.lower_name = '{repo.lower()}'"
f" and ar.commit_sha = '{sha}' and j.status = 5) t),"
" 'labels', (select coalesce(json_agg(agent_labels), '[]'::json)"
" from action_runner where coalesce(deleted, 0) = 0));"
)
try:
out = subprocess.run(
["docker", "exec", "-i", "windy-git-db-1", "sh", "-c",
'psql -U "$POSTGRES_USER" -d gitea -At -v ON_ERROR_STOP=1'],
input=query, capture_output=True, text=True, check=True, timeout=30,
).stdout.strip()
got = json.loads(out or "{}")
runners = [set(json.loads(x or "[]")) for x in got.get("labels") or []]
return [
j for j in got.get("jobs") or []
if any(set(json.loads(j.get("runs_on") or "[]")) <= r for r in runners)
]
except (subprocess.SubprocessError, OSError, ValueError) as e:
print(f" {repo}: queued-job lookup skipped ({type(e).__name__})")
return []
def post_statuses(repo: str, sha: str) -> None: def post_statuses(repo: str, sha: str) -> None:
# Gitea caps a page at 50 (MAX_RESPONSE_ITEMS) whatever `limit` says, and a # Gitea caps a page at 50 (MAX_RESPONSE_ITEMS) whatever `limit` says, and a
# daily scheduled workflow can push a quiet main's runs off page 1. # daily scheduled workflow can push a quiet main's runs off page 1.
@@ -291,17 +242,6 @@ def post_statuses(repo: str, sha: str) -> None:
ctx = f"windy-git/{r['workflow_id'].removesuffix('.yml')}/{r['name']}" ctx = f"windy-git/{r['workflow_id'].removesuffix('.yml')}/{r['name']}"
if ctx not in latest or r["id"] > latest[ctx]["id"]: if ctx not in latest or r["id"] > latest[ctx]["id"]:
latest[ctx] = r latest[ctx] = r
# Queued jobs: `pending` where nothing newer has been picked up. A re-run
# queued behind an old failure must read pending, not the stale red.
for q in queued_jobs(repo, sha):
if NO_DAEMON_JOB.search(q["name"]):
continue
wf = q["workflow_id"].removesuffix(".yml")
if f"{wf}/{q['name']}" in NON_BLOCKING.get(repo, ()):
continue
ctx = f"windy-git/{wf}/{q['name']}"
if ctx not in latest or q["run_number"] > latest[ctx]["run_number"]:
latest[ctx] = {"id": 0, "status": "waiting", "run_number": q["run_number"]}
bad = invalid_workflows(repo, sha) bad = invalid_workflows(repo, sha)
if not (latest or bad): if not (latest or bad):
return return
@@ -343,37 +283,6 @@ def post_statuses(repo: str, sha: str) -> None:
print(f" {repo}@{sha[:7]} {ctx} = {state} -> {st}") print(f" {repo}@{sha[:7]} {ctx} = {state} -> {st}")
GUARD_CTX = "windy-git/compute-guard"
def post_compute_guard(repo: str, sha: str, default_branch: str, is_default_head: bool) -> None:
"""Windy Mind is the only door to AI compute: flag direct provider use (warn-only).
Non-fatal and never a fake OK: if the guard can't run, nothing is posted.
"""
try:
import compute_guard as cg # same directory; loaded lazily so the bridge never depends on it
findings = cg.check(repo, sha, default_branch, is_default_head)
except Exception as e: # noqa: BLE001 — the guard must never break CI signals
print(f" {repo}@{sha[:7]} compute-guard skipped ({type(e).__name__}: {str(e)[:80]})")
return
if findings is None:
return
state, desc, first = cg.status_for(findings, whole_tree=is_default_head)
st, existing = github("GET", f"/repos/{GH_OWNER}/{repo}/commits/{sha}/statuses?per_page=100")
for s in existing or []: # newest first: compare the latest guard status only
if s["context"] == GUARD_CTX:
if (s["state"], s.get("description")) == (state, desc):
return
break
url = (f"{PUBLIC}/{WG_OWNER}/{repo}/src/commit/{sha}/{first.path}#L{first.line}"
if first else f"{PUBLIC}/{WG_OWNER}/{repo}/src/commit/{sha}")
st, _ = github("POST", f"/repos/{GH_OWNER}/{repo}/statuses/{sha}",
{"state": state, "context": GUARD_CTX, "description": desc, "target_url": url})
print(f" {repo}@{sha[:7]} {GUARD_CTX} = {state} ({len(findings)} finding(s)) -> {st}")
def main() -> int: def main() -> int:
if not (GITEA_TOKEN and GITHUB_TOKEN): if not (GITEA_TOKEN and GITHUB_TOKEN):
sys.exit("GITEA_ADMIN_TOKEN and GITHUB_TOKEN are required") sys.exit("GITEA_ADMIN_TOKEN and GITHUB_TOKEN are required")
@@ -381,17 +290,13 @@ def main() -> int:
for repo in REPOS: for repo in REPOS:
try: try:
shas = sync_prs(repo) shas = sync_prs(repo)
default_branch, default_head = "main", None
st, br = github("GET", f"/repos/{GH_OWNER}/{repo}") st, br = github("GET", f"/repos/{GH_OWNER}/{repo}")
if st == 200: if st == 200:
default_branch = br["default_branch"] st, b = github("GET", f"/repos/{GH_OWNER}/{repo}/branches/{br['default_branch']}")
st, b = github("GET", f"/repos/{GH_OWNER}/{repo}/branches/{default_branch}")
if st == 200: if st == 200:
default_head = b["commit"]["sha"] shas.append(b["commit"]["sha"])
shas.append(default_head)
for sha in dict.fromkeys(shas): for sha in dict.fromkeys(shas):
post_statuses(repo, sha) post_statuses(repo, sha)
post_compute_guard(repo, sha, default_branch, sha == default_head)
except Exception as e: # one repo's failure must not hide the others' except Exception as e: # one repo's failure must not hide the others'
print(f" FAILED {repo}: {e}") print(f" FAILED {repo}: {e}")
failed = 1 failed = 1

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Re-run a PR's (or branch's) CI on Windy Git. Runs ON Veron 1.
#
# bash scripts/rerun_ci.sh <repo> <branch> <sha-prefix>
#
# Gitea 1.24 has NO rerun API; the web button needs a hub-SSO session as
# windyadmin, which is Grant's identity, so we don't use it. Instead: move the
# Windy Git branch back one commit, let the next sync force-push the GitHub head
# again, and Gitea fires an ordinary push / pull_request_sync event on the SAME
# commit. Every workflow on that event re-runs, not only the failed one.
#
# Safety: refuses unless the branch is exactly at <sha-prefix> (GitHub's head),
# never rewinds while a sync is running (a run already past this repo would
# not push it back), waits for a sync that STARTS after the rewind, and if the
# branch is not verifiably back at <sha-prefix> by the deadline, restores it
# itself, so Windy Git is never left behind GitHub.
set -euo pipefail
repo="${1:?repo}"; branch="${2:?branch}"; want="${3:?sha prefix}"
G="sudo docker exec -u git windy-git-gitea-1 git -C /data/git/repositories/windyadmin/${repo}.git"
head=$($G rev-parse "refs/heads/${branch}")
[[ "$head" == "$want"* ]] || { echo "refusing: ${branch} is at ${head:0:7}, not ${want}"; exit 1; }
parent=$($G rev-parse "${head}^")
# NOT `systemctl is-active`: the sync is Type=oneshot, which reads "activating"
# (exit 3) for its whole run, so is-active says "idle" mid-run.
busy() { case "$(systemctl show windygit-sync -p ActiveState --value)" in
activating|active|deactivating|reloading) return 0;; esac; return 1; }
while busy; do sleep 5; done
$G update-ref "refs/heads/${branch}" "$parent" "$head"
mark=$(awk '{print int($1*1000000)}' /proc/uptime)
echo "rewound ${repo}:${branch} ${head:0:7} -> ${parent:0:7}"
deadline=$(( $(date +%s) + 900 ))
until [ "$(systemctl show windygit-sync -p ExecMainStartTimestampMonotonic --value)" -gt "$mark" ] \
&& [ "$($G rev-parse "refs/heads/${branch}")" = "$head" ]; do
if [ "$(date +%s)" -ge "$deadline" ]; then
$G update-ref "refs/heads/${branch}" "$head" "$($G rev-parse "refs/heads/${branch}")" || true
echo "TIMEOUT: restored ${branch} to ${head:0:7} by hand; NO new run fired"; exit 1
fi
sleep 10
done
echo "restored by sync: ${branch} = ${head:0:7}"
sleep 5
~/bin/wg-q <<SQL
select ar.index, ar.workflow_id, ar.event, ar.status, to_char(to_timestamp(ar.created),'HH24:MI:SS')
from action_run ar join repository r on r.id = ar.repo_id
where r.name = '${repo}' and ar.commit_sha = '${head}' order by ar.id desc limit 6;
SQL

View File

@@ -82,11 +82,7 @@ done
# Jobs that name labels no runner has (ubuntu/macos/windows-latest) would wait # Jobs that name labels no runner has (ubuntu/macos/windows-latest) would wait
# forever and invisibly; cancel them after 30 min. Never fails the sync. # forever and invisibly; cancel them after 30 min. Never fails the sync.
# Both DB steps go through `docker exec`, which hangs outright while the host bash "$(dirname "$0")/cancel_unrunnable.sh" || log "janitor failed (non-fatal)"
# is in an IO stall (09-23: data2 SMR cliff wedged this sync for 10+ min and
# stopped mirroring + the bridge for every lane). They are optional; mirroring
# and the bridge are not. Bound them so a stuck exec costs one step, not the run.
timeout -k 10 120 bash "$(dirname "$0")/cancel_unrunnable.sh" || log "janitor failed or timed out (non-fatal)"
# Private repos can't run GitHub Actions; mirror their open PRs here so CI # Private repos can't run GitHub Actions; mirror their open PRs here so CI
# fires, and post the verdicts back to GitHub as commit statuses. # fires, and post the verdicts back to GitHub as commit statuses.
@@ -96,7 +92,7 @@ fi
# 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)" python3 "$(dirname "$0")/telemetry_emit.py" || log "telemetry emit failed (non-fatal)"
[[ "$FAILED" -ne 0 ]] && { log "COMPLETED WITH FAILURES"; exit 1; } [[ "$FAILED" -ne 0 ]] && { log "COMPLETED WITH FAILURES"; exit 1; }
log "all repos in step with GitHub" log "all repos in step with GitHub"

View File

@@ -20,7 +20,6 @@ from __future__ import annotations
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import time import time
@@ -70,100 +69,6 @@ def iso(epoch: float) -> str:
return datetime.fromtimestamp(epoch, UTC).isoformat().replace("+00:00", "Z") return datetime.fromtimestamp(epoch, UTC).isoformat().replace("+00:00", "Z")
# ---- push velocity: DETECT + ALERT ONLY (G3.4, 2026-09-23) -----------------
# `git push` goes straight to Gitea and never touches our API, so throttle.py
# cannot see it (NOT_ENFORCED_HERE). Gitea's own `action` table does record
# every push, so we read it here, emit `forge.push_velocity` when an account
# crosses a threshold, and let Telemetry Boss's detector page. Nothing here sits
# in the push path; nothing is ever refused (orchestrator, 09-23).
#
# Thresholds = the STANDARD-band bases from config.py (500 pushes/day; the
# force-push base of 10/day is used for ref deletes, the closest thing we can
# see). EI band multipliers are NOT applied: a platinum agent over 500/day is
# still flagged, for a human to look at, not blocked. Gitea records no
# "forced" flag, so force pushes cannot be told apart from pushes: named, not
# guessed.
PV_RULES = ( # (rule, row key, window_s, threshold)
("pushes_1h", "p1h", 3600, 60),
("pushes_24h", "p24h", 86400, 500),
("ref_deletes_24h", "d24h", 86400, 10),
)
# The GitHub -> Windy Git sync pushes as windyadmin every 5 min, by design.
PV_EXEMPT = {"windyadmin"}
# Gitea op_type: 5 commit push, 9 tag push, 16 tag delete, 17 branch delete.
# One action row per WATCHER is written for each push; user_id = act_user_id
# keeps exactly the actor's own copy.
PV_QUERY = """
select a.act_user_id as uid, u.lower_name as login,
(select el.external_id from external_login_user el
where el.user_id = a.act_user_id order by el.external_id limit 1) as wid,
count(*) filter (where a.op_type in (5, 9) and a.created_unix > {h1}) as p1h,
count(*) filter (where a.op_type in (5, 9)) as p24h,
count(*) filter (where a.op_type in (16, 17)) as d24h,
count(distinct a.repo_id) as repos
from action a join "user" u on u.id = a.act_user_id
where a.created_unix > {h24} and a.user_id = a.act_user_id
and a.op_type in (5, 9, 16, 17)
group by 1, 2"""
def passport_from_login(login: str) -> str | None:
"""agent-et26abcd1234 -> ET26-ABCD-1234 (repos.py _owner_login, reversed)."""
m = re.fullmatch(r"agent-([a-z0-9]{4})([a-z0-9]{4})([a-z0-9]{4})", login)
return "-".join(g.upper() for g in m.groups()) if m else None
def push_velocity_events(rows: list[dict], now: float, alerted: dict) -> tuple[list[dict], dict]:
"""(events, alerted') — one row per account per rule per window while over.
`alerted` maps "<uid>:<rule>" -> epoch of the last row. An account still over
the line is re-reported once per window, not every 5 minutes; one that drops
back under is forgotten, so a later burst reports again.
"""
events, keep = [], {}
for r in rows:
login = str(r["login"])
if login in PV_EXEMPT:
continue
agent = login.startswith("agent-")
for rule, key, window, limit in PV_RULES:
n = int(r[key])
if n <= limit:
continue
k = f"{r['uid']}:{rule}"
last = alerted.get(k)
if last is not None and now - float(last) < window:
keep[k] = last
continue
keep[k] = now
ev = {
"ts": iso(now),
"platform": PLATFORM,
"service": "forge",
"event_type": "forge.push_velocity",
"metadata": {
"rule": rule,
"window_s": window,
"count": n,
"threshold": limit,
"repos": int(r["repos"]),
"gitea_user_id": int(r["uid"]),
},
}
# Actor rule (telemetry UPDATE 2): agent/human rows MUST carry an
# actor_id. Humans sign in to the forge only via Windy SSO, so the
# external login id IS their windy_identity_id. No id we can prove
# -> actor_type system + metadata.caller, never an invented id (I-12).
actor_id = passport_from_login(login) if agent else (r.get("wid") or None)
if actor_id:
ev["actor_type"], ev["actor_id"] = ("agent" if agent else "human"), str(actor_id)
else:
ev["actor_type"] = "system"
ev["metadata"]["caller"] = "unknown"
events.append(ev)
return events, keep
def main() -> int: def main() -> int:
dry = "--dry-run" in sys.argv dry = "--dry-run" in sys.argv
state = load_state() state = load_state()
@@ -277,20 +182,6 @@ def main() -> int:
} }
) )
# Isolated: a failing push-velocity query must never cost the ci.run rows.
pv_alerted = state.get("pv_alerted", {})
try:
pv_rows = sql(PV_QUERY.format(h1=int(now) - 3600, h24=int(now) - 86400))
pv_events, pv_alerted = push_velocity_events(pv_rows, now, pv_alerted)
except (subprocess.CalledProcessError, ValueError, KeyError) as e:
print(f"[telemetry] push velocity check FAILED (non-fatal): {type(e).__name__}")
pv_events = []
for e in pv_events:
m = e["metadata"]
print(f"[telemetry] WARNING push velocity: gitea user {m['gitea_user_id']} "
f"{m['rule']} = {m['count']} > {m['threshold']}")
events += pv_events
# ci.job_cancelled: spooled by the janitor (cancel_unrunnable.sh), one JSON per job. # ci.job_cancelled: spooled by the janitor (cancel_unrunnable.sh), one JSON per job.
spool = os.environ.get("JANITOR_SPOOL", "/var/lib/windy-git/janitor-cancelled.jsonl") spool = os.environ.get("JANITOR_SPOOL", "/var/lib/windy-git/janitor-cancelled.jsonl")
spooled = 0 spooled = 0
@@ -373,7 +264,7 @@ def main() -> int:
with open(STATE + ".tmp", "w") as f: with open(STATE + ".tmp", "w") as f:
json.dump( json.dump(
{"last_fin": new_fin, "last_id": new_id, "last_ts": now, {"last_fin": new_fin, "last_id": new_id, "last_ts": now,
"quarantined_unreported": quarantined, "pv_alerted": pv_alerted}, "quarantined_unreported": quarantined},
f, f,
) )
os.replace(STATE + ".tmp", STATE) os.replace(STATE + ".tmp", STATE)