Files
dsh-superagent/GITEA-DESIGN.md
T

113 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<number>` |
| 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:<n>` + retry map
`gitea-retry`; subject.url parsed by `giteaAddress` → read issue+comments[+reviews if PR]) →
2) `pollRepository` streams issues/pulls/comments (KV `gitea-stream:<id>`, 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 `<!-- dsh-superagent:<message.id> -->` 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: <gitea repo>, 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 <gitea login> 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<Record<string,Retry>>('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).