feat: Gitea IssueTracker client + inject into brain server (#52, capture 49c) #58

Merged
mathias merged 2 commits from feat/capture-gitea-tracker into main 2026-06-22 21:32:02 +00:00
Owner

Implements #52 (capture 49c) — the real Gitea client behind the IssueTracker port, the new outbound dependency the brain server gains for capture.

Stacked on #57 (#51). Base is feat/capture-service so this diff is #52-only. Merge order: #56#57#58.

What landed

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

  • CreateIssue / CommentIssue / CloseIssue(+optional closing comment) over the Gitea REST API.
  • Owner is the const "mathias" — never a parameter, so a caller can never redirect a write to another owner's repo.
  • Token handling (AGENTS.md secret-handling; the 2026-06-11 masking-defeat precedent): read once at construction from BRAIN_GITEA_TOKEN, held in the struct, travels only in the Authorization: token … header — never logged, never in argv. Error messages carry status + truncated body, never the token (regression-tested in TestErrorPathDoesNotLeakToken). gitea.New returns nil when URL or token is unset → "tracker disabled" via one nil check.
  • Responses mapped to IssueRef{Repo, Number, URL} → the Ticket receipt fields.

Injection: mcp.Server.WithIssueTracker(capture.IssueTracker) (constructor injection, swappable/testable) + an IssueTracker() accessor; main wires it from BRAIN_GITEA_URL (default https://git.d-ma.be) + BRAIN_GITEA_TOKEN. The use-case consumes it in #53 — wired here so the dependency is constructed once and stays swappable.

Port refinement (1 commit): IssueTracker.CloseIssue gains a comment param to match #52's spec (CloseIssue(repo, number, comment)); the service passes the ticket body as the closing comment. Touches #51's port — forward change on this stacked branch.

Env / deployment note

New env: BRAIN_GITEA_TOKEN (required to enable), BRAIN_GITEA_URL (default https://git.d-ma.be). The 1Password item is DMABE_GITEA_API_TOKEN — the k3s deployment maps it to BRAIN_GITEA_TOKEN (manifest is #55/infra, not here). Secret never enters the repo, argv, or logs.

Acceptance criteria

  • Concrete IssueTracker: 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; verified with a token-leak test
  • Errors → per-item ok:false + message (best-effort; the service already maps a tracker error to a per-ticket failure without aborting)
  • Tests via faked transport (httptest): create, close (+/- comment), comment, error path, auth header, owner forcing
  • task check green (lint 0 issues, vet clean, -race green)

Not auto-merged — review + merge from claude.ai.

Closes #52

🤖 Generated with Claude Code

Implements #52 (capture 49c) — the real Gitea client behind the `IssueTracker` port, the new outbound dependency the brain server gains for capture. > **Stacked on #57 (#51).** Base is `feat/capture-service` so this diff is #52-only. Merge order: **#56 → #57 → #58**. ## What landed **`ingestion/internal/gitea/`** — `Client` implementing `capture.IssueTracker`: - `CreateIssue` / `CommentIssue` / `CloseIssue(+optional closing comment)` over the Gitea REST API. - **Owner is the const `"mathias"`** — never a parameter, so a caller can never redirect a write to another owner's repo. - **Token handling** (AGENTS.md secret-handling; the 2026-06-11 masking-defeat precedent): read once at construction from `BRAIN_GITEA_TOKEN`, held in the struct, travels only in the `Authorization: token …` header — never logged, never in argv. Error messages carry status + truncated body, **never the token** (regression-tested in `TestErrorPathDoesNotLeakToken`). `gitea.New` returns `nil` when URL or token is unset → "tracker disabled" via one nil check. - Responses mapped to `IssueRef{Repo, Number, URL}` → the `Ticket` receipt fields. **Injection:** `mcp.Server.WithIssueTracker(capture.IssueTracker)` (constructor injection, swappable/testable) + an `IssueTracker()` accessor; `main` wires it from `BRAIN_GITEA_URL` (default `https://git.d-ma.be`) + `BRAIN_GITEA_TOKEN`. The use-case consumes it in **#53** — wired here so the dependency is constructed once and stays swappable. **Port refinement (1 commit):** `IssueTracker.CloseIssue` gains a `comment` param to match #52's spec (`CloseIssue(repo, number, comment)`); the service passes the ticket body as the closing comment. Touches #51's port — forward change on this stacked branch. ## Env / deployment note New env: `BRAIN_GITEA_TOKEN` (required to enable), `BRAIN_GITEA_URL` (default `https://git.d-ma.be`). The 1Password item is `DMABE_GITEA_API_TOKEN` — the k3s deployment maps it to `BRAIN_GITEA_TOKEN` (manifest is #55/infra, not here). Secret never enters the repo, argv, or logs. ## Acceptance criteria - [x] Concrete `IssueTracker`: create / close / comment - [x] Owner forced to `mathias`; never read from caller input - [x] Injected into the brain server behind the interface (constructor injection) - [x] Token via env only; no secret in argv/logs; verified with a token-leak test - [x] Errors → per-item `ok:false` + message (best-effort; the service already maps a tracker error to a per-ticket failure without aborting) - [x] Tests via faked transport (`httptest`): create, close (+/- comment), comment, error path, auth header, owner forcing - [x] `task check` green (lint 0 issues, vet clean, `-race` green) Not auto-merged — review + merge from claude.ai. Closes #52 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mathias changed target branch from feat/capture-service to main 2026-06-22 21:31:53 +00:00
mathias added 2 commits 2026-06-22 21:31:53 +00:00
#52's IssueTracker spec is CloseIssue(repo, number, comment). Refine the
#51 port signature to match and have the service pass the ticket body as
the closing comment (empty ⇒ close only). Keeps the close-with-comment
flow first-class rather than forcing two separate ticket items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the IssueTracker port as a real Gitea REST client (#49c) — the
new outbound dependency the brain server gains for capture.

- CreateIssue / CommentIssue / CloseIssue(+optional closing comment) over
  the Gitea API. Owner is the const "mathias", never caller-supplied, so
  a caller cannot redirect a write to another owner's repo.
- Token read once at construction (BRAIN_GITEA_TOKEN), held in the struct,
  travels only in the Authorization header — never logged or in argv.
  Error messages carry status + truncated body, never the token
  (regression-tested). gitea.New returns nil when URL or token is unset,
  so missing config = tracker disabled via one nil check.
- Injected into the MCP server behind the capture.IssueTracker interface
  via WithIssueTracker (constructor injection, swappable/testable); main
  wires it from BRAIN_GITEA_URL (default https://git.d-ma.be) +
  BRAIN_GITEA_TOKEN. Consumed by the capture use-case in #53.

Tests use httptest transports: create (owner+auth header asserted),
comment, close with/without comment, error path that proves the token
never leaks into an error string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mathias merged commit aad90f2dfe into main 2026-06-22 21:32:02 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/hyperguild#58