15 KiB
Workspace App Catalog
Workspace App Center can show built-in apps from two sources:
- embedded app packages committed in
services/tuttid/builtin-apps - remote built-in app packages listed by a JSON catalog
Remote built-in apps are optional. App Center can show their name, description, and icon before the package is downloaded. The package zip is downloaded only when the user installs the app.
Runtime Overrides
TUTTI_APP_CATALOG_FILE points tuttid at a local catalog JSON file and has priority over TUTTI_APP_CATALOG_URL.
When TUTTI_APP_CATALOG_URL is unset, tuttid loads the default published catalog:
https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-releases/catalog.json
Staging releases use a separate catalog and must not write production
latest.json objects:
https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-releases-staging/catalog.json
TUTTI_APP_CATALOG_URL points tuttid at a public HTTP(S) catalog JSON file, usually served through CloudFront or S3. Set it to an empty string to disable the default remote catalog.
Example local mock:
TUTTI_APP_CATALOG_FILE=/tmp/tutti-app-catalog/catalog.json pnpm dev:desktop
Refresh Behavior
tuttid is the source of truth for remote catalog retrieval, manifest validation, artifact URLs, and artifact SHA-256 values. Renderer code should ask tuttid to refresh the catalog instead of fetching the CDN catalog directly.
App Center opening should call POST /v1/workspaces/{workspaceID}/apps/catalog/refresh. The refresh request is in-flight deduplicated by tuttid, keeps local and previously loaded apps visible while loading, and retries retryable network or 5xx failures for a total of three attempts.
App Center Grouping
App Center uses the same daemon catalog snapshot for recommended and community
apps. tuttid should not add a separate community source enum for this
presentation split. The reusable App Center UI keeps "My apps" for local and
local-dev apps, and splits non-local catalog apps between "Official apps"
and "Community apps" by app id in
packages/workspace/app-center/src/core/appCenterAppOrdering.ts.
When moving an app between recommended and community presentation, update the central app id list and the App Center ordering tests in the same change. Community app cards show a developer entry first in the card action menu when the app metadata has an author URL. Prefer publishing author metadata through the app catalog; desktop-local app id overrides are acceptable only as a temporary bridge for community apps whose catalog metadata has not caught up.
Catalog Shape
{
"schemaVersion": "tutti.app.catalog.v1",
"apps": [
{
"localizations": [
{
"locale": "zh-CN",
"name": "产品原型设计",
"description": "创建并迭代产品原型设计",
"tags": ["设计", "原型"]
}
],
"manifest": {
"schemaVersion": "tutti.app.manifest.v1",
"appId": "vibe-design",
"version": "0.1.0+abc123",
"name": "Vibe Design",
"description": "Design workspace",
"icon": {
"type": "asset",
"src": "icon.svg"
},
"runtime": {
"bootstrap": "bootstrap.sh",
"healthcheckPath": "/"
}
},
"distribution": {
"kind": "remote",
"artifactUrl": "https://cdn.example.test/apps/vibe-design/0.1.0%2Babc123/vibe-design-0.1.0%2Babc123.zip",
"artifactSha256": "64-char-sha256",
"iconUrl": "https://cdn.example.test/apps/vibe-design/0.1.0%2Babc123/icon.svg"
}
}
]
}
Remote catalog entries must include distribution.iconUrl, distribution.artifactUrl, distribution.artifactSha256, and a manifest icon asset. The zip package must contain a complete app package with tutti.app.json, bootstrap.sh, AGENTS.md, and the manifest icon asset.
catalog.json keeps schemaVersion: "tutti.app.catalog.v1" so old daemons
continue to accept it. Its apps[] array is the legacy-safe projection: for
each app it contains the highest active release whose minTuttiVersion is
0.0.0. New daemons also read the additive compatibility frontier:
{
"schemaVersion": "tutti.app.catalog.v1",
"apps": [{ "manifest": { "appId": "vibe-design", "version": "0.1.0" } }],
"compatibility": {
"apps": {
"vibe-design": [
{
"minTuttiVersion": "0.0.0",
"app": { "manifest": { "appId": "vibe-design", "version": "0.1.0" } }
},
{
"minTuttiVersion": "0.12.0",
"app": { "manifest": { "appId": "vibe-design", "version": "0.2.0" } }
}
]
}
}
}
The full compatibility entries contain the same manifest, localization, and
remote distribution fields as apps[]; they are abbreviated above only for
clarity. For a valid host version, the daemon selects the eligible entry with
the greatest minTuttiVersion; releases from lower compatibility tiers do not
compete by app version. The frontier keeps only versions that can be selected
for some Tutti version. The builder rejects catalog output above 1 MiB because
legacy daemons enforce that response limit.
Each app also owns apps/<appId>/versions.json. It retains the full active and
withdrawn release history plus the explicit minTuttiVersion for each
immutable release. latest.json remains the absolute latest published release
and is not a compatibility input.
Managed Desktop passes its version to tuttid through TUTTI_APP_VERSION.
Packaged Desktop uses the Electron package version; make dev-gui resolves the
same Git-derived version used by desktop packaging before launching Electron.
The daemon applies compatibility selection consistently to list, refresh,
install, and update workflows. Missing or invalid host versions use apps[].
Desktop preferences choose the active app catalog channel (production or
staging). That channel selects which remote catalog URL to load. Once a
catalog entry is selected for the host, it is authoritative for App Center
update and materialize decisions: when the installed builtin package version
differs from the selected catalog version, Tutti offers or applies that catalog
package even if the catalog version is lower than a package previously installed
from the other channel. Equal version strings leave the installed package in
place.
When a package manifest declares localizationInfo, release tooling reads the
referenced package-local manifest locale files and writes their name,
description, and tags into the catalog entry localizations field. App
Center uses this catalog metadata for uninstalled remote apps, because it must
not download or unzip the app package just to render localized catalog cards.
Workspace app packages do not declare arbitrary runtime kinds or bundle Python/Node. Packages that only need the managed Node/static runtime may declare runtime.profile: "connector-node-static"; all other managed runtime release and download rules belong to Workspace App Runtime.
Release Flow
External app repositories should call .github/workflows/publish-tutti-app-release.yml from this repository. The reusable workflow:
- Checks out the app repository.
- Serializes releases per app and branch.
- Resolves the release version from
release_bumpand existing release tags, or from the package manifest for staging-style runs. - Runs the app repository package command.
- Runs
@tutti-os/app-release-tools. - Generates a zip, immutable
release.json, and mutablelatest.json. - Refuses to overwrite an existing immutable release version.
- Uploads the immutable release directory, then conditionally updates
versions.jsonandlatest.json. - Creates the release tag when
create_release_tagis enabled. - Optionally rebuilds that app from
versions.jsonand conditionally merges it intocatalog.jsonwhenpublish_catalogis enabled.
Production app releases are manually dispatched from GitHub Actions with a
release_bump value of patch, minor, or major. The reusable workflow
fetches existing tags with the configured release_tag_prefix (default
<appId>-v), reads the packaged manifest version, calculates the next stable
semver version from the greater of those sources, publishes the S3 release,
verifies the artifact, then creates an annotated release tag such as
vibe-design-v1.2.4. The workflow never edits or commits the source manifest.
Every normal release must pass an explicit min_tutti_version. Use 0.0.0
only when the release is safe for every legacy Tutti client. Missing
compatibility metadata fails before package upload; there is no permissive
default.
Staging app releases do not create release tags. When release_bump is empty,
the reusable workflow resolves a stable seed from the greater of the packaged
manifest version and existing <appId>-v* release tags, then publishes
<seed>+<short git sha>. Production tags therefore keep staging builds aligned
with the current product line (for example 0.1.45+<sha> after ai-doc-v0.1.45),
instead of freezing on a stale source-manifest version that production never
rewrites.
Staging caller workflows should run automatically on push to main, which is
the event produced after a pull request is merged. Push-triggered staging runs
should publish the staging catalog by default so App Center sees the merged app
without a second manual dispatch. Manual staging dispatch remains useful for
catalog repair, reruns, and targeted validation.
GitHub Actions concurrency groups are repository-scoped and cannot serialize
catalog writes from different app repositories. Mutable versions.json,
latest.json, and catalog.json writes therefore use S3 ETag preconditions and
retry after 412 Precondition Failed. The release workflow reads the existing
catalog from the same S3 prefix, rebuilds the selected app from versions.json,
and optionally invalidates the catalog path when
catalog_cloudfront_distribution_id is set.
The release upload role must be allowed to read and write the app's
versions.json, latest.json, and the shared catalog.json object, and must
have CloudFront invalidation permissions when invalidation is enabled.
Caller workflows usually pass catalog_cloudfront_distribution_id from
TUTTI_APP_RELEASES_PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID or the shared
TUTTI_APP_RELEASES_CLOUDFRONT_DISTRIBUTION_ID variable. Prefer configuring
shared CloudFront distribution ids as organization variables with selected
repository access, and use repository variables only for repository-specific
overrides or temporary setup. When neither variable is configured, the input is
empty, invalidation is skipped, and catalog readers rely on the catalog.json
cache TTL.
The release workflow also supports catalog_only: true for catalog repair and
refresh operations. In catalog-only mode it skips package build, version bump,
release metadata generation, and immutable artifact upload, then rebuilds the
app from apps/<appId>/versions.json and merges it into catalog.json. App
repositories may expose this as a manual input; the Tutti
catalog workflows provide the same repair path when a caller workflow does not
expose catalog-only dispatch.
Retrying a release version is allowed only when the existing immutable
release.json matches the newly generated release metadata. Release archives
normalize entry order, timestamps, and optional zip metadata so rebuilding the
same package produces the same artifact SHA-256. In that case the workflow
repairs mutable state such as latest.json, catalog metadata, and CloudFront
invalidation without re-uploading immutable artifacts.
Each app uploads under:
apps/<appId>/<version>/
apps/<appId>/latest.json
apps/<appId>/versions.json
The Tutti repository owns .github/workflows/publish-tutti-app-catalog.yml.
That workflow reads selected apps/<appId>/versions.json files from S3 and
publishes one shared catalog.json. It defaults to merge mode, which preserves
existing catalog apps and updates only selected app ids. Replace mode publishes
only the selected app ids and should be used only for deliberate full catalog
replacement.
For a controlled withdrawal, dispatch the production workflow in merge mode
with withdraw_app_versions set to comma- or newline-separated
appId@version entries. The workflow marks those records withdrawn in their
mutable versions.json indexes and then rebuilds the catalog. It never deletes
the immutable release directory or latest.json, so restoring a withdrawn
release remains a metadata-only operation. Withdrawal is intentionally rejected
in replace mode to prevent an accidental partial catalog publication.
There are three normal publishing modes:
- Release only:
publish_catalog: falseandcatalog_only: false. This uploads a new app version and updatesapps/<appId>/latest.json. App Center will see it after a latercatalog.jsonpublish. - Release and catalog:
publish_catalog: true. This uploads the app release and immediately merges that release intocatalog.json. - Catalog only:
catalog_only: true, or the Tutti catalog workflow in merge mode withapp_ids: <appId>. Use this after a release already succeeded when catalog was skipped, or when validation should happen before catalog publication. It must not bump or upload a new app version.
Catalog merges are explicit. App repository release workflows are scoped to the
current app_id: publish_catalog: true and catalog_only: true merge only
that app's apps/<appId>/versions.json into catalog.json. If app A published a
release without catalog publication, app B's later release with
publish_catalog: true preserves the existing catalog and updates B only; it
does not discover and add A. To add or refresh multiple apps at once, use the
Tutti catalog workflow with every desired app listed in app_ids, or run each
app's catalog-only dispatch separately. No catalog path scans S3 for all
published app indexes.
Production and staging release metadata must stay on separate S3 prefixes:
tutti-app-releases/apps/<appId>/latest.json
tutti-app-releases/apps/<appId>/versions.json
tutti-app-releases/catalog.json
tutti-app-releases-staging/apps/<appId>/latest.json
tutti-app-releases-staging/apps/<appId>/versions.json
tutti-app-releases-staging/catalog.json
The staging asset base URL must also point at the staging prefix, for example
https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-releases-staging. Do not
reuse the production TUTTI_APP_RELEASES_BASE_URL for staging, because the
published latest.json and catalog.json would then reference production
artifact URLs. For private app repositories that cannot read organization
variables, set the Tutti release variables as repository variables on each app
repository.
Use .github/workflows/publish-tutti-app-catalog-staging.yml to publish a
staging catalog. Use .github/workflows/publish-tutti-app-catalog.yml to
publish the production catalog. Production catalog publishing must read only
production release metadata; staging catalog publishing must read only staging
release metadata. In merge mode, an empty app id input refreshes the existing
catalog and invalidates CloudFront without changing the app set, as long as an
existing catalog.json is already present.