Agotalk Role Package File Contract (v0.4)

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.

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

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

agapps/ is the optional home for browser applications in a role package. Give each app its own subdirectory, such as agapps/word-pop-game/. contents/ remains available for scripts, templates, and existing applications. Keep an existing browser app at its current path when moving it could separate it from locally saved user data. Neither folder is an app registry.

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

visual-media-assistant
english-conversation-teacher
typing-word-game

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:

---
title: "Role or capability title"
description: "Short role summary"
---

title and description describe the reusable role's profession, capability, or system identity. A title is not a person's name. Package files do not name the conversational agent; name a member in the app instead.

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:

schema_version = 1
id = "english-conversation-teacher"
version = "0.1.0"
title = "English Conversation Teacher"
summary = "A warm but exacting English teacher, backed by a leveled Cambridge & IELTS curriculum."
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:

schema_version = 1
id = "typing-word-game"
version = "0.1.0"
title = "Typing Word Game"
summary = "Typing word game bundled as role package content."
kind = "application"
license = "free"
min_app_version = "0.1.57"

[entry]
kind = "static_browser"
target = "agapps/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 — all of these, or the package is rejected:

schema_version = 1                      # the integer 1, not "1" and not "1.0"
id = role id, same as directory name    # the key is `id`, not `role_id`
version = SemVer
title = role or capability display title, not a person's name
summary = short display summary
kind = "role" | "application"
license = "free"                        # this exact string; no other license value is accepted

[state]
remembers_progress = true | false       # required, boolean

[environment]
manager = "agotalk"
dependency_mode = "none" | "locked"

For "locked", ship pyproject.toml and uv.lock at the package root — there is nothing else to declare. See Dependencies below.

Unknown fields are silently ignored. Inventing a table such as [capabilities] or [metadata] does not fail validation — it simply does nothing, and nothing at runtime will ever read it. If a capability is not in this document, declaring it does not create it.

Everything above lives in the manifest root or in the table shown. manager and dependency_mode belong to [environment]; putting them at the top level is the same as not declaring them at all.

[environment] must declare:

manager = "agotalk"
dependency_mode = "none"   # or "locked" — see Dependencies

[state].remembers_progress must be boolean.

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

kind = "application" requires [entry]:

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.

The manifest has one [entry]: it launches one default application. A package may contain additional apps under agapps/, but this file contract does not make them appear in an app dock or launch them automatically. The role's instructions must name any additional page it opens.

A local static_browser page that needs structured agent interaction may use the separate Page Agent API runtime contract. Do not add Page Agent API methods to this manifest.

Dependencies

A package declares whether it needs third-party libraries. Agotalk installs them; your package never installs anything itself.

Two modes

# Conversation-only role, or a purely static application.
[environment]
manager = "agotalk"
dependency_mode = "none"
# Package ships a Python project that needs third-party libraries.
[environment]
manager = "agotalk"
dependency_mode = "locked"

For locked, put pyproject.toml and uv.lock at the package root. Agotalk finds them there; you do not name them in the manifest.

Only uv.lock (Python / uv) is supported today. Node lockfiles are not yet — a package that ships one instead is rejected.

Three hard rules

  1. A missing lockfile is a package bug, not a user-machine problem. If your package needs

numpy, declare it in the lockfile. Do not write instructions that assume the user already has it, and do not degrade silently when it is absent.

  1. Never teach the agent to install dependencies. No pip install, no pip3 install,

no uv pip install, no global npm installs — not in role.md, not in a skill, not in a setup script. Agotalk prepares the declared dependencies before your code runs. An agent that hand-installs is polluting the user's machine, and the publisher will warn on these patterns.

  1. Never write outside the package. Not ~/.claude/, not ~/.local/, not system

directories. If your code needs to find its own files, use the AGOTALK_ROLE_ROOT environment variable (below) — hard-coded home-directory paths are the single most common way a package "works on my machine" and breaks everywhere else.

Prove it runs: contents/bin/setup-check

A locked package must ship an executable at contents/bin/setup-check. Publishing runs it, in a clean environment, on the venv it just built from your lockfile.

exit 0        this package can do its job      → published
exit non-zero this package is broken           → rejected

That is the whole contract. Nothing else about the script is inspected — its stdout is handed back to you verbatim and is never parsed.

Why this exists. A lockfile proves your dependencies install. It does not prove your package works. Real packages have failed at publish-clean time for reasons no manifest field could have caught: a bundled ffmpeg without the codec the code asks for, a font the package assumed was on the machine, a script reaching into ~/.claude/. Those are not one missing field — they are open-ended assumptions about the environment, and an author usually does not know which ones they made. So instead of asking you to declare them, we run your package once and see.

Write it as the check you would run yourself before saying "it works":

#!/usr/bin/env bash
set -uo pipefail
python3 -c "import pandas, numpy" || exit 1        # deps really import
your-tool --version >/dev/null 2>&1 || exit 1      # bundled binaries really run
exit 0

