From a6b00e087356bb2c552b11b43a572884693c2262 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 27 Sep 2026 07:50:22 -0700 Subject: [PATCH] Spec: Triple-C marketplace design Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-09-27-marketplace-design.md | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-27-marketplace-design.md diff --git a/docs/superpowers/specs/2026-09-27-marketplace-design.md b/docs/superpowers/specs/2026-09-27-marketplace-design.md new file mode 100644 index 0000000..d2cef3d --- /dev/null +++ b/docs/superpowers/specs/2026-09-27-marketplace-design.md @@ -0,0 +1,293 @@ +# Triple-C marketplace — design + +Date: 2026-09-27 +Status: approved in conversation (user), section by section; awaiting review of this written spec + +## Goal + +Users add one or more **marketplace** git repos in Settings, browse the agents, skills, commands, +hooks and plugins they contain, and install any item either **for all projects** or **for +individual projects**. Private repos are supported through named sign-in accounts. Installed items +are pinned to a commit and only change when the user accepts an update. + +A public starter marketplace, `shadowdao/triple-c-marketplace` (local clone at +`/workspace/projects/triple-c-marketplace`, its own git repo), is created with one example of each +item type. It is also where marketplace items built later will be published. + +Non-goals (this version): per-marketplace auto-update, a registered Triple-C GitHub/OAuth app, +publishing to a marketplace from inside Triple-C, a separate OS window for the marketplace. + +## Decisions (user-approved) + +| Topic | Decision | +|---|---| +| Repo format | **Hybrid**: Triple-C-managed `agents/`, `skills/`, `commands/`, `hooks/`; `plugins/` is a standard Claude Code marketplace installed with `claude plugin` | +| Auth | **Named accounts**, one chosen per marketplace. GitHub sign-in reuses `gh` (host first, else inside a running container); any host accepts a pasted token. No Triple-C OAuth app | +| Scope | **Global list + per-project additions + per-project opt-out** of global items; global items reach projects created later | +| Updates | **Pinned on install**; "update available" per item; the user reviews a diff and accepts | +| Fetching | **On the host**, into an app cache, with `gix`; files copied into containers. Tokens never enter containers | +| Where the UI lives | Settings sidebar section + a full-width **Marketplace** main-area tab (like Project Home), not an OS window | + +## Current state (verified against `292fc90`) + +- Settings is `components/settings/SettingsPanel.tsx`, rendered in the sidebar, sections as + `AccordionSection`s. Main-area tabs are Zustand-driven (`store/appState.ts` `tabOrder` / + `activeTabKey`; keys `home:` and session ids), rendered by `App.tsx`, strip in + `layout/MainTabs.tsx`. No router. +- Global settings: `models/app_settings.rs` `AppSettings` → `/triple-c/settings.json`. + Per project: `models/project.rs` `Project` → `projects.json`. TS mirror in `lib/types.ts`, + wrappers in `lib/tauri-commands.ts`. Settings export/import in `models/settings_export.rs`. +- Keychain: `storage/secure.rs`; global single-value entries use `read_entry`/`delete_entry` + (shared Claude token, gateway keys). Project secrets are restricted to `PROJECT_SECRET_KEYS`. +- Container start: `commands/project_commands.rs` `start_project_container` runs + `docker::sync_bedrock_credentials` after start (≈:1448) — the pattern the marketplace sync follows. +- Exec/upload: `docker/exec.rs` `upload_bytes_to_container(container_id, dest_dir, file_name, data)`, + `exec_oneshot_streams_as(container_id, user, cmd, env)`, `create_attached_exec_as(…, tty, user)`. + Container user is addressed as `"claude"`. Constant-script + env-data rule: header of + `commands/inspect_commands.rs`. +- `container/entrypoint.sh` merges `CLAUDE_CODE_SETTINGS_JSON` into `~/.claude/settings.json` + (≈:408-447), then runs `claude update` under `flock /tmp/.triple-c-claude-update.lock` + (≈:649) and prints `Triple-C container ready.` (≈:654). Nothing marks readiness on disk today. +- `commands/inspect_commands.rs` `list_container_capabilities` already inventories agents, skills, + commands, hooks and plugins in a container (read-only); `CapabilityTiles.tsx` shows it. +- The container image has `gh`, `git`, `jq`. Claude Code 2.1.283 supports + `claude plugin marketplace add ` / `update` / `remove` and + `claude plugin install|uninstall @`. +- No host-side git or GitHub auth exists today (no `git2`/`gix`; `reqwest` with rustls is present). + `gix` 0.88 is current on crates.io. + +## 1. Marketplace repo format + +``` +/ +├── README.md +├── agents/.md # Claude Code agent file (front matter: name, description) +├── skills//SKILL.md (+ files) # Claude Code skill folder +├── commands/.md # Claude Code slash command +├── hooks//hook.json (+ scripts) # Triple-C hook manifest +└── plugins/ + ├── .claude-plugin/marketplace.json # standard Claude Code marketplace catalog + └── /… # standard Claude Code plugins +``` + +Every folder is optional; a repo with only `plugins/` is valid. + +- **Agent** — any `agents/*.md`. Name/description from YAML front matter (`name` falls back to the + file stem). Installs to `~/.claude/agents/`. +- **Skill** — any `skills//` containing `SKILL.md`; name/description from its front matter. + Installs to `~/.claude/skills//`. +- **Command** — any `commands/*.md`; description from front matter `description` if present, + otherwise the first non-empty line. Installs to `~/.claude/commands/`. +- **Hook** — `hooks//hook.json`: + ```json + { "name": "notify-on-stop", + "description": "Desktop ping when Claude finishes", + "hooks": { "Stop": [{ "hooks": [{ "type": "command", + "command": "${HOOK_DIR}/notify.sh" }] }] } } + ``` + `hooks` is Claude Code's `settings.json` hooks object, verbatim. `${HOOK_DIR}` is substituted + with the install folder `~/.claude/triple-c/hooks/` (absolute path). The whole folder is + copied; files keep their executable bit from the git tree mode. +- **Plugin** — entries of `plugins/.claude-plugin/marketplace.json`; each `source` must be a + relative path inside `plugins/` (remote sources are listed as invalid: they would fetch from + inside the container, bypassing pinning and host-side auth). + +Item identity: `(marketplace_id, kind, key)` where `key` is the file stem / folder name / plugin +name. Keys must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`. + +Invalid items (bad front matter, unparsable `hook.json`, unknown hook event, key failing the +pattern, plugin source missing or outside `plugins/`, symlinks anywhere in an item) are listed with +the reason and cannot be installed; they never stop the rest of the marketplace from loading. + +Limits (defensive, per item): 2 MiB total, 200 files. + +## 2. Data model and storage + +`AppSettings` (all `#[serde(default)]`): + +```rust +pub marketplace_accounts: Vec, +pub marketplaces: Vec, +pub global_marketplace_installs: Vec, + +pub struct MarketplaceAccount { + pub id: String, // uuid + pub label: String, // user-facing, e.g. "Work GitHub" + pub host: String, // e.g. "github.com", "repo.anhonesthost.net" + pub method: AccountMethod, // GhHost | GhContainer | Token + pub username: Option, // resolved at sign-in, display only +} +pub struct Marketplace { + pub id: String, // uuid + pub name: String, // display; slug used in container paths + pub url: String, // https URL only + pub branch: Option,// None = remote default branch + pub account_id: Option, // None = anonymous +} +pub struct MarketplaceInstall { + pub marketplace_id: String, + pub kind: ItemKind, // Agent | Skill | Command | Hook | Plugin + pub key: String, + pub commit: String, // full hex object id +} +``` + +`Project` (all `#[serde(default)]`): + +```rust +pub marketplace_installs: Vec, // project-only additions +pub marketplace_disabled: Vec, // (marketplace_id, kind, key) opted out +``` + +**Secrets.** Token and GhContainer accounts keep their token in the keychain as +`marketplace-account:` (new global helpers in `secure.rs` alongside the gateway ones). +GhHost accounts store nothing: every fetch runs `gh auth token --hostname ` so a later +`gh auth refresh`/logout on the host is honoured. Deleting an account deletes its entry. + +**Effective set for a project** (pure function, unit-tested): +`(global − project.marketplace_disabled) ∪ project.marketplace_installs`, keyed by +`(marketplace_id, kind, key)`; on a key clash the project's entry (and pin) wins. + +**Cache.** `/triple-c/marketplaces/.git` — a bare `gix` clone. Content is read +from git objects at each install's pinned commit, never from a worktree, so different pins of the +same repo coexist. Pinned commits are protected from pruning by writing a ref per pin +(`refs/triple-c/pins/`), refreshed after every install/update/remove. + +Removing a marketplace deletes its cache and its account link; installs that still reference it +stay in the lists and are shown as **source removed**. With no cache there is nothing to copy, so +the next sync removes those items from containers; the UI says so before the marketplace is +removed and offers "Forget" to drop the stale entries. + +**Export/import.** Accounts (without secrets), marketplaces and install lists go into the existing +export; account tokens follow the existing encrypted-secrets policy of `settings_export.rs`. + +## 3. Fetching and signing in + +**Fetch** (`marketplace/git.rs`): `gix` over HTTPS only (reject other URL schemes at add time). +Credentials are supplied through gix's credential callback, never written to disk: username +`x-access-token` for github.com, otherwise the account's `username` (falling back to `oauth2`), +password = token. Shallow fetch is not used (pins need history for diff/ancestry). + +- **Add marketplace** = test fetch of the chosen branch; failures surface immediately. +- **Refresh**: when the Marketplace tab opens and the last fetch is > 15 min old, on the Refresh + button, and once at app start (background, errors logged not toasted). +- **Offline / fetch error**: the last cache stays usable; UI shows "last fetched