[verified] Define fail-closed worktree creator-role compatibility
This commit is contained in:
@@ -77,7 +77,7 @@ Bootstrap 이후 code review는 canonical [Repository cache 운영 지침](../..
|
|||||||
|
|
||||||
검증에는 위험에 맞게 대표적인 구버전 데이터를 사용한다. 실제 구버전 API·CLI 생성 데이터, 출처가 검증된 기존 fixture 등에서 적절한 방법을 선택하고 source revision·환경·데이터 출처·대표성과 한계를 기록한다. 특정 생성 방법을 모든 프로젝트에 강제하지 않는다. 영향을 받는 조회·소비자의 결과를 승인된 기대 동작과 대조하고, 필요한 검증·근거와 통과·실패·미검증·비적용 상태를 위험·검증 matrix에 연결한다. 프로젝트별 도메인 규칙과 전용 fixture·회귀 테스트는 해당 프로젝트에 둔다.
|
검증에는 위험에 맞게 대표적인 구버전 데이터를 사용한다. 실제 구버전 API·CLI 생성 데이터, 출처가 검증된 기존 fixture 등에서 적절한 방법을 선택하고 source revision·환경·데이터 출처·대표성과 한계를 기록한다. 특정 생성 방법을 모든 프로젝트에 강제하지 않는다. 영향을 받는 조회·소비자의 결과를 승인된 기대 동작과 대조하고, 필요한 검증·근거와 통과·실패·미검증·비적용 상태를 위험·검증 matrix에 연결한다. 프로젝트별 도메인 규칙과 전용 fixture·회귀 테스트는 해당 프로젝트에 둔다.
|
||||||
|
|
||||||
별도 워크트리는 만들지 않는다. 기존 impl Issue 워크트리는 다음 조건을 모두 만족할 때 **검증 전용으로 대여**한다. 구현 재개가 아니므로 `state.json`의 `created_by_role`은 impl role로 확인·보존하고 review role로 바꾸지 않는다.
|
별도 워크트리는 만들지 않는다. 기존 impl Issue 워크트리는 다음 조건을 모두 만족할 때 **검증 전용으로 대여**한다. 구현 재개가 아니므로 `state.json`의 `created_by_role`은 canonical producer literal `gitea-impl-agent` 또는 bounded read-only legacy literal `impl`로만 확인·보존하고 review role로 바꾸지 않는다. 이 두 exact 값 외의 missing/null/non-string/bool/list/dict/whitespace/case variant/role name은 거부하며, legacy literal은 login/authentication alias나 permission grant가 아니다. Admission은 state bytes/mode/mtime/inode를 rewrite/migrate하지 않는다.
|
||||||
|
|
||||||
1. Workflow 동안 broker가 관리하는 exact Issue lease를 유지한다. Cache·registration 검증에는 같은 handle의 `acquire_repository` request를 사용하고, 테스트 전에 `release_repository` request를 보낸다.
|
1. Workflow 동안 broker가 관리하는 exact Issue lease를 유지한다. Cache·registration 검증에는 같은 handle의 `acquire_repository` request를 사용하고, 테스트 전에 `release_repository` request를 보낸다.
|
||||||
2. Canonical path·권한·state identity·common Git directory·detached/locked registration을 검증한다. HEAD는 현재 PR head와 같고 미완료 Git operation과 tracked 변경이 없어야 한다.
|
2. Canonical path·권한·state identity·common Git directory·detached/locked registration을 검증한다. HEAD는 현재 PR head와 같고 미완료 Git operation과 tracked 변경이 없어야 한다.
|
||||||
|
|||||||
@@ -1037,7 +1037,8 @@ def _validate_registered_source(plan, policy_path, deadline_monotonic):
|
|||||||
|
|
||||||
if (type(state) is not dict or state.get('schema') != 'gitea-agent.worktree/v1' or state.get('owner') != t['owner']
|
if (type(state) is not dict or state.get('schema') != 'gitea-agent.worktree/v1' or state.get('owner') != t['owner']
|
||||||
or state.get('repository') != t['repository'] or type(state.get('issue')) is not int
|
or state.get('repository') != t['repository'] or type(state.get('issue')) is not int
|
||||||
or state['issue'] != t['issue'] or state.get('created_by_role') != 'gitea-impl-agent'
|
or state['issue'] != t['issue'] or type(state.get('created_by_role')) is not str
|
||||||
|
or state['created_by_role'] not in ('gitea-impl-agent', 'impl')
|
||||||
or state.get('mode') != 'detached'):
|
or state.get('mode') != 'detached'):
|
||||||
reject('worktree registration identity mismatch')
|
reject('worktree registration identity mismatch')
|
||||||
|
|
||||||
|
|||||||
@@ -168,14 +168,16 @@ python3 <harness-root>/harness/lib/managed_lock_broker.py probe \
|
|||||||
"base_sha": "<full-commit-sha>",
|
"base_sha": "<full-commit-sha>",
|
||||||
"issue_updated_at": "<authoritative-timestamp>",
|
"issue_updated_at": "<authoritative-timestamp>",
|
||||||
"issue_body_sha256": "<sha256>",
|
"issue_body_sha256": "<sha256>",
|
||||||
"created_by_role": "<role>",
|
"created_by_role": "gitea-impl-agent",
|
||||||
"mode": "detached"
|
"mode": "detached"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `base_sha`는 40자리 full commit SHA이고 Gitea 또는 승인된 Discovery에서 확인한 구현 기준 revision이어야 한다.
|
- `base_sha`는 40자리 full commit SHA이고 Gitea 또는 승인된 Discovery에서 확인한 구현 기준 revision이어야 한다.
|
||||||
- `issue_body_sha256`은 구현 승인의 근거가 된 authoritative Issue body의 exact UTF-8 bytes에 대해 계산한다.
|
- `issue_body_sha256`은 구현 승인의 근거가 된 authoritative Issue body의 exact UTF-8 bytes에 대해 계산한다.
|
||||||
- `created_by_role`은 credential 값이 아닌 harness role identity다. PID나 현재 process 존재 여부를 worktree 소유권 또는 활성 상태의 정본으로 사용하지 않는다.
|
- `created_by_role`은 credential 값이 아닌 harness role identity이며 신규 구현 producer는 정확히 `gitea-impl-agent`를 기록한다. PID나 현재 process 존재 여부를 worktree 소유권 또는 활성 상태의 정본으로 사용하지 않는다.
|
||||||
|
- Review admission은 schema가 `gitea-agent.worktree/v1`인 기존 state에 한해 정확히 `gitea-impl-agent` 또는 legacy `impl`만 read-only compatibility로 허용한다. 이는 login/authentication alias가 아니며 그 자체로 permission을 부여하지 않는다. 누락, null, bool, list, dict, 빈 문자열, whitespace/case 변형과 다른 role은 거부한다.
|
||||||
|
- 이 bounded exception은 `state.json`을 rewrite/migrate하지 않는다. admission 전후 exact bytes, mode, mtime과 inode를 보존한다.
|
||||||
- 파일은 run-unique sibling temporary file에 mode `0600`으로 완성하고 flush한 뒤 no-replace atomic rename으로 게시한다. 기존 `state.json`을 덮어쓰지 않는다.
|
- 파일은 run-unique sibling temporary file에 mode `0600`으로 완성하고 flush한 뒤 no-replace atomic rename으로 게시한다. 기존 `state.json`을 덮어쓰지 않는다.
|
||||||
|
|
||||||
## Worktree admission gate
|
## Worktree admission gate
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ create/reuse deltas, metadata causality and failed-receipt preservation are
|
|||||||
fail-closed. The workflow contract and canonical runtime artifact paths are in
|
fail-closed. The workflow contract and canonical runtime artifact paths are in
|
||||||
`harness/agents/impl/skills/gitea-issue-rework/references/readback.md`.
|
`harness/agents/impl/skills/gitea-issue-rework/references/readback.md`.
|
||||||
|
|
||||||
|
The review native adapter's registered-worktree fixture accepts only exact
|
||||||
|
`gitea-impl-agent` or read-only legacy `impl` creator-role literals. It preserves
|
||||||
|
`state.json` bytes/mode/mtime/inode and rejects malformed role values without
|
||||||
|
relaxing the Issue lease, Git, target, or security guards.
|
||||||
|
|
||||||
It validates all `test*.py` files recursively under exactly these required
|
It validates all `test*.py` files recursively under exactly these required
|
||||||
roles: `common`, `spec`, `impl`, and `review`. `harness/tests`, every role, and
|
roles: `common`, `spec`, `impl`, and `review`. `harness/tests`, every role, and
|
||||||
every nested test package must have an `__init__.py`; every role and every
|
every nested test package must have an `__init__.py`; every role and every
|
||||||
|
|||||||
@@ -252,7 +252,57 @@ class NativeReviewTests(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
lease = m.HeldIssueLease(fd, policy, time.monotonic() + 30)
|
lease = m.HeldIssueLease(fd, policy, time.monotonic() + 30)
|
||||||
|
canonical_bytes = state.read_bytes(); canonical_stat = state.stat()
|
||||||
lease.check(p)
|
lease.check(p)
|
||||||
|
self.assertEqual(state.read_bytes(), canonical_bytes)
|
||||||
|
self.assertEqual((state.stat().st_mode, state.stat().st_mtime_ns, state.stat().st_ino),
|
||||||
|
(canonical_stat.st_mode, canonical_stat.st_mtime_ns, canonical_stat.st_ino))
|
||||||
|
# The bounded compatibility admission is read-only: mutate only
|
||||||
|
# this scratch fixture from canonical producer state to legacy impl.
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'detached'}))
|
||||||
|
state.chmod(0o600)
|
||||||
|
legacy_bytes = state.read_bytes(); legacy_stat = state.stat()
|
||||||
|
try:
|
||||||
|
lease.check(p)
|
||||||
|
except m.ValidationError as exc:
|
||||||
|
self.fail(f'valid legacy impl registration must be admitted: {exc}')
|
||||||
|
after_stat = state.stat()
|
||||||
|
self.assertEqual(state.read_bytes(), legacy_bytes)
|
||||||
|
self.assertEqual((after_stat.st_mode, after_stat.st_mtime_ns, after_stat.st_ino),
|
||||||
|
(legacy_stat.st_mode, legacy_stat.st_mtime_ns, legacy_stat.st_ino))
|
||||||
|
for bad_role in [None, True, False, 1, [], {}, '', ' impl', 'impl ', 'Impl', 'IMPL', 'gitea-impl-agent ', 'review', 'gitea-review-agent', 'gitea-spec-agent', 'gitea-release-agent', 'other']:
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': bad_role, 'mode': 'detached'}))
|
||||||
|
state.chmod(0o600)
|
||||||
|
with self.subTest(bad_role=repr(bad_role)), self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'):
|
||||||
|
lease.check(p)
|
||||||
|
missing_role = json.loads(legacy_bytes); missing_role.pop('created_by_role')
|
||||||
|
state.write_text(json.dumps(missing_role)); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
bool_issue = json.loads(legacy_bytes); bool_issue['issue'] = True
|
||||||
|
state.write_text(json.dumps(bool_issue)); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'other', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'detached'})); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'other',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'detached'})); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 4, 'created_by_role': 'impl', 'mode': 'detached'})); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'wrong/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'detached'})); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'attached'})); state.chmod(0o600)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree registration identity mismatch'): lease.check(p)
|
||||||
|
state.write_text(json.dumps({'schema': 'gitea-agent.worktree/v1', 'owner': 'acme', 'repository': 'repo',
|
||||||
|
'issue': 3, 'created_by_role': 'impl', 'mode': 'detached'})); state.chmod(0o600)
|
||||||
|
wrong_head = copy.deepcopy(p); wrong_head['target']['head'] = 'b' * 40
|
||||||
|
wrong_head['body'] = wrong_head['body'].replace(head, 'b' * 40)
|
||||||
|
with self.assertRaisesRegex(m.ValidationError, 'worktree HEAD mismatch'): lease.check(wrong_head)
|
||||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||||
with self.assertRaises(m.ValidationError): lease.check(p)
|
with self.assertRaises(m.ValidationError): lease.check(p)
|
||||||
self.assertNotIn('lock:', Path(f'/proc/self/fdinfo/{fd}').read_text())
|
self.assertNotIn('lock:', Path(f'/proc/self/fdinfo/{fd}').read_text())
|
||||||
|
|||||||
Reference in New Issue
Block a user