# Smyrg repository image contract

This document describes the current first release. Schema: https://smyrg.com/schemas/image.schema.json. Discovery: https://smyrg.com/llms.txt.

## Files and manifest

Add these files in a pull request, then merge them into the default branch after CI passes:

```yaml
# .smyrg/image.yml
# yaml-language-server: $schema=https://smyrg.com/schemas/image.schema.json
version: 1
setup: .smyrg/setup.sh
files:
  - source: .smyrg/files/tool-config.json
    destination: tool-config.json
  - source: .smyrg/files/start-service.sh
    destination: bin/start-service.sh
    executable: true
```

`version` must be the number `1`. `setup` is required. `files` is optional, defaults to an empty list, and permits at most 100 items. Each item requires `source` and `destination`; `executable` is optional, boolean, and defaults to false. Unknown properties are rejected at every level.

Paths are relative and at most 240 characters. Each path component contains only ASCII letters, digits, `_`, `-`, or `.`. Empty, `.` and `..` components are forbidden. Source paths must start with `.smyrg/`. Only regular Git files are accepted. Symlinks and submodules are rejected, including in parent paths. Destinations cannot equal each other or contain one another. `.smyrg-setup.sh` is reserved for the setup script; destinations cannot overlap it.

The manifest is at most 8,192 bytes. Duplicate YAML keys, YAML aliases, and invalid YAML are rejected. The total source bytes, including the manifest, must also be at most 65,536 bytes. The complete generated Boat setup script must fit 65,536 bytes. This includes base64-encoded files, JSON, decoder code, and runner preparation. Thus 64 KB of source files does **not** fit. Schema validation cannot prove file sizes, Git file modes, alias rules, or destination overlap; the server also validates these.

## Local validation

Download the standalone checker and run it with Node.js 22 or later:

```bash
curl --fail --location https://smyrg.com/tools/check-image.mjs --output /tmp/smyrg-check-image.mjs
node /tmp/smyrg-check-image.mjs . --json
```

The checker reads local files. It does not execute setup, use credentials, or contact GitHub or Boat. It uses the same manifest and context rules as the service. Check its exit status. JSON output contains `ok`, and either file/digest/budget information or structured errors. Budget includes generated script bytes, source bytes, and remaining bytes. Local validation does not prove repository access, that the selected commit is the current default-branch head, or that setup succeeds in a VM. Use the PR workflow below to test the image before merge. Default-branch changes build and activate automatically.

For a Smyrg source checkout, use `bun run image:validate -- . --json`. The published standalone checker does not need a Smyrg checkout or installed packages.

## Setup environment

Smyrg reads declared files from one immutable Git commit. It does not check out the repository in the build VM. It places the setup script at `/opt/smyrg-build/context/.smyrg-setup.sh`, places declared files under `/opt/smyrg-build/context/<destination>`, changes to that directory, and runs `bash .smyrg-setup.sh` as the normal Boat user with sudo access. Files have mode 0644, or 0755 when executable is true. Setup runs once per image build.

The script is arbitrary Bash. It can install OS packages, Python and Node toolchains, copy configuration files, download public binaries, create users, and install system services. Use noninteractive commands and fail on errors. Pin versions and verify downloaded checksums when reproducibility matters. Network downloads are allowed; they are not a secret delivery feature.

For example:

```bash
#!/usr/bin/env bash
set -euo pipefail
sudo apt-get update
sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-venv nodejs npm jq
sudo install -D -m 0644 tool-config.json /etc/my-tool/config.json
python3 --version
node --version
```

This example uses versions from the base VM's configured distribution repositories. It does not promise a particular Python or Node release. Use a version-pinned vendor package or a public release with a checked SHA-256 for a fixed release. Do not assume a tool is installed merely because it exists on GitHub-hosted runners.

Install tools in locations already on the runner's PATH, such as `/usr/local/bin`, or use explicit paths in workflow steps. An `export PATH=...` in setup does not persist into later jobs. `.bashrc` and `/etc/profile` are not a reliable way to configure non-login runner processes. For additional paths, set workflow `env.PATH` carefully or write to `$GITHUB_PATH` in a workflow step. A systemd unit can be installed and enabled during setup, but verify that it starts in a fresh image VM; an already running build process does not prove that it will restart after cloning.

Smyrg installs its checksum-pinned GitHub runner after the repository script. Do not register a runner or write its credentials. The build and verification VMs get no GitHub installation token, customer Boat key, JIT runner configuration, or deployment secrets. Do not place passwords, private package tokens, `.env` files, or other secrets in scripts or declared files: snapshot contents persist. Build secrets are unsupported.

The script has root-capable access. Functional verification is not a security audit of malicious setup code. Anyone permitted to change image inputs on the default branch can change the environment used by later jobs. Use repository review and branch controls appropriate to that trust. Images are scoped to one account and repository and are not shared by Smyrg.

## Build, verify, activate

