From 8a8db9f2ccb031239a4a541fa70caaf2e94cd243 Mon Sep 17 00:00:00 2001 From: "m2 (AI Agent)" Date: Thu, 2 Jul 2026 02:43:50 +0200 Subject: [PATCH] =?UTF-8?q?specs:=20m2-market=20spec=20discovery=20?= =?UTF-8?q?=E2=80=94=206=20subsystem=20specs=20+=20synthesis=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ deep link, one m2.market.telemetry.v1 envelope, op_ 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 --- specs/BRIEF.md | 53 ++ specs/S1-ledger-economy.md | 759 +++++++++++++++++++++ specs/S2-catalog-registry.md | 969 +++++++++++++++++++++++++++ specs/S3-storefront-m2store.md | 615 +++++++++++++++++ specs/S4-solution-scout.md | 537 +++++++++++++++ specs/S5-packaging-seeds.md | 647 ++++++++++++++++++ specs/S6-proposal-engine-evidence.md | 668 ++++++++++++++++++ specs/SPEC-INDEX.md | 181 +++++ 8 files changed, 4429 insertions(+) create mode 100644 specs/BRIEF.md create mode 100644 specs/S1-ledger-economy.md create mode 100644 specs/S2-catalog-registry.md create mode 100644 specs/S3-storefront-m2store.md create mode 100644 specs/S4-solution-scout.md create mode 100644 specs/S5-packaging-seeds.md create mode 100644 specs/S6-proposal-engine-evidence.md create mode 100644 specs/SPEC-INDEX.md diff --git a/specs/BRIEF.md b/specs/BRIEF.md new file mode 100644 index 0000000..c396459 --- /dev/null +++ b/specs/BRIEF.md @@ -0,0 +1,53 @@ +# Spec-Discovery Brief — m2-market (M2 Marketplace) + +## Mission +Produce implementation-ready SPECs for the M2 Marketplace: operators package repeatable +outcomes as installable **Solutions**, sell them for **M2 credits**; a **Solution Scout** +proposes packages in-session at the moment of need; Hermes composes build-vs-install +proposals. "The unit is not software. The unit is completed work." + +READ FIRST (in order): +1. /home/m2/m2-market/CONCEPT.md — the unified concept v0.2 (your requirements source) +2. /home/m2/m2-market/context/SYSTEM-MAP.md — verified map of m2-gpt, agent.memory.system, m2o +3. /home/m2/m2o/.planning/federated-learning/PLAN.md — the fedlearn MVP rails you build upon + +## Locked decisions (operator-accepted; do NOT relitigate — design WITH them) +1. **Ledger:** standalone `m2-ledger` service, BUT architected to evolve into a crypto-capable + ledger managed under extended **m2-gpt billing** (tenant keys/metering federate; design the + migration path now, ship internal credits first). +2. **Pricing v1:** fixed price per install AND job-based pricing where applicable — sometimes + another m2o operator takes a JOB to deliver a solution (service-style engagement settling + through the same ledger). +3. **Storefront:** MVP = **cargstore Electron revival** (github.com/machine-machine/cargstore) + that connects to Coolify and other parts using credentials from **m2-gpt** (gateway-issued + identity/keys). Web variant later from cargstore's web/ seed. +4. **Scout:** EXPLORE the solution space — standalone supervised watcher vs Hermes plugin vs + herdr plugin. Compare honestly, recommend one, spec it. +5. **Seed Solutions:** mm-pdf (branded PDF generator), agent-scaffold (workspace generator), + competitor-scan (research report). +6. **Economy defaults:** platform cut 10%, starter grant 100 credits. + +## Hard constraints (inherited — every spec must respect) +- Tenant isolation: client data (sdjs=GST, nasr, parlobyg, peter) never crosses into shared + catalog; commercialization of tenant-derived work is owner-initiated + doubly-scrubbed. +- No secrets in repos/images; keys injected at runtime (M2_GPT_API_KEY pattern). +- Mixed fleet images: runtime-sync (fedlearn m2-core-sync) is the universal install path. +- Host fragility: canary-first, idempotent, reversible; new services deploy as Coolify apps + on the `coolify` network. Local registry route is broken (302) — no registry push/pull. +- Harness-agnostic where cheap: Hermes is primary, openclaw stays supported; anything speaking + OpenAI-wire through m2-gpt should work. +- Forgejo (git.machinemachine.ai, org m2) = system of record; memory-api = semantic index. + +## Your output — a SPEC, not a plan +Write to /tmp/mktspec/specs/.md. Structure: +1. **Scope & non-goals** (MVP vs later) +2. **User stories + acceptance criteria** (Given/When/Then where useful) +3. **Interfaces & data contracts** — concrete: schemas (JSON), API endpoints (method+path+ + payload), CLI verbs, file paths, DB tables, events. This is the heart. +4. **Integration contract** with the other subsystems (name exact touchpoints) +5. **Options compared** where your lane has a genuine fork; recommend one +6. **Risks/edge cases** (tenant, secrets, fragility, abuse) +7. **[NEEDS CLARIFICATION]** markers for anything genuinely undecidable (max 3) +Inspect the LIVE system (docker, curl, repos in /home/m2/) — ground every contract in reality. +Read-only on all existing repos; your ONLY write is your spec file. +Last terminal line: SPEC_DONE_ diff --git a/specs/S1-ledger-economy.md b/specs/S1-ledger-economy.md new file mode 100644 index 0000000..478b89a --- /dev/null +++ b/specs/S1-ledger-economy.md @@ -0,0 +1,759 @@ +# S1 — m2-ledger SPEC: Ledger & Economy + +## 1. Scope & Non-Goals + +### MVP scope + +`m2-ledger` is a standalone Coolify app on the shared `coolify` Docker network with Docker DNS alias `m2-ledger` and private base URL `http://m2-ledger:8000`. It is the append-only credit ledger for the M2 Marketplace wedge: + +- Whole-number M2 credits (`m2cr`) only. +- SQLite as the authoritative store for MVP. +- Append-only transaction log; balances are derived from transactions. +- Account creation and starter grants for operators, sellers, platform, resource owners, and system accounts. +- Fixed-price install settlement: buyer debit, seller credit net of platform cut, platform cut, license grant, idempotent install ref. +- Job-based pricing settlement for service-style Solutions: escrow, milestone release, acceptance, dispute/manual cancel, and operator earnings through the same ledger. +- Route/resource settlement hooks so m2-gpt token/resource spend can be represented as ledger `route`/`job` events without duplicating m2-gpt metering. +- Federation with m2-gpt identity: every service call made with a gateway-issued tenant/agent key or service key resolves to a server-side `(tenant_id, agent_id, operator_id)` before ledger mutation. +- Daily snapshot-to-Forgejo audit: balance and transaction-hash snapshots committed to Forgejo org `m2`. +- Admin-only starter grant default: `100` credits. +- Platform cut default: `10%`. + +### Later scope + +- Public fiat purchase and cash payout automation. +- On-chain/crypto settlement, public token, custody, exchange, tax, and fraud workflows. +- Multi-currency balances. The schema reserves `currency`, but MVP accepts only `m2cr`. +- Negative balances, credit lines, chargebacks beyond append-only compensating entries. +- Trustless escrow. MVP escrow is an internal account state machine. +- Replacing m2-gpt billing. MVP federates with it; later it may move under an extended m2-gpt billing plane. + +### Non-goals + +- Catalog truth, listing review, and install package format. S2/S5 own those. S1 consumes `listing_id`, `solution_id`, version, price, seller, and job terms. +- Storefront UX. S3 owns cargstore/M2 Store. +- Scout/Hermes proposal ranking. S4/S6 own discovery/proposals; S1 supplies quote, authorization, and settlement APIs. +- Secrets management implementation. S1 requires runtime-injected keys and documents env names only. + +## 2. User Stories + Acceptance Criteria + +### Install purchase + +Given an operator has a positive M2 credit balance and a published fixed-price listing, when M2 Store or `m2-market install` calls `POST /v1/settlements/install`, then the ledger atomically records buyer debit, seller net credit, platform cut, and a license grant. + +Acceptance: + +- Balance is sufficient before any rows are inserted. +- Repeated calls with the same `idempotency_key` return the existing settlement. +- No grant exists without a paid transaction group. +- Platform cut defaults to 10% unless listing revenue split overrides it. + +### Insufficient funds + +Given a buyer balance is below the required price, when install settlement is requested, then the API returns `402 insufficient_funds` and writes no transactions, grants, or ledger events. + +### Starter grant + +Given an active operator account and an admin/service key with `ledger:grant`, when `POST /v1/grants/starter` is called without amount, then the account receives exactly 100 credits from `acct_mint`. + +### Job-based Solution + +Given a buyer accepts an operator-delivered job quote, when `POST /v1/jobs` is called, then funds move from buyer to `acct_escrow`, a job record is opened, and the assigned operator can claim milestones only through explicit acceptance or admin/manual release rules. + +Acceptance: + +- `quoted -> funded -> in_progress -> submitted -> accepted -> paid` is the happy path. +- Milestone payments release from escrow to seller and platform via append-only rows. +- Cancel before work returns escrow to buyer through compensating rows. +- Dispute freezes remaining escrow until an admin resolution event. + +### Route/resource settlement + +Given m2-gpt records metered model spend in its `spend_log` and rollups, when an authorized route-settlement worker posts a route usage batch, then m2-ledger records a credit-denominated `route` transaction group tied to the m2-gpt source IDs, without modifying m2-gpt records. + +### Audit snapshot + +Given the daily cron runs, when `/v1/admin/snapshots` executes, then it writes a deterministic JSON snapshot containing balances, tx high-water mark, previous snapshot hash, and current tx chain hash, and commits it to Forgejo. + +## 3. Interfaces & Data Contracts + +### Runtime deployment contract + +- Coolify app: `m2-ledger`. +- Network: `coolify`. +- Internal URL: `http://m2-ledger:8000`. +- Public route: none for MVP unless an admin UI needs it; prefer internal network calls. +- Health: `GET /health -> {"status":"healthy","service":"m2-ledger","db":"ok"}`. +- Required env, injected at runtime: + - `M2_LEDGER_DB=/data/m2-ledger.sqlite3` + - `M2_LEDGER_API_KEYS_FILE=/run/secrets/m2-ledger-keys.json` + - `M2_GPT_BASE_URL=https://gpt.machinemachine.ai` + - `M2_GPT_ADMIN_TOKEN` or m2-gpt service key for identity introspection + - `FORGEJO_URL=https://git.machinemachine.ai` + - `FORGEJO_ORG=m2` + - `FORGEJO_AUDIT_REPO=market-audit` + - `FORGEJO_TOKEN` + - `PLATFORM_CUT_BPS=1000` + - `STARTER_GRANT_CREDITS=100` + +No secrets are committed to repos or images. + +### Auth and identity + +MVP supports two accepted caller modes: + +1. `Authorization: Bearer sk-m2-...` for gateway-issued tenant/agent keys. m2-ledger introspects through m2-gpt or validates a signed identity assertion issued by m2-gpt. +2. `X-API-Key: ...` for service/admin keys stored only in runtime secrets. + +Resolved identity object: + +```json +{ + "tenant_id": "m2", + "agent_id": "chris-m2o", + "operator_id": "op_chris", + "principal_ref": "agent:chris-m2o", + "scopes": ["ledger:read", "ledger:install"], + "auth_source": "m2-gpt" +} +``` + +Rules: + +- Request bodies may include `operator_id` only for admin/service operations. Buyer identity for agent/user calls is server-derived from auth. +- Cross-tenant reads return `404`, not existence-revealing `403`, unless caller has `ledger:admin`. +- Service keys are scoped: `ledger:install`, `ledger:job`, `ledger:route`, `ledger:grant`, `ledger:snapshot`, `ledger:admin`. +- All mutations require `Idempotency-Key` header or `idempotency_key` body field; both present must match. + +### SQLite tables + +#### `accounts` + +```sql +CREATE TABLE accounts ( + account_id TEXT PRIMARY KEY, + account_type TEXT NOT NULL CHECK (account_type IN + ('operator','tenant','platform','mint','escrow','resource','service')), + operator_id TEXT, + tenant_id TEXT, + display_name TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('active','suspended','closed')), + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL +); +``` + +System accounts: + +- `acct_mint`: source for starter grants and admin corrections. +- `acct_platform`: platform cut receiver. +- `acct_escrow`: escrow holder for funded jobs. + +#### `ledger_tx` + +```sql +CREATE TABLE ledger_tx ( + tx_id TEXT PRIMARY KEY, + tx_group_id TEXT NOT NULL, + seq INTEGER NOT NULL, + ts TEXT NOT NULL, + from_account_id TEXT NOT NULL REFERENCES accounts(account_id), + to_account_id TEXT NOT NULL REFERENCES accounts(account_id), + amount INTEGER NOT NULL CHECK (amount > 0), + currency TEXT NOT NULL CHECK (currency = 'm2cr'), + tx_type TEXT NOT NULL CHECK (tx_type IN + ('install','payout','grant','route','earn','job','refund','adjustment')), + ref_type TEXT NOT NULL, + ref_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + actor_tenant_id TEXT, + actor_agent_id TEXT, + actor_operator_id TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + prev_hash TEXT NOT NULL, + tx_hash TEXT NOT NULL, + UNIQUE(tx_group_id, seq), + UNIQUE(idempotency_key, tx_type) +); +``` + +Append-only invariant: no `UPDATE` or `DELETE` application paths. Corrections use new rows with `refund` or `adjustment`. + +Balance formula: + +```sql +SUM(CASE WHEN to_account_id = :account THEN amount ELSE 0 END) +- SUM(CASE WHEN from_account_id = :account THEN amount ELSE 0 END) +``` + +Non-system accounts may not go negative. `acct_mint` can issue grants. `acct_escrow` may only release against funded job groups. + +#### `license_grants` + +```sql +CREATE TABLE license_grants ( + grant_id TEXT PRIMARY KEY, + operator_id TEXT NOT NULL, + account_id TEXT NOT NULL REFERENCES accounts(account_id), + tenant_id TEXT NOT NULL, + listing_id TEXT NOT NULL, + solution_id TEXT NOT NULL, + solution_version TEXT NOT NULL, + major_version INTEGER NOT NULL, + tx_group_id TEXT NOT NULL, + install_ref TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('active','revoked','refunded')), + created_at TEXT NOT NULL, + UNIQUE(operator_id, listing_id, major_version) +); +``` + +#### `jobs` + +```sql +CREATE TABLE jobs ( + job_id TEXT PRIMARY KEY, + listing_id TEXT, + solution_id TEXT, + buyer_account_id TEXT NOT NULL REFERENCES accounts(account_id), + seller_account_id TEXT NOT NULL REFERENCES accounts(account_id), + tenant_id TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN + ('quoted','funded','in_progress','submitted','accepted','paid','cancelled','disputed')), + total_amount INTEGER NOT NULL CHECK (total_amount > 0), + currency TEXT NOT NULL CHECK (currency = 'm2cr'), + platform_cut_bps INTEGER NOT NULL DEFAULT 1000, + terms_json TEXT NOT NULL, + escrow_tx_group_id TEXT, + accepted_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +``` + +#### `job_milestones` + +```sql +CREATE TABLE job_milestones ( + milestone_id TEXT PRIMARY KEY, + job_id TEXT NOT NULL REFERENCES jobs(job_id), + idx INTEGER NOT NULL, + title TEXT NOT NULL, + amount INTEGER NOT NULL CHECK (amount > 0), + acceptance_mode TEXT NOT NULL CHECK (acceptance_mode IN ('buyer_accept','auto_after_timeout','admin_release')), + due_at TEXT, + status TEXT NOT NULL CHECK (status IN + ('pending','funded','submitted','accepted','released','cancelled','disputed')), + release_tx_group_id TEXT, + evidence_uri TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE(job_id, idx) +); +``` + +#### `snapshots` + +```sql +CREATE TABLE snapshots ( + snapshot_id TEXT PRIMARY KEY, + snapshot_date TEXT NOT NULL UNIQUE, + tx_high_water_seq INTEGER NOT NULL, + tx_chain_hash TEXT NOT NULL, + prev_snapshot_hash TEXT, + snapshot_hash TEXT NOT NULL, + forgejo_commit TEXT, + forgejo_path TEXT NOT NULL, + created_at TEXT NOT NULL +); +``` + +### HTTP API + +All endpoints are under `/v1` except health. + +#### Accounts and balances + +`GET /v1/accounts/me` + +Response: + +```json +{ + "operator_id": "op_chris", + "tenant_id": "m2", + "account_id": "acct_op_chris", + "status": "active" +} +``` + +`GET /v1/balances/{account_or_operator_id}` + +Requires same operator/tenant or `ledger:admin`. + +Response: + +```json +{ + "account_id": "acct_op_chris", + "operator_id": "op_chris", + "balance": 140, + "currency": "m2cr", + "as_of_tx_seq": 1234, + "as_of": "2026-07-02T00:00:00Z" +} +``` + +`GET /v1/transactions?account_id=&operator_id=&tx_type=&ref_type=&ref_id=&since=&limit=&cursor=` + +Returns paginated append-only entries visible to caller. + +#### Starter grants + +`POST /v1/grants/starter` + +Requires `ledger:grant`. + +Request: + +```json +{ + "operator_id": "op_gunnar", + "tenant_id": "m2", + "amount": 100, + "reason": "starter_grant_2026w27", + "idempotency_key": "starter:op_gunnar:2026w27" +} +``` + +Response: + +```json +{ + "tx_group_id": "txg_01j...", + "tx_ids": ["tx_01j..."], + "balance": 100 +} +``` + +#### Fixed install settlement + +`POST /v1/settlements/install` + +Requires `ledger:install`. + +Request: + +```json +{ + "buyer_operator_id": "op_chris", + "seller_operator_id": "op_sdjs", + "tenant_id": "m2", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "solution_version": "1.0.0", + "install_ref": "lst_competitor-scan-v1.0.0", + "amount": 40, + "currency": "m2cr", + "platform_cut_bps": 1000, + "idempotency_key": "install:op_chris:lst_competitor-scan:1.0.0" +} +``` + +Settlement math: + +- `platform_cut = floor(amount * platform_cut_bps / 10000)`, minimum `1` if `platform_cut_bps > 0` and `amount > 1`. +- `seller_net = amount - platform_cut`. +- Rows: + - buyer -> seller, `seller_net`, `install` + - buyer -> platform, `platform_cut`, `install` +- License grant created in the same SQLite transaction. + +Response: + +```json +{ + "status": "settled", + "tx_group_id": "txg_01j...", + "tx_ids": ["tx_01j_a", "tx_01j_b"], + "grant": { + "grant_id": "gr_01j...", + "operator_id": "op_chris", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "solution_version": "1.0.0", + "major_version": 1, + "status": "active" + } +} +``` + +Errors: + +- `402 insufficient_funds` +- `409 duplicate_idempotency_key` +- `409 license_already_granted` +- `422 invalid_amount_or_split` + +`POST /v1/settlements/install/{tx_group_id}/refund` + +Requires `ledger:admin` or service compensation scope. Creates compensating `refund` rows and marks grant `refunded`; does not delete original install rows. + +#### License check + +`GET /v1/licenses/{operator_id}/{listing_id}?major_version=1` + +Used by `m2-core-sync`/install adapter before apply and by M2 Store for installed state. + +Response: + +```json +{ + "grant": { + "grant_id": "gr_01j...", + "status": "active", + "solution_version": "1.0.0", + "major_version": 1 + } +} +``` + +#### Job quotes and settlement + +`POST /v1/jobs` + +Requires `ledger:job`. + +Request: + +```json +{ + "job_id": "job_01j...", + "buyer_operator_id": "op_chris", + "seller_operator_id": "op_gunnar", + "tenant_id": "m2", + "listing_id": "lst_agent-scaffold-assisted", + "solution_id": "sol_agent-scaffold", + "total_amount": 120, + "currency": "m2cr", + "platform_cut_bps": 1000, + "terms": { + "summary": "Create and install tailored workspace scaffold", + "acceptance": "buyer_accept", + "auto_accept_after_hours": 72 + }, + "milestones": [ + {"title": "Scaffold delivered", "amount": 80, "acceptance_mode": "buyer_accept"}, + {"title": "Install verified", "amount": 40, "acceptance_mode": "buyer_accept"} + ], + "idempotency_key": "job:create:job_01j..." +} +``` + +Behavior: + +- Verifies buyer balance >= total. +- Inserts buyer -> `acct_escrow` row with `tx_type=job`, `ref_type=job`. +- Creates `jobs.status=funded`. +- Creates funded milestone rows. + +`POST /v1/jobs/{job_id}/start` + +Seller/operator marks work `in_progress`. No funds move. + +`POST /v1/jobs/{job_id}/milestones/{milestone_id}/submit` + +Seller submits evidence. + +Request: + +```json +{"evidence_uri":"forgejo://m2/jobs/job_01j/evidence/m1.md"} +``` + +`POST /v1/jobs/{job_id}/milestones/{milestone_id}/accept` + +Buyer or admin accepts. Releases milestone amount: + +- escrow -> seller, `amount - platform_cut` +- escrow -> platform, `platform_cut` + +When all milestones released, job becomes `paid`. If the job produced an installable Solution, the catalog/install subsystem may call fixed install grant separately or include a zero-price license grant only if job terms say delivery includes install rights. + +`POST /v1/jobs/{job_id}/cancel` + +Allowed before any milestone release by buyer/admin, or by admin after dispute. Refunds unreleased escrow to buyer and marks pending milestones cancelled. + +`POST /v1/jobs/{job_id}/dispute` + +Sets `status=disputed`; no movement until admin release/refund. + +#### Route/resource settlement + +`POST /v1/settlements/route` + +Requires `ledger:route`; intended caller is m2-gpt route-settlement worker or marketplace resource router. + +Request: + +```json +{ + "tenant_id": "m2", + "buyer_operator_id": "op_chris", + "resource_account_id": "acct_resource_spark", + "amount": 3, + "currency": "m2cr", + "source": { + "system": "m2-gpt", + "spend_log_ids": ["7fd1..."], + "route_id": "spark-glm", + "usd": "0.018400", + "conversion_rate": {"m2cr_per_usd": 100} + }, + "idempotency_key": "route:m2:op_chris:2026-07-02T00:15" +} +``` + +Rows: + +- buyer -> resource owner/platform, `route`. + +m2-gpt remains the meter of record for tokens/USD; m2-ledger records credit settlement and source IDs only. + +#### Snapshots and audit + +`POST /v1/admin/snapshots` + +Requires `ledger:snapshot`. Also run daily by cron around 00:10 UTC. + +Response: + +```json +{ + "snapshot_id": "snap_2026-07-02", + "path": "audit/ledger/2026/07/2026-07-02.json", + "commit": "abc123...", + "snapshot_hash": "sha256:..." +} +``` + +Snapshot JSON: + +```json +{ + "schema_version": "m2.ledger.snapshot.v1", + "date": "2026-07-02", + "generated_at": "2026-07-02T00:10:00Z", + "tx_high_water_seq": 1234, + "tx_chain_hash": "sha256:...", + "prev_snapshot_hash": "sha256:...", + "balances": [ + {"account_id":"acct_op_chris","operator_id":"op_chris","tenant_id":"m2","balance":140,"currency":"m2cr"} + ] +} +``` + +Forgejo target: `https://git.machinemachine.ai/m2/market-audit`, path `audit/ledger/YYYY/MM/YYYY-MM-DD.json`. If S2 chooses a single `m2/market-registry` repo, use `m2/market-registry/audit/ledger/...`; S1 requires only an append-only Forgejo-backed audit path. + +### Events + +m2-ledger emits JSONL events to stdout for Coolify log collection and optionally `POST /memory/store` for pricing evidence summaries under `agent_id=market:ledger`. + +Event envelope: + +```json +{ + "schema_version": "m2.ledger.event.v1", + "event_id": "evt_01j...", + "ts": "2026-07-02T00:00:00Z", + "type": "install.settled", + "tenant_id": "m2", + "operator_id": "op_chris", + "ref": {"listing_id":"lst_competitor-scan","tx_group_id":"txg_01j..."}, + "amount": 40, + "currency": "m2cr" +} +``` + +Event types: + +- `account.created` +- `grant.starter_issued` +- `install.settled` +- `install.refunded` +- `license.granted` +- `job.funded` +- `job.started` +- `job.milestone_submitted` +- `job.milestone_released` +- `job.cancelled` +- `job.disputed` +- `route.settled` +- `snapshot.committed` + +Memory store payload for non-sensitive pricing evidence: + +```json +{ + "agent_id": "market:ledger", + "tenant_id": "m2", + "memory_type": "episodic", + "source": "api", + "content": "Install settled: lst_competitor-scan v1.0.0 for 40 m2cr; seller net 36; platform cut 4.", + "metadata": { + "event_type": "install.settled", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "amount": 40, + "currency": "m2cr", + "tx_group_id": "txg_01j..." + } +} +``` + +No client data, raw job artifacts, secrets, or private tenant-derived content goes into shared memory events. + +### CLI verbs + +`m2-ledger` admin CLI: + +- `m2-ledger serve` +- `m2-ledger migrate` +- `m2-ledger account create --operator-id op_chris --tenant-id m2` +- `m2-ledger grant starter --operator-id op_chris` +- `m2-ledger balance op_chris` +- `m2-ledger tx list --operator-id op_chris --limit 20` +- `m2-ledger snapshot run` +- `m2-ledger audit verify-chain` + +`m2-market` integration verbs call the HTTP API, not SQLite: + +- `m2-market wallet balance` +- `m2-market wallet tx` +- `m2-market install `: quote/listing lookup -> ledger install settlement -> license grant -> runtime-sync apply. +- `m2-market job create|start|submit|accept|cancel|dispute` + +### File paths + +- Service DB volume: `/data/m2-ledger.sqlite3`. +- Service config: `/etc/m2-ledger/config.toml` generated by Coolify env or mounted secret. +- Service keys: `/run/secrets/m2-ledger-keys.json`. +- Snapshot staging: `/data/snapshots/YYYY-MM-DD.json`. +- Local desktop install state remains owned by S5 path: `~/.m2-market/state.json` and `~/.m2-core/state.json`; ledger only supplies grants and settlement refs. + +## 4. Integration Contract With Other Subsystems + +### m2-gpt + +Touchpoints verified in live repo: + +- Agent keys are minted per tenant/agent and resolve to `(tenant_id, agent_id)`. +- Existing tables include `tenants`, `agents`, `api_keys`, `tenant_budgets`, `spend_log`, and daily/weekly rollups. +- Harnesses call `https://gpt.machinemachine.ai/v1` using `Authorization: Bearer sk-m2-...`. + +Contract: + +- m2-ledger accepts m2-gpt-issued bearer identity for operator-facing reads and purchases. +- m2-ledger service/admin operations use runtime-injected service keys. +- Route settlement references m2-gpt `spend_log.id`, `tenant_id`, `agent_id`, `route_id`; m2-ledger never edits m2-gpt metering rows. +- Future migration path: move `accounts`, `ledger_tx`, `jobs`, and `license_grants` into an extended m2-gpt billing schema, preserving tx hashes and IDs. m2-ledger becomes a compatibility API or billing module. The append-only `ledger_tx` maps to a future `billing_ledger_entries`; `spend_log` maps to source metering; `tenant_rollups_daily.extras` can include credit summary. + +### S2 Catalog / registry + +- S2 provides published listing data: `listing_id`, `solution_id`, `solution_version`, `inventory_type`, `seller`, `price`, `tenant_visibility`, `install_ref`, `revenue_split`. +- S1 validates only the settlement payload and tenant visibility reference supplied by trusted Store/CLI. S1 does not own listing truth. +- Ledger events update install counts and pricing evidence through S2 indexer, not direct catalog mutation. + +### S3 Storefront + +- M2 Store calls: + - `GET /v1/accounts/me` and `GET /v1/balances/{account_or_operator_id}` before purchase display. + - `POST /v1/settlements/install` on install click. + - `GET /v1/licenses/{operator_id}/{listing_id}` to show installed/licensed state. + - Job endpoints for service-style checkout. +- Store must present price, seller, platform cut, and permission diff before settlement. + +### S4 Scout + +- Scout never settles directly from a passive proposal. +- Scout deep-links Store with `listing_id` and optional `proposal_id`. +- If install accepted, Store/CLI includes `proposal_id` in install settlement metadata so ledger events can feed conversion evidence. + +### S5 Packaging / runtime-sync + +- Install order: + 1. Resolve listing and permissions. + 2. Settle ledger install. + 3. Receive license grant. + 4. Apply via runtime-sync / `m2-core-sync`. + 5. On apply failure, call refund/compensation endpoint if no durable install state was written. +- Apply path must check grant via `GET /v1/licenses/...` and write `grant_id` into local install state. +- Mixed fleet constraint: no image-specific ledger client dependency; all calls are HTTP and runtime-sync universal. + +### S6 Proposal engine / Hermes + +- Hermes proposal uses ledger quote/balance endpoints to compare build-vs-install and job options. +- Agents may spend only within operator-set budgets. For MVP, budget enforcement is split: + - m2-gpt enforces token/USD route budgets. + - m2-ledger enforces credit balances and job/install affordability. +- Proposal records include estimated credit cost but no ledger mutation until explicit approval. + +### Forgejo + +- Forgejo is audit system of record for daily snapshots. +- Commit author should be a service identity such as `m2-ledger-bot`. +- Snapshots are append-only by path. Corrections use a new snapshot or correction file; never rewrite historical snapshots after publication. + +### memory-api + +- Optional pricing evidence and event summaries are stored under `agent_id=market:ledger`. +- `tenant_id` must be explicit; shared market evidence uses `tenant_id=m2`. +- Tenant-private job details must not be written to shared memory. Use tenant-scoped memory only when owner-initiated and scrubbed. + +## 5. Options Compared + +### Ledger substrate + +| Option | Pros | Cons | Verdict | +|---|---|---|---| +| Standalone `m2-ledger` SQLite service | Small blast radius, fast MVP, append-only model is easy to audit, deploys as Coolify app | Another service to operate; later migration needed | Recommended for MVP; locked decision | +| Extend m2-gpt billing now | Reuses tenant keys, budgets, spend admin UI | Higher regression risk in live gateway; mixes marketplace credit semantics with token metering too early | Later migration target | +| Forgejo-as-ledger only | Very auditable | Poor concurrency/idempotency; awkward balance queries; harder escrow | Reject | + +### Job settlement model + +| Option | Pros | Cons | Verdict | +|---|---|---|---| +| Full escrow at job acceptance | Protects seller capacity, simple affordability check, clear buyer commitment | Buyer locks full funds | Recommended | +| Milestone pay-as-you-go without escrow | Lower upfront buyer lock | Seller non-payment risk; harder abuse handling | Later for trusted parties | +| Manual invoice only | Simple | Does not close marketplace loop | Reject for MVP | + +### Crypto-capable evolution + +Recommended path: + +1. MVP internal credits in `m2-ledger`. +2. Add exportable hash chain and deterministic snapshots from day one. +3. When m2-gpt billing grows a first-class billing schema, migrate ledger tables under m2-gpt while preserving IDs and hashes. +4. Only after legal/tax/custody decisions, add external payment rails or on-chain mirrors as settlement adapters. The internal ledger remains source of entitlement truth until explicitly replaced. + +## 6. Risks / Edge Cases + +- Tenant leakage: job descriptions and evidence can contain client data. Ledger stores refs and scrubbed summaries only; raw artifacts stay tenant-owned. +- Cross-tenant account probing: return `404` for inaccessible account/license/job IDs. +- Secrets in audit: snapshots include account IDs, balances, tx refs, hashes, not API keys or raw metadata. +- Duplicate purchase clicks: idempotency key required; duplicate install ref returns existing grant. +- Apply failure after settlement: use compensating refund rows; never delete original install rows. +- Platform cut rounding: define basis points and deterministic integer rounding in the API contract. +- Seller buys own listing: allow only with explicit `allow_self_purchase=true` admin/test metadata; default reject to prevent fake volume. +- Suspended account: cannot send or receive except admin refund/adjustment. +- Job abandonment: terms must define auto-accept timeout or admin resolution path; otherwise jobs can lock escrow indefinitely. +- Snapshot commit failure: snapshot remains local with `forgejo_commit=null`, service retries; API health reports degraded audit until committed. +- SQLite concurrency: use WAL mode, one writer, transaction boundaries around every settlement; if write lock timeout occurs return `503 retryable`. +- Hash-chain corruption: `m2-ledger audit verify-chain` must fail closed and require admin intervention before new mutation if local tx hash continuity breaks. +- Abuse/fake earnings: pricing evidence and ratings are catalog-owned; ledger can mark suspicious self-dealing metadata but does not curate. + +## 7. [NEEDS CLARIFICATION] + +1. Exact operator_id source of truth: derive from m2-gpt `agents.principal_ref`, m2o `fleet.json`, or a new marketplace operator registry? MVP can maintain an `accounts.operator_id` mapping, but the canonical owner should be chosen before implementation. +2. Forgejo audit repo name: use a dedicated `m2/market-audit` repo or S2's registry repo with `audit/ledger/...` paths. +3. Credit-to-USD conversion policy for route settlement: fixed operator-configured rate is enough technically, but the initial `m2cr_per_usd` value and who can change it need an operator decision. diff --git a/specs/S2-catalog-registry.md b/specs/S2-catalog-registry.md new file mode 100644 index 0000000..f3e6161 --- /dev/null +++ b/specs/S2-catalog-registry.md @@ -0,0 +1,969 @@ +# S2 — Catalog & Registry SPEC + +Grounded in live inspection on 2026-07-02: `/home/m2/m2-market` already contains v1 +`schemas/solution.schema.json` and `schemas/listing.schema.json`, first-wedge contracts for +registry/catalog/CLI, and a local `/home/m2/m2-core` clone with fedlearn schemas. Live +services are reachable: `https://gpt.machinemachine.ai/health`, `https://memory.machinemachine.ai/health`, +and `https://git.machinemachine.ai/api/healthz`; Docker shows Forgejo, memory-api, Qdrant, +m2o desktops, and Coolify on the `coolify` network. + +## 1. Scope & non-goals + +### MVP scope + +- Freeze the marketplace catalog/registry contracts for installable **Solutions**. +- Treat Forgejo org `m2` as system of record: + - registry repo: `git.machinemachine.ai/m2/m2-market` unless split later into + `m2/market-registry`; + - releases carry immutable Solution bundles; + - listing PRs ride the fedlearn scored PR + human veto pipeline and add the + `commercialize` disposition. +- Define `solution.schema.json` as a commercial superset of fedlearn + `m2-core-manifest.schema.json` and the fedlearn candidate fields already assumed by + m2-market. +- Define `listing.schema.json` as the catalog-facing projection used by cargstore/M2 Store, + Scout, Hermes, and CLI. +- Define `market:catalog` in memory-api as a rebuildable semantic index, not source of truth. +- Define tenant-scoped search/show/install/publish CLI verbs for `m2-market`. +- Support v1 inventory types `solution|capability|resource|service`, but only `solution` + is installable in MVP. `service` may be published and discovered for job-priced work. + +### Non-goals + +- Ledger internals, wallet accounting, starter grants, refunds, payout mechanics: S1 owns + them. S2 defines only catalog-side fields and ledger touchpoints. +- Store UI design and Electron packaging: S3 owns cargstore/M2 Store. +- Scout matching host, thresholds, and notification UX: S4 owns it. S2 defines the catalog + query contract Scout consumes. +- Bundle build format, runtime-sync implementation, signing, and packaging details beyond + registry paths and manifest fields: S5 owns packaging. +- Hermes build-vs-install proposal policy and scoring: S6 owns proposal engine behavior. + +## 2. User stories + acceptance criteria + +### Story S2.1 — Publish a reviewed listing + +As a builder, I package a repeatable outcome with evidence, price, seller identity, license, +permissions, and deployment metadata, then submit it for review. + +- Given a valid bundle directory containing `solution.json`, `listing.json`, `recipe.yaml`, + payload files, and evidence files, when I run `m2-market publish `, then the + CLI validates schemas, computes the bundle hash, creates a Forgejo branch + `listing/-v`, uploads a release asset, and opens a PR. +- Given a listing PR, when CI runs, then it must validate both schemas, verify evidence is + non-empty, verify `solution.content_hash` matches the release asset, verify tenant firewall + rules, and render price, permission diff, evidence summary, and rollback note in the PR. +- Given a PR with no veto after the configured review window and an `approved` label, when it + merges, then `listing.status` becomes `published` and the indexer upserts it to + `market:catalog`. +- Given a PR with label `veto` or `/veto `, when the watcher processes it, then the + PR is closed without indexing and the listing remains `draft` or `rejected`. + +### Story S2.2 — Search tenant-allowed listings + +As an operator, I search by outcome rather than package id and see only listings my tenant may +use. + +- Given a published listing with `tenant_visibility: ["*"]`, when any tenant searches a + semantically related query, then the listing may appear. +- Given a published listing with `tenant_visibility: ["gst"]`, when a caller from tenant + `nasr` searches, then the listing must not be returned by CLI, Store, Scout, or Hermes. +- Given `market:catalog` is deleted or stale, when `m2-market-indexer reindex` runs, then the + catalog is rebuilt from Forgejo `main` without needing any memory-api-only data. + +### Story S2.3 — Show installable evidence and permissions + +As a buyer, I inspect a listing before spending credits. + +- Given a listing id, when I run `m2-market show `, then output includes name, + version, seller, price model, license, evidence summary, applicability, permissions, + install ref, and status. +- Given the listing is delisted, when I run `show`, then it is visible only by exact id or + license/install context and is not returned by broad search. + +### Story S2.4 — Install through catalog contract + +As a buyer, I install a published solution from the registry release that the listing points + at. + +- Given a published fixed-price listing, when `m2-market install ` runs, then S2 + resolves the listing from registry/catalog, verifies status and tenant visibility, fetches + release `-v`, verifies `content_hash`, and hands the commercial charge + request to S1 before calling the S5 apply adapter. +- Given a job-priced listing, when `install` is attempted, then the CLI must refuse direct + install with exit code `1` and print the required proposal/job flow handoff; job settlement + is S1/S6 territory. + +### Story S2.5 — Commercialize a fedlearn candidate + +As a curator, I mark a scored fedlearn candidate as commercially listable rather than free +core. + +- Given a fedlearn cluster passed scoring, when the reviewer chooses `commercialize`, then the + pipeline opens the same listing PR shape as `m2-market publish`. +- Given a fedlearn candidate is tenant-derived, when `commercialize` is selected without + `owner_initiated: true` and `scrub_status.double_scrubbed: true`, then CI rejects it before + human review. +- Given the item is fleet-infra learning, when reviewed, then it must use + `promote-to-core`, not `commercialize`. + +## 3. Interfaces & data contracts + +### 3.1 Forgejo registry of record + +Canonical URL: + +- MVP repo: `https://git.machinemachine.ai/m2/m2-market` +- Registry paths in that repo: + +```text +schemas/ + solution.schema.json + listing.schema.json +listings// + listing.json + solution.json + evidence/ + .json +releases: + -v + assets: + solution-bundle.tar.zst +audit/ + catalog-index/YYYY-MM-DDTHHMMSSZ.json +``` + +If operations later split a clean `m2/market-registry` repo, paths and release tags stay +identical. The split is a remote/location change, not a contract change. + +Release bundle shape: + +```text +solution-bundle.tar.zst + solution.json + listing.json + recipe.yaml + payload/ + evidence/ + manifest.json +``` + +`manifest.json` inside the bundle: + +```json +{ + "schema_version": "m2.solution_bundle.v1", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "solution_version": "1.0.0", + "created_at": "2026-07-02T00:00:00Z", + "files": [ + {"path": "solution.json", "sha256": "hex"}, + {"path": "recipe.yaml", "sha256": "hex"} + ] +} +``` + +Release tag format: `-v`, matching `listing.install_ref`. + +### 3.2 `solution.schema.json` v1 + +Normative schema lives at `schemas/solution.schema.json`. S2 requires the existing schema be +tightened before implementation to cover the locked decisions below. + +Required top-level fields: + +```json +{ + "schema_version": "m2.solution.v1", + "solution_id": "sol_mm-pdf-report", + "name": "MM PDF Report", + "summary": "Generate branded PDF reports from structured input.", + "intent": "Turn approved analysis into a styled client-ready PDF report.", + "behavior": {}, + "tools": [], + "runtime": {}, + "memory_schema": {}, + "permissions": [], + "deployment": { + "recipe_ref": "recipe.yaml", + "entrypoint": "m2-solution run mm-pdf-report", + "verify_command": "m2-solution verify mm-pdf-report" + }, + "applicability": { + "image_classes": ["primus", "agent-latest"], + "roles": ["operator"], + "tool_requirements": [] + }, + "tenant_scope": "m2-core", + "evidence": [], + "content_hash": "sha256:<64-hex>", + "price": {}, + "seller": "op_sdjs", + "license": {}, + "revenue_split": {} +} +``` + +Fedlearn-compatible core subset: + +- `schema_version` +- `applicability.image_classes[]` +- `applicability.roles[]` +- `applicability.tool_requirements[]` +- `tenant_scope` +- `evidence[]` +- `content_hash` +- `scrub_status` when tenant-derived + +Commercial superset fields: + +#### Price + +The current local schema only permits fixed price. MVP must support the locked v1 models: + +```json +{ + "model": "fixed", + "amount": 40, + "currency": "m2cr" +} +``` + +```json +{ + "model": "job", + "currency": "m2cr", + "quote": { + "min_amount": 120, + "max_amount": 300, + "expires_after_hours": 72, + "requires_acceptance": true + }, + "settlement": { + "ledger_reason": "route", + "escrow_required": true + } +} +``` + +Rules: + +- `fixed` is directly installable. +- `job` is discoverable and showable, but install starts a proposal/job route rather than a + direct apply. +- `currency` is always `m2cr` in MVP. +- `amount` is an integer number of credits; no fractional credits. + +#### Seller + +```json +{ + "operator_id": "op_sdjs", + "display_name": "sdjs operator", + "contact_ref": "forgejo:m2/sdjs", + "tenant_id": "machine-machine" +} +``` + +The existing schema allows a bare string. For implementation, accept both during migration, +but normalize registry records to the object form above. + +#### License + +```json +{ + "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" +} +``` + +Rules: + +- License grants are keyed by `(operator_id, listing_id, major_version)`. +- Delisting does not revoke existing grants. +- Redistribution is false unless explicitly allowed. + +#### Revenue split + +```json +{ + "platform_pct": 10, + "seller_pct": 90, + "contributors": [ + {"operator_id": "op_researcher", "pct": 10} + ] +} +``` + +Rules: + +- Percentages must total 100. +- Default platform cut is 10 percent per locked brief. +- Ledger execution and payout are S1, but registry stores the split used to explain pricing + and reproduce the charge request. + +#### Permissions + +```json +[ + { + "permission_id": "perm_memory_read_project", + "kind": "memory", + "access": "read", + "scope": "tenant", + "target": "project_context", + "reason": "Read project brief and prior approved notes.", + "required": true + }, + { + "permission_id": "perm_browser_control", + "kind": "desktop", + "access": "take-control", + "scope": "operator-approved", + "target": "browser", + "reason": "Operate browser only after user approval.", + "required": false + } +] +``` + +Allowed `kind`: `memory|filesystem|network|desktop|mcp|secret_ref|ledger|browser`. + +Allowed `access`: `read|write|execute|observe|suggest|take-control|hand-back`. + +Rules: + +- No raw secret values. Use `secret_ref` identifiers only. +- Permission diff is required in listing PR body and `m2-market show`. +- Tenant-scoped permissions cannot target another tenant. + +#### Evidence + +```json +[ + { + "evidence_id": "ev_mm_pdf_2026w27_001", + "source": "herdr", + "ref": "evidence/ev_mm_pdf_2026w27_001.json", + "machine": "chris-m2o", + "session": "sess_2026w27_pdf", + "summary": "Generated a branded PDF report from structured findings.", + "tokens": 32000, + "wall_time_seconds": 840, + "cost_estimate_m2cr": 12, + "outcome": "success", + "scrub_status": { + "secrets_redacted": true, + "pii_redacted": true, + "double_scrubbed": true + } + } +] +``` + +Rules: + +- At least one evidence item is required for publish. +- Evidence files in `evidence/` must not contain raw tenant data or secrets. +- Tenant-derived work requires owner initiation and double scrub. + +### 3.3 `listing.schema.json` v1 + +Normative schema lives at `schemas/listing.schema.json`. Required implementation shape: + +```json +{ + "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 from structured input.", + "category": "reporting", + "keywords": ["pdf", "report", "brand"], + "icon": "icons/mm-pdf.png", + "price": {"model": "fixed", "amount": 40, "currency": "m2cr"}, + "seller": {"operator_id": "op_sdjs", "display_name": "sdjs operator"}, + "license_ref": "lic_m2_market_standard_v1", + "evidence_summary": "Validated on chris-m2o with one successful report run.", + "tenant_visibility": ["*"], + "applicability": { + "image_classes": ["primus", "agent-latest"], + "roles": ["operator"], + "tool_requirements": [] + }, + "permissions_summary": [ + {"kind": "filesystem", "access": "write", "target": "~/.m2-market/solutions/mm-pdf"} + ], + "stats": { + "installs": 0, + "rating": 0, + "proposals_shown": 0, + "proposals_accepted": 0 + }, + "status": "published", + "install_ref": "lst_mm-pdf-report-v1.0.0", + "registry": { + "repo": "m2/m2-market", + "commit": "", + "release_url": "https://git.machinemachine.ai/m2/m2-market/releases/tag/lst_mm-pdf-report-v1.0.0" + } +} +``` + +State machine: + +```text +draft -> in_review -> published -> delisted + | | + v v + rejected delisted +``` + +State rules: + +- `draft`: local or branch-only; not indexed. +- `in_review`: PR exists; not indexed. +- `published`: merged to main; indexed. +- `delisted`: exact-id show remains possible for license/install history; broad search hides it. +- `rejected`: optional audit state; not indexed. + +### 3.4 Listing PR template + +Every publish/commercialize PR body must include: + +```markdown +## Commercialize Review + +Listing: +Solution: @ +Inventory type: solution|service|capability|resource +Price: +Seller: +Tenant visibility: <["*"] or tenant ids> + +## Evidence + + +## Permission Diff + + +## Tenant Firewall +Owner initiated: true|false|not-applicable +Double scrubbed: true|false|not-applicable + +## Rollback + + +## Veto +Veto until: +Use label `veto` or comment `/veto `. +``` + +Required labels: + +- `market:listing` +- `disposition:commercialize` +- `risk:low|medium|high` +- `inventory:solution|capability|resource|service` +- `veto-until:` +- `tenant:` + +Merge gates: + +- schema validation green; +- content hash check green; +- secret scan green; +- tenant firewall check green; +- no `veto` label; +- `approved` label present; +- high-risk listings require manual merge, never timeout auto-merge. + +### 3.5 `market:catalog` partition in memory-api + +Partition name: `market:catalog`. + +Source: published `listings//listing.json` plus selected fields from +`solution.json`. + +Writer: only `m2-market-indexer`. + +Readers: `m2-market` CLI, M2 Store, Scout, Hermes market-propose skill. + +Record shape stored in memory-api: + +```json +{ + "agent_id": "market:catalog", + "text": "MM PDF Report\nGenerate branded PDF reports...\nintent...\nkeywords...\nevidence summary...", + "metadata": { + "schema_version": "m2.catalog_record.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 from structured input.", + "category": "reporting", + "keywords": ["pdf", "report", "brand"], + "price": {"model": "fixed", "amount": 40, "currency": "m2cr"}, + "seller_operator_id": "op_sdjs", + "tenant_visibility": ["*"], + "status": "published", + "install_ref": "lst_mm-pdf-report-v1.0.0", + "registry_commit": "", + "content_hash": "sha256:<64-hex>", + "applicability": { + "image_classes": ["primus", "agent-latest"], + "roles": ["operator"], + "tool_requirements": [] + }, + "stats": { + "installs": 0, + "rating": 0, + "proposals_shown": 0, + "proposals_accepted": 0 + } + } +} +``` + +Index text fields: + +- `listing.name` +- `listing.summary` +- `listing.category` +- `listing.keywords` +- `solution.intent` +- `solution.description` +- `solution.behavior` summaries only +- `listing.evidence_summary` +- evidence summaries, never raw evidence payloads + +Tenant-scoped query contract: + +```http +POST /memory/search +X-API-Key: +Content-Type: application/json + +{ + "agent_id": "market:catalog", + "query": "generate branded PDF reports", + "limit": 10, + "filters": { + "status": "published", + "tenant_visibility_any": ["*", "machine-machine"], + "inventory_type": ["solution"], + "price_model": ["fixed", "job"], + "image_class": "primus" + } +} +``` + +Response normalized by `m2-market-indexer`/CLI: + +```json +{ + "items": [ + { + "listing_id": "lst_mm-pdf-report", + "score": 0.83, + "listing": {}, + "registry_commit": "" + } + ] +} +``` + +Indexing rules: + +- `published` records are upserted. +- `delisted` records are deleted from broad catalog index but retained in registry. +- `draft`, `in_review`, and `rejected` are never indexed. +- All writes require memory-api `X-API-Key`. +- If memory-api split-brain recurs, registry remains truth and reindex is the recovery path. + +Indexer commands: + +```bash +m2-market-indexer watch --repo /home/m2/m2-market --poll-seconds 300 +m2-market-indexer reindex --repo /home/m2/m2-market --partition market:catalog +m2-market-indexer validate --repo /home/m2/m2-market +``` + +Telemetry partition: `market:evidence`. + +Install/proposal telemetry record: + +```json +{ + "agent_id": "market:evidence", + "text": "Install succeeded for lst_mm-pdf-report on chris-m2o in 52 seconds.", + "metadata": { + "schema_version": "m2.market_evidence.v1", + "event_id": "evt_01h...", + "event_type": "install_succeeded", + "listing_id": "lst_mm-pdf-report", + "operator_id": "op_m2bd", + "tenant_id": "machine-machine", + "desktop": "chris-m2o", + "ts": "2026-07-02T00:00:00Z", + "duration_seconds": 52, + "ledger_ref": "install:lst_mm-pdf-report:", + "scrubbed": true + } +} +``` + +Telemetry folding: + +- A scheduled indexer job folds aggregate counts into `listing.stats`. +- Stats changes may be committed directly by an indexer bot or batched into a PR; registry + remains truth. +- Self-purchases are excluded from quality metrics. + +### 3.6 CLI contract + +Config file: `~/.m2-market/config.toml`, mode `0600`. + +```toml +operator_id = "op_m2bd" +tenant = "machine-machine" +image_class = "primus" + +ledger_url = "http://m2-ledger:8000" +ledger_api_key = "..." + +memory_api_url = "https://memory.machinemachine.ai" +memory_api_key = "..." + +forgejo_url = "https://git.machinemachine.ai" +forgejo_token = "..." + +registry_repo = "m2/m2-market" +apply_adapter = "m2core-sync" +``` + +All commands support `--json`. + +#### `m2-market search` + +```bash +m2-market search "branded pdf report" --type solution --limit 10 +``` + +Behavior: + +- queries `market:catalog`; +- applies tenant filter server-side when memory-api supports it and client-side as a backstop; +- hides delisted records; +- returns id, name, summary, inventory type, price, seller, installs, rating, score. + +#### `m2-market show` + +```bash +m2-market show lst_mm-pdf-report +``` + +Behavior: + +- resolves exact listing from registry first, catalog second; +- shows evidence, price, seller, license, permissions, applicability, status, install ref; +- for delisted listing, shows delisted warning and existing license status if available. + +#### `m2-market install` + +```bash +m2-market install lst_mm-pdf-report --yes +``` + +Behavior for `price.model=fixed`: + +1. Resolve listing and verify `status=published`. +2. Verify tenant visibility and applicability. +3. Display price, license, and permission diff unless `--yes`. +4. Ask S1 ledger to debit buyer and create grant. +5. Fetch release asset named by `install_ref`. +6. Verify `solution.content_hash`. +7. Call S5 apply adapter (`m2-core-sync` when landed; local adapter only for pre-rails wedge). +8. Write install telemetry to `market:evidence`. + +Behavior for `price.model=job`: + +- no direct apply; +- prints proposal handoff instructions and exits non-zero until S6 job flow exists. + +Exit codes: + +- `0`: success or idempotent no-op +- `1`: usage or unsupported flow such as direct install of job listing +- `2`: listing not found +- `3`: insufficient funds from ledger +- `4`: apply failed after charge; refund/remediation delegated to S1/S5 flow +- `5`: validation, tenant firewall, or permission rejection +- `6`: sync rails not landed + +#### `m2-market publish` + +```bash +m2-market publish ./bundle-dir +``` + +Behavior: + +- validates `solution.json`, `listing.json`, evidence, release manifest, content hash; +- enforces tenant/firewall checks before pushing; +- creates or updates `listings//`; +- creates release `-v`; +- opens a PR using the listing template. + +Publish options: + +```bash +m2-market publish ./bundle-dir --draft +m2-market publish ./bundle-dir --tenant-visibility '*' +m2-market publish ./bundle-dir --price fixed:40 +m2-market publish ./bundle-dir --price-job 120:300 +``` + +#### `m2-market wallet` + +Catalog CLI may expose `wallet` for operator convenience, but S1 owns its API semantics. + +#### `m2-market reindex` + +Admin-only command: + +```bash +m2-market reindex --full +``` + +Invokes `m2-market-indexer reindex` and emits audit record under +`audit/catalog-index/.json`. + +### 3.7 APIs S2 consumes or exposes + +S2 does not add a public long-lived service in MVP. It uses these APIs: + +Forgejo: + +```http +GET /api/v1/repos/m2/m2-market +GET /api/v1/repos/m2/m2-market/contents/listings//listing.json?ref=main +POST /api/v1/repos/m2/m2-market/pulls +POST /api/v1/repos/m2/m2-market/releases +GET /api/v1/repos/m2/m2-market/releases/tags/-v +``` + +memory-api: + +```http +GET /health +POST /memory/store +POST /memory/search +``` + +ledger S1 touchpoints: + +```http +POST /tx/install +GET /license/{operator_id}/{listing_id} +``` + +The install charge request S2 sends to S1: + +```json +{ + "buyer_operator_id": "op_m2bd", + "listing_id": "lst_mm-pdf-report", + "solution_version": "1.0.0", + "price": {"model": "fixed", "amount": 40, "currency": "m2cr"}, + "seller": {"operator_id": "op_sdjs"}, + "revenue_split": {"platform_pct": 10, "seller_pct": 90}, + "ref": "install:lst_mm-pdf-report:" +} +``` + +### 3.8 File paths + +Registry: + +- `/home/m2/m2-market/schemas/solution.schema.json` +- `/home/m2/m2-market/schemas/listing.schema.json` +- `/home/m2/m2-market/listings//listing.json` +- `/home/m2/m2-market/listings//solution.json` +- `/home/m2/m2-market/listings//evidence/*.json` + +CLI local state: + +- `~/.m2-market/config.toml` +- `~/.m2-market/state.json` +- `~/.cache/m2-market/releases//solution-bundle.tar.zst` + +Applied solution target: + +- S5 decides exact targets, but S2 requires runtime-sync compatibility across `primus` and + `agent-latest`; no image rebuild and no local registry push/pull. + +## 4. Integration contract with other subsystems + +### S1 ledger + +S2 provides: + +- listing id, seller, price, revenue split, license terms, and install ref. +- install charge request for fixed-price installs. + +S1 provides: + +- debit/credit/grant transaction; +- license lookup; +- refund/remediation path on apply failure; +- ledger audit snapshot. + +Boundary: S2 never computes authoritative balances and never writes ledger rows. + +### S3 store + +S2 provides: + +- `market:catalog` search contract; +- `listing.json` presentation fields compatible with cargstore basics: + `id/name/summary/category/icon/featured-ish keywords/install_ref`; +- `m2-market show/install --json` as stable Store backend. + +S3 provides: + +- UI rendering, deep links, and install button behavior. + +### S4 Scout + +S2 provides: + +- tenant-filtered semantic search; +- catalog fields needed in proposal toast: listing name, summary, price, rating, installs, + evidence summary, install deep link; +- telemetry sink `market:evidence`. + +S4 provides: + +- on-box summarization, opt-in/rate limit enforcement, confidence threshold, notification UX. + +### S5 packaging/runtime-sync + +S2 provides: + +- registry release tag and bundle hash contract; +- `solution.deployment` fields; +- permission manifest and applicability constraints. + +S5 provides: + +- bundle builder; +- signing if added; +- `recipe.yaml`; +- `m2-core-sync`/runtime-sync apply, verify, rollback. + +### S6 proposal engine + +S2 provides: + +- catalog query and listing detail API/CLI; +- job-priced listing discovery; +- evidence and stats fields for build-vs-install proposals. + +S6 provides: + +- proposal composition, cost comparison, job routing, and when to recommend install vs build. + +### Fedlearn + +S2 integrates with fedlearn by: + +- preserving fedlearn-compatible manifest fields; +- adding curation disposition `commercialize`; +- reusing scored PR + veto semantics; +- using runtime-sync as universal install path. + +Fedlearn dispositions: + +```text +promote-to-core -> m2/m2-core, free commons +commercialize -> m2/m2-market listings PR +reject/park -> no catalog listing +``` + +## 5. Options compared + +### Registry repo: `m2/m2-market` vs separate `m2/market-registry` + +- `m2/m2-market`: already present locally, contains schemas, docs, and seed solution dirs; + simplest MVP. +- `m2/market-registry`: cleaner source of truth, smaller clone for desktops, cleaner audit + and release history. + +Recommendation: use `m2/m2-market` for MVP because it exists and the brief names repo layout +`m2/m2-market`; keep paths compatible with a later split to `m2/market-registry`. + +### Catalog truth: Forgejo vs memory-api + +- Forgejo: versioned, reviewable, release assets, PR/veto, audit. +- memory-api: semantic search and tenant-scoped retrieval, but rebuildable and exposed to + split-brain risk. + +Recommendation: Forgejo is truth; `market:catalog` is derived. + +### Publish flow: direct push vs PR/veto + +- Direct push is faster but bypasses tenant and evidence review. +- PR/veto reuses fedlearn gates and gives commercial listings an audit trail. + +Recommendation: all published listings enter through PR/veto; no direct publish to main. + +### Price v1: fixed only vs fixed + job + +- Fixed only is easiest to install and ledger-settle. +- Fixed + job is locked by the brief and needed for operator-delivered services. + +Recommendation: support both in schemas and catalog; only fixed is direct-installable in MVP. + +## 6. Risks/edge cases + +- Tenant data leakage: enforce `tenant_visibility`, `tenant_scope`, owner initiation, double + scrub, evidence summary-only indexing, and CI secret scans. +- Registry/index divergence: registry wins; reindex from main is mandatory operational + recovery. +- Broken local registry route: release assets are fetched from Forgejo, not Docker + push/pull. +- Mixed desktop images: install path must go through runtime-sync/apply adapter, not baked + images. +- Paid but not applied: S2 records enough install ref and telemetry for S1/S5 refund and + remediation, but S1/S5 own transaction compensation and rollback. +- Self-purchase metric gaming: allow install but exclude from quality stats. +- Job listing confusion: direct install must refuse job-priced listings until proposal/job + flow is implemented. +- Evidence bloat or raw data indexing: catalog text embeds summaries only; evidence files + remain scrubbed in Forgejo. +- Permission creep: every permission must include kind/access/scope/target/reason and render + in PR plus `show`. +- Stats churn in registry: batch stats updates to avoid noisy PRs; retain ability to rebuild + stats from `market:evidence`. + +## 7. [NEEDS CLARIFICATION] + +1. [NEEDS CLARIFICATION] Registry remote: should MVP use the existing `m2/m2-market` repo as + the registry of record, or create/split `m2/market-registry` before the first paid install? + S2 recommends existing `m2/m2-market` for MVP. +2. [NEEDS CLARIFICATION] Veto authority and timeout: which operator/team receives listing PR + notifications, and is low-risk auto-merge after 72h allowed for commercial listings or + should every commercial listing require explicit `approved`? +3. [NEEDS CLARIFICATION] Standard license text: what exact `m2-market-standard-v1` terms + should be stored behind `license.terms` before real paid installs? diff --git a/specs/S3-storefront-m2store.md b/specs/S3-storefront-m2store.md new file mode 100644 index 0000000..1e1c8d5 --- /dev/null +++ b/specs/S3-storefront-m2store.md @@ -0,0 +1,615 @@ +# S3 — Storefront: cargstore -> M2 Store + +## 1. Scope & non-goals + +### MVP scope + +Revive `github.com/machine-machine/cargstore` as the in-desktop **M2 Store**. Keep the Electron + React app and add a Solution install backend beside the existing Flatpak backend. + +Grounding inspected 2026-07-02: + +- cargstore `main` has Electron files `electron/main.ts`, `electron/preload.ts`, `electron/flatpak.ts`, `electron/clawdbot-client.ts`; React pages/hooks under `src/`; local JSON catalog `catalog/apps.json`; Dockerized `web/` seed. +- Current app exposes IPC namespace `window.cargstore.flatpak.*`, `window.cargstore.catalog.get`, `window.cargstore.app.*`, and registers `flatpak` protocol only. +- Current catalog fields map closely to listing fields: `id/name/summary/description/category/icon/featured/keywords` plus `flatpakRef|bundleUrl`. +- Existing market repo freezes `schemas/listing.schema.json`, `schemas/solution.schema.json`, `contracts/cli.md`, `contracts/catalog-index.md`, `contracts/apply-adapter.md`, and `contracts/ledger-api.md`. +- Live host has m2-gpt healthy at `https://gpt.machinemachine.ai/health`, Coolify network, memory-api, Forgejo, and m2o desktops. `chris-m2o` runs primus; `gunnar-m2o` is an `agent-latest` canary container currently present from Coolify. + +MVP delivers: + +1. Rebrand product to **M2 Store** while preserving cargstore architecture. +2. Catalog source switch from bundled `catalog/apps.json` to `market:catalog` through the `m2-market` CLI, with local cache fallback. +3. A new Solution backend that shells `m2-market show|install|wallet --json` and emits progress/status to the renderer. +4. Solution detail page showing evidence, permissions diff, price, seller, install status, and install button. +5. Deep-link protocol `m2store://solution/` used by Scout/Hermes/toasts to open a listing detail page. +6. Identity/bootstrap path using m2-gpt-issued agent credentials and runtime-injected marketplace service keys stored in `~/.m2-market/config.toml` mode `0600`. +7. Canary desktop deployment path for `chris-m2o` and one non-client `agent-latest` desktop; broader fleet uses runtime-sync once proven. + +### Later + +- Hosted browser marketplace from cargstore `web/`. +- Service/resource/capability purchase flows beyond read-only discovery. +- Seller/admin publishing UI; builder publishing remains `m2-market publish`. +- Full self-update through store UI. Keep cargstore self-update disabled unless wired to Forgejo/Coolify release trust. +- Flatpak removal. Existing Flatpak backend may remain as an "Apps" inventory surface, but Solution install is separate and primary. + +### Non-goals + +- Ledger internals, pricing policy, wallet accounting, refunds: S1 owns ledger. +- Listing schema/indexer truth model: S2 owns catalog/registry. +- Scout matching host and proposal policy: S4 owns Scout. S3 only specifies Store deep-link and proposal event acceptance. +- Packaging images and runtime-sync implementation: S5 owns packaging. S3 consumes install artifacts. +- Hermes proposal skill logic: S6 owns proposal engine. S3 exposes open/show/install surfaces. + +## 2. User stories + acceptance criteria + +### US1 — Browse published Solutions in the desktop Store + +Given an operator is inside an m2o desktop with `~/.m2-market/config.toml` present, when M2 Store opens, then it loads tenant-visible listings from `market:catalog`, renders featured/category/search views, and never shows listings outside `tenant_visibility`. + +Acceptance: + +- Store calls `m2-market search "" --type solution --limit 50 --json` through main-process IPC. +- Response items validate as `m2.listing.v1` payloads. +- If the catalog backend is unavailable, Store renders the last successful cache from `~/.cache/m2-store/catalog.json` with an offline marker and disables install if listing detail cannot be refreshed. +- No bundled seed listing is treated as authoritative in production. + +### US2 — Inspect evidence, permissions, price, and install impact + +Given a user opens a listing, when the Solution detail page loads, then it shows price in M2 credits, seller, evidence summary, detailed evidence refs, permissions diff, applicability, install state, and rollback note before installation. + +Acceptance: + +- Store calls `m2-market show --json`. +- Detail payload includes listing + solution excerpt + `permissions_diff`. +- Install button text includes the exact credit amount. +- Install button is disabled when `applicability.image_classes` excludes the current image class, when `inventory_type != "solution"`, or when wallet balance is insufficient. +- Permission diff is rendered from structured fields, not free-form Markdown. + +### US3 — Install a Solution through the standard marketplace path + +Given the operator confirms install, when Store invokes install, then money moves before apply, grant is recorded, apply is idempotent, and failures refund through CLI semantics. + +Acceptance: + +- Store calls `m2-market install --yes --json`. +- Store never calls `m2-ledger` directly for install. +- A CLI exit code maps to UI state: + - `0`: installed or no-op. + - `2`: listing not found. + - `3`: insufficient funds, no apply attempted. + - `4`: apply failed and refund attempted. + - `5`: validation/firewall rejection. + - `6`: `m2-core-sync` rails not landed; show remediation and leave state unchanged. +- Re-running install on an already-applied listing renders "Installed" and does not create duplicate tx rows. + +### US4 — Open Store from Scout/Hermes deep link + +Given Scout proposes a Solution, when the operator clicks the toast/link, then M2 Store opens directly to the Solution detail page and records that the proposal was accepted only after the user reaches the page or clicks Install, per S4 policy. + +Acceptance: + +- Desktop registers `m2store://solution/`. +- `m2store://solution/lst_competitor-scan?source=scout&proposal_id=prop_...` opens detail route `/solution/lst_competitor-scan`. +- Unknown listing IDs render a not-found page and do not trigger install. +- Deep links cannot pass shell arguments or override backend URLs. + +### US5 — Operate without baking secrets into images + +Given a desktop is provisioned, when M2 Store starts, then it obtains identity from runtime config generated from m2-gpt-issued credentials and never ships keys in the Electron bundle, image, repo, or catalog. + +Acceptance: + +- Store reads only `~/.m2-market/config.toml` and redacted env metadata for identity. +- Config file mode must be `0600`; Store refuses install and catalog writes if permissions are broader. +- Required config keys: `operator_id`, `tenant`, `ledger_url`, `ledger_api_key`, `memory_api_url`, `memory_api_key`, `forgejo_url`, `forgejo_token`, `apply_adapter`. +- Store logs redact all `*_key`, `*_token`, and bearer-like strings. + +## 3. Interfaces & data contracts + +### 3.1 Repository and branch + +- Upstream repo: `github.com/machine-machine/cargstore`. +- Working branch for this revival: `m2-store`. +- Product metadata: + - `package.json:name`: may remain `cargstore` internally. + - `build.productName`: `M2 Store`. + - `build.appId`: `ai.machinemachine.m2store`. + - protocols: add `m2store`; keep `flatpak` only if Flatpak surface remains. + +### 3.2 Main-process modules + +Keep cargstore modules and add: + +```text +electron/ + flatpak.ts # existing, unchanged except renamed UI surface if desired + solution.ts # new: shells m2-market CLI + market-config.ts # new: reads/validates ~/.m2-market/config.toml, redacts logs + catalog-client.ts # new: CLI-backed market:catalog read + cache + deep-link.ts # new: protocol parse/route handoff + m2-agent-client.ts # renamed/reworked clawdbot-client.ts, still ws://localhost:18789 +``` + +### 3.3 Renderer model + +Replace `CatalogApp` with `StoreItem` while keeping field compatibility for cards: + +```ts +type InventoryType = "solution" | "capability" | "resource" | "service" | "flatpak"; + +interface StoreItem { + id: string; // listing_id for market, flatpak app id for flatpak + backend: "solution" | "flatpak"; + inventory_type: InventoryType; + name: string; + summary: string; + description?: string; + category: string; + icon?: string; + featured?: boolean; + keywords: string[]; + price?: { amount: number; currency: "m2cr"; model: string }; + seller?: string; + stats?: { installs: number; rating?: number; proposals_shown: number; proposals_accepted: number }; + install_ref?: string; +} +``` + +### 3.4 Listing/detail payload + +IPC `solution:show` returns: + +```json +{ + "listing": { + "schema_version": "m2.listing.v1", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "solution_version": "1.0.0", + "inventory_type": "solution", + "name": "Competitor Scan", + "summary": "Research report with sources and recommendations", + "category": "research", + "keywords": ["research", "competitor", "report"], + "icon": "solution-research", + "price": { "amount": 40, "currency": "m2cr", "model": "fixed" }, + "seller": "op_sdjs", + "evidence_summary": "3 successful report runs, median 42 min", + "tenant_visibility": ["*"], + "stats": { "installs": 0, "proposals_shown": 0, "proposals_accepted": 0 }, + "status": "published", + "install_ref": "lst_competitor-scan-v1.0.0" + }, + "solution": { + "schema_version": "m2.solution.v1", + "permissions": [ + { "kind": "memory", "partition": "market:evidence", "access": "write" }, + { "kind": "filesystem", "path": "~/.local/share/m2-solutions/competitor-scan", "access": "write" } + ], + "deployment": { + "recipe_ref": "recipe.yaml", + "entrypoint": "hermes skill competitor-scan", + "verify_command": "m2-solution verify competitor-scan" + }, + "applicability": { + "image_classes": ["primus", "agent-latest"], + "roles": ["any"], + "tool_requirements": ["m2-market-cli"] + }, + "evidence": [ + { "source": "forgejo:m2/market-registry/.../evidence.json", "machine": "chris-m2o", "tokens": 120000, "wall_time": 2520 } + ], + "license": { "terms": "M2 internal commercial license", "major_version_coverage": true } + }, + "wallet": { "operator_id": "op_chris", "balance": 100 }, + "install_state": { "status": "not_installed" }, + "permissions_diff": [ + { "kind": "filesystem", "target": "~/.local/share/m2-solutions/competitor-scan", "current": "absent", "requested": "write", "risk": "low" } + ] +} +``` + +The detail page must not fetch Forgejo raw evidence directly from the renderer. The main process/CLI owns network I/O and returns sanitized JSON. + +### 3.5 Electron preload API + +Expose a new namespace while keeping old Flatpak namespace for compatibility: + +```ts +contextBridge.exposeInMainWorld("m2store", { + window: { minimize, maximize, close }, + catalog: { + search: (query: string, opts?: { type?: string; limit?: number }) => ipcRenderer.invoke("catalog:search", query, opts), + getFeatured: () => ipcRenderer.invoke("catalog:featured"), + getCached: () => ipcRenderer.invoke("catalog:cached") + }, + solution: { + show: (listingId: string) => ipcRenderer.invoke("solution:show", listingId), + install: (listingId: string) => ipcRenderer.invoke("solution:install", listingId), + onProgress: (cb) => ipcRenderer.on("solution:progress", (_e, data) => cb(data)), + openEntrypoint: (listingId: string) => ipcRenderer.invoke("solution:open-entrypoint", listingId) + }, + wallet: { + get: () => ipcRenderer.invoke("wallet:get") + }, + app: { + getVersion, + getIdentity: () => ipcRenderer.invoke("app:get-identity") + } +}); +``` + +IPC contracts: + +```text +catalog:search(query, {type, limit}) -> {items: StoreItem[], source: "live"|"cache", as_of} +catalog:featured() -> same, query="" limit=50, featured derived from listing stats/category policy +solution:show(listing_id) -> detail payload above +solution:install(listing_id) -> InstallResult +wallet:get() -> {operator_id,balance,as_of} +app:get-identity() -> {operator_id, tenant, desktop, image_class, apply_adapter, config_path} +``` + +`InstallResult`: + +```json +{ + "ok": true, + "listing_id": "lst_competitor-scan", + "status": "applied", + "grant_id": "gr_...", + "entrypoint": "hermes skill competitor-scan", + "state_path": "~/.m2-market/state.json" +} +``` + +### 3.6 CLI calls + +Store backend shells the CLI, never ad hoc HTTP for installation: + +```bash +m2-market search "$QUERY" --type solution --limit 50 --json +m2-market show "$LISTING_ID" --json +m2-market install "$LISTING_ID" --yes --json +m2-market wallet --json +``` + +Execution requirements: + +- Use `spawnFile`/argument array, not shell interpolation. +- Hard allowlist command path from config: default `m2-market` found on `PATH`; optional absolute override `M2_MARKET_CLI=/usr/local/bin/m2-market`. +- Timeout: search/show/wallet 15s, install no hard short timeout but stream heartbeat; UI can cancel only before CLI begins apply. +- Parse JSON from stdout; stderr is redacted and logged to `~/.local/state/m2-store/logs/main.log`. + +### 3.7 Config and identity + +MVP config remains the existing CLI config: + +```toml +# ~/.m2-market/config.toml +operator_id = "op_chris" +tenant = "machine-machine" +ledger_url = "http://m2-ledger:8000" +ledger_api_key = "svc_xxx" +memory_api_url = "https://memory.machinemachine.ai" +memory_api_key = "mem_xxx" +forgejo_url = "https://git.machinemachine.ai" +forgejo_token = "frg_xxx" +apply_adapter = "local" +``` + +Identity issuance contract with m2-gpt: + +- m2-gpt remains the authority for tenant/agent identity and budget substrate. +- Existing pattern: `scripts/add-agent.sh ` and `scripts/mint-agent-key.sh ` create a tenant-scoped agent and a one-time `sk-m2-...` bearer, hashed at rest. +- Add market bootstrap as an m2-gpt-admin operation, not a Store operation: + +```http +POST /admin/v1/tenants/{tenant_id}/agents/{agent_id}/market-credentials +Authorization: Bearer +Body: { + "operator_id": "op_chris", + "desktop": "chris-m2o", + "scopes": ["catalog:read", "install:write", "ledger:service", "forgejo:read-release"], + "ttl_days": 90 +} +200 { + "operator_id": "op_chris", + "tenant": "machine-machine", + "ledger_url": "http://m2-ledger:8000", + "ledger_api_key": "svc_...", + "memory_api_url": "https://memory.machinemachine.ai", + "memory_api_key": "mem_...", + "forgejo_url": "https://git.machinemachine.ai", + "forgejo_token": "frg_...", + "expires_at": "2026-09-30T00:00:00Z" +} +``` + +The provisioner or a one-shot desktop bootstrap writes this response to `~/.m2-market/config.toml` with mode `0600`. Store only reads it. It does not mint or refresh admin credentials itself in MVP. + +### 3.8 Catalog source switch + +Current cargstore: + +- `catalog:get` reads bundled `catalog/apps.json`. +- Renderer does local string matching in `useCatalog`. + +M2 Store: + +- `catalog-client.ts` calls `m2-market search`. +- `StoreItem.id = listing.listing_id`. +- Cache path: `~/.cache/m2-store/catalog.json`. +- Cache shape: + +```json +{ + "schema_version": "m2.store.catalog-cache.v1", + "as_of": "2026-07-02T00:00:00Z", + "tenant": "machine-machine", + "items": [] +} +``` + +The bundled `catalog/apps.json` is permitted only as dev fallback when `NODE_ENV=development`. + +### 3.9 Solution backend beside Flatpak + +Current `FlatpakManager` persists Flatpak data under `FLATPAK_USER_DIR || /clawdbot_home/flatpak`. m2o primus docs say user installs persist under `/agent_home/flatpak` and home is `/agent_home/home/developer`. + +Add `SolutionManager`: + +```ts +class SolutionManager { + show(listingId: string): Promise; + install(listingId: string, onProgress?: (event: SolutionProgress) => void): Promise; + listInstalled(): Promise; + openEntrypoint(listingId: string): Promise; +} +``` + +Installed state comes from `~/.m2-market/state.json`: + +```json +{ + "installs": { + "lst_competitor-scan": { + "version": "1.0.0", + "grant_id": "gr_...", + "ts": "2026-07-02T00:00:00Z", + "changeset_hash": "sha256:...", + "status": "applied" + } + } +} +``` + +### 3.10 Deep-link protocol + +Register: + +```json +{ + "name": "M2 Store", + "schemes": ["m2store"] +} +``` + +Supported URLs: + +```text +m2store://solution/ +m2store://solution/?source=scout&proposal_id= +m2store://search?q= +``` + +Rules: + +- `listing_id` must match `^lst_[a-z0-9-]+$`. +- Query params allowed: `source`, `proposal_id`, `q`. Ignore all others. +- No `file:`, `http:`, shell, adapter, or backend override params. +- If app already running, focus existing window and navigate via renderer event `deeplink:navigate`. +- Scout/Hermes use `m2store://solution/` as the stable contract. Do not use `m2store://solution/` because listing controls visibility, price, and version. + +### 3.11 Agent WebSocket integration + +Current `ClawdbotClient` connects to `ws://localhost:18789` and registers app-store tools. Keep transport but rename semantics: + +```json +{ + "type": "register_tools", + "tools": [ + { "name": "m2_store_search", "description": "Search M2 Store Solutions", "parameters": { "query": "string" } }, + { "name": "m2_store_show", "description": "Open a Solution detail page", "parameters": { "listing_id": "string" } }, + { "name": "m2_store_install", "description": "Request human-confirmed install flow for a Solution", "parameters": { "listing_id": "string" } }, + { "name": "m2_store_wallet", "description": "Show wallet balance", "parameters": {} } + ] +} +``` + +`m2_store_install` must navigate to detail and require human button click. It must not silently install on tool call. + +### 3.12 Telemetry write-back + +Store writes only summarized events through CLI or a future `m2-market telemetry` verb. Until that verb exists, append local JSONL: + +```text +~/.local/share/m2-store/events/YYYY-MM-DD.jsonl +``` + +Event: + +```json +{ + "schema_version": "m2.store.event.v1", + "ts": "2026-07-02T00:00:00Z", + "desktop": "chris-m2o", + "operator_id": "op_chris", + "tenant": "machine-machine", + "event": "detail_opened|install_clicked|install_succeeded|install_failed", + "listing_id": "lst_competitor-scan", + "source": "browse|search|scout|hermes|deeplink", + "proposal_id": "prop_optional", + "error_code": null +} +``` + +Batch ship target, owned by S2/S4 path: memory partition `market:evidence`. + +## 4. Integration contract with other subsystems + +### S1 Ledger + +Touchpoints: + +- Store calls ledger only through `m2-market install|wallet`. +- `m2-market install` performs `POST /tx/install`, grant, apply, verify, and refund on apply failure. +- Store displays `402 insufficient_funds` from CLI exit 3 and wallet balance from `m2-market wallet`. + +S3 requires S1 to keep `/tx/install` idempotent by `ref` and `GET /balance/{operator_id}` stable. + +### S2 Catalog/registry + +Touchpoints: + +- Store consumes `m2.listing.v1` and `m2.solution.v1`. +- Store reads semantic catalog via `m2-market search` backed by `market:catalog`. +- Store detail reads the registry-backed full Solution via `m2-market show`. +- Store expects `tenant_visibility` filtering to happen before results reach renderer. + +S3 requires S2 to expose a stable `m2-market show --json` payload including `permissions_diff`. + +### S4 Scout + +Touchpoints: + +- Scout opens `m2store://solution/?source=scout&proposal_id=`. +- Store emits local events with `source=scout` and `proposal_id`. +- Store never performs install from Scout without human detail-page confirmation. + +### S5 Packaging/deployment + +Touchpoints: + +- Store package can be deployed by bake or runtime-sync, but Solutions always install through `m2-market` and the apply adapter. +- MVP canary install path: + - install M2 Store artifact into `/opt/m2-store` or `~/.local/share/m2-store`; + - create desktop entry/protocol handler in the user home volume; + - ensure `m2-market` CLI and config exist; + - canary `chris-m2o` first, then non-client `agent-latest`. +- Fleet-wide deployment waits for S5 image/runtime policy; local registry public route remains broken and must not be assumed. + +### S6 Hermes proposal engine + +Touchpoints: + +- Hermes can call `m2_store_search/show` tools via existing local gateway WebSocket if present. +- Hermes proposals should link to `m2store://solution/` or invoke `m2_store_show`. +- Store does not own build-vs-install reasoning. + +### m2-gpt identity + +Touchpoints: + +- m2-gpt continues tenant/agent identity and budget/metering authority. +- Provisioner/admin mints m2-gpt agent key and market service credentials. +- Store consumes the generated `~/.m2-market/config.toml`. + +### memory-api + +Touchpoints: + +- Store reads through CLI-backed `market:catalog`. +- Store event summaries eventually ship to `market:evidence`. +- Store does not write raw client data or raw session content. + +## 5. Options compared + +### Catalog access + +Option A: Store talks directly to memory-api. + +- Pros: fewer subprocesses, more interactive search. +- Cons: duplicates auth, filtering, and payload logic in Electron; larger secret surface. + +Option B: Store shells `m2-market search/show --json`. + +- Pros: reuses frozen CLI contracts, tenant filtering, registry fetch, future adapter changes; lowest blast radius. +- Cons: subprocess/error parsing and CLI dependency. + +Recommendation: **B for MVP**. Direct HTTP can be added later after CLI contracts harden. + +### Install backend + +Option A: Replace Flatpak with Solution-only. + +- Pros: simpler product story. +- Cons: throws away working app install feature and cargstore code. + +Option B: Add `SolutionManager` beside `FlatpakManager`. + +- Pros: preserves existing asset, permits Apps and Solutions tabs, isolates paid install path. +- Cons: renderer model needs backend discriminator. + +Recommendation: **B**. Make Solutions default surface. + +### Store deployment + +Option A: Bake M2 Store into primus. + +- Pros: available on every new desktop; protocol registration predictable. +- Cons: slower iteration; agent-latest remains mixed; local registry route is broken. + +Option B: Runtime-sync/install into agent-home on canaries. + +- Pros: respects mixed fleet and fedlearn universal path; reversible; canary-first. +- Cons: protocol/desktop entry must be installed per desktop until image bake. + +Recommendation: **B for MVP canaries**, then bake once S5 certifies it. + +### Identity storage + +Option A: Store calls m2-gpt to mint/refresh credentials. + +- Pros: self-service. +- Cons: Store would need high-privilege bootstrap token. + +Option B: Admin/provisioner calls m2-gpt; Store reads scoped runtime config. + +- Pros: no admin secret in desktop app; matches `M2_GPT_API_KEY` injection pattern. +- Cons: requires bootstrap step. + +Recommendation: **B**. + +### Deep-link ID + +Option A: `m2store://solution/`. + +- Problem: solution ID omits price, visibility, and version. + +Option B: `m2store://solution/`. + +- Pros: listing is the catalog-facing, tenant-filtered, priced unit. + +Recommendation: **B**. + +## 6. Risks/edge cases + +- Tenant leakage: renderer must never bypass CLI tenant filters; detail fetch must 404 for invisible listings. +- Secrets: `~/.m2-market/config.toml` contains multiple live keys; require `0600`, redact logs, and never expose full config through preload. +- Shell injection: all CLI calls must use argument arrays and validate listing IDs before spawning. +- Duplicate installs: rely on CLI/ledger idempotency and local state; Store button must debounce while install is running. +- Partial failure: install may debit then fail apply; Store must surface CLI exit 4 and refund status exactly. +- Offline catalog: cache is for browsing only; install requires fresh `show` unless CLI can verify cache freshness. +- Protocol abuse: deep links must only navigate. No params can trigger installation. +- Mixed fleet: agent-latest may lack Hermes/WebSocket or protocol handler support; Store must still work through direct UI/CLI. +- Registry route: do not depend on public local registry push/pull for Store deployment. +- Self-update trust: cargstore self-update currently downloads GitHub releases and extracts into `/opt/cargstore`; disable or rework before production to avoid unreviewed mutation path. +- Flatpak persistence path mismatch: cargstore default `/clawdbot_home/flatpak` differs from m2o primus `/agent_home/flatpak`; set `FLATPAK_USER_DIR=/agent_home/flatpak` if Flatpak surface remains. +- Pricing models: current `solution.schema.json` only allows fixed price even brief mentions job-based pricing. Store UI must render unknown future `price.model` read-only until S1/S2 extend schemas. + +## 7. [NEEDS CLARIFICATION] + +1. [NEEDS CLARIFICATION] Exact m2-gpt endpoint for issuing marketplace service credentials does not exist in inspected code. Spec defines the required contract; implementation owner must decide whether it lands in m2-gpt admin API or a bootstrap script first. +2. [NEEDS CLARIFICATION] `m2-market show --json` must include `permissions_diff`, but current contracts mention it narratively, not as a frozen JSON schema. S2/CLI should freeze the detail payload before Store implementation. +3. [NEEDS CLARIFICATION] Whether Flatpak apps remain visible in M2 Store MVP or are hidden behind a secondary "Apps" tab for canaries. Backend can support either; product default should be set before UI polish. diff --git a/specs/S4-solution-scout.md b/specs/S4-solution-scout.md new file mode 100644 index 0000000..380c023 --- /dev/null +++ b/specs/S4-solution-scout.md @@ -0,0 +1,537 @@ +# S4 — Solution Scout Spec + +## 1. Scope & non-goals + +### MVP scope + +Solution Scout v0 is a per-desktop, propose-only service that notices when an operator is already working on a task that a published M2 Marketplace listing may cover, then surfaces a non-blocking proposal. + +Recommended host: **standalone supervised watcher** inside each desktop container, deployed canary-first on `chris-m2o`, then one `agent-latest` canary such as `gunnar-m2o`. It reads only already-structured local summaries, writes only proposal telemetry, and calls the existing catalog/store/install contracts. + +MVP responsibilities: + +- Watch safe input taps: + - herdr run summaries from `~/.herdr/runs/*.md` and herdr session metadata from `~/.config/herdr/session.json` when present. + - Hermes/OpenClaw session summaries only if the harness writes an explicit summary file or emits a summary event. No raw chat logs unless another subsystem has already summarized and scrubbed them. + - Optional window title summaries only when explicitly enabled in policy; default off. +- Summarize on-box before any external call. The Scout sends a short intent summary, not raw workspace/client text. +- Query the semantic catalog through memory-api using partition/agent namespace `market:catalog`, tenant-filtered by the caller tenant. +- Decide whether to propose using confidence, coverage, recency, rate-limit, dismissal cooldown, and policy thresholds. +- Surface proposals as: + - herdr/XFCE toast with a deep link into M2 Store. + - Hermes chat surfacing when Hermes is active and exposes the market-propose bridge. +- Persist local proposal state and write accept/dismiss/show telemetry to `market:evidence`. +- Respect opt-in config in `~/.config/m2-market/pull-policy.toml`. + +### Later scope + +- Full Hermes-native agent reasoning for build-vs-install proposals belongs to S6. S4 only hands Hermes a candidate proposal event. +- Automatic install is out of scope. Human clicks through M2 Store and `m2-market install` handles debit/grant/apply. +- Raw keystroke capture, screen recording OCR, shell history capture, browser DOM scraping, and unreviewed client data extraction are explicitly out of scope. +- Fleet-wide rollout, packaging format, catalog truth, storefront implementation, and ledger internals are owned by S5/S2/S3/S1 respectively. + +## 2. User stories + acceptance criteria + +### Story 1 — Opt-in canary proposal + +Given `chris-m2o` has `~/.config/m2-market/pull-policy.toml` with `scout.enabled = true` +and a herdr summary describes a competitor research report, +when Scout runs its poll loop, +then it creates an on-box intent summary, queries `market:catalog` within the machine tenant, and shows at most one proposal for `competitor-scan` if score and coverage exceed thresholds. + +Acceptance: + +- The payload sent to memory-api contains no raw run body longer than `summary.max_chars`. +- The proposal contains listing name, price, confidence/coverage, install count/rating if present, and Store deep link. +- `~/.local/share/m2-market/scout/state.json` records the proposal id and cooldown. + +### Story 2 — Human-controlled install path + +Given a proposal toast is shown, +when the operator clicks Open, +then M2 Store opens `m2store://listing/?proposal_id=&source=scout`. + +Acceptance: + +- Scout never calls `m2-market install` directly. +- Store/CLI remain responsible for ledger `/tx/install`, license grant, bundle verification, apply adapter, and refund on failure. +- Scout observes accept/installed telemetry only after Store/CLI emits an explicit event. + +### Story 3 — Dismissal tunes future proposals + +Given an operator dismisses a proposal, +when the same listing/task pair appears again within the configured cooldown, +then Scout suppresses the proposal and writes a dismiss event to evidence. + +Acceptance: + +- Dismissal is keyed by `{tenant_id, operator_id, desktop_id, listing_id, intent_hash}`. +- Dismiss telemetry is stored with a summary hash and reason, not client content. +- Listing evidence can fold `proposals_shown` and `proposals_accepted` into `listing.stats`. + +### Story 4 — Tenant firewall + +Given the desktop belongs to `sdjs`/GST or another client tenant, +when Scout queries the catalog, +then only listings with `tenant_visibility` containing that tenant or `"*"` are eligible. + +Acceptance: + +- Empty tenant filter is a hard error, not an unscoped search. +- Tenant-private summaries never become shared catalog data. +- Owner-initiated commercialization and double scrub remain packaging/catalog obligations, not Scout behavior. + +## 3. Interfaces & data contracts + +### Process and deployment + +Process name: `m2-solution-scout` + +MVP placement: + +- Desktop binary: `/usr/local/bin/m2-solution-scout` +- Supervisor config: `/etc/supervisor/conf.d/m2-solution-scout.conf` +- User config: `~/.config/m2-market/pull-policy.toml` +- Local state: `~/.local/share/m2-market/scout/state.json` +- Local event outbox: `~/.local/share/m2-market/scout/outbox/YYYY-MM-DD.jsonl` +- Logs: `~/.local/share/m2-market/scout/scout.log` + +Deploy via runtime-sync / apply adapter, not image bake, until canary acceptance proves it. The service must tolerate missing herdr/Hermes/OpenClaw sources and run as degraded rather than crash-looping. + +### `pull-policy.toml` + +```toml +[scout] +enabled = false +mode = "suggest" # enum: off|suggest. No take-control mode in S4. +desktop_id = "chris-m2o" +operator_id = "op_chris" +tenant_id = "machine-machine" +min_score = 0.78 +min_coverage = 0.55 +poll_interval_seconds = 90 +max_proposals_per_day = 4 +max_proposals_per_session = 1 +dismiss_cooldown_days = 14 +proposal_cooldown_minutes = 45 + +[scout.sources] +herdr_runs = true +session_summaries = true +window_titles = false +raw_keystrokes = false +raw_client_data = false + +[scout.summary] +max_source_chars = 12000 +max_summary_chars = 900 +redact_secrets = true +redact_client_identifiers = true +fail_closed_on_secret = true + +[scout.catalog] +memory_api_url = "https://memory.machinemachine.ai" +agent_id = "market:catalog" +limit = 5 +routing_strategy = "standard" + +[scout.store] +deeplink_scheme = "m2store" +fallback_command = "m2-market show --json" + +[scout.telemetry] +enabled = true +agent_id = "market:evidence" +batch_seconds = 60 +``` + +Policy rules: + +- `enabled=false` disables all watching and all outbound calls. +- `tenant_id` is required. If missing, Scout exits non-zero and writes no events. +- `raw_keystrokes` and `raw_client_data` are reserved deny keys. If true, Scout refuses to start. +- All keys are runtime-injected/configured. No secrets in this file except references to existing machine config; memory/ledger keys remain in `~/.m2-market/config.toml` with mode `0600`. + +### Input event schema + +Scout normalizes safe local taps into `ScoutObservation`. + +```json +{ + "schema_version": "m2.scout.observation.v1", + "observation_id": "obs_20260702_chris_01j1", + "desktop_id": "chris-m2o", + "operator_id": "op_chris", + "tenant_id": "machine-machine", + "source": "herdr_run", + "source_uri": "file:///home/developer/.herdr/runs/2026-07-02-fedlearn-seed.md", + "source_ts": "2026-07-02T00:15:00Z", + "session_id": "herdr:2026-07-02-fedlearn-seed", + "safe_excerpt": "Short scrubbed excerpt or heading only", + "source_hash": "sha256:...", + "metadata": { + "image_class": "primus", + "harness": "herdr" + } +} +``` + +Constraints: + +- `safe_excerpt` is optional and capped by policy; raw source body is never persisted in telemetry. +- `source_hash` is computed from local source text for dedup, but only the hash leaves the box. +- For herdr MVP, source files are the live path verified on `chris-m2o`: `~/.herdr/runs/*.md`; config/session files are under `~/.config/herdr/`. + +### On-box intent summary schema + +```json +{ + "schema_version": "m2.scout.intent_summary.v1", + "summary_id": "sum_20260702_chris_01j1", + "observation_ids": ["obs_20260702_chris_01j1"], + "desktop_id": "chris-m2o", + "operator_id": "op_chris", + "tenant_id": "machine-machine", + "session_id": "herdr:2026-07-02-fedlearn-seed", + "summary": "Operator is preparing a competitor research report with source gathering and final PDF/report output.", + "intent_terms": ["competitor research", "report", "sources", "PDF"], + "negative_terms": ["private payroll", "credentials"], + "scrub_status": { + "secrets_redacted": true, + "client_identifiers_redacted": true, + "failed_closed": false + }, + "intent_hash": "sha256:...", + "created_at": "2026-07-02T00:35:00Z" +} +``` + +Summarization can be deterministic extractive in MVP. If an LLM is used later, it must call m2-gpt with the desktop tenant key and still send only the capped summary onward. + +### Catalog query + +Use existing memory-api `/memory/search` surface rather than a new Scout API. + +Request: + +```http +POST https://memory.machinemachine.ai/memory/search +X-API-Key: +Content-Type: application/json +``` + +```json +{ + "query": "Operator is preparing a competitor research report with source gathering and final PDF/report output.", + "agent_id": "market:catalog", + "routing_strategy": "standard", + "limit": 5, + "tenant_id": ["machine-machine", "*"], + "filters": { + "tenant_id": ["machine-machine", "*"] + } +} +``` + +Response is the live `RoutedSearchResponse` shape from memory-api: `{results, routing}`. Each result must carry listing payload metadata matching `schemas/listing.schema.json`. The matcher discards any hit whose listing payload is missing, delisted, wrong tenant, or below threshold. + +### Match decision schema + +```json +{ + "schema_version": "m2.scout.match.v1", + "match_id": "mat_20260702_chris_01j1", + "summary_id": "sum_20260702_chris_01j1", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "tenant_id": "machine-machine", + "score": 0.84, + "coverage": 0.72, + "reasons": [ + "intent overlaps listing intent", + "listing supports report generation", + "price/evidence present" + ], + "suppressed": false, + "suppression_reason": null, + "created_at": "2026-07-02T00:35:03Z" +} +``` + +Coverage is Scout's local estimate of how much of the current intent is covered by the listing. It is not authoritative pricing. S6 owns full build-vs-install composition. + +### Proposal schema + +```json +{ + "schema_version": "m2.scout.proposal.v1", + "proposal_id": "prp_20260702_chris_01j1", + "match_id": "mat_20260702_chris_01j1", + "desktop_id": "chris-m2o", + "operator_id": "op_chris", + "tenant_id": "machine-machine", + "listing": { + "listing_id": "lst_competitor-scan", + "name": "Competitor Scan", + "summary": "Research report package for competitor positioning.", + "price": {"amount": 40, "currency": "m2cr", "model": "fixed"}, + "stats": {"installs": 12, "rating": 4.6, "proposals_shown": 20, "proposals_accepted": 7} + }, + "confidence": 0.84, + "coverage": 0.72, + "deeplink": "m2store://listing/lst_competitor-scan?proposal_id=prp_20260702_chris_01j1&source=scout", + "shown_at": "2026-07-02T00:35:04Z", + "expires_at": "2026-07-02T01:20:04Z" +} +``` + +Toast copy should be non-blocking and specific: + +`This looks like Competitor Scan: ~72% coverage, 40 m2cr. Open in M2 Store?` + +### Proposal UX interfaces + +MVP toast command: + +```bash +notify-send "M2 Solution Scout" "This looks like Competitor Scan: ~72% coverage, 40 m2cr. Open in M2 Store?" --action=open="Open" --action=dismiss="Dismiss" +``` + +If `notify-send` actions are unreliable in XFCE/VNC, fallback to herdr toast delivery. The existing herdr config supports `[ui.toast] delivery = "herdr"`, verified in `/home/m2/m2o/desktop/herdr/config.toml`. + +M2 Store deep link: + +```text +m2store://listing/{listing_id}?proposal_id={proposal_id}&source=scout +``` + +Store fallback command: + +```bash +m2-market show {listing_id} --json +``` + +Hermes surfacing event, written to a local bridge file/socket for the `market-propose` skill: + +```json +{ + "schema_version": "m2.hermes.market_proposal.v1", + "event_type": "market.proposal.available", + "proposal_id": "prp_20260702_chris_01j1", + "session_id": "herdr:2026-07-02-fedlearn-seed", + "message": "I found an existing Solution that may cover this task.", + "deeplink": "m2store://listing/lst_competitor-scan?proposal_id=prp_20260702_chris_01j1&source=scout" +} +``` + +The Hermes bridge is best-effort in MVP. If unavailable, toast + Store link is sufficient. + +### Telemetry event schema + +Telemetry is append-only, locally spooled, then written to memory-api `market:evidence` via `/memory/store`. + +```json +{ + "schema_version": "m2.scout.telemetry.v1", + "event_id": "evt_20260702_chris_01j1", + "event_type": "proposal_shown", + "proposal_id": "prp_20260702_chris_01j1", + "listing_id": "lst_competitor-scan", + "solution_id": "sol_competitor-scan", + "operator_id": "op_chris", + "desktop_id": "chris-m2o", + "tenant_id": "machine-machine", + "session_id": "herdr:2026-07-02-fedlearn-seed", + "intent_hash": "sha256:...", + "score": 0.84, + "coverage": 0.72, + "reason": null, + "created_at": "2026-07-02T00:35:04Z" +} +``` + +Allowed `event_type`: + +- `proposal_shown` +- `proposal_opened` +- `proposal_dismissed` +- `proposal_accepted` +- `install_started` +- `install_succeeded` +- `install_failed` +- `proposal_suppressed` + +Memory write: + +```http +POST https://memory.machinemachine.ai/memory/store +X-API-Key: +Content-Type: application/json +``` + +```json +{ + "content": "proposal_shown lst_competitor-scan score=0.84 coverage=0.72", + "agent_id": "market:evidence", + "memory_type": "semantic", + "importance": 0.4, + "source": "system", + "tenant_id": "machine-machine", + "session_id": "herdr:2026-07-02-fedlearn-seed", + "metadata": { + "schema_version": "m2.scout.telemetry.v1", + "event_id": "evt_20260702_chris_01j1", + "event_type": "proposal_shown", + "listing_id": "lst_competitor-scan", + "proposal_id": "prp_20260702_chris_01j1", + "intent_hash": "sha256:..." + } +} +``` + +Telemetry must be batchable and retryable. If memory-api is down, Scout keeps local JSONL outbox and continues proposing within local rate limits. + +### CLI verbs + +Scout management CLI: + +```bash +m2-solution-scout run +m2-solution-scout once --json +m2-solution-scout status --json +m2-solution-scout dismiss [--reason not-relevant] +m2-solution-scout open +m2-solution-scout flush-telemetry +``` + +`once --json` is the canary acceptance hook: read sources, summarize, query, and emit a proposal or suppression decision without daemonizing. + +## 4. Integration contract with other subsystems + +### S1 ledger + +Scout does not debit credits. It includes listing price in the proposal and links to Store. Install remains: + +`M2 Store -> m2-market install -> POST /tx/install -> license grant -> apply -> telemetry` + +Scout listens for Store/CLI install result events and writes evidence only. Refund behavior remains S1/S3/CLI. + +### S2 catalog + +Scout reads `market:catalog` only through memory-api `/memory/search` using `agent_id = "market:catalog"`. Listing payload must validate against `/home/m2/m2-market/schemas/listing.schema.json`. + +Scout writes proposal evidence to `market:evidence`. S2/indexer folds proposal counts into `listing.stats.proposals_shown` and `listing.stats.proposals_accepted`. + +### S3 store + +Scout proposal opens: + +`m2store://listing/{listing_id}?proposal_id={proposal_id}&source=scout` + +Store shows evidence, price, permission diff, and calls `m2-market install`. Store should emit local result events: + +- `~/.local/share/m2-market/events/install-events.jsonl`, or +- a local socket declared by S3. + +Until S3 finalizes this, Scout can infer only `proposal_opened` and `proposal_dismissed`. + +### S5 packaging + +Scout is installed as a Solution/fleet component via the same runtime-sync path and canary policy. It must not require rebuilding primus or agent-latest images. Package should include: + +- binary/script +- supervisor conf +- default disabled `pull-policy.toml` +- rollback removing supervisor entry and disabling the daemon + +### S6 proposal engine + +Scout is push-side discovery. Hermes/S6 is pull-side composition. Scout hands candidate listing evidence to Hermes but does not produce full alternatives such as install+adapt vs operator-assisted vs full custom. + +S6 may consume `m2.hermes.market_proposal.v1` and `market:evidence`; it must preserve Scout's privacy boundary. + +### Existing live system touchpoints + +- Desktop canary: `chris-m2o` is running on `coolify`, has `/usr/local/bin/herdr`, `~/.herdr/runs/*.md`, `~/.config/herdr/`, and supervised `hermes-gateway`. +- Agent-latest canaries exist on `coolify`, including `gunnar-m2o-...`, `nasr-m2o-...`, `parlobyg-m2o-...`, and `peter-desktop-...`. +- memory-api public health is live at `https://memory.machinemachine.ai/health`; internal desktop alias is `http://memory-api:8000` after fedlearn resolver hardening. +- m2-gpt public health is live at `https://gpt.machinemachine.ai/health`; Hermes config points at `https://gpt.machinemachine.ai/v1` with `M2_GPT_API_KEY` injected at runtime. +- New services/components must be canary-first, idempotent, reversible, and use Coolify/runtime envs for secrets. + +## 5. Options compared + +### Option A — Standalone supervised watcher + +What it is: a small daemon/process on each desktop, managed like `hermes-gateway`, reading safe local summary files/events and calling catalog APIs directly. + +Pros: + +- Harness-agnostic: works for Hermes, herdr, OpenClaw, and future OpenAI-wire clients. +- Least coupling to harness release cycles and plugin APIs. +- Clear privacy boundary: only the watcher owns taps, summaries, policy, and telemetry. +- Can be deployed by runtime-sync to mixed primus/agent-latest images. +- Failure is isolated; if Scout breaks, Hermes and herdr keep working. + +Cons: + +- Needs its own supervisor config, local state, and policy file. +- Less direct access to Hermes intent than a Hermes plugin. +- Needs a small bridge to surface in Hermes chat. + +### Option B — Hermes plugin + +What it is: implement Scout inside Hermes or as a Hermes skill/plugin. + +Pros: + +- Sees user intent directly in the primary conversational surface. +- Natural chat UX for explaining build-vs-install options. +- Reuses m2-gpt identity and model path. + +Cons: + +- Hermes-specific; OpenClaw and non-Hermes OpenAI-wire clients lose push discovery. +- Higher surveillance risk because it is closer to raw chat/tool context. +- Plugin failures can degrade the primary agent experience. +- herdr run lifecycle signals still need a separate bridge. + +### Option C — herdr plugin + +What it is: implement Scout as a herdr extension using herdr lifecycle events and run summaries. + +Pros: + +- Strongest access to structured lifecycle/run context. +- Aligns with live `~/.herdr/runs/*.md` capture source and toast delivery. +- Lowest risk of raw chat access if limited to run summaries. + +Cons: + +- Misses Hermes-only or OpenClaw-only sessions that do not run through herdr. +- Couples marketplace discovery to herdr's extension surface and lifecycle. +- Still needs Hermes and Store bridges. + +### Recommendation + +Choose **Option A: standalone supervised watcher** for MVP. + +Reason: S4's hard requirements are privacy, tenant isolation, mixed-fleet installability, and harness-agnostic discovery. A standalone watcher best satisfies those while still consuming herdr summaries and surfacing through Hermes. Hermes and herdr should be integrations, not the host boundary. + +## 6. Risks/edge cases + +- Surveillance creep: prohibit raw keystrokes/client data in config and startup validation; default disabled; cap source and summary sizes; make summary/outbound payload inspectable with `once --json`. +- Tenant leakage: require tenant_id and use tenant-filtered catalog queries. Empty tenant filters fail closed. Telemetry stores intent hashes and short synthetic content only. +- Bad recommendations/spam: enforce daily/session limits, cooldowns, min score, min coverage, repeated-dismiss suppression, and proposal expiry. +- Junk listing amplification: proposal acceptance/dismiss telemetry should affect listing evidence but not bypass S2 curation. Published listing status remains registry-controlled. +- Memory-api split-brain/outage: Scout treats catalog search as best-effort. No proposal is better than unscoped or stale proposals. Telemetry spools locally. +- Secrets: Scout reads credentials from existing `~/.m2-market/config.toml`; never commits or logs keys. Log redaction is mandatory. +- Host fragility: deploy canary-first, supervisor autorestart with backoff, health/status command, rollback by disabling supervisor and leaving state intact. +- Client tenant desktops: client identifiers can appear in herdr summaries. Summarizer must redact known tenant/client names where policy says shared telemetry; for tenant-private telemetry, still avoid raw text. +- Duplicate proposals: dedup by `{listing_id, intent_hash, session_id}` plus cooldown. +- False accept telemetry: Store/CLI install result events must include `proposal_id` to count as accepted. Merely opening Store is not accepted. + +## 7. [NEEDS CLARIFICATION] + +1. Exact M2 Store deep-link registration for Linux desktop: confirm whether `m2store://` is final or whether S3 wants an `m2-store --open-listing` command-only contract. +2. Hermes chat bridge location: confirm whether Hermes should consume a local JSONL file, Unix socket, or m2-gpt subconscious event for `m2.hermes.market_proposal.v1`. +3. Catalog tenant wildcard representation: confirm whether public listings use `tenant_visibility: ["*"]` in registry payload and whether memory-api tenant filter should include `"*"` or a normalized `tenant:m2-shared` namespace. diff --git a/specs/S5-packaging-seeds.md b/specs/S5-packaging-seeds.md new file mode 100644 index 0000000..755f210 --- /dev/null +++ b/specs/S5-packaging-seeds.md @@ -0,0 +1,647 @@ +# S5 Spec — Packaging & Seed Solutions + +Grounded reads: `/tmp/mktspec/BRIEF.md`, `CONCEPT.md`, `context/SYSTEM-MAP.md`, +`/home/m2/m2o/.planning/federated-learning/PLAN.md`, frozen marketplace schemas/contracts, +live Docker fleet, live `m2-gpt` and `memory-api` health checks, `mm-pdf` and +`agent-scaffold` skills. + +## 1. Scope & non-goals + +### MVP scope + +S5 owns the contract for turning a proven outcome into a sellable installable Solution: + +- builder workflow from either fedlearn `commercialize` or direct `m2-market publish` +- canonical bundle layout: `solution.yaml`, `solution.json`, `listing.json`, `payload/`, + `recipe.yaml`, `permissions.yaml`, `invoke.md`, `evidence/` +- packaging validation and PR review requirements for `m2/market-registry` +- install/uninstall/upgrade contract through `m2-market` and the `m2core-sync` apply adapter +- pre-apply license check hook against `m2-ledger` +- evidence attachment for provenance, cost telemetry, permissions, rollback, and installs +- concrete seed bundle specs for `mm-pdf`, `agent-scaffold`, and `competitor-scan` + +The MVP must work with the existing frozen `schemas/solution.schema.json` and +`schemas/listing.schema.json`. The human-authored source manifest is `solution.yaml`; the +publish command renders canonical `solution.json` and `listing.json` that pass frozen JSON +Schema validation. + +### Later + +- metered usage pricing and revenue sharing by invocation +- external public marketplace packaging +- cryptographic bundle signatures beyond content hash verification +- automated LLM-based tenant scrub beyond deterministic scanner + human attestations +- public coin/exchange or external payment settlement +- service escrow/dispute workflow for job-priced Solutions + +### Non-goals + +- Ledger internals, transaction schema, and platform cut mechanics: S1. +- Catalog indexing/search/ranking and storefront fields beyond package/listing emission: S2/S3. +- Scout matching/proposal logic: S4. +- Hermes build-vs-install proposal composition: S6. + +## 2. User stories + acceptance criteria + +### Story A — package a direct proven outcome + +Given a builder has a repeatable skill/playbook and at least one provenance record, +When they run `m2-market package init --from-skill /home/m2/.claude/skills/mm-pdf --id sol_mm-pdf-report`, +Then a bundle directory is created with `solution.yaml`, payload files, deploy recipe, +permissions, invoke docs, and an evidence stub. + +Given that bundle directory, +When the builder runs `m2-market publish ./bundle --price 25 --seller `, +Then the CLI validates schema, permissions, hash, evidence, and tenant firewall rules, +uploads a release asset, and opens a `m2/market-registry` PR in `in_review`. + +### Story B — package from fedlearn commercialize + +Given fedlearn curator marks a cluster disposition `commercialize`, +When the handoff calls the S5 package importer with the staged cluster/core artifact, +Then the importer creates a Solution bundle draft preserving fedlearn provenance, cost +telemetry, applicability, risk class, rollback note, and scrub status. + +Given the generated draft includes tenant-derived evidence, +When `tenant_scope != "m2-core"`, +Then publish fails unless `owner_initiated: true` and +`scrub_status.double_scrubbed: true` are present and reviewed in the PR. + +### Story C — install, verify, uninstall, and re-install + +Given a published fixed-price listing and a buyer with enough credits, +When `m2-market install ` runs, +Then the CLI checks or purchases a license, verifies bundle hash, calls the apply adapter, +runs the recipe verify command, records install state, and prints the invocation hint. + +Given the same listing is already licensed and applied, +When `m2-market install ` runs again, +Then no duplicate debit occurs and the adapter returns no-op success. + +Given an installed Solution, +When `m2-market uninstall ` runs, +Then the adapter executes the recipe uninstall steps, leaves the license grant intact, +updates install state to `rolled_back`, and emits uninstall telemetry. + +### Story D — upgrade + +Given `lst_mm-pdf-report-v1.1.0` is published and the buyer has a major-version license for +v1, +When `m2-market upgrade lst_mm-pdf-report`, +Then the CLI checks license coverage, downloads the new bundle, applies only the versioned +changeset, verifies, and updates state from `1.0.0` to `1.1.0` without a new debit. + +Given a new major version `2.0.0`, +When a v1 license holder upgrades, +Then the CLI requires a new install purchase unless the listing declares explicit grace +terms. + +## 3. Interfaces & data contracts + +### Registry paths + +Truth lives in Forgejo `git.machinemachine.ai/m2/market-registry`. + +```text +listings// +├── listing.json +├── solution.json +├── evidence/ +│ ├── provenance.jsonl +│ ├── cost.json +│ ├── scrub-attestation.json +│ └── permissions-diff.md +└── README.md + +releases/.tar.gz # Forgejo release asset, not committed as a blob +``` + +Builder workspaces may live in this repo under `solutions//`, but the registry PR is +the system of record. + +### Bundle layout + +Release asset tarball: + +```text +solution.json # canonical frozen schema payload +solution.yaml # source manifest; informational inside release +listing.json # catalog-facing record for this version +recipe.yaml # declarative apply/verify/uninstall/upgrade contract +permissions.yaml # source permission declarations rendered into solution.permissions +invoke.md # operator-facing invocation docs; no secrets +payload/ + skills//... + scripts/... + prompts/... + config/... +evidence/ + provenance.jsonl + cost.json + scrub-attestation.json + tests.json +``` + +`content_hash` is `sha256:` over the normalized tarball with `content_hash` blanked before +hashing. Publish rejects hashes computed from a dirty or non-normalized bundle. + +### `solution.yaml` source manifest + +```yaml +schema_version: m2.solution.source.v1 +solution_id: sol_mm-pdf-report +version: 1.0.0 +name: MM PDF Report Generator +summary: Render Machine.Machine branded PDF and HTML reports from Markdown. +intent: Generate branded PDF/HTML deliverables from operator-authored Markdown. +seller: op_sdjs +tenant_scope: m2-core +price: + model: fixed + amount: 25 + currency: m2cr +license: + terms: "Per-operator install; commercial use inside owned/client workspaces permitted." + major_version_coverage: true +revenue_split: + platform_pct: 10 +runtime: + harnesses: ["hermes", "openclaw"] + surfaces: ["desktop"] + adapter: m2core-sync +applicability: + image_classes: ["primus", "agent-latest"] + roles: ["operator", "research", "proposal"] + tool_requirements: ["bash", "docker", "google-chrome-in-target"] +deployment: + recipe_ref: recipe.yaml + entrypoint: "mm-pdf generate [--out output.pdf] [--style dark|purple|light]" + verify_command: "mm-pdf --version || test -x ~/.local/bin/mm-pdf" +behavior: + skills: + - payload/skills/mm-pdf/SKILL.md +tools: + - name: docker + purpose: render inside target m2o container +permissions_ref: permissions.yaml +evidence_ref: evidence/provenance.jsonl +``` + +Publish renders this into frozen `m2.solution.v1` by copying known schema fields and placing +extra source-only fields such as `version` only in `listing.solution_version` and release tag. + +### `recipe.yaml` + +```yaml +schema_version: m2.recipe.v1 +adapter: m2core-sync +install_ref: lst_mm-pdf-report-v1.0.0 +targets: + - id: skill + type: directory + source: payload/skills/mm-pdf + dest: "${AGENT_HOME}/.claude/skills/mm-pdf" + mode: "0755" + - id: command + type: file + source: payload/scripts/mm-pdf + dest: "${AGENT_HOME}/.local/bin/mm-pdf" + mode: "0755" +config_merge: [] +post_install: + - "mkdir -p ${AGENT_HOME}/.local/bin" +verify: + - "test -f ${AGENT_HOME}/.claude/skills/mm-pdf/SKILL.md" + - "test -x ${AGENT_HOME}/.local/bin/mm-pdf" +uninstall: + - "rm -rf ${AGENT_HOME}/.claude/skills/mm-pdf" + - "rm -f ${AGENT_HOME}/.local/bin/mm-pdf" +rollback: + strategy: restore_backups + note: m2core-sync records file backups before replacement and restores by changeset_hash. +``` + +Rules: + +- destinations must be under `${AGENT_HOME}`, `${M2_HOME}`, or a declared per-solution data + directory; host-level writes are rejected for marketplace MVP +- no recipe command may contain inline secrets, tokens, or absolute tenant paths +- install and uninstall must be idempotent +- verify commands must be offline-safe and deterministic +- adapter must run canary-first when installing fleet-wide + +### `permissions.yaml` + +```yaml +schema_version: m2.permissions.v1 +permissions: + - id: fs-write-agent-home + kind: filesystem + scope: "${AGENT_HOME}/.claude/skills/mm-pdf" + access: write + reason: install skill files + - id: docker-exec-local + kind: command + scope: "docker exec " + access: execute + reason: render PDF through Chrome inside a desktop container + - id: memory-read + kind: memory + scope: "market:evidence" + access: write + reason: emit install/invoke telemetry +``` + +Rendered `solution.permissions[]` objects are displayed as the install permissions diff in +CLI and M2 Store. + +### Evidence contracts + +`evidence/provenance.jsonl`: + +```json +{"source":"fedlearn:submissions/sub_2026w27_chris_01hxyz","machine":"chris-m2o","session":"herdr-run-2026-07-01T12:00:00Z","excerpt":"Generated branded PDF from Markdown using Chrome headless.","tokens":12000,"wall_time":480} +``` + +`evidence/cost.json`: + +```json +{ + "schema_version": "m2.solution.cost.v1", + "solution_id": "sol_mm-pdf-report", + "samples": 3, + "build_cost": {"tokens": 180000, "wall_time_seconds": 7200, "credits_equivalent": 18}, + "invoke_cost_estimate": {"tokens": 1000, "wall_time_seconds": 90, "credits_equivalent": 1}, + "maintenance_cost_estimate": {"credits_per_minor": 5}, + "source_refs": ["market:evidence/mm-pdf-2026w27"] +} +``` + +`evidence/scrub-attestation.json`: + +```json +{ + "schema_version": "m2.scrub.v1", + "tenant_scope": "m2-core", + "owner_initiated": true, + "double_scrubbed": true, + "secret_scan": "passed", + "pii_scan": "passed", + "reviewers": ["op_builder", "op_reviewer"], + "reviewed_at": "2026-07-02T00:00:00Z" +} +``` + +Install/invoke telemetry is appended locally, then summarized to `market:evidence`: + +```json +{ + "schema_version": "m2.solution.telemetry.v1", + "event_id": "evt_", + "event": "install|uninstall|upgrade|invoke", + "operator_id": "op_buyer", + "tenant": "m2-core", + "listing_id": "lst_mm-pdf-report", + "solution_version": "1.0.0", + "machine": "chris-m2o", + "result": "success|failed|rolled_back", + "wall_time": 12.4, + "tokens": 0, + "credits": 25, + "error_class": null +} +``` + +### CLI verbs + +Builder: + +- `m2-market package init --from-skill --id --name ` +- `m2-market package import-fedlearn --cluster --out solutions/` +- `m2-market package validate [--strict-tenant]` +- `m2-market package build --out dist/.tar.gz` +- `m2-market publish --seller --price ` + +Buyer: + +- `m2-market install [--yes]` +- `m2-market uninstall [--yes]` +- `m2-market upgrade [--to ]` +- `m2-market invoke -- ` optional wrapper that records invoke telemetry + +Admin/review: + +- `m2-market package diff-permissions ` +- `m2-market package evidence-summary ` +- `m2-market package verify-release ` + +### Install/uninstall/upgrade sequence + +Install: + +1. Resolve listing from `market:catalog`; fetch registry `solution.json`. +2. License hook: `GET /licenses/check?operator_id=&listing_id=&major=`. +3. If no valid grant: `POST /tx/install` with price, seller, platform cut, and ref. +4. Download release tarball by `listing.install_ref`. +5. Verify `content_hash`. +6. Adapter preflight: `m2-core-sync plan --bundle --license-grant `. +7. Apply: `m2-core-sync apply --bundle --state ~/.m2-market/state.json`. +8. Verify recipe. +9. Write install state and telemetry. + +Uninstall: + +1. Confirm installed state exists. +2. Run `m2-core-sync uninstall --install-ref ` or adapter rollback by `changeset_hash`. +3. Verify declared uninstall checks. +4. Mark `rolled_back`; do not revoke license by default. +5. Emit telemetry. + +Upgrade: + +1. Resolve latest published listing version with same listing id. +2. Check major license coverage through ledger. +3. Compute permission and recipe diff. +4. Require confirmation if permissions broaden. +5. Apply upgrade changeset; rollback to prior version on verify failure. +6. Emit telemetry. + +### License check hook + +Required m2-ledger endpoints consumed by S5 install path: + +```http +GET /licenses/check?operator_id=op_buyer&listing_id=lst_mm-pdf-report&major=1 +200 {"valid":true,"grant_id":"gr_...","covers_major":1} +404 {"valid":false} + +POST /tx/install +{"buyer":"op_buyer","listing_id":"lst_mm-pdf-report","seller":"op_sdjs","amount":25,"currency":"m2cr","ref":"lst_mm-pdf-report:chris-m2o:"} +200 {"tx_ids":[1,2],"grant":{"grant_id":"gr_...","covers_major":1}} + +POST /tx/refund +{"ref":"lst_mm-pdf-report:chris-m2o:","reason":"apply_failed"} +200 {"refunded":true} +``` + +The adapter must receive `grant_id` during plan/apply. A bundle apply without a valid grant +must fail before any file writes. + +## 4. Integration contract with other subsystems + +- S1 ledger: S5 calls `/licenses/check`, `/tx/install`, `/tx/refund`; ledger grants remain + authoritative for reinstall/upgrade. +- S2 catalog: S5 emits valid `listing.json` and `solution.json`; S2 indexes merged registry + records into `market:catalog` and folds telemetry into stats. +- S3 store: S3 shows `invoke.md`, evidence, price, and permission diff from S5 bundle + metadata; install button shells `m2-market install --json`. +- S4 Scout: S4 proposals deep-link to listing ids; S5 guarantees install refs and invocation + hints exist after install. +- S6 proposal engine: S6 consumes `evidence/cost.json`, provenance, price model, and invoke + docs for build-vs-install comparisons. +- fedlearn: fedlearn `commercialize` calls `m2-market package import-fedlearn`; staged + provenance and applicability map directly into `solution.evidence[]` and `applicability`. +- m2-core-sync: S5 recipe is the marketplace-specific payload fed to the universal runtime + sync/apply path. Until fedlearn rails land, the existing `ApplyAdapter` local path can + execute the same recipe shape. +- memory-api: S5 writes only summarized package evidence to `market:evidence`; raw tenant + data stays out of shared memory. +- Forgejo: PR labels are `market:listing`, `status:in_review`, `risk:`, + `veto-until:`, optional `approved`, optional `veto`. + +## 5. Options compared + +### Source manifest format + +- JSON only: aligns with schemas, poor for human packaging. +- YAML source rendered to JSON: best authoring ergonomics while preserving frozen schema. +- Python package metadata: too language-specific. + +Recommendation: YAML source plus canonical JSON output. + +### Install backend + +- Direct local file copy: good interim adapter, weak as long-term universal fleet path. +- m2-core-sync: matches locked runtime-sync decision and mixed image constraint. +- Container image install: rejected for MVP; local registry route is broken and secrets must + not be baked. + +Recommendation: recipe contract targets `m2core-sync`; local adapter may execute same recipe +while fedlearn rails are incomplete. + +### Pricing model per package + +- Fixed install: deterministic and supported by frozen schema. +- Job price: required by locked decision, but current JSON Schema only enum-validates + `fixed`. + +Recommendation: MVP bundles with direct install use `fixed`. Job-priced packages are listed +as `inventory_type: service` or use `price_ext.model: job` in `solution.yaml` until +`solution.schema.json` v2 adds `job`. + +### Evidence storage + +- Evidence embedded only in `solution.json`: too shallow for audit. +- Evidence files beside registry listing: reviewable, indexed, and durable. +- Memory-only evidence: not authoritative. + +Recommendation: registry evidence files are truth; memory `market:evidence` is derived and +summarized. + +## 6. Seed Solution package specs + +### Seed 1 — `mm-pdf` + +Source asset: `/home/m2/.claude/skills/mm-pdf`. + +```yaml +solution_id: sol_mm-pdf-report +listing_id: lst_mm-pdf-report +version: 1.0.0 +name: MM PDF Report Generator +price: {model: fixed, amount: 25, currency: m2cr} +category: documents +tenant_scope: m2-core +install_targets: + - "${AGENT_HOME}/.claude/skills/mm-pdf" + - "${AGENT_HOME}/.local/bin/mm-pdf" +entrypoint: "mm-pdf generate [--out output.pdf] [--style dark|purple|light]" +``` + +Bundle contents: + +- `payload/skills/mm-pdf/SKILL.md` +- `payload/skills/mm-pdf/templates/dark.css` +- `payload/skills/mm-pdf/templates/purple.css` +- `payload/scripts/mm-pdf` wrapper that calls skill script `generate.sh` +- `recipe.yaml`, `permissions.yaml`, `invoke.md`, `evidence/*` + +Permissions: + +- write skill directory under `${AGENT_HOME}` +- write executable under `${AGENT_HOME}/.local/bin` +- execute `docker ps`, `docker exec`, `docker cp` against local m2o container +- write output PDFs/HTML only to user-selected working directory +- no network secrets; no tenant memory reads required + +Runtime deps: + +- bash, docker CLI on host/container +- target m2o container with Node/npm and Google Chrome; observed `nasr-m2o` has this +- optional Marp path for slides later, not required for MVP + +Install targets: + +- primus and agent-latest desktops +- canaries: `chris-m2o` first, `gunnar-m2o` second + +Evidence: + +- skill file path and script provenance from `/home/m2/.claude/skills/mm-pdf` +- successful local render transcript +- cost sample for Markdown-to-PDF runs + +### Seed 2 — `agent-scaffold` + +Source asset: `/home/m2/.claude/skills/agent-scaffold`. + +```yaml +solution_id: sol_agent-scaffold +listing_id: lst_agent-scaffold +version: 1.0.0 +name: Agent Workspace Scaffold +price: {model: fixed, amount: 35, currency: m2cr} +category: agent-ops +tenant_scope: m2-core +install_targets: + - "${AGENT_HOME}/.claude/skills/agent-scaffold" + - "${AGENT_HOME}/.local/bin/agent-scaffold" +entrypoint: "agent-scaffold \"\" [--out ]" +``` + +Bundle contents: + +- `payload/skills/agent-scaffold/SKILL.md` +- `payload/skills/agent-scaffold/scripts/scaffold.py` +- `payload/scripts/agent-scaffold` wrapper around `python3 scaffold.py` +- `payload/templates/PRD.md.j2`, `SOUL.md.j2`, `MEMORY.md.j2` only if extracted from script + in a future cleanup; MVP may keep script-generated templates inline +- `invoke.md` documenting env vars `M2_MEMORY_API_URL`, `M2_MEMORY_AGENT_ID`, + `M2_MEMORY_API_KEY` + +Permissions: + +- write `~/agents//PRD.md`, `SOUL.md`, `MEMORY.md` +- read memory-api through configured endpoint and `X-API-Key` +- no raw client data export; memory query runs under buyer tenant/agent id +- optional write to git is manual, not automated in MVP + +Runtime deps: + +- Python 3 standard library +- memory-api reachable from desktop or host; live endpoint verified healthy +- `M2_MEMORY_API_KEY` injected at runtime, never packaged + +Install targets: + +- primus and agent-latest desktops +- Hermes/OpenClaw compatible as a CLI/skill, not harness-specific + +Evidence: + +- source skill and script from `/home/m2/.claude/skills/agent-scaffold` +- sample scaffold output with no tenant-specific content +- wall-time/token estimate from memory query and local file generation + +### Seed 3 — `competitor-scan` + +Source asset: no canonical local skill found; package as a new outcome bundle from proven +research-report workflow, with first implementation as prompts + scripts rather than a +tenant-derived artifact. + +```yaml +solution_id: sol_competitor-scan +listing_id: lst_competitor-scan +version: 1.0.0 +name: Competitor Scan Report +price: {model: fixed, amount: 60, currency: m2cr} +category: research +tenant_scope: m2-core +install_targets: + - "${AGENT_HOME}/.claude/skills/competitor-scan" + - "${AGENT_HOME}/.local/bin/competitor-scan" +entrypoint: "competitor-scan \"\" --out report.md [--pdf]" +``` + +Bundle contents: + +- `payload/skills/competitor-scan/SKILL.md` with research workflow: + scope, source collection, competitor matrix, positioning, pricing, risks, citations, + recommendations +- `payload/prompts/competitor-scan.md` +- `payload/scripts/competitor-scan` orchestrator: + accepts subject, writes `report.md`, optionally calls `mm-pdf` if installed +- `payload/templates/report.md` +- `recipe.yaml`, `permissions.yaml`, `invoke.md`, `evidence/*` + +Permissions: + +- network egress for web research through approved tools/connectors only +- read/write local report output directory +- optional memory read for prior public market research under buyer tenant +- no raw tenant CRM/doc access by default +- if using browser automation, permission must be explicit and shown in install diff + +Runtime deps: + +- Hermes or OpenAI-wire harness through m2-gpt +- approved search/browser connector where available +- optional `mm-pdf` installed for PDF output + +Install targets: + +- primus and agent-latest desktops +- initial canary should be non-client tenant or `m2-core` only until evidence is scrubbed + +Evidence: + +- generic public-market sample only +- citations list in output fixture +- cost telemetry from a dry-run scan: LLM tokens, wall time, number of sources + +Pricing note: + +- fixed install price `60 m2cr` for the reusable workflow +- a human/operator-delivered bespoke competitor scan should be a separate service listing + with job pricing once schema v2 supports `price.model: job` + +## 7. Risks/edge cases + +- Tenant leakage: reject tenant-scoped bundles without owner initiation and double scrub; + competitor-scan must not package client-specific examples as seed evidence. +- Secrets in payload: deterministic scanner blocks API keys, JWTs, SSH keys, high-entropy + strings, `.env`, browser profiles, and absolute secret paths. +- License/apply split brain: if debit succeeds and apply fails, CLI must call refund and + record failed install telemetry; grant without apply remains visible for audit. +- Upgrade permission creep: any broadened permission requires explicit confirmation and PR + review note. +- Uninstall data loss: recipes may remove installed files but must not delete user-created + output directories unless declared as disposable cache. +- Host fragility: install recipes are canary-first, idempotent, reversible, and confined to + agent home paths. +- Local registry route is broken: packages are Forgejo release assets, not Docker images or + registry pushes. +- Job pricing mismatch: locked business decision allows job pricing, but frozen schema only + validates `fixed`; represent jobs as service inventory until schema v2. +- Fake evidence: registry PR requires provenance files and at least one reproducible verify + or sample output; catalog/memory evidence is not authoritative. + +## 8. [NEEDS CLARIFICATION] + +1. Which operator id is the seller of record for the three seed Solutions: `op_sdjs`, + `op_m2bd`, or a platform-owned `op_m2-core`? +2. Should `competitor-scan` MVP be install-only fixed price, or should it launch as the first + service/job listing despite the frozen `solution.schema.json` only accepting fixed price? +3. What exact `m2-core-sync` command names will fedlearn ship for uninstall and upgrade + (`uninstall`, `rollback`, `apply --upgrade`), so S5 can replace adapter placeholders + without aliasing? diff --git a/specs/S6-proposal-engine-evidence.md b/specs/S6-proposal-engine-evidence.md new file mode 100644 index 0000000..dbceb7a --- /dev/null +++ b/specs/S6-proposal-engine-evidence.md @@ -0,0 +1,668 @@ +# S6 — Proposal Engine & Evidence Loop SPEC + +## 1. Scope & non-goals + +### MVP scope + +S6 owns the pull-side proposal engine and pricing evidence loop: + +- Hermes `market-propose` skill: turns a user intent into build-vs-install-vs-job proposals. +- Cost-intelligence layer: combines similar past work from memory, real token/time spend from m2-gpt metering/cognition traces, herdr run durations, and `market:catalog` search. +- Evidence records: every serious job leaves a scrubbed, tenant-scoped record with wanted/tools/contributors/tokens/time/resources/failed/succeeded/reusable-verdict. +- Proposal citations: proposals must cite listing evidence, similar job evidence, and cost estimates separately. +- Harness-agnostic contract: Hermes is the primary surface; OpenAI-wire clients through m2-gpt can call the same tool/API later. + +MVP deploys as: + +1. A Hermes skill installed by runtime-sync in desktop home volumes. +2. A small `m2-market-proposer` service on Coolify network for deterministic retrieval, estimate aggregation, evidence writes, and proposal JSON. +3. A gateway-side optional middleware hook that records real usage and exposes it by correlation id; the proposal decision logic stays outside the gateway. + +### Later + +- Automatic quote negotiation with human operators. +- Dynamic repricing of listings. +- Public web marketplace proposal widgets. +- Full resource-market routing optimizer. +- Crypto-capable ledger integration beyond internal credits. + +### Non-goals + +- Ledger schema or payment settlement: S1. +- Catalog/listing truth, indexing, install flow, and M2 Store UX: S2/S3. +- Scout push notifications: S4. +- Packaging/build artifacts: S5. +- Raw client transcript ingestion. S6 consumes scrubbed summaries only. + +## 2. User stories + acceptance criteria + +### US-1: Operator asks Hermes what path to take + +Given an operator asks Hermes for an outcome such as "automate my eBay listings" +When Hermes invokes `market-propose` with the current intent and tenant identity +Then the response contains at least one of each applicable path: + +- `install_adapt`: existing Solution covers enough of the intent. +- `operator_job`: a Service/operator can deliver or adapt it. +- `custom_build`: no adequate inventory exists or client constraints require custom work. + +Acceptance: + +- Every price/time/token estimate has at least one citation or is marked `low_confidence`. +- Tenant visibility is enforced before any listing or evidence is returned. +- Client-derived evidence is never cited unless `owner_initiated=true` and `double_scrubbed=true`. + +### US-2: Proposal cites evidence without leaking data + +Given similar past work exists in memory under tenant-private and shared partitions +When a proposal is generated for tenant `gst` +Then only `tenant_id in ["gst", "__fleet__", "m2-core"]` and allowed shared records are searched. + +Acceptance: + +- Cross-tenant records are impossible to retrieve through the API filter. +- Citations include stable IDs and short sanitized summaries, not raw transcript text. +- The API returns `evidence_redactions[]` explaining omitted records by class, not identity. + +### US-3: Serious job leaves an evidence record + +Given an install, operator-assisted job, or custom build runs longer than 10 minutes, spends more than 100k tokens, or creates a reusable artifact +When the job ends or is abandoned +Then `m2-market-proposer` writes a `m2.market.evidence.v1` record to memory and attaches its summary to any relevant listing. + +Acceptance: + +- Records validate against the schema below. +- Failed jobs are recorded; they are usable for estimates but do not raise listing success stats. +- Reusable verdict is one of `create_solution`, `update_solution`, `not_reusable`, `unclear`. + +### US-4: Evidence improves future estimates + +Given at least three evidence records exist for a capability or listing +When a later proposal asks for similar work +Then estimates use p50/p80/p95 ranges instead of a single naive value. + +Acceptance: + +- Proposal JSON includes `estimate_basis.sample_count`. +- Outliers remain in evidence but are downweighted only by explicit rule. +- Install path estimates cite listing-level install telemetry before generic similar jobs. + +## 3. Interfaces & data contracts + +### Existing contracts S6 depends on + +- `memory-api`: live `https://memory.machinemachine.ai/health` returns ok; search endpoint shape is `POST /memory/search` with `query`, `agent_id`, `routing_strategy`, `limit`, `tenant_id`, and optional `filters`. +- `m2-gpt`: live `https://gpt.machinemachine.ai/health` returns `{"status":"healthy","gateway":"bifrost"}`. It already has tenants/agents, `tenant_budgets`, `route_budgets`, `spend_log`, cognition traces, and usage passthrough from Bifrost. +- `market:catalog`: existing catalog contract stores published listing payloads under memory partition `agent_id="market:catalog"`. +- `m2.solution.v1` and `m2.listing.v1`: existing schemas already carry evidence, price, seller, tenant visibility, and stats. +- `m2-ledger`: S6 quotes credits but does not debit; install/job settlement remains the ledger contract. + +### Memory partitions + +Use memory-api `agent_id` as partition: + +- `market:catalog`: published listing semantic index, owned by S2/S3. +- `market:evidence`: job and install evidence records, owned by S6. +- `market:proposals`: optional proposal audit summaries, owned by S6. + +Tenant scope: + +- Shared evidence uses `tenant_id="m2-core"` or `tenant_id="__fleet__"`. +- Tenant-private evidence uses canonical tenant id such as `gst`, `nasr`, `parlobyg`, `peter`. +- Queries must pass `tenant_id=[caller_tenant, "m2-core", "__fleet__"]` unless caller is an operator-admin. + +### Evidence record schema + +Schema id: `m2.market.evidence.v1`. + +```json +{ + "schema_version": "m2.market.evidence.v1", + "evidence_id": "ev_2026w27_01j2abc", + "job_id": "job_01j2abc", + "tenant_id": "m2-core", + "operator_id": "sdjs-operator", + "agent_ids": ["chris-m2o"], + "created_at": "2026-07-02T00:00:00Z", + "wanted": { + "intent": "Generate a branded competitor scan report", + "normalized_capabilities": ["research", "report-generation"], + "constraints": ["no raw client data in shared catalog"] + }, + "tools": [ + {"name": "Hermes", "kind": "agent"}, + {"name": "m2-gpt", "kind": "llm_gateway"}, + {"name": "memory-api", "kind": "memory"}, + {"name": "herdr", "kind": "run_capture"} + ], + "contributors": [ + {"kind": "operator", "id": "sdjs-operator", "role": "builder"}, + {"kind": "agent", "id": "chris-m2o", "role": "executor"} + ], + "metering": { + "m2_gpt_correlation_ids": ["turn_01j2abc"], + "prompt_tokens": 120000, + "completion_tokens": 18000, + "total_tokens": 138000, + "cost_usd": 0.42, + "wall_time_seconds": 2400, + "active_time_seconds": 1500, + "herdr_run_ids": ["run_2026-07-02T00-00-00Z"] + }, + "resources": [ + {"kind": "desktop", "id": "chris-m2o", "seconds": 2400}, + {"kind": "model", "id": "GLM-5.1", "tokens": 138000} + ], + "outcome": { + "status": "succeeded", + "failed": [ + {"step": "site scraping", "reason": "blocked by login", "recovered": true} + ], + "succeeded": [ + {"step": "report generation", "artifact_ref": "forgejo:m2/market-registry/..."} + ] + }, + "reusable_verdict": { + "decision": "create_solution", + "confidence": 0.82, + "reason": "Repeatable report workflow with scrubbed inputs" + }, + "listing_refs": [ + {"listing_id": "lst_competitor-scan", "relationship": "supports_estimate"} + ], + "scrub_status": { + "secrets_redacted": true, + "pii_redacted": true, + "double_scrubbed": true, + "raw_sources_retained": false + }, + "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +Required fields: + +`schema_version`, `evidence_id`, `job_id`, `tenant_id`, `created_at`, `wanted`, `tools`, `contributors`, `metering`, `resources`, `outcome`, `reusable_verdict`, `scrub_status`, `content_hash`. + +Memory store payload: + +```json +{ + "content": "Wanted: branded competitor scan report. Outcome: succeeded. Reusable: create_solution. Time: 40m. Tokens: 138k.", + "agent_id": "market:evidence", + "tenant_id": "m2-core", + "metadata": { + "schema_version": "m2.market.evidence.v1", + "evidence_id": "ev_2026w27_01j2abc", + "job_id": "job_01j2abc", + "listing_ids": ["lst_competitor-scan"], + "capabilities": ["research", "report-generation"], + "status": "succeeded", + "reusable_decision": "create_solution", + "tokens_total": 138000, + "wall_time_seconds": 2400, + "content_hash": "sha256:..." + } +} +``` + +### Proposal request schema + +Schema id: `m2.market.proposal-request.v1`. + +```json +{ + "schema_version": "m2.market.proposal-request.v1", + "request_id": "prq_01j2abc", + "tenant_id": "m2-core", + "operator_id": "m2bd", + "agent_id": "chris-m2o", + "harness": "hermes", + "intent": "I want to automate eBay listings", + "context_summary": "User has product photos and wants listing drafts.", + "constraints": { + "max_credits": 150, + "deadline": "2026-07-03", + "data_sensitivity": "tenant_private", + "allowed_inventory_types": ["solution", "service", "resource", "capability"] + }, + "current_tools": ["browser", "filesystem", "m2-market"], + "correlation_id": "turn_01j2abc" +} +``` + +### Proposal response schema + +Schema id: `m2.market.proposal.v1`. + +```json +{ + "schema_version": "m2.market.proposal.v1", + "proposal_id": "prop_01j2abc", + "request_id": "prq_01j2abc", + "tenant_id": "m2-core", + "generated_at": "2026-07-02T00:00:00Z", + "intent_summary": "Automate eBay listing creation from product inputs.", + "paths": [ + { + "path_id": "path_install_adapt_1", + "kind": "install_adapt", + "title": "Install eBay Listing Workflow and adapt prompts", + "recommendation": "recommended", + "coverage": 0.72, + "credits": {"min": 40, "expected": 60, "max": 90, "currency": "m2cr"}, + "time": {"p50_seconds": 7200, "p80_seconds": 14400}, + "tokens": {"p50": 600000, "p80": 1200000}, + "resources": [{"kind": "desktop", "quantity": 1}], + "next_action": { + "type": "install", + "listing_id": "lst_ebay-listing-workflow", + "deeplink": "m2store://listing/lst_ebay-listing-workflow" + }, + "citations": [ + {"kind": "listing", "id": "lst_ebay-listing-workflow", "claim": "catalog match"}, + {"kind": "evidence", "id": "ev_2026w26_01x", "claim": "similar workflow completed in 2h"} + ], + "risks": ["Needs credentialed browser session", "May require image style adaptation"], + "estimate_basis": {"sample_count": 4, "method": "listing_install_p80"} + } + ], + "evidence_redactions": [ + {"reason": "tenant_not_visible", "count": 3}, + {"reason": "not_double_scrubbed", "count": 1} + ], + "audit": { + "catalog_query_id": "memq_01j2abc", + "evidence_query_id": "memq_01j2abd", + "m2_gpt_correlation_id": "turn_01j2abc" + } +} +``` + +Path kinds: + +- `install_adapt`: buy/install Solution and adapt locally. +- `operator_job`: engage operator/service through job-based pricing. +- `custom_build`: build from scratch. +- `resource_route`: rent internal resource/capacity when it is the dominant cost. +- `defer`: no acceptable path under constraints. + +### HTTP API: m2-market-proposer + +Service: `m2-market-proposer`, Coolify app on `coolify` network. Base internal URL: `http://m2-market-proposer:8000`. + +Auth: + +- `X-API-Key`: service key injected at runtime. +- Optional `Authorization: Bearer ` for direct harness calls after gateway identity forwarding lands. + +#### `GET /health` + +Response: + +```json +{"status":"healthy","memory":"ok","m2_gpt":"ok"} +``` + +#### `POST /v1/proposals` + +Body: `m2.market.proposal-request.v1`. +Response: `m2.market.proposal.v1`. + +Required behavior: + +1. Normalize intent into capability tags. +2. Query `market:catalog` with tenant visibility. +3. Query `market:evidence` with tenant scope `[caller, "m2-core", "__fleet__"]`. +4. Fetch m2-gpt spend/time by `correlation_id` where available. +5. Build ranked paths with citations. +6. Store proposal audit summary in `market:proposals` unless `dry_run=true`. + +#### `POST /v1/evidence` + +Body: `m2.market.evidence.v1`. +Response: + +```json +{"evidence_id":"ev_2026w27_01j2abc","stored":true,"attached_listing_ids":["lst_competitor-scan"]} +``` + +Behavior: + +- Validate schema. +- Enforce scrub gate before shared write. +- Store semantic summary in memory partition `market:evidence`. +- Emit listing-attachment event for S2/S3 indexer. + +#### `GET /v1/evidence/{evidence_id}` + +Returns sanitized evidence record if caller tenant can see it; `404` for not found or forbidden to avoid existence leaks. + +#### `POST /v1/estimates` + +Body: + +```json +{ + "tenant_id": "m2-core", + "intent": "build competitor scan report", + "listing_ids": ["lst_competitor-scan"], + "capabilities": ["research", "report-generation"], + "confidence_floor": 0.6 +} +``` + +Response: + +```json +{ + "sample_count": 5, + "tokens": {"p50": 300000, "p80": 800000, "p95": 2000000}, + "time_seconds": {"p50": 1800, "p80": 7200, "p95": 172800}, + "credits": {"p50": 30, "p80": 90, "p95": 300}, + "method": "similar_evidence_weighted", + "citations": [{"kind":"evidence","id":"ev_..."}] +} +``` + +### CLI verbs + +Add to existing `m2-market` CLI surface: + +```bash +m2-market propose "" --tenant --operator --json +m2-market evidence capture --job-id --from-herdr --correlation-id +m2-market evidence submit evidence.json +m2-market evidence show --json +``` + +Exit codes follow existing CLI contract: `0 ok`, `1 usage`, `2 not found`, `5 validation/firewall rejection`. + +### Hermes skill: `market-propose` + +Install path: + +- Primus: `/home/developer/.hermes/skills/market-propose/` +- Agent-latest/OpenClaw-compatible fallback assets: `/home/developer/.m2-core/skills/market-propose/` +- Distributed by runtime-sync/m2-core-sync only; no baked secrets. + +Skill files: + +```text +market-propose/ + SKILL.md + skill.json + scripts/propose.py + scripts/evidence_capture.py +``` + +`skill.json`: + +```json +{ + "name": "market-propose", + "version": "0.1.0", + "runtime": "python3", + "commands": { + "propose": "python3 scripts/propose.py", + "evidence_capture": "python3 scripts/evidence_capture.py" + }, + "env": [ + "M2_MARKET_PROPOSER_URL", + "M2_MARKET_API_KEY", + "M2_GPT_CORRELATION_ID", + "M2_TENANT_ID", + "M2_OPERATOR_ID" + ] +} +``` + +Hermes behavior: + +- Invoke `propose` when the user asks for cost, build/install choice, "is there a package", "can someone do this", or "estimate this job". +- Render concise human prose, but preserve machine-readable proposal JSON in an attached block or local state file. +- Do not install or spend credits without explicit user approval. + +### m2-gpt metering contract + +S6 consumes existing gateway data instead of duplicating metering: + +- `spend_log`: tenant, route/model, recorded time, prompt/completion/total tokens where present, cost. +- cognition trace/audit v2: `tenant_id`, `agent_id`, `turn_id`/correlation id, `model`, `tokens`, `tool`, `status`, latency/time. +- Bifrost OpenAI response `usage`: passthrough source for token counts. + +Add read-only internal endpoint in m2-gpt: + +#### `GET /internal/v1/metering/turn/{correlation_id}` + +Auth: service key, internal network only. + +Response: + +```json +{ + "correlation_id": "turn_01j2abc", + "tenant_id": "m2-core", + "agent_id": "chris-m2o", + "started_at": "2026-07-02T00:00:00Z", + "ended_at": "2026-07-02T00:12:00Z", + "model_calls": [ + { + "route_id": "spark-glm", + "model": "GLM-5.1", + "prompt_tokens": 120000, + "completion_tokens": 18000, + "total_tokens": 138000, + "cost_usd": 0.42, + "status": "ok" + } + ], + "totals": { + "prompt_tokens": 120000, + "completion_tokens": 18000, + "total_tokens": 138000, + "cost_usd": 0.42, + "wall_time_seconds": 720 + } +} +``` + +This endpoint is a gateway concern but S6 defines it as the minimum metering read needed for evidence capture. If not available in MVP, `evidence_capture` records `m2_gpt_correlation_ids` and `tokens_unknown=true`. + +### Events + +S6 emits JSONL-compatible events to stdout and optional webhook for S2/S3: + +```json +{ + "event_type": "market.evidence.created", + "event_version": "v1", + "ts": "2026-07-02T00:00:00Z", + "evidence_id": "ev_2026w27_01j2abc", + "tenant_id": "m2-core", + "listing_ids": ["lst_competitor-scan"], + "reusable_decision": "create_solution" +} +``` + +Other events: + +- `market.proposal.generated` +- `market.proposal.accepted` +- `market.proposal.dismissed` +- `market.evidence.attached_to_listing` + +### Storage and attachment + +Authoritative evidence storage: + +- Memory semantic copy: `agent_id="market:evidence"` in memory-api. +- Audit copy: JSONL at proposer service volume `/var/lib/m2-market-proposer/evidence/YYYY-Www.jsonl`. +- Listing attachment: S6 does not mutate listing truth directly. It emits `market.evidence.attached_to_listing`; S2/S3 indexer updates `listing.evidence_summary`, listing stats, and optional registry audit PR/commit. + +Proposal audits: + +- Stored as summaries under `agent_id="market:proposals"` with `tenant_id` scoped to caller. +- TTL/default retention: 90 days for tenant-private, indefinite for shared scrubbed proposal statistics. + +## 4. Integration contract with other subsystems + +### S1 ledger + +Inputs from S1: + +- Current price for install/job/resource. +- Job escrow/quote status when path kind is `operator_job`. +- Platform cut defaults only for display; S6 does not settle. + +Outputs to S1: + +- Approved path metadata may become ledger `reason=route|install|job` ref. +- Proposal id should be included in ledger refs where possible: `prop_...:path_...`. + +### S2 catalog + +Inputs from S2: + +- `market:catalog` searchable listing payloads. +- Listing stats: installs, rating, proposals_shown, proposals_accepted. +- Tenant visibility policy. + +Outputs to S2: + +- `market.proposal.generated/accepted/dismissed` events. +- Evidence attachment events by listing id. +- Suggested new capability tags from proposal normalization. + +### S3 store/install + +Inputs from S3: + +- M2 Store deep links: `m2store://listing/`. +- Install state and permission diff when available. + +Outputs to S3: + +- Proposal path next actions with listing ids. +- Evidence of install success/failure for listing stats. + +### S4 Scout + +Shared contracts: + +- S4 can call `POST /v1/proposals` with `harness="scout"` and a session summary. +- S4 owns push timing/UI; S6 owns proposal construction and estimate basis. +- Proposal/dismissal telemetry uses the same events. + +### S5 packaging + +Inputs from S5: + +- Build/package evidence created during solution packaging. +- Manifest ids, release refs, tool requirements. + +Outputs to S5: + +- Reusable verdicts: `create_solution` and `update_solution`. +- Evidence citations that should be included in a future `solution.evidence[]`. + +### m2-gpt + +Touchpoints: + +- Tenant/agent identity from bearer key. +- m2-gpt `spend_log` and cognition traces by `correlation_id`. +- Optional gateway middleware/tool-dispatch registration so all OpenAI-wire harnesses can call proposal tool. + +No S6 secrets are baked into images. `M2_MARKET_API_KEY` and `M2_GPT_API_KEY` are runtime-injected. + +### memory-api + +Touchpoints: + +- `POST /memory/search` with `agent_id="market:catalog"` and `agent_id="market:evidence"`. +- Store via existing memory write path once auth is enforced. +- Tenant/principal filters must be passed on every query. + +### m2o/herdr + +Touchpoints: + +- `herdr` run summaries supply active time, failed/succeeded steps, tools invoked. +- Desktop identity from fleet/herdr config supplies `agent_id`. +- Runtime-sync installs the Hermes skill across primus and agent-latest without image rebuild. + +## 5. Options compared + +### Option A: Hermes skill only + +Pros: + +- Fastest MVP. +- Fits primary agent surface. +- Can render proposals conversationally. + +Cons: + +- Duplicates retrieval/estimate logic if OpenClaw or Store needs it. +- Harder to centralize evidence writes and audit. + +### Option B: m2-gpt middleware/tool only + +Pros: + +- Harness-agnostic by default for anything OpenAI-wire. +- Already sees identity, tokens, model usage, and budgets. + +Cons: + +- Bloats latency-sensitive gateway. +- Proposal logic depends on catalog and ledger services, which should not be in the critical chat-completion path. +- Harder to evolve independently. + +### Option C: Proposer service + Hermes skill adapter (recommended) + +Pros: + +- Keeps deterministic business logic and evidence writes centralized. +- Hermes gets a thin local skill; OpenClaw/Store/Scout can call the same API. +- m2-gpt only provides identity/metering and optional tool exposure. +- Can be canary-deployed as a Coolify app and rolled back independently. + +Cons: + +- One new small service to operate. +- Requires service keys and network ACLs. + +Recommendation: Option C for MVP. Add gateway middleware only for two narrow functions: expose `market.propose` as an OpenAI tool later, and provide read-only metering by correlation id. + +### Cost estimate method + +Compared: + +- Static price table: simple but ignores real work history. +- LLM-estimated effort: flexible but unverifiable. +- Evidence-weighted estimates: uses real prior tokens/time/outcomes and listing telemetry. + +Recommendation: evidence-weighted estimates. Fall back to static seed values only when `sample_count < 2`, and mark as `low_confidence`. + +## 6. Risks/edge cases + +- Tenant leakage: all memory searches must include tenant filters; forbidden evidence returns `404`, not `403`. +- Client-derived commercialization: shared evidence requires `owner_initiated=true` and `double_scrubbed=true`; otherwise tenant-private only. +- Secrets in evidence: evidence capture uses the fedlearn scrubber policy and fails closed on high-entropy strings. +- Bad estimates from tiny samples: responses must expose `sample_count`, `method`, and confidence. +- Failed jobs hidden from pricing: failed evidence is required because it improves estimates; proposals must distinguish success evidence from failure evidence. +- Gateway fragility: do not put catalog search inside the hot chat-completion path; proposer service is called explicitly by skill/tool. +- Abuse/spam proposals: proposal audits are rate-limited per operator and do not auto-spend credits. +- Split-brain memory-api: indexer/proposer must use the live endpoint resolver until fedlearn T13 is complete; evidence JSONL audit copy allows replay. +- Clock/correlation mismatch: if m2-gpt turn id is missing, evidence can still record herdr run ids and `tokens_unknown=true`; later backfill may attach metering. +- Cross-harness UX drift: Hermes prose is non-authoritative; proposal JSON is the contract. + +## 7. [NEEDS CLARIFICATION] + +1. What exact m2-gpt correlation id is guaranteed to be available inside Hermes skill execution: `turn_id`, `correlation_id`, or a new `M2_GPT_CORRELATION_ID` env var? +2. Who is allowed to mark tenant-private evidence as `owner_initiated=true` and `double_scrubbed=true`: tenant admin, platform operator, or both? +3. What is the first static fallback conversion from USD/token cost to M2 credits for estimates before ledger/resource pricing data is available? + diff --git a/specs/SPEC-INDEX.md b/specs/SPEC-INDEX.md new file mode 100644 index 0000000..c56b5ed --- /dev/null +++ b/specs/SPEC-INDEX.md @@ -0,0 +1,181 @@ +# 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.*