specs: m2-market spec discovery — 6 subsystem specs + synthesis index

herdr workforce output (6 codex angle-specialists -> claude synthesis):
S1 ledger/economy, S2 catalog/registry, S3 storefront (cargstore->M2 Store),
S4 Solution Scout (standalone watcher recommended), S5 packaging + 3 seed
Solutions (mm-pdf 25cr, agent-scaffold 35cr, competitor-scan 60cr),
S6 proposal engine + evidence loop.

SPEC-INDEX is the tie-break: 23-row cross-cutting contract table (16 mismatches
resolved, e.g. /v1 ledger paths win, m2store://listing/<id> deep link, one
m2.market.telemetry.v1 envelope, op_<slug> operator ids, tenant ids from m2-gpt),
8 deduped clarifications with defaults, 4-phase build order where the paid-install
wedge does NOT block on fedlearn (local ApplyAdapter until rails land), risk register.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m2 (AI Agent) 2026-07-02 02:43:50 +02:00
parent b994572dde
commit 8a8db9f2cc
8 changed files with 4429 additions and 0 deletions

53
specs/BRIEF.md Normal file
View file

@ -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/<YOUR_ID>.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_<YOUR_ID>

759
specs/S1-ledger-economy.md Normal file
View file

@ -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 <listing_id>`: 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.

View file

@ -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 <bundle-dir>`, then the
CLI validates schemas, computes the bundle hash, creates a Forgejo branch
`listing/<listing_id>-v<semver>`, 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 <reason>`, 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 <listing_id>`, 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 <listing_id>` runs, then S2
resolves the listing from registry/catalog, verifies status and tenant visibility, fetches
release `<listing_id>-v<semver>`, 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_id>/
listing.json
solution.json
evidence/
<evidence_id>.json
releases:
<listing_id>-v<semver>
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: `<listing_id>-v<semver>`, 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": "<git-sha>",
"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: <listing_id>
Solution: <solution_id>@<version>
Inventory type: solution|service|capability|resource
Price: <fixed amount m2cr | job quote range>
Seller: <operator_id>
Tenant visibility: <["*"] or tenant ids>
## Evidence
<summary with evidence refs>
## Permission Diff
<new permissions vs current installed state or "new install">
## Tenant Firewall
Owner initiated: true|false|not-applicable
Double scrubbed: true|false|not-applicable
## Rollback
<how to uninstall/rollback release asset>
## Veto
Veto until: <timestamp>
Use label `veto` or comment `/veto <reason>`.
```
Required labels:
- `market:listing`
- `disposition:commercialize`
- `risk:low|medium|high`
- `inventory:solution|capability|resource|service`
- `veto-until:<iso8601>`
- `tenant:<tenant-id|public>`
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_id>/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": "<git-sha>",
"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: <runtime 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": "<git-sha>"
}
]
}
```
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:<uuid>",
"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/<listing_id>/`;
- creates release `<listing_id>-v<semver>`;
- 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/<timestamp>.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_id>/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/<listing_id>-v<semver>
```
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:<uuid>"
}
```
### 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_id>/listing.json`
- `/home/m2/m2-market/listings/<listing_id>/solution.json`
- `/home/m2/m2-market/listings/<listing_id>/evidence/*.json`
CLI local state:
- `~/.m2-market/config.toml`
- `~/.m2-market/state.json`
- `~/.cache/m2-market/releases/<install_ref>/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?

View file

@ -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/<listing_id>` 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 <listing_id> --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 <listing_id> --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/<listing_id>`.
- `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 <tenant_id> <agent_id>` and `scripts/mint-agent-key.sh <agent_id>` 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 <operator-or-tenant-admin-token>
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<SolutionDetail>;
install(listingId: string, onProgress?: (event: SolutionProgress) => void): Promise<InstallResult>;
listInstalled(): Promise<InstalledSolution[]>;
openEntrypoint(listingId: string): Promise<void>;
}
```
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/<listing_id>
m2store://solution/<listing_id>?source=scout&proposal_id=<proposal_id>
m2store://search?q=<urlencoded query>
```
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/<id>` as the stable contract. Do not use `m2store://solution/<solution_id>` 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/<listing_id>?source=scout&proposal_id=<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/<listing_id>` 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/<solution_id>`.
- Problem: solution ID omits price, visibility, and version.
Option B: `m2store://solution/<listing_id>`.
- 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.

