Files
mcpctl/docs/gitproviders.md
Michal 6d38953a96 docs(agentic-teams): document the new resources, and make the webhook reachable
Five resources shipped with no documentation and one documented lie:
docs/inference-tasks.md still told people `mcpctl get tasks` lists the
inference queue. Work items own that name now.

Adds docs/epics.md (the Epic -> EpicTask -> Task hierarchy, the state machine,
risk-scaled review gates, the accept-merges gate, and reconciliation),
docs/gitproviders.md (forge registration, discovery/import, merge-on-accept,
post-merge reopen) and docs/agent-authority.md (the three run modes, and
plainly which two are not implemented).

Two things found while writing them, both fixed rather than filed:

- `webhookSecretKey` was read by the webhook route but writable from NOWHERE
  -- absent from the create schema, update schema, service, CLI and apply
  spec. Since the endpoint fails closed on a null key, post-merge CI failures
  could never reopen their task; the board would keep claiming `merged` for
  work that broke the build. Adds --webhook-secret-key, the schema fields and
  the apply key, with tests covering persist / omit / explicit-null /
  reject-empty. Omission deliberately does not clear the column, so an
  unrelated edit cannot silently disarm the webhook.

- src/cli/src/commands/create.ts contained a literal NUL byte inside a string
  literal (a binary-file check written with the raw character rather than an escape). Identical behaviour, but it made the whole file read as
  binary, so `grep -I` skipped it entirely and 1120 lines of CLI were
  invisible to every text search. Replaced with the escape.

The Epic write path (criteria/status/tradeoffs) is API-only -- documented as
such rather than papered over, and filed as task 180.

Full suite 2612 green; lint at baseline 794/77.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-16 00:14:58 +01:00

150 lines
5.5 KiB
Markdown

# Git providers and repositories
Two resources:
- **`gitprovider`** — a connection to a git forge (Gitea today). One per
instance/account. Owns the credential.
- **`repo`** (CodeRepo) — a repository attached to a project. Either imported
from a provider or added by URL.
Registering a provider is what makes discovery, import and **merge-on-accept**
possible. A repo added by bare URL can be worked in, but nothing can land its
pull requests, because there is no API to land them through.
## Registering a forge
The token lives in a Secret; the provider references it by name and key.
```sh
mcpctl create secret gitea-token --data TOKEN=<your-api-token>
mcpctl create gitprovider mysources \
--url https://mysources.co.uk \
--secret gitea-token --key TOKEN
```
Registration **probes the credential** — a wrong URL or an under-scoped token
fails here with a clear message, rather than later at discovery where it looks
like "the forge has no repos". Use `--skip-probe` to wire config before the
infrastructure exists.
The token needs repository scope. Discovery deliberately calls `/repos/search`
rather than `/user/repos`, because the latter requires `read:user`, which the
repo-scoped tokens people actually create for automation do not carry.
## Discover and import
```sh
mcpctl repos discover mysources # everything visible
mcpctl repos discover mysources -q mcp --limit 20 # filter
mcpctl repos discover mysources --project mcpctl # mark what's already imported
mcpctl repos import mysources michal/mcpctl michal/lab --project mcpctl
mcpctl repos import mysources michal/mcpctl --project mcpctl --ssh
```
Discovery stores nothing — it is a live read. Import creates CodeRepo rows.
Import is partial by design: it takes what it can and reports what it skipped
with reasons, rather than failing the batch because one repo is empty or
archived. An empty repo is flagged unavailable — there is nothing to clone.
Imported repos carry **denormalized provenance** (`sourceProvider`,
`sourceFullName`) and store no credential of their own — they inherit the
forge's. Deleting a provider leaves its repos in place with their provenance
intact, so where a repo came from outlives the connection.
To attach a repo without a provider:
```sh
mcpctl create repo mcpctl --project mcpctl \
--url https://mysources.co.uk/michal/mcpctl.git --branch main
```
```sh
mcpctl get repos
mcpctl get gitproviders
mcpctl describe repo mcpctl
```
## Merge on accept
When a task carrying a PR is accepted, mcpctl merges it through the repo's
provider — see [epics.md](epics.md#the-human-gate). Two refusals matter:
- **Stale head.** The accept records which commit was reviewed. If someone
pushed after that, the merge is refused rather than landing code nobody
approved. Re-review the current head and accept again.
- **Conflict.** Reported as-is; the task stays un-merged.
A failure always leaves the task un-merged and says why. Nothing reports success
the board cannot back up.
Repos with no `gitProviderId` cannot be merged through — the accept says so and
tells you to import from a provider or merge in the forge.
## Post-merge failure
CI can fail *after* a merge lands. Without a way to hear about it, the board
keeps claiming `merged` for work that broke the build. The webhook closes that
loop by moving the task to `reopened`.
Add the signing secret to the provider's Secret and point the provider at it:
```sh
mcpctl create secret gitea-token \
--data TOKEN=<api-token> --data WEBHOOK=<random-signing-secret>
mcpctl create gitprovider mysources \
--url https://mysources.co.uk \
--secret gitea-token --key TOKEN \
--webhook-secret-key WEBHOOK
```
Then in the forge, add a webhook on **commit status** events:
```
URL: https://<your-mcpd>/api/v1/webhooks/git/mysources
Content: application/json
Secret: <the same random signing secret>
```
> **It fails closed.** The endpoint is unauthenticated by nature — a forge calls
> it — so it accepts nothing unless the provider has a signing key configured
> *and* the signature verifies. **A provider without `--webhook-secret-key`
> refuses every delivery**, which means post-merge failures silently never
> reopen anything. If you want this loop, you must set the key.
Accepted signature headers are `X-Gitea-Signature` and `X-Hub-Signature-256`
(HMAC-SHA256 over the raw body, `sha256=` prefix optional), compared in constant
time.
Matching is precise, not approximate: a failing status carries a commit sha, and
the accept path recorded exactly which commit each merge produced. A failure on
a commit mcpctl did not merge is ignored — it is somebody else's business, and
guessing at "whatever merged most recently" would reopen innocent work.
Only `failure` and `error` states do anything. Success and pending are
acknowledged and ignored.
## RBAC
| Resource | Verbs |
|---|---|
| `gitproviders` | `view`, `create`, `edit`, `delete`, **`run`** |
| `repos` | `view`, `create`, `edit`, `delete` |
`run:gitproviders` covers discovery, import and probe. Those reach out to a
third-party system with a stored credential, so they are `run`, not `view` —
holding `view:gitproviders` should not let you drive the forge's API.
```sh
mcpctl create rbac forge-user --role run --resource gitproviders
```
## See also
- [epics.md](epics.md) — the work items whose PRs get merged here
- [agent-authority.md](agent-authority.md) — whose authority an agent acts under
- [secret-backends.md](secret-backends.md) — where the forge credential lives