<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=0 tag=API -->

# PLAN local-G06 - API

## 이 파일을 읽는 구현 에이전트에게

이 작업은 `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` 또는 matching archive `complete.log`가 있어야 시작할 수 있다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## 배경

NomadCode Core는 아직 Gito signed HTTP webhook delivery를 받는 endpoint가 없다. 기존 Gito 소비자는 proto-socket runner로 시작되고, HTTP router에는 Plane webhook만 공개 callback으로 등록되어 있다. 이 작업은 Gito HTTP callback, signature 검증, duplicate delivery/revision guard를 `gitosync` bridge로 연결한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 중 직접 사용자에게 질문하지 않으며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
- Task ids:
  - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다.
  - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md`
- Acceptance scenarios:
  - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test`
  - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/roadmap-sdd/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/check-roadmap-dependency/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
- `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md`
- `../gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `services/core/go.mod`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitosync/bridge.go`
- `services/core/internal/gitosync/bridge_test.go`
- `services/core/internal/gitosync/scanner.go`
- `services/core/internal/http/router.go`
- `services/core/internal/http/handlers.go`
- `services/core/internal/http/handlers_test.go`
- `services/core/internal/http/plane_webhook.go`
- `services/core/internal/http/plane_webhook_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/cmd/server/main.go`

### 테스트 환경 규칙

- 선택한 test_env: `local`.
- `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`를 읽었다.
- 적용 명령: focused HTTP/config tests plus `cd services/core && go test -count=1 ./...`.
- `core-smoke.md`는 core 코드 변경에 최소 `cd services/core && go test ./...`를 요구한다.
- 새 dependency는 필요 없으며 `services/core/go.mod`에 기존 `chi`, Go stdlib crypto/http/json/sync로 충분하다.

### 테스트 커버리지 공백

- 현재 `router.go`에는 Plane webhook bypass-auth 테스트만 있고 Gito webhook route는 없다.
- 현재 `plane_webhook.go`에는 Plane hex signature verifier만 있고 `sha256=` prefix를 허용하는 Gito verifier가 없다.
- 현재 `gitosync.Bridge`는 revision key process-lifetime dedupe만 제공한다. HTTP `X-Gito-Delivery` duplicate guard는 없다.
- 새 테스트가 필요한 동작:
  - signed `branch.updated` HTTP delivery accepted and handler called once.
  - wrong `X-Gito-Event` rejected without handler call.
  - invalid signature rejected without handler call.
  - same `X-Gito-Delivery` duplicate does not call handler twice.
  - same revision key duplicate without delivery id does not call handler twice.
  - route bypasses basic auth.

### 심볼 참조

- renamed/removed symbol: 없음.
- 새 symbol 후보: `GitoWebhookConfig`, `SetGitoWebhookConfig`, `SetGitoBranchEventHandler`, `ReceiveGitoWebhook`, `GitoWebhookConsumerEnabled`.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `m-gito-http-webhook-consumer-readiness`.
- sibling subtasks:
  - `01_payload_boundary`: common base payload decoder.
  - `02+01_http_receiver_idempotency`: HTTP receiver, signature, idempotency, route/config/server wiring. Depends on `01`.
- predecessor `01`: 현재 active/archive `complete.log` 없음. 구현 시작 전 `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` 또는 matching archive complete.log가 필요하다.
- split 이유: decoder foundation과 HTTP route/wiring/idempotency는 dependency boundary와 risk profile이 다르다.

### 범위 결정 근거

- 포함: HTTP callback endpoint, Gito webhook config fields, process-lifetime duplicate guard, server wiring into `gitosync.Bridge`.
- 제외: `scan-enqueue` S04의 full branch scanner -> `EnqueueRoadmapCreationSync` integration proof. 이 plan은 handler seam 호출 1회까지만 검증하고, full enqueue bridge proof는 `sync-bridge` Epic의 `scan-enqueue` Task에서 한다.
- 제외: README/compose/env sample update. `config-docs` Task가 담당한다.
- 제외: Gito provider webhook producer/outbound worker. Milestone 범위 제외와 SDD 비목표에 있다.

### 빌드 등급

- `local-G06`: 새 HTTP callback surface, config/server wiring, idempotency guard가 있지만 범위는 core 내부 Go와 deterministic unit tests로 제한된다.

## 구현 체크리스트

