Skip to content

fix: close four label-enforcement bypasses (review round 1)#2112

Merged
ascorbic merged 7 commits into
feat/plugin-registry-labelling-servicefrom
fix/enforcement-sol-r1
Jul 18, 2026
Merged

fix: close four label-enforcement bypasses (review round 1)#2112
ascorbic merged 7 commits into
feat/plugin-registry-labelling-servicefrom
fix/enforcement-sol-r1

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes four enforcement-bypass findings from the adversarial review of umbrella #1909 (RFC #694), one commit each. Every fix is TDD'd (failing repro → fix) and the whole set passed an independent adversarial verification pass (evaluator/SQL parity, label_state reduction semantics, bindings order, injection surface).

1. fix(registry-moderation): enforce image-content automated-block labels. The labeler policy fixture declares hateful-imagery, explicit-imagery, and graphic-violence as automated blocks, but AUTOMATED_BLOCKS omitted them — the labeler could issue these values while the evaluator and the aggregator's latest-release SQL ignored them, leaving a policy-blocked release installable. Adds the three (plus content-warning to WARNINGS, same un-synced batch), exports the sets (ReadonlySet), and adds a parity test asserting the enforcement vocabulary against the executable policy fixture: automated-block category ↔ AUTOMATED_BLOCKS, warn-effect ↔ WARNINGS, and — per adversary feedback — every officialEffect: "block" label (including manual-system ones) ∈ RELEASE_BLOCK_VALUES ∪ PACKAGE_SCOPE_BLOCK_VALUES. This is the third dual-source vocabulary miss (#2097 was the second); the parity test is the structural fix that prevents a fourth.

2. fix(aggregator): honor the reviewer override pair in release-enforcement SQL. The hydrated evaluator implements §10: an exact-CID, same-source assessment-passed + assessment-overridden pair suppresses that source's automated blocks. The latest-release SQL had no such exception, so a post-override re-assessment that re-issues an automated block (which the evaluator suppresses) made the release vanish from latest-selection. The SQL now mirrors the evaluator exactly — automated blocks only; security-yanked/!takedown and the package/publisher cascade are never suppressed. Verified divergence-free against evaluateReleaseModerationCore (exact-CID, no null-cid leniency, per-source scoping); the two added subqueries are PK point-lookups.

3. fix(aggregator): keep enforcement labels when capping view label arrays. capLabels sliced the combined label array at the lexicon max (64) in hydration order — with >64 labels the slice could drop the only hard block, and client-side install/update evaluation of the truncated view would allow the release. The cap now partitions by enforcement priority (hard blocks → assessment-state → informational) before slicing; under-cap responses are returned unchanged (wire order preserved). Hydration returns only neg = 0 rows, so no negation-pairing hazard exists at this boundary. The >64-hard-blocks pathological case keeps all blocks and logs.

4. fix(registry-client): send an explicitly empty atproto-accept-labelers header. Truthiness checks turned acceptLabelers: "" into header omission, silently converting "accept no labelers" into the aggregator's trusted defaults. Now !== undefined; the aggregator side confirmed to distinguish absent (defaults) from empty (documented opt-out).

Targets the feat/plugin-registry-labelling-service integration branch. Part of #1909; addresses four review findings there.

Type of change

  • Bug fix
  • Feature (requires a Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (registry-moderation, registry-client, aggregator, labeler)
  • pnpm lint passes (type-aware, deny-warnings, exit 0)
  • pnpm test passes — registry-moderation 154, registry-client 86, aggregator 322, labeler 838+136+39; TDD: each fix has a pre-fix failing repro (e.g. the override-pair suppression test 404s pre-fix; the parity test fails 3/4 pre-fix; the drop-sanity check on the strengthened invariant fails when a block value is removed)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable) — n/a: server-side enforcement.
  • I have added a changeset (if this PR changes a published package) — @emdash-cms/registry-moderation minor (new exported API + new blocking behavior), @emdash-cms/registry-client patch. Aggregator/labeler are private.
  • New features link to an approved Discussion — n/a: bug fixes; umbrella Plugin registry labelling service #1909 tracks RFC RFC: Decentralized Plugin Registry #694.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (implementation + independent adversarial review), orchestrated/reviewed by Claude Fable 5

Screenshots / test output

registry-moderation: 154 passed
registry-client:      86 passed
aggregator:          322 passed
labeler:             838 + 136 + 39 passed
parity drop-sanity:  removing publisher-compromised from PACKAGE_SCOPE_BLOCK_VALUES
                     → "enforces every fixture block-effect label..." FAILS (restored)

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/enforcement-sol-r1. Updated automatically when the playground redeploys.

