# SPEC-INDEX — M2 Marketplace spec discovery synthesis Synthesized 2026-07-02 from S1–S6 (all six specs present). Individual specs are unmodified; where they disagree, **this index is the tie-break** and names the owning spec to amend. ## 1. Executive summary The M2 Marketplace turns proven, repeatable outcomes into installable **Solutions** sold for **M2 credits**. Six subsystems compose it: - **S1 `m2-ledger`** — standalone Coolify service, SQLite, append-only credit ledger with hash chain. Handles starter grants (100 m2cr), fixed-price install settlement (10% platform cut), license grants keyed `(operator, listing, major_version)`, escrow-backed job settlement for operator-delivered services, route/resource settlement referencing m2-gpt metering, and daily audit snapshots committed to Forgejo. Architected to migrate under extended m2-gpt billing later; ships internal credits first. - **S2 Catalog/Registry** — Forgejo org `m2` is the system of record. Listings enter via PR + veto (reusing fedlearn's scored-PR pipeline with a new `commercialize` disposition); releases carry immutable bundles verified by `content_hash`. `market:catalog` in memory-api is a rebuildable semantic index, never truth. `m2-market` CLI (search/show/install/publish) is the universal programmatic surface, tenant-filtered end to end. - **S3 M2 Store** — revival of the cargstore Electron app. Adds a `SolutionManager` beside the existing Flatpak backend, shells the `m2-market` CLI (never raw HTTP to ledger), renders price/evidence/permission-diff before install, registers the `m2store://` deep-link protocol, and reads identity from provisioner-written `~/.m2-market/config.toml` (0600). Canary-first on `chris-m2o`, then one `agent-latest` desktop. - **S4 Solution Scout** — standalone supervised per-desktop watcher (recommended over Hermes or herdr plugin). Reads only already-summarized local sources (herdr run summaries), summarizes on-box, queries the tenant-filtered catalog, and surfaces at most a few non-blocking proposals/day via toast + Store deep link. Propose-only; never installs. - **S5 Packaging & Seeds** — bundle contract (`solution.yaml` source → canonical `solution.json`/`listing.json`, `recipe.yaml`, `permissions.yaml`, evidence files), install/ uninstall/upgrade through the runtime-sync (`m2-core-sync`) apply adapter with a local adapter fallback until fedlearn rails land. Three seed Solutions: `mm-pdf` (25 m2cr), `agent-scaffold` (35 m2cr), `competitor-scan` (60 m2cr). - **S6 Proposal Engine** — `m2-market-proposer` Coolify service plus a thin Hermes `market-propose` skill. Composes build-vs-install-vs-job proposals with evidence-weighted p50/p80/p95 estimates, cites listings and prior job evidence, and closes the pricing loop by writing `m2.market.evidence.v1` records for every serious job. The paid-install flow that everything serves: publish (PR+veto) → discover (search/Scout/ Hermes) → inspect (price, evidence, permission diff) → settle (ledger debit + license grant) → apply (runtime-sync recipe) → verify → telemetry back into evidence and listing stats. Tenant isolation, no-secrets-in-artifacts, canary-first reversibility, and Forgejo-as-truth are enforced at every hop. ## 2. Cross-cutting contract table Legend: ✔ = specs agree; **MISMATCH** = specs conflict, fix given; **GAP** = referenced but not yet existing/frozen. "Owner" is the spec whose definition is normative after the fix. | # | Interface | Owner | Consumers | Status / resolution | |---|-----------|-------|-----------|---------------------| | 1 | `solution.schema.json` (`m2.solution.v1`) | S2 | S3, S4, S5, S6 | **MISMATCH** — S2 specs `price.model: fixed\|job`; frozen repo schema and S5 only validate `fixed` (S5 invents `price_ext`/service workaround). **Fix:** extend the enum to `fixed\|job` in one schema PR before first paid install; delete the `price_ext` workaround. Job listings remain non-direct-installable. | | 2 | `listing.schema.json` (`m2.listing.v1`) | S2 | S3, S4, S6 | **MISMATCH** — `seller` is an object in S2, a bare string in S3/S5 examples. **Fix:** object form `{operator_id, display_name, ...}` is canonical in v1; renderers show `display_name`. | | 3 | Install settlement endpoint | S1 | `m2-market` CLI (S2/S5); indirectly S3 | **MISMATCH** — S1 defines `POST /v1/settlements/install`; S2/S3/S4/S5 all say `POST /tx/install`. **Fix:** S1's versioned path wins (ledger owner). Only the CLI calls the ledger directly, so S2/S5 update their touchpoint lists and `contracts/ledger-api.md`; S3/S4 are insulated by the CLI. | | 4 | License check | S1 | S5 install path, S3 installed-state | **MISMATCH** — S1: `GET /v1/licenses/{operator_id}/{listing_id}?major_version=`; S5: `GET /licenses/check?...`. **Fix:** S1 path wins; S5 adapter hook updates. | | 5 | Refund on apply failure | S1 | S5/CLI (exit code 4 path) | **MISMATCH** — S1 refunds by `POST /v1/settlements/install/{tx_group_id}/refund`; S5 refunds by `ref`. **Fix:** CLI persists `tx_group_id` in `~/.m2-market/state.json` at settle time and refunds by it. | | 6 | Balance / wallet | S1 | S3 via `m2-market wallet` | **MISMATCH** — S1: `GET /v1/balances/{account_or_operator_id}`; S3 assumes `GET /balance/{operator_id}`. **Fix:** S1 path; CLI insulates the Store. | | 7 | Jobs API (`POST /v1/jobs`, milestones, cancel, dispute) | S1 | S6 (`operator_job` path), S3 (later checkout), S2 (job-listing refusal handoff) | ✔ — post-wedge. Note S2's job-price example says `settlement.ledger_reason: "route"`; that is wrong — job settlements use `tx_type=job`. Fix in S2 example. | | 8 | `market:catalog` partition + search filter contract | S2 (writer: `m2-market-indexer` only) | CLI, S3, S4, S6 | **MISMATCH** — filter shapes differ: S2 `filters.tenant_visibility_any: ["*", tenant]`; S4/S6 pass `tenant_id` arrays. **Fix:** S2's filter contract is canonical; server-side filtering when memory-api supports it, mandatory client-side backstop in every reader (drop hits failing `tenant_visibility`/`status=published`). | | 9 | `market:evidence` partition | S6 (job evidence) + S2 (stats folding) | S4, S5, S3 write telemetry; S2 indexer folds into `listing.stats`; S6 reads for estimates | **MISMATCH** — five near-colliding schemas, worst being S2 `m2.market_evidence.v1` vs S6 `m2.market.evidence.v1`. **Fix:** reserve `m2.market.evidence.v1` (S6) for job-evidence records; unify all shown/opened/installed/dismissed telemetry (S2/S3/S4/S5 events) under one envelope `m2.market.telemetry.v1` with an `event_type` registry. S2 renames its record. | | 10 | Deep link protocol | S3 | S4, S6 | **MISMATCH** — S3 registers `m2store://solution/`; S4 and S6 both emit `m2store://listing/`. **Fix:** `m2store://listing/` (majority, and the ID *is* a listing_id). S3 amends; params stay `?source=&proposal_id=&q=` only, navigate-only semantics. | | 11 | `m2-market` CLI JSON surface + exit codes 0–6 | S2 (core), S5 (package/install/uninstall/upgrade), S6 (propose/evidence) | S3 (sole backend), S4 (fallback) | ✔ on verbs and exit codes. **GAP** — `show --json` must return a frozen `permissions_diff` schema before Store implementation (S3 clarification #2). Freeze it in `contracts/cli.md`. | | 12 | Release bundle format | S5 | S2 (hash check, release hosting), S3 (display) | **MISMATCH** — S2: `solution-bundle.tar.zst` + inner `manifest.json`; S5: `.tar.gz`, no manifest. **Fix:** S5 owns packaging → `.tar.gz` named `.tar.gz`, but adopt S2's inner `manifest.json` (per-file sha256 list). `content_hash` computed over the normalized tarball. | | 13 | Registry repo, paths, PR labels | S2 | S5 (publish target), S1 (audit), fedlearn `commercialize` | **MISMATCH** — S2 recommends existing `m2/m2-market`; S5 assumes `m2/market-registry`; S1 wants `m2/market-audit`. **Fix:** registry = `m2/m2-market` (exists today; split later is a location change only). Ledger audit = separate `m2/market-audit` so daily snapshot commits don't pollute registry history. S2's label set is canonical; S5 drops `status:in_review` label (state lives in `listing.status`). | | 14 | Identity & credential bootstrap | m2-gpt | S1 (bearer introspection), S3 (config bootstrap), S6 (tenant keys) | **GAP** — S3's `POST /admin/v1/.../market-credentials` and S1's introspection endpoint don't exist in m2-gpt yet. **Fix (MVP):** a `scripts/market-bootstrap.sh` admin script mints keys via existing m2-gpt admin scripts and writes `~/.m2-market/config.toml` (0600); ledger validates service keys from its own keys file and defers bearer introspection to a small m2-gpt verify endpoint added post-wedge. | | 15 | Metering read `GET /internal/v1/metering/turn/{correlation_id}` | m2-gpt | S6 evidence capture | **GAP** — endpoint doesn't exist; S6 already specs the degraded path (`tokens_unknown=true` + herdr run ids). Ship degraded in MVP; add endpoint with the jobs phase. | | 16 | Proposal schemas (`m2.market.proposal-request.v1`, `m2.market.proposal.v1`) | S6 | S4 (may call proposer), S3 (`proposal_id` passthrough), S1 (settlement ref metadata) | **MISMATCH** — ID prefix `prop_` (S3/S6) vs `prp_` (S4). **Fix:** `prop_`. S4's local matcher is fine for MVP; it adopts the proposer API when S6 lands. | | 17 | Scout→Hermes bridge event `m2.hermes.market_proposal.v1` | S4 | Hermes `market-propose` skill (S6) | ✔ shape; transport unresolved → clarification C8. Best-effort in MVP. | | 18 | `~/.m2-market/config.toml` (0600) | S2/CLI | S3, S4, S5 | ✔ — keys: `operator_id, tenant, ledger_url, ledger_api_key, memory_api_url, memory_api_key, forgejo_url, forgejo_token, apply_adapter`. | | 19 | Install state `~/.m2-market/state.json` | S5 | S3 (installed badge), S1 (`grant_id`/`tx_group_id` refs) | ✔ — add `tx_group_id` per fix #5. | | 20 | Apply adapter (`m2-core-sync plan/apply/uninstall` + `m2.recipe.v1`) | fedlearn / S5 wraps | install path (S2 CLI), Scout self-install, Store deploy | ✔ contract; **GAP** on exact uninstall/upgrade verb names (clarification C6). Local `ApplyAdapter` executes the same recipe shape until rails land. | | 21 | Tenant namespace | m2-gpt `tenants` table | everyone | **MISMATCH** — "m2" (S1), "machine-machine" (S2/S3/S4), "m2-core"/"`__fleet__`" (S6). **Fix:** canonical tenant IDs come from the m2-gpt `tenants` table, one documented list in `contracts/tenants.md`. `tenant_visibility: ["*"]` means public; `m2-core` is the shared/commons scope for evidence. No other sentinels (`__fleet__` folds into `m2-core`). | | 22 | Operator ID format | S1 registry mapping | S2, S5, S6 | **MISMATCH** — `op_` (S1/S2/S5) vs bare `sdjs-operator`/`m2bd` (S6). **Fix:** `op_` everywhere. | | 23 | Ledger events JSONL (`m2.ledger.event.v1`) | S1 | S2 indexer (install counts, pricing evidence) | ✔. | ## 3. Consolidated [NEEDS CLARIFICATION] — deduped, with recommended defaults 18 raw markers across S1–S6 reduce to 8. Recommended default in bold; build proceeds on the default unless the operator overrides. 1. **Operator identity source of truth** (S1#1). Where does `op_*` come from? **Default:** m2-gpt is canonical for `(tenant_id, agent_id)`; m2-ledger maintains the `operator_id ↔ principal` mapping table as the operator registry until m2-gpt billing absorbs it. No new service. 2. **Registry & audit repo naming** (S1#2, S2#1). **Default:** registry of record = existing `m2/m2-market`; ledger snapshots = new `m2/market-audit`. Paths already compatible with a later `m2/market-registry` split. 3. **Credit↔USD conversion for route settlement and estimates** (S1#3, S6#3). **Default:** `m2cr_per_usd = 100`, stored as ledger config (`PLATFORM` env / admin endpoint), changeable only by `ledger:admin`. S6 uses the same constant as its static fallback when `sample_count < 2`. 4. **Listing review authority & merge policy** (S2#2). **Default:** every commercial listing requires an explicit `approved` label from the m2 platform operator (mariusz) — no timeout auto-merge for paid listings in MVP; the 72h veto window applies on top, and `risk:high` always needs manual merge. 5. **Seed seller of record + competitor-scan pricing** (S5#1, S5#2). **Default:** platform-owned `op_m2core` is seller for all three seeds (revenue → platform account; clean test of the money loop without self-dealing noise). `competitor-scan` ships as fixed-price install (60 m2cr); the bespoke operator-delivered variant becomes the first job/service listing *after* the jobs phase. 6. **Scrub attestation authority + m2-core-sync verbs** (S6#2, S5#3). **Default:** double-scrub requires two distinct humans: the tenant owner initiates (`owner_initiated=true`), the platform operator countersigns — both recorded in `scrub-attestation.json` `reviewers[]`. Adapter verbs assumed `plan/apply/uninstall` (+ `apply --upgrade`); S5 tracks fedlearn and renames without contract change if needed. 7. **m2-gpt marketplace endpoints: credential issuance + bearer introspection + metering** (S3#1, S6#1). **Default:** MVP uses an admin-run `market-bootstrap.sh` (no new m2-gpt API); `M2_GPT_CORRELATION_ID` is injected as an env var into skill execution where available, else evidence records `tokens_unknown=true`. The three m2-gpt endpoints (market-credentials, token verify, metering-by-turn) land as one small m2-gpt change in the jobs phase. 8. **Standard license text + Hermes bridge transport** (S2#3, S4#2, S3#3). **Default:** write `m2-market-standard-v1` as a one-page internal commercial license (per-operator install, major-version coverage, internal modification allowed, no redistribution, best-effort support) committed at `m2/m2-market/licenses/`; Scout→Hermes bridge = append-only local JSONL at `~/.local/share/m2-market/scout/hermes-bridge.jsonl` (file is the simplest thing both sides can poll); Flatpak surface stays hidden behind a secondary "Apps" tab on canaries. ## 4. Build order ### Dependency picture - **Fedlearn critical path:** only the *universal fleet install path* (`m2-core-sync` runtime-sync rails, fedlearn T-series) and the `commercialize` disposition handoff. S5 explicitly provides a local `ApplyAdapter` that executes the same `m2.recipe.v1` shape, so **the paid-install wedge does not block on fedlearn** — it converges with it. - **Independent of fedlearn:** S1 ledger (fully standalone), S2 schemas/registry/PR-pipeline/ indexer, S3 Store (depends only on CLI), S4 Scout (depends on catalog + deep link), S6 proposer (depends on catalog + memory; metering endpoint optional/degraded). Ordering constraints: S1 and S2 have no upstream dependencies and unblock everything. S5-bundles need S2 schemas fixed (job enum, seller object) first. S3 needs frozen `show --json` and exit codes (S2/S5). S4 needs S3's deep link registered. S6 needs S2 catalog and (optionally) S1 quotes. ### Phase cut — MVP wedge = smallest paid install end-to-end **Phase 0 — contract fixes (days):** apply mismatch fixes #1–#13 above: schema PR (job enum, seller object), freeze `show --json`/`permissions_diff`, adopt `/v1/*` ledger paths in `contracts/ledger-api.md`, deep-link segment, telemetry envelope, tenant namespace doc. **Phase 1 — the wedge (first paid install):** - S1 core: accounts, starter grant, `POST /v1/settlements/install`, license check, refund, snapshots. (Jobs endpoints stubbed/deferred.) - S2 core: registry layout in `m2/m2-market`, publish PR pipeline + CI gates, indexer, `m2-market search/show/publish`. - S5 core: bundle builder + `mm-pdf` seed bundle + local apply adapter + `m2-market install/uninstall`. - Bootstrap: `market-bootstrap.sh` writes config on `chris-m2o`. **Exit criterion:** on `chris-m2o`, an operator with a 100-credit starter grant runs `m2-market install lst_mm-pdf-report`, credits move (buyer→seller→platform), a license grant exists, the skill lands via the recipe, `mm-pdf --version` verifies, and the ledger snapshot commits to Forgejo. No UI, no Scout, no proposer required. **Phase 2 — storefront + seeds:** S3 M2 Store canary (`chris-m2o`, then one `agent-latest`), remaining seeds (`agent-scaffold`, `competitor-scan`), telemetry→stats folding. **Phase 3 — demand side:** S4 Scout canary (propose-only), S6 proposer service + Hermes `market-propose` skill (degraded metering). **Phase 4 — jobs + convergence:** S1 jobs/escrow/dispute, first service listing, the three m2-gpt endpoints (credentials, verify, metering), fedlearn `commercialize` importer once runtime-sync rails land, swap local adapter → `m2-core-sync`. ## 5. Risk register — top 6 (deduped across specs) | # | Risk | Where | Mitigations (already specced) | |---|------|-------|-------------------------------| | 1 | **Tenant data leakage** — client data (gst/nasr/parlobyg/peter) reaching shared catalog, evidence, telemetry, or proposals | S1–S6 | Tenant filters fail closed (empty filter = hard error); `owner_initiated` + `double_scrubbed` CI gate before any commercialization; summary-only indexing (never raw evidence/transcripts); cross-tenant reads return 404 not 403; Scout sends capped on-box summaries only. | | 2 | **Paid-but-not-applied split brain** — debit succeeds, apply fails | S1/S2/S3/S5 | Settle-then-apply order with compensating `refund` rows (never deletes); `Idempotency-Key` on every mutation; CLI exit code 4 surfaces refund state; `tx_group_id` persisted in state.json for refund; append-only ledger + hash chain + `verify-chain` fail-closed. | | 3 | **Secrets sprawl** — live keys in `~/.m2-market/config.toml`, bundles, evidence, logs | S2/S3/S4/S5 | 0600 enforcement (Store refuses to run otherwise); deterministic secret scanner blocks publish; `secret_ref` only in permissions; log redaction of `*_key`/`*_token`/bearer strings; runtime injection only, nothing in repos/images/releases. | | 4 | **Catalog/index split-brain** — memory-api diverges from Forgejo truth (has happened before) | S2/S4/S6 | Forgejo is the only truth; `market:catalog` fully rebuildable via `m2-market-indexer reindex`; Scout/proposer treat search as best-effort and prefer no proposal over stale ones; evidence JSONL audit copies allow replay. | | 5 | **Host/fleet fragility + broken local registry route** — mixed primus/agent-latest images, hard-freeze history, 302 registry | S3/S4/S5 | Everything ships as Forgejo release assets applied by runtime-sync/local adapter (no image bakes, no registry push/pull); canary-first (`chris-m2o` → one `agent-latest`) with idempotent, reversible recipes confined to `${AGENT_HOME}`; new services are Coolify apps on the `coolify` network; Scout degrades instead of crash-looping. | | 6 | **Marketplace gaming & abuse** — self-purchase stat inflation, fake evidence, proposal spam, escrow lockup | S1/S2/S4/S6 | Self-purchases rejected by default and always excluded from quality stats; publish requires reproducible provenance evidence; Scout rate limits/cooldowns/dismissal suppression; failed jobs recorded but never raise success stats; job terms must define auto-accept timeout or admin resolution so escrow can't lock forever. | --- *Next actions: run Phase 0 contract fixes as a single PR set against `m2/m2-market` (`schemas/`, `contracts/`), then confirm the 8 clarification defaults with the operator.*