capture #49c: Gitea IssueTracker client (new outbound dependency) #52

Closed
opened 2026-06-22 20:27:10 +00:00 by mathias · 1 comment
Owner

Parent: #49 · Spec: specs/capture-bdd-spec.md §1 · Depends on: #51 (the IssueTracker port) · Blocks: #49d

Scope

Implement the IssueTracker port (#51) as a real Gitea client. This is the new outbound dependency the brain server does not have today — per the #49 ground-truth, the brain server does brain-local file ops only and knows nothing about Gitea. Isolated and testable against a Gitea test instance.

What to build

  • A concrete IssueTracker impl: CreateIssue(repo, title, body), CloseIssue(repo, number, comment), CommentIssue(repo, number, body). Owner is always mathias (hard-coded; never caller-supplied).
  • Gitea API HTTP client, injected into the brain server behind the IssueTracker interface (swappable/testable — the use-case never sees the concrete type).
  • Token handling: Gitea API token via env, never in argv or logs (AGENTS.md secret-handling; the 2026-06-11 base64-masking-defeat incident is the cautionary precedent). Token read once at construction.
  • Map Gitea responses to the Ticket receipt fields ({repo, number, action, url, ok}).

Acceptance criteria

  • Concrete IssueTracker implementing #51's port: create / close / comment
  • Owner forced to mathias; never read from caller input
  • Injected into the brain server behind the interface (constructor injection)
  • Token via env only; no secret in argv/logs; verify with a log-scrape test
  • Errors mapped to per-item receipt ok:false + message (best-effort; one failed ticket doesn't abort the call)
  • Tests against a Gitea test instance (or a recorded/faked transport): create, close, comment, error path
  • task check green

Notes

Decision from #49: the Gitea client lives in the brain server, injected behind the interface — not a separate composing service. This keeps capture as one use-case while keeping the dependency swappable. The interface boundary is what lets #51's use-case tests stay hermetic (fake tracker) while this issue tests the real wire.

**Parent:** #49 · **Spec:** `specs/capture-bdd-spec.md` §1 · **Depends on:** #51 (the `IssueTracker` port) · **Blocks:** #49d ## Scope Implement the `IssueTracker` port (#51) as a real Gitea client. This is the **new outbound dependency the brain server does not have today** — per the #49 ground-truth, the brain server does brain-local file ops only and knows nothing about Gitea. Isolated and testable against a Gitea test instance. ## What to build - A concrete `IssueTracker` impl: `CreateIssue(repo, title, body)`, `CloseIssue(repo, number, comment)`, `CommentIssue(repo, number, body)`. Owner is always `mathias` (hard-coded; never caller-supplied). - Gitea API HTTP client, injected into the brain server behind the `IssueTracker` interface (swappable/testable — the use-case never sees the concrete type). - **Token handling:** Gitea API token via env, **never in argv or logs** (AGENTS.md secret-handling; the 2026-06-11 base64-masking-defeat incident is the cautionary precedent). Token read once at construction. - Map Gitea responses to the `Ticket` receipt fields (`{repo, number, action, url, ok}`). ## Acceptance criteria - [ ] Concrete `IssueTracker` implementing #51's port: create / close / comment - [ ] Owner forced to `mathias`; never read from caller input - [ ] Injected into the brain server behind the interface (constructor injection) - [ ] Token via env only; no secret in argv/logs; verify with a log-scrape test - [ ] Errors mapped to per-item receipt `ok:false` + message (best-effort; one failed ticket doesn't abort the call) - [ ] Tests against a Gitea test instance (or a recorded/faked transport): create, close, comment, error path - [ ] `task check` green ## Notes Decision from #49: the Gitea client lives **in the brain server**, injected behind the interface — not a separate composing service. This keeps capture as one use-case while keeping the dependency swappable. The interface boundary is what lets #51's use-case tests stay hermetic (fake tracker) while this issue tests the real wire.
Author
Owner

Implemented — PR #58 (open, stacked on #57)

PR: #58 · base feat/capture-service (#52-only diff). Merge order: #56#57#58.

ingestion/internal/gitea/Client implementing capture.IssueTracker:

  • create / comment / close(+optional closing comment) over the Gitea REST API.
  • Owner const "mathias" — never a param; a caller can't redirect to another owner's repo.
  • Token: read once from BRAIN_GITEA_TOKEN, only ever in the Authorization header — never logged/argv. TestErrorPathDoesNotLeakToken proves the token never appears in an error string. gitea.New → nil when unconfigured.
  • Injected via mcp.Server.WithIssueTracker (constructor injection) + IssueTracker() accessor; main wires from BRAIN_GITEA_URL (default https://git.d-ma.be) + BRAIN_GITEA_TOKEN. Consumed by the use-case in #53.

Port refinement: CloseIssue gained a comment param to match this issue's spec; service passes the ticket body as the closing comment.

Deploy note: 1Password DMABE_GITEA_API_TOKEN → maps to BRAIN_GITEA_TOKEN in the k3s manifest (#55/infra). Secret never in repo/argv/logs.

All ACs met (httptest-backed: create/comment/close±comment/error/auth-header/owner-forcing); task check green.

Critical path next: #53POST /capture REST adapter + OAuth2 + the I1 sovereignty gate (server-derived origin). All three converging deps (#50 classification, #51 service, #52 tracker) are now in flight; #53 wires them together and adds the origin gate. After #53 = "capture works" for direct-REST harnesses.

## Implemented — PR #58 (open, stacked on #57) **PR:** https://git.d-ma.be/mathias/hyperguild/pulls/58 · base `feat/capture-service` (#52-only diff). Merge order: **#56 → #57 → #58**. `ingestion/internal/gitea/` — `Client` implementing `capture.IssueTracker`: - create / comment / close(+optional closing comment) over the Gitea REST API. - **Owner const `"mathias"`** — never a param; a caller can't redirect to another owner's repo. - **Token:** read once from `BRAIN_GITEA_TOKEN`, only ever in the `Authorization` header — never logged/argv. `TestErrorPathDoesNotLeakToken` proves the token never appears in an error string. `gitea.New` → nil when unconfigured. - Injected via `mcp.Server.WithIssueTracker` (constructor injection) + `IssueTracker()` accessor; `main` wires from `BRAIN_GITEA_URL` (default `https://git.d-ma.be`) + `BRAIN_GITEA_TOKEN`. Consumed by the use-case in #53. **Port refinement:** `CloseIssue` gained a `comment` param to match this issue's spec; service passes the ticket body as the closing comment. **Deploy note:** 1Password `DMABE_GITEA_API_TOKEN` → maps to `BRAIN_GITEA_TOKEN` in the k3s manifest (#55/infra). Secret never in repo/argv/logs. All ACs met (`httptest`-backed: create/comment/close±comment/error/auth-header/owner-forcing); `task check` green. **Critical path next: #53** — `POST /capture` REST adapter + OAuth2 + the I1 sovereignty gate (server-derived origin). All three converging deps (#50 classification, #51 service, #52 tracker) are now in flight; #53 wires them together and adds the origin gate. After #53 = "capture works" for direct-REST harnesses.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#52