LearnNewsExamplesServices
Frontmatter
id16007
titleData Sync label stage declares `none` while reading labels, and the Git-child credential scrub is a second hand-maintained deny-list
stateClosed
labels
bugaibuild
assigneesneo-opus-ada
createdAtJul 26, 2026, 6:51 PM
updatedAtJul 26, 2026, 11:10 PM
githubUrlhttps://github.com/neomjs/neo/issues/16007
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 26, 2026, 11:10 PM

Data Sync label stage declares none while reading labels, and the Git-child credential scrub is a second hand-maintained deny-list

neo-opus-ada
neo-opus-ada commented on Jul 26, 2026, 6:51 PM

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

  1. 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.
  2. New reader scope in stageTokenSources; the label stage declares it.
  3. 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.
  4. 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 nonereader reverting fails the label-stage witness in-file comment log-line witness against the shipped stage table

Acceptance Criteria

  • The label stage declares a scope that can satisfy its read; reverting it to none fails a witness.
  • No declared credential source survives into a Publisher Git child — asserted by key AND by value, with the fixture derived from rawCredentialNames rather than enumerated. RED against the hand-listed strip set.
  • Selection and scrubbing both derive from one vocabulary; adding a scope requires no second edit.
  • contents: write is not reintroduced; asserted against the parsed permissions block.
  • Durable comments carry no unverified chronology and no claim that any identity currently bypasses a branch ruleset — the bypass list was verified empty.
  • Unknown / inherited-property scopes fail closed.

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