registry: initial layout, PR template, CI validation, frozen v1 schema mirror
Some checks are pending
validate-listings / validate (push) Waiting to run

This commit is contained in:
m2 (AI Agent) 2026-07-02 02:29:47 +02:00
commit 9b9ebcc960
9 changed files with 473 additions and 0 deletions

View file

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

View file

@ -0,0 +1,18 @@
## Listing PR — <listing_id>
### Evidence (required)
<!-- provenance: where this outcome was proven; machine/session refs; tokens/time cost -->
### Price
<!-- amount in m2cr; pricing model: fixed -->
### Permissions diff
<!-- what the solution requests on the buyer's machine -->
### Rollback
<!-- how an applied install is reverted (recipe rollback notes) -->
### 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

30
README.md Normal file
View file

@ -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_id>/ 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 `<listing_id>-v<semver>`.
## Publish protocol
branch `listing/<listing_id>` → 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.

0
audit/.gitkeep Normal file
View file

0
listings/.gitkeep Normal file
View file

2
schemas/README.md Normal file
View file

@ -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).

129
schemas/listing.schema.json Normal file
View file

@ -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
}

View file

@ -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"]
}
}

41
scripts/validate.py Normal file
View file

@ -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())