dispatch#67 fix #2 needed a read-only CI credential and hit a hard wall: Gitea's own token-creation and admin-user endpoints reject token auth outright -- POST /users/{u}/tokens returns 'auth required', POST /admin/users returns 'token does not have at least one of required scope(s), required=[write:admin]' -- for ANY existing token, by design. An existing token cannot mint another token or create an account; the only way through either endpoint is real basic auth as an actual site admin. That is exactly what a sandboxed agent session cannot safely hold. internal/adminbootstrap: the core bootstrap, HTTP-only, fully testable against a fake transport (5 tests, no network). Given real admin username/password, it creates the target account (or resets its password if it already exists -- deliberately unconditional, gated behind a required ConfirmServiceAccount flag since that reset is only safe for a dedicated bot account), mints it a scoped token by authenticating AS that account (Gitea requires this specifically -- an admin cannot mint on another account's behalf), grants it read-only collaborator access on named repos, and stores the result via a SecretStore -- never returning the token or either password to the caller. internal/localtools: the MCP tool wrapper (mint_service_account_token) and the real op-CLI-backed SecretStore (token passed via stdin, never argv, per the estate's own secret-handling rule). Deliberately its OWN package, not internal/tools -- this capability is far too dangerous to risk it ever being wired into RegisterAll and reachable from the shared HTTP gitea-mcp server, even behind OIDC. internal/mcp/stdio.go: a stdio JSON-RPC transport (one message per line, standard MCP stdio framing) for the existing registry.Tool interface -- the only transport gitea-mcp had before this was HTTP (server.go), which a sandboxed session can reach but a real admin password never should be piped through. cmd/gitea-admin-mcp: the binary. Meant to be spawned by a LOCAL Claude Code session (.mcp.json stdio server) or run by hand, via 'op run --env-file ~/.op-env' so admin credentials arrive just-in-time and never touch a sandboxed session. 11 new tests total, all passing under -race. Nothing in cmd/gitea-mcp (the shared HTTP server) changed.
93 lines
3.3 KiB
Markdown
93 lines
3.3 KiB
Markdown
# gitea-admin-mcp
|
|
|
|
A separate, local-only stdio MCP server. One tool: `mint_service_account_token`.
|
|
|
|
## Why this exists
|
|
|
|
dispatch#67 fix #2 needed a read-only CI credential for a dedicated service
|
|
account, and hit a hard wall: Gitea's own `POST /users/{u}/tokens` and
|
|
`POST /admin/users` both reject token auth outright —
|
|
|
|
{"message":"auth required"}
|
|
{"message":"token does not have at least one of required scope(s), required=[write:admin], ..."}
|
|
|
|
— by design: an existing API token cannot mint another token, or create an
|
|
account, no matter its scope. The only way through either endpoint is real
|
|
basic auth as an actual site admin. A sandboxed agent session has no
|
|
interactive password and shouldn't be handed one — that's broader exposure
|
|
than the job needs. This binary is meant to run where real admin credentials
|
|
already exist just-in-time: your own machine, via `op run`.
|
|
|
|
## What it does
|
|
|
|
`mint_service_account_token` does the whole bootstrap in one call:
|
|
|
|
1. Creates the named account if it doesn't exist (admin basic auth), or
|
|
resets its password if it does. **This is why `confirm_service_account`
|
|
is required and checked first** — the tool unconditionally (re)sets
|
|
whatever password the target account has, which is correct for a
|
|
dedicated bot account and would be destructive pointed at a real person's
|
|
login by mistake.
|
|
2. Mints a scoped token for that account, authenticating as the account
|
|
itself (Gitea requires this specifically — an admin cannot mint a token
|
|
on another account's behalf).
|
|
3. Grants the account read-only collaborator access on any named
|
|
`owner/repo` targets (admin action, least privilege).
|
|
4. Stores the token in 1Password via `op item create`, value passed on
|
|
stdin — never as a literal argv element, matching the estate's own
|
|
secret-handling rule. Returns only the `op://` reference, never the
|
|
token value itself, to the caller.
|
|
|
|
## Setup
|
|
|
|
Deliberately **not** wired into `cmd/gitea-mcp` (the shared HTTP server) —
|
|
this capability is too dangerous to expose over the network, even behind
|
|
OIDC. `internal/localtools` is a separate package specifically so it can
|
|
never be wired into that server's `RegisterAll` by accident.
|
|
|
|
Build:
|
|
|
|
```
|
|
go build -o gitea-admin-mcp ./cmd/gitea-admin-mcp
|
|
```
|
|
|
|
Configure as a local stdio MCP server (Claude Code `.mcp.json`, run where
|
|
your real `op` session lives):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"gitea-admin": {
|
|
"command": "op",
|
|
"args": ["run", "--env-file", "~/.op-env", "--", "/path/to/gitea-admin-mcp"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Required env (see `~/.op-env`):
|
|
|
|
| Var | Meaning |
|
|
|---|---|
|
|
| `GITEA_BASE_URL` | e.g. `https://git.d-ma.be` |
|
|
| `GITEA_ADMIN_USERNAME` | a real site-admin account |
|
|
| `GITEA_ADMIN_PASSWORD` | that account's real password |
|
|
| `OP_VAULT` | 1Password vault for minted tokens (default `AgentSecrets`, per the estate's agent-governed ops-secrets policy) |
|
|
|
|
## Example call
|
|
|
|
```json
|
|
{
|
|
"username": "dispatch-bot",
|
|
"email": "dispatch@d-ma.be",
|
|
"scopes": ["read:repository", "read:issue"],
|
|
"token_name": "dispatch-ci-readonly",
|
|
"secret_name": "gitea-dispatch-bot-ci-readonly",
|
|
"grant_read_repos": ["mathias/cobalt-dingo"],
|
|
"confirm_service_account": true
|
|
}
|
|
```
|
|
|
|
Result carries `SecretRef` (e.g. `op://AgentSecrets/gitea-dispatch-bot-ci-readonly/credential`)
|
|
and `GrantedRepos` — never the token or either password.
|