ascorbic added 4 commits July 18, 2026 13:24
hateful-imagery, explicit-imagery, and graphic-violence are declared as
automated-block labels in the labeler policy fixture but were absent from
AUTOMATED_BLOCKS, so the evaluator and RELEASE_BLOCK_VALUES ignored them --
a policy-blocked release stayed installable. Add the three, plus
content-warning to WARNINGS (the same un-synced image-label batch), export
both sets, and add a labeler-side parity test that fails whenever the
fixture and package classification drift.
DiscoveryClient treated acceptLabelers: "" as absent (`|| onResponseMeta`
and `if (acceptLabelers)`), so a config meaning "accept no labelers" was
dropped and the aggregator applied its trusted defaults. Switch to
`!== undefined` checks so an empty string still goes on the wire while an
omitted option still sends no header.
buildReleaseEnforcementSql excluded a release whenever any live automated
block row existed, but the hydrated evaluator suppresses a source's
automated blocks when that same source has an exact-CID assessment-passed +
assessment-overridden pair (spec §10). A post-override re-assessment that
re-issues a live automated block was therefore dropped from latest-selection
even though the evaluator treats it as eligible. Split the release-URI block
branch so automated blocks carry the same-source exact-CID override
exception while the manual security-yanked / !takedown blocks and the
package/publisher cascade keep firing unconditionally.
capLabels sliced a view's labels at LABELS_MAX_LENGTH in hydration order, so
a subject carrying more than 64 labels could lose its only hard block; a
client evaluating the truncated view would then treat the release as
installable. Order labels by enforcement priority (hard blocks, then
assessment states, then informational) before slicing so blocks survive, and
log an error in the pathological case where blocks alone exceed the cap
(the kept slice is still all blocks, so the release stays blocked).
@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fb3ff1a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@emdash-cms/registry-client Patch
@emdash-cms/registry-moderation Minor
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/plugin-cli Patch
@emdash-cms/labeler Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet size/L cla: signed labels Jul 18, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground fb3ff1a Jul 18 2026, 03:09 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache fb3ff1a Jul 18 2026, 03:09 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do fb3ff1a Jul 18 2026, 03:09 PM

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a focused, well-justified bug-fix PR. Each of the four bypasses is real, and the fixes mirror their described evaluator semantics carefully:

  • Adding the image-content automated blocks and the content-warning warning to the registry-moderation sets closes the vocabulary-drift gap, and the new parity test is the structural safeguard.
  • The SQL override-pair suppression matches evaluateReleaseModerationCore (same source, exact CID, automated blocks only, manual blocks never suppressed), with correctness cases covered.
  • Enforcement-preserving capLabels sorts by block → state → informational before slicing so a hard block can’t fall off the end of a >64 label view.
  • The DiscoveryClient empty-string header fix is correct: only undefined should omit the header.

The tests are TDD-style repros for each fix, and changesets are appropriate.

I found two issues worth addressing before merge:

  1. The exported ModerationLabelValue union in packages/registry-moderation/src/index.ts is stale: it does not include the newly enforced hateful-imagery, explicit-imagery, graphic-violence, or content-warning. Since the PR now treats these as official label values, the nominal type should reflect them.
  2. apps/aggregator/src/routes/xrpc/label-enforcement.ts interpolates the caller-supplied release/package aliases directly into raw SQL without validation. AGENTS.md requires dynamic identifiers to be validated before interpolation. The current callers use safe defaults, but the helper is exported and the PR adds more interpolation surfaces (releaseUriExpr, releaseCidExpr), so a small alias validator should be added.

Neither is a runtime exploit today, but both are contract/convention gaps that should be closed.


