Mint one-repo runner tokens for Easy Mode
Part of epic "Build sponsored Easy Mode app hosting" (PRD task 13412, architecture task 13413). Architecture child **C8 Runner tokens** (from operator answer Q5). Reads child C4's `projects.easy_mode` column.
## Original requirement (verbatim)
> Deploy: auto on push to main, from a repo Jaah creates and owns (temporarily) in Jaah's existing GitHub org, from a template carrying the deploy workflow (per-app OIDC role scoped to that app's resources only). Repo hand-over to the client later (transfer + trust removal).
> Machines: Easy Mode orgs run their AI work on our shared node pool, placed automatically (Jaah picks a shared operator node with room); the operator picks no node.
Operator decision Q5 (architecture): runner sessions on the client's shared node get push access to their repo through a small GitHub App with one-repo tokens — the admin credential must never reach a client node.
## Approach (settled by the Architect)
- New GitHub App "Jaah Easy Mode runners" (permissions Contents, Pull requests, Workflows, Metadata) installed on `jaah-ai`. This child ships a manifest link in the knob's Settings help so the operator can create it; then, in this run, ask the operator ONCE (AskUserQuestion, header `Human act`) to create/install the App and save its credential, naming exactly what is needed.
- New credential type `github_app_key` (`app_id`, `installation_id`, `private_key`) in `apps/api/app/credentials/registry.py` (code registry; no migration). Operator Settings knob `easy_mode_runner_app_credential_id` (catalog pattern `operator_settings/catalog_billing.py:92-95`), shown in Settings.
- Seam: the node pulls its git login on demand (`apps/jaahd/cred_broker.py:495` → `GET /api/agent/tasks/{id}/git-token`, `routes/agent/task_secrets.py:199-246`, and the clone path `GET /git-token?project=`, `:387-390`), both resolving through `git_access.resolve_for_task` (`git_access.py:256`) and `token_for` (`:952`). In `task_secrets._git_token_or_refusal` (`:150-178`), before the person resolution, for a project with `easy_mode = true` whose repo owner equals the `easy_mode_github_org` knob (default `jaah-ai`; if C6 has not landed yet, read it with the same default): mint `POST /app/installations/{installation_id}/access_tokens {repositories:[<repo>], permissions:{contents:write, pull_requests:write, workflows:write, metadata:read}}` (JWT RS256 signed with the `github_app_key` private key), cache until 5 min before expiry, serve it instead of a person's token. Everywhere else unchanged. `jaah-forge` needs no change. Blank knob → a readable refusal naming the Settings knob.
## Acceptance criteria (verbatim, BA)
- **AC-14 Sponsored AI runs** (this child: the push prerequisite). "A coder task in the Easy Mode project runs on the org's shared node on the Jaah account, and the wallet balance drops by its charge, drawing on the sponsored credit …" — the session must be able to push its branch and open its PR with a token limited to its one repo.
- **AC-6 Placement** (task runs part). "… coder tasks in it dispatch there." — a task on the shared node gets a working git login without any person's token.
End-to-end runs (a coder task in a real Easy Mode project pushing from the shared node) are owned by the wizard child C7.
Credentials & reach:
- AC-14 runner tokens (C8) — new GitHub App "Jaah Easy Mode runners" on `jaah-ai` + its `github_app_key` credential — deferred — does not exist yet by design; the operator chose to create it (Q5); C8 supplies the manifest link and asks in its own run.
- AC-9, AC-11, AC-19, C3 template creation, C6 GitHub writes — `me/users/roman@gmind.tech/github_admin` (classic token, `repo, workflow, admin:org`; admin of `jaah-ai`) — verified — `GET /user/memberships/orgs/jaah-ai` = admin/active. (Here only to create two throwaway test repos for the scope check, never placed on a node.)
Out of scope: a separate GitHub org; a separate app apex domain; custom domains, registrar purchase and paid hosting after sponsorship (draft task 10233 stays as that later work); the always-on shared-box hosting tier. Do NOT change token resolution for non-Easy-Mode projects or repos outside `easy_mode_github_org`; do NOT place the admin credential (or the App private key) on any node; do NOT add a schema migration.
Done means:
- api tests: an `easy_mode` project in `jaah-ai` gets an installation token request restricted to exactly its one repository with the four pinned permissions; cached until 5 min before expiry; every other project resolves exactly as before (existing tests unchanged); blank knob → readable refusal.
- Settings shows the knob with help text and the manifest link (screenshot on dev).
- Live scope check (after the operator creates the App): a minted token for test repo A can push a branch to A and is refused on test repo B (cite both git outputs, token never printed). If the App is still not created after the one question, deliver everything else and settle with this oracle named as blocked.
- `./dc check quick` green.
Activity
- Draft
- Ready