# Gitea MCP Execution Profiles

## Purpose

This document defines the **task-scoped execution profile model** for the
`gitea-mcp` package of the MCP Control Plane. It describes *what a profile is*,
the metadata each profile carries, and the safety rules that govern which
profile may perform which Gitea operation.

This issue defines the **model only**. It does not implement runtime profile
loading, profile switching, or any review/merge behavior — see
[Relationship to roadmap issues](#relationship-to-roadmap-issues).

## Principle: LLMs are not roles

The central rule of this model:

```text
The LLM is not the role.
The MCP credential/profile used for the task is the role.
```

An LLM session is not permanently an "author," a "reviewer," or a "merger."
Any LLM session may perform any of these roles — but only while operating
through a **task-appropriate execution profile** whose authenticated Gitea
identity and allowed operations fit the task.

Consequences:

- A task selects a profile; a profile is not assigned to a model.
- The same LLM may act as author for one task and reviewer for another, by
  using different profiles — never by escalating a single profile.
- Two roles that must not be held by one identity (e.g. author and merger of
  the same PR) are separated by using **different authenticated identities**,
  not by trusting the LLM to behave.

## Profile model

A Gitea MCP execution profile is a named, declarative description of an
authenticated capability set. Each profile defines the following fields:

| Field | Type | Meaning |
|-------|------|---------|
| `profile_name` | string | Stable identifier, e.g. `gitea-reviewer`. |
| `authenticated_username` | string | The Gitea login this profile authenticates as (verified at runtime via `gitea_whoami`, not trusted from config). |
| `allowed_operations` | list | Operation categories this profile may perform. |
| `forbidden_operations` | list | Operation categories this profile must never perform. |
| `token_source_name` | string | The *name* of the secret source (e.g. env var name or secret key). **Never the token value.** |
| `audit_label` | string | Short label attached to audit records for actions by this profile. |
| `can_approve_prs` | bool | May submit an approving PR review. |
| `can_merge_prs` | bool | May merge a PR. |
| `can_push_branches` | bool | May push branches / create commits. |
| `can_mutate_issues` | bool | May create/edit/label/close issues and comment. |
| `can_author_impl_prs` | bool | May author implementation PRs (branch + commit + open PR). |

`token_source_name` records **where** a token comes from (a variable or key
name), never the token itself. Token values are never part of a profile object,
never logged, never returned by a tool, and never committed.

## Example profiles

The following are the reference profiles. Booleans express intended capability
boundaries; they are the model, not a runtime enforcement mechanism yet.

### `gitea-issue-manager`

- **allowed:** `read`, `issue.create`, `issue.comment`, `issue.label`, `issue.close`
- **forbidden:** `pr.approve`, `pr.merge`, `branch.push`
- `can_approve_prs`: `false`
- `can_merge_prs`: `false`
- `can_push_branches`: `false`
- `can_mutate_issues`: `true`
- `can_author_impl_prs`: `false`

### `gitea-author`

- **allowed:** `read`, `branch.push`, `pr.create`, `pr.comment`, `issue.comment`
- **forbidden:** `pr.approve`, `pr.merge`
- `can_approve_prs`: `false`
- `can_merge_prs`: `false`
- `can_push_branches`: `true`
- `can_mutate_issues`: `false` (may comment, may not manage)
- `can_author_impl_prs`: `true`

### `gitea-reviewer`

- **allowed:** `read`, `pr.comment`, `pr.review`, `pr.approve`, `pr.request_changes`, `issue.comment`
- **forbidden:** `pr.merge`, `branch.push`
- `can_approve_prs`: `true`
- `can_merge_prs`: `false`
- `can_push_branches`: `false`
- `can_mutate_issues`: `false`
- `can_author_impl_prs`: `false`

### `gitea-merger`

- **allowed:** `read`, `pr.merge`
- **forbidden:** `pr.approve`, `branch.push`, `pr.create`
- `can_approve_prs`: `false` (a merger must not also be the sole approver)
- `can_merge_prs`: `true`
- `can_push_branches`: `false`
- `can_mutate_issues`: `false`
- `can_author_impl_prs`: `false`

### `gitea-owner`

- **allowed:** broad administrative access; use sparingly and never for routine
  LLM workflow tasks.
- **forbidden:** nothing structurally, which is exactly why it must not be the
  default profile for automated work.
- `can_approve_prs`: `true`
- `can_merge_prs`: `true`
- `can_push_branches`: `true`
- `can_mutate_issues`: `true`
- `can_author_impl_prs`: `true`

> `gitea-owner` exists for human/administrative use. Automated LLM workflows
> should prefer the narrowest sufficient profile. An all-powerful profile is a
> convenience, not a role, and it does not exempt a session from the
> self-review / self-merge rule below.

## Allowed and forbidden operations

Operations are grouped into coarse categories so profiles stay readable:

- `read` — view issues, PRs, files, identity (`gitea_whoami`).
- `issue.*` — `create`, `comment`, `label`, `close`.
- `pr.*` — `create`, `comment`, `review`, `approve`, `request_changes`, `merge`.
- `branch.push` — push branches / create commits.

Rules:

- `forbidden_operations` always wins over `allowed_operations`. If an operation
  appears in both, it is forbidden.
- An operation not present in `allowed_operations` is treated as **not
  allowed** (deny by default).

## Operation-name normalization (#106)

Canonical operation names are namespaced: `{service}.{area}.{verb}` (e.g.
`gitea.pr.merge`, `jenkins.build.read`). Legacy unqualified spellings are
accepted **only** through the explicit alias table below (the code of record
is `GITEA_OPERATION_ALIASES` in `gitea_config.py`; the enforcement matrix is
`tests/test_op_normalization.py`).

| Legacy spelling   | Canonical operation        |
|-------------------|----------------------------|
| `read`            | `gitea.read`               |
| `review`          | `gitea.pr.review`          |
| `comment`         | `gitea.pr.comment`         |
| `approve`         | `gitea.pr.approve`         |
| `request_changes` | `gitea.pr.request_changes` |
| `merge`           | `gitea.pr.merge`           |
| `pr.create`       | `gitea.pr.create`          |
| `branch.push`     | `gitea.branch.push`        |
| `branch`          | `gitea.branch.create`      |
| `commit`          | `gitea.repo.commit`        |
| `push`            | `gitea.branch.push`        |
| `open_pr`         | `gitea.pr.create`          |

For non-Gitea services, a single unqualified word namespaces to the checked
service (`read` → `jenkins.read` when checking Jenkins); names already
prefixed with that service pass through unchanged.

Enforcement rules (`gitea_config.check_operation`, run **before** any
allowed/forbidden membership check):

- Unknown operation names fail closed (denied).
- Ambiguous names — dotted names that are neither service-prefixed nor in the
  alias table — fail closed.
- Cross-service names are never accepted by the wrong service
  (`jenkins.read` never matches a Gitea check, and a Gitea alias is never
  applied to another service).
- `forbidden_operations` overrides `allowed_operations` after both sides are
  normalized, so a legacy spelling can never bypass a canonical forbidden
  entry (or vice versa).
- An allowed entry that cannot be normalized grants nothing; a forbidden
  entry that cannot be normalized denies the request. Normalization can
  therefore never silently widen permissions.
- An empty or missing `allowed_operations` list denies everything.

## Issue comments versus PR reviews (#126)

Issue discussion comments and PR reviews are different capabilities and are
gated by different operations:

- **Issue comments** (`gitea_list_issue_comments`, `gitea_create_issue_comment`)
  post to and read from an issue's discussion thread
  (`/issues/{n}/comments`). Listing requires `gitea.read`; creating requires
  `gitea.issue.comment`. They never submit review verdicts.
- **PR reviews** (`gitea_review_pr`, `gitea_submit_pr_review`) submit
  approve/request-changes/comment verdicts on pull requests
  (`/pulls/{n}/reviews`) and are gated by the `gitea.pr.*` family
  (`gitea.pr.review`, `gitea.pr.approve`, `gitea.pr.request_changes`,
  `gitea.pr.comment`).

A profile holding the full PR review/merge set still cannot post issue
discussion comments unless it also allows `gitea.issue.comment`, and vice
versa — neither family implies the other. Both comment tools require an
explicit issue number; the target repo comes only from the standard
remote/org/repo arguments. Create operations are audit-logged
(`create_issue_comment`) when `GITEA_AUDIT_LOG` is configured, errors are
redacted, and normal output contains no endpoint URLs
(`GITEA_MCP_REVEAL_ENDPOINTS=1` is the local admin opt-in for web links).

## PR edits versus PR closure (#216)

Editing a pull request and closing one are different capabilities:

- **PR edits** (`gitea_edit_pr` with `title`/`body`/`base`, or reopening with
  `state="open"`) stay on the ordinary edit path and need no dedicated
  capability.
- **PR closure** (`gitea_edit_pr` with `state="closed"`) requires the
  distinct `gitea.pr.close` operation. The resolver task is `close_pr`
  (`gitea_resolve_task_capability(task="close_pr")`, author-side). Without
  `gitea.pr.close` the close attempt fails closed — no API call, structured
  `permission_report` — so the broad edit path can never be used as an
  untracked close fallback.
- Closures are audited as a distinct `close_pr` action with
  `required_permission: gitea.pr.close` in the request metadata, so final
  reports can prove exactly which mutation capability was exercised (#191).

`gitea.pr.close` has no legacy alias; spell it canonically. It is not part of
any default profile: the operator grants it deliberately (e.g. for an
explicit operator-directed closure of a contaminated PR). If `close_pr` ever
resolves as unknown, agents must fail closed rather than fall back to the
edit path.

## Reconciler profile for already-landed open PRs (#304 / #310)

Normal author and reviewer profiles must not gain broad `gitea.pr.close`
authority. Already-landed open PRs (head SHA is an ancestor of the target
branch) need a dedicated reconciler profile such as `prgs-reconciler` with a
narrow operation set:

- `gitea.read`
- `gitea.pr.comment`
- `gitea.issue.comment`
- `gitea.issue.close`
- `gitea.pr.close`

Forbidden on reconciler profiles: `gitea.pr.approve`, `gitea.pr.merge`,
`gitea.pr.review`, `gitea.pr.create`, `gitea.branch.push`, and
`gitea.repo.commit`.

Launch a static `gitea-reconciler` MCP namespace with
`GITEA_MCP_PROFILE=prgs-reconciler`. Profile shape is validated by
`reconciler_profile.assess_reconciler_profile` (#304). Use the
`gitea_reconcile_already_landed_pr` tool (#310). The resolver task is
`reconcile_already_landed_pr`. PR close is allowed only after live PR fetch,
fresh target-branch fetch, recorded target SHA, and ancestor proof. PRs whose
heads are not already landed cannot be closed through this path.

## Identity and fail-closed rules

Before **any** mutating action, a workflow must know both:

1. **The active profile** — which profile is in effect for this task.
2. **The authenticated identity** — the real Gitea login, verified via
   `gitea_whoami` (issue #11), not read from configuration and trusted.

Fail-closed requirements:

- If the active profile is unknown → **stop; do not mutate.**
- If the authenticated identity cannot be determined → **stop; do not mutate.**
- If the requested operation is not in the profile's `allowed_operations`, or is
  in `forbidden_operations` → **stop; do not mutate.**
- Ambiguity is treated as denial. The safe default is always "do not act."

Read-only actions may proceed without a resolved profile, but must still never
expose token or credential material.

## Self-review and self-merge prevention

A profile/session **must not approve or merge a PR authored by the same
authenticated Gitea user.**

- The check compares the profile's *verified* `authenticated_username`
  (from `gitea_whoami`) against the **PR author**.
- If they match, `pr.approve` and `pr.merge` fail closed, regardless of what the
  profile's capability booleans say.
- This is why author and merger/reviewer roles are separated by **identity**,
  not by prompt or by a single escalating profile. It is also why this was the
  concrete blocker discovered while dogfooding PR #8 for issue #52.

## Token and secret handling

- Token **values** are never logged, never returned by any tool, and never
  committed to the repository.
- Profiles reference a `token_source_name` (a variable/key *name*) only.
- `Authorization` headers and raw credentials must never appear in tool output,
  audit records, or error messages.

## Separation from other MCP boundaries

`gitea-mcp` profile work stays within the Gitea trust boundary. It must **not**
add or absorb Jenkins, Ops, GlitchTip, Release, deploy, rollback, migration,
restart, or production behavior. Those belong to their own MCP packages under
the "one server per trust boundary" model described in
[`tool-boundaries.md`](tool-boundaries.md) and
[`credential-isolation.md`](credential-isolation.md).

## Profile Activation and Runtime Identity Clarity (#131)

To make Gitea MCP profile activation and runtime identity state explicit, the following mechanisms are supported:

### 1. Static-Profile vs. Dynamic-Profile Mode
- **Static-Profile Mode (Default):** The active profile is fixed at server launch based on the `GITEA_MCP_PROFILE` environment variable (with `GITEA_MCP_CONFIG` pointing to the config path). Local environment variables are static once a subprocess is spawned by the host. Modifying the environment variables on the host does not dynamically update an already-connected MCP server process.
- **Dynamic-Profile Mode:** Profile switching via the `gitea_activate_profile` tool is supported **only** if the configuration JSON explicitly opts in by setting `"allow_runtime_switching": true` under rules or top-level keys. Otherwise, attempting to switch profiles dynamically will fail closed.

### 2. Dual MCP Namespaces Recommendation
For security-sensitive or high-risk tasks, the preferred safety model uses separate, isolated MCP server instances (namespaces/sessions) launched with static profiles:
- `gitea-author`: Exposes tools configured with author permissions; cannot perform approvals or merges.
- `gitea-reviewer`: Exposes tools configured with reviewer permissions; used for PR reviews. Review and merge are separate workflow roles. A reviewer approval is not merge authorization.
- `gitea-merger`: Exposes tools configured with merger permissions; used for PR merges.
This layout maintains physical separation of credentials and prevents privilege escalation within a single session.
This is the model accepted in #139; deployment details, rationale, and client
setup live in
[`gitea-dual-namespace-deployment.md`](gitea-dual-namespace-deployment.md).

### 3. Verification Post-Switching
When dynamic profile switching is enabled and a profile is activated via `gitea_activate_profile`, the session MUST immediately:
1. Clear the cached identity.
2. Call `gitea_whoami` with the target remote to prove and verify the fresh Gitea authenticated identity.
This guarantees the active profile operations align with the actual Gitea authenticated user credential.

## Gitea MCP Runtime Isolation and Worktree Safety

To ensure high availability and prevent broken feature worktrees from disabling essential security/identity controls, the Gitea MCP server implements runtime isolation:

- **Startup Conflict Check:** The MCP server (`mcp_server.py`) acts as a conflict-free loader. On startup, it scans all Python files in the directory for unresolved git merge conflicts (`<<<<<<<`, `=======`, `>>>>>>>`). If any are found, it prints an `infra_stop` message and exits immediately.
- **Workflow Guard:** Before starting reviewer work, task routing checks if the MCP runtime source is mid-merge (by checking for `.git/MERGE_HEAD` or conflict markers). If dirty, it returns `infra_stop` (never `wrong_role_stop` or empty queue) to prevent unsafe mutations.
- **Recovery Instructions:** To recover from an `infra_stop` state:
  1. Resolve all merge conflicts in the local repository or abort the merge (`git merge --abort` / `git rebase --abort`).
  2. Restart the Gitea MCP server process.
  3. Retry the task.

## Relationship to roadmap issues

This document defines the **model only**. Related work is tracked separately
under roadmap [#10](https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools/issues/10):

- **#11** — Authenticated-user identity lookup (`gitea_whoami`). *Complete;
  this model depends on it for verified identity.*
- **#19** — Runtime profile configuration via environment (loading real
  profiles/tokens). *Not this issue.*
- **#13** — Read-only profile discovery (exposing the active profile). *Not this
  issue.*
- **#14** — PR author / reviewer eligibility checks. *Not this issue.*
- **#15** — Gated PR review/approve actions. *Not this issue.*
- **#16** — Gated PR merge workflow. *Not this issue.*
- **#18** — Audit logging of mutating actions with profile metadata. *Not this
  issue.*

## Non-goals

- Do **not** implement runtime profile switching or selection here.
- Do **not** implement multi-token loading here.
- Do **not** implement approve, merge, or eligibility workflows here.
- Do **not** expose, log, or commit any token or secret.
- Do **not** add Jenkins, Ops, GlitchTip, Release, deploy, or production
  behavior.
- Do **not** create an all-powerful server; `gitea-owner` is administrative, not
  a default automation role.
