"Marketplace distribution — cyberos-plugin publish pushes signed bundle to plugins.cyberskill.world + mirrors to agentskills.io; revenue-share + vetted badge"
§1 — Description (BCP-14 normative)
The PLUGIN module MUST ship a marketplace publish surface at services/plugin-host/src/marketplace/. CLI cyberos-plugin publish <bundle> uploads a packed bundle to plugins.cyberskill.world (OCI-compatible registry); public plugins additionally mirror to agentskills.io. The marketplace server itself (registry API + UI) is scaffolded here and fully implemented in task-PLUGIN-008a.
- MUST implement
cyberos-plugin publish <bundle>CLI with flags per DEC-2470 + DEC-2471:
--visibility {public,private,enterprise}— default reads from manifest'smarketplace.visibility(per TASK-PLUGIN-001 schema)--registry <url>— defaults tohttps://plugins.cyberskill.world--mirror-agentskills— boolean, default true for public visibility; force-false otherwise--vetted-badge-token <token>— optional, supplied by CyberSkill after manual review (clause 5)
- MUST validate bundle integrity before upload per DEC-2475:
- Re-run
cyberos-plugin doctor <bundle>and require all 8 INTEROP invariants pass - Re-validate canonical manifest against
manifest.schema.jsonper TASK-PLUGIN-001 - Verify Sigstore Rekor anchor matches the bundle bytes
- Verify bundle's target adapter output matches the per-target reproducibility check
- MUST push to OCI-compatible registry at
plugins.cyberskill.world/v2/<id>/blobsper OCI Distribution Spec v1.1:
- Bundle bytes pushed as
application/vnd.cyberskill.plugin.v1+zip(or+folderfor codex-cli) - Manifest pushed as
application/vnd.cyberskill.plugin.manifest.v1+json - Tag =
<version>(e.g.1.0.0) - Authentication via OAuth-PKCE bearer token (per TASK-PLUGIN-005)
- MUST mirror public-visibility plugins to
agentskills.ioper DEC-2472:
- Only
marketplace.visibility == "public"qualifies - Mirror uploads the canonical manifest + skills/ folder (NOT the binary, NOT the OAuth-PKCE-bound runtime fields)
- agentskills.io receives an "anthropic-skills" subset of the manifest
- Failure to mirror is a soft failure (publish to plugins.cyberskill.world succeeds, mirror retried via TASK-PLUGIN-006 audit_outbox pattern)
- MUST support the vetted-by-CyberSkill badge per DEC-2474:
- Badge token issued via out-of-band manual review process (initially) at
https://plugins.cyberskill.world/admin/vet - Token is JWT signed by CyberSkill with
aud: "plugin:<id>:<version>" - Publish embeds token in marketplace metadata; registry verifies signature; badge persists across reads
- Token-less publish still succeeds; just no badge
- MUST support three visibility modes per DEC-2471:
public— discoverable by any user; appears in marketplace search; mirrored to agentskills.ioprivate— discoverable only within the publishing tenant; mirror disabled; install requires tenant membershipenterprise— like private, but the registry is a separate tenant-isolated origin (Strategy Level 5 white-label); URL patternplugins.<enterprise-domain>.cyberskill.world
- MUST validate revenue-share rules per DEC-2473:
- Free plugins (
marketplace.price_usd_per_monthabsent or 0): no revenue share, badge irrelevant - Paid plugins (
marketplace.price_usd_per_month > 0): 70/30 author/CyberSkill split enforced server-side at billing time marketplace.revenue_share_percentdefaults to 70 per manifest schema; values < 70 trigger publish-time warning (author getting less than fair share)
- MUST emit memory audit row
plugin.publishedper DEC-2476 with body containing:
plugin_id,version,sha256of bundle,signature.rekor_uuid,visibility,vetted_badge: boolean,mirror_targets: [...],trace_idAudit emission follows TASK-PLUGIN-006 retry semantics.
- MUST enforce version monotonicity at registry — uploading version
1.0.0after1.0.1MUST fail. SemVer 2.0 semantic ordering per TASK-PLUGIN-001 clause 4.
- MUST support
cyberos-plugin yank <id>@<version>for emergency removal — yanked versions are hidden from search but remain installable by users with the SHA-256 (so existing installs don't break). Yank emits memory auditplugin.yanked.
- MUST NOT publish a bundle that fails any of the 8 INTEROP invariants per DEC-2475 + clause 2.
- MUST NOT mirror private or enterprise plugins to agentskills.io per DEC-2472 + clause 4. Private bundles MUST stay on CyberSkill's infrastructure.
- MUST NOT allow a paid plugin to set revenue_share_percent > 100 — schema enforces 0-100 range; publish double-checks.
- MUST NOT publish without a verified Sigstore signature anchor per clause 2.
§2 — Why this design
Why OCI-compatible registry (DEC-2470, clause 3)? OCI registries are the de-facto standard for bundle distribution. Docker Hub, GitHub Container Registry, AWS ECR all speak OCI. Using OCI gives us free integration with existing tooling (cosign signing, opens up multi-cloud mirroring). Marketplace UI sits on top of the OCI API.
Why three visibility modes (DEC-2471, clause 6)? Strategy Level 3 needs public (anyone installs); Strategy Level 1 OSS amplifies via public mirror; Strategy Level 4 (vertical packs) is mostly public but may be tenant-specific in early stages → private; Strategy Level 5 (enterprise white-label) is enterprise. Three covers all four levels cleanly.
Why mirror to agentskills.io (DEC-2472, clause 4)? Strategy §2 lists agentskills.io as the open Anthropic registry; CyberSkill is a citizen. Public plugins benefit from being discoverable via Anthropic's ecosystem search, not just CyberSkill's. Mirror gives reach without ceding ownership (registry of record remains plugins.cyberskill.world).
Why 70/30 split (DEC-2473, clause 7)? Industry standard (Apple App Store, Shopify post-Shopify Capital, Salesforce AppExchange). 70% is the "fair share" headline; deviation triggers a warning because plugin authors deserve protection from accidental low splits.
Why JWT-signed vetted badge (DEC-2474, clause 5)? Badge claim must be unforgeable. JWT signed by CyberSkill's marketplace key is verifiable client-side (cyberos-plugin doctor rechecks). Manual review at first; automated for security-only plugins later.
Why publish-time re-validation (DEC-2475, clause 2)? Local validation can be tampered with (modified cyberos-plugin binary). Server-side re-validation is the trust anchor. Same reason GitHub re-verifies signed commits.
Why version monotonicity (clause 9)? Out-of-order publish (1.0.1 then 1.0.0) confuses dependency resolvers. Either version is "latest"? Monotonic publish-by-version means latest tag always points at the highest version.
Why yank-not-delete (clause 10)? Permanent deletion breaks existing installs that have the SHA-256. Yank hides from discovery but preserves install ability. Same model as crates.io, npm.
Why audit publish (DEC-2476, clause 8)? Strategy §2 demands every action be audit-chained. Publish is a high-stakes action (introduces code into users' environments). Audit row is forensic record + DSAR-exportable.
Why no mirror for private/enterprise (clause 12)? Private plugins may contain tenant-confidential content (vertical packs for specific clients). Mirroring to a public registry leaks. Hard separation.
§3 — API contract
CLI surface
cyberos-plugin publish <bundle> [--visibility VIS] [--registry URL] [--mirror-agentskills] [--vetted-badge-token TOKEN]
cyberos-plugin yank <id>@<version> [--reason TEXT]
cyberos-plugin list [--visibility VIS] [--vetted]
cyberos-plugin info <id>@<version>
Plugin registry Postgres schema (scaffold for task-PLUGIN-008a server)
-- services/plugin-marketplace/migrations/0001_plugin_registry.sql
CREATE TABLE plugin_marketplace.plugins (
plugin_id TEXT NOT NULL,
version TEXT NOT NULL,
owner_tenant_id UUID NOT NULL,
visibility TEXT NOT NULL CHECK (visibility IN ('public','private','enterprise')),
bundle_sha256 BYTEA NOT NULL,
bundle_size_bytes BIGINT NOT NULL,
rekor_uuid TEXT NOT NULL,
vetted_badge_token TEXT,
vetted_at TIMESTAMPTZ,
price_usd_per_month NUMERIC(10,2),
revenue_share_percent SMALLINT NOT NULL DEFAULT 70,
yanked_at TIMESTAMPTZ,
yanked_reason TEXT,
published_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_by_subject UUID NOT NULL,
trace_id CHAR(32),
PRIMARY KEY (plugin_id, version)
);
CREATE INDEX ON plugin_marketplace.plugins (visibility, published_at DESC)
WHERE yanked_at IS NULL;
CREATE INDEX ON plugin_marketplace.plugins (owner_tenant_id, visibility);
CREATE INDEX ON plugin_marketplace.plugins (vetted_at) WHERE vetted_at IS NOT NULL;
Publish flow
// services/plugin-host/src/marketplace/publish.rs
pub async fn publish(
bundle_path: &Path,
visibility: Visibility,
registry: &Url,
mirror_agentskills: bool,
vetted_badge_token: Option<&str>,
) -> Result<PublishResult> {
// Step 1: validate bundle (re-run doctor + manifest schema)
let invariants = doctor(bundle_path)?;
if !invariants.all_pass() {
return Err(PublishError::InvariantViolation(invariants.failures));
}
// Step 2: parse manifest
let manifest = read_canonical_manifest_from_bundle(bundle_path)?;
// Step 3: verify Sigstore anchor matches bundle bytes
sigstore::verify(bundle_path, &manifest.signature.rekor_uuid).await?;
// Step 4: push to plugins.cyberskill.world OCI
let push_result = oci_push(bundle_path, &manifest, registry).await?;
// Step 5: mirror to agentskills.io if public
let mirror_status = if visibility == Visibility::Public && mirror_agentskills {
mirror_agentskills::push(&manifest, bundle_path).await.into()
} else {
MirrorStatus::Skipped
};
// Step 6: emit memory audit
audit::emit_published(
&manifest, &push_result.sha256, visibility, mirror_status.clone(),
).await?;
Ok(PublishResult {
plugin_id: manifest.id,
version: manifest.version,
sha256: push_result.sha256,
rekor_uuid: manifest.signature.rekor_uuid,
mirror_status,
vetted_badge: vetted_badge_token.is_some(),
})
}
Vetted-badge token format
JWT (RS256) signed by CyberSkill marketplace key:
iss = "https://plugins.cyberskill.world"
aud = "plugin:<id>:<version>"
sub = <reviewer_subject_id>
iat, exp (typically 90-day validity)
body:
review_id = "REV-2026-...",
review_date = "...",
findings_summary = "..."
Mirror request to agentskills.io
POST https://agentskills.io/v1/skills/publish HTTP/1.1
Content-Type: application/json
Authorization: Bearer <agentskills.io API key for CyberSkill org>
{
"publisher": "cyberskill",
"skill_id": "cyberos",
"version": "1.0.0",
"skill_md_url": "https://plugins.cyberskill.world/cyberos/1.0.0/SKILL.md",
"license": "Apache-2.0",
"description": "..."
}
§4 — Acceptance criteria
publishrejects bundle failing doctor invariants — mock a bundle with broken signature; publish exits 1.publishrejects unsigned bundle — bundle missing Sigstore anchor; publish exits 1.publishpushes to OCI registry — mock registry receives PUT to/v2/cyberos/blobs/uploads/.publishfor public mirrors to agentskills.io — mock agentskills.io receives mirror call.publishfor private does NOT mirror — mock agentskills.io receives 0 calls.publishemits plugin.published audit — memory row exists with kind='plugin.published'.- plugin.published body has version + sha256 + rekor_uuid + visibility — body field check.
- Out-of-order version rejected — publish 1.0.1; publish 1.0.0 next; second fails.
- Yank hides from default search — publish; yank;
cyberos-plugin listdoes not show yanked. - Yanked plugin still installable by SHA-256 — install by hash succeeds.
- Vetted badge persists on info —
cyberos-plugin info <id>@<version>shows vetted: true when token verified. - Bad vetted badge token rejected — JWT signed by wrong key; publish stores but
infoshows vetted: false with reason. - Private plugin cross-tenant install denied — tenant B trying to install tenant A's private plugin fails.
- Enterprise plugin appears only at enterprise origin —
plugins.cyberskill.worlddoes NOT list it;plugins.acme.cyberskill.worlddoes. - revenue_share_percent < 70 triggers warning — publish-time stderr warning; publish still succeeds.
- revenue_share_percent > 100 rejected — schema + publish double-check fails.
- Mirror failure is soft — agentskills.io 500s; publish still succeeds; mirror row queued in retry table.
- Publish audit row scrubbed — body MUST NOT contain bundle bytes (only sha256).
inforeturns visibility, vetted status, version list — RPC works.list --vettedfilters to vetted-only — query semantics.- OCI blob has correct media type —
application/vnd.cyberskill.plugin.v1+zip. - Publish requires OAuth-PKCE bearer token — anonymous publish fails 401.
§5 — Verification
// services/plugin-host/tests/publish_invariant_check_test.rs
#[tokio::test]
async fn publish_rejects_unsigned_bundle() {
let bundle = pack_bundle_without_signature().await;
let result = publish(&bundle, Visibility::Public, &mock_registry_url(), false, None).await;
assert!(matches!(result, Err(PublishError::InvariantViolation(_))));
}
#[tokio::test]
async fn publish_rejects_broken_doctor() {
let bundle = pack_bundle_with_wrong_tool_naming().await;
let result = publish(&bundle, Visibility::Public, &mock_registry_url(), false, None).await;
assert!(matches!(result, Err(PublishError::InvariantViolation(_))));
}
// services/plugin-host/tests/publish_mirrors_to_agentskills_test.rs
#[tokio::test]
async fn public_publish_mirrors() {
let agentskills_mock = MockAgentSkillsServer::start().await;
let bundle = pack_valid_bundle().await;
publish(&bundle, Visibility::Public, &mock_registry_url(), true,
Some("https://localhost/agentskills")).await.unwrap();
let calls = agentskills_mock.recorded_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].path, "/v1/skills/publish");
}
#[tokio::test]
async fn private_publish_does_not_mirror() {
let agentskills_mock = MockAgentSkillsServer::start().await;
let bundle = pack_valid_bundle().await;
publish(&bundle, Visibility::Private, &mock_registry_url(), true, None).await.unwrap();
assert_eq!(agentskills_mock.recorded_calls().len(), 0);
}
// services/plugin-host/tests/publish_emits_audit_test.rs
#[tokio::test]
async fn publish_emits_audit_row() {
let ctx = TestContext::with_memory_mock().await;
let bundle = pack_valid_bundle().await;
publish(&bundle, Visibility::Public, &ctx.registry, false, None).await.unwrap();
let rows = ctx.memory.fetch_rows(kind="plugin.published").await;
assert_eq!(rows.len(), 1);
let body = &rows[0]["body"];
assert_eq!(body["plugin_id"], "cyberos");
assert_eq!(body["version"], "1.0.0");
assert_eq!(body["visibility"], "public");
assert!(body["sha256"].as_str().unwrap().len() == 64);
assert!(body["rekor_uuid"].is_string());
// body MUST NOT contain bundle bytes
let body_str = serde_json::to_string(body).unwrap();
assert!(!body_str.contains("PK\x03\x04")); // zip magic
}
// services/plugin-host/tests/private_visibility_scoped_to_tenant_test.rs
#[tokio::test]
async fn cross_tenant_private_install_denied() {
let ctx_a = TestContext::for_tenant("a").await;
let bundle = ctx_a.pack_and_publish(Visibility::Private).await;
let ctx_b = TestContext::for_tenant("b").await;
let result = ctx_b.install_by_id(&bundle.plugin_id, &bundle.version).await;
assert!(matches!(result, Err(InstallError::NotFound)));
}
§6 — Implementation skeleton
(API contract + Postgres schema in §3 are the skeleton. Full marketplace server implementation deferred to task-PLUGIN-008a, which fleshes out search, browse UI, billing integration with TASK-TEN-005, and admin review tooling.)
§7 — Dependencies
- Upstream: TASK-PLUGIN-006 (audit emission for plugin.published / plugin.yanked); TASK-PLUGIN-007 (per-target adapters produce the bundles being published).
- Downstream: TASK-TEN-005 (vertical pack pricing — paid plugins billed via TEN service); TASK-PORTAL-005 (branded Genie chat — enterprise marketplaces appear in branded portal); task-PLUGIN-008a (full marketplace server: search, UI, billing).
- Cross-module: Strategy §4 Levels 1, 3, 4, 5 — every distribution-facing milestone in the strategy depends on this surface shipping at least at the publish-CLI level.
§8 — Example payloads
Publish result
{
"plugin_id": "cyberos",
"version": "1.0.0",
"sha256": "a1b2c3d4...",
"rekor_uuid": "24296fb24b8ad77a...",
"mirror_status": "mirrored",
"vetted_badge": true,
"registry_url": "https://plugins.cyberskill.world/cyberos/1.0.0"
}
plugin.published audit body
{
"plugin_id": "cyberos",
"version": "1.0.0",
"sha256": "a1b2c3d4...",
"rekor_uuid": "24296fb...",
"visibility": "public",
"vetted_badge": true,
"mirror_targets": ["agentskills.io"],
"size_bytes": 184320,
"trace_id": "01HX..."
}
Vetted badge JWT (decoded)
{
"iss": "https://plugins.cyberskill.world",
"aud": "plugin:cyberos:1.0.0",
"sub": "reviewer-uuid-...",
"iat": 1748000000,
"exp": 1755800000,
"review_id": "REV-2026-042",
"review_date": "2026-05-19",
"findings_summary": "All TASK-PLUGIN-001..007 invariants pass; manual review passed."
}
§9 — Open questions
All resolved.
- ~~Should the marketplace server ship in this task?~~ → No. CLI publish + registry scaffolding in this task; full server (search, UI, billing) in task-PLUGIN-008a.
- ~~Should agentskills.io mirror be optional (off-by-default for public)?~~ → No, on by default per clause 1. Strategy §4 Level 1 OSS-distribution depends on agentskills.io presence.
- ~~Should revenue_share allow > 70 for author (more generous)?~~ → Yes (schema 0-100); only < 70 triggers warning. Author getting more than 70 is fine.
- ~~Should we support arbitrary review chains (multiple vetters)?~~ → Deferred to task-PLUGIN-008b; v1 is single CyberSkill reviewer.
§10 — Failure modes inventory
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Bundle doctor invariants fail | publish.rs step 1 | exit 1 with details | Re-pack bundle correctly |
| Sigstore anchor mismatch | sigstore::verify | exit 1 | Re-sign bundle |
| Registry unreachable | OCI client | exit 1 | Retry with backoff (manual) |
| agentskills.io unreachable | mirror call | soft fail; outbox queue | Mirror retries via TASK-PLUGIN-006 outbox |
| Out-of-order version | registry monotonicity check | exit 1 | Use higher version number |
| Yank without permission | registry RBAC | exit 1 | Owner-or-admin only |
| Vetted badge token wrong sig | badge_verify | publish proceeds; badge: false; warning | Get badge re-issued |
| Vetted badge token expired | badge_verify exp check | badge: false | Renew via review process |
| Private plugin published to public origin | visibility check | exit 1 | Use visibility=private + correct registry |
| Enterprise plugin to wrong enterprise origin | origin check | exit 1 | Use the correct enterprise registry URL |
| Bundle byte change after sign | sigstore re-verify | exit 1 | Re-sign |
| Cross-tenant private install | registry RLS | 404 | inherent |
| revenue_share > 100 | schema + double-check | exit 1 | Author fixes |
| Audit emission fails | TASK-PLUGIN-006 outbox path | publish succeeds; audit queued | Inherent retry |
| Publisher token lacks scope | OAuth scope check | 401 | Re-authenticate with publish scope |
§11 — Implementation notes
- §11.1 Why scaffold the marketplace server here. Full server is task-PLUGIN-008a (registry API, search UI, browse, install metrics, billing). This task ships the CLI side + Postgres schema so the publish path is operational. Server implementation can land independently because schema is locked.
- §11.2 OCI push library. Rust
oci-distributioncrate (4.x) handles the OCI v1.1 spec including blobs, manifests, tags. Adapter wraps it for the CyberSkill-specific media types.
- §11.3 agentskills.io API key. CyberSkill-org-level API key stored in AWS Secrets Manager at
plugins.cyberskill.world/agentskills_api_key. Fetched once at process start; rotated quarterly.
- §11.4 Vetted badge token issuance flow. Out of scope of this task; manual at
https://plugins.cyberskill.world/admin/vetfor v1. Future automation: scan bundle for known-bad patterns (task-PLUGIN-008b).
- §11.5 Enterprise origin pattern.
plugins.<enterprise-name>.cyberskill.world— DNS managed by CyberSkill; routed to the same OCI backend with tenant_id filter. Enterprise tenant_id derived from origin.
- §11.6 Mirror retry semantics. Mirror failures populate the TASK-PLUGIN-006 outbox with kind=
plugin.mirror_retry(special variant). Retry worker checks every 5 minutes. Mirror is best-effort, not blocking.
- §11.7 Why
yankinstead ofdelete. Yank model from crates.io. Permanent deletion breaks reproducibility of historical installs. Yank hides from discovery but preserves bytes. The bundle bytes can be force-deleted by admin (DSAR or court order); that emits a separateplugin.deletedaudit kind.
- §11.8 Revenue billing integration. TASK-TEN-005 receives memory audit
plugin.invokedrows from paid plugins; bills the user tenant; pays the author tenant perrevenue_share_percent. This task ships the data; TEN does the money.
- §11.9 Why CyberSkill 30% (not lower). Below 20% the marketplace cannot fund: hosting, security review, fraud detection, OAuth issuance, audit chain storage. Above 30% authors balk. 30% is the equilibrium.
- §11.10 CLI ergonomics.
cyberos-plugin publish dist/cyberos-1.0.0.plugin --visibility publicis the canonical command. Defaults are aggressive (mirror on, OAuth-PKCE handshake automatic) so authors don't need to manage flags.
End of TASK-PLUGIN-008 spec.