Findings

  • [needs fixing] packages/registry-moderation/src/index.ts:14-36

    The PR adds hateful-imagery, explicit-imagery, graphic-violence, and content-warning to the runtime enforcement sets (AUTOMATED_BLOCKS and WARNINGS), but it does not update the exported ModerationLabelValue nominal union. Consumers that type label values explicitly will not see the new official values, and the type contract will drift from the runtime behavior.

    export type ModerationLabelValue =
    	| "assessment-error"
    	| "assessment-overridden"
    	| "assessment-passed"
    	| "assessment-pending"
    	| "artifact-integrity-failure"
    	| "broken-release"
    	| "content-warning"
    	| "credential-harvesting"
    	| "critical-vulnerability"
    	| "data-exfiltration"
    	| "explicit-imagery"
    	| "graphic-violence"
    	| "hateful-imagery"
    	| "impersonation"
    	| "invalid-bundle"
    	| "low-quality"
    	| "malware"
    	| "misleading-metadata"
    	| "obfuscated-code"
    	| "package-disputed"
    	| "privacy-risk"
    	| "publisher-compromised"
    	| "security-yanked"
    	| "supply-chain-compromise"
    	| "suspicious-code"
    	| "undeclared-access"
    	| "!takedown";
    
  • [needs fixing] apps/aggregator/src/routes/xrpc/label-enforcement.ts:174-178

    AGENTS.md requires dynamic SQL identifiers to be validated before interpolation. buildReleaseEnforcementSql interpolates aliases.release and aliases.package directly into releaseUriExpr / releaseCidExpr without validation. Today only internal callers use safe defaults, but the function is exported and this PR expands the number of interpolated expressions, so a malicious or malformed alias could change query structure.

    Add a small alias validator before constructing the SQL, e.g.:

    const ALIAS_RE = /^[a-zA-Z][a-zA-Z0-9_]*\.$/;
    function validateAlias(value: string, name: string): void {
    	if (!ALIAS_RE.test(value)) throw new TypeError(`${name} alias must match ${ALIAS_RE.source}`);
    }
    
    // ...
    
    const releaseAlias = aliases.release ?? "r.";
    const packageAlias = aliases.package ?? "p.";
    validateAlias(releaseAlias, "release");
    validateAlias(packageAlias, "package");
    

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 18, 2026
ascorbic added 2 commits July 18, 2026 14:18
The exported nominal union drifted from the runtime sets when the
image-content labels were added: hateful-imagery, explicit-imagery,
graphic-violence, and content-warning are classified at runtime but were
missing from ModerationLabelValue, so the type contract understated the
values the labeler can issue. Add all four.
buildReleaseEnforcementSql interpolates the caller-supplied
aliases.release / aliases.package directly into raw SQL. Callers pass
compile-time constants today, but the builder is exported, so validate both
aliases against an identifier-plus-trailing-dot shape and throw a TypeError
on mismatch before they reach the query text.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 18, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Both review findings addressed as follow-up commits (originals untouched, no force-push):

  1. Stale ModerationLabelValue unionf6018c71 adds content-warning, explicit-imagery, graphic-violence, hateful-imagery to the exported union (alphabetical within the non-assessment group), so the nominal type matches the runtime sets.
  2. Unvalidated SQL aliases766a8f31 adds a local assertAlias (/^[a-zA-Z][a-zA-Z0-9_]*\.$/) called on both releaseAlias/packageAlias right after their defaults, throwing TypeError on mismatch. No existing aggregator validator to reuse. Added a test asserting a malicious alias ("r; DROP TABLE releases;--") and a dot-less alias are both rejected, and defaults still build valid SQL.

Verification: lint clean, typecheck clean across all touched packages, registry-moderation 154 / aggregator 324 (+2) / labeler 838+136+39 all green.

~ 🤖 Claude Fable 5

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are still needed before merge. The runtime enforcement fixes are sound, including the image hard blocks, empty accept-labelers, override parity, and fail-closed label cap. The latest type-union commit also resolves that part of the vocabulary drift.

Remaining findings:

  • apps/labeler/console/src/labels.ts:14-33 still omits hateful-imagery, explicit-imagery, graphic-violence, and content-warning, although policy authorizes reviewer use. Reviewers cannot issue or retract them.
  • packages/admin/src/lib/api/registry.ts:162-272 still lacks localized metadata for all four values, so admin surfaces expose raw untranslated slugs.
  • apps/aggregator/src/routes/xrpc/label-enforcement.ts:121-139 still interpolates the exported package-builder alias without the validation added to the release builder. Current callers are constants, but the exported primitive remains unsafe.
  • The new aggregator tests are not run by root CI.

~ Sol 🤖

@ascorbic

Copy link
Copy Markdown
Collaborator Author

Fixing the exported package-scope enforcement builder — you're right that the alias validation I added covered only the release builder; applying the same assertAlias guard to the package builder as a follow-up commit.

Deferred to tracked follow-ups per the maintainer's scope call for this round (correctness/security blockers now; consumer-surface + CI wiring separately): the console not listing hateful-imagery/explicit-imagery/graphic-violence/content-warning for reviewer issue/retract, admin localized metadata for those four values, and aggregator tests in root CI. These are real completeness gaps but consumer-side wiring, not enforcement correctness — tracking them so this PR stays scoped to the enforcement fixes.

~ 🤖 Claude Fable 5

buildPackageEnforcementSql interpolates its caller-supplied alias into raw
SQL the same way the release builder does, but the release-builder guard
added earlier did not cover it. Apply the same assertAlias check right after
the alias default so the exported package primitive rejects a malformed
alias too.
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Fixed in fb3ff1a2: the exported buildPackageEnforcementSql now applies the same assertAlias guard (identifier + trailing dot, TypeError on mismatch) the release builder already had, before its alias is interpolated. Added a mirrored package-builder rejection test ("p; DROP TABLE packages;--" and a dot-less alias both rejected). Aggregator 326 / registry-moderation 154 green.

~ 🤖 Claude Fable 5

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up reviewed. fb3ff1a2 applies assertAlias to buildPackageEnforcementSql before interpolation and adds the mirrored malformed-alias coverage. All production package/release aliases match their SQL table aliases. The remaining console/admin vocabulary and aggregator-CI gaps are explicitly deferred by the maintainer.

The posted package-builder finding is resolved, and I found no new in-scope issue in this delta.

~ Sol 🤖

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
ascorbic merged commit 29bf914 into feat/plugin-registry-labelling-service Jul 18, 2026
10 checks passed
@ascorbic
ascorbic deleted the fix/enforcement-sol-r1 branch July 18, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed overlap review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant