Address REQUEST_CHANGES on PR #688: - migrate_profiles: emit fully canonical reconciler/author/reviewer defaults; canonicalize explicit ops; fail if reconciler loses required pr.close/read - gitea_delete_branch: deny reconciler (and non-author roles); refuse preservation/protected branches before any API call - gitea_cleanup_merged_pr_branch: require reconciler role only; block preservation/evidence branches via branch_cleanup_guard - docs: end-to-end operator runbook for profile migrate/apply/reconnect/cleanup - tests: migration canonicalization, idempotency, raw-delete denial, guarded cleanup success and unmerged/preserve rejections Closes #687.
550 lines
25 KiB
Markdown
550 lines
25 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`, `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`
|
||
- `gitea.branch.delete` (merged-branch cleanup only — see below)
|
||
|
||
Forbidden on reconciler profiles: `gitea.pr.approve`, `gitea.pr.merge`,
|
||
`gitea.pr.review`, `gitea.pr.create`, `gitea.branch.push`, and
|
||
`gitea.repo.commit`.
|
||
|
||
### Merged-branch cleanup ownership (`gitea.branch.delete`)
|
||
|
||
The reconciler is the repository-supported owner of merged-PR source-branch
|
||
cleanup: `task_capability_map` maps `cleanup_merged_pr_branch` (and
|
||
`reconciliation_cleanup`) to role `reconciler` with permission
|
||
`gitea.branch.delete`. Post-merge branch lifecycle is reconciliation work —
|
||
it happens after the author, reviewer, and merger roles have completed, and
|
||
it must not be reachable from those roles.
|
||
|
||
Least-privilege constraints:
|
||
|
||
- `gitea.branch.delete` is granted **only** to reconciler profiles. Author,
|
||
reviewer, and merger profiles must never hold it; `gitea_delete_branch`
|
||
and `gitea_cleanup_merged_pr_branch` fail closed on any profile without
|
||
the permission.
|
||
- Even with the permission, reconciler deletion is only supported through the
|
||
guarded `gitea_cleanup_merged_pr_branch` path (#514 / #687): the PR must be
|
||
merged, the head an ancestor of the target, the branch not protected
|
||
(`master`/`main`/`dev`), the branch not a preservation/evidence ref (e.g.
|
||
`chore/issue-681-preserve-review-session-wip`), no open PR may still use the
|
||
head, and an explicit `CLEANUP MERGED PR <n> BRANCH <branch>` confirmation is
|
||
required. Raw `gitea_delete_branch` is **denied** to reconciler even when
|
||
`gitea.branch.delete` is present.
|
||
- Raw `git branch -d` / `git push --delete` cleanup remains blocked by
|
||
`branch_cleanup_guard` and the final-report validator regardless of
|
||
profile permissions.
|
||
- `gitea.branch.delete` has no short alias in `GITEA_OPERATION_ALIASES`;
|
||
write it fully qualified in `allowed_operations`. Migration must emit
|
||
canonical names such as `gitea.pr.close` (never bare `pr.close` /
|
||
`issue.close`, which the production normalizer rejects or drops).
|
||
|
||
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.
|
||
|
||
### Operational runbook: grant reconciler `gitea.branch.delete` (#687)
|
||
|
||
Merging a code PR that updates `migrate_profiles.py` / `reconciler_profile.py`
|
||
**does not** change the live operator profile on disk. Apply the profile
|
||
change deliberately, then reconnect the client-managed namespace.
|
||
|
||
1. **Approved migration / profile-update command** (from the repo root, using
|
||
the project venv if present):
|
||
|
||
```bash
|
||
# Dry-run first (default): validates v2 output, writes nothing
|
||
python3 migrate_profiles.py -i ~/.config/gitea-tools/profiles.json
|
||
|
||
# Apply: creates backup then writes migrated v2 config
|
||
python3 migrate_profiles.py -i ~/.config/gitea-tools/profiles.json -w
|
||
# Optional explicit paths:
|
||
# python3 migrate_profiles.py -i ~/.config/gitea-tools/profiles.json \
|
||
# -o ~/.config/gitea-tools/profiles.json \
|
||
# --backup ~/.config/gitea-tools/profiles.json.bak -w
|
||
```
|
||
|
||
If the live file is already v2, edit the reconciler identity’s
|
||
`allowed_operations` / `forbidden_operations` under
|
||
`environments.<env>.services.gitea.identities.reconciler` (or the
|
||
`prgs-reconciler` alias target) so allowed includes the canonical set
|
||
below — then re-validate with a load of the config (see step 3).
|
||
|
||
2. **Inspect the generated (or edited) profile** — confirm the reconciler
|
||
identity, for example:
|
||
|
||
```bash
|
||
python3 - <<'PY'
|
||
import json
|
||
from pathlib import Path
|
||
cfg = json.loads(Path.home().joinpath(".config/gitea-tools/profiles.json").read_text())
|
||
# v2 environments shape:
|
||
ident = cfg["environments"]["prgs"]["services"]["gitea"]["identities"]["reconciler"]
|
||
print("role:", ident.get("role"))
|
||
print("allowed:", ident.get("allowed_operations"))
|
||
print("forbidden:", ident.get("forbidden_operations"))
|
||
PY
|
||
```
|
||
|
||
3. **Validate canonical operation names and least privilege**
|
||
|
||
Expected canonical **allowed** (defaults after migration):
|
||
|
||
- `gitea.read`
|
||
- `gitea.pr.close` (required)
|
||
- `gitea.pr.comment`
|
||
- `gitea.issue.comment`
|
||
- `gitea.issue.close`
|
||
- `gitea.branch.delete` (recommended; cleanup only)
|
||
|
||
Expected **forbidden** includes at least: `gitea.pr.approve`,
|
||
`gitea.pr.merge`, `gitea.pr.review`, `gitea.pr.create`,
|
||
`gitea.branch.push`, `gitea.repo.commit`.
|
||
|
||
No shorthand (`pr.close`, `issue.close`, `pr.comment`) may remain.
|
||
Validate with the production loader:
|
||
|
||
```bash
|
||
python3 - <<'PY'
|
||
import gitea_config, reconciler_profile
|
||
from pathlib import Path
|
||
path = str(Path.home() / ".config/gitea-tools/profiles.json")
|
||
gitea_config.load_config(path) # fails closed on invalid config
|
||
# Or assess the reconciler lists directly after extracting them:
|
||
# print(reconciler_profile.assess_reconciler_profile(allowed, forbidden))
|
||
PY
|
||
```
|
||
|
||
4. **Merging PR #688 (or any code PR) does not update the live profile.**
|
||
Code changes only the migration helper, schema, docs, and tests. The
|
||
operator must still run `migrate_profiles.py -w` or an equivalent
|
||
authorized edit of `~/.config/gitea-tools/profiles.json`.
|
||
|
||
5. **Supported apply method:** `python3 migrate_profiles.py … -w` (backup
|
||
created automatically) **or** operator-authorized edit of the live
|
||
profiles file after backup. Unsupported: silent mtime tricks, manual
|
||
process kill to “reload”, or undocumented env overrides.
|
||
|
||
6. **Backup and validation:** `-w` copies the input to
|
||
`<input_path>.bak` (or `--backup PATH`) before writing. Re-run
|
||
`load_config` / `assess_reconciler_profile` after write. Keep the
|
||
`.bak` until live whoami/capability checks pass.
|
||
|
||
7. **Client-managed namespace reconnect/reload:** reconnect or reload the
|
||
IDE MCP client so `gitea-reconciler` restarts from current `master` and
|
||
the updated `GITEA_MCP_PROFILE=prgs-reconciler` config. Do not hand-launch
|
||
`mcp_server.py` / `gitea_mcp_server.py` with ad hoc `GITEA_*` env
|
||
(see #686 / #630).
|
||
|
||
8. **Live reverification** (through the client-managed `gitea-reconciler`
|
||
namespace only):
|
||
|
||
- `gitea_whoami` → identity + profile `prgs-reconciler`
|
||
- `gitea_assess_master_parity` → `stale=false`, `restart_required=false`
|
||
- `gitea_resolve_task_capability(task="cleanup_merged_pr_branch")` →
|
||
`allowed_in_current_session=true` only when permission and role match
|
||
- `gitea_resolve_task_capability(task="delete_branch")` →
|
||
**not** allowed for reconciler (role denial must be enforced)
|
||
|
||
9. **Guarded cleanup usage** (example for a merged PR whose source branch
|
||
remains on the remote):
|
||
|
||
```text
|
||
gitea_cleanup_merged_pr_branch(
|
||
pr_number=<N>,
|
||
branch=<exact PR head branch>,
|
||
confirmation="CLEANUP MERGED PR <N> BRANCH <exact PR head branch>",
|
||
remote="prgs",
|
||
org="Scaled-Tech-Consulting",
|
||
repo="Gitea-Tools",
|
||
worktree_path="<path under branches/>",
|
||
)
|
||
```
|
||
|
||
The tool refuses unmerged PRs, protected branches, preservation/evidence
|
||
branches, open-PR heads, mismatched branch names, and wrong confirmation.
|
||
|
||
10. **Prohibitions**
|
||
|
||
- No raw `git push --delete`, `git branch -d` / `-D`, or delete refspecs
|
||
- No arbitrary `gitea_delete_branch` from reconciler
|
||
- No unsupported profile switching mid-run without full re-preflight
|
||
- No ad hoc hand-edits of live profiles **unless** operator-authorized,
|
||
backed up, and revalidated as above
|
||
|
||
Canonical migrated reconciler example:
|
||
|
||
```json
|
||
{
|
||
"role": "reconciler",
|
||
"allowed_operations": [
|
||
"gitea.read",
|
||
"gitea.pr.close",
|
||
"gitea.pr.comment",
|
||
"gitea.issue.comment",
|
||
"gitea.issue.close",
|
||
"gitea.branch.delete"
|
||
],
|
||
"forbidden_operations": [
|
||
"gitea.pr.approve",
|
||
"gitea.pr.merge",
|
||
"gitea.pr.review",
|
||
"gitea.pr.create",
|
||
"gitea.branch.push",
|
||
"gitea.repo.commit"
|
||
]
|
||
}
|
||
```
|
||
|
||
## 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.
|