AppGenerator Output Assembly Contract¶
Status: Canonical contract Purpose: Define exactly how AppGenerator turns persistent UI intent into bundle artifacts.
Owned Artifacts¶
AppGenerator emits deterministic app-bundle artifacts for persistent app UI:
app.jsonadmin/admin_registry.yamlui/pages/*.yamlbrand/theme_config.jsonconfig/shell.jsonconfig/asset_manifest.json
The artifact split is strict:
app.jsondefines app identity, targets, auth intent, and startup behavior such asstartup.landing_spot.admin/admin_registry.yamldeclares all AdminPortal pages for the generated app's admin surface. Module panels reference these page ids via thepagefield inmodules/{module}/contracts/admin.yaml. Standard generated-app admin paths use/admin;/apps/:appId/...belongs to first-party Studio or explicit hosted-operator surfaces.ui/pages/*.yamldefine persistent page structure and route ownership.brand/theme_config.jsondefines visual tokens, shared primitives, and semanticui.chat/ui.shell/ui.pagestyling.config/shell.jsondefines compact app-wide shell behavior: header logo/actions, canonical shortcuts, navigation policy, non-page-owned navigation items, and chrome mode defaults.config/asset_manifest.jsondefines reusable media inventory metadata for non-token assets (icons/images/video), including source/provenance and usage hints.
AppGenerator does not own the full build lifecycle, agent workflows, agent UI tools, or workflow transition surfaces. AgentGenerator produces workflow/agent artifacts, and the Refinement Engine governs AppContextVersion selection, validation, review, and ArtifactVersion acceptance/promotion. Generated bundle files become source of truth only through artifact acceptance and promotion.
When AppGenerator assembles module artifacts into the app workspace, companion manifests stay under modules/{module}/contracts/. Canonical event files are contracts/events.yaml, contracts/reactions.yaml, and contracts/notifications.yaml; do not flatten them to the module root or emit contracts/subscriptions.yaml. Persistent module backends use backend/schemas.py, not backend/models.py.
Upstream Inputs¶
AppGenerator should compile these inputs in priority order:
captured_theme_config- optional canonical ThemeCapture artifact
-
strongest visual source when present
-
app_build_plan -
carries
theme_preferences,brand_intent, optionalshell_preset_hint, pages, entities, capability packs, auth, and integrations -
experience_spec_document/ui_design_document -
persistent page intent and layout guidance
-
concept_blueprintand related design docs - fallback context when no stronger artifact exists
Compilation Flow¶
1. ThemeCapture¶
ThemeCapture produces canonical visual evidence only.
It emits:
themeidentityassetsprimitivesfontscolorsshadowsui.chatui.shellui.page
It does not emit shell content such as header actions, profile menu items, notification copy, or footer links.
2. AppSchemaAgent¶
AppSchemaAgent compiles persistent UI into one AppSchemaOutput with six payloads:
manifestpagescustom_route_bundletheme_config_patchshell_configasset_manifest
Theme vs Shell Ownership¶
Two artifacts handle visual and behavioral customization:
theme_config_patch→ visual tokens only: colors, typography, spacing scale, shadows, density,ui.chat/ui.shell/ui.pagesemantic stylingshell_config→ shell behavior only: header logo/actions, canonicalshortcuts,navigation.policy, non-page-ownednavigation.items, andchromemode overrides
shell_preset_hint is not an artifact. It is prompt-time AppGenerator guidance from factory_app/build_context/AppGenerator/shell_presets.yaml. AppSchemaAgent uses it to choose page navigation, page shell_mode, and whether a compact shell_config override is necessary. The preset id is never written into the generated app bundle.
Do not mix them. Raw spacing/width/density tokens belong in theme_config_patch. Header action labels and app-wide shell placement rules belong in shell_config.
Rules:
manifest.default_routeis persisted toapp.json -> startup.landing_spotcustom_route_bundleis a rare bounded escape hatch for persistent routes that cannot be expressed cleanly through shipped primitivestheme_config_patchis a partial patch forbrand/theme_config.jsonshell_configis a partial patch forconfig/shell.jsonasset_manifestis a partial patch forconfig/asset_manifest.json- raw spacing, width, density, and sizing tokens belong in
theme_config_patch, notshell_config - generated
shell_config.shortcutsmay only containheader,profile,mobile,footer, andfooterHideOnMobile - do not emit custom shortcut catalogs or shell fields outside
AppShellConfigPatch - shell actions may use semantic
variants[].whenfor context-aware label/target changes; do not emit path-prefix, wildcard, or query-param override rules - page-owned shell navigation belongs on
ui/pages/*.yaml -> navigation;shell_config.navigation.policyowns app-wide placement rules - page-owned header/footer/bottom-bar intent belongs on
ui/pages/*.yaml -> shell_mode;shell_config.chromeowns only app-wide mode-policy overrides - reusable media inventory belongs in
asset_manifest, not intheme_config_patchorshell_config - custom routes must be owned exclusively by
custom_route_bundle(ui/route_manifest.json+ui/pages/custom/*.jsx) and must not duplicate anyui/pages/*.yamlroute - every custom route manifest entry must have exactly one
page_filesentry whoseregistry_keymatches the routecomponent;save_app_schemasynthesizesui/index.jsfrom that matched pair app/ui/index.jsmust register every component referenced byui/route_manifest.json; missing registrations are export/download blockers, not runtime surprises- scoped private routes should declare route
meta.routeAuthwhen route path or query params identify a resource whose visibility depends on membership, ownership, tenant/workspace access, or an invitation/access state. Declarative pages useui/pages/*.yaml -> meta.routeAuth; custom routes useui/route_manifest.json -> pages[].meta.routeAuth. admin/admin_registry.yamldeclares admin page and panel metadata; it is not a route registry and must not own full-page custom route components- managed-capability pages must bind through an app-owned facade module endpoint such as
/api/modules/analytics_dashboard/get_metrics, never directly to managed-capability internals - declarative pages may launch workflow sessions through typed page actions (
action_type: workflow), but workflow-local React still belongs to AgentGenerator andchat.tool_call
Managed-capability facade binding¶
When a build has a selected managed capability pack, AppGenerator must keep the generated app boundary explicit:
managed_capability
-> app/services/integrations/{pack_id}_client.py
-> app-owned facade module
-> ui/pages bind to the facade module
Provider-neutral example:
managed_analytics
-> app/services/integrations/managed_analytics_client.py
-> modules/analytics_dashboard/
-> ui/pages/analytics.yaml
-> /api/modules/analytics_dashboard/get_metrics
The pack descriptor may provide surfaces, supported_domains, branding, generation_rules, adapter_template, and capability_source. Those fields are planning metadata. They do not authorize generated pages to call managed provider internals directly.
For SaaS apps that select the mozaikspay managed capability, the generated app bundle must include the portable SaaS contract, not managed provider internals:
config/subscriptions.yamlwith plans, gates, and only the token wallets, token allowances, top-up products, and usage limits required by the app's AI usage, credit, or quota modelservices/integrations/mozaikspay_client.pyas the app-side connector clientmodules/billing_portal/as the app-owned facade module- billing and usage pages bound only to
/api/modules/billing_portal/* - no
modules/mozaikspay/, managed billing module, wallet module, or direct provider SDK ownership in the generated app
Managed packs that own subscription assignment writes must declare that through provides_capabilities: [subscription_write_path] in the pack contract.yaml. The scanner uses that provider-neutral capability flag to skip entitlement_dispatch; otherwise any generated app with config/subscriptions.yaml -> assignment_store must include the entitlement_dispatch generated module so self-hosted and custom-provider apps still have a deterministic assignment writer. This rule is pack-driven and must not special-case MozaiksPay in scanner logic.
The deterministic app-bundle acceptance gate enforces this boundary on the assembled bundle, not on isolated task output. A selected managed capability must pass the generated bundle scanner and the app_runtime_load check, which materializes the same file map that generate_and_download would package and loads it through AppLoader.load(). This catches semantic drift such as pages binding to provider internals, facade handlers accepting synthetic payload wrappers, missing app-level service packages, invalid companion manifests, or template files that cannot be imported by the runtime loader.
When config/integrations.yaml declares a managed setup lane, the same scanner also rejects raw payment-provider env handles, provider webhook/checkout routes, and direct provider SDK mechanics anywhere in the generated bundle. Managed setup is a contract boundary: provider callbacks and processor mechanics stay with the managed/hosted product, while the generated app uses the managed capability client and app-owned facade.
Route/component registration drift¶
For custom full-page React routes, the route contract is complete only when all of these are true:
ui/route_manifest.json -> pages[].componentnames the runtime component keyui/pages/custom/*.jsxcontains exactly one default-exported page for that keyui/index.jsregisters the same key throughregisterComponent- no declarative
ui/pages/*.yamlroute owns the same path - no implicit file discovery or admin-registry fallback may substitute for the three files above
Generation should catch missing or mismatched component registrations before download/export.
Scoped route authorization¶
routeAuth is the first-class route metadata contract for declarative pages and custom full-page routes that need a pre-render authorization check. It is not app-specific and it does not encode policy in the shell. Route metadata names an app-owned module action, and the module action makes the authorization decision using the normal service/policy layer.
In AppSchemaAgent structured output, routeAuth.params is emitted as strict {key, value} entries so provider response-format validation remains enabled. save_app_schema normalizes those entries into the runtime object shape written to generated YAML/JSON.
Example:
{
"name": "ProjectSettings",
"route": "/projects/:projectId/settings",
"title": "Project Settings",
"page_type": "settings",
"layout": "grid",
"meta": {
"routeAuth": {
"module": "project_access",
"action": "authorize_project_route",
"params": [{ "key": "project_id", "value": "$route.projectId" }]
}
},
"sections": [
{
"id": "project-settings-header",
"primitive": "PageHeader",
"config": { "title": "Project Settings" }
}
]
}
The target module action must return { "allowed": true } to render the route. Returning { "allowed": false, "reason": "..." } denies the route and lets the shell show the denial reason. This gate protects deep links and avoids rendering pages before scope is known, but backend module actions must still perform their own authorization checks.
2b. AdminRegistryAgent¶
AdminRegistryAgent runs after AppUIQualityAgent passes and before AssemblyAgent.
It produces AdminRegistryOutput with two payloads:
admin_registry— typedAdminRegistryobject (page list with ids, labels, paths, icons, scope, order)code_files— serializedadmin/admin_registry.yaml
Rules:
- Always emits
overviewandsettingsapp-scope pages - Includes additional standard pages (
access,billing,usage,activity,operations,integrations,support) based onapp_build_plan.capability_packsentity domains andauth_strategy - Standard pages are modular, not a "full app" bundle preset. A generated app should only receive pages whose source capability, entity domain, integration need, or access model creates a real operator task for that page.
- Includes operator-only pages (e.g.
hosting) only whencapability_packsis non-empty and explicitly targets hosted/operator app management — never in plain OSS contexts without workspace build context - Uses
scope: appfor all generated app pages; workspace-scope pages only for hosted operator contexts - Hosted global operator registries belong to hosted product workspaces and must not be emitted into standard generated app bundles
- Page ids must cover every entity domain that
ConfigMiddlewareAgentassigns module admin panels to - The module contract quality gate validates at generation time that every
admin.yamlpanelpagefield resolves to a declared page id
Operator-only pages:
| Page id | Inclusion condition | Path |
|---|---|---|
hosting | capability_packs explicitly targets hosted/operator app management such as hosting or hosted deployment records; portable deployment artifacts alone do not qualify | /apps/:appId/hosting |
Add new operator-only pages here when a managed capability context introduces a new operator surface. Do not add them to the standard inclusion rules.
AssemblyAgent must include admin/admin_registry.yaml in the final bundle output.
3. save_app_schema¶
save_app_schema is the persistence tool for schema-driven app bundles.
It must:
- write generated artifacts under
$MOZAIKS_GENERATED_ARTIFACTS_PATH/apps/{app_id}/{build_id}/app/ - write
app.json - write
ui/pages/{name}.yaml - write
ui/route_manifest.jsonwhencustom_route_bundleexists - write
ui/pages/custom/*.jsxwhencustom_route_bundleexists - synthesize
ui/index.jsfromcustom_route_bundle.page_fileswhen custom routes exist - reject or warn on custom route registry drift before assembly: missing page files, duplicate registry keys,
.jsroute files, non-default-exported React, or route components that no page file registers - deep-merge
theme_config_patchintobrand/theme_config.json - deep-merge
shell_configintoconfig/shell.json - deep-merge
asset_manifestintoconfig/asset_manifest.json - store
app_manifest,app_pages,app_custom_route_bundle,app_theme_config_patch,app_shell_config,app_asset_manifest, andapp_schema_readyin workflow context
It must not write directly into an active runtime-loaded app root such as a workspace's app/ bundle or factory_app/app. Activation requires an explicit promotion step.
4. AssemblyAgent¶
When app_schema_ready == true, AssemblyAgent must emit those artifacts back out as code_files so downstream download/export tools can bundle them.
Required schema-driven outputs:
app.jsonadmin/admin_registry.yamlui/pages/{name}.yamlui/route_manifest.jsonwhenapp_custom_route_bundleexistsui/pages/custom/*.jsxwhenapp_custom_route_bundleexistsui/index.jswhenapp_custom_route_bundleexistsbrand/theme_config.jsonwhenapp_theme_config_patchexistsconfig/shell.jsonwhenapp_shell_configexistsconfig/asset_manifest.jsonwhenapp_asset_manifestexists
When app_schema_ready == false, AssemblyAgent should use task batch outputs via assemble_app_tasks and must still preserve the page contract.
4b. Raw Frontend Path Removed¶
AppGenerator no longer carries a secondary raw frontend page/component generation lane.
Rules:
- ordinary persistent pages still compile through
AppSchemaAgent - raw React page/component tasks do not belong in AppGenerator build plans outside the explicit
custom_route_bundlecontract - shell content compiles through
shell_config, not through a separate frontend shell agent - if the primitive system is insufficient, the platform should add a primitive, pattern, or page capability rather than reviving a second frontend codegen path
5. IntegrationReadinessAgent¶
IntegrationReadinessAgent runs after AssemblyAgent and before validation or download. It is not a manual preflight step. It is the agentic aggregation point for third-party connector needs discovered while planning or executing decomposed build tasks.
Inputs:
app_build_plan.external_integrationscapability_packs[].required_integrationsbuild_tasks[].integration_needsapp_task_batch_resultsfrom task batch executionintegration_needsrecorded by task agents withrecord_integration_need
Rules:
- Reuse ready app-scoped connectors from the platform connector store.
- Prompt inline only for unresolved required credentials or required non-secret connector configuration.
- Emit structured
integration.requiredrequests withintegration_id, provider/service id,purpose,required_fields,secret_fields,non_secret_fields,permissions_required, and resume context. - Persist newly supplied credentials through the platform connector service so
/apps/{appId}/integrationsreflects the result. - Treat the workspace integration catalog as inventory only. The app's integration page is driven by
integration_needsand managed capability requirements, not by every available catalog entry. - For monetized apps,
save_integration_manifestrecordsmozaikspayas a removable default app integration when the build did not explicitly declare a monetization connector. This default is optional and operator-removable; an explicitcapability_packs[].required_integrationsentry remains the source of truth for blocking credential requirements. - Persist only frontend-safe non-secret config in connector metadata. Secret fields are write-only and must not be returned to chat, generated code, or frontend read APIs.
- Do not generate app-owned API-key tables, credential collections, or custom credential forms.
- If required credentials are declined or unavailable, block validation/download and return to the user with the unresolved service list.
6. generate_and_download¶
generate_and_download is the bundling tool.
It does not reason about artifact ownership. It packages the materialized file set into the downloadable app bundle after the deterministic app-bundle acceptance gate passes.
Download archives may include a single top-level folder named for the bundle such as GeneratedApp/. Studio artifact promotion treats that folder as a zip transport wrapper only when stripping it reveals an app-root bundle containing app.json. The promoted active app root must contain app.json, config/, modules/, ui/, and other app-bundle families directly; it must not require the platform host to look under GeneratedApp/.
When AgentGenerator workflow metadata is present, the acceptance gate is export-blocking. AppGenerator must wire each generated workflow through:
modules/{module}/module.yamlcapabilities[]withkind: workflow, the AgentGeneratorcapability_id, and the AgentGenerator workflow name astargetmodules/{module}/contracts/events.yamldeclaring every workflow trigger eventmodules/{module}/module.yamlactionemits[]for the trigger event that starts the workflowmodules/{module}/contracts/reactions.yamlrouting each trigger event to the AgentGenerator workflow capability id
The gate also blocks semantic drift: trigger-event capability_id values must match the workflow capability id from AgentGenerator metadata, generated app modules must not invent workflow capabilities absent from that metadata, and a workflow trigger event must not also route to a different generated workflow capability unless that route is declared by the same metadata.
When workflow integration fails, the acceptance gate writes a bounded repair contract before export can proceed:
workflow_integration_repair_statusworkflow_integration_repair_countworkflow_integration_repair_requestworkflow_integration_repair_failed_testsworkflow_integration_repair_result
needs_revision routes back to ConfigMiddlewareAgent, which runs in workflow-integration repair mode. That mode emits only corrected module contract YAML for the affected module, using AgentGenerator workflow metadata and the injected [WORKFLOW INTEGRATION CONTRACT] as authority. It must not regenerate backend Python, frontend code, pages, data contracts, service foundation files, or unrelated modules. After the configured attempt limit, the status becomes blocked and the workflow returns to the user.
When the generated-bundle scanner fails on a file-family violation, the same acceptance gate writes a bundle repair contract before export can proceed:
bundle_repair_statusbundle_repair_targetbundle_repair_attempt_countbundle_repair_max_attemptsbundle_repair_requestbundle_repair_errorsbundle_repair_result
Both workflow-integration and generated-bundle repair contracts also persist a stable failure fingerprint and a no_progress flag. The acceptance gate normalizes the current failure evidence and hashes it before scheduling a repair. If validation after a repair produces the identical fingerprint, the gate marks the repair blocked immediately instead of spending another model turn on the same unchanged failure. A changed fingerprint may consume the next bounded attempt. Passing validation clears the fingerprint and no-progress state.
This makes the repair controller deterministic:
- observe validation evidence
- classify the narrowest owning agent
- emit a bounded repair request
- apply only that agent's owned file delta
- re-run the authoritative acceptance gate
- stop on pass, repeated evidence, or attempt exhaustion
Agent prompts may propose a patch, but they do not decide whether the loop continues. The acceptance gate, failure fingerprint, ownership table, and retry budget are the control authority.
The automated repair controller currently covers workflow-integration failures and generated-bundle scanner failures. Other acceptance failures still fail closed to the user. Browser interaction evidence (console errors, failed network requests, screenshots, and replayable user-flow assertions) is not yet a first-class automatic repair input. Closing that gap requires a typed, environment-scoped verification-evidence contract that maps each failure to an owning agent and replays the same scenario after the patch. Browser evidence may inform repair, but it must not bypass deterministic acceptance, review, or promotion.
The target must be the narrowest owning agent: AppSchemaAgent for page/schema endpoint drift, ConfigMiddlewareAgent for config or managed-capability client drift, ServiceAgent for backend Python/service drift, and FrontendStubAgent for generated frontend helper drift. A repair agent may remove stale or invalid artifacts by emitting deleted_files; AssemblyAgent, the acceptance gate, and DownloadAgent all apply those deletions before validation or packaging.
Deterministic smoke coverage:
This smoke injects an app-local token wallet ledger, verifies that scanner repair routes to ServiceAgent, applies deleted_files, re-runs acceptance, checks the export gate, and proves packaging no longer contains the removed artifact.
When a build/export context requests deployment output, or the generated files already contain deployment.manifest.json, Dockerfile, docker-compose.yml, .github/workflows/readiness.yml, or .github/workflows/deploy.yml, the acceptance gate runs the provider-neutral deployment artifact validator. Deployment-ready bundles must include valid deployment artifacts such as Dockerfile, .env.example, .env.staging.example, .env.production.example, and deployment.manifest.json; missing deployment artifacts block export/promotion instead of being discovered by a later hosting adapter.
The acceptance gate also runs app_runtime_load, a no-live-call runtime loader check. It writes the assembled file map to a temporary app/ root and calls AppLoader.load(). Export and promotion are blocked when the app manifest, module contracts, companion manifests, handler entrypoints, runtime extension contracts, data/subscription config, or app-level services.* imports cannot load. The check persists app_runtime_load_passed and app_runtime_load_result into workflow context and includes app_runtime_load in app_bundle_acceptance_result.validation_evidence.
7. AppValidation Strategy¶
AppValidationAgent must use an explicit validation strategy contract instead of implicit E2B-only behavior.
Canonical strategy values:
e2bdockerlocalskip
Canonical status values:
passedfailedskipped
Rules:
- Studio/hosted environments may prefer
e2bwhen sandbox credentials are available. - Local environments with a running Docker daemon resolve to
docker, which also exposes a preview URL (see app-validation-sandboxes.md). - CLI/local environments may resolve to
localor explicitskip. - generation/export must not be blocked solely because E2B is unavailable.
skipis explicit and deterministic; it is not a hidden fallback and it is not reported aspassed.- export gating must allow only
passedor explicitskipped, and still requires integration readiness and wiring checks to pass.
Materialization rule:
- typed agent outputs such as
app_backend_admin_config,python_files, andjs_filesare the source of truth for their owned lanes - the same applies to
database_files,model_files, andservice_foundation_bundle.files - extraction may regenerate canonical file content from those typed fields before packaging
- raw
code_filesare the serialized mirror, not the authority, when a typed lane exists
Bundle Rules¶
Do:
- keep persistent pages declarative
- keep shell content separate from shell styling
- reuse ThemeCapture output when available
- deep-merge generated theme/shell patches into canonical runtime files
Do not:
- generate raw React files for persistent pages by default
- generate any AppGenerator-managed raw React page/component files for persistent pages
- place header/footer action content in
theme_config.json - place spacing/padding/density tokens in
shell.json - place reusable media inventory in
theme_config.jsonorshell.json - route visual shell concerns through AgentGenerator
Why This Exists¶
Without this split, AppGenerator either under-specifies visual/media control or mixes styling, shell behavior, and asset inventory. The contract above keeps bundle generation deterministic, keeps ThemeCapture reusable, and gives the runtime a stable set of artifacts to consume.