# Agotalk Role Package File Contract (v0.3)

This is the public file contract for role packages. It is reference material, not a runtime protocol and not a shared source-code dependency.

Producer: a role repository, such as `agotalk-roles`, creates a package file.
Consumer: the Agotalk App reads that package file.
Boundary: the package bytes and this public contract. The producer must not call Agotalk app repository scripts, and the app must not require the role repository checkout.

## Package Unit

A role package is a single top-level directory compressed as a `.zip`.

```text
<role-id>/
  role.md
  agotalk.role.toml
  avatar.png
  intro.html
  skills/
  contents/
  assets/
```

Only `role.md` is required for a local import-only role. Publishing to `roles.agotalk.com` requires `agotalk.role.toml`.

`<role-id>` must use lowercase words joined by single hyphens:

```text
visual-media-assistant
teacher-tori
words-john
```

The top-level directory name is the role identity. It must equal `agotalk.role.toml` field `id` when the manifest exists.

## Required Files

`role.md` is the role behavior source. It contains front matter plus the role narrative. The only consumed front matter keys are:

```yaml
---
title: "Role title"
description: "Short role summary"
---
```

`avatar:` in `role.md` front matter is retired. Put the avatar image at package root as `avatar.png`.

`agotalk.role.toml` is the machine-readable publishing contract. There is no second manifest.

## Manifest

Minimal conversational role:

```toml
schema_version = 1
id = "teacher-tori"
version = "0.1.0"
title = "Tori"
summary = "A warm but exacting English teacher."
kind = "role"
license = "free"
min_app_version = "0.1.57"

[preview]
intro_html = "intro.html"

[state]
remembers_progress = true

[environment]
manager = "agotalk"
dependency_mode = "none"
```

Launchable static browser application:

```toml
schema_version = 1
id = "words-john"
version = "0.1.0"
title = "Words John"
summary = "Typing word game bundled as role package content."
kind = "application"
license = "free"
min_app_version = "0.1.57"

[entry]
kind = "static_browser"
target = "contents/word-pop-game/index.html"
address_bar = "hidden"

[preview]
intro_html = "intro.html"
cover_image = "assets/screenshots/word-pop-game-preview.png"
screenshots = ["assets/screenshots/word-pop-game-preview.png"]

[state]
remembers_progress = false

[environment]
manager = "agotalk"
dependency_mode = "none"
```

## Manifest Fields

Required:

```text
schema_version = 1
id = role id, same as directory name
version = SemVer
title = display title
summary = short display summary
kind = "role" | "application"
license = "free"
```

Publishing requires `min_app_version`, either in the manifest or as an explicit publisher option.

`[environment]` must declare:

```toml
manager = "agotalk"
dependency_mode = "none"
```

`[state].remembers_progress` must be boolean.

`kind = "role"` forbids `[entry]`. Its behavior is `role.md`.

`kind = "application"` requires `[entry]`:

```toml
kind = "static_browser"
target = "package-relative/path.html"
address_bar = "visible" # or "hidden"
```

All package paths are relative to the package root, must stay inside the package, and must point at regular files.

## Localized Display Copy

`[i18n."<locale>"]` tables are optional and localize the display `title` and `summary`:

```toml
[i18n."zh-CN"]
title = "英语老师 Tori"
summary = "一位通过对话授课的英语老师，背后是剑桥与雅思的分级课程库。"
```

Rules:

- Locale keys look like `en` or `zh-CN` (`^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$`), at most 8 locales per manifest.
- Each locale table must declare exactly `title` and `summary` — a complete override pair. Same limits as the top-level fields: title up to 120 characters, summary up to 500.
- Top-level `title`/`summary` stay the canonical fallback for any locale without an entry. Write them in the role's primary language.
- The publisher projects `i18n` onto the catalog role entry, and `/api/v1/roles` (list and detail) exposes it as `i18n`: an object keyed by locale, or `null` when absent. The field is additive; consumers that ignore it keep working.
- Role-level `i18n` refreshes on every publish of that package — the manifest is the only source. Publishing a version whose manifest omits `[i18n]` clears the catalog entry.

## Assets

`avatar.png` at package root is the role avatar. Recommended source size: 512x512 PNG.

Preview source images should be PNG:

```text
cover_image: 1280x720
screenshots: 1 to 3 images, each 1280x720
```

Avoid JPEG for text-heavy screenshots. SVG is acceptable for logos/icons, not screenshots.

During publish, the role repository publisher projects these package-local files into website presentation assets:

```text
avatar.png                  -> /api/v1/roles/<role-id>/versions/<version>/assets/avatar.webp
preview.cover_image         -> /api/v1/roles/<role-id>/versions/<version>/assets/cover.webp
preview.screenshots[0..2]   -> /api/v1/roles/<role-id>/versions/<version>/assets/screenshots/<n>.webp
```

The publisher normalizes orientation, converts website presentation assets to WebP, then stores the optimized bytes in R2 under `role-assets/<role-id>/<version>/...`.
`roles.agotalk.com` reads the catalog `presentation` projection and serves only catalog-declared assets. Official role images are not copied into the website source tree.

## Archive Rules

The published artifact is a `.zip` whose top-level directory is `<role-id>/`.

Do not include:

```text
.DS_Store
.git/
node_modules/
.env
```

Symlinks are rejected. Non-regular files are rejected. File names must be canonical Unicode NFC and must not contain slash tricks.

The same role version is immutable. Republishing the same `roleId + version` must produce identical bytes and metadata. Different bytes for an existing version must fail.

## Publishing Authorization

Publishing credentials belong to the role repository, not the Agotalk app repository.

Use a local ignored `.env` file in the role repository:

```dotenv
CLOUDFLARE_ACCOUNT_ID=...
CLOUDFLARE_R2_ACCESS_KEY_ID=...
CLOUDFLARE_R2_SECRET_ACCESS_KEY=...
AGOTALK_ROLE_R2_BUCKET=agotalk-roles
AGOTALK_ROLE_PUBLIC_ORIGIN=https://roles.agotalk.com
```

Commit only `.env.example` or documentation with field names. Never commit or paste secret values into progress logs.

## Failure Semantics

Validation and publishing must fail loudly. Common error codes:

```text
ROLE_PACKAGE_MANIFEST_MISSING
ROLE_PACKAGE_ID_MISMATCH
ROLE_PACKAGE_KIND_INVALID
ROLE_PACKAGE_ENTRY_REQUIRED
ROLE_PACKAGE_ENTRY_FORBIDDEN
ROLE_PACKAGE_PATH_OUTSIDE_ROOT
ROLE_PACKAGE_LEGACY_AVATAR_FIELD
ROLE_PUBLISH_MIN_APP_VERSION_MISSING
ROLE_VERSION_IMMUTABLE_CONFLICT
ROLE_PUBLISH_AUTH_MISSING
ROLE_PUBLISH_CATALOG_CONFLICT
```

No tool should silently substitute app versions, silently ignore a broken manifest, or publish from a dirty source tree.

## Minimal Self-Check

Before publish:

```bash
npm run role:package -- --package roles/<role-id> --skip-remote
npm run role:auth-check
npm run role:package -- --package roles/<role-id>
```

The first command validates and packages locally. The second checks R2 credentials without printing secrets. The third compares the package against the public catalog and reports `create` or `unchanged`.
