Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
314 lines
15 KiB
Markdown
314 lines
15 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-releases/catalog.json
|
|
```
|
|
|
|
Staging releases use a separate catalog and must not write production
|
|
`latest.json` objects:
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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](./workspace-app-runtime.md).
|
|
|
|
## Release Flow
|
|
|
|
External app repositories should call `.github/workflows/publish-tutti-app-release.yml` from this repository. The reusable workflow:
|
|
|
|
1. Checks out the app repository.
|
|
2. Serializes releases per app and branch.
|
|
3. Resolves the release version from `release_bump` and existing release tags,
|
|
or from the package manifest for staging-style runs.
|
|
4. Runs the app repository package command.
|
|
5. Runs `@tutti-os/app-release-tools`.
|
|
6. Generates a zip, immutable `release.json`, and mutable `latest.json`.
|
|
7. Refuses to overwrite an existing immutable release version.
|
|
8. Uploads the immutable release directory, then conditionally updates
|
|
`versions.json` and `latest.json`.
|
|
9. Creates the release tag when `create_release_tag` is enabled.
|
|
10. Optionally rebuilds that app from `versions.json` and conditionally merges
|
|
it into `catalog.json` when `publish_catalog` is 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
1. Release only: `publish_catalog: false` and `catalog_only: false`. This
|
|
uploads a new app version and updates `apps/<appId>/latest.json`. App Center
|
|
will see it after a later `catalog.json` publish.
|
|
2. Release and catalog: `publish_catalog: true`. This uploads the app release
|
|
and immediately merges that release into `catalog.json`.
|
|
3. Catalog only: `catalog_only: true`, or the Tutti catalog workflow in merge
|
|
mode with `app_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:
|
|
|
|
```text
|
|
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.
|