commit 9b9ebcc96016b994b027f35ac8c756c04938183a Author: m2 (AI Agent) Date: Thu Jul 2 02:29:47 2026 +0200 registry: initial layout, PR template, CI validation, frozen v1 schema mirror diff --git a/.forgejo/workflows/validate.yml b/.forgejo/workflows/validate.yml new file mode 100644 index 0000000..a3cb5be --- /dev/null +++ b/.forgejo/workflows/validate.yml @@ -0,0 +1,14 @@ +name: validate-listings +on: + pull_request: + push: + branches: [main] +jobs: + validate: + runs-on: docker + container: + image: python:3.12-slim + steps: + - uses: actions/checkout@v4 + - run: pip install --quiet jsonschema + - run: python3 scripts/validate.py diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..37bcfad --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## Listing PR — + +### Evidence (required) + + +### Price + + +### Permissions diff + + +### Rollback + + +### Attestations +- [ ] No secrets or client identifiers anywhere in this PR or the bundle +- [ ] `content_hash` matches the release asset +- [ ] If tenant-derived (`tenant_scope` != m2-core): owner-initiated AND double-scrubbed diff --git a/README.md b/README.md new file mode 100644 index 0000000..c50e809 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# m2/market-registry — M2 Marketplace registry of record + +Truth for marketplace listings and ledger audit snapshots. Everything downstream +(the `market:catalog` semantic index, the M2 Store, the `m2-market` CLI) is DERIVED from +this repo and rebuildable from it. + +Contract: `m2-market` repo, `specs/001-market-first-wedge/contracts/registry-layout.md`. + +## Layout + +``` +listings// listing.json + solution.json + evidence/ +audit/YYYY-MM-DD.json daily ledger balance snapshots (m2-ledger /snapshot) +schemas/ MIRROR of the frozen v1 schemas (source of truth: m2-market repo) +scripts/validate.py validation used by CI and locally +``` + +Solution bundles attach as repo releases tagged `-v`. + +## Publish protocol + +branch `listing/` → PR (template enforces evidence/price/permissions/rollback) +→ CI validation (hard gate) → human review window → label `veto` = rejected, label +`approved` + merge = published. Delist = PR setting `status: "delisted"` (licenses stay valid). + +## Rules + +- No secrets, ever. Evidence is scrubbed before commit. +- Tenant-derived listings require owner-initiated + double-scrub attestations in the PR body. +- On index divergence the registry wins: `m2-market-indexer reindex` rebuilds the catalog. diff --git a/audit/.gitkeep b/audit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/listings/.gitkeep b/listings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..f9d8245 --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,2 @@ +Frozen v1 schemas — MIRROR. Source of truth: m2-market repo `schemas/`. +Update only by syncing from there (schema changes require a version bump per the contract). diff --git a/schemas/listing.schema.json b/schemas/listing.schema.json new file mode 100644 index 0000000..78db109 --- /dev/null +++ b/schemas/listing.schema.json @@ -0,0 +1,129 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.machinemachine.ai/m2/market-registry/schemas/listing.schema.json", + "title": "Listing", + "description": "Catalog-facing record for a marketplace listing. Truth lives in m2/market-registry; derived copy indexed into market:catalog.", + "type": "object", + "properties": { + "schema_version": { + "const": "m2.listing.v1" + }, + "listing_id": { + "type": "string", + "pattern": "^lst_[a-z0-9-]+$" + }, + "solution_id": { + "type": "string", + "pattern": "^sol_[a-z0-9-]+$" + }, + "solution_version": { + "type": "string" + }, + "inventory_type": { + "type": "string", + "enum": ["solution", "capability", "resource", "service"] + }, + "name": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "category": { + "type": "string" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "type": "string" + }, + "price": { + "type": "object", + "description": "Denormalized from solution at listing time.", + "properties": { + "amount": { + "type": "integer", + "minimum": 1 + }, + "currency": { + "const": "m2cr" + }, + "model": { + "type": "string" + } + }, + "required": ["amount", "currency", "model"], + "additionalProperties": false + }, + "seller": { + "type": "string", + "description": "operator_id" + }, + "evidence_summary": { + "type": "string" + }, + "tenant_visibility": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "stats": { + "type": "object", + "description": "Conversion signals (FR-012), updated by telemetry, start zeroed.", + "properties": { + "installs": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "rating": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "proposals_shown": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "proposals_accepted": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + "required": ["installs", "proposals_shown", "proposals_accepted"], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": ["draft", "in_review", "published", "delisted"] + }, + "install_ref": { + "type": "string", + "pattern": "^lst_[a-z0-9-]+-v\\d+\\.\\d+\\.\\d+$" + } + }, + "required": [ + "schema_version", + "listing_id", + "solution_id", + "solution_version", + "inventory_type", + "name", + "summary", + "category", + "price", + "seller", + "tenant_visibility", + "status", + "install_ref" + ], + "additionalProperties": false +} diff --git a/schemas/solution.schema.json b/schemas/solution.schema.json new file mode 100644 index 0000000..9b905fa --- /dev/null +++ b/schemas/solution.schema.json @@ -0,0 +1,239 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.machinemachine.ai/m2/market-registry/schemas/solution.schema.json", + "title": "Solution", + "description": "The installable bundle manifest for the m2 marketplace. Frozen v1 contract (specs/001-market-first-wedge/data-model.md, research.md R3).", + "type": "object", + "properties": { + "schema_version": { + "const": "m2.solution.v1" + }, + "solution_id": { + "type": "string", + "pattern": "^sol_[a-z0-9-]+$" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "intent": { + "type": "string", + "minLength": 1 + }, + "behavior": { + "type": "object" + }, + "tools": { + "type": "array", + "items": { + "type": "object" + } + }, + "runtime": { + "type": "object" + }, + "memory_schema": { + "type": "object" + }, + "permissions": { + "type": "array", + "items": { + "type": "object" + } + }, + "deployment": { + "type": "object", + "description": "recipe.yaml ref + entrypoint + verify command", + "properties": { + "recipe_ref": { + "type": "string", + "minLength": 1 + }, + "entrypoint": { + "type": "string", + "minLength": 1 + }, + "verify_command": { + "type": "string", + "minLength": 1 + } + }, + "required": ["recipe_ref", "entrypoint", "verify_command"], + "additionalProperties": false + }, + "applicability": { + "type": "object", + "description": "fedlearn-compatible applicability constraints", + "properties": { + "image_classes": { + "type": "array", + "items": { + "type": "string" + } + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool_requirements": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "tenant_scope": { + "type": "string", + "minLength": 1, + "description": "'m2-core' for shared/free, or a tenant id for tenant-scoped work" + }, + "owner_initiated": { + "type": "boolean" + }, + "scrub_status": { + "type": "object", + "properties": { + "double_scrubbed": { + "type": "boolean" + } + }, + "additionalProperties": true + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "source": { + "type": "string", + "minLength": 1 + }, + "excerpt": { + "type": "string" + }, + "machine": { + "type": "string" + }, + "session": { + "type": "string" + }, + "tokens": { + "type": "integer", + "minimum": 0 + }, + "wall_time": { + "type": "number", + "minimum": 0 + } + }, + "required": ["source"], + "additionalProperties": false + } + }, + "content_hash": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "price": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "minimum": 1 + }, + "currency": { + "const": "m2cr" + }, + "model": { + "type": "string", + "enum": ["fixed"] + } + }, + "required": ["amount", "currency", "model"], + "additionalProperties": false + }, + "seller": { + "type": "string", + "minLength": 1, + "description": "operator_id" + }, + "license": { + "type": "object", + "properties": { + "terms": { + "type": "string", + "minLength": 1 + }, + "major_version_coverage": { + "const": true + } + }, + "required": ["terms", "major_version_coverage"], + "additionalProperties": false + }, + "revenue_split": { + "type": "object", + "properties": { + "platform_pct": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "additionalProperties": false + } + }, + "required": [ + "schema_version", + "solution_id", + "name", + "summary", + "intent", + "deployment", + "applicability", + "tenant_scope", + "evidence", + "content_hash", + "price", + "seller", + "license" + ], + "additionalProperties": false, + "if": { + "properties": { + "tenant_scope": { + "const": "m2-core" + } + }, + "required": ["tenant_scope"] + }, + "else": { + "properties": { + "owner_initiated": { + "const": true + }, + "scrub_status": { + "type": "object", + "properties": { + "double_scrubbed": { + "const": true + } + }, + "required": ["double_scrubbed"] + } + }, + "required": ["owner_initiated", "scrub_status"] + } +} diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..cbe1ab9 --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Validate all listings against the frozen v1 schemas. + +Used by CI and locally: python3 scripts/validate.py +Exits non-zero on any violation. jsonschema required (pip install jsonschema). +""" +import json +import sys +from pathlib import Path + +from jsonschema import Draft202012Validator + +ROOT = Path(__file__).resolve().parent.parent +SOLUTION = Draft202012Validator(json.loads((ROOT / "schemas/solution.schema.json").read_text())) +LISTING = Draft202012Validator(json.loads((ROOT / "schemas/listing.schema.json").read_text())) + +def main() -> int: + failures = [] + for d in sorted((ROOT / "listings").iterdir()): + if not d.is_dir(): + continue + for name, validator in (("listing.json", LISTING), ("solution.json", SOLUTION)): + f = d / name + if not f.exists(): + failures.append(f"{d.name}: missing {name}") + continue + errors = sorted(validator.iter_errors(json.loads(f.read_text())), key=str) + failures += [f"{d.name}/{name}: {e.json_path}: {e.message}" for e in errors] + if (d / "listing.json").exists(): + listing = json.loads((d / "listing.json").read_text()) + if listing.get("listing_id") != d.name: + failures.append(f"{d.name}: directory name != listing_id") + if failures: + print("VALIDATION FAILED:") + print("\n".join(f" - {f}" for f in failures)) + return 1 + print("all listings valid") + return 0 + +if __name__ == "__main__": + sys.exit(main())