# Autonomous app publishing through MCP An authorized Pro agent can create a browser app, save its static build, fill its discovery and listing metadata, attach or capture media, run checks, and publish a complete release through MCP. The owner does not need to fill the creator's app, listing, or upload forms for each release. The owner still signs in and authorizes the client once. OAuth consent chooses the scopes and either one existing owned project or all owned projects, including future projects. Creating a new app requires the latter boundary and `projects:write`. Use `projects:read`, `projects:write`, `projects:publish`, `files:read`, and `files:write` for app authoring and publication. Replacing existing media additionally requires the owner's explicit `media:write` consent, which is unchecked by default. An ordinary app-writing credential can fill missing initial media without that replacement scope. `offline_access` adds rotating renewal credentials. An active plan with MCP access is required on every operation; losing access or revoking consent stops the connection. Manual browser publishing remains free. See [MCP.md](/docs/mcp.md) for transport, OAuth, scope boundaries and [CLIENT_SETUP.md](/mcp) for client configuration. Connect to the actual deployed `https://YOUR_APP_HOST/mcp` endpoint, with secrets held by the client's credential mechanism. ## Inline JSON and larger builds `hosting_info.limits.max_inline_json_bytes` reports the independent **16 MiB wire-JSON ceiling** for `/mcp` and Studio's JSON file-edit endpoint. The smaller current account/file/bundle allowance still applies. JSON escaping and base64 consume wire bytes, so the source-file payload must fit within that complete request envelope. A 413 response points to native resumable ZIP uploads; splitting an oversized inline request across HTTP transfer chunks does not raise its limit. For larger builds, use `bundle_upload_create`, bounded `bundle_upload_chunk` calls and `bundle_upload_finalize`, with the current draft version, exact total size and full-file SHA-256. These retain the full configured upload/storage allowance without exporting a connector bearer. Existing binary PATCH, media and multipart transfer limits are unchanged. The inline ceiling is a transport memory safeguard, not a reduction of the administrator's bundle-size policy. Large-bundle import/finalization still materializes the selected expanded files in memory. Chunk transfer bounds request memory; it does not yet provide a streaming expanded-bundle commit architecture. Operators should size import workloads to available memory while that separate architecture work remains outstanding. ## Author the app and its listing 1. Call `hosting_info` and read `hackemist://docs/hosting`, `hackemist://docs/creator-guide` and `hackemist://docs/save-sdk`. Use the current taxonomy identifiers, `responsive_authoring` contract and limits returned by the server. For new stateful apps, use SDK 1.4 `createProgressStore` with capture, validated restore, initial-state and recovery UI callbacks. Do not reset on missing cloud data when local progress exists; do not write fresh state after a read/restore/migration error. Preserve originals and use gated cache writes. Test authenticated saves inside the player, not the read-only standalone preview. Never claim gameplay/save verification from a page-load or static-check result. 2. Call `project_create` with title, description, category, subcategory, visibility and tags. Read `hosting_info.creator_program_enabled`, `plus_publishing_enabled`, `plus_content_enabled` and the current tool schema. The program is off by default. An administrator's selected-account Plus publishing grant permits `premium` visibility while off; the creator's Plus apps then require viewer plan benefits including `premium_apps`. Other creators can create public/private apps and preserve unchanged paused settings on ordinary updates to already published apps; their stored premium apps are effectively public/free. Omit hidden program fields when editing. Monetization cannot be enabled while the global program is off, even with the Plus grant. An agent cannot grant its owner access, and authorization is rechecked after a grant is revoked. To revise app details, read the current listing `revision` and pass it as `base_revision` to `project_update`. When globally enabled, the existing Plus eligibility and approved-creator monetization rules apply. Building, publishing and Pro/MCP subscriptions remain independent of these switches. 3. Build the app in the coding client. Transfer static output with `files_replace`, `file_write`, or resumable binary bundle upload. Supply the current `base_version_id` to reject a concurrent draft edit. A relative root `index.html` and local assets are the supported artifact format. Hackemist does not run submitted Node/Python servers, package installs or build hooks. 4. Read `listing_get`, then call `listing_update` with its integer `base_revision` and the metadata below. Listing edits and media remain drafts until explicit publication. ```json { "project_id": "YOUR_PROJECT_ID", "base_revision": 0, "metadata": { "input_controls": ["touch", "keyboard", "mouse"], "device_targets": ["phone", "tablet", "desktop"], "languages": ["en"], "orientation": "any", "accessibility": ["keyboard-navigation", "reduced-motion", "no-audio-required"], "engine": "Canvas 2D", "framework": "Vanilla JavaScript", "licence": "MIT", "credits": "Describe the authors and any third-party assets.", "requirements": "A browser with JavaScript and Canvas 2D.", "release_notes": "Describe what this release adds or changes." } } ``` Only claim controls, languages and accessibility features the app actually supports. A filled descriptor is not proof of accessibility conformance. Use rich-listings.md (operator repository reference: `rich-listings.md`) for enums and field bounds. A private publication stays out of public discovery and does not make its copied app URL accessible to other accounts. When the creator requests sharing, use `project_access_list`, `project_access_grant` or `project_access_revoke` for registered recipients. Signed-in visitors can request access from a blocked shared link; the creator reviews requests in Studio or through `project_access_requests_list` and `project_access_request_decide`, with targeted in-app decision notices. A request alone grants nothing. Grants permit the current published private release, never drafts or source/editor access. See [private account access](/docs/mcp.md#private-account-access) for scopes and expiry fields. Author responsive layout before recording or releasing. A wide desktop should use its available width for the app's stage, panels and readable controls; a phone should reflow those elements for portrait and landscape. Use fluid grids/flex layouts, bounded typography and available viewport height instead of shrinking the entire desktop page into a narrow column. Include ``. Test the actual menu, gameplay, HUD, pause and any forms after resizing and changing orientation. Also inspect a 320-pixel phone manually when advertising phone support. Choose the source contract on `html`, `body` or the app root: `data-hackemist-layout="responsive"` for fluid CSS; add `data-hackemist-viewport="fit"` for a fullscreen game that must keep its complete stage/HUD/controls visible. An engine that handles `hackemist:resize` can declare `data-hackemist-layout="adaptive"`, which also requires viewport fit. Adaptive apps may provide `data-hackemist-resolutions='[{"width":1280,"height":720},{"width":720,"height":1280}]'`: at most eight supported logical viewports, integer dimensions 120–8192 and area at most 33,554,432 CSS pixels per profile. Implement both layouts before declaring them. The player selects a supported profile and uses uniform contain scaling, so aspect ratios remain intact. `data-hackemist-layout="fixed"` preserves an authored stage with possible empty bars. See the creator guide for dynamic `hackemist:layout` declarations. Set honest `device_targets` and `orientation` before checking or capturing. The release matrix uses desktop 1440 × 900, phone portrait 390 × 844 and landscape 844 × 390, tablet portrait 768 × 1024 and landscape 1024 × 768, and TV 1920 × 1080 for advertised classes. Portrait/landscape declarations filter phone and tablet checks; desktop and TV retain their wide viewports. An empty target list defaults browser diagnostics to desktop and phone, but cannot complete a release. Changing targets or orientation requires fresh browser evidence for the app. Retained media keeps its original capture-policy fingerprint; the optional current-media policy can require a matching new capture. Complete MCP releases advertising phone/tablet require `responsive_contract` in the authored source and an active responsive/adaptive or viewport-fit declaration observed in the browser at every mobile target. The private check measures declared responsive horizontal overflow, both axes for viewport-fit games, and required stage/control bounds even when root scrolling is hidden. `viewport-overflow` findings include dimensions and a source repair action. Normal responsive documents and forms can scroll vertically. Undeclared/fixed source overflow produces a warning because this source checker does not simulate the player's display transform. Existing live releases are retained; manual and explicit basic releases keep their operator gate. A declaration or capture is evidence of only the checked state: inspect real gameplay and fix source rather than claiming that a setting made it responsive. Allow up to 300 seconds for MCP capture calls, including browser startup, recording, encoding and media validation. Browser checking reserves 12 seconds for startup plus eight seconds per requested target, bounded from 25 to 60 seconds; the private HTTP response has a further seven-second margin. Capture uses a separate server-derived budget of at most 240 seconds with a ten-second private HTTP margin for bounded cleanup and response delivery. Release/publish calls should allow at least 90 seconds. A `checker_deadline` resource signal means the job is unavailable and needs retry/startup inspection; it proves neither a layout defect nor a passing release. For Three.js, bundle the selected engine version, loaders, models, textures and any Draco/KTX2 decoder JS/WASM with relative asset paths. Read `hosting_info` and `compatibility_get`: `graphics_settings` describes site-wide mouse capture, workers, WASM threads and WebXR switches, enabled by default, and the effective `policy` describes what the runtime can currently grant. These graphics options do not require a per-project approval request. The administrator controls them globally; workers, shared memory and immersive XR require available isolated hosting. `stored_policy` holds the app's separately configured save/cache/frame/network options. Legacy per-project graphics booleans do not override global switches. Implement real renderer/camera resizing, normal pointer/touch input, screen-mode fallbacks and JSON saves. Use a genuine user click for PointerLockControls or an immersive session. WebGPURenderer must initialize and handle unavailable GPU capabilities honestly; its WebGL fallback does not implement arbitrary WebGPU compute. Private browser checks/capture use the runtime's current permissions but do not emulate a headset or establish hardware performance. Record the normal screen mode and test immersive controls separately on suitable hardware. See the Three.js section of the creator guide. ## Attach existing media Call `media_upload_create` with `project_id`, `kind` (`thumbnail`, `screenshot` or `demo`), `byte_length`, SHA-256, current `base_revision`, `caption`, `alt_text`, `description` and optional idempotency key. Asset text is bounded to 240, 500 and 2000 characters respectively. Complete releases require all three text fields for every selected asset. Send the complete bytes with a binary HTTP `PUT` to the returned `upload_url`, using **the same bearer credential** in the Authorization header. The URL contains no credential. Do not place bearer tokens or media base64 in listing metadata. The server checks byte length, checksum, current owner/access, project boundary and revision before attaching sanitized media to the draft. Images use the existing bounded decoders; demo clips are bounded MP4/H.264 and are reencoded without audio. `media_upload_status` reads the ticket and `media_upload_abort` cancels an unfinished transfer. A repeated successful PUT returns the same attachment; an idempotency key reused with different inputs is rejected. Unlike bundle uploads, a media PUT sends one complete file rather than resumable PATCH chunks. Use `listing_asset_update` with `project_id`, `asset_id`, current `base_revision` and `metadata` containing `caption`, `alt_text` and/or `description` to describe existing draft media without uploading again. Text edits are stored with the listing snapshot; they preserve the live and historical text until publication. Studio exposes the same controls for thumbnails, screenshots and demos, including described staged thumbnail uploads. Use `listing_asset_remove` to detach media and `listing_gallery_reorder` to change screenshot order. Refresh `listing_get` after each edit because each operation advances the listing revision. Retain replaced and detached assets even if they were never published. Read `listing_media_history` to find the owner's retained media. On the creator's request, use `listing_asset_restore` with `project_id`, `asset_id` and the current `base_revision` to recover an asset into the draft, preserving its actual original provenance. To permanently erase an asset, first detach it from the draft and current live release, then call `listing_asset_delete` with those identifiers and `confirmed: true`. Removal from the draft alone does not erase bytes. Restore/delete and media text/order changes require explicit `media:write` consent plus project-writing permission. Retained history uses the existing storage quota and 40-asset project cap; report a limit error and let the creator choose what to delete. ## Capture the app itself `project_capture_media` runs the saved version in the private Chromium checker. With video enabled, it stages a thumbnail, eight screenshots by default and a silent MP4 recording of the running app. `seconds: 0` captures only a thumbnail. Fresh captures use contract v3, including actual viewport provenance for each still. The video is recorded browser output; it is not a static screenshot slideshow. Capture is an explicit media refresh. When creating an initial complete release, generate or upload missing required media as part of that publishing work. When updating an existing app, retain its thumbnail, screenshots and demo unless the creator asks to refresh them and authorizes `media:write`. Saving app details, source or release notes does not authorize replacing the gallery. Fill missing mandatory items for the requested release while preserving existing items. A client lacking media replacement consent must report the missing permission instead of retrying through another upload or removal tool. Use `media_kinds` to select only the requested kinds: a nonempty unique array drawn from `thumbnail`, `screenshot` and `demo`. An explicit video capture defaults to all three kinds. Zero seconds defaults to `['thumbnail']`; use this for a thumbnail-only refresh. Unselected existing media IDs, descriptions and provenance stay unchanged. If screenshots are selected, `replace_screenshots: true` replaces only the draft screenshot list; false appends within its eight-image limit. Live and historical media remain intact until publication. Supply `project_id`, optional `version_id`, current `base_listing_revision`, and a fresh `idempotency_key`. Capture uses the current saved draft. Optional even `width` (320–1920), even `height` (180–1080), and integer `seconds` (0–60) select the video viewport and length. Video defaults are 1280 × 720 and 30 seconds, encoded at 15 frames per second. The thumbnail is captured separately at a native 1280 × 800 viewport (16:10), matching the listing-card frame. The app reflows at that viewport; pixels are not stretched. Listing artwork fits within its frame without cropping or changing shape, including older and uploaded thumbnails. This is the encoded video frame rate; software rendering can repeat frames when fresh samples arrive more slowly. Use 30–60 seconds for a public demonstration; explicit shorter clips remain available for focused diagnostics, and `seconds: 0` preserves thumbnail-only capture. `actions` can enter a useful app state before capture. `demo_actions` can interact during recording at sorted `at_ms` times from zero through `seconds * 1000`, at most 60000 milliseconds. Actions support only the allowlisted click, key, key_down, key_up and bounded wait operations advertised in `tools/list`; they do not execute arbitrary JavaScript, navigate to external URLs or access account credentials. There are at most 60 actions across both lists. Pre-capture waits are at most 2000 milliseconds each and 4000 milliseconds total; timed demo actions support clicks and keys. Pair key_down/key_up actions to hold and release a supported gameplay key; the worker also releases held keys during cleanup. Use `screenshot_plan` to choose up to eight actual viewports and scenes. Each entry requires even `width` (320–1920), even `height` (180–1080), integer `at_ms` and a factual `scene` label of 1–120 plain characters without control characters. Scheduled times must be sorted, unique and between 1 and `seconds * 1000`, no later than 60000. One to eight entries are accepted for diagnostics; initial/new complete galleries still need five to eight distinct screenshots. The app runs in one session: for each scheduled capture the checker resizes it to that screenshot's viewport, captures a fresh native PNG, checks its layout and restores the video viewport. Coordinate input timing with the responsive app's real controls. Scene labels describe your intended capture and do not establish that a feature worked. If neither screenshot selector is provided, the server distributes eight captures over the advertised device families and timeline. An app advertising desktop, phone and tablet gets three desktop, three phone and two tablet screenshots. While device metadata is unset, the existing desktop/phone diagnostic fallback applies; complete publication still requires honest device metadata. Do not advertise untested devices just to populate the gallery. To keep the legacy single-viewport behavior, use `screenshot_at_ms` with five to eight sorted, unique times at the video's dimensions. Do not send both selectors. A mixed-device plan for an app tested in all three shapes can include: ```json { "screenshot_plan": [ {"width": 1440, "height": 900, "at_ms": 1000, "scene": "Desktop menu"}, {"width": 1440, "height": 900, "at_ms": 4000, "scene": "Desktop gameplay"}, {"width": 1440, "height": 900, "at_ms": 7000, "scene": "Desktop progress"}, {"width": 390, "height": 844, "at_ms": 10000, "scene": "Phone pause controls"}, {"width": 390, "height": 844, "at_ms": 14000, "scene": "Phone gameplay"}, {"width": 844, "height": 390, "at_ms": 18000, "scene": "Phone landscape progress"}, {"width": 768, "height": 1024, "at_ms": 23000, "scene": "Tablet gameplay"}, {"width": 1024, "height": 768, "at_ms": 29000, "scene": "Tablet result"} ] } ``` Use scenes that the app actually reaches and supply eight matching captions, alt texts and descriptions. The five-distinct-content rule still applies across device shapes; changing dimensions alone does not establish different features. Each still is freshly captured independently of repeated video frames. `replace_screenshots` defaults to true and replaces the draft gallery in the same atomic attachment while preserving live and historical galleries. Set it to false to append; the combined draft gallery must still fit the eight-screenshot limit. Plan the demonstration from the saved app's real controls and the states you have inspected. Choose a readable starting scene, a few representative interactions and a visible result: for a game, show a menu/start transition, useful movement or decisions, then a level finish, score change, puzzle solution or other outcome that actually occurs. Use `actions` to prepare a deterministic local state when needed; use `demo_actions` to perform the planned inputs over the 30–60-second timeline. Leave enough time for animation and for viewers to read the result. Capture runs the declared inputs; it does not discover exciting gameplay or select highlights automatically. Verify the resulting video before describing its scenes. Do not invent unseen wins, saves or interactions in captions. Use exact accessible button names or a permitted simple CSS selector, or send supported keys the app already handles. The private checker can test local assets and a temporary SDK stub; it has no authenticated player's saves, operator keys or data volume. External network services remain unavailable to capture. Use truthful existing media if the desired flow needs an external backend or unsupported input. The checker measures each screenshot viewport and the ending app state. Declared responsive horizontal overflow and viewport-fit game overflow/clipped required controls reject the whole capture with a `viewport-overflow` source repair hint; no media attaches and existing media stays intact. Scrolling responsive documents and legacy fixed displays retain their normal behavior. These checks cover the captured states and dimensions; they do not replace the advertised-device release matrix or testing every gameplay state. Describe the captured scenes with `thumbnail_caption`, `thumbnail_alt_text`, `thumbnail_description`, `demo_caption`, `demo_alt_text`, `demo_description`, and one entry per chosen screenshot time in `screenshot_captions`, `screenshot_alt_texts`, and `screenshot_descriptions`, up to eight entries each. Captions, alt text and descriptions allow at most 240, 500 and 2000 characters respectively. Omitted capture text receives factual generic descriptions of the app and capture position; supply more useful scene-specific text from your app knowledge. Explicit empty text remains empty and prevents a complete release. All descriptive text is part of the idempotent inputs and saved immutable assets. Receipts echo the normalized `screenshot_plan`. Stored v3 stills record `capture_version`, `capture_width`, `capture_height`, `capture_at_ms`, `capture_elapsed_ms` and `capture_scene`, alongside their original saved source version and browser-policy fingerprint. `capture_at_ms` is the scheduled time; `capture_elapsed_ms` records actual elapsed time, which may be later because resizing and browser work take time. Retained older media and idempotent retries keep their actual original provenance. Receipts and asset metadata also include `frame_count`, `distinct_frames` and `motion_detected`. These compare captured browser frame bytes and establish whether output changed; they do not establish gameplay, successful saves or external-service correctness. A static output honestly reports no visual change. Capture cannot complete unsupported external-service flows because its network access is limited to bundled assets. Fix or supply truthful existing media for an unsupported capture instead of claiming a complete gameplay demonstration. Reusing media never rewrites its provenance to imply a new capture. Use `capture_status` with the returned `capture_id` to inspect the job. A repeated call with the same idempotency key and identical inputs reuses its result. The media set attaches in one listing revision only after successful capture and validation. A stale draft/revision, failed action or capture failure does not publish the app. Read `listing_get` after completion for the actual media IDs and new revision. For the acceptance fixture's accessible controls, a single-viewport capture using the legacy screenshot selector looks like this: ```json { "project_id": "YOUR_PROJECT_ID", "version_id": "YOUR_SAVED_DRAFT_VERSION", "base_listing_revision": 1, "idempotency_key": "release-gameplay-001", "thumbnail_caption": "Colour Steps start screen", "thumbnail_alt_text": "Colour Steps board with a Start game button", "thumbnail_description": "The ready screen before the recorded round begins.", "screenshot_captions": ["Ready menu", "Round started", "Pause controls", "Five-step board progress", "Completed round"], "screenshot_alt_texts": ["Ready board and Start game button", "Blue board and gameplay controls", "Paused board and Resume game button", "Red board after five steps", "Green board with four gold tiles and Round complete text"], "screenshot_descriptions": ["The menu explains the eight-step goal.", "Starting reveals the board controls and progress counter.", "Pausing after two moves retains the board state.", "Resuming advances the round through five moves.", "The eighth move reveals the win state."], "demo_caption": "A complete Colour Steps round", "demo_alt_text": "Thirty-second recording of Colour Steps from menu to win", "demo_description": "The ready screen leads into a round with pause/resume controls, eight turns and the green win board.", "width": 960, "height": 720, "seconds": 30, "screenshot_at_ms": [1000, 4000, 10000, 18000, 29000], "replace_screenshots": true, "actions": [], "demo_actions": [ {"type": "click", "name": "Start game", "at_ms": 2000}, {"type": "click", "name": "Advance action", "at_ms": 5000}, {"type": "click", "name": "Advance action", "at_ms": 8000}, {"type": "click", "name": "Pause game", "at_ms": 9000}, {"type": "click", "name": "Resume game", "at_ms": 11000}, {"type": "click", "name": "Advance action", "at_ms": 12000}, {"type": "click", "name": "Advance action", "at_ms": 14000}, {"type": "click", "name": "Advance action", "at_ms": 17000}, {"type": "click", "name": "Advance action", "at_ms": 20000}, {"type": "click", "name": "Advance action", "at_ms": 23000}, {"type": "click", "name": "Advance action", "at_ms": 26000} ] } ``` The private publishing-checker service must be healthy and reachable from control. The supplied local and Dokploy Compose files include it; do not expose its port or disable Chromium's sandbox. The application image includes FFmpeg/ffprobe for media processing. ## Update an already published app Read `project_get` and `listing_get` before editing. Owner responses include saved app details in `project_details_draft`; top-level project fields and public visitors retain the published title, description, taxonomy, tags and listing snapshot. Public access follows the project's effective `plus_content_enabled`: stored premium visibility requires `premium_apps` viewer access when the global program is on or its owner has the administrator's Plus grant; otherwise it is effectively public/free while off. Use the shared listing revision as `project_update.base_revision` and `listing_update.base_revision`. A stale revision rejects the save. A real app-details, listing or media edit starts a new numbered draft by copying the published source if no unpublished draft exists. Later metadata edits reuse that draft; an unchanged save creates neither a version nor a listing revision. File writes continue to create immutable source versions. After each save, reread the current draft version and listing revision. Revise source with the current `base_version_id`, describe the release changes, and keep existing media by default. Check and publish that intended draft explicitly. Publication advances the active version and makes the saved app details and listing visible with the retained thumbnail, screenshot list and demo. Their IDs, text and original capture provenance remain unchanged unless explicitly edited. Refresh only the kinds the creator requests, using `media_kinds` on capture or their dedicated upload tools. Historical source and release snapshots remain retained. A switch to private is an immediate withdrawal of public access, while its new release details still follow the draft workflow. Switching back to an allowed public visibility takes effect through publication. Follow the fresh schema before sending premium settings; never enable monetization while the program is off. Studio uses the same workflow: save **App details**, source or **Listing & gallery**, preview the new draft, then choose **Publish changes**. A published details-only edit also receives a new version number. Reload a conflicting tab before retrying rather than overwriting another editor's work. ## Check and release Call `project_release_checklist` to inspect the saved build and listing and see which requirements remain. An initial complete release requires title, description, category, at least one tag, a subcategory when the category has enabled children, controls, device targets, languages, licence, credits, requirements, release notes, a thumbnail, at least five screenshots with five distinct trusted stored content checksums, a validated 30–60-second demo and an authored build beyond the initial starter version. The screenshots should illustrate different actual app functions or scenes. Different IDs, descriptions or filenames cannot satisfy the distinct-content check; different bytes alone cannot prove different features, so inspect the images yourself. Missing trusted storage checksums require suitable new media or upload. Every selected media asset requires caption, alt text and description. The default `media_policy: "preserve"` accepts retained same-project media with its truthful original source/policy provenance. Optional `media_policy: "require_current"` reports captured media whose source version or policy differs from the intended release; it never generates or replaces media automatically. The current app's static and fresh browser checks must still pass under either policy. Blocked/quarantined projects cannot release, and production owners must have a verified email. Fix every returned `missing` field, including `screenshots`, `distinct_screenshots` or `demo_duration`, before attempting a complete release. Explicit basic and manual publishing keep their existing media requirements. Call `project_release` with `project_id`, the intended saved `version_id`, and the current `listing_revision`. It runs static and private desktop/mobile browser checks, then publishes the selected version and listing together only if the release is complete and the checks pass. `project_release_checklist`, `project_release` and complete `project_publish` accept the same optional `media_policy` (`preserve` by default, or `require_current` when the creator wants current-source media). A revision or build change during the operation prevents publication of a stale release. Fix a failure and retry with the current identifiers. Read `ready_scope`: the backward-compatible `ready` flag is preflight eligibility, not proof of browser checks or publication. Contract v2 separately reports actual `media_inventory`, `legacy_media_exemptions`, current complete requirements, browser evidence/pending checks, current account requirements and `publication.completed`. Missing media is never retained, even for a legacy media-free publication. Real unchanged short demos/small galleries may keep explicit legacy update allowances under preserve, but those allowances do not satisfy `complete_release_ready`. Preserve existing media unless its change is authorized. Browser checks load bundled startup/layout at advertised viewports; gameplay, authenticated save/restore and external services require separate verification. See the readiness contract and recovery plan (operator repository reference: `RELEASE_READINESS_HARDENING.md`). Under `preserve`, unchanged media kinds from an earlier published listing keep their existing eligibility by the same asset IDs. An older manual release can therefore retain a two-image gallery, a short demo or an absent legacy thumbnail/demo without inventing replacements during an app update. Initial complete releases and newly added/replaced media use the current five-distinct-image and 30–60-second requirements. Caption/alt-text/description, app metadata, source, authorization and fresh browser checks still apply. `retained_media_kinds` reports the kinds being kept; its screenshot-gallery label is `screenshots`, while capture input uses singular `screenshot`. `project_publish` defaults to the same complete-release workflow as `project_release`, including its descriptive media requirements and fresh browser checks. It cannot silently publish an empty gallery; pass the current `listing_revision`. An intentionally limited release can explicitly select `release_mode: "basic"` on `project_publish`; this retains the operator-configured publication gate, including any required checks, and does not claim complete-release readiness. Use that opt-in only when a limited release is intended. Manual browser publishing retains its existing optional listing/media behavior. Existing published apps are not modified by these MCP requirements. The agent can request browser capabilities through `compatibility_request`, but approval is a separate administrator action. Subscription purchases retain their authorization rules. While `creator_program_enabled` is false, the administrator's account grant can permit Plus publishing, but creator-program applications, monetization and financial actions remain unavailable; ordinary updates can retain unchanged paused choices. Enabling the program restores the existing eligibility rules and does not bypass creator approval, provider onboarding or payout review. ## Acceptance and limits `tests/browser_autonomous_mcp.spec.mjs` exercises a real verified non-admin owner, OAuth/PKCE, temporary nonfinancial Pro access, MCP-only authoring and metadata, private captured media, release checks, public images, five distinct timed screenshots, a decoded 30-second MP4 with menu, pause, gameplay and outcome frames, and playable guest runtime controls. It also revises published app details/source/notes and completes an update while retaining all original media IDs, descriptions and capture provenance. A writer without media consent fills missing initial media and updates source/details, while replacement fails without changing the draft or revision. The refresh client explicitly selects media:write. It then refreshes only the thumbnail, retaining screenshots/demo, and separately refreshes all kinds for a strict current-media release. Finally, it recovers/deletes an older published screenshot through MCP and a never-published thumbnail through actual Studio history controls, preserving the live gallery. It also checks free access denial, restricted scopes, stale revisions, idempotent capture and access loss. `tests/browser_published_updates.spec.mjs` covers staged MCP app-details/source/listing edits and manual Studio details edits on an already published app, version increments, unchanged live releases and stale-save conflicts. Cleanup removes temporary entitlements, revokes credentials, deletes the accounts, unpublishes their apps and erases listing media. Retained source versions follow the platform's account-deletion retention policy. The authenticated local Docker acceptance passed on 2026-10-12 with Chrome and the private sandboxed publishing-checker. This historical record covers the earlier two-screenshot workflow as implemented on that date; production and third-party client checks remain separate. The five-screenshot complete gate, minimum 30-second demo and revised published-update fixtures require their own dated verification. Final local verification on that date: - Python regression: 748 tests, 746 passed and two environment skips for Node/opt-in browser tests in the Python-only runner. Official MCP SDK initialization and resource/tool calls passed. - Frontend contract/DOM/theme aggregate passed; the combined real-Chrome theme, responsive controls and viewer/navigation run passed 21 tests. - Private Docker bridge/capture: 13 passed, including decoded changing-frame H264 video, with Chromium's sandbox and network isolation intact. The test mount contains only trusted harness scripts and the SDK. - Authenticated Docker acceptance: the verified non-admin Pro owner's complete MCP-only release passed, followed by public gallery media, decoded video playback and playable guest controls. Cleanup completed. - Capture concurrency, changed scopes/project grants, revoked access, policy/draft changes and read-only preview save isolation passed. Encrypted thumbnail/gallery backup and offline restore passed. All five local services were healthy after deployment. These results cover local behavior; the configured GitHub workflow has not been run remotely by this task. Run it against a disposable local or staging deployment with the browser checker and SMTP configured: ```powershell $env:HACKEMIST_BASE_URL = 'http://localhost:8000' $env:HACKEMIST_ALLOW_MUTATIONS = '1' $env:HACKEMIST_MAILPIT_URL = 'http://localhost:8025' # Supply the administrator test email/password privately through the environment. node --test tests/browser_autonomous_mcp.spec.mjs ``` The local generated-credential runner can supply those secrets without printing them: ```powershell node .local/run-launch-tests.mjs tests/browser_autonomous_mcp.spec.mjs ``` Do not run mutation acceptance against production accounts or financial data. Local acceptance does not establish live third-party client consent behavior, public HTTPS routing, external API compatibility or authenticated save correctness under production domains. Test those parts on the actual deployment. ## Local verification — 2026-10-14 This verification covers commit `aa90dd7` before the subsequent media-preservation, explicit replacement-consent and recoverable-history changes. Their acceptance is recorded separately. The application and private publishing-checker were rebuilt together, and all six local Compose services were healthy before and after acceptance. The current backend suite completed 1,081 tests: 1,078 passed and three skipped, in 128.443 seconds. This includes the five-image/distinct-storage-checksum gate, validated minimum demo duration, capture reservation/retry guards, draft metadata and source updates, storage and private withdrawal coverage. Native sandboxed capture passed at 1280 × 720 with silent H.264 output and five freshly captured, post-paint PNG scene stills: | Demo | Encoded frames / rate | Distinct sampled JPEG hashes | Wall time | Approximate MP4 size | | --- | --- | --- | --- | --- | | 30 seconds | 450 / 15 fps | 143 | 34.269 seconds | 0.937 MB | | 60 seconds | 900 / 15 fps | 278 | 64.160 seconds | 1.857 MB | All 60 per-second decoded samples of the longer clip were distinct. The encoded rate can repeat frames on software rendering; these results do not claim 15 fresh samples per second. Eighteen hardened checker cases passed, including renderer-stall deadlines, cleanup followed by a successful job, startup, abort and resource-limit failures. The private generated-credential runner then passed all three real-Chrome suites serially, with zero skips, in 150.870 seconds: - `browser_autonomous_mcp.spec.mjs`: two actual 30-second captures, five distinct menu/start/pause/progress/win screenshots with intact descriptions, complete initial publication, then a published app-details/source/release-notes edit, replacement capture and second complete publication. The old public details, gallery and guest runtime remained live until the second release; the new release exposed exactly the five new screenshot IDs and a decoded 30-second demo. - `browser_published_updates.spec.mjs`: actual MCP and Studio details/listing/source forms, numbered drafts and publication, unchanged public builds before release, immutable historical source, no-op saves, stale revision/source conflicts and preservation of typed content. - `browser_mcp_plans.spec.mjs`: existing plan, manual import/publishing, OAuth/project-scope and browser-permission review acceptance. The generated public-gallery and Studio screenshots were visually inspected. The five gallery scenes show different actual states and controls, and Studio shows the completed manual release at version 6. Both new fixtures passed disposable-account deletion and public-project removal assertions; no page errors were reported. Local screenshot artifacts remain under `tests/browser-artifacts/` and are excluded from Git. These results cover the local deployment and authored fixtures; production domains, physical-device performance and live external-service/client integrations retain their separate checks. ## Media preservation, consent and history verification — 2026-10-14 The follow-up backend run completed 1,114 tests: 1,111 passed and three optional tests skipped, in 125.478 seconds. The frontend aggregate passed. A later history-query optimization and old-backup migration case passed a separate ten-test history run; this is not an additional full-suite result. The application was rebuilt with these final changes, and all six local Compose services were healthy before and after real-browser acceptance. The native recorder was unchanged; its preceding 30/60-second sandbox proof is recorded above. The private credential runner passed all three serial Chrome suites with zero skips or failures in 181.334 seconds: - Autonomous MCP acceptance, 136.552 seconds: a writer without media replacement permission created missing initial media and edited app details/source; a complete update retained every original thumbnail/screenshot/demo ID, description and capture provenance. Strict current-source media was an explicit opt-in policy. Unconsented replacement failed without a draft fork or listing change. A separately consented OAuth client then refreshed only the thumbnail, kept screenshots/demo, and explicitly refreshed all kinds before strict publication. Both positive captures produced decoded 30-second demos and five distinct timed menu/start/pause/progress/win screenshots. - Media history in that same acceptance: every replaced original asset and the never-published thumbnail remained retained. An older screenshot was restored and explicitly erased through MCP, with selected-asset and missing-confirmation deletion denied. The never-published thumbnail was recovered through Studio's **Restore to draft** button and erased through its **Permanently delete** confirmation after detachment. Original provenance was preserved on recovery; erased owner URLs returned 404 and the historical deletion record remained. The current public gallery stayed unchanged throughout recovery/deletion. - Published updates, 32.393 seconds: an older media-free app completed an MCP source/details/listing update without inventing media, then actual Studio forms published another numbered release. No-op saves, stale CAS, typed-content preservation, immutable old source and the previous public runtime remained covered. MCP plans acceptance also passed in 10.982 seconds. Visual review confirmed the five different public scenes and descriptions, the updated listing/release history, Studio's retained-media count and recovery/deletion controls, and the final manual **Published · v6** state. Full-page captures taken before scrolling can contain unloaded lazy media; decoded playback, owner byte retrieval and the five-image gallery were independently checked. Disposable account/entitlement/token cleanup and guest project removal passed, with no page errors. The local log is `.local/media-history-browser.log`; generated screenshot artifacts are excluded from Git. This remains local acceptance, with deployment and external-client checks separate.