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
8 changes: 4 additions & 4 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: ed22133c-9fa7-43fe-8968-e883cf5c1247
generation_id: f664572a-7dc0-499b-9a19-46357300068f
openapi_spec_hash: 28504532afa0c3f477f005e641ec7d6d
openapi_transformed_spec_hash: 13422b63a47a5d25533cf55573de7632
config_hash: 4446d9cc8e7a00d29ce72282aa5616f9
codegen_sha: 60fe0b2314d2370d16ef526cebfd5aa058962d68
codegen_hash: e20a23b060bd9cb27f5b9e51b6bff14be4f845d2585d0ef54dfb6693747439b5
public_codegen_sha: 63178f1c3de0fdf686ebb74b4233a6dc7b8b6eaf
codegen_sha: 46b4c0fbaf91c9d178f4b29d2fed8a03794b5662
codegen_hash: 753430c43af080e5995298094195c43f14ec9db4b146a14c7e33bf2c2d12df8b
public_codegen_sha: 89db964dcda3fa55c381ce03108f5d46b61279eb
388 changes: 366 additions & 22 deletions .github/workflows/castiron-custom-code-comment.yml

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions .github/workflows/castiron-custom-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ name: Castiron custom code
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, edited]
pull_request_review:
types: [submitted, edited, dismissed]
# Notify the existing trusted handler for queue candidates.
merge_group:
types: [checks_requested]
Expand All @@ -16,11 +18,11 @@ concurrency:
cancel-in-progress: false

env:
REPORTER_SHA256: 6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880
REPORTER_SHA256: a52a1aabf4047f4c20e96d1d788c25a4aa78c1987b4fa73a13d22c92ef213a3f

