herdr workforce output (6 codex angle-specialists -> claude synthesis): S1 ledger/economy, S2 catalog/registry, S3 storefront (cargstore->M2 Store), S4 Solution Scout (standalone watcher recommended), S5 packaging + 3 seed Solutions (mm-pdf 25cr, agent-scaffold 35cr, competitor-scan 60cr), S6 proposal engine + evidence loop. SPEC-INDEX is the tie-break: 23-row cross-cutting contract table (16 mismatches resolved, e.g. /v1 ledger paths win, m2store://listing/<id> deep link, one m2.market.telemetry.v1 envelope, op_<slug> operator ids, tenant ids from m2-gpt), 8 deduped clarifications with defaults, 4-phase build order where the paid-install wedge does NOT block on fedlearn (local ApplyAdapter until rails land), risk register. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
19 KiB
Markdown
181 lines
19 KiB
Markdown
# 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/<listing_id>`; S4 and S6 both emit `m2store://listing/<listing_id>`. **Fix:** `m2store://listing/<listing_id>` (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: `<install_ref>.tar.gz`, no manifest. **Fix:** S5 owns packaging → `.tar.gz` named `<install_ref>.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_<slug>` (S1/S2/S5) vs bare `sdjs-operator`/`m2bd` (S6). **Fix:** `op_<slug>` 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.*
|