Software Factories on AWS

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.

Plugin Publishing

Putting a plugin in the plugin ecosystem: the public, searchable directory every organization on this instance can browse and install from.

Highlights

Overview

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.

How it works

  1. Claim a publisher handle for your root organization, accepting the current publisher terms.
  2. Build and scan the plugin version in your own org as usual. Scan gates decide whether it is publishable at all.
  3. Submit a request — a new listing, a new version, a listing update, a yank, an unpause, a transfer, a claim, a profile change, a Verified application or an advisory.
  4. The digest is pinned into the request. Approval verifies it still matches, and fails closed if not.
  5. An Ecosystem Manager decides, or an auto-approval rule does. Some request kinds need two people, and separation of duties stops one person supplying both approvals.
  6. On approval the image is copied into the public namespace, freshly signed and SBOM-attested, and the listing becomes installable.
  7. You keep unilateral control of reach. Pausing a listing or a version takes effect immediately without anyone’s approval.

Configuration

  1. Claim your handle on the Publisher page (dashboard → Build → Publisher). 2–39 lowercase letters or digits with single hyphens; it names your registry namespace (public/<handle>/<plugin>), so choose carefully. See Your publisher profile.
  2. Accept the publisher terms. Re-accept when they change, or new requests are refused.
  3. Check your plan’s limits. The listings quota caps active public listings; see Plans and limits.
  4. Get the version through the scan gates before submitting — see Scan gates.
  5. Decide how metadata is supplied. The catalog metadata is detected and then accepted or edited; see Catalog metadata: accept or edit.
  6. Submit from the UI, the CLI or a script — see Publishing from the CLI.
  7. Apply for Verified if you are eligible: it needs a Team-or-above plan, a DNS-verified domain and an owner with a second factor. The badge is awarded after review, never bought.

Your publisher profile

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.

Managing the publisher profile requires publishers:manage. Submitting listing and version requests, and pausing, requires plugins:publish. Owners and admins hold both.

Trust tiers

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.

Plans and limits

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

Scan gates

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:

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.

Requests

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.

What a version request needs

A new-listing or new-version request is refused, with the list of failing checks, unless the version:

The digest is pinned

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.

Publishing from a script or the loader

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.

Publishing from the CLI

pipeline-manager plugin publish --dir <plugin> runs every local check, then submits the request with one upload:

  1. Pre-flight. It runs the server’s own checks (the spec and config schemas and the `` contract, shared from api-core) and the catalog’s Dockerfile rules: a non-root final USER, every download through fetch-verified, no pipe-to-shell installers. Any problem stops it before anything is uploaded.
  2. Scan preview. It builds the image, makes an SBOM with syft and scans it with grype, as the platform will after the build. A critical vulnerability stops the publish. If syft or grype isn’t installed, it says the preview did not run rather than passing silently. --skip-scan skips it on purpose, and --image scans an image you built.
  3. Accept or edit. It shows each catalog field with its detected value and source (below) and asks you to accept, edit or clear it. --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.
  4. Upload. It checks your publisher profile, root organization and terms, then uploads the package with 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

Catalog metadata: accept or edit

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 — —

How requests are decided

You’re told about every decision in-app and by email (notice N25; N7 for a Verified application; N9 and N10 for transfers).

Pausing

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.

Reviews of your listings

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.

Review and reply actions are audited with affectedOrgId = your organization: plugin.review.* and plugin.review.reply.*.

Submitting without an account

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.

How it works

  1. Choose the package. The same .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.
  2. Check the details. The package is read without being stored. Every catalog field is shown with the value detected from it and where that value came from, and you accept or edit each one, exactly as in Catalog metadata: accept or edit. You also see a preview of the directory card, the Dockerfile and spec lint results, and a scan for suspicious patterns. Execution settings (commands, env, secrets, egress, the smoke test) come only from the spec and can’t be edited. A community listing can’t use a curated vendor icon, so its card shows a monogram.
  3. Confirm and submit. Give an email address and accept the submission terms: you confirm you have the right to publish the code under its license, and that it may be listed publicly. Submitting stores the package in a quarantine bucket. Nothing is built yet.
  4. Confirm your email. The confirmation link expires in 30 minutes and works once. It opens a page with a Confirm submission button. The link never confirms on its own, so mail scanners that open links can’t use it up. Unconfirmed submissions are deleted after 30 days.
  5. Automated checks. The plugin is built in an isolated sandbox: a separate build service with no credentials, no cloud identity and network access only to package mirrors. Every check must pass:
    • the spec and execution contract are valid;
    • the license is an allowed SPDX identifier;
    • the Dockerfile and spec have no lint errors;
    • the image doesn’t run as root;
    • the image’s fixable critical findings are under the instance’s threshold;
    • no high-severity suspicious patterns (crypto-miners, obfuscated shell, reading cloud or CI credentials, piping downloads to a shell);
    • no secret-looking default values in env;
    • a smokeTest is declared and passes (it runs with no network);
    • the name is allowed (see below).

    If any check fails, you’re emailed the failed checks and nothing is listed.

  6. Moderation. A submission that passes every check enters the Ecosystem console’s queue as a Community submission. Two Ecosystem Managers must approve it. They see the check results, every suspicious-pattern finding (including lower-severity ones you aren’t shown), the SBOM, the scan report, and the difference from the previous approved version. The target is two business days.
  7. The decision. You’re emailed either way. An approved plugin is published to 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.

Names and updates

Claiming your listing

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.

Limits and privacy

Submission API

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.

Why the ecosystem works this way

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.

Errors you may see

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.