# Raw API Dispatch (`gitea_request`) `gitea_request` is a generic escape hatch that can call **any** Gitea REST endpoint by method and path. It exists for the long tail of the Gitea API that the curated, typed tools do not cover (merging PRs, reviews, writing files, webhooks, branch/tag protections, collaborators, Actions/CI, packages, notifications, and so on). > Prefer the dedicated tools whenever one exists. Use `gitea_request` only for > endpoints they do not cover. It is subject to policy, write-mode, and the > sensitive-path denylist described below. ## Arguments | Field | Type | Notes | |-------|------|-------| | `method` | enum | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` (case-insensitive). Any other method is rejected before any network call. | | `path` | string | Gitea REST path. The `/api/v1` prefix is optional. A full URL may be supplied — the host and query string are stripped. | | `query` | object | Optional query-string parameters. | | `body` | object | Optional JSON request body. **Never logged.** | The response is returned in a stable envelope: ```json { "method": "GET", "path": "/api/v1/repos/acme/app/pulls/1", "write": false, "repository": "acme/app", "data": { "...": "..." } } ``` List responses add `count` and `omitted`; oversized objects are returned as a truncated JSON string with `"truncated": true`. All responses are bounded by `MAX_TOOL_RESPONSE_ITEMS` / `MAX_TOOL_RESPONSE_CHARS`. ## Two-layer authorization A single tool surface would normally collapse the granularity of `policy.yaml`. To preserve it, every call is authorized twice: 1. **Central gate (`server.py`).** The registered `gitea_request` tool name is allowed/denied like any other tool. In service-PAT mode the central gate also parses the target repository from the path and verifies that the signed-in user has permission on that repository before the service PAT is used. 2. **Handler gate (`raw_tools.py`).** The handler derives a coarse **virtual tool name** of the form `gitea_request::` (for example `gitea_request:GET:repos` or `gitea_request:DELETE:repos`) and runs it back through the policy engine with the parsed repository, target path, and a `is_write` flag (`true` for any method other than GET/HEAD). This reuses the existing write-mode + write-whitelist enforcement and lets `policy.yaml` allow or deny raw dispatch per method and per top-level path segment. Because the policy engine matches tool names by **exact set membership** (only `paths` use globbing), the virtual name is deliberately coarse and stable. ### Example: lock raw dispatch to reads ```yaml tools: deny: - gitea_request:POST:repos - gitea_request:PUT:repos - gitea_request:PATCH:repos - gitea_request:DELETE:repos ``` ## Sensitive-path denylist Independently of `policy.yaml`, the handler blocks endpoints that touch an admin or credential surface **for every method, including GET** (a GET on these already leaks credentials or privileged configuration): - `/admin` - `*tokens*` - `*secrets*` - `*hooks*` - `*keys*` (and `*gpg_keys*`) - `applications/oauth2` - `actions/runners/registration-token` This denylist lives in the handler and **cannot be re-opened from `policy.yaml`.** It is overridden only by setting `RAW_API_ALLOW_SENSITIVE=true`. ## Configuration | Variable | Default | Notes | |----------|---------|-------| | `RAW_API_ENABLED` | `true` | Killswitch. When `false`, `gitea_request` refuses every dispatch with a `403`. | | `RAW_API_ALLOW_SENSITIVE` | `false` | When `true`, the admin/credential denylist is bypassed. Leave `false` unless you fully understand the exposure. | ## Security warning > With `WRITE_MODE=true`, the **write whitelist is the only brake** on > `POST`/`PUT`/`PATCH`/`DELETE` across the *entire* Gitea API surface reachable > by `gitea_request`. Any write method against a whitelisted repository will be > attempted. Keep the whitelist tight, prefer denying the write virtual tool > names in `policy.yaml`, and keep `RAW_API_ALLOW_SENSITIVE=false`. ## Behavioral notes and edge cases - **Full URL supplied instead of a path:** only the path is used; the host and query string are discarded (`query` carries query parameters). - **Path traversal (`..`):** rejected during argument validation (`400`). - **Unknown / non-HTTP method:** rejected during argument validation, before any network call. - **Cross-repo endpoints** such as `/repos/search` and `/repos/issues/search` are intentionally *not* treated as repository-scoped, so `repository` is `null` for them. - **Non-repository writes** such as `POST /user/repos` or `POST /orgs` are denied with *"write operation requires a repository target"*. This is the secure default — the per-user permission model is repository-scoped, so there is no repository against which to verify the write. This behavior is intentional and is not worked around. - **Service-PAT mode:** non-repository endpoints (for example `GET /user/orgs`) are denied by the central gate because per-user permission can only be verified against a repository target. Use the dedicated tools for those, or run in OAuth-only mode.