jobs:
queue-signal:
if: github.event_name == 'merge_group'
if: github.event_name == 'merge_group' || github.event_name == 'pull_request_review'
runs-on: ubuntu-latest
timeout-minutes: 1
permissions: {}
Expand Down
73 changes: 68 additions & 5 deletions scripts/castiron/CUSTOM_CODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ and human review, and [AGENTS.md](../../AGENTS.md#custom-code-budget) for agent

## What is counted

Repositories may separately activate `"ratchet": "go"` in their policy after
this checker reaches trusted main. Activation must be a human-reviewed,
policy-only PR; SDK promotion does not carry policy edits. Keep the existing
`max_custom_patch_lines` ceiling.

The Go ratchet rejects net growth in generated-owned custom patch additions
plus deletions **and physical lines of handwritten Go** compared with the
captured live target. It includes helpers, tests, comments, blank lines, nested
modules, and SDK-owned CI Go retained in private checkpoints. Non-text and
nonregular handwritten Go fail validation. Both revisions use their own verified
generated snapshots, read as Git objects without executing candidate code.

Stale PRs compare with the live target, not their historical merge base. Merge
queues check the complete combined candidate. Every landed reduction becomes
the next baseline automatically. An activated ratchet cannot be disabled;
raising the independent patch-only ceiling does not restore retired headroom.
Existing policy isolation, human review, and snapshot ownership rules still apply.

The budget is **added lines plus deleted lines in the entire current custom patch**
against the candidate's verified pure-generated snapshot. It is not the PR's diff
against main, and additions and deletions never cancel. Replacing 500 generated
Expand Down Expand Up @@ -188,14 +206,34 @@ PR targeting next; preserve the existing budget/schema fields:

The SHA must belong to that same repository's authenticated main first-parent
history and support this pin protocol. Selection reads the protected target
base, never the candidate. Updates require an exact-head human approving review
from a repository owner, member, or collaborator, in addition to ordinary
CODEOWNERS and required reviews. Bot reviews, review-body markers, stale reviews,
and dismissed approvals do not authorize activation.
base, never the candidate. Pin-only updates and initial pin adoption use ordinary
CODEOWNERS and required reviews when the pin does not roll back to an older main
checker, the effective pinned budget stays the same or decreases, and its schema,
strictness and ratchet mode stay unchanged. Initial adoption compares checker age
with authenticated bootstrap main; later updates compare with the current protected
target pin and effective policy, even for stale PRs. A
checker rollback, an effective pinned-budget increase or mode transition, a local numeric budget increase, or Go
ratchet activation still requires an exact-head human approving review from a
repository owner, member, or collaborator. Bot reviews, stale reviews and dismissed
approvals do not authorize those changes.

For those human-required changes, the approving reviewer must explicitly attest that
they personally performed the human review. The entire review body must be the
following sentence, substituting the exact full current head SHA:

```text
I personally reviewed and approve this policy change as a human: <40-character head SHA>
```

Blank reviews, ordinary code-review text, quoted attestations, and attestations for
an earlier head do not qualify. Agents must never submit this attestation, even
through a human account. This is an explicit reviewer attestation, not a claim that
GitHub account type or team membership proves who operated the account. Ordinary
SDK code reviews continue to accept automation under their normal repository gates.

For initial adoption only, a base with no pin may accept an exact policy-only
pin-initialization PR under authenticated current-main checker/policy, with the
same human review requirement. The proposed pin is validated but never executed
same effective-budget and mode review requirements. The proposed pin is validated but never executed
for its own initialization. A malformed base pin cannot enter this path; other
changes without a base pin fail closed. Ordinary pin updates execute under the
previous approved version and cannot include SDK/tooling changes.
Expand All @@ -215,3 +253,28 @@ immediately before publication. Dismissed, replaced, stale, automated or missing
approvals fail closed even if ordinary automated approvals keep the queue live.
The publisher consumes the selector's versioned review rules, with cross-runtime
predicate fixtures; candidate artifacts never supply authorization or outcomes.

### Review changes after publication

The unprivileged reporter workflow signals submitted, edited and dismissed reviews.
For a current next PR changing the policy file, the trusted main handler marks its
required budget statuses and affected live queue statuses pending, then requests
normal reporter reruns. Only the ordinary authenticated evaluator may restore
success; the review signal never selects executable code, approval rules or an
arbitrary status target. Fork runs with empty PR lists use authenticated commit
associations and open-PR lookup, retaining live head/merge binding. The handler
marks each authenticated snapshot pending before slow run pagination, then retries
discovery if the PR or queue changes. Pending statuses on superseded affected
SHAs cannot authorize a merge; observed successes stay revoked even if a later
lookup fails. Reporter reruns must resolve to this same PR under the canonical
main-preference rule, not merely share its head SHA. Missing or ambiguous reporter
runs remain pending. Status writers share a serialization group.

Retain native required approving reviews and CODEOWNERS at merge time. Review-event
delivery and reevaluation are asynchronous; this mechanism is not an atomic human
approval check at merge. In particular, automated reviews using human accounts do
not become human policy authorization. Do not weaken native gates or treat this
handler as their replacement. A failed invalidation or rerun must be investigated;
missing reporter runs remain pending until the normal trusted run is requested.
Install and validate the review signal on the protected target before activating
next policy checks, including actual PR and queue revocation after dismissal.
135 changes: 102 additions & 33 deletions scripts/castiron/custom_code_budget.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def strict_policy(repo: Path, revision: str) -> bool:
return cast(int, read_policy(repo, revision)["schema_version"]) == 2


def go_ratchet(repo: Path, revision: str) -> bool:
return cast(bool, read_policy(repo, revision).get("ratchet") == "go")


MANIFEST = b"scripts/castiron/generated-files.json"
# Accept the relocated tooling only from the verified checkpoint. Deploy this
# reader before changing the producer so trusted-main checks can validate moves.
Expand Down Expand Up @@ -266,6 +270,8 @@ def check_isolation(repo: Path, base: str, head: str) -> int | None:
return None
if strict_policy(repo, base) and not strict_policy(repo, head):
raise ValueError("strict generated-file ownership cannot be downgraded")
if go_ratchet(repo, base) and not go_ratchet(repo, head):
raise ValueError("Go customization ratchet cannot be disabled")
if paths != {POLICY.encode()}:
raise ValueError(ISOLATION_MESSAGE)
return proposed
Expand All @@ -290,6 +296,40 @@ def outcome(state: str, description: str) -> dict[str, str]:
return {"state": state, "description": description}


def go_customization(repo: Path, revision: str, measured: dict[str, Any]) -> dict[str, Any]:
"""Count patches plus physical handwritten Go, using only verified Git objects."""
added, removed = count_custom_lines(measured)
# Use the reporter's ownership view. Private snapshots also retain SDK-owned
# CI files excluded from that view; those Go files are still handwritten.
generated = report.tree_entries(repo, measured["after"]["commit"])
handwritten = {
path: entry
for path, entry in report.tree_entries(repo, revision, include_workflows=True).items()
if path.endswith(b".go") and path not in generated
}
for path, entry in handwritten.items():
if entry.mode not in {b"100644", b"100755"}:
raise ValueError(f"nonregular handwritten Go: {path!r}")
blobs = report.read_blobs(repo, handwritten)
files: list[dict[str, Any]] = []
for path, entry in sorted(handwritten.items()):
content = blobs[entry.oid]
content.decode("utf-8")
if b"\0" in content:
raise ValueError(f"non-text handwritten Go: {path!r}")
lines = content.count(b"\n") + int(bool(content) and not content.endswith(b"\n"))
files.append({"path": path.decode("utf-8"), "lines": lines})
handwritten_lines = sum(file["lines"] for file in files)
return {
"revision": revision,
"generated": measured["after"],
"patch_lines": added + removed,
"handwritten_lines": handwritten_lines,
"handwritten_files": files,
"total": added + removed + handwritten_lines,
}


def evaluate(
repo: Path,
base: str,
Expand Down Expand Up @@ -362,11 +402,38 @@ def evaluate(
result["checked_limit"] = checked_limit
state = "success" if total <= checked_limit else "failure"
description = f"+{added} / -{removed} = {total} custom lines; limit {checked_limit}."
if any(go_ratchet(repo, revision) for revision in (policy, base, head)):
# The report's historical merge base is for presentation only. Measure
# the captured live target against its own hash-verified snapshot.
baseline, _ = report.build_report(
repo,
base,
base,
public=public,
fetch=fetch,
require_head_hash=True,
target_branch=target_branch,
)
before = go_customization(repo, base, baseline)
after = go_customization(repo, head, measured)
delta = after["total"] - before["total"]
result["ratchet"] = {"kind": "go", "before": before, "after": after, "delta": delta}
description += (
f" Go customization {before['total']} -> {after['total']} (delta {delta:+d})."
)
if delta > 0:
state = "failure"
if go_ratchet(repo, base) and not go_ratchet(repo, head):
state = "failure"
description += " Go customization ratchet cannot be disabled."
# Main's strict policy applies even if the candidate tries to remove it.
# A proposed activation must itself satisfy the stricter contract.
if strict_policy(repo, policy) or strict_policy(repo, base) or strict_policy(repo, head):
violations = verify_generated(
repo, measured["after"]["commit"], head, previous=measured["before"]["commit"]
repo,
measured["after"]["commit"],
head,
previous=(measured["before"] or measured["adoption_baseline"])["commit"],
)
result["strict_violations"] = violations
if violations:
Expand Down Expand Up @@ -512,24 +579,23 @@ def github_evaluate(
f"{branch} must require a merge queue before activating the budget gate"
)
page += 1
if branch == "next":
# This receipt is created locally by the trusted-main selector before the
# pinned checkout. It must never come from candidate workflow artifacts.
if (
selection is None
or type(selection.get("schema_version")) is not int
or any(
selection.get(key) != value
for key, value in {
"schema_version": 1,
"target_branch": branch,
"base_sha": base,
"head_sha": head,
"policy_sha": main,
}.items()
)
):
raise ValueError("trusted next policy selection does not match evaluation")
# This receipt is created locally by the trusted-main selector before the
# pinned checkout. It must never come from candidate workflow artifacts.
if (
selection is None
or type(selection.get("schema_version")) is not int
or any(
selection.get(key) != value
for key, value in {
"schema_version": 1,
"target_branch": branch,
"base_sha": base,
"head_sha": head,
"policy_sha": main,
}.items()
)
):
raise ValueError("trusted policy selection does not match evaluation")
measurement = None
if trusted_report_dir is not None:
# Only the preceding main-branch step may supply this directory. Never
Expand All @@ -556,19 +622,6 @@ def github_evaluate(
report.git(
repo, "fetch", "--quiet", "--no-tags", "origin", *dict.fromkeys((main, base, head))
)
if branch == "next":
assert selection is not None
base_pin = read_policy(repo, base).get("next_policy")
if base_pin is None:
if selection.get("bootstrap") is not True or changed_paths(repo, base, head) != {
POLICY.encode()
}:
raise ValueError("unreviewed next policy initialization")
elif selection.get("bootstrap") is not False or base_pin != {
"repository": repository,
"main_sha": main,
}:
raise ValueError("trusted next base pin changed; rerun policy selection")
pull_heads: list[str] | None = None
if run["event"] == "merge_group":
if report.git(repo, "merge-base", base, head).decode().strip() != base:
Expand All @@ -585,6 +638,22 @@ def github_evaluate(
pull_heads.append(candidate)
if not pull_heads:
raise ValueError("cannot verify merge-group PR membership; retry the current group")
if branch == "next":
assert selection is not None
base_pin = read_policy(repo, base).get("next_policy")
if base_pin is None:
# evaluate() checks each verified queue constituent for policy isolation.
if (
selection.get("bootstrap") is not True
or "next_policy" not in read_policy(repo, head)
or (pull_heads is None and changed_paths(repo, base, head) != {POLICY.encode()})
):
raise ValueError("unreviewed next policy initialization")
elif selection.get("bootstrap") is not False or base_pin != {
"repository": repository,
"main_sha": main,
}:
raise ValueError("trusted next base pin changed; rerun policy selection")
result, patch = evaluate(
repo,
base,
Expand All @@ -602,7 +671,7 @@ def github_evaluate(

def main() -> int:
if sys.argv[1:2] == ["select-policy"]:
return policy.main(sys.argv[1:])
return cast(int, policy.main(sys.argv[1:]))
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
generated = commands.add_parser("verify-generated")
Expand Down
Loading
Loading