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