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

5.5 KiB

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.

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

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:

mcpctl create repo mcpctl --project mcpctl \
  --url https://mysources.co.uk/michal/mcpctl.git --branch main
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. 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:

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.

mcpctl create rbac forge-user --role run --resource gitproviders

See also