537
specs/S4-solution-scout.md Normal file
View file

@ -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/<listing_id>?proposal_id=<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: <memory api key from ~/.m2-market/config.toml>
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: <memory 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 <proposal_id> [--reason not-relevant]
m2-solution-scout open <proposal_id>
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.

647
specs/S5-packaging-seeds.md Normal file
View file

@ -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 <operator_id>`,
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 <listing_id>` 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 <listing_id>` runs again,
Then no duplicate debit occurs and the adapter returns no-op success.
Given an installed Solution,
When `m2-market uninstall <listing_id>` 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_id>/
├── listing.json
├── solution.json
├── evidence/
│ ├── provenance.jsonl
│ ├── cost.json
│ ├── scrub-attestation.json
│ └── permissions-diff.md
└── README.md
releases/<install_ref>.tar.gz # Forgejo release asset, not committed as a blob
```
Builder workspaces may live in this repo under `solutions/<slug>/`, 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/<skill-name>/...
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 <input.md> [--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 <selected m2o container>"
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_<uuid>",
"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 <path> --id <solution_id> --name <name>`
- `m2-market package import-fedlearn --cluster <cluster_id> --out solutions/<slug>`
- `m2-market package validate <bundle-dir> [--strict-tenant]`
- `m2-market package build <bundle-dir> --out dist/<install_ref>.tar.gz`
- `m2-market publish <bundle-dir> --seller <operator_id> --price <credits>`
Buyer:
- `m2-market install <listing_id> [--yes]`
- `m2-market uninstall <listing_id> [--yes]`
- `m2-market upgrade <listing_id> [--to <semver>]`
- `m2-market invoke <listing_id> -- <args>` optional wrapper that records invoke telemetry
Admin/review:
- `m2-market package diff-permissions <old-bundle> <new-bundle>`
- `m2-market package evidence-summary <bundle-dir>`
- `m2-market package verify-release <install_ref>`
### Install/uninstall/upgrade sequence
Install:
1. Resolve listing from `market:catalog`; fetch registry `solution.json`.
2. License hook: `GET /licenses/check?operator_id=<op>&listing_id=<listing>&major=<n>`.
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 <tar> --license-grant <grant_id>`.
7. Apply: `m2-core-sync apply --bundle <tar> --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 <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:<uuid>"}
200 {"tx_ids":[1,2],"grant":{"grant_id":"gr_...","covers_major":1}}
POST /tx/refund
{"ref":"lst_mm-pdf-report:chris-m2o:<uuid>","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:<low|medium|high>`,
`veto-until:<timestamp>`, 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 <input.md> [--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 <agent-name> \"<what this agent does>\" [--out <dir>]"
```
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/<agent-name>/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 \"<company or market>\" --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?

View file

@ -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 <m2-gpt tenant key>` 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 "<intent>" --tenant <tenant> --operator <operator_id> --json
m2-market evidence capture --job-id <job_id> --from-herdr <run_id> --correlation-id <turn_id>
m2-market evidence submit evidence.json
m2-market evidence show <evidence_id> --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/<listing_id>`.
- 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?

181
specs/SPEC-INDEX.md Normal file
View file

@ -0,0 +1,181 @@
# SPEC-INDEX — M2 Marketplace spec discovery synthesis
Synthesized 2026-07-02 from S1S6 (all six specs present). Individual specs are unmodified;
where they disagree, **this index is the tie-break** and names the owning spec to amend.
## 1. Executive summary
The M2 Marketplace turns proven, repeatable outcomes into installable **Solutions** sold for
**M2 credits**. Six subsystems compose it:
- **S1 `m2-ledger`** — standalone Coolify service, SQLite, append-only credit ledger with
hash chain. Handles starter grants (100 m2cr), fixed-price install settlement (10% platform
cut), license grants keyed `(operator, listing, major_version)`, escrow-backed job
settlement for operator-delivered services, route/resource settlement referencing m2-gpt
metering, and daily audit snapshots committed to Forgejo. Architected to migrate under
extended m2-gpt billing later; ships internal credits first.
- **S2 Catalog/Registry** — Forgejo org `m2` is the system of record. Listings enter via PR +
veto (reusing fedlearn's scored-PR pipeline with a new `commercialize` disposition);
releases carry immutable bundles verified by `content_hash`. `market:catalog` in memory-api
is a rebuildable semantic index, never truth. `m2-market` CLI (search/show/install/publish)
is the universal programmatic surface, tenant-filtered end to end.
- **S3 M2 Store** — revival of the cargstore Electron app. Adds a `SolutionManager` beside the
existing Flatpak backend, shells the `m2-market` CLI (never raw HTTP to ledger), renders
price/evidence/permission-diff before install, registers the `m2store://` deep-link
protocol, and reads identity from provisioner-written `~/.m2-market/config.toml` (0600).
Canary-first on `chris-m2o`, then one `agent-latest` desktop.
- **S4 Solution Scout** — standalone supervised per-desktop watcher (recommended over Hermes
or herdr plugin). Reads only already-summarized local sources (herdr run summaries),
summarizes on-box, queries the tenant-filtered catalog, and surfaces at most a few
non-blocking proposals/day via toast + Store deep link. Propose-only; never installs.
- **S5 Packaging & Seeds** — bundle contract (`solution.yaml` source → canonical
`solution.json`/`listing.json`, `recipe.yaml`, `permissions.yaml`, evidence files), install/
uninstall/upgrade through the runtime-sync (`m2-core-sync`) apply adapter with a local
adapter fallback until fedlearn rails land. Three seed Solutions: `mm-pdf` (25 m2cr),
`agent-scaffold` (35 m2cr), `competitor-scan` (60 m2cr).
- **S6 Proposal Engine**`m2-market-proposer` Coolify service plus a thin Hermes
`market-propose` skill. Composes build-vs-install-vs-job proposals with evidence-weighted
p50/p80/p95 estimates, cites listings and prior job evidence, and closes the pricing loop by
writing `m2.market.evidence.v1` records for every serious job.
The paid-install flow that everything serves: publish (PR+veto) → discover (search/Scout/
Hermes) → inspect (price, evidence, permission diff) → settle (ledger debit + license grant)
→ apply (runtime-sync recipe) → verify → telemetry back into evidence and listing stats.
Tenant isolation, no-secrets-in-artifacts, canary-first reversibility, and Forgejo-as-truth
are enforced at every hop.
## 2. Cross-cutting contract table
Legend: ✔ = specs agree; **MISMATCH** = specs conflict, fix given; **GAP** = referenced but
not yet existing/frozen. "Owner" is the spec whose definition is normative after the fix.
| # | Interface | Owner | Consumers | Status / resolution |
|---|-----------|-------|-----------|---------------------|
| 1 | `solution.schema.json` (`m2.solution.v1`) | S2 | S3, S4, S5, S6 | **MISMATCH** — S2 specs `price.model: fixed\|job`; frozen repo schema and S5 only validate `fixed` (S5 invents `price_ext`/service workaround). **Fix:** extend the enum to `fixed\|job` in one schema PR before first paid install; delete the `price_ext` workaround. Job listings remain non-direct-installable. |
| 2 | `listing.schema.json` (`m2.listing.v1`) | S2 | S3, S4, S6 | **MISMATCH**`seller` is an object in S2, a bare string in S3/S5 examples. **Fix:** object form `{operator_id, display_name, ...}` is canonical in v1; renderers show `display_name`. |
| 3 | Install settlement endpoint | S1 | `m2-market` CLI (S2/S5); indirectly S3 | **MISMATCH** — S1 defines `POST /v1/settlements/install`; S2/S3/S4/S5 all say `POST /tx/install`. **Fix:** S1's versioned path wins (ledger owner). Only the CLI calls the ledger directly, so S2/S5 update their touchpoint lists and `contracts/ledger-api.md`; S3/S4 are insulated by the CLI. |
| 4 | License check | S1 | S5 install path, S3 installed-state | **MISMATCH** — S1: `GET /v1/licenses/{operator_id}/{listing_id}?major_version=`; S5: `GET /licenses/check?...`. **Fix:** S1 path wins; S5 adapter hook updates. |
| 5 | Refund on apply failure | S1 | S5/CLI (exit code 4 path) | **MISMATCH** — S1 refunds by `POST /v1/settlements/install/{tx_group_id}/refund`; S5 refunds by `ref`. **Fix:** CLI persists `tx_group_id` in `~/.m2-market/state.json` at settle time and refunds by it. |
| 6 | Balance / wallet | S1 | S3 via `m2-market wallet` | **MISMATCH** — S1: `GET /v1/balances/{account_or_operator_id}`; S3 assumes `GET /balance/{operator_id}`. **Fix:** S1 path; CLI insulates the Store. |
| 7 | Jobs API (`POST /v1/jobs`, milestones, cancel, dispute) | S1 | S6 (`operator_job` path), S3 (later checkout), S2 (job-listing refusal handoff) | ✔ — post-wedge. Note S2's job-price example says `settlement.ledger_reason: "route"`; that is wrong — job settlements use `tx_type=job`. Fix in S2 example. |
| 8 | `market:catalog` partition + search filter contract | S2 (writer: `m2-market-indexer` only) | CLI, S3, S4, S6 | **MISMATCH** — filter shapes differ: S2 `filters.tenant_visibility_any: ["*", tenant]`; S4/S6 pass `tenant_id` arrays. **Fix:** S2's filter contract is canonical; server-side filtering when memory-api supports it, mandatory client-side backstop in every reader (drop hits failing `tenant_visibility`/`status=published`). |
| 9 | `market:evidence` partition | S6 (job evidence) + S2 (stats folding) | S4, S5, S3 write telemetry; S2 indexer folds into `listing.stats`; S6 reads for estimates | **MISMATCH** — five near-colliding schemas, worst being S2 `m2.market_evidence.v1` vs S6 `m2.market.evidence.v1`. **Fix:** reserve `m2.market.evidence.v1` (S6) for job-evidence records; unify all shown/opened/installed/dismissed telemetry (S2/S3/S4/S5 events) under one envelope `m2.market.telemetry.v1` with an `event_type` registry. S2 renames its record. |
| 10 | Deep link protocol | S3 | S4, S6 | **MISMATCH** — S3 registers `m2store://solution/<listing_id>`; S4 and S6 both emit `m2store://listing/<listing_id>`. **Fix:** `m2store://listing/<listing_id>` (majority, and the ID *is* a listing_id). S3 amends; params stay `?source=&proposal_id=&q=` only, navigate-only semantics. |
| 11 | `m2-market` CLI JSON surface + exit codes 06 | S2 (core), S5 (package/install/uninstall/upgrade), S6 (propose/evidence) | S3 (sole backend), S4 (fallback) | ✔ on verbs and exit codes. **GAP**`show --json` must return a frozen `permissions_diff` schema before Store implementation (S3 clarification #2). Freeze it in `contracts/cli.md`. |
| 12 | Release bundle format | S5 | S2 (hash check, release hosting), S3 (display) | **MISMATCH** — S2: `solution-bundle.tar.zst` + inner `manifest.json`; S5: `<install_ref>.tar.gz`, no manifest. **Fix:** S5 owns packaging → `.tar.gz` named `<install_ref>.tar.gz`, but adopt S2's inner `manifest.json` (per-file sha256 list). `content_hash` computed over the normalized tarball. |
| 13 | Registry repo, paths, PR labels | S2 | S5 (publish target), S1 (audit), fedlearn `commercialize` | **MISMATCH** — S2 recommends existing `m2/m2-market`; S5 assumes `m2/market-registry`; S1 wants `m2/market-audit`. **Fix:** registry = `m2/m2-market` (exists today; split later is a location change only). Ledger audit = separate `m2/market-audit` so daily snapshot commits don't pollute registry history. S2's label set is canonical; S5 drops `status:in_review` label (state lives in `listing.status`). |
| 14 | Identity & credential bootstrap | m2-gpt | S1 (bearer introspection), S3 (config bootstrap), S6 (tenant keys) | **GAP** — S3's `POST /admin/v1/.../market-credentials` and S1's introspection endpoint don't exist in m2-gpt yet. **Fix (MVP):** a `scripts/market-bootstrap.sh` admin script mints keys via existing m2-gpt admin scripts and writes `~/.m2-market/config.toml` (0600); ledger validates service keys from its own keys file and defers bearer introspection to a small m2-gpt verify endpoint added post-wedge. |
| 15 | Metering read `GET /internal/v1/metering/turn/{correlation_id}` | m2-gpt | S6 evidence capture | **GAP** — endpoint doesn't exist; S6 already specs the degraded path (`tokens_unknown=true` + herdr run ids). Ship degraded in MVP; add endpoint with the jobs phase. |
| 16 | Proposal schemas (`m2.market.proposal-request.v1`, `m2.market.proposal.v1`) | S6 | S4 (may call proposer), S3 (`proposal_id` passthrough), S1 (settlement ref metadata) | **MISMATCH** — ID prefix `prop_` (S3/S6) vs `prp_` (S4). **Fix:** `prop_`. S4's local matcher is fine for MVP; it adopts the proposer API when S6 lands. |
| 17 | Scout→Hermes bridge event `m2.hermes.market_proposal.v1` | S4 | Hermes `market-propose` skill (S6) | ✔ shape; transport unresolved → clarification C8. Best-effort in MVP. |
| 18 | `~/.m2-market/config.toml` (0600) | S2/CLI | S3, S4, S5 | ✔ — keys: `operator_id, tenant, ledger_url, ledger_api_key, memory_api_url, memory_api_key, forgejo_url, forgejo_token, apply_adapter`. |
| 19 | Install state `~/.m2-market/state.json` | S5 | S3 (installed badge), S1 (`grant_id`/`tx_group_id` refs) | ✔ — add `tx_group_id` per fix #5. |
| 20 | Apply adapter (`m2-core-sync plan/apply/uninstall` + `m2.recipe.v1`) | fedlearn / S5 wraps | install path (S2 CLI), Scout self-install, Store deploy | ✔ contract; **GAP** on exact uninstall/upgrade verb names (clarification C6). Local `ApplyAdapter` executes the same recipe shape until rails land. |
| 21 | Tenant namespace | m2-gpt `tenants` table | everyone | **MISMATCH** — "m2" (S1), "machine-machine" (S2/S3/S4), "m2-core"/"`__fleet__`" (S6). **Fix:** canonical tenant IDs come from the m2-gpt `tenants` table, one documented list in `contracts/tenants.md`. `tenant_visibility: ["*"]` means public; `m2-core` is the shared/commons scope for evidence. No other sentinels (`__fleet__` folds into `m2-core`). |
| 22 | Operator ID format | S1 registry mapping | S2, S5, S6 | **MISMATCH**`op_<slug>` (S1/S2/S5) vs bare `sdjs-operator`/`m2bd` (S6). **Fix:** `op_<slug>` everywhere. |
| 23 | Ledger events JSONL (`m2.ledger.event.v1`) | S1 | S2 indexer (install counts, pricing evidence) | ✔. |
## 3. Consolidated [NEEDS CLARIFICATION] — deduped, with recommended defaults
18 raw markers across S1S6 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 | S1S6 | 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.*