From cd43dac89feee0ab6ff15c43766f9462d5ec04f9 Mon Sep 17 00:00:00 2001 From: "m2 (AI Agent)" Date: Thu, 2 Jul 2026 03:31:01 +0200 Subject: [PATCH] phase0: freeze marketplace contract fixes --- contracts/cli.md | 126 +++++ contracts/deeplink.md | 24 + contracts/ids.md | 22 + contracts/ledger-api.md | 139 ++++++ contracts/telemetry.md | 62 +++ contracts/tenants.md | 34 ++ licenses/m2-market-standard-v1.md | 47 ++ schemas/listing.schema.json | 238 +++++----- schemas/solution.schema.json | 441 +++++++++--------- .../tests/fixtures/listing_bad_status.json | 6 +- .../listing_missing_tenant_visibility.json | 6 +- .../tests/fixtures/solution_bad_price.json | 14 +- .../fixtures/solution_missing_evidence.json | 14 +- .../fixtures/solution_tenant_violation.json | 14 +- schemas/tests/fixtures/valid_listing.json | 10 +- schemas/tests/fixtures/valid_solution.json | 27 +- schemas/tests/test_schemas.py | 17 +- 17 files changed, 897 insertions(+), 344 deletions(-) create mode 100644 contracts/cli.md create mode 100644 contracts/deeplink.md create mode 100644 contracts/ids.md create mode 100644 contracts/ledger-api.md create mode 100644 contracts/telemetry.md create mode 100644 contracts/tenants.md create mode 100644 licenses/m2-market-standard-v1.md diff --git a/contracts/cli.md b/contracts/cli.md new file mode 100644 index 0000000..1d7931c --- /dev/null +++ b/contracts/cli.md @@ -0,0 +1,126 @@ +# m2-market CLI contract v1 + +The CLI is the only marketplace surface that calls m2-ledger directly. Store, Scout, and Hermes +consume the CLI JSON contract. + +Config: `~/.m2-market/config.toml` mode `0600`. + +Required keys: `operator_id`, `tenant`, `ledger_url`, `ledger_api_key`, `memory_api_url`, +`memory_api_key`, `forgejo_url`, `forgejo_token`, `apply_adapter`. + +State: `~/.m2-market/state.json`. Install entries MUST store `grant_id` and `tx_group_id`. + +## Verbs + +- `m2-market search [--type solution|capability|resource|service] [--limit N] [--json]` +- `m2-market show [--json]` +- `m2-market install [--yes] [--json]` +- `m2-market uninstall [--yes] [--json]` +- `m2-market upgrade [--to ] [--yes] [--json]` +- `m2-market wallet [--operator ] [--json]` +- `m2-market publish [--tenant-visibility ]` +- `m2-market package init|import-fedlearn|validate|build|diff-permissions|evidence-summary|verify-release` +- `m2-market reindex` +- `m2-market propose "" --tenant --operator --json` +- `m2-market evidence capture|submit|show` +- `m2-market telemetry submit ` + +## Exit codes + +| Code | Meaning | +| --- | --- | +| 0 | Success, including idempotent no-op install. | +| 1 | Usage error, unsupported operation, or direct install refused for `price.model=job`. | +| 2 | Listing, release, license, or local install state not found. | +| 3 | Insufficient funds or ledger payment declined before apply. | +| 4 | Apply, verify, uninstall, or upgrade failed after settlement; CLI attempted refund when applicable. | +| 5 | Schema validation, tenant firewall, secret scan, permissions, or publish gate rejection. | +| 6 | External dependency unavailable or deferred adapter path not landed. | + +## `show --json` + +The detail payload is frozen for Store and Scout: + +```json +{ + "schema_version": "m2.market.show.v1", + "listing": { + "schema_version": "m2.listing.v1", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "inventory_type": "solution", + "name": "MM PDF Report", + "summary": "Generate branded PDF reports.", + "category": "reporting", + "price": {"model": "fixed", "amount": 25, "currency": "m2cr"}, + "seller": {"operator_id": "op_m2core", "display_name": "M2 Core"}, + "content_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "tenant_visibility": ["*"], + "status": "published", + "install_ref": "lst_mm-pdf-report-v1.0.0" + }, + "solution": { + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Report", + "summary": "Generate branded PDF reports.", + "intent": "Turn approved analysis into a styled client-ready PDF report.", + "deployment": { + "recipe_ref": "recipe.yaml", + "entrypoint": "mm-pdf generate ", + "verify_command": "mm-pdf --version" + }, + "applicability": { + "image_classes": ["primus", "agent-latest"], + "roles": ["operator"], + "tool_requirements": ["bash"] + }, + "tenant_scope": "m2-core", + "evidence": [{"source": "forgejo:m2/m2-market/listings/lst_mm-pdf-report/evidence/provenance.jsonl"}], + "content_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "price": {"model": "fixed", "amount": 25, "currency": "m2cr"}, + "seller": {"operator_id": "op_m2core", "display_name": "M2 Core"}, + "license": { + "license_id": "lic_m2_market_standard_v1", + "terms": "m2-market-standard-v1", + "scope": "operator", + "major_version_coverage": true, + "allows_internal_modification": true, + "allows_redistribution": false, + "support": "best_effort" + } + }, + "wallet": {"operator_id": "op_chris", "balance": 100, "currency": "m2cr", "as_of": "2026-07-02T00:00:00Z"}, + "install_state": { + "status": "not_installed", + "installed_version": null, + "grant_id": null, + "tx_group_id": null + }, + "permissions_diff": { + "schema_version": "m2.permissions_diff.v1", + "mode": "new_install", + "listing_id": "lst_mm-pdf-report", + "current_install_ref": null, + "target_install_ref": "lst_mm-pdf-report-v1.0.0", + "summary": {"added": 2, "removed": 0, "changed": 0, "unchanged": 0, "risk": "medium"}, + "items": [ + { + "permission_id": "fs-write-agent-home", + "action": "added", + "kind": "filesystem", + "access": "write", + "scope": "${AGENT_HOME}/.claude/skills/mm-pdf", + "target": "skill files", + "reason": "install skill files", + "risk": "low", + "requires_confirmation": false + } + ] + } +} +``` + +`permissions_diff.mode` is one of `new_install`, `upgrade`, `reinstall`, `uninstall`, or +`no_change`. `items[].action` is one of `added`, `removed`, `changed`, or `unchanged`. diff --git a/contracts/deeplink.md b/contracts/deeplink.md new file mode 100644 index 0000000..bfaaa63 --- /dev/null +++ b/contracts/deeplink.md @@ -0,0 +1,24 @@ +# M2 Store deep-link contract v1 + +Canonical listing detail URI: + +```text +m2store://listing/?source=&proposal_id=&q= +``` + +Examples: + +```text +m2store://listing/lst_competitor-scan?source=scout&proposal_id=prop_01j2abc +m2store://listing/lst_mm-pdf-report?source=hermes&q=pdf%20report +``` + +Rules: + +- Path segment is `listing`, not `solution`. +- `` uses the `lst_` prefix from `contracts/ids.md`. +- Allowed query parameters are exactly `source`, `proposal_id`, and `q`. +- The URI is navigate-only. It never installs, purchases, grants licenses, or mutates state. +- Unknown listing IDs open Store search/detail error UI without leaking tenant-forbidden records. +- Store may keep an internal route such as `/solution/`, but the external protocol + stays `m2store://listing/`. diff --git a/contracts/ids.md b/contracts/ids.md new file mode 100644 index 0000000..22344a0 --- /dev/null +++ b/contracts/ids.md @@ -0,0 +1,22 @@ +# Marketplace ID prefixes v1 + +Canonical prefixes: + +| Prefix | Object | Example | +| --- | --- | --- | +| `op_` | Operator ID | `op_m2core` | +| `prop_` | Proposal ID | `prop_01j2abc` | +| `lst_` | Listing ID | `lst_mm-pdf-report` | +| `sub_` | Fedlearn submission ID | `sub_01j2candidate` | +| `clu_` | Fedlearn cluster ID | `clu_01j2cluster` | +| `core_` | Promoted core learning ID | `core_01j2learning` | + +Rules: + +- Operators use `op_` everywhere. Bare names such as `m2bd` or `sdjs-operator` are display + names, not marketplace IDs. +- Proposals use `prop_`, never `prp_`. +- Listings use `lst_` and control price, visibility, version, and license grants. +- Solution manifests keep `sol_` for `solution_id`; this is already frozen in schema and is not + a replacement for `lst_` in Store links or ledger refs. +- Ledger transaction groups, grants, accounts, and jobs keep S1-owned prefixes outside this file. diff --git a/contracts/ledger-api.md b/contracts/ledger-api.md new file mode 100644 index 0000000..cda2af7 --- /dev/null +++ b/contracts/ledger-api.md @@ -0,0 +1,139 @@ +# m2-ledger API contract v1 + +S1 owns ledger semantics. Marketplace clients MUST use the `/v1/*` paths below; older +`/tx/install`, `/tx/refund`, `/license/*`, `/licenses/check`, and `/balance/*` paths are not +contractual for m2-market. + +All mutation requests require `Idempotency-Key` or an `idempotency_key` body field. If both are +present, they must match. All amounts are integer `m2cr`. + +## Identity + +`GET /v1/accounts/me` + +Returns the caller's resolved ledger identity: + +```json +{ + "account_id": "acct_op_chris", + "operator_id": "op_chris", + "tenant_id": "m2", + "scopes": ["ledger:read", "ledger:install"] +} +``` + +## Balances + +`GET /v1/balances/{account_or_operator_id}` + +Returns a balance derived from the append-only transaction log. Store and Scout do not call this +directly; they use `m2-market wallet`. + +```json +{ + "account_id": "acct_op_chris", + "operator_id": "op_chris", + "tenant_id": "m2", + "balance": 100, + "currency": "m2cr", + "as_of": "2026-07-02T00:00:00Z" +} +``` + +## Install settlement + +`POST /v1/settlements/install` + +Fixed-price install settlement. Job-priced listings are refused by the CLI before this call and +use the jobs API instead. + +```json +{ + "buyer_operator_id": "op_chris", + "seller_operator_id": "op_m2core", + "tenant_id": "m2", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "major_version": 1, + "install_ref": "lst_mm-pdf-report-v1.0.0", + "price": {"model": "fixed", "amount": 25, "currency": "m2cr"}, + "revenue_split": {"platform_pct": 10, "seller_pct": 90} +} +``` + +Success returns the transaction group and license grant. The CLI MUST persist `tx_group_id` in +`~/.m2-market/state.json` before apply so refund can be addressed by group id. + +```json +{ + "tx_group_id": "txg_01j2install", + "grant_id": "gr_01j2license", + "balance": 75, + "license": { + "operator_id": "op_chris", + "listing_id": "lst_mm-pdf-report", + "major_version": 1, + "status": "active" + } +} +``` + +`402 insufficient_funds` writes no transactions or grant. Repeated idempotency keys return the +original settlement. + +## Refund on apply failure + +`POST /v1/settlements/install/{tx_group_id}/refund` + +Refunds by `tx_group_id`, not by listing ref. Refunds are compensating append-only rows and mark +the related license grant `refunded`. + +```json +{ + "reason": "apply_failed", + "apply_error": "verify command failed", + "listing_id": "lst_mm-pdf-report" +} +``` + +## Licenses + +`GET /v1/licenses/{operator_id}/{listing_id}?major_version=1` + +Returns the active/refunded/revoked grant for `(operator_id, listing_id, major_version)`, or `404` +for not found or forbidden. + +## Starter grants + +`POST /v1/grants/starter` + +Admin/service scoped. Default amount is `100 m2cr`. + +## Jobs + +`POST /v1/jobs` + +Creates a funded job/escrow for job-priced listings. Job settlements use ledger `tx_type=job`. + +Additional job paths: + +- `POST /v1/jobs/{job_id}/start` +- `POST /v1/jobs/{job_id}/milestones/{milestone_id}/submit` +- `POST /v1/jobs/{job_id}/milestones/{milestone_id}/accept` +- `POST /v1/jobs/{job_id}/cancel` +- `POST /v1/jobs/{job_id}/dispute` + +## Route/resource settlement + +`POST /v1/settlements/route` + +Records credit-denominated route/resource rows tied to m2-gpt metering references. It does not +modify m2-gpt metering. + +## Audit snapshots + +`POST /v1/admin/snapshots` + +Writes deterministic balance/hash-chain snapshots to Forgejo repo `m2/market-audit`, not to the +registry repo `m2/m2-market`. diff --git a/contracts/telemetry.md b/contracts/telemetry.md new file mode 100644 index 0000000..a2cc4b8 --- /dev/null +++ b/contracts/telemetry.md @@ -0,0 +1,62 @@ +# Marketplace telemetry contract v1 + +All shown/opened/installed/dismissed marketplace telemetry uses one envelope: +`m2.market.telemetry.v1`. + +`m2.market.evidence.v1` is reserved for S6 job evidence records only. Do not write install, +proposal, Store, Scout, or CLI telemetry with the evidence schema. + +## Envelope + +```json +{ + "schema_version": "m2.market.telemetry.v1", + "event_id": "evt_01j2telemetry", + "event_type": "market.install.succeeded", + "occurred_at": "2026-07-02T00:00:00Z", + "tenant_id": "m2", + "operator_id": "op_chris", + "agent_id": "chris-m2o", + "listing_id": "lst_mm-pdf-report", + "solution_id": "sol_mm-pdf-report", + "solution_version": "1.0.0", + "proposal_id": "prop_01j2abc", + "source": "cli", + "content_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload": {} +} +``` + +Required fields: `schema_version`, `event_id`, `event_type`, `occurred_at`, `tenant_id`, +`source`, and at least one of `listing_id`, `solution_id`, or `proposal_id`. + +Telemetry must contain summaries and IDs only. No raw client data, secrets, transcript bodies, or +private job artifacts. + +## Event type registry + +- `market.listing.shown` +- `market.listing.opened` +- `market.proposal.generated` +- `market.proposal.shown` +- `market.proposal.opened` +- `market.proposal.accepted` +- `market.proposal.dismissed` +- `market.install.started` +- `market.install.succeeded` +- `market.install.failed` +- `market.install.refunded` +- `market.uninstall.succeeded` +- `market.upgrade.succeeded` +- `market.invoke.succeeded` +- `market.invoke.failed` + +Indexers may fold telemetry into `listing.stats.installs`, `listing.stats.proposals_shown`, and +`listing.stats.proposals_accepted`. Self-purchases and self-generated proposal loops are excluded +from quality statistics. + +## Job evidence reservation + +`m2.market.evidence.v1` records are S6 job evidence records and carry `evidence_id`, `job_id`, +`wanted`, `tools`, `contributors`, `metering`, `resources`, `outcome`, `reusable_verdict`, +`scrub_status`, and `content_hash`. They are not a general event envelope. diff --git a/contracts/tenants.md b/contracts/tenants.md new file mode 100644 index 0000000..80ad6e7 --- /dev/null +++ b/contracts/tenants.md @@ -0,0 +1,34 @@ +# Tenant namespace contract v1 + +Canonical tenant IDs come from the live m2-gpt `tenants` table. Marketplace contracts must not +invent alternate sentinels or display-name aliases. + +Live table read on 2026-07-02: + +| tenant_id | display_name | status | +| --- | --- | --- | +| `erlengrund-m2o` | erlengrund-m2o | active | +| `gunnar-m2o` | gunnar-m2o | active | +| `m2` | m2 Operator | active | +| `m2bd-m2o` | m2bd-m2o | active | +| `matrix` | matrix | active | +| `parlobyg-m2o` | parlobyg-m2o | active | +| `smoke7623` | Smoke smoke7623 | active | +| `e2e-verify-0617` | E2E Verify delete me | archived | + +Marketplace-reserved scopes: + +- `*` in `listing.tenant_visibility` means public to all tenants. +- `m2-core` is the shared commons scope for reusable, scrubbed marketplace evidence and shared + Solutions. It replaces `__fleet__` for marketplace contracts. + +Rules: + +- `tenant_visibility` is an allowlist of `*` or canonical active tenant IDs. +- Empty tenant filters fail closed. +- Readers must apply the S2 filter contract: + `filters.tenant_visibility_any = ["*", ""]`, with client-side backstop that + drops hits where `status != "published"` or caller tenant is not visible. +- Cross-tenant not-found and forbidden states are surfaced as `404` to avoid existence leaks. +- `m2-core` is allowed for `solution.tenant_scope` and evidence scope, but it is not a buyer + tenant and is not a replacement for `tenant_visibility: ["*"]`. diff --git a/licenses/m2-market-standard-v1.md b/licenses/m2-market-standard-v1.md new file mode 100644 index 0000000..3e70140 --- /dev/null +++ b/licenses/m2-market-standard-v1.md @@ -0,0 +1,47 @@ +# M2 Market Standard License v1 + +This license applies to marketplace Solutions that reference +`license_id=lic_m2_market_standard_v1` and `terms=m2-market-standard-v1`. + +## Grant + +After successful ledger settlement, M2 grants the purchasing operator a non-exclusive internal +commercial license to install and use the purchased Solution for the covered major version. +License grants are keyed by `(operator_id, listing_id, major_version)`. + +## Scope + +The licensed operator may use the Solution inside owned or client workspaces where the operator is +authorized to work, subject to the listing's tenant visibility, permissions, and applicable tenant +data policy. + +## Major-version coverage + +The grant covers all minor and patch releases in the purchased major version. A new major version +requires a new grant unless the listing explicitly states otherwise. + +## Internal modification + +Internal modification is allowed for installation, configuration, debugging, and workflow +adaptation. Modified copies remain internal and do not change the seller's support obligations. + +## No redistribution + +Redistribution, resale, public mirroring, or repackaging as another marketplace listing is not +allowed without explicit written permission from the seller and platform operator. + +## Support + +Support is best effort. Listings may include verification commands, rollback notes, and evidence, +but no uptime, fitness, or outcome warranty is implied. + +## Delisting and refunds + +Delisting does not revoke existing active grants. Refunds for failed apply flows are handled by +append-only ledger refund rows and may mark the related grant as refunded. + +## Secrets and tenant data + +The license does not permit embedding secrets, raw client data, or tenant-private artifacts in +published bundles, telemetry, or shared evidence. Tenant-derived material must pass the required +owner-initiation and double-scrub controls before shared commercial use. diff --git a/schemas/listing.schema.json b/schemas/listing.schema.json index 78db109..896320e 100644 --- a/schemas/listing.schema.json +++ b/schemas/listing.schema.json @@ -1,115 +1,9 @@ { "$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.", + "$id": "https://git.machinemachine.ai/m2/m2-market/schemas/listing.schema.json", + "title": "m2.listing.v1", + "description": "Catalog-facing projection of a marketplace listing. Forgejo m2/m2-market is truth; market:catalog is rebuildable.", "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", @@ -121,9 +15,133 @@ "category", "price", "seller", + "content_hash", "tenant_visibility", "status", "install_ref" ], - "additionalProperties": false + "properties": { + "schema_version": { "const": "m2.listing.v1" }, + "listing_id": { + "type": "string", + "pattern": "^lst_[a-z0-9][a-z0-9-]*$" + }, + "solution_id": { + "type": "string", + "pattern": "^sol_[a-z0-9][a-z0-9-]*$" + }, + "solution_version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$" + }, + "inventory_type": { + "type": "string", + "enum": ["solution", "capability", "resource", "service"] + }, + "name": { "type": "string", "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, + "category": { "type": "string", "minLength": 1 }, + "keywords": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "default": [] + }, + "icon": { "type": "string" }, + "price": { "$ref": "#/$defs/price" }, + "seller": { "$ref": "#/$defs/seller" }, + "evidence_summary": { "type": "string" }, + "content_hash": { "$ref": "#/$defs/sha256" }, + "tenant_visibility": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Allowed tenants. [\"*\"] means public. Other values must be canonical tenant ids from contracts/tenants.md." + }, + "stats": { + "type": "object", + "required": ["installs", "proposals_shown", "proposals_accepted"], + "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 } + }, + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": ["draft", "in_review", "published", "delisted"] + }, + "install_ref": { + "type": "string", + "pattern": "^lst_[a-z0-9][a-z0-9-]*-v\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$" + } + }, + "additionalProperties": false, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "price": { + "oneOf": [ + { + "type": "object", + "required": ["model", "amount", "currency"], + "properties": { + "model": { "const": "fixed" }, + "amount": { "type": "integer", "minimum": 1 }, + "currency": { "const": "m2cr" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["model", "currency", "quote", "settlement"], + "properties": { + "model": { "const": "job" }, + "currency": { "const": "m2cr" }, + "quote": { + "type": "object", + "required": ["min_amount", "max_amount", "expires_after_hours", "requires_acceptance"], + "properties": { + "min_amount": { "type": "integer", "minimum": 1 }, + "max_amount": { "type": "integer", "minimum": 1 }, + "expires_after_hours": { "type": "integer", "minimum": 1 }, + "requires_acceptance": { "type": "boolean" } + }, + "additionalProperties": false + }, + "settlement": { + "type": "object", + "required": ["ledger_reason", "escrow_required"], + "properties": { + "ledger_reason": { "const": "job" }, + "escrow_required": { "type": "boolean" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "seller": { + "type": "object", + "required": ["operator_id", "display_name"], + "properties": { + "operator_id": { + "type": "string", + "pattern": "^op_[a-z0-9][a-z0-9-]*$" + }, + "display_name": { "type": "string", "minLength": 1 }, + "contact_ref": { "type": "string", "minLength": 1 }, + "tenant_id": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + } } diff --git a/schemas/solution.schema.json b/schemas/solution.schema.json index 9b905fa..bd43681 100644 --- a/schemas/solution.schema.json +++ b/schemas/solution.schema.json @@ -1,200 +1,9 @@ { "$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).", + "$id": "https://git.machinemachine.ai/m2/m2-market/schemas/solution.schema.json", + "title": "m2.solution.v1", + "description": "Commercial Solution manifest. v1 is a marketplace superset of the fedlearn core fields: applicability, tenant_scope, evidence, content_hash, and scrub_status for tenant-derived work.", "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", @@ -210,30 +19,238 @@ "seller", "license" ], - "additionalProperties": false, - "if": { - "properties": { - "tenant_scope": { - "const": "m2-core" - } + "properties": { + "schema_version": { "const": "m2.solution.v1" }, + "solution_id": { + "type": "string", + "pattern": "^sol_[a-z0-9][a-z0-9-]*$" }, - "required": ["tenant_scope"] - }, - "else": { - "properties": { - "owner_initiated": { - "const": true + "name": { "type": "string", "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, + "description": { "type": "string" }, + "intent": { "type": "string", "minLength": 1 }, + "behavior": { "type": "object", "additionalProperties": true }, + "tools": { + "type": "array", + "items": { "type": "object", "additionalProperties": true }, + "default": [] + }, + "runtime": { "type": "object", "additionalProperties": true }, + "memory_schema": { "type": "object", "additionalProperties": true }, + "permissions": { + "type": "array", + "items": { "$ref": "#/$defs/permission" }, + "default": [] + }, + "deployment": { + "type": "object", + "required": ["recipe_ref", "entrypoint", "verify_command"], + "properties": { + "recipe_ref": { "type": "string", "minLength": 1 }, + "entrypoint": { "type": "string", "minLength": 1 }, + "verify_command": { "type": "string", "minLength": 1 } }, - "scrub_status": { - "type": "object", + "additionalProperties": false + }, + "applicability": { "$ref": "#/$defs/applicability" }, + "tenant_scope": { + "type": "string", + "minLength": 1, + "description": "Canonical tenant id from contracts/tenants.md. Use m2-core for shared commons/evidence." + }, + "owner_initiated": { "type": "boolean" }, + "scrub_status": { + "type": "object", + "properties": { + "double_scrubbed": { "type": "boolean" }, + "reviewers": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + }, + "additionalProperties": true + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/evidence" } + }, + "content_hash": { "$ref": "#/$defs/sha256" }, + "price": { "$ref": "#/$defs/price" }, + "seller": { "$ref": "#/$defs/seller" }, + "license": { "$ref": "#/$defs/license" }, + "revenue_split": { "$ref": "#/$defs/revenue_split" } + }, + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { "tenant_scope": { "const": "m2-core" } }, + "required": ["tenant_scope"] + }, + "else": { "properties": { - "double_scrubbed": { - "const": true + "owner_initiated": { "const": true }, + "scrub_status": { + "type": "object", + "required": ["double_scrubbed"], + "properties": { + "double_scrubbed": { "const": true } + }, + "additionalProperties": true } }, - "required": ["double_scrubbed"] + "required": ["owner_initiated", "scrub_status"] } + } + ], + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" }, - "required": ["owner_initiated", "scrub_status"] + "applicability": { + "type": "object", + "required": ["image_classes", "roles", "tool_requirements"], + "properties": { + "image_classes": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "roles": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "tool_requirements": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + }, + "additionalProperties": false + }, + "evidence": { + "type": "object", + "required": ["source"], + "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 }, + "content_hash": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": true + }, + "price": { + "oneOf": [ + { + "type": "object", + "required": ["model", "amount", "currency"], + "properties": { + "model": { "const": "fixed" }, + "amount": { "type": "integer", "minimum": 1 }, + "currency": { "const": "m2cr" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["model", "currency", "quote", "settlement"], + "properties": { + "model": { "const": "job" }, + "currency": { "const": "m2cr" }, + "quote": { + "type": "object", + "required": ["min_amount", "max_amount", "expires_after_hours", "requires_acceptance"], + "properties": { + "min_amount": { "type": "integer", "minimum": 1 }, + "max_amount": { "type": "integer", "minimum": 1 }, + "expires_after_hours": { "type": "integer", "minimum": 1 }, + "requires_acceptance": { "type": "boolean" } + }, + "additionalProperties": false + }, + "settlement": { + "type": "object", + "required": ["ledger_reason", "escrow_required"], + "properties": { + "ledger_reason": { "const": "job" }, + "escrow_required": { "type": "boolean" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "seller": { + "type": "object", + "required": ["operator_id", "display_name"], + "properties": { + "operator_id": { + "type": "string", + "pattern": "^op_[a-z0-9][a-z0-9-]*$" + }, + "display_name": { "type": "string", "minLength": 1 }, + "contact_ref": { "type": "string", "minLength": 1 }, + "tenant_id": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "license": { + "type": "object", + "required": ["license_id", "terms", "scope", "major_version_coverage", "allows_internal_modification", "allows_redistribution", "support"], + "properties": { + "license_id": { "const": "lic_m2_market_standard_v1" }, + "terms": { "const": "m2-market-standard-v1" }, + "scope": { "const": "operator" }, + "major_version_coverage": { "const": true }, + "allows_internal_modification": { "const": true }, + "allows_redistribution": { "const": false }, + "support": { "const": "best_effort" } + }, + "additionalProperties": false + }, + "revenue_split": { + "type": "object", + "required": ["platform_pct", "seller_pct"], + "properties": { + "platform_pct": { "type": "integer", "minimum": 0, "maximum": 100 }, + "seller_pct": { "type": "integer", "minimum": 0, "maximum": 100 }, + "contributors": { + "type": "array", + "items": { + "type": "object", + "required": ["operator_id", "pct"], + "properties": { + "operator_id": { + "type": "string", + "pattern": "^op_[a-z0-9][a-z0-9-]*$" + }, + "pct": { "type": "integer", "minimum": 1, "maximum": 100 } + }, + "additionalProperties": false + }, + "default": [] + } + }, + "additionalProperties": false + }, + "permission": { + "type": "object", + "required": ["permission_id", "kind", "access", "scope", "target"], + "properties": { + "permission_id": { "type": "string", "minLength": 1 }, + "kind": { "type": "string", "minLength": 1 }, + "access": { "type": "string", "minLength": 1 }, + "scope": { "type": "string", "minLength": 1 }, + "target": { "type": "string", "minLength": 1 }, + "reason": { "type": "string" }, + "secret_ref": { "type": "string" } + }, + "additionalProperties": true + } } } diff --git a/schemas/tests/fixtures/listing_bad_status.json b/schemas/tests/fixtures/listing_bad_status.json index fee8e4d..4d460aa 100644 --- a/schemas/tests/fixtures/listing_bad_status.json +++ b/schemas/tests/fixtures/listing_bad_status.json @@ -12,7 +12,11 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core" + }, + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "tenant_visibility": ["*"], "status": "archived", "install_ref": "lst_mm-pdf-report-v1.0.0" diff --git a/schemas/tests/fixtures/listing_missing_tenant_visibility.json b/schemas/tests/fixtures/listing_missing_tenant_visibility.json index 43c6330..5225c05 100644 --- a/schemas/tests/fixtures/listing_missing_tenant_visibility.json +++ b/schemas/tests/fixtures/listing_missing_tenant_visibility.json @@ -12,7 +12,11 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core" + }, + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "status": "published", "install_ref": "lst_mm-pdf-report-v1.0.0" } diff --git a/schemas/tests/fixtures/solution_bad_price.json b/schemas/tests/fixtures/solution_bad_price.json index 1d625f9..c55483b 100644 --- a/schemas/tests/fixtures/solution_bad_price.json +++ b/schemas/tests/fixtures/solution_bad_price.json @@ -26,9 +26,17 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core" + }, "license": { - "terms": "Single-operator install license; covers all machines under the buying operator_id.", - "major_version_coverage": true + "license_id": "lic_m2_market_standard_v1", + "terms": "m2-market-standard-v1", + "scope": "operator", + "major_version_coverage": true, + "allows_internal_modification": true, + "allows_redistribution": false, + "support": "best_effort" } } diff --git a/schemas/tests/fixtures/solution_missing_evidence.json b/schemas/tests/fixtures/solution_missing_evidence.json index 4f3d66f..5b9e3bb 100644 --- a/schemas/tests/fixtures/solution_missing_evidence.json +++ b/schemas/tests/fixtures/solution_missing_evidence.json @@ -22,9 +22,17 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core" + }, "license": { - "terms": "Single-operator install license; covers all machines under the buying operator_id.", - "major_version_coverage": true + "license_id": "lic_m2_market_standard_v1", + "terms": "m2-market-standard-v1", + "scope": "operator", + "major_version_coverage": true, + "allows_internal_modification": true, + "allows_redistribution": false, + "support": "best_effort" } } diff --git a/schemas/tests/fixtures/solution_tenant_violation.json b/schemas/tests/fixtures/solution_tenant_violation.json index 415554d..06cbf20 100644 --- a/schemas/tests/fixtures/solution_tenant_violation.json +++ b/schemas/tests/fixtures/solution_tenant_violation.json @@ -26,9 +26,17 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core" + }, "license": { - "terms": "Single-operator install license; covers all machines under the buying operator_id.", - "major_version_coverage": true + "license_id": "lic_m2_market_standard_v1", + "terms": "m2-market-standard-v1", + "scope": "operator", + "major_version_coverage": true, + "allows_internal_modification": true, + "allows_redistribution": false, + "support": "best_effort" } } diff --git a/schemas/tests/fixtures/valid_listing.json b/schemas/tests/fixtures/valid_listing.json index 1922157..c2f963b 100644 --- a/schemas/tests/fixtures/valid_listing.json +++ b/schemas/tests/fixtures/valid_listing.json @@ -8,14 +8,20 @@ "summary": "Generate Machine.Machine branded PDF reports and slide decks from Markdown", "category": "reporting", "keywords": ["pdf", "report", "branded", "slides"], - "icon": "https://git.machinemachine.ai/m2/market-registry/assets/mm-pdf-report/icon.png", + "icon": "https://git.machinemachine.ai/m2/m2-market/assets/mm-pdf-report/icon.png", "price": { "amount": 500, "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core", + "contact_ref": "forgejo:m2/m2-market", + "tenant_id": "m2" + }, "evidence_summary": "Generated branded PDF report for SDJS quarterly summary; operator confirmed styling matched brand deck.", + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "tenant_visibility": ["*"], "stats": { "installs": 0, diff --git a/schemas/tests/fixtures/valid_solution.json b/schemas/tests/fixtures/valid_solution.json index 7442ad6..71cef48 100644 --- a/schemas/tests/fixtures/valid_solution.json +++ b/schemas/tests/fixtures/valid_solution.json @@ -27,8 +27,12 @@ }, "permissions": [ { - "scope": "filesystem:write", - "path": "/tmp/mm-pdf-output" + "permission_id": "fs-write-output", + "kind": "filesystem", + "access": "write", + "scope": "agent", + "target": "/tmp/mm-pdf-output", + "reason": "write rendered report output" } ], "deployment": { @@ -58,12 +62,23 @@ "currency": "m2cr", "model": "fixed" }, - "seller": "sdjs-operator", + "seller": { + "operator_id": "op_m2core", + "display_name": "M2 Core", + "contact_ref": "forgejo:m2/m2-market", + "tenant_id": "m2" + }, "license": { - "terms": "Single-operator install license; covers all machines under the buying operator_id.", - "major_version_coverage": true + "license_id": "lic_m2_market_standard_v1", + "terms": "m2-market-standard-v1", + "scope": "operator", + "major_version_coverage": true, + "allows_internal_modification": true, + "allows_redistribution": false, + "support": "best_effort" }, "revenue_split": { - "platform_pct": 10 + "platform_pct": 10, + "seller_pct": 90 } } diff --git a/schemas/tests/test_schemas.py b/schemas/tests/test_schemas.py index aae8dc8..f6a0cc1 100644 --- a/schemas/tests/test_schemas.py +++ b/schemas/tests/test_schemas.py @@ -32,6 +32,16 @@ def errors(validator, instance): return list(validator.iter_errors(instance)) +def flatten_errors(errs): + flat = [] + stack = list(errs) + while stack: + err = stack.pop() + flat.append(err) + stack.extend(err.context) + return flat + + # --- valid fixtures --------------------------------------------------------- def test_valid_solution_passes(): @@ -76,10 +86,11 @@ def test_solution_tenant_violation_fails_on_else_branch(): def test_solution_bad_price_fails_on_price_amount_minimum(): errs = errors(solution_validator, fixture("solution_bad_price.json")) assert errs, "expected validation errors" + flat = flatten_errors(errs) assert any( - e.validator == "minimum" and list(e.path) == ["price", "amount"] - for e in errs - ), [(e.validator, list(e.path), e.message) for e in errs] + e.validator == "minimum" and list(e.path) in (["price", "amount"], ["amount"]) + for e in flat + ), [(e.validator, list(e.path), e.message) for e in flat] # --- invalid listing fixtures ------------------------------------------------ -- 2.45.2