Read the MARY governance snapshot with a down-scoped petrova-act installation token, not a PAT
rank: decision outranks: []
Section titled “rank: decision outranks: []”Read the MARY governance snapshot with a down-scoped petrova-act installation token, not a PAT
Section titled “Read the MARY governance snapshot with a down-scoped petrova-act installation token, not a PAT”Date: 2026-10-10
Status: open
Supersedes: none (proposes amending ruling (ii) of docs/decisions/2026-10-07-mary-governance-console-route.md; that doc stays current until this one is countersigned)
Superseded-by: none — current
MR-compliance: MR-4, MR-7
Context
Section titled “Context”Ruling (ii) of 2026-10-07-mary-governance-console-route.md (open question 1,
FLIGHT, “ii - go”) made the mary-wiki snapshot read use “a separate read-only
credential (fine-grained token, contents: read on mary-wiki only), not the
petrova-act App installation”. host/src/sources/mary-governance.ts implements
it as MARY_SNAPSHOT_READ_TOKEN. The same token is read by
.github/workflows/mary-diagnose.yml.
That token has three homes: the Actions secret, Railway petrova-host and the
Vercel apex. On 2026-10-10 petrova.mary.governance_snapshot answered
{"ok":false,"degraded":true,"upstream":"mary-governance","status":401} on
mcp.petrova.host. A new fine-grained PAT had been minted for Actions (the
Actions secret was updated 2026-10-09T21:07Z), and the Railway and Vercel copies
went stale. The read degrades gracefully by design, so nothing alerted. The
credential-liveness probe in #561 now makes that failure visible. It cannot
remove the cause. A PAT is a human-minted secret with an expiry, and it has to
be copied by hand to three places.
The facts as of 2026-10-10:
- petrova-act is already installed on the mary-wiki org. The output of
gh api /orgs/mary-wiki/installationsincludespetrova-act(app 3662455, installation 150940777,repository_selection: all). Its permissions are actions, contents, issues, organization_projects, pull_requests and workflows write, plus metadata read. The host’s installation auto-discovery (host/src/github-auth.ts) already resolvesmary-wikito it. - The host already holds the petrova-act private key. Its other GitHub reads use it on both surfaces. Ruling (ii) keeps that key out of this read path. It does not keep the key off the host.
- GitHub can mint an installation token narrower than the installation.
POST /app/installations/{id}/access_tokensacceptsrepositoriesandpermissions. GitHub enforces both at mint time, and the token expires after one hour. In Actions,actions/create-github-app-tokenexposes the same narrowing asrepositories:andpermission-<name>:inputs.
Decision (proposed)
Section titled “Decision (proposed)”Replace MARY_SNAPSHOT_READ_TOKEN with a petrova-act installation token that
is minted per read, restricted to mary-wiki/mary, and limited to
contents: read + actions: read:
- Host.
mary-governance.tsmints the token through a new, narrow helper ingithub-auth.ts, for examplemintReadOnlyRepoToken("mary-wiki", "mary", { contents: "read", actions: "read" }). The helper returns a bearer string and never an Octokit. The source keeps making its rawfetchcalls with that bearer. The helper refuses any permission other thanread. Unit tests assert the exactrepositories/permissionsbody sent to the mint endpoint. - Fallback, for one release. If the App cannot mint (no App env, or
mary-wiki is not resolvable), the source falls back to
MARY_SNAPSHOT_READ_TOKEN. The UNREAD reason says which credential was used and why it failed. The fallback, the env name and its inventory row are removed after one clean week of credential-liveness on both surfaces. - Actions.
mary-diagnose.ymlgains one step before Tick:actions/create-github-app-tokenwithowner: mary-wiki,repositories: mary,permission-contents: read,permission-actions: read,app-id: ${{ secrets.PETROVA_APP_ID }}andprivate-key: ${{ secrets.PETROVA_APP_PRIVATE_KEY }}. Its output becomesMARY_SNAPSHOT_READ_TOKENfor the Tick step, sohost/scripts/mary-diagnose.mjsis unchanged. The secret stays as a fallback for one week.
What ruling (ii) asked for still holds:
- The read path is read-only. GitHub enforces the scope of the minted token, not the code. A bug or injection in the read path holding this token cannot write to mary-wiki.
- The token covers mary-wiki only. It is restricted to the one repository.
- What changes is where it comes from. The credential is derived from the App key instead of being separately minted by a human. The write-capable root material was already on the host.
Open question for the countersign
Section titled “Open question for the countersign”- Is “derived and scoped by GitHub at mint” a “separate read-only
credential” within the meaning of ruling (ii)? (i) Yes: adopt the
decision above. (ii) No: keep the PAT. Then FLIGHT re-sets it on Railway and
Vercel now, records its expiry in
docs/runbooks/secrets-inventory.yaml, and relies on credential-liveness for the next rotation. (iii) No, but also retire the PAT: create a dedicated read-only GitHub App (contents: read,actions: read, installed onmary-wiki/maryonly) with its own key on both surfaces and in Actions. This is the most literal reading of R-3, at the cost of one more App and key to hold.
The drafting agent recommends (i). The 2026-10-10 failure came from the PAT’s lifecycle (human-minted, three homes, an expiry), not from its scope. Under (i) there is nothing to rotate.
Alternatives considered
Section titled “Alternatives considered”- Keep the PAT and rely on the liveness probe (#561). This catches the next stale copy within 6 hours, but it still needs a human to rotate the PAT in three places at every expiry. It is option (ii) above.
- Use the petrova-act installation token unscoped (
makeOctokitForOwner("mary-wiki")). Rejected. That token carries contents/actions/workflows write on every mary-wiki repo, which is exactly what ruling (ii) refused. - Reuse
devarno-hubble-ingest, which is already read-only on mary-wiki (actions/contents read, all repos). Rejected. It is hubble’s credential, in another trust domain, and petrova does not hold its key.
Consequences
Section titled “Consequences”For code: a mintReadOnlyRepoToken helper and tests in
host/src/github-auth.ts; mary-governance.ts prefers the helper and falls
back to the PAT. The header comment saying the module “deliberately does not
import github-auth.ts” is rewritten to say it imports only the read-only mint
helper. The token step is added to mary-diagnose.yml. No code changes until
this decision is countersigned.
For docs: docs/runbooks/secrets-inventory.yaml loses the
MARY_SNAPSHOT_READ_TOKEN row after the fallback week, and the
PETROVA_APP_* rows gain mary-diagnose.yml as a consumer.
For in-flight phases: none.
For invariants: none. Term 4 of the 2026-10-07 decision (the host never
reads mary-wiki docs/) is unaffected: the URLs fetched do not change.
References
Section titled “References”docs/decisions/2026-10-07-mary-governance-console-route.md(open question 1, ruling (ii))docs/decisions/2026-10-08-mary-governance-generated-statements-render.md(term 5 (b):mary-diagnose.ymlreads the snapshot with the same token)host/src/sources/mary-governance.ts(TOKEN_ENV,createMaryGovernanceSource)host/src/github-auth.ts(installation auto-discovery,resolveAppAuthForOwner).github/workflows/mary-diagnose.yml- petrova-codes/petrova#561 (credential liveness and secrets inventory)
Sign-off
Section titled “Sign-off”- Subagent: Claude Opus 5.5, 2026-10-10 (drafted; installation read with
gh api /orgs/mary-wiki/installations) - Human countersign — rules the open question (i, ii or iii) and, under (i), admits the down-scoped installation token as the mary-wiki snapshot read credential — flight@arno.host, 2026-10-27 (jzero.run T1)