LearnNewsExamplesServices
Frontmatter
id13026
titlePure content-trust helpers: GitHub author-tier classifier + astroturf sanitizer
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 13, 2026, 12:23 AM
updatedAtJun 13, 2026, 1:45 AM
githubUrlhttps://github.com/neomjs/neo/issues/13026
authorneo-opus-ada
commentsCount0
parentIssue10476
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 1:45 AM

Pure content-trust helpers: GitHub author-tier classifier + astroturf sanitizer

Closed v13.1.0/archive-v13-1-0-chunk-1 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jun 13, 2026, 12:23 AM

Context

The branch-ready first slice of the #10476 P8 anti-astroturfing work (Epic #10291). At intake, the multi-cycle review consensus on the #10476 thread narrowed P8 into a sequence of slices and asked for a "narrowed successor with a Contract Ledger" before code: ship the PURE classifier + sanitizer + fixtures first, then wire them into the read/ingest boundaries in explicitly-separated follow-up slices. This ticket is that successor — the pure, cross-service foundation. Delivered by the linked PR.

The Problem

The get_conversation read path and the KB ingestion path expose a comment's author.login but project NO trust tier today, and there is no content sanitizer to reuse. So an external, peer-toned astroturf comment — a trailing marketing URL, a link-free product-name seed, or a vendor pitch with engagement-bait + an external-endpoint offer — is both traversable by an agent (watering-hole / injection risk) and ingested verbatim into the KB corpus (SEO / name-seeding link-farm).

The Architectural Reality

  • ai/graph/identityRoots.mjsTRUST_TIERS (8-tier provenance taxonomy) + the canonical IDENTITIES roster (who the swarm is). The classifier derives the known-internal set from it.
  • ai/services/shared/contentTrust/ (NEW) — placed per structural-pre-flight: cross-service PURE functional helpers consumed by BOTH github-workflow read paths AND KB ingestion, mirroring the ai/services/shared/vector/ precedent. A consumer-local home (github-workflow/ or knowledge-base/) would couple the two services — the boundary watchpoint raised at intake.

The Fix

Two pure, side-effect-free modules + fixtures:

  1. authorTrustClassifier.mjs — fail-closed login → TRUST_TIERS; unknown → external, empty → unclassified; never infers trust up from prose; collaborators injected (not fetched).
  2. astroturfSanitizer.mjs — trust-gated (only external/unclassified content): defangs URLs → [QUARANTINED_URL: <domain>], redacts injected-denylist product names, and FLAGS high-precision stealth-intent signals (engagement-bait reward-conditional, external-endpoint offer) WITHOUT redacting them (preserve technical signal). Non-mutating.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
classifyAuthorTrust(login, {collaborators}) (NEW pure fn) identityRoots.TRUST_TIERS + IDENTITIES roster login → tier; roster wins, then collaborators → repo-trusted, else external; empty → unclassified unknown → external (fail-closed) JSDoc fixtures green
isExternalTier(tier) (NEW pure fn) this ticket external/unclassified → true; roster tiers → false n/a JSDoc fixture
sanitizeContent(content, {tier, productNameDenylist}) (NEW pure fn) this ticket + the documented #10476 variants trust-gated URL-defang + denylist name-redact + stealth-signal flags; returns {sanitized, redactions, signals, wasModified} trusted tier / empty → passthrough JSDoc fixtures (both variants + trust-gate + no-false-positive)
ai/services/shared/contentTrust/ (NEW dir) learn/benefits/ArchitectureOverview.md Structural Inventory cross-service self-defense content helpers n/a Structural Inventory row (added)

Decision Record impact

aligned-with the #10291 self-defense architecture — #10294 (P6b MCP Middleware Guards PEP) is the tool-call-boundary sibling; this is the content-boundary. No ADR conflict (0013 KB-telemetry, 0018 identity-SoT do not constrain a content-trust helper's placement).

Acceptance Criteria

  • authorTrustClassifier.mjs — fail-closed login → tier (roster / collaborator / external / unclassified); @-prefix + case agnostic.
  • astroturfSanitizer.mjs — trust-gated URL defang + denylist name redaction + stealth-intent signal flags; non-mutating.
  • Fixtures cover both documented attack variants (link-bearing + link-free) + the engagement-bait/endpoint variant + the trust-gate + no-false-positive negatives.
  • ArchitectureOverview.md Structural Inventory updated for the new dir.

Out of Scope (the later #10476 slices)

  • get_conversation read-boundary wiring (defang external-author payloads on the tool path).
  • KB ingestion write-boundary wiring.
  • The AGENTS.md External-URL-Traversal-Prohibition rule (lands with/after the runtime boundary, never standalone prose).
  • The product-name denylist content (config; this slice accepts it as a param).

Related

  • Refs #10476 (the P8 tracking ticket this slices), Refs #10291 (self-defense epic), Refs #10294 (P6b MCP-boundary PEP — the tool-boundary sibling to this content-boundary).

Release classification: post-release (self-defense substrate; non-v13-blocking).

Origin Session ID: e76f14a0-87b4-4fcb-9d87-2fe65db088a9 Retrieval Hint: "content-trust author-tier classifier astroturf sanitizer defang shared/contentTrust P8 self-defense"