Skip to main content

Agentic Crowdy Studio: the in-browser agent pane

CrowdyJS 16 docks an AI agent beside the Crowdy Studio editor. The agent is the DeepSeek Harness running entirely in the player's browser: the stock harness web UI in a same-origin iframe, its plugin tree in a Web Worker, and a filesystem whose files are the open Studio project (the bound GitHub repository when the project has one). It reaches a model through the Game API's metered model endpoint with the player's own app token, so usage is priced per request at the app's rate card and billed to the player's wallet by default (or the app's org wallet when its billing admin chose that).

The human remains the authority. The model edits files and runs draft tests through the page; a live deploy waits for the player to confirm in the pane; the model has no shell, no network of its own, and only observes the game.

Allowlisted, fail-closed

The pane appears only when the app's Management policy enables the agent and the player holds use_studio_agent. Model usage is metered; the pane shows today's spend against the policy ceiling and who pays.

CrowdyJS 15 and earlier

The Ask / Build / Play dock, client.crowdyStudioAgent, the agent mount option, PlayerControlGate, AgentControlBanner and the lease manager were removed in 16.0.0 together with the server orchestrator they drove. See the migration notes.

What the agent can do​

ToolWhat happensWhere
read_file, write_file, edit_file, read_image, searchOn the project mount /dsh/workspace/{server,client}; writes commit to the bound branch with expectedCommitSha when the project is bound to GitHub, otherwise save through StudioWorker
draft_testCompiles and runs the exact saved revision as a draft; returns diagnostics, the build log and a screenshot after the client module loadsPage, via the Studio controller
deploy_liveAsks the player on the page (Deploy live / Not now); only an approval runs the deployPage
screenshotCaptures the game canvas the host provides, downscaled and PNG-encoded, into captures/Page
game_observeThe host's PlayerHostAdapterV1.observe(); observation only, there is no command dispatchPage
runtime_status, client_logs, project_list/open/createRead-only Studio state, or a project switch the page performsPage

Everything the model asks for runs on the page with the player's browser authority, through the same controller methods the human buttons use.

GitHub is optional and changes nothing the agent can see. When the project is bound to a repository (CrowdyJS 17), the agent's writes are commits on the bound branch, each carrying the project's current commit SHA as expectedCommitSha, exactly as the editor's saves are; a stale commit is refused and the worker re-reads before writing again. Monaco and the agent read one tree — the server's mirror of the repository — so a file the agent commits is what the editor shows, and a push made from an IDE reaches both after one Refresh. An unbound project is saved through Studio as before.

The game editor: a Construct game's repository​

When the open project is bound to a game generated from the Construct template (the API reports its layout kind as CONSTRUCT_GAME), Studio edits the game itself, and the agent pane boots in game mode. It needs CrowdyJS 18.23, @crowdedkingdoms/crowdy-dsh 0.5 and a page that gives the controller a draft store (the Construct framework 0.4 does); otherwise the project opens in the mod editor as before.

