Self-hosted platform for standing up and running software factories on AWS: complete DevSecOps environments with secure delivery pipelines, supply-chain controls, compliance evidence and multi-tenant governance — in minutes, not months.
Putting a plugin in the plugin ecosystem: the public, searchable directory every organization on this instance can browse and install from.
private / org / public visibility ladder never puts anything in the directory.A listing is a plugin name published by a publisher, written @acme/terraform-plan. This page covers the publisher profile, trust tiers, the request model, scan gates, and the anonymous submission path.
Using listings — installs, version policies and each organization’s consumption policy — is covered in Plugin Installing.
Managing the publisher profile requires publishers:manage. Submitting listing and version requests, and pausing, requires plugins:publish. Owners and admins hold both.
public/<handle>/<plugin>), so choose carefully. See Your publisher profile.listings quota caps active public listings; see Plans and limits.A publisher is your organization’s public identity. It has a handle (acme, shown as @acme/terraform-plan), a display name, a description, a homepage and a trust tier. Each root organization has one publisher.
public/<handle>/<plugin>), so choose it carefully.pipeline-builder, official, community, system and others) can’t be claimed. The system org can also reserve names, for example to protect a vendor’s brand. If a reserved handle is yours, submit a claim request and the system org decides.Managing the publisher profile requires publishers:manage. Submitting listing and version requests, and pausing, requires plugins:publish. Owners and admins hold both.
| Tier | Who | How it is earned |
|---|---|---|
| Official | The platform’s own catalog (the pipeline-builder publisher, owned by the system org) |
Every listing and version is approved by two Ecosystem Managers, except the one-time initial catalog load and gate-green routine updates from the catalog loader. |
| Verified | A publisher on the Team or Enterprise plan, with a DNS-verified domain and two-factor authentication on every owner, whose application the system org approved | Apply from the Publisher page. The badge is earned through review and can be withdrawn. It is never sold. |
| Community | Any signed-in organization, on any plan, within its listings limit | Each listing is approved by the system org. |
| Unverified | Anyone, without an account, through Submitting without an account. Listed under the platform’s community publisher |
Every version passes the automated checks and is approved by two Ecosystem Managers. |
The tier travels with every published image as a signed annotation (pb.trust), so changing a tier re-signs every image the publisher has published.
Publishing is available on every plan, within a listings limit: the number of live (listed or unmaintained) listings your root organization publishes.
| Plan | Listings | Verified eligibility |
|---|---|---|
| Developer | 3 | — |
| Pro | 10 | — |
| Team | 25 | Yes |
| Enterprise | 100 | Yes |
| Self-hosted (billing off) | Unlimited | Yes |
VERIFIED_PLAN_REQUIRED);VERIFIED_DOMAIN_REQUIRED);VERIFIED_OWNER_MFA_REQUIRED).If the checks can’t be run at that moment the application is refused as unavailable (503); try again shortly.
Every plugin image is scanned for vulnerabilities when it’s built: an SBOM is made with syft and scanned with grype. This applies to every build, not just published ones: uploads, prebuilt images, AI-generated plugins, bulk uploads and the catalog loader. Anonymous submissions go through the same gate.
Fixable findings. A finding is fixable when the scanner knows a version of the affected package that fixes it. The gates count fixable Critical findings only, because a Critical with no fix yet can’t be resolved by rebuilding. Plugin views show both numbers for Critical and High, for example 2 fixable / 5 Critical.
A build that can’t be scanned fails. If the scan can’t run, the build is retried like any other transient failure. After the last attempt it fails with IMAGE_SCAN_UNAVAILABLE: nothing is stored and the plugin quota the upload reserved is released. Build it again once the scanner is back.
The operator can let such builds through with PLUGIN_ALLOW_UNSCANNED=true. The version is then stored without a scan, shows an Unscanned badge, and the skip is audited (plugin.scan.skipped).
A scoped exemption, when a package genuinely cannot be fixed. Some images cannot reach zero fixable Criticals however current they are. A browser-bundling test runner is the clear case: artillery ships Chromium through its own @playwright/browser-chromium dependency and playwright downloads Firefox, both pinned to the upstream release — and browsers ship CVE fixes weekly while pinned builds lag by design. Raising PLUGIN_VULN_MAX_CRITICAL would exempt every plugin from every Critical, which is not a trade worth making for one package.
PLUGIN_VULN_WAIVERS exempts named packages for one plugin, until a date:
PLUGIN_VULN_WAIVERS=artillery:2026-12-31:chromium,chrome;playwright:2026-12-31:firefox
Entries are ;-separated; each is <plugin>[@<version>]:<YYYY-MM-DD>:<package>[,<package>…]. Pin the version where you can: a waiver is a judgement about a specific image, and left unpinned it carries silently to every future version, including ones whose findings nobody has looked at. Three properties are enforced, not merely intended:
(name, version, orgId) and not globally, so without it any tenant could inherit an exemption simply by naming their plugin artillery. Waivers cover the Official catalog the operator curates; an organization’s own plugin of the same name is unaffected, and so is an organization’s stricter compliance rule, which is evaluated after the floor on the raw scan.plugin.vuln.waived (the hash-chained record, not just a metric that resets), reported on the build stream, logged, counted by plugin_vuln_waived_total{plugin}, and shown in the UI both on a blocked build and on a version that passed under one. Malformed entries are logged at boot and counted by plugin_vuln_waivers_invalid; remaining lifetime is published as plugin_vuln_waiver_days_remaining{plugin}, with PluginVulnWaiverInvalid and PluginVulnWaiverExpiringSoon alerting on both.A malformed entry is dropped, never treated as a wildcard — the failure mode of a typo is “the gate still blocks”, not “everything is waived”. An entry with no package list is malformed for exactly this reason, so it cannot mean “waive this plugin”. The nightly rescan applies the same waivers, so it will not flag a version the build gate passed.
The waiver changes what the floor counts; it does not hide the finding. The scan still records it, and it still appears in the version’s findings.
The platform floor. A version with more fixable Critical findings than PLUGIN_VULN_MAX_CRITICAL fails its build with PLUGIN_VULN_GATE (default 0, so any fixable Critical fails; -1 turns the floor off). The build’s failure message lists the top findings and the versions that fix them. Upgrade those packages, or the base image, and build again. Your organization’s compliance rules can be stricter than the floor, but not looser.
Nightly rescans. Stored versions are rescanned every night against the latest vulnerability data. This includes every listed version, which is rescanned from its own public image even if the organization that built it has since deleted the plugin. When a rescan finds more fixable Critical findings than the floor allows, the version is flagged: plugin views show Flagged by rescan with the top findings and their fixed versions, and pipelines that resolve it get a VULN_FLAGGED warning (see Plugin Installing). The flag clears on the first rescan that finds the findings resolved. To clear it yourself, rebuild the plugin on patched packages, or publish a new version.
Who is told. A blocked build (notice N30) is always reported right away. New Critical or High findings from a rescan (notice N31) follow your organization’s settings. Admins set both under Settings → Organization → Plugin security notifications: who receives them (the uploader and everyone who can write plugins, or chosen members), whether rescan findings are sent and as a daily or weekly digest, an optional signed webhook, and one external address. The external address gets nothing until its owner opens the confirmation link and presses Confirm address. Send test sends a test notice to every configured channel.
Every change to the ecosystem is a request. Open requests show on the Publisher page’s Requests tab, where you can withdraw them.
| Request | What it asks for | Permission |
|---|---|---|
| New listing | List one of your plugin versions under a new name | plugins:publish |
| New version | Publish another version of an existing listing | plugins:publish |
| Listing update | Change the listing’s summary, description, category, keywords, license, links, icon or README | plugins:publish |
| Yank | Stop a listed version resolving for new pipeline synths | plugins:publish |
| Unpause | Lift a pause you applied | plugins:publish |
| Transfer | Move a listing to another publisher (the receiving publisher must accept first) | publishers:manage |
| Claim | Take a reserved handle, or a listing under the platform’s community publisher |
publishers:manage |
| Profile change | Change your handle or display name | publishers:manage |
| Verified | Apply for the Verified badge (Team and Enterprise) | publishers:manage |
You can have at most one open request of each kind for the same listing and version.
A new-listing or new-version request is refused, with the list of failing checks, unless the version:
When you submit a version request, the version’s image digest is recorded on the request and the version is frozen. From that moment it can’t be re-uploaded, edited or deleted (the upload answers 409 PLUGIN_VERSION_FROZEN). The approval publishes exactly that digest. If the stored digest changed anyway, the approval fails closed with PLUGIN_DIGEST_MISMATCH. Withdrawing or a rejection releases the freeze, unless the version is already listed.
POST /api/plugins/upload accepts publishRequest=true (together with visibility=public). Once the build completes, the plugin service submits a new-listing request (or a new-version request if the name is already listed) as the uploader. The Official catalog loader (deploy/bin/load-plugins.sh) uploads every plugin this way.
pipeline-manager plugin publish --dir <plugin> runs every local check, then submits the request with one upload:
USER, every download through fetch-verified, no pipe-to-shell installers. Any problem stops it before anything is uploaded.--skip-scan skips it on purpose, and --image scans an image you built.--yes accepts everything detected. --metadata <file.yaml> supplies your edits without prompting, and every other detected value is accepted. The file uses the same keys and validator as the upload form, so contract keys such as commands are refused.visibility=public, publishRequest=true and your edits as metadata. The request is submitted once the build completes.plugin validate --dir <plugin> prints the same catalog report without publishing: which fields would be empty or invalid, and where each value comes from. plugin test runs the plugin in its image first (see Pipeline Manager).
# catalog.yaml, for plugin publish --metadata catalog.yaml
summary: Lints Terraform with tflint and the AWS ruleset.
homepageUrl: https://github.com/acme/tflint-plugin
keywords: [terraform, lint, aws]
documentationUrl: null # null clears a detected value
The metadata your plugin package already declares is detected and pre-filled. You accept each value or edit it.
Where each value comes from (the first source with a value wins):
| Field | 1. plugin-spec.yaml |
2. README.md |
3. The plugin’s own Dockerfile LABEL |
|---|---|---|---|
| Summary (160 characters) | summary |
— | — (then the first sentence of the description) |
| Description | description |
first paragraph | org.opencontainers.image.description |
| Display name | — | first # heading |
org.opencontainers.image.title |
| License | license |
— | org.opencontainers.image.licenses |
| Homepage / source / documentation links | homepageUrl / sourceUrl / documentationUrl |
— | org.opencontainers.image.url / .source / .documentation |
| Category, keywords, icon, changelog | spec | — | — |
OFFICIAL_AUTO_APPROVAL_ENABLED.A rule never approves a major or breaking version, a new listing, a version with new critical or high vulnerabilities, or one that adds secrets, egress hosts, required inputs or root.
public/<publisher>/<name> namespace, signs it fresh with your tier, records the version on the listing and makes it immutable.You’re told about every decision in-app and by email (notice N25; N7 for a Verified application; N9 and N10 for transfers).
Pause a listing or one version from the Publisher page’s Listings tab. It takes effect at once and needs no review.
Unpausing is a request, decided by the system org.
Signed-in users rate and review your listings on their public pages. Nobody in your organization, or a team under it, can review your listings or vote on their reviews.
publishers:manage in your organization can post one public reply per review from the listing’s Reviews tab (PUT /api/plugins/reviews/:id/reply with { body }), edit it, or delete it (DELETE on the same path). Replies are Markdown, rendered on the server like reviews, up to 5,000 characters. The reviewer is told (N16). Moderators can remove a reply.ecosystem.reviews.email). You see the reviewer’s display name only, never their organization.Review and reply actions are audited with affectedOrgId = your organization: plugin.review.* and plugin.review.reply.*.
Anyone can submit a plugin from the public directory, without signing in: Submit a plugin at /plugins/submit. It’s meant for people who want to share one plugin and don’t need a publisher of their own. If you have an account, publish from your organization’s Publisher page instead.
The instance must turn it on: ANONYMOUS_SUBMISSIONS_ENABLED=true, and outbound email must be configured. Otherwise the page says submissions aren’t enabled, and the API answers 404 SUBMISSIONS_DISABLED.
.zip an in-app upload takes (plugin-spec.yaml, a Dockerfile, and optionally a README.md), up to 50 MB. Zips with symbolic links, hard links or device files are refused.env;smokeTest is declared and passes (it runs with no network);If any check fails, you’re emailed the failed checks and nothing is listed.
public/community/<name>, signed fresh with the Unverified tier, and listed at /plugins/community/<name>. A rejection email carries the moderator’s reason.After confirming, you get a status link (/plugins/submit/status?token=…), shown once and repeated in every email. It shows the status, each check’s result and the decision reason, and links the listing once approved. Bookmark it: anyone with the link can see the status, but not your email address.
community publisher, as community/<name>, where <name> is the spec’s name.-, _ and ., and reading look-alikes (0→o, 1→l, 3→e, 5→s, rn→m, vv→w), or one edit away from one.409 NAME_TAKEN.To manage the plugin from an account, create one with the same email address, create your publisher, and file a claim request for the community listing from the Publisher page. The system org decides it like a transfer. When your verified account email matches the address that submitted the listing, the moderators see that it matches. Otherwise they see “email does not match submitter” and approve only with a written justification. Once the claim is approved, the listing moves to your publisher, and you’re notified.
429 SUBMISSION_LIMIT.The routes are public, under /api/public/plugin-submissions, and rate limited per network address. None of them accepts or needs a session.
| Method | Endpoint | Body | Result |
|---|---|---|---|
GET |
/challenge |
— | { challenge, difficulty, expiresAt }: a single-use puzzle. Find a decimal nonce such that SHA-256(<challenge>:<nonce>) starts with at least difficulty zero bits. |
POST |
/inspect |
multipart: plugin (zip), pow ({"challenge","nonce"} as JSON) |
The spec summary, every detected catalog field with its source, lint results and a suspicious-pattern preview. Stores nothing. |
POST |
/ |
multipart: plugin, email, pow, acceptTerms=true, optional metadata (the edited catalog fields, as JSON) |
202 { id, status: "pending_verification" }. Sends the confirmation email. |
POST |
/verify |
JSON { token } |
{ id, status, statusToken } |
GET |
/status?token=<statusToken> |
— | { id, name, version, status, reason?, gates?, listing? } |
Statuses: pending_verification, pending_review (checks running or waiting for a moderator), gate_failed, approved, rejected, expired, claimed.
| Code | Status | Meaning |
|---|---|---|
SUBMISSIONS_DISABLED |
404 | Anonymous submissions are off on this instance, or outbound email isn’t configured. |
PROOF_OF_WORK_INVALID |
400 | The puzzle answer is missing, wrong, expired or already used. Get a new challenge. |
VALIDATION_ERROR |
400 | The package, email, terms or an edited field is invalid. Editing an execution setting through metadata is refused here too. |
SUBMISSION_LIMIT |
429 | 3 submissions in 24 hours from this email or network address. |
NAME_TAKEN |
409 | The community listing was submitted from a different email address. |
A plugin is not passive content. At pipeline runtime its image runs in the consuming organization’s CodeBuild project with that organization’s declared secrets, the pipeline’s IAM role (often able to deploy to production) and its checked-out source. A listed plugin is therefore a software-supply-chain dependency, like an npm package or a GitHub Action, and the rules below all follow from keeping “uploaded” and “trusted to run” separate.
Governance. Only the system organization decides what enters or changes the
ecosystem. Its Ecosystem Manager role (built in, system-org only, assigned by
superadmins, never grantable through custom roles) holds plugins:moderate and
publishers:verify, so managers don’t need full superadmin. The permissions are
already separate, so splitting the role later is only a seed change. Tenant orgs
still decide what runs in their own pipelines (their consumption policy) —
that governs only their org, never the ecosystem. See the
moderation runbook and
Permissions.
Decisions behind the rules:
| Question | Answer, and why |
|---|---|
| Must anonymous submitters verify an email? | Yes, plus a self-hosted proof-of-work puzzle. The feature is unavailable without outbound email. |
| Anonymous reviews? | No. Anyone may read; writing needs a signed-in person. |
| May Unverified or Community plugins receive secrets? | Not by default; an org’s consumption policy can allow it per tier. |
| Paid plugins or revenue share? | Not offered; the publisher model leaves room for it. |
| Is every listing and version decided by the system org, even from signed-in publishers? | Yes. To keep that sustainable, low-risk updates (patch/minor from Verified publishers, every gate green, no new secrets or egress) are approved by auto-approval rules the system org owns — audited, and revocable per rule. |
Do installs count against the plugins quota? |
No; installs are free. |
| Can a self-hosted instance install from the hosted directory? | Not yet. Each instance has its own directory; the listing format is designed so sync can be added without a schema change. |
| How do consumers pull another org’s images? | Approval copies the pinned image into the read-only public/<publisher>/<name> namespace and signs it fresh there with the tier annotation. Copying — rather than widening token grants to publishers’ namespaces — makes listed versions immutable, survives the publisher’s deletion, and keeps the registry’s token rules simple. public/* is pullable by every authenticated identity and writable only by image-registry. |
| How much does a review without verified use count? | 0.5, labelled “not verified”. |
| May members install directly? | Members hold plugins:install; whether that installs or files an approval request is the org’s consumption policy (requireApprovalToInstall, on by default below Verified). |
| May a publisher pause without approval? | Yes. Pausing only narrows their own reach (hidden from new installs; existing installs keep resolving). Unpausing is a request. |
| Which plans may publish, install, or be Verified? | Installing, reviewing and every safety control are on every plan. Publishing is on every plan within a listings limit (3 / 10 / 25 / 100). Verified is open to Team and Enterprise only and is earned through review, never bought; there is no paid priority review. A downgrade keeps listings listed, freezes non-security updates while over the limit, and gives Verified a 30-day grace period. |
| Are Official plugins installed for every org? | Yes, implicitly — a virtual install (no rows) with policy minor; majors are never automatic. Orgs can install explicitly, opt out (officialInstalls: explicit) or block listings (blockedListings); an org’s own same-name plugin still wins, with a shadowing warning. Explicit rows for everything would break existing pipelines, and seeding rows at org creation would miss Official plugins added later. See Plugin Installing. |
| Where do listing logos come from? | Simple Icons (CC0) first, vendor press-kit marks only where the vendor’s terms allow, monograms otherwise. Curated marks go only on Official listings and on Verified publishers who own the mark; Community listings upload raster icons or get a monogram. Free logo choice would enable impersonation, and SVG uploads would enable XSS. |
| Do routine Official catalog updates need two-person approval? | No — a seeded auto-approval rule covers gate-green patch/minor updates of existing Official listings submitted by the catalog loader service account, with no new secrets, egress, required inputs, root or vulnerabilities, capped at 1 per listing and 50 per day, and switchable off with OFFICIAL_AUTO_APPROVAL_ENABLED. New Official listings, majors and riskier updates keep two-person approval. |
| Where does catalog metadata come from, and who has the last word? | The package’s own declarations are detected and pre-filled (spec, then README, then the plugin’s own Dockerfile OCI labels — never labels inherited from its base image), and you accept or edit each field. Only descriptive fields are editable; execution-contract fields come from the spec alone, so changing one is a new version. Each field’s source is shown to moderators. See Catalog metadata. |
Versions are immutable once requested. A version is frozen from the moment a
request references it: re-uploading it is refused (PLUGIN_VERSION_FROZEN), and
approval publishes the request’s pinned digest, failing closed if the stored
digest differs. A yank stops a version resolving for new synths; pipelines pinned
by digest keep pulling it. A public/* image is garbage-collected only when it
was yanked more than 180 days ago and no deployed pipeline references it.
Out of scope: running plugins anywhere but CodeBuild, importing from other ecosystems (GitHub Actions, npm), paid listings, and syncing between instances.
| Code | Meaning |
|---|---|
PUBLISHER_REQUIRED |
Create your publisher profile first. |
PUBLISHER_TERMS_REQUIRED |
Accept the current publisher terms. |
PUBLISHER_ROOT_ORG_REQUIRED |
Switch to your root organization; teams can’t publish. |
PUBLISHER_HANDLE_RESERVED |
The handle is reserved; submit a claim request if it’s yours. |
PUBLISH_GATE_FAILED |
The version fails a check; details.gates lists which. |
QUOTA_EXCEEDED (details.quotaType: listings) |
You’re at, or over, your plan’s listings limit. |
PUBLISHER_SUSPENDED |
The system org suspended the publisher. |
PLUGIN_PUBLISHING_DISABLED |
Publishing is turned off on this instance (PLUGIN_PUBLISHING_ENABLED). |
PLUGIN_VERSION_FROZEN |
The version is referenced by a request or already listed. |
IMAGE_SCAN_UNAVAILABLE |
The build’s image couldn’t be scanned after every retry. Nothing was stored; build again later. |
PLUGIN_VULN_GATE |
The build’s image has more fixable Critical findings than PLUGIN_VULN_MAX_CRITICAL allows. The message lists them with their fixed versions. |
DUPLICATE_ENTRY |
An open request of that kind already exists, or the handle is taken. |