# Gitea Source — design & decisions (working doc) Goal per docs/008 Cookbook: new Source kind `gitea` (plugin `dsh-superagent-gitea`, files `src/sources/gitea*.ts`) for the user's self-hosted **Gitea 1.25.4 at https://gitea.bitnasdaq.vip** (OpenAPI verified against the live instance; dump at D:/Code/AI/gitea-spec.json). Gitea's REST API deliberately mirrors GitHub's shape (`/notifications` + `X-Poll-Interval` header, Issue has `pull_request` meta, numbers shared Issue/PR sequence), so `gitea.ts` mirrors `github.ts` structure with a direct HTTPS fetch client instead of the `gh` CLI. ## Domain map (cookbook step 1) | Concept | Gitea mapping | | --- | --- | | Trigger | Issue body `key('gitea-issue', n)` (same format for PRs — shared number sequence); comment/review `key('gitea-comment', url||thread, id, updated_at)` | | Thread | `String(number)` primary; full key `gitea:` | | Actor | `user.login` (allowed list names logins). Gitea 1.25.4 API has **no `is_bot` field** → `human: true` unless actor is the agent or on the Policy `bots` list (bots list is the lever). Muted/mirrored authors: `user` may be null → login `ghost`, id ''. | | addressed | `mentions(body, login)` — same `@login` regex as github-trigger.ts | | Channel address | `{ number, url }` — `url` = html_url of comment/issue, ALWAYS stored (cookbook: put every needed fact in the address) | | Receiving scope rows | `issues` / `pulls` / `comments` (switches `observeIssues`/`observePulls`/`observeComments`, all default false). CI + inline review comments + PR-close binding = **M2, not MVP** | ## Config schema (stored in Settings.sources.gitea.config — generic record, zero core schema change) ```ts { url: '', // base, no trailing slash: ^https?://[^/]+$ repository: '', // owner/name tokenEnv: 'GITEA_TOKEN', // NAME of env var holding the API token; token NEVER persisted (pattern: gh keeps creds out of config) pollSeconds: 60, failuresBeforeError: 3, observeIssues: false, observePulls: false, observeComments: false } ``` Token source: `process.env[tokenEnv]` in the **orchestrator host process** (DSH injects `~/.dsh/.credentials.yaml` keys into process env → add `GITEA_TOKEN` there). Note: DSH strips credential-shaped env names from *worker* children (infra/environment.ts header comment) — irrelevant, workers never need the token in MVP. ## Verified live endpoints (1.25.4) - `GET /api/v1/notifications?all&since&limit&page` → `NotificationThread[]` {id, unread, repository{full_name}, subject{type,url,latest_comment_url,html_url,title,state}, updated_at}; header `X-Poll-Interval`. - `GET /user` → {login,id}; `GET /repos/{o}/{r}` → {full_name, permissions{pull,push}}. - `GET /repos/{o}/{r}/issues` (issues only), `/pulls` (PRs only) — unlike GitHub where /issues includes PRs! Client-side filter updated_at >= cursor (server `since` support not trusted). - `GET /repos/{o}/{r}/issues/{index}` → Issue with `pull_request` non-null when PR (same dual read as GitHub). - `GET|POST /repos/{o}/{r}/issues/{index}/comments` (no `since` param — client filter on updated_at; pagination per_page=100&page=N until short page). - `GET /repos/{o}/{r}/pulls/{index}/reviews` → PullReview[] {id,state,body,commit_id,user,html_url,submitted_at,updated_at}. - **No repo-wide review-comment stream** (`/pulls/comments` MISSING in 1.25.4) → MVP review intake only via per-PR reads. - CI for M2: `/repos/{o}/{r}/branches/{branch}` (user_can_push!), `/commits/{ref}/status` (CombinedStatus), `/actions/runs`. ## Poll flow (mirrors github.ts; MVP subset) `pollRound` = 1) `pollNotifications` (KV gitea-since + per-number cursor KV `gitea-cursor:` + retry map `gitea-retry`; subject.url parsed by `giteaAddress` → read issue+comments[+reviews if PR]) → 2) `pollRepository` streams issues/pulls/comments (KV `gitea-stream:`, cursorAt −5 s overlap, stable ids dedup, horizon POLL_HORIZON_MS, `started` per kind) → 3) `pollThreads` subscribed threads via poll.ts `pollThreads` (skip PR-close observation). 5 s test-timer-friendly `pause`. 3 consecutive round failures → health error (`failuresBeforeError`). Own messages never Triggers. Health entry: `['gitea']`. ## Channel send (idempotent, GitHub pattern) Marker `` appended; before POST, list issue comments and if any comment by the agent login contains the marker → 'sent'. POST `issues/{n}/comments` {body}. 'gone' when: GET issue → 404 (deleted) or POST 403 `/locked/i`. Other throws → Outbox retries 5→300 s. `link()` = {url: address.url, label `Gitea #n`}. `allowed(address, config)`: number parses && config.repository non-empty (address always has its own url). ## threadContext (assessments) `messageId(trigger)`: issue body trigger → `${number}-body`; comment/review → `c${id}`/`r${id}`. `read(part)`: fetch issue + all comments (page loop); messages = [body `${title}\n\n${body}`] + comments (+reviews when PR and part allows), ContextMessage {id, from, at: updated_at??created_at, text, url}; 'root' → just body message; flat forum — 'parent' returns the page containing the message (no threading in Gitea issue comments); `next` = page cursor `p2` etc. body payload = the messages. ## Policy part `allowed: [{id: login}]` regex `^[A-Za-z0-9._-]+$`; optional `bots` same shape (the substitute for missing is_bot — documented hint: list automation accounts here). `explicit`: "名单里的账号在 Issue 或评论里 @ 智能体的账号。". `rows`: issues/pulls/comments rows like github.ts (cells from observe* flags). No `sync` (self-hosted, no rosters). ## Environment variables (registered by this kind) DSH_SUPERAGENT_GITEA_REPOSITORY (owner/name), DSH_SUPERAGENT_GITEA_URL (base url), DSH_SUPERAGENT_GITEA_ACCOUNT (agent login — from deployment.gitea), DSH_SUPERAGENT_GITEA_NUMBER (issue/PR number that started ticket, else ''). No token vars in worker env ever. ## Core patch (required: `configured()` hard-requires gh — app/setup.ts resolve() → resolveDeployment throws) 1. `deployment.ts`: add `GiteaSetupSchema = {repository,url,tokenEnv}`, `SETUP_FIELDS.gitea`, `Deployment.gitea?: {url, login}`; `resolveGiteaDeployment(machine, config, fetchImpl, env)`: url→GET /user (token env), repo→GET /repos (canonical full_name + permissions.push required). Gitea-only Deployment shape: `{ ...machine, repository: , github: {cli:'',login:'',noreply:''}, gitea: {...} }` — github source disabled in that profile, its readers never run (verified consumers: github.ts env/facts/poll only when enabled; githubLink via registry per kind; writeEnvironment/verifyEnvironment patched next). 2. `app/setup.ts resolve()`: if github config empty (no repository && no cli) → gitea path; else GitHub path unchanged. Gitea path: resolveGiteaDeployment; `writeEnvironment` branch when `!github.cli && gitea`: shims for blocked commands (message "act as on Gitea"), NO gh shim, NO github.com credential section; `verifyEnvironment` skipped for gitea-only (credential check is github.com-specific). 3. `checkAccounts`: block `source:gitea` → resolveGiteaDeployment (place 'gitea' so unrelated errors don't block saves). 4. `src/setup.ts` PROBLEMS: add gitea codes (place 'gitea'): giteaUrlUnset, giteaRepositoryUnset, giteaTokenMissing, giteaTokenInvalid, giteaRepositoryUnreadable, giteaRepositoryReadOnly. 5. KV (`src/kv.ts`): `gitea: { since: record('gitea-since'), retries: record>('gitea-retry'), cursor: family('gitea-cursor:'), stream: family('gitea-stream:') }` (tests/store.spec.ts requires key literals only there). 6. Build wiring: tsdown entry `'gitea-watch': 'src/sources/gitea.ts'`; package.json exports `"./gitea-watch"`; cordis.patch.yml bundle insert `dsh-superagent-gitea` name `dsh-superagent/gitea-watch` **disabled: true** (the router profile enables it; web profile must not). 007-architecture.md module+test tables (tests/docs.spec.ts enforces completeness) + 001-domain "What is reusable" row. Tests: `tests/gitea.spec.ts` (fetch mocked via injected fetchImpl / vi.stubGlobal), pattern of tests/sources.spec.ts. ## MVP cut lines (M2 later): Gitea PR observation + threadClosed/bindThread + auto-merge; inline review comments; CI (branch + PR checks) triggers; agent-credential git helper for the Gitea host (MVP workers use the machine's ambient git auth = user's own account for pushes; agent identity applies to API writes); prompt seeds that mention gh. ## Test plan order translate → scope filtered → round (stored/filtered/cursor/dup) → mention explicit → send sent/gone/throw → threadContext pages → apply registers + setupPending → policy rows → live smoke vs gitea.bitnasdaq.vip (needs user: agent account + token + repo).