Future: multiple forges and multiple projects (GitHub alongside Gitea, several Gitea instances) #26

Open
opened 2026-09-08 21:55:51 +00:00 by dev-agent · 0 comments
Collaborator

Recording the outcome of a design discussion so it is not lost. Not scheduled — future work, no action needed now.

The shape

Support several projects (tracked, one worked on at a time) across a mix of GitHub and Gitea, including more than one Gitea instance.

The abstraction that makes all of it fall out is a named forge connection: id, kind (gitea | github), base URL, credential, operator allowlist. A project references a connection by id rather than carrying host details.

The payoff: once connections exist, N instances of the same kind cost nothing — a second Gitea is a config row using the identical client. Only a new kind needs code.

Sizing, measured rather than guessed

  • Consumers touch only four client methods directly (open_pull_request, create_issue, list_comments, post_comment), plus find_pull_request via the A3 reconciler. gitea.py is 193 lines; git.py is already forge-neutral.
  • A ForgeClient protocol is a mechanical extraction. A GitHubClient is ~150–200 lines, because Gitea's API deliberately mirrors GitHub's.
  • Differences that would bite: Authorization: Bearer vs token, pagination via Link headers rather than a page parameter, and rate limiting surfacing as 403 with X-RateLimit-* rather than the 429 our retry currently keys on.

Three consequences worth remembering

  1. The operator allowlist becomes per-connection, and that is correctness. A4 already says allowlists are per channel; a Gitea login on instance A need not match instance B and certainly will not match a GitHub handle. A global list would let the wrong forge's identity satisfy a gate.
  2. A7 (Bitwarden) starts pulling its weight. One token in .env was fine; several tokens across several hosts is precisely what the secrets client exists for.
  3. The project registry becomes curated configuration. With a single Gitea, "which projects exist" was derivable from the forge — external truth. With several forges there is no single place to ask, so the roster becomes operational config. (Knowledge about projects still belongs in A17; the roster itself does not.)

Where the effort actually goes

Not the forge layer. A2 requires "per-project state-machine position", and agent_state is implemented as a single row (id = 1). That is sufficient while only one project is ever known, but tracking several and switching between them means keying state by project — a schema migration plus a modest orchestrator change. This is independent of GitHub and is the larger half of the work.

Non-code: a GitHub identity for the agent, an egress hole per forge host in B3's sealed allowlist, and branch protection per repo — where GitHub forbids self-approval outright, giving that property for free.

Possible directions

A config file listing connections and projects (small), or a web configuration interface (a materially larger scope).

Recording the outcome of a design discussion so it is not lost. **Not scheduled** — future work, no action needed now. ### The shape Support several projects (tracked, one worked on at a time) across a mix of GitHub and Gitea, including more than one Gitea instance. The abstraction that makes all of it fall out is a **named forge connection**: id, kind (`gitea` | `github`), base URL, credential, operator allowlist. A project references a connection by id rather than carrying host details. The payoff: once connections exist, **N instances of the same kind cost nothing** — a second Gitea is a config row using the identical client. Only a new *kind* needs code. ### Sizing, measured rather than guessed - Consumers touch only four client methods directly (`open_pull_request`, `create_issue`, `list_comments`, `post_comment`), plus `find_pull_request` via the A3 reconciler. `gitea.py` is 193 lines; `git.py` is already forge-neutral. - A `ForgeClient` protocol is a mechanical extraction. A `GitHubClient` is ~150–200 lines, because Gitea's API deliberately mirrors GitHub's. - Differences that would bite: `Authorization: Bearer` vs `token`, pagination via `Link` headers rather than a `page` parameter, and rate limiting surfacing as **403 with `X-RateLimit-*`** rather than the 429 our retry currently keys on. ### Three consequences worth remembering 1. **The operator allowlist becomes per-connection, and that is correctness.** A4 already says allowlists are per channel; a Gitea login on instance A need not match instance B and certainly will not match a GitHub handle. A global list would let the wrong forge's identity satisfy a gate. 2. **A7 (Bitwarden) starts pulling its weight.** One token in `.env` was fine; several tokens across several hosts is precisely what the secrets client exists for. 3. **The project registry becomes curated configuration.** With a single Gitea, "which projects exist" was derivable from the forge — external truth. With several forges there is no single place to ask, so the roster becomes operational config. (Knowledge *about* projects still belongs in A17; the roster itself does not.) ### Where the effort actually goes Not the forge layer. **A2 requires "per-project state-machine position", and `agent_state` is implemented as a single row (`id = 1`).** That is sufficient while only one project is ever known, but tracking several and switching between them means keying state by project — a schema migration plus a modest orchestrator change. This is independent of GitHub and is the larger half of the work. Non-code: a GitHub identity for the agent, an egress hole per forge host in B3's sealed allowlist, and branch protection per repo — where GitHub forbids self-approval outright, giving that property for free. ### Possible directions A config file listing connections and projects (small), or a web configuration interface (a materially larger scope).
Sign in to join this conversation.