From 992bee21006986b6f67f687a0bbd6bebb98274e6 Mon Sep 17 00:00:00 2001 From: Piers Date: Sat, 27 Jun 2026 23:28:15 +1000 Subject: [PATCH] feat(catalog): enforce add-on admission gate + carry provisions_scopes Mirror the registry admission gate at catalog-build time so violations fail CI before R2 upload. Add-on sources must be first-party, declare scopes within the reviewed allowlist, and keep credential scopes a subset of the provisions_scopes envelope. - build-catalog.js: assertAddOnAdmission() on add_on entries; carry provisions_scopes through addOnToCatalogEntry into the served catalog. - catalog-entry schema: add provisions_scopes. - sync vendored add-on schema. Co-Authored-By: Claude Opus 4.8 --- schemas/catalog-entry.schema.json | 5 + schemas/stallari-add-on.schema.json | 288 ++++++++++++---------------- scripts/build-catalog.js | 64 +++++++ 3 files changed, 196 insertions(+), 161 deletions(-) diff --git a/schemas/catalog-entry.schema.json b/schemas/catalog-entry.schema.json index 97d3304..de2a6ef 100644 --- a/schemas/catalog-entry.schema.json +++ b/schemas/catalog-entry.schema.json @@ -336,6 +336,11 @@ "ships_software_to_external": { "const": false } } }, + "provisions_scopes": { + "type": "array", + "description": "DD-404 add_on: the reviewed envelope of auth scopes this add-on may mint across all its credentials — the surface reviewed at the gated first-party admission lane. Every provisions_auth scope is a subset of this set.", + "items": { "type": "string" } + }, "provisions_auth": { "type": "array", "description": "DD-404 add_on: scoped credentials this add-on mints (least-privilege bearer per DD-186). Catalog summary of the manifest provisions_auth[].", diff --git a/schemas/stallari-add-on.schema.json b/schemas/stallari-add-on.schema.json index d947dfa..318684c 100644 --- a/schemas/stallari-add-on.schema.json +++ b/schemas/stallari-add-on.schema.json @@ -1,212 +1,178 @@ { - "$id": "https://stallari.app/schemas/stallari-add-on.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": false, + "$id": "https://stallari.app/schemas/stallari-add-on.schema.json", + "title": "Stallari Add-on Manifest", "description": "Schema for stallari-add-on.yaml — the manifest format for Stallari add-ons. An add-on is the TERTIARY Marketplace artifact class (after packs and plugins/MCPs, DD-404): a Stallari-side module that unhides otherwise-hidden integration UI and provisions scoped auth for an external system or directly-attached hardware. An add-on ships NO software to the external system (enforced: external_system.ships_software_to_external is const false). It is distinct from a pack (ships no skills/workloads) and a plugin/MCP (exposes no LLM tool surface). Instances: Sovereign Compute, Elgato Stream Deck.", + "type": "object", + "required": ["add_on", "name", "version", "description", "author", "license", "external_system", "provisions_scopes"], + "additionalProperties": false, "properties": { "add_on": { - "description": "Add-on manifest spec version (e.g. '1.0'). Discriminates this artifact as an add-on, mirroring the pack manifest's `pack:` field.", + "type": "string", "pattern": "^\\d+\\.\\d+$", - "type": "string" + "description": "Add-on manifest spec version (e.g. '1.0'). Discriminates this artifact as an add-on, mirroring the pack manifest's `pack:` field." }, - "author": { - "description": "Add-on author identifier.", - "type": "string" + "name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$", + "description": "Add-on identifier. Lowercase alphanumeric with hyphens (e.g. 'elgato-streamdeck', 'sovereign-compute')." }, - "author_type": { - "description": "Author provenance. first-party = published and maintained by Stallari (the common case for add-ons, which provision auth).", - "enum": [ - "first-party", - "community" - ], - "type": "string" + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "Semantic version (major.minor.patch)." }, "description": { - "description": "Human-readable summary of what the add-on reveals and provisions.", + "type": "string", "maxLength": 200, - "type": "string" + "description": "Human-readable summary of what the add-on reveals and provisions." + }, + "author": { + "type": "string", + "description": "Add-on author identifier." + }, + "license": { + "type": "string", + "description": "SPDX license identifier (e.g. MIT, Apache-2.0)." + }, + "author_type": { + "type": "string", + "enum": ["first-party", "community"], + "description": "Author provenance. first-party = published and maintained by Stallari (the common case for add-ons, which provision auth)." + }, + "readiness": { + "type": "string", + "enum": ["production", "beta", "experimental", "stub"], + "description": "Testing completeness. production = fully tested. beta = partially tested. experimental = untested. stub = scaffolded but not functional." + }, + "icon": { + "type": "string", + "description": "Relative URL path to icon asset for marketplace display." + }, + "homepage": { + "type": "string", + "format": "uri", + "description": "Add-on homepage or repository URL." + }, + "repository": { + "type": "string", + "format": "uri", + "description": "Source repository URL." + }, + "notes": { + "type": "string", + "description": "Free-form notes (curation status, caveats, context)." + }, + "requires": { + "type": "object", + "additionalProperties": false, + "description": "Dependency requirements.", + "properties": { + "stallari": { + "type": "string", + "pattern": "^[><=!~^]+\\d+\\.\\d+\\.\\d+", + "description": "Minimum Stallari version (e.g. >=0.99.0)." + } + } }, "external_system": { + "type": "object", "additionalProperties": false, + "required": ["kind", "ships_software_to_external"], "description": "The external system or directly-attached hardware this add-on bridges. The add-on provisions auth and reveals UI for it but never installs software onto it.", "properties": { - "consumer_ref": { - "description": "Human-readable reference (and ideally a link) to the external consumer that receives the provisioned auth — e.g. 'Elgato Stream Deck plugin (Elgato Marketplace)'. The add-on links to it; it never bundles or installs it.", - "type": "string" - }, "kind": { - "description": "Nature of the external system. attached-hardware = a USB/peripheral device (e.g. Elgato Stream Deck). cloud-endpoint = a remote compute/service endpoint (e.g. Sovereign Compute BYO box). external-app = a third-party application.", - "enum": [ - "attached-hardware", - "cloud-endpoint", - "external-app" - ], - "type": "string" + "type": "string", + "enum": ["attached-hardware", "cloud-endpoint", "external-app"], + "description": "Nature of the external system. attached-hardware = a USB/peripheral device (e.g. Elgato Stream Deck). cloud-endpoint = a remote compute/service endpoint (e.g. Sovereign Compute BYO box). external-app = a third-party application." + }, + "vendor": { + "type": "string", + "description": "External system vendor (e.g. 'elgato')." }, "product": { - "description": "External system product (e.g. 'stream-deck').", - "type": "string" + "type": "string", + "description": "External system product (e.g. 'stream-deck')." + }, + "consumer_ref": { + "type": "string", + "description": "Human-readable reference (and ideally a link) to the external consumer that receives the provisioned auth — e.g. 'Elgato Stream Deck plugin (Elgato Marketplace)'. The add-on links to it; it never bundles or installs it." }, "ships_software_to_external": { "const": false, "description": "Class invariant (DD-404): an add-on MUST NOT ship software to the external system — it only reveals Stallari-side UI and provisions auth. Schema-enforced to false." - }, - "vendor": { - "description": "External system vendor (e.g. 'elgato').", - "type": "string" } - }, - "required": [ - "kind", - "ships_software_to_external" - ], - "type": "object" - }, - "homepage": { - "description": "Add-on homepage or repository URL.", - "format": "uri", - "type": "string" + } }, - "icon": { - "description": "Relative URL path to icon asset for marketplace display.", - "type": "string" - }, - "license": { - "description": "SPDX license identifier (e.g. MIT, Apache-2.0).", - "type": "string" - }, - "lifecycle": { - "additionalProperties": false, - "description": "Optional lifecycle hook descriptors. Enabling reveals UI + readies provisioning; disabling/removing must revoke provisioned credentials.", - "properties": { - "on_disable": { - "description": "What happens on disable.", - "type": "string" - }, - "on_enable": { - "description": "What happens on enable (reveal UI, ready provisioning).", - "type": "string" - }, - "on_revoke": { - "description": "What happens on revoke — MUST kill credentials provisioned via provisions_auth.", - "type": "string" - } - }, - "type": "object" - }, - "name": { - "description": "Add-on identifier. Lowercase alphanumeric with hyphens (e.g. 'elgato-streamdeck', 'sovereign-compute').", - "pattern": "^[a-z0-9][a-z0-9-]*$", - "type": "string" - }, - "notes": { - "description": "Free-form notes (curation status, caveats, context).", - "type": "string" + "provisions_scopes": { + "type": "array", + "uniqueItems": true, + "description": "The complete, reviewed envelope of auth scopes this add-on is permitted to mint across ALL of its provisioned credentials. This is the single surface groupthink reviews at catalog admission: the gated first-party lane rejects any add-on whose declared scopes fall outside the reviewed allowlist, and rejects any credential in provisions_auth whose scopes are not a subset of this envelope. An add-on that mints nothing declares an empty array. Each scope is a dotted, lowercase capability string (e.g. 'workload.trigger', 'inference.invoke').", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$" + } }, "provisions_auth": { - "description": "Scoped credentials this add-on is permitted to mint (Stallari-side, via the DD-186 CredentialStore) and emit as a connection bundle for the external consumer. Least-privilege; revocable add-on-side.", + "type": "array", + "description": "Scoped credentials this add-on is permitted to mint (Stallari-side, via the DD-186 CredentialStore) and emit as a connection bundle for the external consumer. Least-privilege; revocable add-on-side. Every scope listed here MUST also appear in provisions_scopes — the reviewed envelope is the superset; admission enforces the subset relation.", "items": { + "type": "object", "additionalProperties": false, + "required": ["id", "kind"], "properties": { "id": { - "description": "Credential identifier (e.g. 'console-bearer').", + "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", - "type": "string" + "description": "Credential identifier (e.g. 'console-bearer')." }, "kind": { - "description": "Credential type provisioned for the external consumer.", - "enum": [ - "bearer", - "token", - "none" - ], - "type": "string" + "type": "string", + "enum": ["bearer", "token", "none"], + "description": "Credential type provisioned for the external consumer." + }, + "scopes": { + "type": "array", + "items": { "type": "string" }, + "description": "Least-privilege scopes granted to the provisioned credential (e.g. ['workload.trigger', 'hitl.approve', 'status.read'])." }, "revocable": { + "type": "boolean", "default": true, - "description": "Whether the add-on can revoke this credential (killing the external consumer's access) independent of the consumer's state. Should be true.", - "type": "boolean" - }, - "scopes": { - "description": "Least-privilege scopes granted to the provisioned credential (e.g. ['workload.trigger', 'hitl.approve', 'status.read']).", - "items": { - "type": "string" - }, - "type": "array" + "description": "Whether the add-on can revoke this credential (killing the external consumer's access) independent of the consumer's state. Should be true." } - }, - "required": [ - "id", - "kind" - ], - "type": "object" - }, - "type": "array" - }, - "readiness": { - "description": "Testing completeness. production = fully tested. beta = partially tested. experimental = untested. stub = scaffolded but not functional.", - "enum": [ - "production", - "beta", - "experimental", - "stub" - ], - "type": "string" - }, - "repository": { - "description": "Source repository URL.", - "format": "uri", - "type": "string" - }, - "requires": { - "additionalProperties": false, - "description": "Dependency requirements.", - "properties": { - "stallari": { - "description": "Minimum Stallari version (e.g. >=0.99.0).", - "pattern": "^[><=!~^]+\\d+\\.\\d+\\.\\d+", - "type": "string" } - }, - "type": "object" + } }, "reveals_ui": { + "type": "array", "description": "Otherwise-hidden Stallari UI surfaces this add-on unhides when enabled (so users without the add-on see no clutter).", "items": { + "type": "object", "additionalProperties": false, + "required": ["surface"], "properties": { - "description": { - "description": "What the revealed surface lets the user do.", - "type": "string" - }, "surface": { - "description": "Named UI surface identifier the app reveals (e.g. 'stream-deck-console', 'sovereign-compute').", + "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", - "type": "string" + "description": "Named UI surface identifier the app reveals (e.g. 'stream-deck-console', 'sovereign-compute')." + }, + "description": { + "type": "string", + "description": "What the revealed surface lets the user do." } - }, - "required": [ - "surface" - ], - "type": "object" - }, - "type": "array" + } + } }, - "version": { - "description": "Semantic version (major.minor.patch).", - "pattern": "^\\d+\\.\\d+\\.\\d+$", - "type": "string" + "lifecycle": { + "type": "object", + "additionalProperties": false, + "description": "Optional lifecycle hook descriptors. Enabling reveals UI + readies provisioning; disabling/removing must revoke provisioned credentials.", + "properties": { + "on_enable": { "type": "string", "description": "What happens on enable (reveal UI, ready provisioning)." }, + "on_disable": { "type": "string", "description": "What happens on disable." }, + "on_revoke": { "type": "string", "description": "What happens on revoke — MUST kill credentials provisioned via provisions_auth." } + } } - }, - "required": [ - "add_on", - "name", - "version", - "description", - "author", - "license", - "external_system" - ], - "title": "Stallari Add-on Manifest", - "type": "object" + } } diff --git a/scripts/build-catalog.js b/scripts/build-catalog.js index 8e37b22..af89826 100644 --- a/scripts/build-catalog.js +++ b/scripts/build-catalog.js @@ -578,6 +578,61 @@ function packToCatalogEntry(pack) { }; } +// Reviewed scope allowlist for the gated first-party add-on admission lane. +// The allowlist IS the review artifact: adding a scope is a reviewed change. +// Mirrors the canonical gate in the registry worker (validate.ts). +const REVIEWED_ADDON_SCOPES = new Set([ + "workload.trigger", + "hitl.approve", + "status.read", + "inference.invoke", +]); +const ADDON_SCOPE_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/; + +/** + * Build-time admission gate for add-ons (CI fail-fast mirror of the registry + * worker). Add-ons provision scoped auth, so they go through a first-party + * gated lane: first-party only, scopes within the reviewed allowlist, and every + * credential scope a subset of the declared provisions_scopes envelope. + * Throws on violation so the catalog build fails before R2 upload. + */ +function assertAddOnAdmission(raw) { + const errors = []; + if (raw.author_type !== "first-party") { + errors.push( + 'add-ons are admitted only through the first-party gated lane (author_type: "first-party"); third-party add-ons are not yet accepted', + ); + } + const envelope = new Set( + Array.isArray(raw.provisions_scopes) ? raw.provisions_scopes : [], + ); + if (!Array.isArray(raw.provisions_scopes)) { + errors.push("provisions_scopes (reviewed scope envelope) is required"); + } else { + for (const scope of raw.provisions_scopes) { + if (!ADDON_SCOPE_PATTERN.test(String(scope))) { + errors.push(`invalid scope format: "${scope}"`); + } else if (!REVIEWED_ADDON_SCOPES.has(String(scope))) { + errors.push(`scope "${scope}" is outside the reviewed allowlist`); + } + } + } + for (const cred of Array.isArray(raw.provisions_auth) ? raw.provisions_auth : []) { + for (const scope of Array.isArray(cred.scopes) ? cred.scopes : []) { + if (!envelope.has(String(scope))) { + errors.push( + `credential "${cred.id}" mints scope "${scope}" not declared in provisions_scopes`, + ); + } + } + } + if (errors.length) { + throw new Error( + `Add-on "${raw.name}" fails admission:\n - ${errors.join("\n - ")}`, + ); + } +} + /** * DD-404 — Convert an add-on catalog-entry source to a CatalogEntry. * Add-ons reveal UI + provision scoped auth for an external system; they ship @@ -608,6 +663,10 @@ function addOnToCatalogEntry(raw) { license: raw.license || null, // DD-404 add-on-specific surface external_system: raw.external_system || null, + provisions_scopes: + Array.isArray(raw.provisions_scopes) && raw.provisions_scopes.length > 0 + ? raw.provisions_scopes + : null, provisions_auth: Array.isArray(raw.provisions_auth) && raw.provisions_auth.length > 0 ? raw.provisions_auth @@ -872,6 +931,11 @@ async function main() { `Catalog entry ${raw.name || file} fails schema validation:\n${verdict.errorsText}`, ); } + // Add-ons provision scoped auth — enforce the gated first-party + // admission lane (CI fail-fast mirror of the registry worker). + if (type === "add_on") { + assertAddOnAdmission(raw); + } entries.push(convert(raw)); } }