Marketplace integrity
Status: Normative. Covers four interlocking subsystems that keep the open registry safe for users and fair to developers without breaking the open, self-hostable standard: a security sweep at publish/update, reporting & moderation, private / proprietary hosting, and anti-clone / provenance. Each builds on azphalt's existing trust primitives rather than inventing new cryptography. All three implementation phases are on main (@azphalt/registry's scanPackage, the visibility field + ACL download gate, and the clone/provenance signals). The shapes and wiring here are normative; enforcement policy (exact thresholds, block-vs-flag staffing) is a deployment choice — the sign-off decisions below record the flagship's defaults, which a self-hosted registry overrides.
Principles
- The sandbox is the primary user-safety mechanism, not source review. A
codeextension is deny-by-default and powerless (capability-model.md): no network, no filesystem, no host engine. The sweep is defense-in-depth and a labelling layer — it never becomes the thing that makes an extension safe. This is why azphalt can carry closed-source extensions safely. - Neutral standard, opinionated flagship. The standard fixes shapes and wiring (report format, scan-report format, visibility field, revocation feed). Enforcement policy (block vs flag, thresholds, moderation staffing) lives in a registry deployment — our flagship is strict; a self-hosted registry sets its own.
- Build on what exists. Publisher identity is already an Ed25519 key with publisher-continuity (
package-format.md § Signing); the registry can counter-sign (vouch); hosts poll a revocation feed (repository-api.md § Revocations); downloads already gate on entitlement tokens (repository-api.md § Download); discovery already filters by app-scoping. Integrity reuses all of these.
Foundations reused
| Existing primitive | Integrity use |
|---|---|
| Ed25519 publisher signature + continuity | Stable developer identity → provenance / anti-clone ownership |
| Registry counter-signature | "The registry vouches for this version" → elevates trust, gates auto-actions |
Revocation feed (/revocations, yank) | The outcome channel for a report or a re-scan denylist hit |
Entitlement tokens + 401/402 download gate | The access-control substrate for private packages |
App-scoping (targetApps) discovery filter | The pattern visibility filtering follows |
| Capability model (deny-by-default) | What the sweep audits declarations against |
1. Security sweep (publish- and update-time)
Every publish and every new version runs an automated battery; each check yields a verdict — pass, flag (index with a warning a host surfaces), or block (refuse to index).
- Container & signature — the existing
verifyAzp+verifyTrust(integrity, safe paths, signature, publisher continuity). Failure ⇒ block (already the behavior). - Capability least-privilege audit — cross-check declared
capabilitiesagainst thecontributesa package actually registers; a package that declares far more than it contributes is flagged ("requestslayers,audiobut contributes onefilter"). A declaration touching the never-list is blocked. - Secret scan — generalize the
kind:"mcp"credential-keyed rule (validateMcpManifest) to every kind: a credential-keyed value anywhere in the manifest that isn't an${input:…}reference is a leaked secret ⇒ block. (0.1 scans the manifest only, by key; scanning non-manifest text-asset payloads and value-shape/entropy heuristics are a planned future phase — the same coverage/false- positive trade-off deferred inmcp-server.md § Secret detection.) - Capability scope — a declared capability outside the capability model (
capability-model.md) — a never-list request (camera, microphone, network, filesystem, sensors, a host's proprietary engine …) or an unknown value — has no API to grant, so it ⇒ block (sweep.tscapability-scope). - Payload static analysis (planned) — for
code, confirm the module's host-function imports are a subset of the granted capabilities and flag an import of an undeclared capability before install (the runtime already fails instantiation at load, so this is a pre-install convenience). Not yet implemented; obfuscation / packing heuristics would likewise be advisory flags only. - Denylist — known-bad container digests and revoked publisher keys (fed by moderation outcomes, § 2) ⇒ block, and a re-scan of already-indexed versions on denylist update ⇒ revocation.
Output. A scan report { verdict, checks[], scannedAt } the registry counter-signs and attaches to the version. Hosts read it like the revocation feed: block never reaches them; flag carries a badge a host shows before install. The sweep is a @azphalt/registry module (server-side, run in publish) plus a reference CI action authors can run pre-submit.
Decision: the per-check block vs flag lines above are normative. Obfuscation / packing heuristics are advisory flags only — the flagship does not auto-block on them (a legitimate minifier or packer would be caught); a human reviews a flagged packing signal.
2. Reporting & moderation
POST /reports— anyone reports{ packageId, version?, reason, detail? }.reason∈malware · clone · deceptive · secret-leak · broken · ip-claim · other. Rate-limited by IP + optional reporter identity (a signed report from a counter-signed host or a verified account counts as trusted).- Developer IP claim (
reason: "ip-claim"). A rights-holder claims a package infringes their own work, adding{ originalPackageId, claimant?, signature? }. Two lanes: a signed claim — the claimant signs the canonical messageazphalt-ip-claim:v1:<packageId>:<originalPackageId>with the publisher key that signedoriginalPackageId, proving provenance ownership → stored trusted; and a plain DMCA-style claim (no/invalid signature) → stored untrusted for human review. The claim carries clone-similarity evidence (below) so a moderator sees why it's a clone even when no bytes were copied. A single trusted claim never auto-yanks on its own — it escalates to a human. - Queue & triage. Reports accumulate on a version. Auto-quarantine (yank pending review) fires on either (a) a security-sweep denylist hit, or (b) a threshold of trusted reports; everything else waits for human review. Quarantine ≠ deletion — the version stays resolvable by exact id so an appeal can restore it.
- Outcome → revocation. A confirmed report
yanks the version with itsreasononto the existing/revocationsfeed; installed hosts learn on their next poll. A malicious publisher key can be added to the sweep denylist, revoking all their versions. - Appeal. The publisher (proven by their signing key) can contest a quarantine; the standard fixes the report/appeal shape, the deployment runs the process.
- API.
POST /reports(public, rate-limited) +GET /reports(moderation, authenticated). The open registry stays neutral: a self-hosted registry may auto-approve, human-moderate, or ignore — the standard only pins the report object and the revocation wiring.
Decision: reports do not require an identity — anyone may POST /reports (rate-limited by IP). A report is trusted when it is signed by a counter-signed host or a verified account; only trusted reports count toward auto-quarantine. The threshold is deployment-configured; the flagship default is 3 trusted reports on a version (or a single security-sweep denylist hit) → auto-quarantine pending human review. Untrusted reports never trip the threshold — they queue for a human.
3. Private & proprietary hosting
- Visibility — a new manifest/registry field
visibility ∈ public | unlisted | private(defaultpublic).unlisted= resolvable by exact id, hidden from browse (like an app-scoped package);private= access-gated. The registry filters browse/search by it exactly as it doestargetApps. - Proprietary is already first-class. MIT permits closed/sold extensions, and the safety guarantee is the sandbox, not disclosure — so azphalt never requires source. This RFC makes that explicit: the sweep and clone-checks operate on the container and behavior, never source; a closed-source, paid, private extension is a fully-supported citizen.
- Private download —
GET …/downloadalready gates on a Bearer/entitlement token; extend the gate with a visibility/ACL check so aprivatepackage requires an access grant (buyer entitlement or org/user membership), returning401/402otherwise — the same envelope as paid downloads. - Privately-hosted source & registries — the standard is already self-hostable (
repository-server). Guidance: run a private registry (withauth) to serve private/proprietary packages to your org; the consignment overlay handles paid closed-source. No source ever leaves the publisher.
Decision: v1 is per-user access grants (buyer-style entitlements) only — the download gate already understands them, so private reuses that check with no new primitive. Org/team membership is deferred: a deployment that needs it maps its own team model onto the same per-user grant at the gate, so nothing here forecloses it. A first-class team object may be added later without changing the visibility field or the gate's shape.
3a. Content maturity (age-gating)
- Manifest field —
maturity ∈ general | mature(defaultgeneral), a developer self-attestation that rides in the manifest (extension-manifest.md) and projects onto the registry/wirePackageSummaryexactly asvisibilitydoes.maturemarks 18+ content. - Store gate — a store puts a
maturelisting behind an age-confirmation before its card/detail is revealed or opened; the confirmation is remembered for the session. Likevisibility,maturityis a discovery/UX signal a store surfaces and gates on — it is developer-set, not identity-verified age assurance, and a deployment MAY layer stronger enforcement (e.g. an ACL download gate) on top.
Decision: v1 is a manifest flag + client-side confirmation gate — the smallest thing that lets a developer responsibly list 18+ work and keeps it out of an unsuspecting browse grid. Verified age assurance is explicitly out of scope and left to the deployment.
4. Anti-clone & provenance
Goes beyond today's exact-version duplicate check (a re-publish of the same id@version is refused).
- Provenance = the publisher key. The first signer of a body of content holds the provenance claim; publisher-continuity already stops a hijacked update. This section extends it across packages.
- Clone signals at publish (each a flag, never an auto-block — legitimate forks, CC-shared assets, and templates create false positives). These catch more than exact copies: a reimplemented clone that shares no bytes still surfaces via shape + name/description resemblance (
packageSimilarity,clone-shape):- Asset-content hashing (
clone-assets) — a new package whose asset bytes largely match an existing package under a different publisher key. - Reimplemented shape (
clone-shape) — little/no shared bytes, but the same kind + asset-type palette + capability set and a resembling name or description. This is the byte-different clone an asset-digest check alone misses. - Name / id squatting (
clone-name) — a fuzzy (trigram) match ofname/idto an established package by a different key, not just a substring.
- Asset-content hashing (
- Developer protection. A clone flag carries the provenance evidence (which earlier package, which publisher key, similarity score) to moderation. If the original is registry-counter-signed or from a verified publisher, a strong match auto-quarantines the clone; otherwise it's human-reviewed. Legitimate forks clear review; a verbatim re-upload under a new id does not.
- User protection. A surviving flag also surfaces to users — a host shows "possible clone of
XbyY" so buyers aren't deceived.
Decision: a strong match against a verified / counter-signed original auto-quarantines the clone (never auto-blocks — a verbatim re-upload is restored on a successful appeal, a legitimate fork clears review). Everything weaker is human-reviewed. The similarity thresholds and false-positive tolerance are deployment-tuned: the standard fixes the signals (asset-content hash match, normalized code fingerprint, name/id fuzzy match) and the evidence a flag carries (which earlier package, which publisher key, the similarity score), never the numbers.
Phasing
- Sweep + reporting — extend
@azphalt/azp/@azphalt/registrywith the sweep battery + scan report; addPOST /reportsand wire outcomes to the existing revocation feed. (Highest user-safety value, smallest new surface.) - Visibility + private download — the
visibilityfield + the ACL download gate. - Clone / provenance — asset/code fingerprinting + the provenance-evidence moderation flow.
All three phases are implemented and on main.
Resolved
- Code-fingerprint algorithm — the signal is normative (a normalized shingled fingerprint whose similarity score rides in the clone flag's evidence); the exact shingling window / hash / threshold and their publish-time cost are a deployment choice, not fixed by the standard.
- Reporter anti-abuse — only trusted reports (counter-signed host / verified account) count toward auto-quarantine, so mass-reporting by anonymous accounts can't trip it; untrusted reports queue for a human, rate-limited by IP + reporter identity (§ 2).
- Scan report — folded into the repository-API version object and registry counter-signed, not a separate artifact; hosts read it like the revocation feed (§ 1).
- Verified publisher — "verified" is conferred by a registry counter-signature (the registry vouches for the key). A deployment MAY layer an additional out-of-band identity check to elevate further, but the counter-signature is the standard's baseline for the auto-actions above.