[verified] Gate mirror fetch on attached worktree topology

This commit is contained in:
2026-10-12 14:21:50 +00:00
parent cc4fbd0008
commit 14b27bb708
3 changed files with 266 additions and 4 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Read-only worktree topology gate; caller owns admission and repository lock.
Reuse the harness inventory binder's bounded, fixed-Git reader and pinned,
no-symlink directory opening. This is not a standalone cache authenticator.
"""
import argparse
import json
import os
import re
if __package__:
from . import test_inventory_binding as binding
else:
import test_inventory_binding as binding
class PrefetchError(ValueError):
pass
def parse_worktrees(raw):
if type(raw) is not bytes or not raw or not raw.endswith(b'\0\0'):
raise PrefetchError('invalid_worktree_listing')
records = []
seen = set()
for chunk in raw[:-2].split(b'\0\0'):
fields = chunk.split(b'\0')
if not fields or not fields[0].startswith(b'worktree '):
raise PrefetchError('invalid_worktree_listing')
try:
path = fields[0][9:].decode('utf-8')
except UnicodeError:
raise PrefetchError('invalid_worktree_listing') from None
if not path or not os.path.isabs(path) or path != os.path.normpath(path) or path in seen:
raise PrefetchError('invalid_worktree_listing')
seen.add(path)
tokens = fields[1:]
if tokens == [b'bare']:
records.append({'path': path, 'kind': 'bare'})
continue
if len(tokens) < 2 or not re.fullmatch(rb'HEAD [0-9a-f]{40}|HEAD [0-9a-f]{64}', tokens[0]):
raise PrefetchError('invalid_worktree_listing')
branch = None
if tokens[1].startswith(b'branch refs/heads/'):
try:
branch = tokens[1][7:].decode('utf-8')
except UnicodeError:
raise PrefetchError('invalid_worktree_listing') from None
parts = branch.split('/')
if (not branch[len('refs/heads/'):] or '..' in branch or '@{' in branch
or re.search(r'[\x00-\x20\x7f~^:?*\[\\\\]', branch)
or any(not p or p.startswith('.') or p.endswith(('.', '.lock')) for p in parts)):
raise PrefetchError('invalid_worktree_listing')
elif tokens[1] != b'detached':
raise PrefetchError('invalid_worktree_listing')
optional = tokens[2:]
keys = [x.split(b' ', 1)[0] for x in optional]
if any(x not in (b'locked', b'prunable') for x in keys) or len(set(keys)) != len(keys):
raise PrefetchError('invalid_worktree_listing')
record = {'path': path, 'kind': 'attached' if branch is not None else 'detached'}
if branch is not None:
record['branch'] = branch
records.append(record)
if sum(x['kind'] == 'bare' for x in records) != 1:
raise PrefetchError('invalid_worktree_listing')
return records
def inspect_repository(git_dir):
fd = None
try:
fd = binding._canonical_git_dir(git_dir)
pinned = '/proc/self/fd/' + str(fd)
bare = binding._git(pinned, ['rev-parse', '--is-bare-repository'], check=True, git_fd=fd)
if bare is None or bare.stdout != b'true\n':
raise PrefetchError('not_bare_repository')
listing = binding._git(pinned, ['worktree', 'list', '--porcelain', '-z'], check=True, git_fd=fd)
if listing is None:
raise PrefetchError('git_read_unavailable')
records = parse_worktrees(listing.stdout)
attached = [{'branch': x['branch'], 'path': x['path']} for x in records if x['kind'] == 'attached']
return {'status': 'block' if attached else 'pass', 'attached_worktrees': attached,
'reason': 'attached_worktree' if attached else 'no_attached_worktrees'}
except PrefetchError:
raise
except (binding.BindingError, OSError, ValueError, TypeError):
raise PrefetchError('git_read_unavailable') from None
finally:
if fd is not None:
os.close(fd)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--git-dir', required=True, help='Already-admitted canonical absolute bare mirror')
args = parser.parse_args()
try:
result = inspect_repository(args.git_dir)
except PrefetchError as error:
print(json.dumps({'status': 'block', 'attached_worktrees': [], 'reason': str(error)}))
return 2
print(json.dumps(result, ensure_ascii=True))
return 0 if result['status'] == 'pass' else 1
if __name__ == '__main__':
raise SystemExit(main())
+9 -4
View File
@@ -153,10 +153,15 @@ python3 <harness-root>/harness/lib/managed_lock_broker.py probe \
1. Issue workflow는 broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request한다. 별도 승인된 non-Issue workflow는 해당 workflow에 명시된 상위 lock과 repository lock 순서를 따른다. 1. Issue workflow는 broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request한다. 별도 승인된 non-Issue workflow는 해당 workflow에 명시된 상위 lock과 repository lock 순서를 따른다.
2. target과 `repository.git`의 path, ownership, permissions와 bare 상태를 다시 검증한다. 2. target과 `repository.git`의 path, ownership, permissions와 bare 상태를 다시 검증한다.
3. `origin`의 canonical owner/repository identity가 요청과 일치하는지 확인한다. URL 문자열의 credential이나 transport hostname만으로 identity를 판정하지 않는다. 3. `origin`의 canonical owner/repository identity가 요청과 일치하는지 확인한다. URL 문자열의 credential이나 transport hostname만으로 identity를 판정하지 않는다.
4. 명시적으로 검증한 origin에서 mirror refs를 prune/fetch한다. 임의 remote 교체, force reset 또는 다른 repository 재사용은 금지한다. 4. 같은 repository lock을 유지한 상태에서 fetch 직전에 아래 read-only preflight를 실행하고 exit 0인 경우에만 다음 단계로 진행한다. `ADMITTED_GIT_DIR`은 위 path·bare·remote admission에서 확인한 exact canonical absolute `repository.git` 경로이며 helper의 PASS가 admission·인증·lock 소유 증명을 대신하지 않는다.
5. Gitea에서 default branch와 SHA를 다시 읽고 local ref와 일치하는지 확인한다. ```sh
6. 일치한 exact SHA를 이번 작업의 pinned source revision으로 기록한다. python3 -B "$HARNESS_ROOT/harness/lib/repository_prefetch.py" --git-dir "$ADMITTED_GIT_DIR"
7. Issue workflow는 같은 handle에 `release_repository`를 request한다. 별도 non-Issue workflow는 자신의 repository lock을 명시된 순서로 해제한다. ```
Helper는 canonical harness의 `test_inventory_binding.py`에 있는 기존 fixed `/usr/bin/git`, no-symlink directory FD와 bounded reader를 재사용한다. `rev-parse --is-bare-repository`와 `worktree list --porcelain -z`만 조회하며 조회별 5초·64 MiB 출력 한도를 유지한다. Bare 자체의 record와 detached linked worktree는 attached branch로 오인하지 않는다. 등록된 linked worktree가 branch에 붙어 있으면 branch 이름 규칙과 무관하게 모든 exact branch·worktree path를 JSON `attached_worktrees`로 반환해 exit 1로 차단한다. 경로의 공백·개행은 NUL 구분으로 보존하고 JSON에서 escape한다. 조회 실패·malformed·지원하지 않는 결과는 안전한 reason code와 exit 2로 차단하며 Git stderr는 출력하지 않는다. Attached 상태의 자동 repair, detach·reset·worktree prune·ref 삭제·force fetch는 금지하고 운영자에게 해당 경로를 보고한다. 실패는 fetch를 승인하지 않으며 기존 lock/cleanup 계약대로 종료한다. Cooperative lock 아래의 pre-fetch 관찰이지 외부 프로세스의 무단 변경에 대한 sandbox나 원자적 fetch 보장이 아니다.
5. Preflight PASS 뒤 명시적으로 검증한 origin에서 mirror refs를 prune/fetch한다. 임의 remote 교체, force reset 또는 다른 repository 재사용은 금지한다.
6. Gitea에서 default branch와 SHA를 다시 읽고 local ref와 일치하는지 확인한다.
7. 일치한 exact SHA를 이번 작업의 pinned source revision으로 기록한다.
8. Issue workflow는 같은 handle에 `release_repository`를 request한다. 별도 non-Issue workflow는 자신의 repository lock을 명시된 순서로 해제한다.
## Issue worktree 모델 ## Issue worktree 모델
@@ -0,0 +1,150 @@
"""Synthetic local Git pre-fetch gate checks; no provider/network operations."""
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / 'harness/lib/repository_prefetch.py'
class RepositoryPrefetchTests(unittest.TestCase):
def git(self, *args):
result = subprocess.run(['/usr/bin/git', '-c', 'core.hooksPath=/dev/null', *map(str, args)],
env=self.env, capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def setUp(self):
self.temp = tempfile.TemporaryDirectory(dir=os.environ.get('TMPDIR', '/opt/data/cache/scratch'))
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.env = {'PATH': '/usr/bin:/bin', 'HOME': str(self.root), 'GIT_CONFIG_NOSYSTEM': '1',
'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_TERMINAL_PROMPT': '0'}
self.seed = self.root / 'seed'
self.bare = self.root / 'mirror.git'
self.git('init', '-q', self.seed)
self.git('-C', self.seed, 'config', 'user.name', 'Synthetic')
self.git('-C', self.seed, 'config', 'user.email', 'fixture@example.invalid')
(self.seed / 'a').write_text('synthetic')
self.git('-C', self.seed, 'add', 'a')
self.git('-C', self.seed, 'commit', '-qm', 'synthetic')
self.git('clone', '--mirror', self.seed, self.bare)
def gate(self):
self.assertTrue(SCRIPT.is_file(), 'canonical read-only prefetch helper is missing')
result = subprocess.run(['python3', '-B', str(SCRIPT), '--git-dir', str(self.bare)],
env=self.env, capture_output=True, text=True, timeout=15)
return result.returncode, json.loads(result.stdout)
def test_bare_only_passes_real_git_read_only(self):
before = self.git('--git-dir', self.bare, 'show-ref')
code, value = self.gate()
self.assertEqual(code, 0)
self.assertEqual(value, {'status': 'pass', 'attached_worktrees': [], 'reason': 'no_attached_worktrees'})
self.assertEqual(self.git('--git-dir', self.bare, 'show-ref'), before)
def test_attached_blocks_exact_branch_and_path_without_mutation(self):
checkout = self.root / 'attached space\npath'
self.git('--git-dir', self.bare, 'worktree', 'add', '-b', 'issue-fixture', checkout, 'HEAD')
self.git('--git-dir', self.bare, 'worktree', 'lock', checkout)
state = checkout.parent / 'state.json'
state.write_text('sentinel')
before = self.fingerprint(self.bare, checkout, state)
code, value = self.gate()
self.assertEqual(code, 1)
self.assertEqual(value, {'status': 'block', 'attached_worktrees': [
{'branch': 'refs/heads/issue-fixture', 'path': str(checkout)}], 'reason': 'attached_worktree'})
self.assertEqual(self.fingerprint(self.bare, checkout, state), before)
def test_detached_locked_worktree_passes_without_mutation(self):
checkout = self.root / 'detached'
self.git('--git-dir', self.bare, 'worktree', 'add', '--detach', '--lock', checkout, 'HEAD')
before = self.fingerprint(self.bare, checkout)
code, value = self.gate()
self.assertEqual(code, 0)
self.assertEqual(value['attached_worktrees'], [])
self.assertEqual(self.fingerprint(self.bare, checkout), before)
def test_mixed_worktrees_report_all_attached_without_branch_name_policy(self):
detached = self.root / 'detached'
self.git('--git-dir', self.bare, 'worktree', 'add', '--detach', detached, 'HEAD')
expected = []
for number, branch in enumerate(['feature/normal', 'issue-fixture']):
checkout = self.root / ('attached-' + str(number))
self.git('--git-dir', self.bare, 'worktree', 'add', '-b', branch, checkout, 'HEAD')
expected.append({'branch': 'refs/heads/' + branch, 'path': str(checkout)})
code, value = self.gate()
self.assertEqual(code, 1)
self.assertEqual(sorted(value['attached_worktrees'], key=lambda x: x['path']), expected)
def test_unavailable_nonbare_and_symlink_input_fail_closed(self):
self.assertEqual(self.gate()[0], 0)
original = self.bare
link = self.root / 'alias'
link.symlink_to(original, target_is_directory=True)
for target in [self.root / 'absent', self.seed / '.git', link]:
with self.subTest(target=str(target)):
self.bare = target
code, value = self.gate()
self.assertEqual(code, 2)
self.assertEqual(value['status'], 'block')
self.bare = original
# Raw Git stderr must not become diagnostic evidence.
(original / 'config').write_text('secret-like fixture invalid config [')
result = subprocess.run(['python3', '-B', str(SCRIPT), '--git-dir', str(original)],
env=self.env, capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 2)
self.assertNotIn('secret-like', result.stdout + result.stderr)
def test_malformed_nul_listing_is_rejected_not_treated_as_empty(self):
from harness.lib import repository_prefetch as module
raw = self.git('--git-dir', self.bare, 'worktree', 'list', '--porcelain', '-z').encode()
self.assertEqual(module.parse_worktrees(raw)[0]['kind'], 'bare')
for bad in [b'', raw[:-1], raw + raw, raw.replace(b'bare', b'unknown'),
b'worktree /x\0HEAD ' + b'a' * 40 + b'\0detached\0\0',
raw + b'worktree /x\0HEAD ' + b'a' * 40 + b'\0branch refs/heads/../bad\0\0']:
with self.subTest(raw=bad):
with self.assertRaises(module.PrefetchError):
module.parse_worktrees(bad)
def test_reader_failure_and_malformed_output_fail_closed_after_valid_control(self):
from harness.lib import repository_prefetch as module
from unittest.mock import patch
self.assertEqual(module.inspect_repository(str(self.bare))['status'], 'pass')
bare = subprocess.CompletedProcess([], 0, b'true\n')
bad = subprocess.CompletedProcess([], 0, b'invalid secret-like fixture')
for responses in ([bare, bad], [bare, None]):
with self.subTest(responses=str(responses)):
with patch.object(module.binding, '_git', side_effect=responses):
with self.assertRaises(module.PrefetchError) as error:
module.inspect_repository(str(self.bare))
self.assertNotIn('secret-like', str(error.exception))
with patch.object(module.binding, '_git', side_effect=module.binding.BindingError('secret-like fixture')):
with self.assertRaisesRegex(module.PrefetchError, '^git_read_unavailable$'):
module.inspect_repository(str(self.bare))
def test_documentation_contract_requires_gate_before_fetch(self):
text = (ROOT / 'harness/operations/repository-cache.md').read_text()
section = text.split('## 기존 cache 갱신', 1)[1].split('\n## ', 1)[0]
self.assertIn('repository_prefetch.py', section)
self.assertLess(section.index('repository_prefetch.py'), section.index('prune/fetch'))
for rule in ['--git-dir', 'repository lock', '자동 repair', 'exit 0', 'exit 1', 'exit 2']:
self.assertIn(rule, section)
def fingerprint(self, *roots):
import hashlib
result = {}
for root in roots:
for path in ([root] if root.is_file() else root.rglob('*')):
if path.is_file():
st = path.stat()
result[str(path)] = (hashlib.sha256(path.read_bytes()).hexdigest(),
st.st_mode, st.st_mtime_ns, st.st_ino)
return result
if __name__ == '__main__':
unittest.main()