[verified] docs: harden Gitea access and release contracts

This commit is contained in:
2026-09-30 16:39:33 +09:00
parent c05b4226bf
commit c50440b81b
8 changed files with 191 additions and 61 deletions
+73
View File
@@ -0,0 +1,73 @@
# Gitea authenticated access 운영 지침
이 문서는 Gitea Agent harness가 Gitea의 authoritative state를 읽고 승인된 mutation을 수행할 때 사용하는 canonical access 계약이다. 모든 agent와 skill은 특정 transport를 정본으로 취급하지 않고, 이 문서의 identity·credential·readback 조건을 충족하는 authenticated API adapter를 사용한다.
## 고정 origin과 transport
- 허용된 Gitea origin은 exact `https://gitea.jsfamily.quest`다.
- Authenticated API adapter는 전용 Gitea MCP connection 또는 release/spec/impl 등 현재 role의 credential을 사용하는 직접 Gitea REST client일 수 있다.
- MCP method 이름, REST client 구현과 command 성공은 정본이 아니다. 위 origin이 반환한 authenticated response와 fresh readback이 정본이다.
- 다른 origin, mirror API, public web page, unauthenticated response 또는 다른 role의 adapter로 fallback하지 않는다.
- Redirect는 자동 추적하지 않는다. 특히 credential을 다른 origin에 전달하지 않는다.
## Identity gate
1. 모든 workflow는 첫 authoritative read 전에 authenticated adapter로 현재 사용자를 읽는다. REST adapter는 exact `GET /api/v1/user`를 사용한다.
2. Stable numeric account ID, login과 현재 workflow role을 기록한다. Transport에 따라 login 표시가 달라도 stable account ID가 같으면 같은 identity다.
3. 예상 role identity를 확인할 수 없거나 API가 `401`, `403`, redirect, malformed response 또는 anonymous identity를 반환하면 fail-closed한다.
4. Workflow 동안 adapter와 credential source를 바꾸지 않는다. 재인증이 필요하면 현재 operation을 중단하고 snapshot부터 다시 시작한다.
## 직접 REST adapter
- Base URL은 exact `https://gitea.jsfamily.quest/api/v1`로 고정하고 TLS certificate 검증을 끄지 않는다.
- Token은 현재 role profile의 exact `$HERMES_HOME/.env`에 `GITEA_TOKEN`으로 저장하고 해당 파일의 mode를 `0600`으로 유지한다. Named profile에서는 `$HERMES_HOME`이 `~/.hermes/profiles/<profile>`이므로 같은 변수 이름을 사용해도 role별 token이 격리된다. 승인된 external secret manager를 사용하면 같은 profile scope에 `GITEA_TOKEN`으로 주입한다. 다른 profile, inherited ambient token, interactive prompt 또는 public access로 fallback하지 않는다.
- Token을 URL, command argv, Git config, remote URL, log, exception text, state, artifact 또는 repository file에 기록하지 않는다.
- Client는 token을 process 내부에서 읽어 `Authorization` header에 넣거나 보호된 file descriptor·stdin과 동등한 mechanism으로 전달한다. Secret이 shell expansion 후 argv에 나타나는 호출은 금지한다.
- Response status와 JSON shape를 검증한다. Collection endpoint는 Gitea pagination metadata에 따라 마지막 page까지 읽고 cardinality를 기록한다.
- `429`와 일시 오류는 read operation에서만 bounded retry할 수 있다. Mutation timeout·오류는 blind retry하지 않고 authoritative reconciliation을 먼저 수행한다.
## Repository와 Git
- Repository metadata, default branch와 branch full SHA는 authenticated adapter로 exact target에서 읽는다.
- Git clone/fetch/push credential은 API token을 credential-bearing URL에 넣지 않고 short-lived askpass 또는 동등한 child-process mechanism으로 전달한다.
- Git command 성공만으로 remote state를 확정하지 않는다. Fetch 후 default-branch ref를 API SHA와 비교하고, push 후 exact remote ref를 authenticated adapter로 다시 읽는다.
## Mutation과 readback
- Direct REST와 MCP에 동일한 승인 gate, exact payload, idempotency, drift 검사와 readback 규칙을 적용한다.
- Mutation 직전에 target identity, approval snapshot과 baseline을 다시 읽는다.
- 승인된 exact endpoint와 payload만 한 번 호출한다. Unstated default field에 의존하지 않는다.
- 성공 response만으로 완료를 주장하지 않는다. Exact target을 fresh readback하고 author stable account ID, payload, timestamp, commit/ref와 collection cardinality를 baseline과 비교한다.
- Timeout, malformed response 또는 connection loss 뒤에는 같은 mutation을 재호출하기 전에 complete collection을 한 번 읽어 결과를 reconciliation한다. 결과가 없거나 여러 개이거나 경쟁 mutation과 구분할 수 없으면 uncertain/partial로 중단한다.
## Credential 안전
- Credential 값은 role profile의 secret store에만 저장하고 repository, harness 문서와 workflow artifact에는 저장하지 않는다.
- Child process에 credential이 필요하면 최소 범위 environment 또는 보호된 descriptor로 전달하고 종료 시 helper와 temporary file을 제거한다.
- Credential의 존재 여부, source와 cleanup 결과만 보고하며 값을 출력하지 않는다.
- Read-only 단계에는 필요한 최소 scope를, tag push·review·Issue mutation에는 승인된 작업에 필요한 scope만 사용한다.
## Fail-closed 조건
다음 중 하나라도 발생하면 authenticated operation을 완료로 보고하지 않는다.
- 허용 origin 또는 target repository 불일치
- Authenticated stable account identity 확인 실패
- Role credential source 불명확 또는 다른 role credential 사용
- Redirect, TLS 검증 실패, `401`, `403` 또는 malformed API response
- Pagination 미완료 또는 baseline cardinality 불명확
- Mutation 결과와 fresh readback 불일치
- Credential 노출 또는 helper cleanup 결과 불확실
## 보고
완료 보고에는 adapter 종류 자체보다 다음 검증 결과를 포함한다.
- exact Gitea origin
- authenticated stable account ID와 login
- exact repository와 authoritative default branch full SHA
- 사용한 transport 종류와 role credential source의 이름만 표시한 provenance
- 수행한 mutation의 exact target과 fresh readback 결과
- pagination, drift, reconciliation과 partial/fail-closed 상태
Token 값, authorization header와 credential-bearing URL은 보고하지 않는다.
+22 -11
View File
@@ -1,19 +1,28 @@
# Repository cache 운영 지침
이 문서는 Gitea Agent harness가 사용하는 local repository cache의 canonical 운영 규칙이다. 모든 agent와 skill은 cache를 생성·갱신·검증할 때 이 문서를 읽고 따라야 하며 별도의 완화된 규칙을 만들지 않는다.
이 문서는 Gitea Agent harness가 사용하는 local repository cache의 canonical 운영 규칙이다. 모든 agent와 skill은 cache를 생성·갱신·검증할 때 이 문서와 [Gitea authenticated access 운영 지침](gitea-access.md)을 읽고 따라야 하며 별도의 완화된 규칙을 만들지 않는다.
## 목적과 범위
Repository cache는 Gitea repository의 Git 객체와 refs를 재사용하기 위한 bare mirror다. 문서 근거 조사와 Issue별 worktree 생성의 source로 사용할 수 있지만 그 자체는 checkout, 작업 공간 또는 외부 상태의 정본이 아니다.
- Gitea MCP readback이 repository identity와 authoritative ref의 정본이다.
- Canonical access 계약을 충족하는 authenticated Gitea API readback이 repository identity와 authoritative ref의 정본이다.
- `repository.git`은 확인한 Gitea 상태와 일치할 때만 사용한다.
- cache 생성과 갱신은 local filesystem 및 Git refs만 변경한다. Gitea에는 write하지 않는다.
## Harness root 결정
Repository cache 경로는 고정된 machine-local 절대경로나 현재 작업 directory에서 추측하지 않는다. Agent가 이번 작업에서 `read_file`로 실제 읽은 이 문서의 경로가 exact `<harness-root>/harness/operations/repository-cache.md` 형식인지 확인하고, 그 경로에서 `<harness-root>`를 계산한다. 따라서 repository 전체를 다른 위치에 설치해도 cache root는 항상 `<harness-root>/repositories`다.
- 이 문서의 경로, `operations`, `harness`와 `<harness-root>`를 각각 `lstat`하고 symlink가 아닌지 확인한다.
- 문서 경로가 위 구조와 exact-match하지 않거나 `<harness-root>`를 하나로 결정할 수 없으면 fail-closed한다.
- `pwd`, shell 시작 directory, 사용자 home, 다른 checkout, 환경변수 또는 과거 session 값을 root 결정 근거로 사용하지 않는다.
- Source checkout과 runtime cache가 같은 root 아래 있더라도 repository source로 혼동하지 않는다. Runtime cache는 `<harness-root>/repositories` 아래에서만 관리한다.
## 경로 구조
```text
/opt/data/gitea-agent/repositories/
<harness-root>/repositories/
├── .locks/
│ └── <owner>/
│ └── <repository>.bootstrap.lock
@@ -40,11 +49,13 @@ Repository cache는 Gitea repository의 Git 객체와 refs를 재사용하기
1. `owner`와 `repository`는 요청에서 받은 값을 보존하되 각각 안전한 단일 경로 요소인지 검증한다.
2. 빈 값, 경로 구분자, NUL, `.`과 `..` 또는 traversal을 허용하지 않는다.
3. `/opt/data/gitea-agent`는 지정된 trusted harness root다. NFS mount root의 numeric owner가 실행 UID와 달라도 허용하지만 exact path가 directory이고 symlink가 아니며 world-writable이 아닌지 확인한다.
4. trust boundary 아래 `repositories`, `.locks`, owner, repository와 `locks`는 생성 전후 `lstat`으로 검사한다. 모두 symlink가 아닌 directory, 실행 role 소유, mode `0700`이어야 한다.
5. lock 파일과 임시 credential helper는 실행 role 소유와 mode `0600`을 요구한다.
6. `worktrees`, `issue-<issue-number>`와 `checkout`은 symlink가 아닌 directory이고 실행 role 소유여야 한다. `worktrees`와 Issue directory는 mode `0700`, `state.json`은 mode `0600`이어야 하며 checkout과 그 상위 경로는 group 또는 world-writable이면 안 된다.
7. 기존 path가 malformed 상태면 삭제, chmod, chown, reset 또는 덮어쓰기로 자동 복구하지 않고 fail-closed한다. 권한 변경은 운영자가 exact path와 mode를 명시적으로 승인한 경우에만 수행하고 즉시 재검증한다.
3. 위 규칙으로 계산하고 검증한 `<harness-root>`만 trusted harness root로 사용한다. Exact path가 symlink가 아닌 directory이고 world-writable이 아닌지 확인한다. NFS mount root의 numeric owner가 실행 UID와 달라도 허용한다.
4. 기존 `repositories`는 `lstat`으로 검사해 symlink가 아닌 directory, 실행 role 소유이며 world-writable이 아님을 확인한다. Group-writable이면 directory의 numeric GID가 현재 system에서 해석되고, 해당 GID를 primary group으로 쓰는 계정과 group member 목록을 합친 effective member가 실행 role 하나뿐임을 확인해야 한다. Group identity나 전체 membership을 확인할 수 없거나 다른 계정이 포함되면 fail-closed한다.
5. 새 `repositories`를 만들 때는 실행 role 소유와 mode `0700`으로 생성한다. 기존 directory가 4번의 검증을 통과하면 mode `0700`, `0750`, `0755`, `0770` 또는 `0775`를 자동으로 chmod하지 않고 그대로 사용할 수 있다.
6. Trust boundary 아래 `.locks`, owner, repository와 `locks`는 생성 전후 `lstat`으로 검사한다. 모두 symlink가 아닌 directory, 실행 role 소유, mode `0700`이어야 한다.
7. Lock 파일과 임시 credential helper는 실행 role 소유와 mode `0600`을 요구한다.
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를 명시적으로 승인한 경우에만 수행하고 즉시 재검증한다.
## Lock 규칙
@@ -60,7 +71,7 @@ Repository cache는 Gitea repository의 Git 객체와 refs를 재사용하기
## 신규 cache 초기화
1. Gitea MCP `get_me`로 허용된 role identity를 확인한다.
1. Authenticated Gitea API adapter로 허용된 role의 stable account identity를 확인한다. MCP면 `get_me`, 직접 REST면 `GET /api/v1/user`를 사용한다.
2. exact `owner/repository` metadata, clone URL, default branch와 branch SHA를 Gitea에서 읽는다.
3. bootstrap lock을 획득한 뒤 target을 다시 검사한다. 이미 유효한 cache가 있으면 새로 만들지 않고 기존 cache 검증 절차로 전환한다.
4. trusted parent 아래 같은 filesystem에 run-unique sibling staging directory를 mode `0700`으로 만든다.
@@ -121,7 +132,7 @@ Repository cache는 Gitea repository의 Git 객체와 refs를 재사용하기
Worktree를 신규 생성하거나 기존 worktree에서 구현을 재개하기 전에 다음을 모두 확인한다.
1. exact `owner`, `repository`와 Issue 번호가 검증됐고 Issue lock을 획득했다.
2. Gitea MCP로 authenticated role, exact repository metadata, Issue body, `updated_at`과 구현 승인 상태를 읽었다.
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를 확정하거나 중단한다.
5. `repository.git`에 exact base SHA의 commit object가 존재한다. 문자열 형식이나 object 존재만으로 Gitea 또는 승인된 Discovery에서 유래했다는 검증을 대체하지 않는다.
@@ -195,7 +206,7 @@ Dirty worktree, untracked 파일 또는 base 이후 commit은 진행 중 구현
- worker에게 credential 값 또는 credential-bearing URL을 전달하지 않는다.
- Git 인증이 필요하면 short-lived askpass 또는 동등한 child-process mechanism을 사용하고 종료 시 제거한다.
- inherited credential, 다른 role의 credential 또는 interactive prompt로 fallback하지 않는다.
- 인증에 실패하면 public clone이나 MCP content read를 initialized mirror의 증거로 대체하지 않고 중단한다.
- 인증에 실패하면 public clone이나 unauthenticated API·content read를 initialized mirror의 증거로 대체하지 않고 중단한다.
- Worktree source, state, Git config, reflog message, commit, patch, test artifact 또는 push refspec에 credential을 기록하지 않는다.
## Fail-closed 조건