Compare commits

...
16 changed files with 149 additions and 32 deletions
@@ -11,6 +11,8 @@ from dataclasses import dataclass
import os
from pathlib import Path
import re
from aios_target_branch_policy import validate_target_branch
import subprocess
import tempfile
from typing import Mapping, Sequence
@@ -95,7 +97,8 @@ class AuthorityGitExecutor:
return path
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
branch_path = branch[len("refs/heads/"):] if branch.startswith("refs/heads/") else ""
if not branch.startswith("refs/heads/") or not validate_target_branch(branch_path):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid")
@@ -9,6 +9,8 @@ import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import re
from aios_target_branch_policy import validate_target_branch
import sqlite3
from typing import Any, Mapping
@@ -210,8 +212,7 @@ class GovernedRuntime:
if (
not task_id
or repository != self.config.repository
or not branch.startswith("agent/")
or branch in {"main", "master"}
or not validate_target_branch(branch)
or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}
or generation < 1
or not self._valid_oid(base)
@@ -287,7 +288,7 @@ class GovernedRuntime:
for value in self.bindings.values():
task_id = str(value.get("task_id") or "").strip()
branch = str(value.get("target_branch") or "").strip()
if not task_id or not branch.startswith("agent/"):
if not task_id or not validate_target_branch(branch):
continue
authority = self.authority.read_ref("refs/heads/" + branch)
try:
@@ -10,6 +10,8 @@ from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone
import re
from aios_target_branch_policy import validate_target_branch
from typing import Protocol
@@ -60,7 +62,7 @@ class WriterLeaseRequestV2:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
_oid(self.expected_branch_oid, "expected_branch_oid")
if self.target_branch in {"main", "master"} or self.target_branch.startswith(("refs/", "tags/")):
if not validate_target_branch(self.target_branch):
raise ContinuationError("protected_branch_rejected")
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
raise ContinuationError("generation_invalid")
+3 -1
View File
@@ -8,6 +8,8 @@ from pathlib import Path
import secrets
from typing import Any, Callable, Mapping
from aios_target_branch_policy import validate_target_branch
@dataclass(frozen=True, slots=True)
class CredentialLease:
@@ -44,7 +46,7 @@ class CredentialBroker:
self._leases: dict[str, CredentialLease] = {}
def issue(self, *, task_id: str, repository: str, target_branch: str, generation: int, scopes: tuple[str, ...] = ("git:push", "gitea:read"), lease_seconds: int = 900) -> dict[str, Any]:
if repository != "aios/aios" or not target_branch.startswith("agent/") or generation < 1 or not 1 <= lease_seconds <= 86400:
if repository != "aios/aios" or not validate_target_branch(target_branch) or generation < 1 or not 1 <= lease_seconds <= 86400:
raise ValueError("CREDENTIAL_LEASE_BINDING_INVALID")
now = self.now().astimezone(timezone.utc)
lease = CredentialLease("lease-" + secrets.token_hex(16), task_id, repository, target_branch, generation, now.isoformat(), (now + timedelta(seconds=lease_seconds)).isoformat(), tuple(scopes), type(self.provider).__name__)
+2 -1
View File
@@ -11,6 +11,7 @@ from urllib.parse import quote
from urllib.request import Request, urlopen
from aios_gitops_reconciliation_core import RemoteObservation
from aios_target_branch_policy import validate_target_branch
@dataclass(frozen=True, slots=True)
@@ -35,7 +36,7 @@ class GiteaPublisher:
@staticmethod
def validate_branch(branch: str) -> None:
if not isinstance(branch, str) or not branch.startswith("agent/") or branch in {"main", "master"}:
if not validate_target_branch(branch):
raise ValueError("BRANCH_SCOPE_DENIED")
def repository_identity(self) -> str:
@@ -18,6 +18,8 @@ import tempfile
from datetime import datetime, timezone
from typing import Any, Callable, Mapping
from aios_target_branch_policy import validate_target_branch
class JobError(RuntimeError):
pass
@@ -274,7 +276,7 @@ class GitOpsJobController:
raise JobError("EXPECTED_BRANCH_OID_INVALID")
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise JobError("PAYLOAD_DIGEST_INVALID")
if request["branch"] in {"main", "master"} or request["branch"].startswith(("refs/", "tags/")):
if not validate_target_branch(request["branch"]):
raise JobError("PROTECTED_BRANCH_REJECTED")
binding = {field: str(request[field]) for field in fields}
# Operation-specific, non-secret binding inputs must survive the
@@ -383,7 +385,7 @@ class GitOpsJobController:
raise JobError("JOB_NOT_FOUND")
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if row["branch"] in {"main", "master"}:
if not validate_target_branch(row["branch"]):
raise JobError("PROTECTED_BRANCH_REJECTED")
now = utc_now()
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, lease_expires_at, "ACTIVE", 0))
@@ -4,6 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import re
from aios_target_branch_policy import validate_target_branch
from typing import Optional
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@@ -52,7 +54,7 @@ def decide_reconciliation(observation: RemoteObservation) -> ReconciliationDecis
"""Decide from a fresh observation; never complete on a missing remote."""
if observation.repository_identity != "aios/aios":
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REPOSITORY_IDENTITY_MISMATCH")
if not observation.branch.startswith("agent/"):
if not validate_target_branch(observation.branch):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "BRANCH_SCOPE_DENIED")
if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID")
+3 -1
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass
import re
from aios_target_branch_policy import validate_target_branch
from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@@ -39,7 +41,7 @@ def evaluate_policy(value: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDec
return PolicyDecision("DENY", "REPOSITORY_IDENTITY_MISMATCH")
if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid):
return PolicyDecision("DENY", "OID_INVALID")
if not value.target_branch.startswith("agent/") or value.target_branch in {"main", "master"}:
if not validate_target_branch(value.target_branch):
return PolicyDecision("DENY", "BRANCH_SCOPE_DENIED")
if "force_push" in value.requested_capabilities or "force" in value.operation.lower():
return PolicyDecision("DENY", "FORCE_PUSH_DENIED")
@@ -11,7 +11,7 @@ from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
from aios_target_branch_policy import BRANCH_RE, validate_target_branch
class BootstrapError(RuntimeError):
@@ -61,7 +61,7 @@ class GovernedTaskBootstrap:
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
if not validate_target_branch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
@@ -0,0 +1,56 @@
"""Single-source validation for governed implementation branch names.
The policy accepts ordinary implementation branches while rejecting protected
refs, malformed Git ref names, traversal-like components, and characters that
Git treats specially. Callers translate a false result into their own stable
error contract.
"""
from __future__ import annotations
import re
from typing import Any
BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
PROTECTED_BRANCHES = frozenset({"HEAD", "main", "master"})
PROTECTED_PREFIXES = (
"main/",
"master/",
"refs/",
"tags/",
"tag/",
"release/",
"releases/",
"baseline/",
"production/",
)
def validate_target_branch(value: Any) -> bool:
"""Return whether *value* is a safe, non-protected implementation branch."""
if not isinstance(value, str) or not 3 <= len(value) <= 180:
return False
if not BRANCH_RE.fullmatch(value):
return False
if value in PROTECTED_BRANCHES or value == "@":
return False
if value.startswith(PROTECTED_PREFIXES):
return False
if value.startswith("/") or value.endswith("/"):
return False
if value.endswith(".") or value.endswith(".lock"):
return False
if ".." in value or "//" in value or "@{" in value:
return False
if any(ord(char) < 0x20 or ord(char) == 0x7F or char in " ~^:?*[\\" for char in value):
return False
components = value.split("/")
if any(
not component
or component in {".", "..", "@"}
or component.startswith(".")
or component.endswith(".lock")
for component in components
):
return False
return True
@@ -33,7 +33,7 @@ from signer_contract_compatibility import (
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
TASK_RE = re.compile(r"^[A-Z0-9][A-Z0-9._:-]{7,127}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
from aios_target_branch_policy import BRANCH_RE, validate_target_branch
OWNER_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{3,255}$")
REQUIRED_REQUEST_FIELDS = frozenset(
@@ -353,11 +353,7 @@ def _validate_request(
raise ProvisioningError("BASE_REF_REJECTED", 409)
if not isinstance(raw["base_oid"], str) or not OID_RE.fullmatch(raw["base_oid"]):
raise ProvisioningError("BASE_OID_REJECTED")
if (
not isinstance(raw["target_branch"], str)
or not BRANCH_RE.fullmatch(raw["target_branch"])
or raw["target_branch"] in {"main", "master"}
):
if not validate_target_branch(raw["target_branch"]):
raise ProvisioningError("TARGET_BRANCH_REJECTED")
if not isinstance(raw["task_id"], str) or not TASK_RE.fullmatch(raw["task_id"]):
raise ProvisioningError("TASK_ID_REJECTED")
@@ -23,6 +23,7 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Mapping, Protocol
from aios_target_branch_policy import validate_target_branch
from authorized_task_writer_provisioning import (
AuthorizedTaskWriterProvisioner,
BRANCH_RE,
@@ -335,11 +336,7 @@ def _validate_request(
or not OID_RE.fullmatch(raw["base_oid"])
):
raise OwnerAuthorizationError("BASE_OID_REJECTED")
if (
not isinstance(raw["target_branch"], str)
or not BRANCH_RE.fullmatch(raw["target_branch"])
or raw["target_branch"] in {"main", "master"}
):
if not validate_target_branch(raw["target_branch"]):
raise OwnerAuthorizationError("TARGET_BRANCH_REJECTED")
if (
not isinstance(raw["task_id"], str)
@@ -49,7 +49,7 @@ OID_RE = re.compile(r"^[0-9a-f]{40}$")
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
from aios_target_branch_policy import validate_target_branch
MAX_FRAME = 64 * 1024
CHALLENGE_TTL_SECONDS = 120
@@ -207,10 +207,7 @@ def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
raise BootstrapError("BASE_REF_REJECTED")
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
raise BootstrapError("BASE_OID_REJECTED")
if (
not isinstance(request["target_branch"], str)
or not BRANCH_RE.fullmatch(request["target_branch"])
):
if not validate_target_branch(request["target_branch"]):
raise BootstrapError("TARGET_BRANCH_REJECTED")
if (
not isinstance(request["task_id"], str)
+3 -2
View File
@@ -17,6 +17,8 @@ import hashlib
import json
import os
import re
from aios_target_branch_policy import validate_target_branch
import secrets
import tempfile
from dataclasses import dataclass
@@ -192,8 +194,7 @@ def _validate_config(config: IdentityOverlayConfig) -> None:
if not OID_RE.fullmatch(binding.base_oid):
raise IdentityOverlayError("OVERLAY_BASE_OID_INVALID", 503)
if (
not BRANCH_RE.fullmatch(binding.target_branch)
or binding.target_branch in {"main", "master"}
not validate_target_branch(binding.target_branch)
):
raise IdentityOverlayError("OVERLAY_TARGET_BRANCH_INVALID", 503)
if not OWNER_IDENTITY_RE.fullmatch(config.owner_identity):
+1 -1
View File
@@ -53,7 +53,7 @@ def test_exact_binding_passes():
[
(("owner_identity",), "owner", None),
(("authenticated_scope",), "*", "OWNER_SCOPE_INVALID"),
(("request", "target_branch"), "patch/not-allowed", "TARGET_BRANCH_REJECTED"),
(("request", "target_branch"), "release/not-allowed", "TARGET_BRANCH_REJECTED"),
(("request", "production_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
(("request", "tag_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
(("request", "baseline_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
+55
View File
@@ -0,0 +1,55 @@
from __future__ import annotations
from aios_target_branch_policy import validate_target_branch
def test_accepts_ordinary_implementation_branches() -> None:
for branch in (
"agent/worker-change-20260815",
"patch/not-allowed-anymore",
"fix/target-branch-policy",
"feature/ordinary-implementation",
"chore/code_stats-v2",
):
assert validate_target_branch(branch), branch
def test_rejects_protected_namespaces() -> None:
for branch in (
"main",
"master",
"main/hotfix",
"master/hotfix",
"refs/heads/patch/change",
"tags/release-v1",
"tag/release-v1",
"release/v1",
"releases/v1",
"baseline/current",
"production/hotfix",
):
assert not validate_target_branch(branch), branch
def test_rejects_malformed_git_refs_and_traversal() -> None:
for branch in (
"../escape",
"patch/../escape",
"patch//escape",
"patch/.",
"patch/.hidden",
"patch/name.lock",
"patch/name.",
"patch/name~bad",
"patch/name^bad",
"patch/name:bad",
"patch/name?bad",
"patch/name*bad",
"patch/name[bad",
"patch/name\\\\bad",
"patch/name@{bad}",
"patch/name with-space",
"patch/line\\nbreak",
"@",
):
assert not validate_target_branch(branch), repr(branch)