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
150 lines
5.5 KiB
Markdown
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
|