Skip to content

Read the MARY governance snapshot with a down-scoped petrova-act installation token, not a PAT


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

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/installations includes petrova-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 resolves mary-wiki to 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_tokens accepts repositories and permissions. GitHub enforces both at mint time, and the token expires after one hour. In Actions, actions/create-github-app-token exposes the same narrowing as repositories: and permission-<name>: inputs.

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:

  1. Host. mary-governance.ts mints the token through a new, narrow helper in github-auth.ts, for example mintReadOnlyRepoToken("mary-wiki", "mary", { contents: "read", actions: "read" }). The helper returns a bearer string and never an Octokit. The source keeps making its raw fetch calls with that bearer. The helper refuses any permission other than read. Unit tests assert the exact repositories/permissions body sent to the mint endpoint.
  2. 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.
  3. Actions. mary-diagnose.yml gains one step before Tick: actions/create-github-app-token with owner: mary-wiki, repositories: mary, permission-contents: read, permission-actions: read, app-id: ${{ secrets.PETROVA_APP_ID }} and private-key: ${{ secrets.PETROVA_APP_PRIVATE_KEY }}. Its output becomes MARY_SNAPSHOT_READ_TOKEN for the Tick step, so host/scripts/mary-diagnose.mjs is 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.
  1. 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 on mary-wiki/mary only) 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.

  • 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.

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.

  • 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.yml reads 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)
  • 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)