M3.3: the trust dial (the agent merges approved PRs)
A project's trust.merge is operator (the default) or approved (L3, D5, #65). Set to approved, the harness, never the model, merges a PR in the PR watch once an approving review from a gitea.operators account is on its current commit, its checks pass (none at all counts; pending waits) and it's mergeable; the item then goes on as after any merge. Each reason to hold off (failed checks, conflicts, a refused merge) is said once per commit. Gitea enforces it: protection requires approvals from an operators-only whitelist and dismisses stale ones. At start, for each approved project, the agent checks by behaviour that its own unapproved merge is refused, with a throwaway branch and PR made through the API (never the workspace); if it isn't refused, the operator merges until a restart. - gitea: approvals, combined check status, a PR's head commit and mergeability, API-made branches and their deletion. - Test and bring-up repositories get the protection. - Tests: the decision (unit), the hard wall on a real Gitea (test_trust_live), an e2e test where the operator only approves, and a walk property, merges_only_approved, checked against a mutant that ignores which commit was approved. Closes #65 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
21cbf1682c
commit
ab469b60bb
17 files changed
+733
-11
No files matched your search
@@ -67,7 +67,12 @@ PROJECT_OPTIONAL = (
|
||||
"labels",
|
||||
"secrets",
|
||||
"egress",
|
||||
"trust.merge",
|
||||
)
|
||||
# The trust dial (L3, D5): who merges a project's PRs. "approved": the
|
||||
# harness merges once an operator approved the PR's current commit and its
|
||||
# checks pass, behind protection that requires that approval.
|
||||
MERGE_MODES = ("operator", "approved")
|
||||
# Who may see a project secret (D7, D8): the model's commands get it as an
|
||||
# environment variable, redacted from what comes back; harness-only ones are
|
||||
# never in a command the model chose.
|
||||
@@ -315,6 +320,9 @@ def _parse_project(
|
||||
errors.append(f"{where}: set a push token or a push key, not both")
|
||||
labels = _strings(values.get("labels", []), f"{where}.labels", errors)
|
||||
egress = _strings(values.get("egress", []), f"{where}.egress", errors)
|
||||
merge = str(values.get("trust.merge") or "operator")
|
||||
if merge not in MERGE_MODES:
|
||||
errors.append(f"{where}.trust.merge: must be one of {', '.join(MERGE_MODES)}")
|
||||
secrets = _parse_secrets(entry.get("secrets"), where, env, errors)
|
||||
return Project(
|
||||
name=name,
|
||||
@@ -333,6 +341,7 @@ def _parse_project(
|
||||
labels=labels,
|
||||
secrets=secrets,
|
||||
egress=egress,
|
||||
merge=merge,
|
||||
)
|
||||
|
||||
|
||||
@@ -454,6 +463,7 @@ class Project:
|
||||
labels: tuple[str, ...] = () # added to every issue's own (D1)
|
||||
secrets: Mapping[str, Secret] = field(default_factory=dict)
|
||||
egress: tuple[str, ...] = () # beyond the default; recorded, not yet enforced
|
||||
merge: str = "operator" # the trust dial (L3): who merges its PRs
|
||||
|
||||
def command_secrets(self) -> dict[str, str]:
|
||||
"""The secrets the model's commands may see, by name."""
|
||||
|
||||
Reference in new issue
Block a user