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
This commit is contained in:
149
docs/gitproviders.md
Normal file
149
docs/gitproviders.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user