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>
25 KiB
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
mainhas Electron fileselectron/main.ts,electron/preload.ts,electron/flatpak.ts,electron/clawdbot-client.ts; React pages/hooks undersrc/; local JSON catalogcatalog/apps.json; Dockerizedweb/seed. - Current app exposes IPC namespace
window.cargstore.flatpak.*,window.cargstore.catalog.get,window.cargstore.app.*, and registersflatpakprotocol only. - Current catalog fields map closely to listing fields:
id/name/summary/description/category/icon/featured/keywordsplusflatpakRef|bundleUrl. - Existing market repo freezes
schemas/listing.schema.json,schemas/solution.schema.json,contracts/cli.md,contracts/catalog-index.md,contracts/apply-adapter.md, andcontracts/ledger-api.md. - Live host has m2-gpt healthy at
https://gpt.machinemachine.ai/health, Coolify network, memory-api, Forgejo, and m2o desktops.chris-m2oruns primus;gunnar-m2ois anagent-latestcanary container currently present from Coolify.
MVP delivers:
- Rebrand product to M2 Store while preserving cargstore architecture.
- Catalog source switch from bundled
catalog/apps.jsontomarket:catalogthrough them2-marketCLI, with local cache fallback. - A new Solution backend that shells
m2-market show|install|wallet --jsonand emits progress/status to the renderer. - Solution detail page showing evidence, permissions diff, price, seller, install status, and install button.
- Deep-link protocol
m2store://solution/<listing_id>used by Scout/Hermes/toasts to open a listing detail page. - Identity/bootstrap path using m2-gpt-issued agent credentials and runtime-injected marketplace service keys stored in
~/.m2-market/config.tomlmode0600. - Canary desktop deployment path for
chris-m2oand one non-clientagent-latestdesktop; 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 --jsonthrough main-process IPC. - Response items validate as
m2.listing.v1payloads. - If the catalog backend is unavailable, Store renders the last successful cache from
~/.cache/m2-store/catalog.jsonwith 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_classesexcludes the current image class, wheninventory_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-ledgerdirectly 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-syncrails 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.tomland 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 remaincargstoreinternally.build.productName:M2 Store.build.appId:ai.machinemachine.m2store.- protocols: add
m2store; keepflatpakonly if Flatpak surface remains.
3.2 Main-process modules
Keep cargstore modules and add:
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:
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:
{
"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:
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:
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:
{
"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:
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-marketfound onPATH; optional absolute overrideM2_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:
# ~/.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>andscripts/mint-agent-key.sh <agent_id>create a tenant-scoped agent and a one-timesk-m2-...bearer, hashed at rest. - Add market bootstrap as an m2-gpt-admin operation, not a Store operation:
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:getreads bundledcatalog/apps.json.- Renderer does local string matching in
useCatalog.
M2 Store:
catalog-client.tscallsm2-market search.StoreItem.id = listing.listing_id.- Cache path:
~/.cache/m2-store/catalog.json. - Cache shape:
{
"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:
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:
{
"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:
{
"name": "M2 Store",
"schemes": ["m2store"]
}
Supported URLs:
m2store://solution/<listing_id>
m2store://solution/<listing_id>?source=scout&proposal_id=<proposal_id>
m2store://search?q=<urlencoded query>
Rules:
listing_idmust 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 usem2store://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:
{
"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:
~/.local/share/m2-store/events/YYYY-MM-DD.jsonl
Event:
{
"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 installperformsPOST /tx/install, grant, apply, verify, and refund on apply failure.- Store displays
402 insufficient_fundsfrom CLI exit 3 and wallet balance fromm2-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.v1andm2.solution.v1. - Store reads semantic catalog via
m2-market searchbacked bymarket:catalog. - Store detail reads the registry-backed full Solution via
m2-market show. - Store expects
tenant_visibilityfiltering 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=scoutandproposal_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-marketand the apply adapter. - MVP canary install path:
- install M2 Store artifact into
/opt/m2-storeor~/.local/share/m2-store; - create desktop entry/protocol handler in the user home volume;
- ensure
m2-marketCLI and config exist; - canary
chris-m2ofirst, then non-clientagent-latest.
- install M2 Store artifact into
- 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/showtools via existing local gateway WebSocket if present. - Hermes proposals should link to
m2store://solution/<listing_id>or invokem2_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_KEYinjection 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.tomlcontains multiple live keys; require0600, 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
showunless 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/flatpakdiffers from m2o primus/agent_home/flatpak; setFLATPAK_USER_DIR=/agent_home/flatpakif Flatpak surface remains. - Pricing models: current
solution.schema.jsononly allows fixed price even brief mentions job-based pricing. Store UI must render unknown futureprice.modelread-only until S1/S2 extend schemas.
7. [NEEDS CLARIFICATION]
- [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.
- [NEEDS CLARIFICATION]
m2-market show --jsonmust includepermissions_diff, but current contracts mention it narratively, not as a frozen JSON schema. S2/CLI should freeze the detail payload before Store implementation. - [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.