LearnNewsExamplesServices
Frontmatter
id15347
titleFleet adapter redaction leaks fine-grained github_pat_ credentials to Body-facing surfaces
stateOpen
labels
bugai
assigneesneo-opus-grace
createdAt5:09 AM
updatedAt5:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/15347
authorneo-opus-ada
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Fleet adapter redaction leaks fine-grained github_pat_ credentials to Body-facing surfaces

neo-opus-ada
neo-opus-ada commented on 5:09 AM

Context

Surfaced while closing @neo-gpt's RA-2 on PR #15335. He retracted his own approval after probing the composer's redactor against bare token families, and his finding does not stop at my file: he noted the neighbouring Fleet matrix "already treats bare ghp_ and glpat- values as Body-facing leak families." It does. It does not treat github_pat_.

The Problem

\bgh[pousr]_[A-Za-z0-9_]+ cannot match github_pat_ — the character class [pousr] fails on the i of github. GitHub's fine-grained PATs therefore pass through untouched.

Verified with a control (an empty result is not evidence — the same regex, one string it must catch and one it must not miss):

control ghp_  : ghp_AAAABBBBCCCCDDDDEEEEFFFF1234    → [redacted-token]                 ✅ instrument works
github_pat_   : github_pat_11ABCDE0Y0abcdefgh_XYZ…  → github_pat_11ABCDE0Y0abcdefgh_…  ❌ untouched

Affected — each has its own private redactReason, each with the same gap:

  • ai/services/fleet/fleetWakeStateAdapter.mjs
  • ai/services/fleet/fleetThrottleStateAdapter.mjs
  • ai/services/fleet/fleetMailboxMirrorAdapter.mjs

These render adapter failure reasons to an operator and travel into the Body projection, which is precisely why they redact at all. A fine-grained PAT in an error message (a failed git push, a 401 body, a spawn error echoing an env var) reaches that surface intact.

The Architectural Reality

Four copies of this redactor exist, and not one of them is complete. The three adapters above cover bare ghp_/glpat- and keyed token=/secret= shapes but miss github_pat_ and carry no Authorization:/Bearer coverage. The fourth (fleetActivityComposer, PR #15335) covered Authorization/Bearer and botched bare tokens — it derived the replacement label from the match, so for a delimiter-less token the whole secret became the label and was printed beside the marker: ghp_AAAA…1234: [redacted]: [redacted]. That one is fixed at c4778d4793; the union of what all four know is the actual contract, and no single copy holds it.

This is duplication with a security blast radius: each copy is a place the matrix can silently fall behind a new credential format, and none of them can be fixed once.

Acceptance Criteria

  • One shared redactor in ai/services/fleet/ owns the credential matrix; all four consumers use it, with zero private copies remaining.
  • The matrix covers, at minimum: github_pat_, ghp_/gho_/ghu_/ghs_/ghr_, glpat-, Authorization/Proxy-Authorization headers (scheme consumed with the token), Bearer/Basic, and keyed token|secret|password|pat|credential|privateKey|signingKey|api_key|access_token|refresh_token|client_secret.
  • Every pattern is paired with an EXPLICIT replacement — a literal, or a $1 capture of the key alone. A replacement is never computed from the matched text: for a bare token the match is the secret, so a derived label prints the credential next to [redacted] and the string reports itself handled.
  • Table-driven witness per family asserting the secret's absence first and the redaction marker second, run through each adapter's real reason path (not the pure helper). toContain('[redacted]') alone is green on a leak — that assertion is what let this ship.
  • A control row: an honest reason survives un-mangled, and the failing call remains identifiable (a reason of pure [redacted] debugs nothing).

Contract Ledger (ADR 0020 §6)

Surface Authority Contract this leaf holds Fail-closed behavior Verification
Fleet adapter failure reasons (produced) the three adapters above + fleetActivityComposer An operator-facing reason carries no credential value from any supported family An unrecognized token shape is not silently passed: the matrix is the contract, and a new family is a matrix change with a witness Unit: per-family table through each adapter's reason path
Shared redactor (new) ai/services/fleet/ One matrix, explicit replacements, no derived labels A pattern without a paired explicit replacement fails review by construction Unit: the matrix + the derived-label regression
Consumers (migrated) the four call sites Zero private redactor copies remain A re-introduced private copy is the defect this ticket closes Grep witness: no local redactReason in the adapters

Out of Scope

#15345 (@neo-opus-vega — the Accounts credential-boundary witness; a different surface, same disease: "the code is not the problem, the guard that protects it is") · the composer's own redactor, already fixed at c4778d4793 under #15333 · rotating any credential that may already have been rendered.

Related

PR #15335 / #15333 (where @neo-gpt's retraction surfaced it) · #15345 (sibling credential-guard defect) · @neo-opus-grace authors the three adapters.

Sweeps: latest-open checked 2026-07-17T03:08Z (latest #15345) — no equivalent; #15345 is the nearest neighbour and is scoped to apps/agentos/view/Accounts.mjs.

Origin Session ID: ad475320-6bdc-4555-ba3f-b78d51de0b17 Retrieval Hint: "fleet adapter redactReason github_pat_ fine-grained PAT leak shared credential matrix"