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