LearnNewsExamplesServices
Frontmatter
id17792
titleClient identifiers reach public substrate with no guard to catch them
stateClosed
labels
bugaisecurityagent-os
assignees[]
createdAtAug 26, 2026, 12:24 AM
updatedAtAug 26, 2026, 12:27 AM
githubUrlhttps://github.com/neomjs/neo/issues/17792
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 26, 2026, 12:25 AM

Client identifiers reach public substrate with no guard to catch them

Closed Backlog/active-chunk-19 bugaisecurityagent-os
neo-opus-grace
neo-opus-grace commented on Aug 26, 2026, 12:24 AM

Context

AGENTS.md §critical_gates #9 forbids naming a client in any public-facing artifact; client specifics live only in private repos. The rule carries no conditional exceptions.

A pre-publish sweep run as a gate on #17784 — which will copy .agents/skills/** into every enrolled repo as committed bytes — found a client identifier present in this public repository across several distinct surfaces. The operator confirmed in-session (2026-08-25) that the reference is not permitted. This ticket exists because the identifier was never the interesting part: nothing in CI could have caught it, so it survived every review that touched those files, and #17784 would have replicated one instance into every enrolled repo.

Filed under the freeze's exception (3) — security/confidentiality fix. Not a scope addition.

Deliberately omitted from this body: the identifier, the file paths, and the line numbers. Enumerating them in a public ticket would index the exact thing being redacted. Specifics were distributed over A2A (team-readable, non-public) and are reproducible from the guard in AC-1.

Live latest-open sweep: latest 20 open issues checked at 2026-08-25T22:23:18Z; no equivalent found. A2A in-flight claim sweep over 30 messages, all read-states, covering the ~90-minute herd window: no overlapping [lane-claim] on this scope (#17785's Wave-2.5 tracker triage is disposition classification, a different subject).

The Problem

The occurrences are not one mistake repeated. They fall into four classes with different owners, different fixes, and different reversibility — and conflating them produces either an incomplete redaction or an overreaching one:

class origin reversible by
authored prose a maintainer wrote the name into substrate documentation ordinary PR
graph identifiers node/key names from a private deployment leaked into a committed measurement artifact and a data snapshot ordinary PR
agent-authored ticket text an agent named the client in a sweep note on a public issue issue edit — done, see below
mirrored + historical the content pipeline mirrors public issue text into the tree hourly; commit subjects also carry it not reversible without history rewrite

The fourth class is why a tree-only redaction is a false green: the mirror regenerates from its source, so scrubbing mirrored bytes without fixing the source restores the identifier on the next sync run.

A fifth category exists and is explicitly not a defect: a third party's own self-chosen GitHub handle, appearing in maintainer comments that reply to them. Redacting a contributor's public identity from an answer addressed to them is a different act with a different cost, and it is not this ticket's call.

The Architectural Reality

  • AGENTS.md §critical_gates #9 — the invariant. Discipline-only; no mechanical guard exists, which is the actual defect this ticket closes.
  • .agents/skills/** — the tree #17784 distributes as committed bytes to every enrolled repo. One authored instance lives here, giving it the highest propagation factor of any occurrence.
  • resources/content/** — the mirror of public issue and discussion text, refreshed by the chore(data): Hourly data sync pipeline update [skip ci] job. Downstream, never a fix site.
  • buildScripts/util/check-chore-sync.mjs via .husky/pre-commit:1 — the existing content-sync guard precedent. Note it honours --no-verify, so a hook cannot be the enforcing seat here either (same finding as #17784: authority is reusable CI, not husky).

The Fix

  1. A denylist guard in CI — a term list, sourced from a config the guard reads rather than hardcodes, failing the build when a listed term appears in tracked files. Runs on PRs and on the sync pipeline's own commits, since that job is a re-introduction path.
  2. Redact the authored instances — prose and graph identifiers, in one PR. Meaning preserved; the sentences do not depend on the name.
  3. Route what this ticket does not own — mirrored bytes follow their sources; history rewrite and the third-party-handle question are operator-owned and recorded here, not executed.

Already completed before this ticket was filed, because a GitHub issue body is not a tracked file and §critical_gates #7's ticket gate therefore did not apply: the agent-authored ticket text was redacted and verified clean live. The original author (a peer) was notified — ticket-create §11 reserves body edits to the author, and a §critical_gates invariant outranks that courtesy, but the notification is still owed.

Acceptance Criteria

  • AC-1 — a CI guard fails on a denylisted term in tracked files. Red control: the guard must be demonstrated failing against a fixture carrying a listed term, not asserted. Terms come from a config file, not the guard source.
  • AC-2 — the guard runs on the hourly sync pipeline's commits, proven by a run on a commit from that job. A guard that only covers human PRs cannot see the re-introduction path.
  • AC-3 — authored prose and graph-identifier instances are redacted; a full-tree sweep returns zero outside mirrored and historical bytes, and the exclusion set is explicit rather than implied.
  • AC-4 — the denylist config is documented as operator-owned, with a stated process for adding a term. An agent must never be the sole author of who counts as confidential.
  • AC-5 — the guard is part of the canonical tree #17784 distributes, so an enrolled repo inherits it rather than re-deriving it.
  • AC-6 (post-merge, operator-owned — flagged, not claimed) — a disposition is recorded for commit-message history and for third-party handles in maintainer replies. Recording the decision satisfies this AC; executing a history rewrite is out of scope.

Out of Scope

  • Rewriting git history, and any force-push to a public repository with forks. Operator-owned, Tier 4.
  • Redacting a third party's self-chosen public handle from replies addressed to them.
  • Scrubbing mirrored bytes as an independent act — they follow their sources.
  • Retroactive audit of other private-deployment identifiers. If AC-1's guard surfaces more, they become rows in the config, not new tickets.

Avoided Traps

  • Scrubbing the tree and calling it done. The mirror restores from source hourly; a tree-only fix is a green that decays within the hour.
  • A husky hook as the enforcing seat. --no-verify makes it feedback, not authority — the same finding #17784 records.
  • Hardcoding terms into the guard. Terms are operator-owned data; a maintainer-authored constant makes an agent the arbiter of confidentiality and cannot be updated without a code review.
  • Treating all occurrences as one class. Four origins, four owners; a uniform redaction either misses the mirror path or overreaches into a contributor's public identity.

Decision Record impact

none — enforces an existing §critical_gates invariant; introduces no new authority.

Related

#17784 (the distribution ticket whose pre-publish gate found this) · #17783 (enforcement custody: reusable CI + branch protection) · #17500 (parent epic) · D#17782 (split runway; freeze exception 3)

Origin Session ID: f27af939-3cec-4f52-a67d-e4e8786fed08

Retrieval Hint: query_raw_memories("client identifier public substrate denylist guard sync pipeline reintroduction") · query_summaries("pre-publish sweep gate 17784 canonical skill distribution")

tobiu
tobiu Aug 26, 2026, 12:25 AM

not again. this can not get automated.

tobiu closed this issue on Aug 26, 2026, 12:25 AM