Studio's Live code is the way in. The button, on the app's page in Studio once a repository is bound, opens the app's launch URL with the app and the project in the address, and the game opens the game editor on that project. The launch URL is wherever you serve the game: your own deployment, or, while you develop, the template's dev server (LIVE_DRAFT=1 npm run dev, at http://localhost:5175). Set it on the app in Studio, under Settings > Sign-in & redirect URIs; see Host your game.

  • A draft, not commits. The agent and the Files pane edit a draft of the repository that only this player sees. On your deployed game it lives in the player's browser, an overlay over the bound branch; on the template's local dev server (LIVE_DRAFT=1) it is the checkout. Writes are guarded: a file the player changed since the agent read it fails as stale instead of being overwritten.
  • Previewed in place. The game reloads its content layer (scenes, the content registry, the catalog) from the draft without a page reload. The agent calls preview_status to learn whether the world took the change and which file and line failed. Code outside that layer still needs a build.
  • One commit, when asked. commit_game_files asks for approval in the pane, then the page shows the files and the message and the player confirms. The draft lands as one commit through crowdyStudioGitHubCommitFiles. If the branch moved meanwhile, every edit that does not collide is rebased and the rest are listed for the player to settle (keep mine, or take GitHub's).
  • The game's own guidance. The agent reads the game's STUDIO.md as its instructions and lists the skills the game's playbook marks for the dock. It has the file tools, search, screenshot, game_observe, client_logs, read_playbook, preview_status and commit_game_files; the mod tools (draft_test, deploy_live, grid programs) are not offered.

Hosting needs nothing new for a game on the Construct framework: its build adds a service worker for the preview (construct-draft-sw.js) that answers only its own __draft/ URLs, and the page's CSP stays as it is. Serve that file as JavaScript and do not cache it for long; see Host your game.

Player workflow​

  1. Open Crowdy Studio on the project and grid you intend to use; the Agent pane docks beside the editor.
  2. The first time, read the provider-data notice (your project source, your messages and any screenshots you share go to a model provider through Crowded Kingdoms under zero-data-retention routing) and click I understand, start the agent. This records crowdyStudioSetProviderConsent for the app.
  3. Talk to the agent in the harness UI. Use Screenshot in the pane header to share what you see; click Fix with AI on any compiler diagnostic in the Problems panel to queue it as a prompt.
  4. The agent edits files and runs draft_test; the editor reloads changed files as they land.
  5. When the agent asks to deploy live, the pane shows Deploy live / Not now. Nothing ships until you click the first; the prompt declines itself after a minute.
  6. The spend line reads Today: $x of $y · n requests · paid by your wallet (or the app's wallet). Add funds links to Studio's wallet.

SDK quickstart​

import { createCrowdyStudioEmbed } from '@crowdedkingdoms/crowdyjs/crowdy-studio';

const studio = createCrowdyStudioEmbed({
client: game, // CrowdyClient (crowdyStudio, crowdyStudioGitHub, playerWallet)
appId,
dsh: {
graphql: game.graphql,
webBase: `${import.meta.env.BASE_URL}dsh/`, // where dist/dsh-web is served
graphqlUrl: game.graphqlEndpoint,
apiOrigin: new URL(game.graphqlEndpoint).origin,
getToken: () => game.getToken(),
persistScope: `${appId}/${playerId}`, // per player: OPFS session restore key
studioOrigin: 'https://studio.crowdedkingdoms.com',
},
});

// Per open: what the model may see.
studio.toggle({
gridId,
targetPermissions,
playerHost, // PlayerHostAdapterV1 (observe only) -> game_observe
dshHost: {
captureFrame: () => renderer.domElement, // screenshot tool + auto-capture after draft tests
describeView: () => hud.summary(),
clientLogs: () => modLogs.tail(200),
},
});

captureFrame may return a canvas, ImageBitmap or Blob. For a WebGL canvas, either construct the renderer with preserveDrawingBuffer: true or render one frame synchronously before returning it; otherwise the capture is blank.

Shipping the harness​

The harness is the published @crowdedkingdoms/crowdy-dsh npm package. Its dist/dsh-web/ (web client, worker, packed plugin image, BUILD.json, MIT notices, ~13 MB) must be served from the game's own origin, under the path you pass as webBase. The usual arrangement is a devDependency plus a prebuild/predev script that copies dist/dsh-web into a gitignored public/dsh/:

{
"devDependencies": { "@crowdedkingdoms/crowdy-dsh": "0.4.5-dev.1" },
"scripts": { "predev": "node scripts/copy-dsh-web.mjs", "prebuild": "node scripts/copy-dsh-web.mjs" }
}

Pin it exactly like the SDK and keep its CrowdyJS major equal to the SDK's; the harness build refuses to bundle a different major. The version above is an example: take the one your tier's dist-tag names (npm view @crowdedkingdoms/crowdy-dsh dist-tags), the same way you pick the SDK's. BUILD.json says which harness tag and CrowdyJS the artifact carries.

Headers​

The host document must allow the iframe (frame-src 'self') and serve the harness path with script-src 'self' 'unsafe-eval' 'unsafe-inline' blob:, connect-src 'self' blob: (plus the API origin), worker-src 'self' blob: and frame-ancestors 'self'. The game page's own policy stays strict. One serving detail: preview/vfs-image.tar.gz must reach the browser with no Content-Encoding header; the worker inflates it itself.

The harness path needs its own policy at whatever serves it. A CDN or edge function that stamps one CSP on every path of the game will stamp the game's strict policy (frame-ancestors 'none', no 'unsafe-eval') on /dsh/* too, and the pane then fails at boot with a blank frame and CSP violations in the console while every game route verifies green. Give /dsh/ (or wherever webBase points) a per-path rule, and make your deploy's smoke test fetch <webBase>index.html and assert the harness policy on it; the game page's policy must stay strict.

The iframe is same-origin by design (BroadcastChannel and OPFS are origin-scoped), so its sandbox="allow-scripts allow-same-origin" does not isolate it from the page; the CSP on the harness path is the control. Serving the harness from its own origin is a tracked follow-up.

Host your game lists every header a game's host sends, with both policies written out for production.

Where the token goes​

The harness worker needs the player's app token to call the model endpoint and the Studio GraphQL as the player. It is sent over the page/worker channel (page.hello, refreshed by page.token), held in the worker's memory, and never written to a seed file, the worker's virtual filesystem or OPFS. The model has no tool that reads the environment, and the Crowdy filesystem backend serves only the project mount, so read_file cannot reach it either. Every frame on the channel carries a per-boot nonce; frames without it are dropped.

Custom chrome​

@crowdedkingdoms/crowdyjs/crowdy-dsh exports the pieces: CrowdyStudioDshPane (the default pane), StudioDshBridge (the page half of the bridge, with a confirmLiveDeploy hook), CrowdyStudioDshTransport (models, consent, setConsent, usage) and the bridge protocol types. @crowdedkingdoms/crowdyjs/player-host keeps PlayerHostAdapterV1, its schemas and the error / preemption vocabulary.

Budgets, disabled states, errors​

Spend is metered per request at the app's rate card against a per-request ceiling and a player-day budget; the wallet (player or org) must cover each request's worst-case reservation. Who pays is decided in two layers: the platform sets the billing mode (platform-funded absorbs the cost; metered charges), and the app's billing admin chooses the payer in Studio — the player by default, or the org wallet. The two payers settle differently: player usage accumulates in micro-USD and is billed by the hourly player tick, so sub-cent requests add up before rounding; org usage is debited from the org wallet per request, rounded up to the cent, so each request is its own auditable ledger line. The spend line in the pane names which wallet is paying. When the feature, app, model, permission, policy replica or provider is unavailable the agent fails closed and the pane shows the platform error code (AGENT_DISABLED, AGENT_PERMISSION_DENIED, AGENT_SCOPE_DENIED, AGENT_MODEL_NOT_ALLOWED, AGENT_BUDGET_EXHAUSTED, AGENT_FUNDS_NEEDED, AGENT_PROVIDER_UNAVAILABLE, ...). Manual Crowdy Studio and human gameplay remain available.

The GraphQL companions are crowdyStudioProviderConsent, crowdyStudioSetProviderConsent and crowdyStudioModelUsage; the REST endpoint is described with the Game API.