registry: initial layout, PR template, CI validation, frozen v1 schema mirror
Some checks are pending
validate-listings / validate (push) Waiting to run
Some checks are pending
validate-listings / validate (push) Waiting to run
This commit is contained in:
commit
9b9ebcc960
9 changed files with 473 additions and 0 deletions
14
.forgejo/workflows/validate.yml
Normal file
14
.forgejo/workflows/validate.yml
Normal 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
|
||||||
18
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
18
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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
30
README.md
Normal 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
0
audit/.gitkeep
Normal file
0
listings/.gitkeep
Normal file
0
listings/.gitkeep
Normal file
2
schemas/README.md
Normal file
2
schemas/README.md
Normal 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
129
schemas/listing.schema.json
Normal 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
|
||||||
|
}
|
||||||
239
schemas/solution.schema.json
Normal file
239
schemas/solution.schema.json
Normal 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
41
scripts/validate.py
Normal 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())
|
||||||
Loading…
Reference in a new issue