agent: fix readiness authority-git mode mischeck (group-shared repo)

ServerOwnedEvidenceSource.assert_ready() applied the evidence-root strict
group/world-writable check to the authority Git directory too. A valid
setgid/group-shared authority repository (core.sharedRepository=group,
02775) permanently failed readiness with COLLECTOR_EVIDENCE_ROOT_MODE_DENIED.

Fix: authority Git requires only exists + directory + non-symlink; the strict
st_mode & 0o022 and owner checks apply to the evidence root only. Builder
readiness remains infrastructure-only (independent of recipe evidence);
BuildTrustedCandidateV1 stays evidence-gated and fail-closed.

Add regression proving a group-shared 02775 authority is accepted while a
group/world-writable evidence root is still rejected.

Base: 8bf363143e (exact)
Files: 2 (modified), +36/-11
Patch SHA-256: 5335025a9b193a50ad08c45e41ca6e8c7abdc77defec90c348f5149a0fe13b44
This commit is contained in:
2026-09-08 18:37:39 +08:00
parent 8bf363143e
commit 49d30a2510
2 changed files with 36 additions and 11 deletions
@@ -367,15 +367,15 @@ class ServerOwnedEvidenceSource:
raise BuilderError(code) from exc
if not stat.S_ISDIR(metadata.st_mode) or path.is_symlink():
raise BuilderError(code)
if os.name != "nt" and metadata.st_mode & 0o022:
raise BuilderError("COLLECTOR_EVIDENCE_ROOT_MODE_DENIED")
if (
path == self._evidence_root
and os.name != "nt"
and self._expected_owner_uid is not None
and metadata.st_uid != self._expected_owner_uid
):
raise BuilderError("COLLECTOR_EVIDENCE_ROOT_OWNER_DENIED")
if path == self._evidence_root:
if os.name != "nt" and metadata.st_mode & 0o022:
raise BuilderError("COLLECTOR_EVIDENCE_ROOT_MODE_DENIED")
if (
os.name != "nt"
and self._expected_owner_uid is not None
and metadata.st_uid != self._expected_owner_uid
):
raise BuilderError("COLLECTOR_EVIDENCE_ROOT_OWNER_DENIED")
def _digest(self, path: Path) -> str:
digest = hashlib.sha256()
+27 -2
View File
@@ -323,9 +323,11 @@ def test_readiness_is_independent_from_recipe_evidence_but_build_remains_fail_cl
policy = load_builder_policy(POLICY_PATH)
config = TrustedCandidateBuilderConfig.from_policy(policy)
evidence_root = tmp_path / "evidence"
evidence_root.mkdir(mode=0o700)
evidence_root.mkdir()
evidence_root.chmod(0o700)
authority_git = tmp_path / "repo.git"
authority_git.mkdir(mode=0o700)
authority_git.mkdir()
authority_git.chmod(0o2775)
source = ServerOwnedEvidenceSource(
config,
@@ -358,6 +360,29 @@ def test_readiness_is_independent_from_recipe_evidence_but_build_remains_fail_cl
server.handle_request(build, connection=object())
def test_readiness_keeps_evidence_root_permissions_strict_with_group_shared_authority(tmp_path) -> None:
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
evidence_root = tmp_path / "evidence"
evidence_root.mkdir()
evidence_root.chmod(0o720)
authority_git = tmp_path / "repo.git"
authority_git.mkdir()
authority_git.chmod(0o2775)
source = ServerOwnedEvidenceSource(
config,
authority_git,
evidence_root,
expected_owner_uid=None,
git_runner=lambda *_args, **_kwargs: SimpleNamespace(
stdout="a" * 40 + "\n", returncode=0
),
)
with pytest.raises(BuilderError, match="COLLECTOR_EVIDENCE_ROOT_MODE_DENIED"):
source.assert_ready()
def test_unix_client_applies_bounded_timeout_and_fails_closed(monkeypatch) -> None:
observed = {}