e0861bcb03
Promote the #103 minimal alias map to the documented public table GITEA_OPERATION_ALIASES and add the #106 enforcement layer: - normalize_operation(op, service): canonical namespaced names; legacy spellings accepted only via the explicit table; unknown, ambiguous, and cross-service names fail closed. - check_operation(op, allowed, forbidden, service): normalizes BOTH the requested operation and the profile lists before any membership check; forbidden always overrides allowed; unnormalizable allowed entries grant nothing and unnormalizable forbidden entries deny the request, so normalization can never silently widen permissions; empty/missing allowed list denies everything. - gitea_check_pr_eligibility now routes its capability check through check_operation, fixing the mismatch where canonical namespaced profile ops (gitea.pr.merge) never matched the raw action (merge) and namespaced forbidden entries were never enforced. - Document the normalization table and enforcement rules in docs/gitea-execution-profiles.md, replacing the stale 'enforcement out of scope' caveat. - tests/test_op_normalization.py: full #106 matrix (27 tests) — qualified/legacy allowed and forbidden, unknown, ambiguous, service mismatch, forbidden-overrides-allowed, empty/missing allowed, duplicates after normalization, no permission widening, and eligibility integration proving normalization happens before enforcement. Existing v1/env unqualified behaviour stays compatible. Closes #106 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
258 lines
11 KiB
Markdown
258 lines
11 KiB
Markdown
# 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`
|
|
- **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.
|
|
|
|
## 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).
|
|
|
|
## 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.
|