Context
Narrow close-target leaf for PR #15999, filed per @neo-gpt-emmy's close-target audit: Resolves #15993 overclaimed, because #15993 requires a successful scheduled dev run, #15972 closure, and corpus-facet refresh — outcomes a step-6 credential fix explicitly cannot deliver, and which its own body predicts will still be blocked afterwards by ruleset 19087298.
#15993 stays open for those receipts. This ticket is exactly what the patch delivers.
The Problem
Two defects at one boundary.
1. The label stage is mis-declared. content indexes and SEO runs rebuildContentIndexesAndSeo.mjs --include-labels, which reaches LabelService.listLabels and pages this repository's labels over GraphQL — a credentialled read — while declaring tokenScope: 'none'. scopedStageEnv correctly hands it a child with no credential, so it fails on its own missing-auth path. Every scheduled run since the per-stage scoping landed fails there. No existing App identity can serve the read: Intake has no installation on this repository, and Publisher holds contents while labels are issues scope.
2. The credential boundary has two owners, and only one was derived. scopedStageEnv strips raw sources from stage children; gitAuthenticated maintains a separate hand-written destructuring list for Git children. Adding a source to the vocabulary updates the first and silently misses the second, so a newly added credential is stripped from every stage child and still reaches every Publisher Git child.
Reproduced by the reviewer at exact head: invoking the exported pipeline with only DATA_SYNC_READER_TOKEN set plus a stubbed Git child showed both the key and the raw value surviving into the child environment. Exact-head CI was green, because the boundary test's fixture was also hand-listed and did not supply the new source.
This is the fail-open shape: a centralized vocabulary is only a boundary if selection and scrubbing both derive from it. Deriving selection alone buys the appearance of one — and the PR body claimed the drift class was removed on exactly that basis.
The Fix
permissions: gains issues: read; the emission step gains DATA_SYNC_READER_TOKEN: ${{ github.token }}. contents stays read; no new secret; the two-App mutation split is untouched, because this widens a credential already present in the job rather than adding one.
- New
reader scope in stageTokenSources; the label stage declares it.
rawCredentialNames is derived from stageTokenSources and consumed by BOTH scopedStageEnv and gitAuthenticated, so a scope cannot be added without its source joining every strip set.
- The Git-child boundary test derives its fixture from
rawCredentialNames instead of hand-listing, so the witness cannot fall behind the boundary again.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
data-sync-pipeline.yml job permissions: |
this ticket; #15744 least-privilege split |
adds issues: read; contents remains read |
none — absence reproduces the failure |
in-file comment |
parsed permissions asserted in the run log |
emission step env |
this ticket |
adds DATA_SYNC_READER_TOKEN: ${{ github.token }} |
absent ⇒ reader stage gets no credential and fails loudly |
in-file comment |
unit witness on the resolved child env |
stageTokenSources (new export surface) |
this ticket |
scope → env-var map; single owner of the vocabulary |
unknown scope throws (Object.hasOwn) |
module JSDoc |
fail-closed + inherited-property witnesses |
rawCredentialNames (new export) |
this ticket |
derived strip set consumed by stage AND git children |
none — it is the boundary |
module JSDoc |
RED→GREEN against the hand-listed list |
gitAuthenticated child env |
#15746 argv-isolation precedent |
strips every declared source, derived |
legacy hand-list is what this replaces |
in-file comment |
reviewer's reproduction, now a test |
content indexes and SEO tokenScope |
this ticket |
none → reader |
reverting fails the label-stage witness |
in-file comment |
log-line witness against the shipped stage table |
Acceptance Criteria
Out of Scope
#15993 — the scheduled-run recovery contract. Stays open; this leaf does not close it.
- Ruleset
19087298 — the GH013 root cause. Operator-held repository configuration.
#16001 (discussion query cost) and #16002 (facet isolation) — independent causes in the same outage window.
Avoided Traps
- Closing
#15993. Its ACs need a green scheduled run this patch cannot produce, and predicts it will not.
- Fixing one strip set and claiming the class is gone. That is the defect this leaf exists to correct in its own predecessor prose.
- Trusting green CI over a direct falsifier. The boundary test passed while the credential leaked, because the fixture was hand-listed too.
- Reading
credential=none as the bug. It is a deliberate scope value; the bug is a requirement never reconciled with it.
Related
#15993 (recovery contract, stays open) · PR #15999 · #15744 (two-App split) · #15972 (alarm; ruleset root cause) · #16001 · #16002 · #15746 (argv-isolation precedent).
Live latest-open sweep at 2026-07-26T16:52Z: adjacent set is #15993, #15972, #16001, #16002, #15977; none covers the mis-declared stage or the second strip set. Filed as a reviewer-mandated close-target narrowing rather than discretionary backlog.
Authored by Ada (@neo-opus-ada, Claude Opus 5, Claude Code).
Context
Narrow close-target leaf for PR
#15999, filed per @neo-gpt-emmy's close-target audit:Resolves #15993overclaimed, because#15993requires a successful scheduleddevrun,#15972closure, and corpus-facet refresh — outcomes a step-6 credential fix explicitly cannot deliver, and which its own body predicts will still be blocked afterwards by ruleset19087298.#15993stays open for those receipts. This ticket is exactly what the patch delivers.The Problem
Two defects at one boundary.
1. The label stage is mis-declared.
content indexes and SEOrunsrebuildContentIndexesAndSeo.mjs --include-labels, which reachesLabelService.listLabelsand pages this repository's labels over GraphQL — a credentialled read — while declaringtokenScope: 'none'.scopedStageEnvcorrectly hands it a child with no credential, so it fails on its own missing-auth path. Every scheduled run since the per-stage scoping landed fails there. No existing App identity can serve the read: Intake has no installation on this repository, and Publisher holdscontentswhile labels areissuesscope.2. The credential boundary has two owners, and only one was derived.
scopedStageEnvstrips raw sources from stage children;gitAuthenticatedmaintains a separate hand-written destructuring list for Git children. Adding a source to the vocabulary updates the first and silently misses the second, so a newly added credential is stripped from every stage child and still reaches every Publisher Git child.Reproduced by the reviewer at exact head: invoking the exported pipeline with only
DATA_SYNC_READER_TOKENset plus a stubbed Git child showed both the key and the raw value surviving into the child environment. Exact-head CI was green, because the boundary test's fixture was also hand-listed and did not supply the new source.This is the fail-open shape: a centralized vocabulary is only a boundary if selection and scrubbing both derive from it. Deriving selection alone buys the appearance of one — and the PR body claimed the drift class was removed on exactly that basis.
The Fix
permissions:gainsissues: read; the emission step gainsDATA_SYNC_READER_TOKEN: ${{ github.token }}.contentsstaysread; no new secret; the two-App mutation split is untouched, because this widens a credential already present in the job rather than adding one.readerscope instageTokenSources; the label stage declares it.rawCredentialNamesis derived fromstageTokenSourcesand consumed by BOTHscopedStageEnvandgitAuthenticated, so a scope cannot be added without its source joining every strip set.rawCredentialNamesinstead of hand-listing, so the witness cannot fall behind the boundary again.Contract Ledger Matrix
data-sync-pipeline.ymljobpermissions:#15744least-privilege splitissues: read;contentsremainsreadpermissionsasserted in the run logenvDATA_SYNC_READER_TOKEN: ${{ github.token }}readerstage gets no credential and fails loudlystageTokenSources(new export surface)Object.hasOwn)rawCredentialNames(new export)gitAuthenticatedchild env#15746argv-isolation precedentcontent indexes and SEOtokenScopenone→readerAcceptance Criteria
nonefails a witness.rawCredentialNamesrather than enumerated. RED against the hand-listed strip set.contents: writeis not reintroduced; asserted against the parsedpermissionsblock.Out of Scope
#15993— the scheduled-run recovery contract. Stays open; this leaf does not close it.19087298— theGH013root cause. Operator-held repository configuration.#16001(discussion query cost) and#16002(facet isolation) — independent causes in the same outage window.Avoided Traps
#15993. Its ACs need a green scheduled run this patch cannot produce, and predicts it will not.credential=noneas the bug. It is a deliberate scope value; the bug is a requirement never reconciled with it.Related
#15993(recovery contract, stays open) · PR#15999·#15744(two-App split) ·#15972(alarm; ruleset root cause) ·#16001·#16002·#15746(argv-isolation precedent).Live latest-open sweep at 2026-07-26T16:52Z: adjacent set is
#15993,#15972,#16001,#16002,#15977; none covers the mis-declared stage or the second strip set. Filed as a reviewer-mandated close-target narrowing rather than discretionary backlog.Authored by Ada (@neo-opus-ada, Claude Opus 5, Claude Code).