feat(harness): allocate canonical issue-scoped broker runtime
This commit is contained in:
@@ -63,8 +63,10 @@ Repository cache 경로는 고정된 machine-local 절대경로나 현재 작업
|
||||
|
||||
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 작업을 시작한다. Ready 전에 broker는 bound **filesystem socket node**의 type·owner UID·device/inode와 exact mode `0600`을 검증한다. Socket FD의 sockfs metadata는 이 검증을 대신하지 못한다. Client는 raw token을 직접 read/log하지 않으며, broker public `request` API가 handle을 인증하고 token을 캡슐화한다.
|
||||
- Broker runtime은 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/runtime/<workflow-run-id>/`에 둔다. `<workflow-run-id>`는 verification stage의 `<run-id>`와 독립적이다. Runtime은 broker 소유 transient active resource이며 `verification/<run-id>`는 보존되는 evidence다.
|
||||
- Runtime 디렉터리는 broker가 owner/repository/issue/role/workflow-run-id/TTL을 받아 만들고 serve-ready JSON의 `handle`과 `socket` 필드로 실제 경로를 반환한다. 호출자는 이 값을 각각 `HANDLE`과 `SOCKET` 변수로 사용하며 임의 runtime directory, handle 또는 socket을 미리 만들지 않는다. Canonical basename은 `broker.handle`과 `broker.sock`이며 이미 제공한 handle/socket 옵션은 정확히 canonical 경로일 때만 허용된다.
|
||||
- runtime 생성은 승인이나 admission을 의미하지 않는다. Admission 전에 Issue lock을 획득하고, 필요한 trusted parent 생성만을 위한 짧은 own-repository lock을 획득한 다음 runtime을 만든다. Bootstrap은 Issue lease 아래 실행하지 않는다.
|
||||
- `serve`는 tracked background process로 시작하고 ready JSON을 기다린 뒤에만 Issue 작업을 시작한다. Ready 전에 broker는 bound filesystem socket node의 type·owner UID·device/inode와 exact mode `0600`을 검증한다. Client는 raw token을 read/log하지 않고 public request API가 인증을 캡슐화한다.
|
||||
|
||||
이 broker의 보안 경계에서 filesystem owner UID는 trusted principal이다. mode `0700`/`0600`과 random bearer token은 다른 UID 또는 foreign token을 사용한 작업을 막지만, 같은 UID를 공유하는 상호 비신뢰 프로세스를 서로 격리하지는 못한다. 그런 격리가 필요한 배포는 각 role을 distinct UID로 실행해야 한다.
|
||||
|
||||
@@ -73,34 +75,33 @@ Bootstrap 이후 모든 Issue workflow는 role-neutral shared managed-lock broke
|
||||
- 정상, 오류와 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하지 않는다.
|
||||
|
||||
- 소켓 정리는 부모 directory FD를 기준으로 기록한 filesystem device/inode·type·owner가 일치하는 경로만 삭제한다. 경로가 다른 객체나 symlink로 교체되면 그 객체는 삭제하지 않고 `socket-identity-changed`로 보고한다. 같은 UID의 경쟁 프로세스까지 원자적 격리를 보장하지 않으며, SIGKILL/crash 뒤 소켓 삭제를 보장하지 않는다. Stale 경로를 자동 삭제하거나 재사용하지 않는다. 첫 소켓 정리 실패 뒤에는 부모 FD를 닫고 경로를 다시 열어 자동 재시도하지 않는다. 잔여 경로의 제거는 운영자가 exact 경로와 소유권을 확인하는 별도 복구이며, 반복 cleanup은 이전 부분 실패를 보존한다.
|
||||
- 정상 release 응답은 기존 `{"status":"released"}`를 유지한다. 정리가 일부 실패하면 broker는 `{"error":"cleanup-incomplete","cleanup":...}`를 응답하고 종료하며, `cleanup`의 `lease_released`, `socket_removed`, `handle_saved`, `errors`로 해제와 잔여 정리를 구분한다. Handle 기록 가능 시 같은 결과를 비밀값 없이 저장하고 stderr에도 오류 코드만 보고한다. 부분 실패는 nonzero process exit로 보고하며, 성공한 cleanup을 재적용하거나 실패 이력을 성공으로 덮어쓰지 않는다. 이 오류만으로 이미 해제한 lease를 재해제하거나 workflow 외부 write를 반복하지 않는다.
|
||||
- 정상 release 응답은 기존 `{"status":"released"}`를 유지한다. 정리가 일부 실패하면 broker는 `{"error":"cleanup-incomplete","cleanup":...}`를 응답하고 종료하며, `cleanup`의 `lease_released`, `socket_removed`, `handle_saved`, `errors`로 해제와 잔여 정리를 구분한다. Handle 기록 가능 시 같은 결과를 비밀값 없이 저장하고 stderr에도 오류 코드만 보고한다. 부분 실패는 nonzero process exit로 보고하며, 성공한 cleanup을 재적용하거나 실패 이력을 성공으로 덮어쓰지 않는다. 성공 기록을 실패로 덮어쓰지 않는다. 이 오류만으로 이미 해제한 lease를 재해제하거나 workflow 외부 write를 반복하지 않는다.
|
||||
- 완료 gate는 기존 release → wait/reap → Issue probe 순서에 더해 broker의 정상 종료와 run-owned 소켓 경로 부재를 확인한다. `probe free=true`는 lock 해제 증거이지 소켓 권한·삭제 증거가 아니다. Lease 해제와 소켓·handle 정리 실패는 별도로 보고하고 완료로 간주하지 않는다.
|
||||
|
||||
### Canonical command templates
|
||||
|
||||
```sh
|
||||
# <runtime-dir> is pre-created mode 0700 outside every checkout.
|
||||
# Runtime paths come only from broker ready JSON; never create them or read the token.
|
||||
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 \
|
||||
--role <role> --run-id <workflow-run-id> \
|
||||
--ttl-seconds <ttl-seconds>
|
||||
# Parse only public paths from ready JSON; do not print token-bearing fields.
|
||||
HANDLE=<ready-json-HANDLE>
|
||||
SOCKET=<ready-json-SOCKET>
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
|
||||
--handle "$HANDLE" --action status
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
|
||||
--handle "$HANDLE" --action acquire_repository
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
|
||||
--handle "$HANDLE" --action release_repository
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
|
||||
--handle "$HANDLE" --action renew --ttl-seconds <ttl-seconds>
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py request \
|
||||
--handle "$HANDLE" --action release
|
||||
|
||||
# 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.
|
||||
# After wait/reap of tracked broker; free=true mandatory for Issue resource.
|
||||
python3 <harness-root>/harness/lib/managed_lock_broker.py probe \
|
||||
--root <harness-root> \
|
||||
--owner <owner> --repository <repository> --issue <issue-number> \
|
||||
@@ -186,15 +187,23 @@ Worktree를 신규 생성하거나 기존 worktree에서 구현을 재개하기
|
||||
3. repository cache가 이 문서의 path, ownership, permission, bare, remote identity와 authoritative ref 검증을 통과했다.
|
||||
4. 구현에 사용할 exact base SHA와 그 출처를 결정했다. Discovery pinned revision과 현재 default branch SHA가 다르면 어느 한쪽을 자동 선택하지 않고 변경을 조사해 요구사항과 충돌하지 않는 base를 확정하거나 중단한다.
|
||||
5. `repository.git`에 exact base SHA의 commit object가 존재한다. 문자열 형식이나 object 존재만으로 Gitea 또는 승인된 Discovery에서 유래했다는 검증을 대체하지 않는다.
|
||||
6. canonical target path, `state.json`과 Git worktree registration의 존재 여부를 함께 조사했다.
|
||||
7. 신규 생성이면 target path와 동일 Issue registration이 모두 없어야 한다. 재사용이면 아래의 기존 worktree 검증을 모두 통과해야 한다.
|
||||
6. canonical `checkout`, `state.json`과 Git worktree registration의 존재 여부를 함께 조사하고 Issue container의 entry·type·owner·mode를 확인한다. 안전한 `runtime/`·`verification/`만 존재하고 checkout·state·동일 Issue registration이 모두 없는 경우는 container-only 신규 생성 후보이며 기존 worktree가 아니다. Checkout·state·registration 중 일부만 있거나 unknown entry가 있으면 자동 cleanup/repair 없이 거부한다.
|
||||
7. 신규 생성은 container 부재 또는 위 container-only 상태에서만 허용한다. 재사용은 일관된 checkout·state·registration이 존재하고 아래의 기존 worktree 검증을 모두 통과해야 한다. Runtime 생성 자체는 구현 승인이나 admission을 의미하지 않는다.
|
||||
|
||||
계획된 container-shape 확인은 같은 handle로 repository lease를 획득한 뒤 공통 helper로 실행한다. Helper는 exact Git registration을 read-only로 조회하며 다음 JSON `container`를 반환한다: `absent`, `runtime-only`, `checkout-candidate`, `invalid`. 앞의 두 상태는 신규 생성 후보일 뿐 구현 승인·base·cache 검증을 대신하지 않으며, `checkout-candidate`도 아래 기존 worktree 검증을 모두 통과해야 한다. `invalid` 또는 helper 오류는 자동 복구 없이 중단한다. 실제 state 계약은 `state.json`이고 별도의 `registration.json`을 만들지 않는다.
|
||||
|
||||
```sh
|
||||
python3 <harness-root>/harness/lib/runtime_root.py inspect-worktree \
|
||||
--root <harness-root> --owner <owner> --repository <repository> \
|
||||
--issue <issue-number> --handle "$HANDLE"
|
||||
```
|
||||
|
||||
Gate가 끝난 뒤 write 직전에 Issue body, `updated_at`과 구현 승인 상태를 다시 읽는다. admission snapshot과 다르면 worktree를 생성하거나 기존 worktree를 수정하지 않는다.
|
||||
|
||||
## Worktree 신규 생성
|
||||
|
||||
1. Broker Issue lease를 유지한 채 같은 handle에 `acquire_repository`를 request해 repository lease를 중첩한다.
|
||||
2. cache와 base commit을 다시 검증하고 `worktrees`와 Issue directory를 canonical mode로 생성한 뒤 즉시 `lstat`한다.
|
||||
2. cache와 base commit을 다시 검증한다. Broker가 준비한 `worktrees`와 Issue directory는 canonical mode·owner·type을 다시 검증하여 그대로 사용한다. Container 부재가 입증된 경로만 별도 안전한 생성 절차로 만들며 기존 경로를 덮어쓰거나 repair하지 않는다.
|
||||
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을 사용하지 않는다.
|
||||
5. worktree를 Issue 용도로 `git worktree lock`하고 reason에 repository credential이나 민감정보 없이 Issue 번호를 기록한다.
|
||||
@@ -212,7 +221,7 @@ Git 명령의 성공 exit만으로 생성 완료를 판정하지 않는다. Git
|
||||
|
||||
## 기존 worktree 재사용
|
||||
|
||||
기존 directory 또는 Git registration 중 하나라도 발견되면 신규 생성으로 덮어쓰지 않는다. Broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 얻은 repository lease 아래에서 다음을 모두 확인한다.
|
||||
기존 `checkout`, `state.json` 또는 동일 Issue의 Git registration 중 하나라도 발견되면 신규 생성으로 덮어쓰지 않는다. 안전한 runtime/verification-only container는 이 재사용 조건에 포함하지 않는다. 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 상태다.
|
||||
@@ -248,7 +257,9 @@ 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으로 우회하지 않는다.
|
||||
|
||||
제거할 때는 broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 repository lease를 중첩한 상태에서 worktree lock을 해제하고 Git operation으로 checkout과 registration을 먼저 제거한다. 제거 전에 아래 verification 계약에 따라 보존할 artifact의 identity, manifest·plan digest와 읽기 가능 여부를 확인한다. 두 대상의 부재를 확인한 뒤 exact `state.json`과 identity가 확인된 현재 run의 state temporary file만 제거한다. `verification/`이 남으면 Issue directory를 보존한다. 다른 알 수 없는 파일·directory가 남아 있으면 자동 정리하지 않고 partial로 보고한다. 비어 있는 Issue directory만 제거할 수 있다. 마지막으로 exact checkout, Git registration과 `state.json`의 부재 및 보존한 verification의 경로·digest가 제거 전과 같음을 다시 확인하고 `release_repository`를 request한다. Verification이 정상 보존돼 Issue directory가 남는 것은 checkout 제거 실패가 아니다.
|
||||
제거할 때는 broker Issue lease를 유지하고 같은 handle의 `acquire_repository` request로 repository lease를 중첩한 상태에서 worktree lock을 해제하고 Git operation으로 checkout과 registration을 먼저 제거한다. 제거 전에 verification 계약에 따라 보존할 artifact identity, manifest·plan digest와 읽기 가능 여부를 확인한다. 두 대상의 부재를 확인한 뒤 exact `state.json`과 identity가 확인된 현재 run의 state temporary file만 제거한다. `runtime/`과 `verification/`은 서로 독립된 자원이며 worktree 제거 절차는 이를 삭제하거나 active broker/socket/handle을 건드리지 않는다. `verification/`이 남으면 Issue directory를 보존한다. `runtime/`만 남은 경우도 같은 보존 원칙을 적용하며, unknown entry·부분 생성 조합·identity/mode mismatch는 자동 복구·삭제하지 않고 partial로 보고한다. 마지막으로 exact checkout, Git registration과 `state.json`의 부재 및 verification 경로·digest가 제거 전과 같음을 재확인하고 `release_repository`를 request한다. 정상 보존된 runtime/verification 때문에 Issue directory가 남는 것은 checkout 제거 실패가 아니다. 비어 있는 Issue directory만 제거할 수 있으며 자동 만료·재귀 삭제를 도입하지 않는다.
|
||||
|
||||
Runtime 실행에서 만든 server·합성 DB·임시 credential은 해당 workflow가 종료 전에 정리·부재를 확인한다. Broker cleanup은 lease 해제·프로세스 종료·소유 socket 삭제·handle 기록 결과를 검증하며 기존 부분 실패 계약을 유지한다. Terminal handle과 run directory는 private 진단 자료로 남을 수 있으나 secret-bearing handle을 verification/reviewer 기록으로 복사하거나 token을 출력하지 않는다. 이 잔존 자체를 socket 정리 실패로 세지 않으며, stale run은 재사용·자동 삭제하지 않는다. 남은 runtime의 폐기는 exact 경로와 활성 자원 부재를 확인한 별도 승인 절차로만 수행한다.
|
||||
|
||||
## Credential 안전
|
||||
|
||||
|
||||
Reference in New Issue
Block a user