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>
11 KiB
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.
Principle: LLMs are not roles
The central rule of this model:
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:falsecan_merge_prs:falsecan_push_branches:falsecan_mutate_issues:truecan_author_impl_prs:false
gitea-author
- allowed:
read,branch.push,pr.create,pr.comment,issue.comment - forbidden:
pr.approve,pr.merge can_approve_prs:falsecan_merge_prs:falsecan_push_branches:truecan_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:truecan_merge_prs:falsecan_push_branches:falsecan_mutate_issues:falsecan_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:truecan_push_branches:falsecan_mutate_issues:falsecan_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:truecan_merge_prs:truecan_push_branches:truecan_mutate_issues:truecan_author_impl_prs:true
gitea-ownerexists 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_operationsalways wins overallowed_operations. If an operation appears in both, it is forbidden.- An operation not present in
allowed_operationsis 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.readnever matches a Gitea check, and a Gitea alias is never applied to another service). forbidden_operationsoverridesallowed_operationsafter 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_operationslist denies everything.
Identity and fail-closed rules
Before any mutating action, a workflow must know both:
- The active profile — which profile is in effect for this task.
- 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 inforbidden_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(fromgitea_whoami) against the PR author. - If they match,
pr.approveandpr.mergefail 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. Authorizationheaders 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 and
credential-isolation.md.
Relationship to roadmap issues
This document defines the model only. Related work is tracked separately under roadmap #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-owneris administrative, not a default automation role.