# S3 — Storefront: cargstore -> M2 Store ## 1. Scope & non-goals ### MVP scope Revive `github.com/machine-machine/cargstore` as the in-desktop **M2 Store**. Keep the Electron + React app and add a Solution install backend beside the existing Flatpak backend. Grounding inspected 2026-07-02: - cargstore `main` has Electron files `electron/main.ts`, `electron/preload.ts`, `electron/flatpak.ts`, `electron/clawdbot-client.ts`; React pages/hooks under `src/`; local JSON catalog `catalog/apps.json`; Dockerized `web/` seed. - Current app exposes IPC namespace `window.cargstore.flatpak.*`, `window.cargstore.catalog.get`, `window.cargstore.app.*`, and registers `flatpak` protocol only. - Current catalog fields map closely to listing fields: `id/name/summary/description/category/icon/featured/keywords` plus `flatpakRef|bundleUrl`. - Existing market repo freezes `schemas/listing.schema.json`, `schemas/solution.schema.json`, `contracts/cli.md`, `contracts/catalog-index.md`, `contracts/apply-adapter.md`, and `contracts/ledger-api.md`. - Live host has m2-gpt healthy at `https://gpt.machinemachine.ai/health`, Coolify network, memory-api, Forgejo, and m2o desktops. `chris-m2o` runs primus; `gunnar-m2o` is an `agent-latest` canary container currently present from Coolify. MVP delivers: 1. Rebrand product to **M2 Store** while preserving cargstore architecture. 2. Catalog source switch from bundled `catalog/apps.json` to `market:catalog` through the `m2-market` CLI, with local cache fallback. 3. A new Solution backend that shells `m2-market show|install|wallet --json` and emits progress/status to the renderer. 4. Solution detail page showing evidence, permissions diff, price, seller, install status, and install button. 5. Deep-link protocol `m2store://solution/` used by Scout/Hermes/toasts to open a listing detail page. 6. Identity/bootstrap path using m2-gpt-issued agent credentials and runtime-injected marketplace service keys stored in `~/.m2-market/config.toml` mode `0600`. 7. Canary desktop deployment path for `chris-m2o` and one non-client `agent-latest` desktop; broader fleet uses runtime-sync once proven. ### Later - Hosted browser marketplace from cargstore `web/`. - Service/resource/capability purchase flows beyond read-only discovery. - Seller/admin publishing UI; builder publishing remains `m2-market publish`. - Full self-update through store UI. Keep cargstore self-update disabled unless wired to Forgejo/Coolify release trust. - Flatpak removal. Existing Flatpak backend may remain as an "Apps" inventory surface, but Solution install is separate and primary. ### Non-goals - Ledger internals, pricing policy, wallet accounting, refunds: S1 owns ledger. - Listing schema/indexer truth model: S2 owns catalog/registry. - Scout matching host and proposal policy: S4 owns Scout. S3 only specifies Store deep-link and proposal event acceptance. - Packaging images and runtime-sync implementation: S5 owns packaging. S3 consumes install artifacts. - Hermes proposal skill logic: S6 owns proposal engine. S3 exposes open/show/install surfaces. ## 2. User stories + acceptance criteria ### US1 — Browse published Solutions in the desktop Store Given an operator is inside an m2o desktop with `~/.m2-market/config.toml` present, when M2 Store opens, then it loads tenant-visible listings from `market:catalog`, renders featured/category/search views, and never shows listings outside `tenant_visibility`. Acceptance: - Store calls `m2-market search "" --type solution --limit 50 --json` through main-process IPC. - Response items validate as `m2.listing.v1` payloads. - If the catalog backend is unavailable, Store renders the last successful cache from `~/.cache/m2-store/catalog.json` with an offline marker and disables install if listing detail cannot be refreshed. - No bundled seed listing is treated as authoritative in production. ### US2 — Inspect evidence, permissions, price, and install impact Given a user opens a listing, when the Solution detail page loads, then it shows price in M2 credits, seller, evidence summary, detailed evidence refs, permissions diff, applicability, install state, and rollback note before installation. Acceptance: - Store calls `m2-market show --json`. - Detail payload includes listing + solution excerpt + `permissions_diff`. - Install button text includes the exact credit amount. - Install button is disabled when `applicability.image_classes` excludes the current image class, when `inventory_type != "solution"`, or when wallet balance is insufficient. - Permission diff is rendered from structured fields, not free-form Markdown. ### US3 — Install a Solution through the standard marketplace path Given the operator confirms install, when Store invokes install, then money moves before apply, grant is recorded, apply is idempotent, and failures refund through CLI semantics. Acceptance: - Store calls `m2-market install --yes --json`. - Store never calls `m2-ledger` directly for install. - A CLI exit code maps to UI state: - `0`: installed or no-op. - `2`: listing not found. - `3`: insufficient funds, no apply attempted. - `4`: apply failed and refund attempted. - `5`: validation/firewall rejection. - `6`: `m2-core-sync` rails not landed; show remediation and leave state unchanged. - Re-running install on an already-applied listing renders "Installed" and does not create duplicate tx rows. ### US4 — Open Store from Scout/Hermes deep link Given Scout proposes a Solution, when the operator clicks the toast/link, then M2 Store opens directly to the Solution detail page and records that the proposal was accepted only after the user reaches the page or clicks Install, per S4 policy. Acceptance: - Desktop registers `m2store://solution/`. - `m2store://solution/lst_competitor-scan?source=scout&proposal_id=prop_...` opens detail route `/solution/lst_competitor-scan`. - Unknown listing IDs render a not-found page and do not trigger install. - Deep links cannot pass shell arguments or override backend URLs. ### US5 — Operate without baking secrets into images Given a desktop is provisioned, when M2 Store starts, then it obtains identity from runtime config generated from m2-gpt-issued credentials and never ships keys in the Electron bundle, image, repo, or catalog. Acceptance: - Store reads only `~/.m2-market/config.toml` and redacted env metadata for identity. - Config file mode must be `0600`; Store refuses install and catalog writes if permissions are broader. - Required config keys: `operator_id`, `tenant`, `ledger_url`, `ledger_api_key`, `memory_api_url`, `memory_api_key`, `forgejo_url`, `forgejo_token`, `apply_adapter`. - Store logs redact all `*_key`, `*_token`, and bearer-like strings. ## 3. Interfaces & data contracts ### 3.1 Repository and branch - Upstream repo: `github.com/machine-machine/cargstore`. - Working branch for this revival: `m2-store`. - Product metadata: - `package.json:name`: may remain `cargstore` internally. - `build.productName`: `M2 Store`. - `build.appId`: `ai.machinemachine.m2store`. - protocols: add `m2store`; keep `flatpak` only if Flatpak surface remains. ### 3.2 Main-process modules Keep cargstore modules and add: ```text electron/ flatpak.ts # existing, unchanged except renamed UI surface if desired solution.ts # new: shells m2-market CLI market-config.ts # new: reads/validates ~/.m2-market/config.toml, redacts logs catalog-client.ts # new: CLI-backed market:catalog read + cache deep-link.ts # new: protocol parse/route handoff m2-agent-client.ts # renamed/reworked clawdbot-client.ts, still ws://localhost:18789 ``` ### 3.3 Renderer model Replace `CatalogApp` with `StoreItem` while keeping field compatibility for cards: ```ts type InventoryType = "solution" | "capability" | "resource" | "service" | "flatpak"; interface StoreItem { id: string; // listing_id for market, flatpak app id for flatpak backend: "solution" | "flatpak"; inventory_type: InventoryType; name: string; summary: string; description?: string; category: string; icon?: string; featured?: boolean; keywords: string[]; price?: { amount: number; currency: "m2cr"; model: string }; seller?: string; stats?: { installs: number; rating?: number; proposals_shown: number; proposals_accepted: number }; install_ref?: string; } ``` ### 3.4 Listing/detail payload IPC `solution:show` returns: ```json { "listing": { "schema_version": "m2.listing.v1", "listing_id": "lst_competitor-scan", "solution_id": "sol_competitor-scan", "solution_version": "1.0.0", "inventory_type": "solution", "name": "Competitor Scan", "summary": "Research report with sources and recommendations", "category": "research", "keywords": ["research", "competitor", "report"], "icon": "solution-research", "price": { "amount": 40, "currency": "m2cr", "model": "fixed" }, "seller": "op_sdjs", "evidence_summary": "3 successful report runs, median 42 min", "tenant_visibility": ["*"], "stats": { "installs": 0, "proposals_shown": 0, "proposals_accepted": 0 }, "status": "published", "install_ref": "lst_competitor-scan-v1.0.0" }, "solution": { "schema_version": "m2.solution.v1", "permissions": [ { "kind": "memory", "partition": "market:evidence", "access": "write" }, { "kind": "filesystem", "path": "~/.local/share/m2-solutions/competitor-scan", "access": "write" } ], "deployment": { "recipe_ref": "recipe.yaml", "entrypoint": "hermes skill competitor-scan", "verify_command": "m2-solution verify competitor-scan" }, "applicability": { "image_classes": ["primus", "agent-latest"], "roles": ["any"], "tool_requirements": ["m2-market-cli"] }, "evidence": [ { "source": "forgejo:m2/market-registry/.../evidence.json", "machine": "chris-m2o", "tokens": 120000, "wall_time": 2520 } ], "license": { "terms": "M2 internal commercial license", "major_version_coverage": true } }, "wallet": { "operator_id": "op_chris", "balance": 100 }, "install_state": { "status": "not_installed" }, "permissions_diff": [ { "kind": "filesystem", "target": "~/.local/share/m2-solutions/competitor-scan", "current": "absent", "requested": "write", "risk": "low" } ] } ``` The detail page must not fetch Forgejo raw evidence directly from the renderer. The main process/CLI owns network I/O and returns sanitized JSON. ### 3.5 Electron preload API Expose a new namespace while keeping old Flatpak namespace for compatibility: ```ts contextBridge.exposeInMainWorld("m2store", { window: { minimize, maximize, close }, catalog: { search: (query: string, opts?: { type?: string; limit?: number }) => ipcRenderer.invoke("catalog:search", query, opts), getFeatured: () => ipcRenderer.invoke("catalog:featured"), getCached: () => ipcRenderer.invoke("catalog:cached") }, solution: { show: (listingId: string) => ipcRenderer.invoke("solution:show", listingId), install: (listingId: string) => ipcRenderer.invoke("solution:install", listingId), onProgress: (cb) => ipcRenderer.on("solution:progress", (_e, data) => cb(data)), openEntrypoint: (listingId: string) => ipcRenderer.invoke("solution:open-entrypoint", listingId) }, wallet: { get: () => ipcRenderer.invoke("wallet:get") }, app: { getVersion, getIdentity: () => ipcRenderer.invoke("app:get-identity") } }); ``` IPC contracts: ```text catalog:search(query, {type, limit}) -> {items: StoreItem[], source: "live"|"cache", as_of} catalog:featured() -> same, query="" limit=50, featured derived from listing stats/category policy solution:show(listing_id) -> detail payload above solution:install(listing_id) -> InstallResult wallet:get() -> {operator_id,balance,as_of} app:get-identity() -> {operator_id, tenant, desktop, image_class, apply_adapter, config_path} ``` `InstallResult`: ```json { "ok": true, "listing_id": "lst_competitor-scan", "status": "applied", "grant_id": "gr_...", "entrypoint": "hermes skill competitor-scan", "state_path": "~/.m2-market/state.json" } ``` ### 3.6 CLI calls Store backend shells the CLI, never ad hoc HTTP for installation: ```bash m2-market search "$QUERY" --type solution --limit 50 --json m2-market show "$LISTING_ID" --json m2-market install "$LISTING_ID" --yes --json m2-market wallet --json ``` Execution requirements: - Use `spawnFile`/argument array, not shell interpolation. - Hard allowlist command path from config: default `m2-market` found on `PATH`; optional absolute override `M2_MARKET_CLI=/usr/local/bin/m2-market`. - Timeout: search/show/wallet 15s, install no hard short timeout but stream heartbeat; UI can cancel only before CLI begins apply. - Parse JSON from stdout; stderr is redacted and logged to `~/.local/state/m2-store/logs/main.log`. ### 3.7 Config and identity MVP config remains the existing CLI config: ```toml # ~/.m2-market/config.toml operator_id = "op_chris" tenant = "machine-machine" ledger_url = "http://m2-ledger:8000" ledger_api_key = "svc_xxx" memory_api_url = "https://memory.machinemachine.ai" memory_api_key = "mem_xxx" forgejo_url = "https://git.machinemachine.ai" forgejo_token = "frg_xxx" apply_adapter = "local" ``` Identity issuance contract with m2-gpt: - m2-gpt remains the authority for tenant/agent identity and budget substrate. - Existing pattern: `scripts/add-agent.sh ` and `scripts/mint-agent-key.sh ` create a tenant-scoped agent and a one-time `sk-m2-...` bearer, hashed at rest. - Add market bootstrap as an m2-gpt-admin operation, not a Store operation: ```http POST /admin/v1/tenants/{tenant_id}/agents/{agent_id}/market-credentials Authorization: Bearer Body: { "operator_id": "op_chris", "desktop": "chris-m2o", "scopes": ["catalog:read", "install:write", "ledger:service", "forgejo:read-release"], "ttl_days": 90 } 200 { "operator_id": "op_chris", "tenant": "machine-machine", "ledger_url": "http://m2-ledger:8000", "ledger_api_key": "svc_...", "memory_api_url": "https://memory.machinemachine.ai", "memory_api_key": "mem_...", "forgejo_url": "https://git.machinemachine.ai", "forgejo_token": "frg_...", "expires_at": "2026-09-30T00:00:00Z" } ``` The provisioner or a one-shot desktop bootstrap writes this response to `~/.m2-market/config.toml` with mode `0600`. Store only reads it. It does not mint or refresh admin credentials itself in MVP. ### 3.8 Catalog source switch Current cargstore: - `catalog:get` reads bundled `catalog/apps.json`. - Renderer does local string matching in `useCatalog`. M2 Store: - `catalog-client.ts` calls `m2-market search`. - `StoreItem.id = listing.listing_id`. - Cache path: `~/.cache/m2-store/catalog.json`. - Cache shape: ```json { "schema_version": "m2.store.catalog-cache.v1", "as_of": "2026-07-02T00:00:00Z", "tenant": "machine-machine", "items": [] } ``` The bundled `catalog/apps.json` is permitted only as dev fallback when `NODE_ENV=development`. ### 3.9 Solution backend beside Flatpak Current `FlatpakManager` persists Flatpak data under `FLATPAK_USER_DIR || /clawdbot_home/flatpak`. m2o primus docs say user installs persist under `/agent_home/flatpak` and home is `/agent_home/home/developer`. Add `SolutionManager`: ```ts class SolutionManager { show(listingId: string): Promise; install(listingId: string, onProgress?: (event: SolutionProgress) => void): Promise; listInstalled(): Promise; openEntrypoint(listingId: string): Promise; } ``` Installed state comes from `~/.m2-market/state.json`: ```json { "installs": { "lst_competitor-scan": { "version": "1.0.0", "grant_id": "gr_...", "ts": "2026-07-02T00:00:00Z", "changeset_hash": "sha256:...", "status": "applied" } } } ``` ### 3.10 Deep-link protocol Register: ```json { "name": "M2 Store", "schemes": ["m2store"] } ``` Supported URLs: ```text m2store://solution/ m2store://solution/?source=scout&proposal_id= m2store://search?q= ``` Rules: - `listing_id` must match `^lst_[a-z0-9-]+$`. - Query params allowed: `source`, `proposal_id`, `q`. Ignore all others. - No `file:`, `http:`, shell, adapter, or backend override params. - If app already running, focus existing window and navigate via renderer event `deeplink:navigate`. - Scout/Hermes use `m2store://solution/` as the stable contract. Do not use `m2store://solution/` because listing controls visibility, price, and version. ### 3.11 Agent WebSocket integration Current `ClawdbotClient` connects to `ws://localhost:18789` and registers app-store tools. Keep transport but rename semantics: ```json { "type": "register_tools", "tools": [ { "name": "m2_store_search", "description": "Search M2 Store Solutions", "parameters": { "query": "string" } }, { "name": "m2_store_show", "description": "Open a Solution detail page", "parameters": { "listing_id": "string" } }, { "name": "m2_store_install", "description": "Request human-confirmed install flow for a Solution", "parameters": { "listing_id": "string" } }, { "name": "m2_store_wallet", "description": "Show wallet balance", "parameters": {} } ] } ``` `m2_store_install` must navigate to detail and require human button click. It must not silently install on tool call. ### 3.12 Telemetry write-back Store writes only summarized events through CLI or a future `m2-market telemetry` verb. Until that verb exists, append local JSONL: ```text ~/.local/share/m2-store/events/YYYY-MM-DD.jsonl ``` Event: ```json { "schema_version": "m2.store.event.v1", "ts": "2026-07-02T00:00:00Z", "desktop": "chris-m2o", "operator_id": "op_chris", "tenant": "machine-machine", "event": "detail_opened|install_clicked|install_succeeded|install_failed", "listing_id": "lst_competitor-scan", "source": "browse|search|scout|hermes|deeplink", "proposal_id": "prop_optional", "error_code": null } ``` Batch ship target, owned by S2/S4 path: memory partition `market:evidence`. ## 4. Integration contract with other subsystems ### S1 Ledger Touchpoints: - Store calls ledger only through `m2-market install|wallet`. - `m2-market install` performs `POST /tx/install`, grant, apply, verify, and refund on apply failure. - Store displays `402 insufficient_funds` from CLI exit 3 and wallet balance from `m2-market wallet`. S3 requires S1 to keep `/tx/install` idempotent by `ref` and `GET /balance/{operator_id}` stable. ### S2 Catalog/registry Touchpoints: - Store consumes `m2.listing.v1` and `m2.solution.v1`. - Store reads semantic catalog via `m2-market search` backed by `market:catalog`. - Store detail reads the registry-backed full Solution via `m2-market show`. - Store expects `tenant_visibility` filtering to happen before results reach renderer. S3 requires S2 to expose a stable `m2-market show --json` payload including `permissions_diff`. ### S4 Scout Touchpoints: - Scout opens `m2store://solution/?source=scout&proposal_id=`. - Store emits local events with `source=scout` and `proposal_id`. - Store never performs install from Scout without human detail-page confirmation. ### S5 Packaging/deployment Touchpoints: - Store package can be deployed by bake or runtime-sync, but Solutions always install through `m2-market` and the apply adapter. - MVP canary install path: - install M2 Store artifact into `/opt/m2-store` or `~/.local/share/m2-store`; - create desktop entry/protocol handler in the user home volume; - ensure `m2-market` CLI and config exist; - canary `chris-m2o` first, then non-client `agent-latest`. - Fleet-wide deployment waits for S5 image/runtime policy; local registry public route remains broken and must not be assumed. ### S6 Hermes proposal engine Touchpoints: - Hermes can call `m2_store_search/show` tools via existing local gateway WebSocket if present. - Hermes proposals should link to `m2store://solution/` or invoke `m2_store_show`. - Store does not own build-vs-install reasoning. ### m2-gpt identity Touchpoints: - m2-gpt continues tenant/agent identity and budget/metering authority. - Provisioner/admin mints m2-gpt agent key and market service credentials. - Store consumes the generated `~/.m2-market/config.toml`. ### memory-api Touchpoints: - Store reads through CLI-backed `market:catalog`. - Store event summaries eventually ship to `market:evidence`. - Store does not write raw client data or raw session content. ## 5. Options compared ### Catalog access Option A: Store talks directly to memory-api. - Pros: fewer subprocesses, more interactive search. - Cons: duplicates auth, filtering, and payload logic in Electron; larger secret surface. Option B: Store shells `m2-market search/show --json`. - Pros: reuses frozen CLI contracts, tenant filtering, registry fetch, future adapter changes; lowest blast radius. - Cons: subprocess/error parsing and CLI dependency. Recommendation: **B for MVP**. Direct HTTP can be added later after CLI contracts harden. ### Install backend Option A: Replace Flatpak with Solution-only. - Pros: simpler product story. - Cons: throws away working app install feature and cargstore code. Option B: Add `SolutionManager` beside `FlatpakManager`. - Pros: preserves existing asset, permits Apps and Solutions tabs, isolates paid install path. - Cons: renderer model needs backend discriminator. Recommendation: **B**. Make Solutions default surface. ### Store deployment Option A: Bake M2 Store into primus. - Pros: available on every new desktop; protocol registration predictable. - Cons: slower iteration; agent-latest remains mixed; local registry route is broken. Option B: Runtime-sync/install into agent-home on canaries. - Pros: respects mixed fleet and fedlearn universal path; reversible; canary-first. - Cons: protocol/desktop entry must be installed per desktop until image bake. Recommendation: **B for MVP canaries**, then bake once S5 certifies it. ### Identity storage Option A: Store calls m2-gpt to mint/refresh credentials. - Pros: self-service. - Cons: Store would need high-privilege bootstrap token. Option B: Admin/provisioner calls m2-gpt; Store reads scoped runtime config. - Pros: no admin secret in desktop app; matches `M2_GPT_API_KEY` injection pattern. - Cons: requires bootstrap step. Recommendation: **B**. ### Deep-link ID Option A: `m2store://solution/`. - Problem: solution ID omits price, visibility, and version. Option B: `m2store://solution/`. - Pros: listing is the catalog-facing, tenant-filtered, priced unit. Recommendation: **B**. ## 6. Risks/edge cases - Tenant leakage: renderer must never bypass CLI tenant filters; detail fetch must 404 for invisible listings. - Secrets: `~/.m2-market/config.toml` contains multiple live keys; require `0600`, redact logs, and never expose full config through preload. - Shell injection: all CLI calls must use argument arrays and validate listing IDs before spawning. - Duplicate installs: rely on CLI/ledger idempotency and local state; Store button must debounce while install is running. - Partial failure: install may debit then fail apply; Store must surface CLI exit 4 and refund status exactly. - Offline catalog: cache is for browsing only; install requires fresh `show` unless CLI can verify cache freshness. - Protocol abuse: deep links must only navigate. No params can trigger installation. - Mixed fleet: agent-latest may lack Hermes/WebSocket or protocol handler support; Store must still work through direct UI/CLI. - Registry route: do not depend on public local registry push/pull for Store deployment. - Self-update trust: cargstore self-update currently downloads GitHub releases and extracts into `/opt/cargstore`; disable or rework before production to avoid unreviewed mutation path. - Flatpak persistence path mismatch: cargstore default `/clawdbot_home/flatpak` differs from m2o primus `/agent_home/flatpak`; set `FLATPAK_USER_DIR=/agent_home/flatpak` if Flatpak surface remains. - Pricing models: current `solution.schema.json` only allows fixed price even brief mentions job-based pricing. Store UI must render unknown future `price.model` read-only until S1/S2 extend schemas. ## 7. [NEEDS CLARIFICATION] 1. [NEEDS CLARIFICATION] Exact m2-gpt endpoint for issuing marketplace service credentials does not exist in inspected code. Spec defines the required contract; implementation owner must decide whether it lands in m2-gpt admin API or a bootstrap script first. 2. [NEEDS CLARIFICATION] `m2-market show --json` must include `permissions_diff`, but current contracts mention it narratively, not as a frozen JSON schema. S2/CLI should freeze the detail payload before Store implementation. 3. [NEEDS CLARIFICATION] Whether Flatpak apps remain visible in M2 Store MVP or are hidden behind a secondary "Apps" tab for canaries. Backend can support either; product default should be set before UI polish.