Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/host-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A missing tool is a host gap, not a repo problem. Install it and re-run, rather

### Where a Tool Comes From, and How Old It May Be

Presence is the weaker half of this contract. Both host defects this fleet has actually hit are **version** facts on a tool that is installed, answers `--version`, and looks healthy, so the table above cannot see either one. [`spec/host-tools.json`][host-tools] carries the floors as data and records the defect each one encodes, and [`scripts/host_gate.py`][host-gate] reads it. A floor exists only where a version is known to break a documented procedure, so most entries carry none, deliberately: a floor nobody can justify becomes a host failure nobody can act on.
Presence is the weaker half of this contract. Both host defects this fleet has actually hit are **version** facts on a tool that is installed, answers `--version`, and looks healthy, so the table above cannot see either one. [`spec/host-tools.json`][host-tools] carries the floors as data and records the defect each one encodes, and [`scripts/host_gate.py`][host-gate] reads it. A floor is one of two kinds and names its own kind in the `why` it carries. A **measured** floor sits above a version known to break a documented procedure, which is what both `gh` and `git-restore-mtime` carry. A **target** floor names the version the repo's own toolchain is configured for, which is what `python3` carries at 3.13, where `pyproject.toml` sets ruff and mypy to that version, so a lower interpreter is unverified rather than known broken and the entry says exactly that. Everything else carries none, deliberately: a floor nobody can justify becomes a host failure nobody can act on.

**`gh` must not come from the distribution's package on Linux.** This is the one place this document names a source, because here the source *is* the requirement rather than a convenience. The GitHub CLI maintainers state that the community-distributed `2.45.x` / `2.46.x` is **broken by deprecated GitHub APIs**, so install from the official apt repository at [cli.github.com][cli-install-link] and upgrade from there. Both `gh` limitations recorded in [`OPERATIONS.md`][operations] were observed on a host carrying a distribution `gh 2.46.0`, and both are the deprecation class that note describes. On **Windows** `winget` tracks upstream releases, and on macOS Homebrew does, so neither raises this hazard and neither needs a note of its own.