- [ ] `01_payload_boundary` predecessor `complete.log`가 있는지 확인하고, 없으면 구현을 시작하지 않는다.
- [ ] Gito HTTP webhook receiver와 route를 추가하고, signed `branch.updated` delivery accepted 및 wrong event/invalid signature side-effect-free reject를 검증한다.
- [ ] `X-Gito-Delivery`와 revision key 기준 process-lifetime duplicate guard를 추가하고, duplicate delivery/revision이 handler를 한 번만 호출함을 검증한다.
- [ ] config/server wiring을 추가해 Gito webhook secret/repo/branch/develop checkout/todo state가 준비된 경우 HTTP receiver가 `gitosync.Bridge`를 호출하게 한다.
- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다.
- [ ] `cd services/core && go test -count=1 ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `02+01_http_receiver_idempotency`는 sibling predecessor `01_payload_boundary`에 의존한다.
- runtime source of truth: directory name `02+01_http_receiver_idempotency`.
- 구현 시작 전 확인 후보:
  - active: `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log`
  - archive: `agent-task/archive/*/*/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log`
- predecessor complete.log가 없으면 이 plan의 구현은 blocked이며 사용자 리뷰 요청이 아니라 dependency 대기 상태다.

### [API-1] Gito HTTP webhook receiver

#### 문제

`services/core/internal/http/router.go:23`에는 healthz와 Plane webhook만 public route로 등록되어 있다. Gito HTTP webhook callback route가 없어서 signed `branch.updated` delivery를 받을 수 없다.

Before:

```go
// services/core/internal/http/router.go:23
r.Get("/healthz", handler.Healthz)
r.Post("/api/integrations/plane/webhook", handler.ReceivePlaneWebhook)
```

#### 해결 방법

- `services/core/internal/http/gito_webhook.go`를 추가한다.
- public route `POST /api/integrations/gito/webhook`를 등록한다.
- `ReceiveGitoWebhook`는:
  - `GitoWebhookConfig.Secret` 없으면 `503`.
  - body max size는 Plane과 같은 1MiB 기준을 쓰거나 `maxGitoWebhookBodyBytes`를 둔다.
  - `X-Gito-Signature`를 HMAC-SHA256으로 검증하고 `sha256=` prefix와 raw hex를 모두 허용한다.
  - `X-Gito-Event`가 `branch.updated`가 아니면 `400`으로 reject하고 handler를 호출하지 않는다.
  - body는 `gitoevents.DecodeBranchUpdatedPayload`로 decode한다.
  - target repo/branch가 아니면 `202 {"status":"ignored"}`로 side effect 없이 ack한다.
  - valid on-target event는 configured `GitoBranchEventHandler.Handle`에 전달한다.

After outline:

```go
r.Post("/api/integrations/plane/webhook", handler.ReceivePlaneWebhook)
r.Post("/api/integrations/gito/webhook", handler.ReceiveGitoWebhook)
```

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/http/router.go`: Gito webhook public route 추가.
- [ ] `services/core/internal/http/handlers.go`: Handler fields와 setter seam 추가.
- [ ] `services/core/internal/http/gito_webhook.go`: receiver, signature validation, response mapping.
- [ ] `services/core/internal/http/gito_webhook_test.go`: accepted/wrong event/invalid signature/route bypass tests.

#### 테스트 작성

- 작성한다.
- `TestReceiveGitoWebhookAcceptsSignedBranchUpdatedDelivery`: valid signature and header event calls fake handler once.
- `TestReceiveGitoWebhookRejectsWrongEventWithoutHandlerCall`: wrong header event returns 400 and no handler call.
- `TestReceiveGitoWebhookRejectsInvalidSignatureWithoutHandlerCall`: invalid signature returns 401 and no handler call.
- `TestGitoWebhookRouteBypassesBasicAuth`: route mounted outside protected router.

#### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/http
```

예상: PASS. Go test cache output은 허용하지 않는다.

### [API-2] Delivery/revision duplicate guard

#### 문제

`services/core/internal/gitosync/bridge.go:29`의 duplicate guard는 `(repo, branch, after)` revision만 처리한다. Milestone Task는 `X-Gito-Delivery`와 revision key 양쪽 duplicate를 중복 enqueue 없이 처리하라고 요구한다.

Before:

```go
// services/core/internal/gitosync/bridge.go:29
// ProcessedRevisionStore is the duplicate guard seam: it reports whether a
// (repo, branch, after) revision has already been processed and records it.
// MarkProcessed must only be called after the revision's enqueue work
// succeeded, so a failed run is retried rather than silently swallowed.
type ProcessedRevisionStore interface {
	Seen(repoID, branch, after string) bool
	MarkProcessed(repoID, branch, after string)
}
```

#### 해결 방법

- HTTP layer에 process-lifetime dedupe store를 둔다. Bridge의 revision guard는 유지한다.
- duplicate keys:
  - `delivery:<X-Gito-Delivery>` when header exists.
  - `revision:<repo_id>:<branch>:<after>` always when `after` exists.
- valid on-target delivery가 handler `Handle`에서 nil을 반환한 뒤 keys를 mark한다.
- sequential duplicate request는 handler를 다시 호출하지 않고 `202 {"status":"duplicate"}` 또는 `{"status":"accepted","duplicate":"true"}` 중 하나로 응답한다. 응답 shape는 tests와 docs 없이 내부 contract로만 고정한다.
- handler error에서는 mark하지 않아 retry 가능성을 보존한다.

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/http/gito_webhook.go`: dedupe store, key construction, mark policy.
- [ ] `services/core/internal/http/gito_webhook_test.go`: duplicate delivery id and duplicate revision tests.