Never exit non-zero because an API key is missing. The publishing machine holds none of your users' keys, so a key check there fails on a perfectly good package. Keys are between your package and its user — ship one, leave it blank, read it from the environment, whatever suits you. Agotalk does not manage them and the gate does not judge them. (It will warn — without blocking — if it spots what looks like a real key committed inside the package. Publishing sends your package to strangers; that bill would be yours.)

What the script can count on: the freshly built venv is first on PATH, AGOTALK_ROLE_ROOT points at your package root, and HOME is an empty scratch directory — so anything your package expects to find in a home directory will fail here, on purpose.

Setting up dependencies

One command. It builds your lockfile with the exact uv and Python your users will run, so what resolves here is what installs there:

git clone https://github.com/42atom/agotalk-roles.git   # if you do not have it yet
cd agotalk-roles && npm install
npm run role:env-init -- --package roles/<role-id> --add pandas --add numpy

Your package lives under roles/<role-id>/ in that checkout — that is also where npm run role:package and npm run role:publish below expect to find it.

It writes pyproject.toml and uv.lock at your package root and sets dependency_mode = "locked" in your manifest. Nothing else is yours to write.

Agotalk must be installed under /Applications — that is where the uv and Python come from. Without it the command refuses rather than using whatever is on your PATH.

It will not overwrite existing files. To change dependencies later, edit pyproject.toml and run uv lock with Agotalk's uv.

Never write or edit uv.lock by hand, and never have an AI write one from memory. The publisher verifies the lockfile is real (ROLE_PACKAGE_LOCKFILE_MALFORMED, ROLE_PACKAGE_LOCKFILE_HASH_INVALID); a hand-written lockfile would not install on the user's machine even if it looked plausible.

npm run role:publish refuses a locked package whose dependencies do not actually install: before shipping, it runs the real uv sync your users' machines will run. npm run role:package skips that step so local iteration stays fast and offline — which means a package can pass role:package and still be broken. Run the publish dry-run before you believe a locked package works.

The Python you get

Agotalk provides Python 3.11. Keep your requires-python compatible with it:

[project]
requires-python = ">=3.11"

A lockfile resolved against an interpreter Agotalk does not provide fails loudly on the user's machine. Agotalk never downloads a second Python to satisfy a package.

Finding your own files: AGOTALK_ROLE_ROOT

When a command runs for a thread bound to your role, Agotalk sets:

VariableMeaning
AGOTALK_ROLE_ROOTAbsolute path to your package root
VIRTUAL_ENVThe prepared Python environment (only when it is ready)

Your package's contents/bin/ is also placed on PATH. Resolve your own assets from AGOTALK_ROLE_ROOT, never from a hard-coded home directory:

import os, pathlib
role_root = pathlib.Path(os.environ["AGOTALK_ROLE_ROOT"])
template = role_root / "contents" / "templates" / "poster.json"
node "$AGOTALK_ROLE_ROOT/contents/bin/compose.js" "$@"

Where dependencies live

Outside your package, keyed by role id, and removed with the app:

~/.config/agotalk/
├── roles/<role-id>/        your package (read-only to the dependency layer)
└── role-envs/<role-id>/    the prepared environment

Your package directory is overwritten wholesale on update, so nothing generated at runtime may live inside it.

Localized Display Copy

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

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

Rules:

Assets

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

Preview source images should be PNG:

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:

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:

.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:

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:

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.

Common Rejections

Every row below was produced by a real agent building a package from this document. The publisher stops at the first problem, so fix them top-down.

Publisher saysYou wroteFix
schema_version must equal 1schema_version = "1.0"The integer 1, unquoted
id must be a non-empty trimmed stringrole_id = "..."The key is id
license must equal "free"license = "MIT"license = "free"
environment.manager must equal "agotalk"manager at the top levelPut it under [environment]
state.remembers_progress must be booleanno [state] tableAdd [state] with a boolean remembers_progress
uv.lock requires pyproject.toml in the same directorylockfile aloneShip both, side by side
Role package subtree has uncommitted changesuncommitted editsCommit the package first
requires an executable contents/bin/setup-checka locked package with no self-checkAdd one; it only has to exit 0 when the package works
setup-check exists but is not executableforgot the permission bitchmod +x contents/bin/setup-check
setup-check exited non-zero in a clean environmentthe package genuinely cannot run thereRead the output — it is your own script's. A common cause is failing on a missing API key: don't, the publisher has none

Minimal Self-Check

Before publish:

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.

For a dependency_mode = "locked" package, the first command also reports what it found:

{ "dependencyMode": "locked", "lockfiles": ["uv.lock"], "warnings": [] }

Check both:

~/.claude, global npm install) — fix the package, do not ignore it.

Then verify the lockfile itself resolves against the Python Agotalk ships:

cd contents/compose-engine && uv lock --check