Add shared managed lock broker

This commit is contained in:
2026-10-07 02:23:39 +00:00
parent 18676070b2
commit 470c7fb09b
16 changed files with 1583 additions and 120 deletions
+57 -13
View File
@@ -57,16 +57,60 @@ Repository cache 경로는 고정된 machine-local 절대경로나 현재 작업
8. `worktrees`, `issue-<issue-number>`와 `checkout`은 symlink가 아닌 directory이고 실행 role 소유여야 한다. `worktrees`와 Issue directory는 mode `0700`, `state.json`은 mode `0600`이어야 하며 checkout과 그 상위 runtime 경로는 group 또는 world-writable이면 안 된다.
9. 기존 path가 malformed 상태면 삭제, chmod, chown, reset 또는 덮어쓰기로 자동 복구하지 않고 fail-closed한다. 권한 변경은 운영자가 exact path와 mode를 명시적으로 승인한 경우에만 수행하고 즉시 재검증한다.
## Managed-lock broker (Issue workflow 필수)
Bootstrap 이후 모든 Issue workflow는 role-neutral shared managed-lock broker 하나를 사용한다. 개별 impl/spec/review skill이 직접 `flock`을 획득하거나 broker 규칙을 재정의하지 않는다. Broker executable은 `harness/lib/managed_lock_broker.py`이며 authenticated request action은 `status`, `renew`, `acquire_repository`, `release_repository`, `release`이고 최대 TTL은 `3600`초다.
- Bootstrap lock을 해제한 뒤 broker를 시작한다. checkout 밖 trusted parent 아래 role-owned mode `0700` runtime directory를 미리 만들고, run-unique handle과 socket path를 사용한다. Handle file은 mode `0600`이고 socket은 mode `0600`이며 둘 다 symlink가 아니어야 한다.
- `serve`는 tracked background process로 시작하고 ready JSON을 기다린 뒤에만 Issue 작업을 시작한다. Client는 raw token을 직접 read/log하지 않으며, broker public `request` API가 handle을 인증하고 token을 캡슐화한다.
이 broker의 보안 경계에서 filesystem owner UID는 trusted principal이다. mode `0700`/`0600`과 random bearer token은 다른 UID 또는 foreign token을 사용한 작업을 막지만, 같은 UID를 공유하는 상호 비신뢰 프로세스를 서로 격리하지는 못한다. 그런 격리가 필요한 배포는 각 role을 distinct UID로 실행해야 한다.
- Issue lock은 broker의 `serve`가 관리한다. Repository lock이 필요하면 같은 handle로 `acquire_repository`를 request하고, 끝나면 `release_repository`를 request한다. 중첩 repository lock에 raw `flock ... sleep` 또는 별도 lock holder를 사용하지 않는다.
- TTL 만료 전에 `renew`를 request한다. Broker crash 뒤 kernel이 advisory lock을 해제해도 stale handle은 진단 정보일 뿐이며 `probe`가 authoritative하다.
- 정상, 오류와 cancel 경로는 동일한 cleanup을 수행한다: 보유 중이면 `release_repository`, `release`, broker process wait/reap, 그리고 Issue `probe`를 실행해 `free=true`를 요구한다. 다른 Issue workflow가 합법적으로 repository lock을 먼저 획득할 수 있으므로 완료 gate는 Issue resource를 검사하고, 종료한 broker process가 자신의 repository FD를 더 이상 보유하지 않음을 wait/reap으로 증명한다. 이 순서를 검증하기 전에는 completed/log/report를 주장하지 않는다. Lock 파일은 절대 unlink하지 않는다.
### Canonical command templates
```sh
# <runtime-dir> is pre-created mode 0700 outside every checkout.
python3 <harness-root>/harness/lib/managed_lock_broker.py serve \
--root <harness-root> \
--owner <owner> --repository <repository> --issue <issue-number> \
--role <role> --run-id <run-id> \
--handle <runtime-dir>/<run-id>.handle \
--socket <runtime-dir>/<run-id>.sock \
--ttl-seconds <ttl-seconds>
# After ready JSON; never print/read the token value.
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
--handle <runtime-dir>/<run-id>.handle --action status
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
--handle <runtime-dir>/<run-id>.handle --action acquire_repository
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
--handle <runtime-dir>/<run-id>.handle --action release_repository
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
--handle <runtime-dir>/<run-id>.handle --action renew --ttl-seconds <ttl-seconds>
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
--handle <runtime-dir>/<run-id>.handle --action release
# After wait/reap of the tracked broker process; free=true is mandatory for the Issue resource.
python3 <harness-root>/harness/lib/managed_lock_broker.py probe \
--root <harness-root> \
--owner <owner> --repository <repository> --issue <issue-number> \
--resource issue
```
## Lock 규칙
- Linux OS advisory lock을 사용한다. lock file descriptor는 symlink를 따라가지 않는 방식으로 열고 원자적으로 lock을 획득한다.
- 독립적인 신규 cache 초기화 순서는 `bootstrap.lock → repository.lock`이다.
- Issue workflow가 기존 cache를 사용할 때의 순서는 `issue-<issue-number>.lock → repository.lock`이다.
- Issue 작업에 사용할 cache가 없으면 Issue lock 획득 전에 신규 cache 초기화 절차를 완료한다. 초기화 중에는 Issue를 변경하지 않는다. Bootstrap의 repository lock과 bootstrap lock을 모두 해제한 뒤 Issue lock을 획득하며, 이후에는 기존 cache 사용 순서인 `issue-<issue-number>.lock → repository.lock`을 따른다. Issue 작업의 기준 snapshot은 Issue lock 획득 후 읽는다.
- Worktree 생성, 등록, lock, 이동, 제거 또는 repair는 Issue lock을 유지한 채 repository lock을 추가로 획득해서 수행한다. 일반 파일 편집, 테스트와 commit에는 Issue lock만 유지하고 repository lock은 보유하지 않는다.
- Issue workflow가 기존 cache를 사용할 때 broker가 Issue lease를 보유하고, 같은 broker handle의 `acquire_repository` request로 repository lease를 중첩한다. 개념적 순서는 `Issue(broker) → repository(broker)`이며 client는 raw descriptor lock을 사용하지 않는다.
- Issue 작업에 사용할 cache가 없으면 Issue broker lease 획득 전에 신규 cache 초기화 절차를 완료한다. 초기화 중에는 Issue를 변경하지 않는다. Bootstrap의 repository lock과 bootstrap lock을 모두 해제한 뒤 broker를 시작하고 Issue lease를 획득하며, 이후에는 broker의 `Issue → acquire_repository` 순서를 따른다. Issue 작업의 기준 snapshot은 broker Issue lease 획득 후 읽는다.
- Worktree 생성, 등록, lock, 이동, 제거 또는 repair는 broker Issue lease를 유지한 채 같은 broker handle의 `acquire_repository` request로 repository lease를 추가 획득해서 수행한다. 일반 파일 편집, 테스트와 commit에는 broker Issue lease만 유지하고 repository lease는 보유하지 않는다.
- 같은 repository의 다른 Issue worktree를 검사, 변경 또는 제거하지 않는다. 한 Issue workflow는 자신의 exact Issue worktree만 다룬다.
- 반대 순서로 lock을 획득하거나 이미 잡은 상위 lock을 해제한 뒤 하위 작업을 계속하지 않는다.
- 정상 종료, 오류와 중단 경로 모두에서 descriptor를 닫아 lock을 해제한다.
- 정상 종료, 오류와 중단 경로 모두에서 broker cleanup protocol을 수행한다: repository lease release, Issue lease release, broker wait/reap, Issue resource `probe --resource issue`의 `free=true` readback. 다른 Issue가 그 사이 repository lock을 획득할 수 있으므로 repository의 순간적 busy 상태만으로 현재 broker cleanup 실패로 판정하지 않는다.
- lock 파일은 unlink하지 않는다. 프로세스 종료 시 kernel이 advisory lock을 해제하므로 남아 있는 파일을 stale active lock으로 간주하거나 PID 기준으로 삭제하지 않는다.
## 신규 cache 초기화
@@ -87,13 +131,13 @@ Repository cache 경로는 고정된 machine-local 절대경로나 현재 작업
## 기존 cache 갱신
1. 필요한 상위 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 상태를 다시 검증한다.
3. `origin`의 canonical owner/repository identity가 요청과 일치하는지 확인한다. URL 문자열의 credential이나 transport hostname만으로 identity를 판정하지 않는다.
4. 명시적으로 검증한 origin에서 mirror refs를 prune/fetch한다. 임의 remote 교체, force reset 또는 다른 repository 재사용은 금지한다.
5. Gitea에서 default branch와 SHA를 다시 읽고 local ref와 일치하는지 확인한다.
6. 일치한 exact SHA를 이번 작업의 pinned source revision으로 기록한다.
7. repository lock을 해제한다.
7. Issue workflow는 같은 handle에 `release_repository`를 request한다. 별도 non-Issue workflow는 자신의 repository lock을 명시된 순서로 해제한다.
## Issue worktree 모델
@@ -131,7 +175,7 @@ Repository cache 경로는 고정된 machine-local 절대경로나 현재 작업
Worktree를 신규 생성하거나 기존 worktree에서 구현을 재개하기 전에 다음을 모두 확인한다.
1. exact `owner`, `repository`와 Issue 번호가 검증됐고 Issue lock을 획득했다.
1. exact `owner`, `repository`와 Issue 번호가 검증됐고 broker `serve`의 ready response로 Issue lease 보유를 확인했다.
2. Authenticated Gitea API adapter로 role identity, exact repository metadata, Issue body, `updated_at`과 구현 승인 상태를 읽었다.
3. repository cache가 이 문서의 path, ownership, permission, bare, remote identity와 authoritative ref 검증을 통과했다.
4. 구현에 사용할 exact base SHA와 그 출처를 결정했다. Discovery pinned revision과 현재 default branch SHA가 다르면 어느 한쪽을 자동 선택하지 않고 변경을 조사해 요구사항과 충돌하지 않는 base를 확정하거나 중단한다.
@@ -143,7 +187,7 @@ Gate가 끝난 뒤 write 직전에 Issue body, `updated_at`과 구현 승인 상
## Worktree 신규 생성
1. Issue lock을 유지한 채 repository lock을 획득한다.
1. Broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request해 repository lease를 중첩한다.
2. cache와 base commit을 다시 검증하고 `worktrees`와 Issue directory를 canonical mode로 생성한 뒤 즉시 `lstat`한다.
3. final `checkout`, `state.json`과 동일 Issue의 Git worktree registration이 모두 없는지 다시 확인한다.
4. `git --git-dir=<repository.git> worktree add --detach <checkout> <full-base-sha>`와 동등한 호출로 exact final checkout을 생성한다. branch 생성 option을 사용하지 않는다.
@@ -156,13 +200,13 @@ Gate가 끝난 뒤 write 직전에 Issue body, `updated_at`과 구현 승인 상
- `git rev-parse --git-common-dir`가 canonical `repository.git`과 동일하다.
- `git worktree list --porcelain`에 exact checkout path가 한 번만 있고 locked 상태다.
- checkout path와 `state.json`의 identity, ownership과 permission이 canonical 계약을 충족한다.
8. 모든 검증 후 repository lock을 해제한다. Issue lock은 해당 Issue workflow가 끝날 때까지 유지한다.
8. 모든 검증 후 같은 handle에 `release_repository`를 request한다. Broker Issue lease는 해당 Issue workflow가 끝날 때까지 유지한다.
Git 명령의 성공 exit만으로 생성 완료를 판정하지 않는다. Git registration, checkout, state와 Issue snapshot을 모두 검증해야 한다.
## 기존 worktree 재사용
기존 directory 또는 Git registration 중 하나라도 발견되면 신규 생성으로 덮어쓰지 않는다. Issue lock을 유지하고 repository lock 아래에서 다음을 모두 확인한다.
기존 directory 또는 Git registration 중 하나라도 발견되면 신규 생성으로 덮어쓰지 않는다. Broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 얻은 repository lease 아래에서 다음을 모두 확인한다.
- `state.json`이 유효한 `gitea-agent.worktree/v1`이고 owner, repository, Issue와 role이 현재 workflow와 일치한다.
- exact checkout이 canonical `repository.git`에 한 번만 등록돼 있고 locked 상태다.
@@ -176,7 +220,7 @@ Dirty worktree, untracked 파일 또는 base 이후 commit은 진행 중 구현
## Worktree 작업과 delivery
- 일반 편집, 테스트와 commit은 exact Issue lock 아래에서만 수행한다. 현재 worktree 외의 source, 다른 checkout 또는 installed package를 수정하거나 구현 근거로 사용하지 않는다.
- 일반 편집, 테스트와 commit은 exact broker Issue lease 아래에서만 수행한다. 현재 worktree 외의 source, 다른 checkout 또는 installed package를 수정하거나 구현 근거로 사용하지 않는다.
- commit 전후 base SHA, HEAD SHA, staged·unstaged·untracked 상태를 기록한다. detached 상태를 local branch로 자동 전환하지 않는다.
- 명시적으로 승인된 push는 source와 destination을 모두 지정해 `HEAD:refs/heads/<approved-branch>` 형태와 동등하게 수행한다. Canonical cache remote에 `remote.<name>.mirror=true`가 설정돼 있으면 credential을 포함하지 않은 explicit repository URL을 사용하거나 `git -c remote.<name>.mirror=false push <name> HEAD:refs/heads/<approved-branch>`와 동등하게 해당 호출에서만 mirror 동작을 끈다. Persistent remote config는 변경하지 않는다. destination 없는 push, force push와 mirror push는 금지한다.
- Push 후 Gitea에서 exact remote branch SHA를 다시 읽어 local HEAD와 일치하는 경우에만 전달 완료로 판정한다. PR 생성, merge, Issue comment·label·state 변경은 각각 별도 승인이 필요하다.
@@ -184,7 +228,7 @@ Dirty worktree, untracked 파일 또는 base 이후 commit은 진행 중 구현
## Worktree 실패 처리와 제거
현재 run이 worktree를 생성했고 편집이나 commit이 시작되지 않았으며 worktree가 clean하고 HEAD가 base SHA와 같다는 것을 증명할 때만 생성 실패를 자동 cleanup할 수 있다. 이때 Issue lock과 repository lock을 모두 유지하고 worktree lock을 해제한 뒤 `git worktree remove`와 동등한 Git operation으로 exact registration과 checkout을 제거한다. filesystem 삭제를 먼저 수행하지 않는다.
현재 run이 worktree를 생성했고 편집이나 commit이 시작되지 않았으며 worktree가 clean하고 HEAD가 base SHA와 같다는 것을 증명할 때만 생성 실패를 자동 cleanup할 수 있다. 이때 broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 repository lease를 중첩한 뒤 worktree lock을 해제하고 `git worktree remove`와 동등한 Git operation으로 exact registration과 checkout을 제거한다. filesystem 삭제를 먼저 수행하지 않는다.
다음 상태는 자동 cleanup하지 않고 보존한 채 partial 또는 fail-closed로 보고한다.
@@ -198,7 +242,7 @@ Dirty worktree, untracked 파일 또는 base 이후 commit은 진행 중 구현
정상 worktree 제거는 사용자가 exact worktree 폐기를 승인했거나, 승인된 delivery가 완료되어 remote branch SHA가 local HEAD와 일치함을 authoritative readback한 경우에만 수행한다. 제거 전 base SHA, final HEAD SHA, dirty·untracked 상태, 미전달 commit과 remote 보존 여부를 확인하고 보고한다. Dirty 상태, 미전달 commit 또는 판정 불가능한 파일이 있으면 `--force`, filesystem 재귀 삭제, reset 또는 clean으로 우회하지 않는다.
제거할 때는 Issue lock과 repository lock 아래에서 worktree lock을 해제하고 Git operation으로 checkout과 registration을 먼저 제거한다. 두 대상의 부재를 확인한 뒤 exact `state.json`과 비어 있는 Issue worktree directory만 제거하며, 현재 run이 만든 state temporary file이 있으면 identity를 확인한 뒤 함께 제거한다. 다른 파일이나 하위 directory가 남아 있으면 재귀 삭제하지 않고 중단한다. 마지막으로 exact checkout, Git registration, `state.json`과 Issue worktree directory가 모두 없는지 다시 검증한다.
제거할 때는 broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 repository lease를 중첩한 상태에서 worktree lock을 해제하고 Git operation으로 checkout과 registration을 먼저 제거한다. 두 대상의 부재를 확인한 뒤 exact `state.json`과 비어 있는 Issue worktree directory만 제거하며, 현재 run이 만든 state temporary file이 있으면 identity를 확인한 뒤 함께 제거한다. 다른 파일이나 하위 directory가 남아 있으면 재귀 삭제하지 않고 중단한다. 마지막으로 exact checkout, Git registration, `state.json`과 Issue worktree directory가 모두 없는지 다시 검증하고 `release_repository`를 request한다.
## Credential 안전