Expand Down
8 changes: 4 additions & 4 deletions host-setup/agent-safety/gh-write-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@
| (?:label|secret|variable|ruleset)\s+(?:create|delete|edit|set)
| gist\s+(?:create|edit|delete)
)\b""",
re.X,
re.VERBOSE,
)
_GH_API = re.compile(r"\bgh\s+api\b")
_EXPLICIT_WRITE_METHOD = re.compile(r"(?:--method|-X)\s+(?:POST|PUT|PATCH|DELETE)\b", re.I)
_EXPLICIT_WRITE_METHOD = re.compile(r"(?:--method|-X)\s+(?:POST|PUT|PATCH|DELETE)\b", re.IGNORECASE)
# A gh api call with a field flag defaults to POST even without -X, so it is a write.
_API_FIELD_FLAG = re.compile(r"(?:^|\s)(?:-f|-F|--field|--raw-field|--input)\b")
_GRAPHQL = re.compile(r"\bgh\s+api\b.*\bgraphql\b", re.S)
_GRAPHQL = re.compile(r"\bgh\s+api\b.*\bgraphql\b", re.DOTALL)
_MUTATION = re.compile(r"\bmutation\b")
# Loose pre-filter only: matches `git` before `push` even with global options between them
# (git -C <dir> push). _push_arg_lists is the accurate arbiter that confirms an executable push.
_GIT_PUSH = re.compile(r"\bgit\b.*?\bpush\b", re.S)
_GIT_PUSH = re.compile(r"\bgit\b.*?\bpush\b", re.DOTALL)

# --- Bypass-of-branch-rule detectors (Rule 4) --------------------------------------------------------
# A git operation is denied when it would only succeed by bypassing an active branch rule.
Expand Down
2 changes: 1 addition & 1 deletion host-setup/agent-safety/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def reject(where, held, want):
newline, existing = "\n", ""
for marker, filename in blocks:
snippet = (HERE / filename).read_text(encoding="utf-8").strip()
block_re = re.compile(rf"<!-- {marker} v\d+ start -->.*?<!-- {marker} v\d+ end -->", re.S)
block_re = re.compile(rf"<!-- {marker} v\d+ start -->.*?<!-- {marker} v\d+ end -->", re.DOTALL)
if block_re.search(existing):
existing, action = block_re.sub(lambda _: snippet, existing), "updated"
else:
Expand Down
2 changes: 1 addition & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ A stale-backticked-path check was built and **rejected**: a template repo legiti

The host contract in [`docs/host-setup.md`][host-setup] as a check, reading the tool floors declared in [`spec/host-tools.json`][host-tools]. It exists because presence is the weaker half of that contract: both host defects this fleet has hit are version facts on a tool that is installed, answers `--version`, and looks healthy.

**A floor is declared only where a version is known to break a documented procedure**, and each one records that defect rather than a preference. Two exist today. A distribution `gh` in the `2.45.x` / `2.46.x` range is named broken by the GitHub CLI maintainers, and both `gh` limitations in [`OPERATIONS.md`][operations] were observed on one. A `git-restore-mtime` before `2025.08` calls `git whatchanged`, which current `git` refuses, so it restores nothing, prints its ordinary statistics and **exits 0**. Everything else is presence-only, which is deliberate, since a floor nobody can justify becomes a host failure nobody can act on.
**A floor is either measured or a target, and its `why` says which**, since a host failing one has a defect to point at where a host failing the other does not. A measured floor records the defect rather than a preference, and two exist today. A distribution `gh` in the `2.45.x` / `2.46.x` range is named broken by the GitHub CLI maintainers, and both `gh` limitations in [`OPERATIONS.md`][operations] were observed on one. A `git-restore-mtime` before `2025.08` calls `git whatchanged`, which current `git` refuses, so it restores nothing, prints its ordinary statistics and **exits 0**. Everything else is presence-only, which is deliberate, since a floor nobody can justify becomes a host failure nobody can act on.

The three states a tool can be in are kept apart because their remedies differ: **absent** means install it, **unreadable** means the declared pattern is wrong and the fix is in this repo rather than on the host, and **read** means the floor applies. A probe that runs and exits non-zero is not an answer, which is what separates a tool that is missing from one this file cannot parse.

Expand Down
7 changes: 6 additions & 1 deletion scripts/host_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
below. 2 = the declaration itself could not be read, which is a defect here rather than on the host.
"""
from __future__ import annotations
import argparse, json, re, subprocess, sys

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path

SPEC = Path(__file__).resolve().parent.parent / 'spec' / 'host-tools.json'
Expand Down
18 changes: 13 additions & 5 deletions scripts/pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,16 @@
GOVERNANCE.md "Repository Boundaries and Write Safety" for the rules `reply` enforces.
"""
from __future__ import annotations
import argparse, io, json, re, subprocess, sys, tarfile, time
from datetime import datetime, timezone

import argparse
import io
import json
import re
import subprocess
import sys
import tarfile
import time
from datetime import UTC, datetime
from pathlib import Path

REVIEWER = 'copilot-pull-request-reviewer'
Expand Down Expand Up @@ -978,7 +986,7 @@ def checks_unreadable(pr: dict) -> bool:
than as this reading having failed. A silent narrowing is the failure mode this whole script
is built against, and it does not get an exception for its own newest field.
"""
return bool(((pr.get('commits') or {}).get('nodes') or [])) and not head_commit(pr)
return bool((pr.get('commits') or {}).get('nodes') or []) and not head_commit(pr)


