Adds docs/raw-api.md (two-layer policy, sensitive denylist, env vars, write-mode warning), links it from index and api-reference, documents RAW_API_ENABLED / RAW_API_ALLOW_SENSITIVE in .env.example, and adds commented virtual-tool-name deny examples to policy.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.1 KiB
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_requestonly 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:
{
"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:
- Central gate (
server.py). The registeredgitea_requesttool 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. - Handler gate (
raw_tools.py). The handler derives a coarse virtual tool name of the formgitea_request:<METHOD>:<top-path-segment>(for examplegitea_request:GET:reposorgitea_request:DELETE:repos) and runs it back through the policy engine with the parsed repository, target path, and ais_writeflag (truefor any method other than GET/HEAD). This reuses the existing write-mode + write-whitelist enforcement and letspolicy.yamlallow 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
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/oauth2actions/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 onPOST/PUT/PATCH/DELETEacross the entire Gitea API surface reachable bygitea_request. Any write method against a whitelisted repository will be attempted. Keep the whitelist tight, prefer denying the write virtual tool names inpolicy.yaml, and keepRAW_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 (
querycarries 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/searchand/repos/issues/searchare intentionally not treated as repository-scoped, sorepositoryisnullfor them. - Non-repository writes such as
POST /user/reposorPOST /orgsare 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.