Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff6593084 |
@@ -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")
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user