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>
19 KiB
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
m2is the system of record. Listings enter via PR + veto (reusing fedlearn's scored-PR pipeline with a newcommercializedisposition); releases carry immutable bundles verified bycontent_hash.market:catalogin memory-api is a rebuildable semantic index, never truth.m2-marketCLI (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
SolutionManagerbeside the existing Flatpak backend, shells them2-marketCLI (never raw HTTP to ledger), renders price/evidence/permission-diff before install, registers them2store://deep-link protocol, and reads identity from provisioner-written~/.m2-market/config.toml(0600). Canary-first onchris-m2o, then oneagent-latestdesktop. - 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.yamlsource → canonicalsolution.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-proposerCoolify service plus a thin Hermesmarket-proposeskill. 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 writingm2.market.evidence.v1records 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.
- 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 theoperator_id ↔ principalmapping table as the operator registry until m2-gpt billing absorbs it. No new service. - Registry & audit repo naming (S1#2, S2#1).
Default: registry of record = existing
m2/m2-market; ledger snapshots = newm2/market-audit. Paths already compatible with a laterm2/market-registrysplit. - Credit↔USD conversion for route settlement and estimates (S1#3, S6#3).
Default:
m2cr_per_usd = 100, stored as ledger config (PLATFORMenv / admin endpoint), changeable only byledger:admin. S6 uses the same constant as its static fallback whensample_count < 2. - Listing review authority & merge policy (S2#2).
Default: every commercial listing requires an explicit
approvedlabel from the m2 platform operator (mariusz) — no timeout auto-merge for paid listings in MVP; the 72h veto window applies on top, andrisk:highalways needs manual merge. - Seed seller of record + competitor-scan pricing (S5#1, S5#2).
Default: platform-owned
op_m2coreis seller for all three seeds (revenue → platform account; clean test of the money loop without self-dealing noise).competitor-scanships as fixed-price install (60 m2cr); the bespoke operator-delivered variant becomes the first job/service listing after the jobs phase. - 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 inscrub-attestation.jsonreviewers[]. Adapter verbs assumedplan/apply/uninstall(+apply --upgrade); S5 tracks fedlearn and renames without contract change if needed. - 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_IDis injected as an env var into skill execution where available, else evidence recordstokens_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. - Standard license text + Hermes bridge transport (S2#3, S4#2, S3#3).
Default: write
m2-market-standard-v1as a one-page internal commercial license (per-operator install, major-version coverage, internal modification allowed, no redistribution, best-effort support) committed atm2/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-syncruntime-sync rails, fedlearn T-series) and thecommercializedisposition handoff. S5 explicitly provides a localApplyAdapterthat executes the samem2.recipe.v1shape, 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-pdfseed bundle + local apply adapter +m2-market install/uninstall. - Bootstrap:
market-bootstrap.shwrites config onchris-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.