Summary
Wire the existing contentTrust sanitizer/projection substrate into the GitHub workflow write and ingestion boundaries, so untrusted external GitHub-authored content is defanged before it is persisted into resources/content/* and before KB / graph ingestion turns that content into retrievable context.
Release Classification
Post-release hardening / organism self-defense. This is not a v13 release blocker, but it closes a remaining slice of #10476 now that the pure helper and read-path projection slices have landed.
Context
Parent #10476 defined three anti-astroturfing protections:
- read-boundary URL defanging for GitHub conversations
- an explicit external URL traversal prohibition for agents
- P7 / Memory Core contextual sanitization so Neo does not persist untrusted marketing or watering-hole links into its own memory substrate
The first code slice is already done:
- #13026 closed the pure author-tier classifier and
astroturfSanitizer helper.
- #13046 closed
contentTrust projection for conversation read paths.
The remaining code gap is the write / ingestion boundary. The local GitHub syncers still serialize external GitHub-authored Markdown verbatim into repo content files, and the downstream KB / graph readers consume those files as-is.
Verify-Before-Assert Evidence
To assert the write-boundary gap, I checked the current source and let the code shape determine this ticket:
ai/services/github-workflow/sync/IssueSyncer.mjs #formatIssueMarkdown() writes issue.body and IssueComment event.body directly into the local issue Markdown.
ai/services/github-workflow/sync/PullRequestSyncer.mjs writes PR body, comments, and review bodies directly into local PR Markdown.
ai/services/github-workflow/sync/DiscussionSyncer.mjs writes discussion body, comments, and replies directly into local discussion Markdown.
ai/services/knowledge-base/source/TicketSource.mjs chunks the full local issue Markdown content for KB indexing.
ai/services/ingestion/IssueIngestor.mjs embeds open issue and discussion title/body content into the graph collection.
- Duplicate sweep before creation: live open issue search for
contentTrust ingestion sanitizer astroturf 10476, P7 contentTrust, and External URL Traversal / QUARANTINED_URL returned only parent #10476.
Problem
get_conversation callers now see a defanged view, but the periodic GitHub sync path can still write raw external URLs and product-seeding prose into resources/content/issues, resources/content/pulls, and resources/content/discussions.
Once written, that content is no longer just a remote GitHub payload. It becomes local source material for KB chunks and graph embeddings, which recreates the risk #10476 was meant to remove: Neo can preserve and later surface untrusted external links or astroturf payloads as internal context.
Acceptance Criteria
Test Plan
- Add focused unit coverage around the sync formatting/projection helper layer for issue, PR, and discussion authored-node shapes.
- Add one downstream ingestion regression that writes a sanitized external issue/discussion fixture and proves
TicketSource / IssueIngestor output contains [QUARANTINED_URL: domain] and not the raw URL.
- Run the narrow affected Playwright unit suites, not the default Playwright command.
Out of Scope
- Deleting, hiding, or editing GitHub comments remotely.
- Rewriting all historical
resources/content/* files in this slice.
- The AGENTS.md external traversal prohibition from #10476, unless a separate turn-memory pre-flight explicitly scopes that substrate edit.
Related
Summary
Wire the existing
contentTrustsanitizer/projection substrate into the GitHub workflow write and ingestion boundaries, so untrusted external GitHub-authored content is defanged before it is persisted intoresources/content/*and before KB / graph ingestion turns that content into retrievable context.Release Classification
Post-release hardening / organism self-defense. This is not a v13 release blocker, but it closes a remaining slice of #10476 now that the pure helper and read-path projection slices have landed.
Context
Parent #10476 defined three anti-astroturfing protections:
The first code slice is already done:
astroturfSanitizerhelper.contentTrustprojection for conversation read paths.The remaining code gap is the write / ingestion boundary. The local GitHub syncers still serialize external GitHub-authored Markdown verbatim into repo content files, and the downstream KB / graph readers consume those files as-is.
Verify-Before-Assert Evidence
To assert the write-boundary gap, I checked the current source and let the code shape determine this ticket:
ai/services/github-workflow/sync/IssueSyncer.mjs#formatIssueMarkdown()writesissue.bodyandIssueCommentevent.bodydirectly into the local issue Markdown.ai/services/github-workflow/sync/PullRequestSyncer.mjswrites PR body, comments, and review bodies directly into local PR Markdown.ai/services/github-workflow/sync/DiscussionSyncer.mjswrites discussion body, comments, and replies directly into local discussion Markdown.ai/services/knowledge-base/source/TicketSource.mjschunks the full local issue Markdown content for KB indexing.ai/services/ingestion/IssueIngestor.mjsembeds open issue and discussion title/body content into the graph collection.contentTrust ingestion sanitizer astroturf 10476,P7 contentTrust, andExternal URL Traversal / QUARANTINED_URLreturned only parent #10476.Problem
get_conversationcallers now see a defanged view, but the periodic GitHub sync path can still write raw external URLs and product-seeding prose intoresources/content/issues,resources/content/pulls, andresources/content/discussions.Once written, that content is no longer just a remote GitHub payload. It becomes local source material for KB chunks and graph embeddings, which recreates the risk #10476 was meant to remove: Neo can preserve and later surface untrusted external links or astroturf payloads as internal context.
Acceptance Criteria
projectConversationTrustbefore serializing root issue bodies and timeline comments to local Markdown. External or unclassified authors get URL defanging and configured product-name redaction; trusted authors remain byte-identical.contentTrust-projected content from legacy unsanitized content. A frontmatter summary is acceptable; do not require every historical file to be rewritten in one shot.TicketSourcechunks orIssueIngestorembedding documents for the covered untrusted-author cases.Test Plan
TicketSource/IssueIngestoroutput contains[QUARANTINED_URL: domain]and not the raw URL.Out of Scope
resources/content/*files in this slice.Related