Compare commits

...
16 changed files with 149 additions and 32 deletions
@@ -11,6 +11,8 @@ from dataclasses import dataclass
import os import os
from pathlib import Path from pathlib import Path
import re import re
from aios_target_branch_policy import validate_target_branch
import subprocess import subprocess
import tempfile import tempfile
from typing import Mapping, Sequence from typing import Mapping, Sequence
@@ -95,7 +97,8 @@ class AuthorityGitExecutor:
return path return path
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str: 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") raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_oid, "expected_oid") _oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid") _oid(new_oid, "new_oid")
@@ -9,6 +9,8 @@ import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
import re import re
from aios_target_branch_policy import validate_target_branch
import sqlite3 import sqlite3
from typing import Any, Mapping from typing import Any, Mapping
@@ -210,8 +212,7 @@ class GovernedRuntime:
if ( if (
not task_id not task_id
or repository != self.config.repository or repository != self.config.repository
or not branch.startswith("agent/") or not validate_target_branch(branch)
or branch in {"main", "master"}
or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"} or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}
or generation < 1 or generation < 1
or not self._valid_oid(base) or not self._valid_oid(base)
@@ -287,7 +288,7 @@ class GovernedRuntime:
for value in self.bindings.values(): for value in self.bindings.values():
task_id = str(value.get("task_id") or "").strip() task_id = str(value.get("task_id") or "").strip()
branch = str(value.get("target_branch") 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 continue
authority = self.authority.read_ref("refs/heads/" + branch) authority = self.authority.read_ref("refs/heads/" + branch)
try: try:
@@ -10,6 +10,8 @@ from __future__ import annotations
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import re import re
from aios_target_branch_policy import validate_target_branch
from typing import Protocol from typing import Protocol
@@ -60,7 +62,7 @@ class WriterLeaseRequestV2:
raise ContinuationError("branch_mode_invalid") raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid") _oid(self.base_oid, "base_oid")
_oid(self.expected_branch_oid, "expected_branch_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") raise ContinuationError("protected_branch_rejected")
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1: if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
raise ContinuationError("generation_invalid") raise ContinuationError("generation_invalid")
+3 -1
View File
@@ -8,6 +8,8 @@ from pathlib import Path
import secrets import secrets
from typing import Any, Callable, Mapping from typing import Any, Callable, Mapping
from aios_target_branch_policy import validate_target_branch
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CredentialLease: class CredentialLease:
@@ -44,7 +46,7 @@ class CredentialBroker:
self._leases: dict[str, CredentialLease] = {} 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]: 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") raise ValueError("CREDENTIAL_LEASE_BINDING_INVALID")
now = self.now().astimezone(timezone.utc) 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__) 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 urllib.request import Request, urlopen
from aios_gitops_reconciliation_core import RemoteObservation from aios_gitops_reconciliation_core import RemoteObservation
from aios_target_branch_policy import validate_target_branch
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -35,7 +36,7 @@ class GiteaPublisher:
@staticmethod @staticmethod
def validate_branch(branch: str) -> None: 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") raise ValueError("BRANCH_SCOPE_DENIED")
def repository_identity(self) -> str: def repository_identity(self) -> str:
@@ -18,6 +18,8 @@ import tempfile
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Callable, Mapping from typing import Any, Callable, Mapping
from aios_target_branch_policy import validate_target_branch
class JobError(RuntimeError): class JobError(RuntimeError):
pass pass
@@ -274,7 +276,7 @@ class GitOpsJobController:
raise JobError("EXPECTED_BRANCH_OID_INVALID") raise JobError("EXPECTED_BRANCH_OID_INVALID")
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest): if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise JobError("PAYLOAD_DIGEST_INVALID") 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") raise JobError("PROTECTED_BRANCH_REJECTED")
binding = {field: str(request[field]) for field in fields} binding = {field: str(request[field]) for field in fields}
# Operation-specific, non-secret binding inputs must survive the # Operation-specific, non-secret binding inputs must survive the
@@ -383,7 +385,7 @@ class GitOpsJobController:
raise JobError("JOB_NOT_FOUND") raise JobError("JOB_NOT_FOUND")
if int(row["revision"]) != expected_revision: if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT") raise JobError("REVISION_CAS_CONFLICT")
if row["branch"] in {"main", "master"}: if not validate_target_branch(row["branch"]):
raise JobError("PROTECTED_BRANCH_REJECTED") raise JobError("PROTECTED_BRANCH_REJECTED")
now = utc_now() 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)) 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 dataclasses import dataclass
from enum import Enum from enum import Enum
import re import re
from aios_target_branch_policy import validate_target_branch
from typing import Optional from typing import Optional
OID_RE = re.compile(r"^[0-9a-f]{40}$") 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.""" """Decide from a fresh observation; never complete on a missing remote."""
if observation.repository_identity != "aios/aios": if observation.repository_identity != "aios/aios":
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REPOSITORY_IDENTITY_MISMATCH") 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") return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "BRANCH_SCOPE_DENIED")
if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid): if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID") return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID")
+3 -1
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import re import re
from aios_target_branch_policy import validate_target_branch
from typing import Any, Mapping from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$") 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") return PolicyDecision("DENY", "REPOSITORY_IDENTITY_MISMATCH")
if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid): if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid):
return PolicyDecision("DENY", "OID_INVALID") 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") return PolicyDecision("DENY", "BRANCH_SCOPE_DENIED")
if "force_push" in value.requested_capabilities or "force" in value.operation.lower(): if "force_push" in value.requested_capabilities or "force" in value.operation.lower():
return PolicyDecision("DENY", "FORCE_PUSH_DENIED") return PolicyDecision("DENY", "FORCE_PUSH_DENIED")
@@ -11,7 +11,7 @@ from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore from aios_gitops_job_controller import GitOpsJobController, JobStore
OID_RE = re.compile(r"^[0-9a-f]{40}$") 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): class BootstrapError(RuntimeError):
@@ -61,7 +61,7 @@ class GovernedTaskBootstrap:
def create(self, request: Mapping[str, Any]) -> dict[str, Any]: 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")} 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 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"]) existing = self._read_existing(values["task_id"])
if existing is not None: 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") 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}$") OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$") SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
TASK_RE = re.compile(r"^[A-Z0-9][A-Z0-9._:-]{7,127}$") 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}$") OWNER_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{3,255}$")
REQUIRED_REQUEST_FIELDS = frozenset( REQUIRED_REQUEST_FIELDS = frozenset(
@@ -353,11 +353,7 @@ def _validate_request(
raise ProvisioningError("BASE_REF_REJECTED", 409) raise ProvisioningError("BASE_REF_REJECTED", 409)
if not isinstance(raw["base_oid"], str) or not OID_RE.fullmatch(raw["base_oid"]): if not isinstance(raw["base_oid"], str) or not OID_RE.fullmatch(raw["base_oid"]):
raise ProvisioningError("BASE_OID_REJECTED") raise ProvisioningError("BASE_OID_REJECTED")
if ( if not validate_target_branch(raw["target_branch"]):
not isinstance(raw["target_branch"], str)
or not BRANCH_RE.fullmatch(raw["target_branch"])
or raw["target_branch"] in {"main", "master"}
):
raise ProvisioningError("TARGET_BRANCH_REJECTED") raise ProvisioningError("TARGET_BRANCH_REJECTED")
if not isinstance(raw["task_id"], str) or not TASK_RE.fullmatch(raw["task_id"]): if not isinstance(raw["task_id"], str) or not TASK_RE.fullmatch(raw["task_id"]):
raise ProvisioningError("TASK_ID_REJECTED") raise ProvisioningError("TASK_ID_REJECTED")
@@ -23,6 +23,7 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Mapping, Protocol from typing import Any, Mapping, Protocol
from aios_target_branch_policy import validate_target_branch
from authorized_task_writer_provisioning import ( from authorized_task_writer_provisioning import (
AuthorizedTaskWriterProvisioner, AuthorizedTaskWriterProvisioner,
BRANCH_RE, BRANCH_RE,
@@ -335,11 +336,7 @@ def _validate_request(
or not OID_RE.fullmatch(raw["base_oid"]) or not OID_RE.fullmatch(raw["base_oid"])
): ):
raise OwnerAuthorizationError("BASE_OID_REJECTED") raise OwnerAuthorizationError("BASE_OID_REJECTED")
if ( if not validate_target_branch(raw["target_branch"]):
not isinstance(raw["target_branch"], str)
or not BRANCH_RE.fullmatch(raw["target_branch"])
or raw["target_branch"] in {"main", "master"}
):
raise OwnerAuthorizationError("TARGET_BRANCH_REJECTED") raise OwnerAuthorizationError("TARGET_BRANCH_REJECTED")
if ( if (
not isinstance(raw["task_id"], str) 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}$") 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}$") 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}$") 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 MAX_FRAME = 64 * 1024
CHALLENGE_TTL_SECONDS = 120 CHALLENGE_TTL_SECONDS = 120
@@ -207,10 +207,7 @@ def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
raise BootstrapError("BASE_REF_REJECTED") raise BootstrapError("BASE_REF_REJECTED")
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]): if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
raise BootstrapError("BASE_OID_REJECTED") raise BootstrapError("BASE_OID_REJECTED")
if ( if not validate_target_branch(request["target_branch"]):
not isinstance(request["target_branch"], str)
or not BRANCH_RE.fullmatch(request["target_branch"])
):
raise BootstrapError("TARGET_BRANCH_REJECTED") raise BootstrapError("TARGET_BRANCH_REJECTED")
if ( if (
not isinstance(request["task_id"], str) not isinstance(request["task_id"], str)
+3 -2
View File
@@ -17,6 +17,8 @@ import hashlib
import json import json
import os import os
import re import re
from aios_target_branch_policy import validate_target_branch
import secrets import secrets
import tempfile import tempfile
from dataclasses import dataclass from dataclasses import dataclass
@@ -192,8 +194,7 @@ def _validate_config(config: IdentityOverlayConfig) -> None:
if not OID_RE.fullmatch(binding.base_oid): if not OID_RE.fullmatch(binding.base_oid):
raise IdentityOverlayError("OVERLAY_BASE_OID_INVALID", 503) raise IdentityOverlayError("OVERLAY_BASE_OID_INVALID", 503)
if ( if (
not BRANCH_RE.fullmatch(binding.target_branch) not validate_target_branch(binding.target_branch)
or binding.target_branch in {"main", "master"}
): ):
raise IdentityOverlayError("OVERLAY_TARGET_BRANCH_INVALID", 503) raise IdentityOverlayError("OVERLAY_TARGET_BRANCH_INVALID", 503)
if not OWNER_IDENTITY_RE.fullmatch(config.owner_identity): 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), (("owner_identity",), "owner", None),
(("authenticated_scope",), "*", "OWNER_SCOPE_INVALID"), (("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", "production_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
(("request", "tag_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"), (("request", "tag_change_authorized"), True, "CONSEQUENTIAL_CAPABILITY_REJECTED"),
(("request", "baseline_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)