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.
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.
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
| Tool | What happens | Where |
|---|---|---|
read_file, write_file, edit_file, read_image, search | On 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 Studio | Worker |
draft_test | Compiles and runs the exact saved revision as a draft; returns diagnostics, the build log and a screenshot after the client module loads | Page, via the Studio controller |
deploy_live | Asks the player on the page (Deploy live / Not now); only an approval runs the deploy | Page |
screenshot | Captures the game canvas the host provides, downscaled and PNG-encoded, into captures/ | Page |
game_observe | The host's PlayerHostAdapterV1.observe(); observation only, there is no command dispatch | Page |
runtime_status, client_logs, project_list/open/create | Read-only Studio state, or a project switch the page performs | Page |
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_statusto 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_filesasks for approval in the pane, then the page shows the files and the message and the player confirms. The draft lands as one commit throughcrowdyStudioGitHubCommitFiles. 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.mdas 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_statusandcommit_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
- Open Crowdy Studio on the project and grid you intend to use; the Agent pane docks beside the editor.
- 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
crowdyStudioSetProviderConsentfor the app. - 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.
- The agent edits files and runs
draft_test; the editor reloads changed files as they land. - 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.
- The spend line reads
Today: $x of $y · n requests · paid by your wallet(orthe 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.