cmux-next backend: app store (AppDO, installs, grants, agent approvals, search)
App store backend for cmux-next (spec manaflow-ai/cmux-next-spec app-platform.md sections 9 to 12, contract v1 "Store ops"). Draft: stacked on https://github.com/manaflow-ai/cmux/pull/16762 (branch feat-cmux-next-automations), so this diff shows its commits until it lands. The app store commits start after c6411da1d03.
Owners. AppDO (one per app id, wrangler tag v4, because v3 is taken by ConnectionDO/AccountIndexDO in https://github.com/manaflow-ai/cmux/pull/16762) owns the listing, versions, tier and yanks. app.version.submit checks manifest essentials (id, semver equal to the tag, engines.cmux range, scope names, 64 KiB), the publisher segment equals the repository's GitHub owner, refuses version reuse even after a yank, and lets only the publishing team submit later versions. Ids are claimed only in development-like environments or by staff (APP_STORE_STAFF) until GitHub owner verification exists. app.version.yank, app.listing.set_tier (staff). UserDO and TeamDO hold installs, app grants and agent approvals through one pure reducer; TeamDO adds the team app policy (app.policy.set: allowed tiers, allowlist, blocklist) and requires a team owner or admin.
Pure reducers with owner-side lookups. OwnerEngine.submit takes SubmitOptions.resolved: the owner looks up the release in AppDO before deciding (OwnerDO.resolve hook, also on the WebSocket path), the reducer sees it as params.resolved, the event records it (mirror replay decides the same), and the idempotency hash excludes it (a retry whose lookup changed replays the first decision). A client-sent resolved is always replaced. The release is pinned by AppDO revision (app_revision). ReduceContext gains origin.
Installs only from the user's client (decision 2026-10-02). app.install, a scope-growing app.update and app.approval.decide answer app.install.user_only (not retryable) unless origin is user and the principal is not an agent: users install from the App Store in the app or the web store. These three ops are not MCP tools; CLI apps install answers the error. The agent approval path (D48: approval_required, session-only decide that re-resolves the release, 10-minute expiry, merged duplicates, 20 pending max, stale check) stays in code, unreachable, and reopens when owners stamp the actor. Tiers are exactly first-party (cmux), verified and unverified (third-party). New apps stay unverified and hidden from search until staff set a tier (decided).
Hide and defaults (decision 2026-10-02). app.hide / app.unhide (UserDO, any origin, MCP opt-in) set hidden on a personal install; a hidden app keeps running, clients drop its entries; app.list and the app_installs.hidden column carry it. First-party apps in APP_STORE_DEFAULT_APPS count as installed for every user with nothing recorded (by_default in app.list, granted their required scopes) until the user hides or removes one; a removal leaves a tombstone. Samples stay opt-in.
Search. app.search (owner cloud:planetscale) reads apps through a read-only Hyperdrive binding HYPERDRIVE_READ: generated tsvector + GIN, word-prefix to_tsquery('simple', ...) built from letters and digits only, no trigram index. Unverified apps are never listed. The Worker routes owners through one table (routing.ts). Migration 0005_app_store.sql (expand): app_publishers, apps, app_versions, app_installs, each row with one writer and source_stream/source_seq guards. No backend:apply-migrations label yet.
Dashboard. /apps, /apps/$publisher/$name (scopes with reasons, tier badge, versions, Install), /apps/installed (approvals, Remove). CLI noun apps <verb>; MCP: search/info/list default, remove opt-in, install/update/decide/publish/grant/yank/policy never.
Ordering and limits (review fixes). OwnerDO runs ops one at a time per object including the lookup, so an op behind an install on the same socket cannot commit first (test red without the change). The lookup runs only when the engine will decide (needsDecision), so retries replay from the ledger. Team-scope ops check mutate-shared (teamRisk). Identical pending agent requests are merged, at most 20 wait per owner, expiry is recorded, and an approval whose install changed since is refused as stale. Teams admit unverified apps only by listing the tier. Versions refuse build metadata and are capped at 100 per app (state is one JSON row).
Verified. Typecheck of every package; 91 api tests in workerd (35 app reducer tests incl. hide/unhide idempotency and default-app tombstones incl. origin cli/mcp/user per op incl. a 2000-sequence scope invariant, 4 end-to-end: publish, info, install replay, idempotency conflict, MCP approval, yank, team policy, forged resolved, WebSocket op order); ownership tests for resolved; migration lint; all migrations from zero on scratch Postgres 17 plus the three drains (stale-seq guard, yank keeps the manifest) and the search query (prefix, categories, tiers, paging, hostile input); dashboard pages rendered with dev-only fixtures (CMUX_DASHBOARD_APPS_MOCK=1).
Not verified / gaps. No HYPERDRIVE_READ config or read-only role exists yet (search answers owner.unreachable until an operator creates them). Bundle fetch, sha256 and artifact attestation checks are not done at submit (the record stores what the publisher sends). Publisher verification in TeamDO and its app_publishers writer, version records in a side table (the 100-version cap), icons, app.version.published pushes and compatible_with are not implemented. origin is client-declared, so a process that claims origin user can still install until the actor stamp lands (identity spec section 6). No staging deploy.
Changelog
none
š¤ Generated with Claude Code