The acceptance harness pinned a forge client that has never existed in any
commit or pod (/opt/coordinator/gitea_api.py, digest f0943db4..., GIT/POST
grammar, an askpass helper). Every Gitea-backed check was therefore
unrunnable as merged. Point the harness at the credential-isolated SCM
broker client that actually ships in the agent pod.
- policy: GITEA_CLIENT=/opt/scm/gitea_api.py; trust /opt/scm/ instead of
the phantom /opt/coordinator/; admit the client's real grammar
(`read <api-path>`, exactly one path) with the same atlas/titan-iac pin
and dot-segment rejection; bare HTTP methods are refused in every mode.
The armed POST/PATCH/DELETE windows remain but are documented as
deferred: the deployed client cannot execute them.
- exec: pin the client digest to the sha256 of
services/hermes/scm-common/scripts/gitea_api.py — the exact file the
hermes-scm-boundary-v2 ConfigMap mounts at /opt/scm/gitea_api.py — so
the pin is derivable from merged source and equal to the deployed
client. gitea_api.py gains a narrow /api/v1/user identity read in
_authorize_read (see below), so the pin is the NEW source hash
76efd16dedbeb74425b12fbbdbfaa391854771292077e0463bf22706855ae6dc.
Drop the dangling GIT_ASKPASS (no helper exists; broker git needs
none) and swap /opt/coordinator for /opt/scm in SAFE_PATH.
- checks: all forge/baseline/lineage probes use (client, "read", path).
The SELF-vantage identity checks now truthfully assert the *broker's*
forge identity (the only one the platform can exercise) is not an
administrator and holds push-scoped, non-administrative repository
authority; the administrative-route check asserts the broker read
allowlist's live refusal of branch_protections. The remote-main step
keeps `origin` (the broker remote exists only in pool workspaces and
the broker origin is cluster-local); its rationale now tells the
operator to ensure origin fetchability.
- gitea_api.py/_authorize_read: allow exactly `/api/v1/user` (no query,
no sibling routes) as operation "identity" so the harness can prove
the broker identity is not an administrator. The broker imports the
same module, so one reviewed edit covers both sides of the boundary.
- rules: DENIAL_MARKERS now match the client's real refusal lines
("SCM broker request failed with HTTP 400/403" and the no-credential
rejection) and drop "gitea api returned http 403", which the client
never emits; a broker 404 is deliberately not denial evidence.
- ephemeral: index/verification reads use the real grammar; manual
cleanup guidance now says close/delete require operator forge
credentials (the client exposes no mutation besides create-draft);
armed mode is documented as deferred until the probes are rebuilt on
the broker's bounded mutation surface.
- docs: broker vantage/evidence section, operator prerequisites (broker
healthy, no /vault/secrets/gitea-token anywhere on the harness path,
current ConfigMap mount, operator-side client + origin fetchability),
armed-mode deferral.
- tests: read-grammar accepted / GET refused in every mode, /opt/scm
attestation pin proven equal to the merged source digest, real
denial-marker matching, /api/v1/user identity route bounds; the
repository-pin mutant probe speaks the new grammar. Full handoff +
gitea + broker families pass (952 tests), mutation gate 13/13, per-file
line+branch coverage >=95%, all touched sources within the 500-line cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
451 lines
16 KiB
Python
451 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""Discoverable, residue-surfacing, self-cleaning armed ephemeral run.
|
|
|
|
The create response — not the index — is the authoritative name of a pull
|
|
request this harness made, and the index is read to its last page. When either
|
|
source is uncertain the run fails closed and reports the exact residue plus the
|
|
exact commands that remove it, so an operator is never left guessing what was
|
|
created.
|
|
|
|
Arming, provenance, and push-target rules live in :mod:`hermes_handoff_arming`
|
|
and are re-exported here for callers that treat armed mode as one surface.
|
|
|
|
Armed mode is currently deferred: the deployed broker client speaks only
|
|
``read`` and ``create-draft``, so the POST/PATCH/DELETE probes below cannot
|
|
execute against the live platform and fail closed at the client. They will be
|
|
rebuilt on the broker's bounded mutation surface before armed mode is taken up.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
from hermes_handoff_arming import (
|
|
CONFIRMATION,
|
|
REF_PREFIX,
|
|
ArmingError,
|
|
ArmRequest,
|
|
assert_push_target_allowed,
|
|
bounded_read,
|
|
normalise_branch,
|
|
preflight,
|
|
)
|
|
from hermes_handoff_exec import Outcome, Runner, Vantage
|
|
from hermes_handoff_model import EPHEMERAL, FAIL, NOT_RUN, PASS, CheckResult, CheckSpec
|
|
from hermes_handoff_policy import (
|
|
EXPECTED_BASE,
|
|
GITEA_CLIENT,
|
|
register_ephemeral_pull,
|
|
)
|
|
from hermes_handoff_rules import strict_json
|
|
|
|
__all__ = [
|
|
"CONFIRMATION",
|
|
"DISCOVERY_PAGE_BUDGET",
|
|
"DISCOVERY_PAGE_SIZE",
|
|
"REF_PREFIX",
|
|
"ArmRequest",
|
|
"ArmingError",
|
|
"assert_push_target_allowed",
|
|
"bounded_read",
|
|
"normalise_branch",
|
|
"preflight",
|
|
"run_armed",
|
|
]
|
|
|
|
DRAFT_TITLE_PREFIX = "WIP: "
|
|
DISCOVERY_PAGE_SIZE = 50
|
|
DISCOVERY_PAGE_BUDGET = 20
|
|
|
|
|
|
def _spec(identifier: str, title: str) -> CheckSpec:
|
|
return CheckSpec(
|
|
id=f"ephemeral.{identifier}",
|
|
title=title,
|
|
group="ephemeral",
|
|
rule="armed",
|
|
steps=(),
|
|
mandatory=True,
|
|
scope=EPHEMERAL,
|
|
)
|
|
|
|
|
|
def _result(
|
|
identifier: str,
|
|
title: str,
|
|
status: str,
|
|
reason: str,
|
|
outcomes: Sequence[Outcome] = (),
|
|
evidence: dict[str, Any] | None = None,
|
|
) -> CheckResult:
|
|
return CheckResult(
|
|
spec=_spec(identifier, title),
|
|
status=status,
|
|
reason=reason,
|
|
evidence=evidence or {},
|
|
outcomes=list(outcomes),
|
|
)
|
|
|
|
|
|
def _protected_refusal_result() -> CheckResult:
|
|
accepted: list[str] = []
|
|
for candidate in ("main", "master", "refs/heads/main", "HEAD", f"{REF_PREFIX}/x"):
|
|
try:
|
|
assert_push_target_allowed(candidate)
|
|
except ArmingError:
|
|
continue
|
|
accepted.append(candidate)
|
|
return _result(
|
|
"protected-branch-refusal",
|
|
"Arming against a protected branch is refused before any network call",
|
|
FAIL if accepted else PASS,
|
|
f"guard accepted {accepted}"
|
|
if accepted
|
|
else "every protected and malformed target was refused",
|
|
evidence={"accepted": accepted},
|
|
)
|
|
|
|
|
|
def _detail(outcome: Outcome) -> str:
|
|
return (
|
|
outcome.error or outcome.combined[:200] or f"exit status {outcome.returncode}"
|
|
)
|
|
|
|
|
|
def _pr_problems(payload: Any, request: ArmRequest) -> list[str]:
|
|
if not isinstance(payload, dict):
|
|
return ["pull request is not an object"]
|
|
wanted = {
|
|
"state": "open",
|
|
"draft": True,
|
|
"merged": False,
|
|
"base": {"ref": EXPECTED_BASE, "sha": request.expected_base_sha},
|
|
"head": {"ref": request.ref, "sha": request.expected_head},
|
|
}
|
|
problems: list[str] = []
|
|
if (
|
|
not isinstance(payload.get("number"), int)
|
|
or isinstance(payload.get("number"), bool)
|
|
or payload["number"] <= 0
|
|
):
|
|
problems.append("number is malformed")
|
|
for field in ("state", "draft", "merged"):
|
|
if (
|
|
type(payload.get(field)) is not type(wanted[field])
|
|
or payload.get(field) != wanted[field]
|
|
):
|
|
problems.append(f"{field} does not match")
|
|
for group, values in (("base", wanted["base"]), ("head", wanted["head"])):
|
|
observed = payload.get(group)
|
|
if not isinstance(observed, dict):
|
|
problems.append(f"{group} is malformed")
|
|
continue
|
|
for field, value in values.items():
|
|
if observed.get(field) != value:
|
|
problems.append(f"{group}.{field} does not match")
|
|
return problems
|
|
|
|
|
|
def _created_number(outcome: Outcome, request: ArmRequest) -> tuple[int, str]:
|
|
"""Return the number the create response itself reports.
|
|
|
|
The create response is the only source that names a pull request the harness
|
|
definitely made. Relying on the index alone means an index that fails or
|
|
answers ambiguously leaves an unnamed, unclosed pull request behind.
|
|
"""
|
|
if not outcome.ok:
|
|
return (0, f"pull-request creation failed: {_detail(outcome)}")
|
|
try:
|
|
payload = strict_json(outcome.stdout)
|
|
except ValueError as exc:
|
|
return (0, f"pull-request creation returned malformed JSON: {exc}")
|
|
problems = _pr_problems(payload, request)
|
|
if problems:
|
|
return (0, f"created pull request is invalid: {', '.join(problems)}")
|
|
return (payload["number"], "")
|
|
|
|
|
|
def _index_pages(
|
|
runner: Runner, vantage: Vantage, api: str, reserve: float
|
|
) -> tuple[list[Outcome], list[Any], str]:
|
|
"""Read every page of the pull index; a truncated page can hide a match."""
|
|
outcomes: list[Outcome] = []
|
|
rows: list[Any] = []
|
|
for page in range(1, DISCOVERY_PAGE_BUDGET + 1):
|
|
outcome = runner.run(
|
|
(
|
|
GITEA_CLIENT,
|
|
"read",
|
|
f"{api}/pulls?state=all&limit={DISCOVERY_PAGE_SIZE}&page={page}",
|
|
),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
outcomes.append(outcome)
|
|
if not outcome.ok:
|
|
return (outcomes, [], f"discovery page {page} failed: {_detail(outcome)}")
|
|
try:
|
|
payload = strict_json(outcome.stdout)
|
|
except ValueError as exc:
|
|
return (outcomes, [], f"discovery page {page} is malformed JSON: {exc}")
|
|
if not isinstance(payload, list):
|
|
return (outcomes, [], f"discovery page {page} did not return an array")
|
|
rows.extend(payload)
|
|
if len(payload) < DISCOVERY_PAGE_SIZE:
|
|
return (outcomes, rows, "")
|
|
return (outcomes, [], "pull-request discovery exceeded its page budget")
|
|
|
|
|
|
def _residue(request: ArmRequest, numbers: Sequence[int], api: str) -> dict[str, Any]:
|
|
"""Name the exact residue and the exact actions that remove it by hand.
|
|
|
|
The broker client exposes no close or delete operation, so the cleanup
|
|
actions require the operator's own forge credentials.
|
|
"""
|
|
manual = [
|
|
f"close {api}/pulls/{number} (state=closed) with operator forge credentials"
|
|
for number in numbers
|
|
]
|
|
if not numbers:
|
|
manual.append(
|
|
f"{GITEA_CLIENT} read {api}/pulls?state=all&limit={DISCOVERY_PAGE_SIZE}"
|
|
f" then close by hand every pull whose head ref is {request.ref}"
|
|
)
|
|
manual.append(
|
|
f"delete {api}/branches/{request.ref} with operator forge credentials"
|
|
)
|
|
return {"residue_ref": request.ref, "manual_cleanup": manual}
|
|
|
|
|
|
def _match_pulls(rows: Sequence[Any], request: ArmRequest) -> tuple[list[int], str]:
|
|
matches: list[int] = []
|
|
for index, item in enumerate(rows):
|
|
if (
|
|
not isinstance(item, dict)
|
|
or not isinstance(item.get("head"), dict)
|
|
or not isinstance(item.get("base"), dict)
|
|
):
|
|
return ([], f"pull-request discovery row {index + 1} is malformed")
|
|
if item["head"].get("ref") != request.ref:
|
|
continue
|
|
problems = _pr_problems(item, request)
|
|
if problems:
|
|
return ([], f"discovered pull request is invalid: {', '.join(problems)}")
|
|
matches.append(item["number"])
|
|
if len(matches) != 1:
|
|
return (
|
|
matches,
|
|
f"expected one exact draft pull request, discovered {len(matches)}",
|
|
)
|
|
return (matches, "")
|
|
|
|
|
|
def _closed_pr_problems(payload: Any, request: ArmRequest) -> list[str]:
|
|
"""Validate the same exact PR identity with state changed to closed."""
|
|
problems = _pr_problems(payload, request)
|
|
if isinstance(payload, dict) and payload.get("state") == "closed":
|
|
problems = [
|
|
problem for problem in problems if problem != "state does not match"
|
|
]
|
|
elif "state does not match" not in problems:
|
|
problems.append("state is not closed")
|
|
return problems
|
|
|
|
|
|
def _cleanup(
|
|
runner: Runner,
|
|
vantage: Vantage,
|
|
request: ArmRequest,
|
|
api: str,
|
|
numbers: Sequence[int],
|
|
discovery_error: str = "",
|
|
) -> CheckResult:
|
|
outcomes: list[Outcome] = []
|
|
problems = [discovery_error] if discovery_error else []
|
|
for number in sorted(set(numbers)):
|
|
closed = runner.run(
|
|
(GITEA_CLIENT, "PATCH", f"{api}/pulls/{number}", "--field", "state=closed"),
|
|
vantage,
|
|
)
|
|
outcomes.append(closed)
|
|
if not closed.ok:
|
|
problems.append(f"pull request #{number} close failed")
|
|
continue
|
|
try:
|
|
payload = strict_json(closed.stdout)
|
|
except ValueError:
|
|
problems.append(f"pull request #{number} close response is malformed")
|
|
continue
|
|
expected = _closed_pr_problems(payload, request)
|
|
if expected:
|
|
problems.append(f"pull request #{number} close response does not match")
|
|
deleted = runner.run(
|
|
(GITEA_CLIENT, "DELETE", f"{api}/branches/{request.ref}"), vantage
|
|
)
|
|
outcomes.append(deleted)
|
|
if not deleted.ok:
|
|
problems.append("ephemeral branch deletion failed")
|
|
remaining = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"), vantage
|
|
)
|
|
outcomes.append(remaining)
|
|
if not remaining.ok or remaining.stdout.strip():
|
|
problems.append("ephemeral branch absence could not be verified")
|
|
for number in sorted(set(numbers)):
|
|
observed = runner.run((GITEA_CLIENT, "read", f"{api}/pulls/{number}"), vantage)
|
|
outcomes.append(observed)
|
|
if not observed.ok:
|
|
problems.append(f"pull request #{number} closure could not be verified")
|
|
continue
|
|
try:
|
|
payload = strict_json(observed.stdout)
|
|
except ValueError:
|
|
problems.append(f"pull request #{number} verification is malformed")
|
|
continue
|
|
expected = _closed_pr_problems(payload, request)
|
|
if expected:
|
|
problems.append(f"pull request #{number} is not exactly closed")
|
|
return _result(
|
|
"cleanup-verified",
|
|
"Every ephemeral ref and pull request is removed and the removal is verified",
|
|
FAIL if problems else PASS,
|
|
"; ".join(problems)
|
|
if problems
|
|
else "the exact branch and draft pull request are gone",
|
|
outcomes,
|
|
{"ref": request.ref, "pull_requests": sorted(set(numbers))},
|
|
)
|
|
|
|
|
|
def _nothing_created(branch: CheckResult, draft_reason: str) -> list[CheckResult]:
|
|
"""Report the two results that follow a run which created no artifact."""
|
|
return [
|
|
branch,
|
|
_result(
|
|
"draft-pull-request",
|
|
"A draft pull request can be opened and closed",
|
|
NOT_RUN,
|
|
draft_reason,
|
|
),
|
|
_result(
|
|
"cleanup-verified",
|
|
"Every ephemeral artifact is removed",
|
|
NOT_RUN,
|
|
"nothing was created",
|
|
),
|
|
]
|
|
|
|
|
|
def run_armed(
|
|
runner: Runner, vantage: Vantage, request: ArmRequest
|
|
) -> list[CheckResult]:
|
|
"""Create, discover, validate, and clean one exact ephemeral branch/PR."""
|
|
guard = _protected_refusal_result()
|
|
branch_title = "A unique ephemeral feature branch can be pushed and removed"
|
|
if guard.status != PASS:
|
|
unrun = _result("feature-branch-push", branch_title, NOT_RUN, "guard failed")
|
|
return [guard, *_nothing_created(unrun, "protected-ref guard failed")]
|
|
api = f"/api/v1/repos/{request.repo}"
|
|
reserve = min(6000.0, max(300.0, runner.command_timeout * 10))
|
|
existing = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
if not existing.ok or existing.stdout.strip():
|
|
reason = (
|
|
_detail(existing) if not existing.ok else "the ephemeral ref already exists"
|
|
)
|
|
refused = _result("feature-branch-push", branch_title, FAIL, reason, [existing])
|
|
return [guard, *_nothing_created(refused, "branch preflight failed")]
|
|
local_head = runner.run(
|
|
("git", "rev-parse", "HEAD"), vantage, reserve_seconds=reserve
|
|
)
|
|
pushed = (
|
|
runner.run(
|
|
("git", "push", request.remote, f"HEAD:refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
if local_head.ok and local_head.stdout.strip() == request.expected_head
|
|
else Outcome(
|
|
argv=("git", "push"),
|
|
vantage=vantage.name,
|
|
error="local HEAD changed after static preflight",
|
|
)
|
|
)
|
|
verified = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
branch_ok = verified.ok and verified.stdout.split() == [
|
|
request.expected_head,
|
|
f"refs/heads/{request.ref}",
|
|
]
|
|
branch_result = _result(
|
|
"feature-branch-push",
|
|
branch_title,
|
|
PASS if branch_ok else FAIL,
|
|
"the exact reviewed HEAD was pushed"
|
|
if branch_ok
|
|
else "push could not be verified exactly",
|
|
[local_head, pushed, verified],
|
|
{"ref": request.ref, "head": request.expected_head},
|
|
)
|
|
numbers: list[int] = []
|
|
problems: list[str] = ["branch creation was not verified"]
|
|
create_outcomes: list[Outcome] = []
|
|
if branch_ok:
|
|
created = runner.run(
|
|
(
|
|
GITEA_CLIENT,
|
|
"POST",
|
|
f"{api}/pulls",
|
|
"--field",
|
|
f"title={DRAFT_TITLE_PREFIX}Hermes handoff acceptance ephemeral probe",
|
|
"--field",
|
|
f"head={request.ref}",
|
|
"--field",
|
|
f"base={EXPECTED_BASE}",
|
|
"--field",
|
|
"body=Ephemeral acceptance probe. Closed and deleted by the harness.",
|
|
),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
number, create_error = _created_number(created, request)
|
|
pages, rows, index_error = _index_pages(runner, vantage, api, reserve)
|
|
found, match_error = (
|
|
_match_pulls(rows, request) if not index_error else ([], "")
|
|
)
|
|
create_outcomes = [created, *pages]
|
|
# Either source alone names a pull request this run definitely made: the
|
|
# ephemeral ref was proven absent in preflight, so any pull on it is ours.
|
|
numbers = sorted({item for item in (number, *found) if item})
|
|
problems = [text for text in (index_error, match_error) if text]
|
|
if number and found and found != [number]:
|
|
problems.append(f"created #{number} is not the discovered {found}")
|
|
if not numbers:
|
|
problems.append(
|
|
create_error or "no exact draft pull request was identified"
|
|
)
|
|
for item in numbers:
|
|
register_ephemeral_pull(item)
|
|
exact = len(numbers) == 1 and not problems
|
|
draft_result = _result(
|
|
"draft-pull-request",
|
|
"A draft pull request can be opened and closed against the ephemeral branch",
|
|
PASS if exact else FAIL,
|
|
f"discovered exact draft pull request #{numbers[0]}"
|
|
if exact
|
|
else "; ".join(problems),
|
|
create_outcomes,
|
|
{"pull_requests": numbers}
|
|
if exact
|
|
else {"pull_requests": numbers, **_residue(request, numbers, api)},
|
|
)
|
|
cleanup = _cleanup(runner, vantage, request, api, numbers, "; ".join(problems))
|
|
return [guard, branch_result, draft_result, cleanup]
|