def checks_tally(nodes: list[dict]) -> tuple[int, int]:
Expand Down Expand Up @@ -1056,7 +1064,7 @@ def digest(owner: str, repo: str, num: int, seen: set[str] | None = None,
"""
pr = gql(Q_FULL, owner, repo, num) if pr is None else pr
stalled = stall_of(owner, repo, num, pr) if stalled is None else stalled
now = datetime.now(timezone.utc) if now is None else now
now = datetime.now(UTC) if now is None else now
head = pr['headRefOid']
revs = reviewer_nodes(pr, 'reviews')
# `revs` is every round and `on_head` is the ones that reviewed this commit.
Expand Down Expand Up @@ -1664,7 +1672,7 @@ def main(argv: list[str] | None = None) -> int:
# The stall is re-read here rather than carried out of the loop.
# A request picked up since that reading would still report as picked up by nothing.
stalled = stall_of(owner, repo, a.number, final)
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
# Parsed here and handed down, so the digest and the exit code share one read of the rollup.
# Deriving the stuck shapes from that list costs no parse, which is what was doubled.
checks = check_nodes(final)
Expand Down
9 changes: 8 additions & 1 deletion scripts/prose_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
Exit 1 if any violation is found. Read-only, never edits.
"""
from __future__ import annotations
import argparse, io, re, subprocess, sys, tokenize, unicodedata

import argparse
import io
import re
import subprocess
import sys
import tokenize
import unicodedata
from pathlib import Path
from typing import NamedTuple, TypedDict

Expand Down
10 changes: 7 additions & 3 deletions scripts/repo_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@
actionlint, editorconfig-checker, spec/validate.py).
"""
from __future__ import annotations
import argparse, re, subprocess, sys
from pathlib import Path, PurePosixPath

import argparse
import re
import subprocess
import sys
from fnmatch import fnmatch
from pathlib import Path, PurePosixPath

# GOVERNANCE.md documents exactly one floating-ref exception.
SHA_EXCEPTIONS = {'dotnet/nbgv'}
USES = re.compile(r'^\s*-?\s*uses:\s*(?P<ref>[^\s#]+)', re.M)
USES = re.compile(r'^\s*-?\s*uses:\s*(?P<ref>[^\s#]+)', re.MULTILINE)
PIN = re.compile(r'^[0-9a-f]{40}$')
WORKFLOW = re.compile(r'workflows/.*\.ya?ml$')
# What `gh` prints when GitHub answered, as opposed to when nothing was reached at all.
Expand Down
26 changes: 22 additions & 4 deletions scripts/test_host_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
neither was visible from reading the code.
"""
from __future__ import annotations

import json
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import host_gate # noqa: E402
import host_gate
Comment thread
ptr727 marked this conversation as resolved.


def tool(name, minimum=None, required=True, probes=None, pattern=r'v(\d+(?:\.\d+)*)', **extra):
Expand Down Expand Up @@ -403,10 +404,27 @@ def test_a_floor_carries_a_source_so_the_finding_names_a_remedy(self):
if t['minimum'] is not None:
self.assertTrue(t.get('source'), f'{t["name"]} declares a floor and no source to install from')

def test_the_two_known_defects_are_the_declared_floors(self):
"""A floor exists only where a defect is known, so this asserts the set rather than a count."""
def test_the_declared_floors_are_the_ones_with_a_stated_reason(self):
"""A floor is justified or it is not there, so this asserts the set rather than a count.

Two kinds qualify. A measured floor sits above a version known to break a documented
procedure, and a target floor names the version the repo's toolchain is configured for.
The set is asserted so that adding a floor is a deliberate edit here rather than a silent
one in the data, which is what caught the python3 floor being added without this line.
"""
floors = {t['name'] for t in self.data['tools'] if t['minimum'] is not None}
self.assertEqual(floors, {'gh', 'git-restore-mtime'})
self.assertEqual(floors, {'gh', 'git-restore-mtime', 'python3'})

