diff --git a/README.md b/README.md index cea59eb..a7a7297 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,10 @@ Notes: `gitea_get_profile` returns the full non-secret profile metadata so a workflow can inspect which runtime it is talking to before deciding to act. - See [`docs/gitea-execution-profiles.md`](docs/gitea-execution-profiles.md) for - the full profile model. + the full profile model, and + [`docs/llm-workflow-runbooks.md`](docs/llm-workflow-runbooks.md) for the + task-scoped, profile-based runbooks (create/review/merge/close, thin + launchers, migration, fail-closed rules). - **Audit logging (#18):** mutating actions emit a durable, redacted JSON audit record — timestamp, action, result (`allowed`/`blocked`/`failed`/`succeeded`), profile name + audit label, authenticated username, target repo/issue/PR, diff --git a/docs/llm-workflow-runbooks.md b/docs/llm-workflow-runbooks.md new file mode 100644 index 0000000..b54d4e5 --- /dev/null +++ b/docs/llm-workflow-runbooks.md @@ -0,0 +1,251 @@ +# LLM-Operated Gitea Workflow Runbooks + +## Purpose + +Runbooks for the common Gitea workflows an LLM performs through the `gitea-mcp` +package of the MCP Control Plane: creating issues, implementing them, opening +and reviewing pull requests, merging, and closing out — safely and +reproducibly. + +These runbooks are **operational guidance only**. They add no tooling; the +behavior they rely on already exists (canonical runtime profiles, the +interactive setup menu, identity/eligibility checks, gated review/merge, and +audit logging). See [Related documents](#related-documents). + +## Principle: the profile is the role, not the LLM + +```text +The LLM is not the role. +The MCP execution profile used for the task is the role. +``` + +An LLM session is never permanently an "author," "reviewer," or "merger." Any +session may perform any of these roles — but only while operating through a +**task-appropriate profile** whose authenticated Gitea identity and allowed +operations fit the task. A task selects a profile; a profile is not assigned to +a model. See [`gitea-execution-profiles.md`](gitea-execution-profiles.md). + +Example role-scoped instructions: + +```text +Use an author profile to implement issue #N and open a PR. +Use any eligible reviewer profile to review PR #N. +Use any eligible merger profile to merge PR #N if checks pass. +``` + +## Prerequisites: canonical config + thin launchers + +Runtime profiles live in **one canonical JSON file**, referenced by every LLM +launcher. No client config contains raw credentials. + +### Canonical config file + +Selected by two environment variables: + +- `GITEA_MCP_CONFIG` — path to the canonical file (e.g. + `~/.config/gitea-tools/profiles.json`). +- `GITEA_MCP_PROFILE` — the named profile to activate. + +Shape (see [`../gitea-mcp.example.json`](../gitea-mcp.example.json)): + +```json +{ + "version": 1, + "profiles": { + "prgs-reviewer": { + "base_url": "https://gitea.example.invalid", + "username": "", + "auth": { "type": "keychain", "id": "prgs-reviewer-token" }, + "default_owner": "", + "execution_profile": "gitea-reviewer" + }, + "prgs-author": { + "base_url": "https://gitea.example.invalid", + "username": "", + "auth": { "type": "env", "name": "GITEA_TOKEN_PRGS_AUTHOR" }, + "default_owner": "", + "execution_profile": "gitea-author" + } + } +} +``` + +- `version` — canonical schema version (currently `1`). +- `profiles` — map of profile name → profile. +- `auth` — a **reference**, never an inline secret: + - **keychain**: `{ "type": "keychain", "id": "" }` — the token is + read from the macOS keychain on demand. + - **env**: `{ "type": "env", "name": "" }` — the token is read + from that environment variable. + +Inline `token`/`password` keys are rejected. Token *values* are never stored in, +returned by, or logged from profile metadata. Precedence: explicit process env +vars override JSON profile values; the JSON profile fills only what the +environment leaves unset. With `GITEA_MCP_CONFIG` unset, behavior is exactly the +legacy environment-only mode. + +### Thin launcher pattern + +An LLM MCP launcher (Claude / Gemini / Codex) contains **only** command, args, +and the two `GITEA_MCP_*` variables — never a token or password: + +```json +"gitea-tools": { + "command": "/path/to/Gitea-Tools/venv/bin/python3", + "args": ["/path/to/Gitea-Tools/mcp_server.py"], + "env": { + "GITEA_MCP_CONFIG": "/path/to/.config/gitea-tools/profiles.json", + "GITEA_MCP_PROFILE": "prgs-reviewer" + } +} +``` + +Run the same server as several launcher entries (e.g. `-author`, `-reviewer`, +`-merger`), each pointing at a different `GITEA_MCP_PROFILE`. + +## Setup runbook — interactive menu + +Create and manage profiles without hand-editing JSON: + +```bash +python gitea_config.py menu +``` + +Menu options: list / add / edit / remove profiles · validate config · test +profile authentication · show authenticated user · generate launcher snippets +(Claude/Gemini/Codex) · check reviewer eligibility for a PR. + +**Create an author + a reviewer profile:** + +1. `add profile` → name `prgs-author`, base URL, username, default owner/repo, + execution profile `gitea-author`, auth type `keychain` or `env`. + - keychain: store the token now (hidden prompt); it goes to the keychain + under an id like `prgs-author-token` — never into the JSON. + - env: record a var name like `GITEA_TOKEN_PRGS_AUTHOR`; set that variable + yourself in the environment. +2. `add profile` again → name `prgs-reviewer`, execution profile + `gitea-reviewer`. Existing profiles are preserved. +3. `validate config` → confirm no problems. +4. `generate launcher snippets` → paste the printed snippet into each LLM + client's MCP config (it contains no secret). +5. `test profile authentication` → prints the resolved Gitea username (the only + time an API call is made, and only on request). +6. `check reviewer eligibility for a PR` → enter a PR number; prints the + authenticated user, the PR author, and `ELIGIBLE` / `INELIGIBLE`. Read-only — + it never approves or merges. + +## Migration runbook — away from duplicated credential blocks + +Old setups duplicated `GITEA_USER_*`, `GITEA_PASS_*`, and `GITEA_SITE_*` across +every LLM's `mcp_config.json` — duplicating profiles and exposing secrets. + +1. For each instance/role, create one canonical profile (menu → `add profile`), + storing the secret in the keychain or an env var and referencing it by + id/name only. +2. `validate config`, then `test profile authentication` for each profile. +3. Replace each LLM's server block with the thin launcher (command + args + + `GITEA_MCP_CONFIG` + `GITEA_MCP_PROFILE`). +4. Delete the `GITEA_USER_*` / `GITEA_PASS_*` / `GITEA_SITE_*` blocks from every + LLM config. +5. Rotate any token that previously sat in a client config. + +Legacy environment-only setups keep working unchanged until migrated. + +## Workflow runbooks + +Each runbook names the **profile role** it runs under, the steps, and a safe +prompt. Confirm the active profile first (`gitea_get_profile` / `gitea_whoami`). + +### Create an issue / child issues + +- **Profile:** issue-manager or author (any profile allowed to create issues). +- **Steps:** create the parent/roadmap issue; create child issues; apply the + minimal label set; link children to the parent. +- **Prompt:** `Using the issue-manager profile, create issue "" with body + <body>, then create child issues for <list> and link them to the parent.` + +### Implement an issue and open a PR + +- **Profile:** author. +- **Steps:** claim the issue (`status:in-progress`); branch from latest + `master` (`feat/issue-<n>-...` / `fix/...` / `docs/...`); implement narrowly; + add/update tests if behavior changes; run the full suite; commit with an + issue-linked message; open a PR to `master`. **Do not** review or merge your + own PR. +- **Prompt:** `Use an author profile to implement issue #N and open a PR to + master. Do not self-review or self-merge.` + +### Review a PR / request changes / approve + +- **Profile:** reviewer (must be allowed to review/approve/request_changes, and + must **not** be the PR author). +- **Steps:** confirm identity + eligibility (menu eligibility check or + `gitea_check_pr_eligibility`); read the diff; confirm scope matches the linked + issue; post the review (`comment` / `request_changes` / `approve`) via the + gated review tool. Pin the reviewed head SHA where supported. +- **Prompt:** `Use any eligible reviewer profile to review PR #N. Approve only + if scope matches issue #M and checks pass; otherwise request changes.` + +### Merge a PR + +- **Profile:** merger (allowed to merge; must **not** be the PR author). +- **Steps:** confirm eligibility; require explicit confirmation + (`MERGE PR <n>`); optionally pin head SHA / changed-file set; merge only when + Gitea reports the PR mergeable (branch-protection checks satisfied). No force, + no ignore-checks. +- **Prompt:** `Use any eligible merger profile to merge PR #N if checks pass and + it is mergeable. Confirm with "MERGE PR N". Do not force-merge.` + +### Close the issue after merge + +- **Profile:** issue-manager or merger. +- **Steps:** verify remote `master` actually contains the merge; close the + issue (or rely on a `Closes #N` keyword); release `status:in-progress`; + clean up merged branches. +- **Prompt:** `After confirming master contains the merge of PR #N, close issue + #M and delete the merged branch.` + +### Stop on blocker + +- **Any profile.** If a required gate cannot be satisfied — identity + unverifiable, ineligible profile, self-authored PR, moved head, unexpected + files, detected secret, or any production/deploy behavior — **stop, report the + blocker, and take no mutating action.** Fail closed; never work around a gate. + +## Fail-closed behavior + +Before any mutating action the workflow verifies identity, active profile, +requested operation, target repo, target issue/PR, and (for review/merge) the PR +author. If any check cannot be satisfied, it **fails closed** — no mutation: + +| Condition | Result | +|-----------|--------| +| Authenticated identity cannot be verified | blocked | +| Unknown / unconfigured profile | blocked | +| Profile not allowed the requested operation | blocked | +| Authenticated user **is** the PR author (approve/merge) | blocked (no self-review/-merge) | +| PR head SHA changed since review | blocked | +| PR's changed-file set differs from expected | blocked | +| PR not open, or Gitea reports it not mergeable | blocked | +| Secret / token detected in content | blocked | +| Production / deploy / Ops behavior requested | blocked (out of scope for gitea-mcp) | + +All mutating attempts — allowed, blocked, failed, or succeeded — are audit-logged +with the profile and authenticated user when `GITEA_AUDIT_LOG` is set (see +[`safety-model.md`](safety-model.md)). + +## Safety notes + +- Never place raw tokens or passwords in any LLM MCP config; reference secrets + by keychain id or env var name only. +- Never self-review or self-merge; never bypass Gitea branch protections. +- No Jenkins / Ops / deploy / production behavior in `gitea-mcp`. + +## Related documents + +- [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model. +- [`safety-model.md`](safety-model.md) — trust boundaries and audit logging. +- [`tool-boundaries.md`](tool-boundaries.md) — per-tool allowed operations. +- [`credential-isolation.md`](credential-isolation.md) — credential handling. +- [`release-workflows.md`](release-workflows.md) — release/merge workflow. +- [`../README.md`](../README.md) — canonical config, thin launchers, the menu.