1. Commit the configuration to the repository's default branch. The GitHub App must have Contents read permission and access to this repository. The account needs its connected Boat credential.
2. Smyrg detects changed image inputs and builds from the current default-branch head. Push events request a check; periodic recovery checks also find missed changes. Pull requests and other branches do not promote images. Branch protection is not required by the service.
3. Smyrg starts a fresh small build VM, saves a unique named snapshot, and verifies it with separate fresh small and medium VMs. Boat charges build and verification compute and snapshot storage separately from the Smyrg subscription.
4. Smyrg activates the verified version only if it still matches the latest desired image inputs and account scope. A stale build cannot replace a newer requested image. A failed or unresolved build keeps the last good image active.
5. Use `runs-on: smyrg-small` or `runs-on: smyrg-medium`. New jobs select that repository's active image. Each job gets a fresh VM. Existing jobs keep their selected version. Smyrg never saves a job VM as an image.

No Images-page action is needed for normal configuration changes. The Images page shows build state and provides removal controls for current organization owners. To roll back, revert the image inputs on the default branch. Smyrg selects or builds the matching verified image. The existing manual rebuild API is available for explicit recovery. It accepts the current default-branch name or its exact head SHA; other refs and older commits are rejected. Failed input is not retried automatically without a new context or credential. Remove a version only when it is inactive and no job or unresolved VM operation needs it. Replacing the Boat key requires a new image tied to the current credential.

There can be one active or unresolved build per account and ten non-deleted versions per account. At the version limit, automatic builds can retire the oldest eligible ready or failed version. Active, previous, desired, and unresolved versions are retained. If no version can be removed safely, the new build waits. States are `requested`, `building`, `saving`, `verifying`, `ready`, `failed`, `recovery_required`, `deleting`, and `deleted`. A failed build leaves the active image unchanged. An uncertain provider response is not treated as success or proof that a VM does not exist. If recovery is required, check Boat and the recorded error before requesting another operation.

## Test the snapshot in a pull request

Users edit `.smyrg/image.yml`, the setup script, and declared files to add or remove tools. Keep normal jobs simple:

```yaml
runs-on: smyrg-small
```

Use `smyrg-medium` for a medium runner. Normal CI, including code-only PRs, uses the latest ready default-branch image. A newer pending or failed build does not replace that image. Each job pins its selected image before VM creation.

When a trusted same-repository PR changes `.smyrg`, Smyrg adds a workflow-routing commit to that PR. It changes supported `runs-on` values only; it does not change the user's `.smyrg` files or test steps. The generated expression selects an isolated run/attempt label for that PR and the normal label after merge. Users do not need to write pool names or run IDs. An expression can remain in the merged YAML; its fallback is the normal runner label.

The new PR head builds and tests the candidate snapshot. A workflow on the original head may already have started before Smyrg receives the event; that run is not candidate-image proof. Check CI on the new head. Unsupported runner expressions or workflow forms need a routing correction; Smyrg must not guess how to rewrite them.

Candidate images require current write, maintain, or admin access. Fork PRs and authors without write access cannot build them. The App needs Contents write and Workflows write for the route commit, plus Pull requests read, Actions read, and Commit statuses write. Source and access checks are repeated before publication, and an atomic head check prevents overwriting a concurrent user commit.

Smyrg holds candidate jobs until their image is ready. Candidate runners have no shared normal label. A stale or closed PR cannot select the candidate. PR candidates never replace the default-branch image. CI tests the installed tools; no separate verification script is required.

`smyrg/image` reports the image build result and links to protected logs. It does not mean that the repository's CI tests passed; those jobs report their own results. Logs are limited to 64 KB per build VM and kept for seven days. Retry a failed build in Images after cleanup is complete. An uncertain provider operation must be resolved first.

Boat excludes some cache directories from snapshots. Put required tools outside those directories. For example, install Playwright browsers under `/opt/ms-playwright` and set `PLAYWRIGHT_BROWSERS_PATH: /opt/ms-playwright` in browser-test jobs. A successful tool install in the seed VM does not prove that the tool is present after snapshot restore.

## Existing account API

The workspace uses the following routes under the current signed-in session:

- `GET /api/images`: repositories, versions, and limits.
- `POST /api/images/build`: JSON `{ "repositoryId": 123, "ref": "main" }`; ref is optional.
- `POST /api/images/<version-id>/activate`: existing manual endpoint; source reconciliation can replace this selection. Use a default-branch configuration revert for a lasting rollback.
- `GET /api/images/<version-id>/logs`: protected build output and status delivery results.
- `POST /api/images/<version-id>/retry`: retry a settled failed build after current source checks.
- `DELETE /api/images/<version-id>`: retire an unused version.

These are authenticated workspace routes, not an API-key interface. Mutations require the existing session, same-origin checks, and CSRF protection. Use the workspace controls; do not invent a bearer key, bypass CSRF, or store browser cookies in a repository. An unauthenticated agent can prepare and validate configuration. Commit and push require the user’s authorization; the service handles automatic build and activation after the change reaches the default branch.

## Provider limits

Smyrg gives each version a unique Boat snapshot name and pins jobs to that version. Boat's documented boot API selects by name, so the Boat account owner can still replace a name outside Smyrg. Do not claim provider-enforced immutable disk identity. `noEnv` and VM isolation depend on Boat's provider contract. Named snapshot deletion is separate from tracked VM deletion and does not prove a physical erasure time. Boat can accept a VM delete and stop compute billing while background purge waits for upload URLs to expire, up to six hours after the last upload according to its data-retention documentation. Smyrg tracks that receipt until completion.