def test_a_target_floor_says_so_rather_than_implying_a_defect(self):
"""The python3 floor is a target, so its `why` has to distinguish itself from a measured one.

A reader who takes a target floor for a measured one goes looking for a defect report that
does not exist, which is the failure the two-kinds wording was written to prevent.
"""
python3 = next(t for t in self.data['tools'] if t['name'] == 'python3')
self.assertEqual(python3['minimum'], '3.13')
self.assertIn('target', python3['why'])
self.assertIn('unverified rather than known broken', python3['why'])


if __name__ == '__main__':
Expand Down
15 changes: 11 additions & 4 deletions scripts/test_pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
Run as `python3 scripts/test_pr_review.py`, or under `python3 -m unittest discover -s scripts`.
"""
from __future__ import annotations
import contextlib, io, json, re, subprocess, sys, unittest
from datetime import datetime, timedelta, timezone

import contextlib
import io
import json
import re
import subprocess
import sys
import unittest
from datetime import UTC, datetime, timedelta
from itertools import count
from pathlib import Path
from unittest import mock
Expand Down Expand Up @@ -105,7 +112,7 @@ def thread(tid: str, resolved: bool = False, login: str = pr_review.REVIEWER,


# A fixed clock, so a case holds a check at a known age instead of at whatever the suite runs at.
NOW = datetime(2026, 8, 6, 17, 0, 0, tzinfo=timezone.utc)
NOW = datetime(2026, 8, 6, 17, 0, 0, tzinfo=UTC)


def ago(seconds: int) -> str:
Expand All @@ -121,7 +128,7 @@ def ago(seconds: int) -> str:

def real_ago(seconds: int) -> str:
"""A timestamp `seconds` before the real clock, for the `wait` path, which reads that clock."""
return (datetime.now(timezone.utc) - timedelta(seconds=seconds)).strftime('%Y-%m-%dT%H:%M:%SZ')
return (datetime.now(UTC) - timedelta(seconds=seconds)).strftime('%Y-%m-%dT%H:%M:%SZ')


def check(name: str = 'Check pull request workflow status job', status: str = 'COMPLETED',
Expand Down
14 changes: 11 additions & 3 deletions scripts/test_prose_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
Run as `python3 scripts/test_prose_lint.py`, or under `python3 -m unittest discover -s scripts`.
"""
from __future__ import annotations
import contextlib, io, json, re, subprocess, sys, tempfile, unittest

import contextlib
import io
import json
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

Expand Down Expand Up @@ -129,14 +137,14 @@ class TestGovernanceCoupling(unittest.TestCase):

def setUp(self) -> None:
self.doc = GOVERNANCE.read_text(encoding='utf-8')
section = re.search(r'^### Character Set$(.*?)^### ', self.doc, re.M | re.S)
section = re.search(r'^### Character Set$(.*?)^### ', self.doc, re.MULTILINE | re.DOTALL)
if section is None:
self.fail('the Character Set heading moved, so the parse is blind')
self.section = section.group(1)

def tier_codepoints(self, label: str) -> set[int]:
"""Codepoints named in one tier's bullet, read out of the rule text itself."""
m = re.search(rf'^- \*\*Tier {label},(.*?)(?=^- \*\*)', self.section, re.M | re.S)
m = re.search(rf'^- \*\*Tier {label},(.*?)(?=^- \*\*)', self.section, re.MULTILINE | re.DOTALL)
if m is None:
self.fail(f'the Tier {label} bullet moved, so the parse is blind')
return {int(h, 16) for h in re.findall(r'U\+([0-9A-Fa-f]{4})', m.group(1))}
Expand Down
10 changes: 9 additions & 1 deletion scripts/test_repo_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
Run as `python3 scripts/test_repo_gate.py`, or under `python3 -m unittest discover -s scripts`.
"""
from __future__ import annotations
import contextlib, io, re, shutil, subprocess, sys, tempfile, unittest

import contextlib
import io
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

Expand Down
Loading