#### 테스트 작성

- 작성한다.
- `TestReceiveGitoWebhookDeduplicatesSameDeliveryID`: same header twice -> fake handler call count 1.
- `TestReceiveGitoWebhookDeduplicatesSameRevisionWithoutDeliveryID`: no delivery header, same repo/branch/after twice -> fake handler call count 1.

#### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/http
```

예상: PASS. Go test cache output은 허용하지 않는다.

### [API-3] Config/server wiring

#### 문제

`services/core/internal/config/config.go:43`는 Gito proto-socket URL 중심 config만 갖고 있고, `services/core/cmd/server/main.go:120`은 proto-socket runner만 시작한다. HTTP receiver가 `gitosync.Bridge`로 들어갈 server wiring이 없다.

Before:

```go
// services/core/internal/config/config.go:43
GitoProtoSocketURL              string
GitoRepoID                      string
GitoBranch                      string
GitoDevelopRepoPath             string
GitoRemoteName                  string
RoadmapCreationTodoStateID      string
```

```go
// services/core/cmd/server/main.go:120
// Gito branch event consumer: drives Plane-origin Milestone creation sync
// from a develop push. Started only when the endpoint, repo, local develop
// checkout, and Todo state id are all configured; otherwise the Core server
// behaves exactly as before. Cancelled with the root context on shutdown.
if cfg.GitoBranchEventsEnabled() {
	gitoRunner, err := newGitoRunner(cfg, planeClient, taskScheduler, logger)
```

#### 해결 방법

- Config에 `GitoWebhookSecret string`를 추가하고 `GITO_WEBHOOK_SECRET`에서 로드한다.
- `GitoWebhookConsumerEnabled()`를 추가한다. Required fields: secret, repo id, develop repo path, roadmap todo state id. Branch defaults to develop.
- `newGitoBridge(cfg, reader, enqueuer, logger) (*gitosync.Bridge, error)` helper를 만들어 scanner/bridge assembly를 proto runner와 HTTP receiver가 공유할 수 있게 한다.
- `newGitoRunner`는 bridge를 받아 `gitosync.NewRunner(cfg.GitoProtoSocketURL, ...)`만 담당하게 축소한다.
- server run에서 handler 생성 후 `handler.SetGitoWebhookConfig(...)`와 `handler.SetGitoBranchEventHandler(gitoBridge)`를 설정한다.
- 기존 proto-socket runner는 제거하지 않는다. `config-docs` Task에서 docs/env 교체를 다룬다.

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/config/config.go`: env field/load/enabled method.
- [ ] `services/core/internal/config/config_test.go`: defaults and enabled requirements.
- [ ] `services/core/cmd/server/main.go`: bridge factoring and handler wiring.

#### 테스트 작성

- 작성한다.
- `TestConfigGitoWebhookDefaultsAndDisabled`: default secret empty and disabled.
- `TestConfigGitoWebhookConsumerEnabledRequiresInputs`: secret/repo/develop/todo required, branch default accepted.
- `cmd/server`는 helper unit seam이 작으면 test 추가, 아니면 `go test ./cmd/server` compile check로 검증한다.

#### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/config ./cmd/server
```

예상: PASS. Go test cache output은 허용하지 않는다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/http/router.go` | API-1 |
| `services/core/internal/http/handlers.go` | API-1, API-2 |
| `services/core/internal/http/gito_webhook.go` | API-1, API-2 |
| `services/core/internal/http/gito_webhook_test.go` | API-1, API-2 |
| `services/core/internal/config/config.go` | API-3 |
| `services/core/internal/config/config_test.go` | API-3 |
| `services/core/cmd/server/main.go` | API-3 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server
cd services/core && go test -count=1 ./...
```

예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
