lace-app-sdk
LaceAppManifest reference
LaceAppManifest declares everything your app provides. LACE discovers manifests and wires providers at bootstrap — no registration calls, no glue code.
from lace_app_sdk.manifest import LaceAppManifest, AppRuntimeUiDeclaration
from lace_app_sdk.data import AppDataCollectionSchema
from app.data import TICKETS
MANIFEST = LaceAppManifest(
app_id="acme.field_intake",
display_name="Field Intake",
version="0.1.0",
description="Field intake with triage agent.",
data_collections=[TICKETS],
# pure data — no import paths for handlers
tools=[], # populated by scanning @tool — see tool_modules below
tool_modules=["app.tools"],
route_providers=["app.routes:RouteProvider"],
agent_providers=["app.agents:AgentProvider"],
pipeline_providers=["app.pipelines:IntakePipelines"],
runtime_ui=AppRuntimeUiDeclaration(
module_id="field_intake.ui", module_version="1.0.0",
entrypoints={"session_home": "./RuntimeView"}, isolation_mode="iframe"),
)
# emit the canonical JSON the publisher seals
from lace_app_sdk.manifest import write_app_manifest
write_app_manifest(MANIFEST, "lace_app_manifest.json")
What a manifest declares
| Field | Type | What it owns |
|---|---|---|
app_id | str | Stable id, e.g. acme.field_intake (used as sidecar mount prefix, collection scope, tool scope) |
display_name / version / description | str | Human metadata; version is informational — platform release id is rel_* |
data_collections | list[AppDataCollectionSchema] | Typed, tenant-scoped collections (see data) |
tools + tool_modules | list[AppToolDeclaration] / list[str] | Tool contracts (pure data) + modules to import so decorators run (tools) |
route_providers | list[str] | Dotted "app.routes:Provider" paths |
agent_providers / agent_runtime_extensions | list[str] | AppAgentProvider + extensions (agents) |
pipeline_providers | list[str] | AppPipelineProvider (pipelines) |
runtime_ui | AppRuntimeUiDeclaration | Module Federation remote (routes & UI) |
migration_provider | str | AppData migrations |
scheduler/schedule_specs | list[AppScheduleSpecDeclaration] | Cron schedules → app.route.invoke tasks |
permission/policy/settings_providers | list[str] | App-scoped policy and settings |
Pure data — why the manifest carries no import paths for handlers
tools[] entries are data (tool_id, description_for_model, input_schema, policy) — no dotted refs — so the platform can mint lace.catalog_definitions rows without executing app code. tool_modules is the import sidecar's hint: it imports those modules in the app's own process so @tool decorators populate the registry. Without it, a manifest with tools only would 404 at invoke time. Same split as job_handler_modules.
Canonical JSON — what gets sealed
{
"app_id": "acme.field_intake",
"display_name": "Field Intake",
"version": "0.1.0",
"data_collections": [ ... ],
"route_providers": ["app.routes:RouteProvider"],
"agent_providers": ["app.agents:AgentProvider"],
"tool_modules": ["app.tools"]
}
Call write_app_manifest(MANIFEST, "lace_app_manifest.json") (from lace_app_sdk.manifest) to emit the canonical file. The publisher (src/lace/builder/publisher.py) reads it — and also auto-derives collections from app/data.py as a fallback. The file is the blast-radius diff reviewers read before publish.
Declaration is enforcement
Because the manifest is the only way capabilities enter the runtime, an app cannot acquire a tool, route, collection, or schedule it did not declare. Reviewing the manifest diff is reviewing the blast radius.
Next: data collections → tools → agents.