# Piece YAML Schema This is the reference for the piece YAML format consumed by `src/engine/piece-runner.ts` (`loadPiece` / `validatePieceDef`) and the `/api/pieces` HTTP layer (`src/bridge/pieces-api.ts` `validatePiece`). Field names are snake_case in the YAML; the engine maps them to camelCase internally (see `Movement` in `src/engine/agent-loop.ts`). ## Top-level | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | lowercase `[a-z0-9-]+` | | `description` | string | yes | shown in the piece classifier | | `max_movements` | positive integer | yes | hard cap on movement count per run | | `initial_movement` | string | yes | must reference a `movements[].name` | | `triggers.keywords` | string[] | no | classifier hint only | | `required_mcp` | string[] | no | `[a-z0-9_-]{1,64}` server slugs | | `model` | string | no | preferred LLM model | | `movements` | Movement[] | yes | non-empty array | ## Movement | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | unique within the piece | | `edit` | boolean | yes | when true, Write/Edit are exposed | | `persona` | string | yes | system-prompt persona | | `instruction` | string | yes | the movement's task description | | `allowed_tools` | string[] | yes | tool names; `'mcp__*'` wildcard allowed | | `allowed_commands` | string[] | no | Bash command allowlist (overrides default) | | `allowed_ssh_connections` | string[] | conditional | see below | | `rules` | Rule[] | yes | transition rules; may be empty | | `default_next` | string | no | engine-internal fallback (sentinel-friendly) | | `max_consecutive_revisits` | number | no | loop-detection threshold override | ## `allowed_ssh_connections` Per-movement SSH connection allowlist (Phase 4 of the SSH tool integration | Value | Meaning | |-------|---------| | `undefined` (field omitted) | SSH tools reject with `no_allowed_connections_declared`. | | `[]` (empty array) | SSH tools reject with `no_allowed_connections_declared`. The empty form is preferred over omission when the movement intentionally denies all connections (intent is explicit). | | `['', ...]` | Only listed connection IDs may be passed to SSH tools. | | `['*']` | Any registered connection may be passed. Still subject to ownership and grant checks (defense in depth). Use sparingly — typically only `ssh-ops`-style pieces. | **Required**: If a movement's `allowed_tools` contains any of `SshExec`, `SshUpload`, or `SshDownload`, then `allowed_ssh_connections` MUST be present. `validatePieceDef` and `validatePiece` both reject pieces that omit it for SSH-using movements. **Format**: each entry must be `'*'` or a lowercase hex/hyphen id with 8+ characters (loose match against `randomUUID()` output). Example: ```yaml movements: - name: ops edit: false persona: ops-operator instruction: Run health checks on production hosts. allowed_tools: [SshExec, Read] allowed_ssh_connections: - 6f9619ff-8b86-d011-b42d-00c04fc964ff - 7a8b9cde-1234-4567-89ab-cdef12345678 rules: - condition: all checks pass next: COMPLETE ``` ## Rule ```yaml - condition: next: ``` `rules[].next` may NOT use the reserved terminal sentinels `COMPLETE` / `ABORT` / `ASK` — those are reachable only through the `complete` tool (status: `success` / `aborted` / `needs_user_input`). `default_next` does accept the terminal sentinels because it is an engine-internal fallback (context overflow, ASK limit, SpawnSubTask unavailable). ## Validation paths Two validators implement the same rules: - `validatePieceDef` in `src/engine/piece-runner.ts` — runs on every `loadPiece` (file-backed) and `CreatePiece` (runtime). - `validatePiece` in `src/bridge/pieces-api.ts` — runs on `PUT /api/pieces/:name` (UI editor). Both must stay in sync. When changing the schema, update both and add test coverage in `src/engine/piece-runner.test.ts` and `src/bridge/pieces-api.test.ts`.