# Keldrin AI game adapter development handoff

Version of this handoff: November 4, 2026. It reflects the public Keldrin Game
Adapter SDK 0.9.140 Developer Preview and Keldrin 0.9.140. Facts here come
from the public developer documentation at https://keldrin.ca/developers/game-adapters,
the SDK bundle's own documents, and the SDK's example adapters. Where something was
unclear, this file says so and marks it **Open question for maintainers**.

---

## 1. Purpose and how to use this file

### Who hands this to whom

A person (or an AI acting for that person) gives this single file to an AI
assistant that has no other Keldrin context. That AI, called **the authoring AI**
below, writes a new Keldrin game adapter for one game and hands back a **return
bundle** (defined in section 9). Keldrin maintainers then review, sign, test and,
if it passes, publish the adapter. The person who gave you this file is called
**the human operator** below.

### What the authoring AI must produce

One return bundle for one game, containing:

1. an unsigned adapter payload (`<package-id>-payload-v2.json`) written only in
   the supported declarative JSON vocabulary;
2. any declared assets (for example `assets/icon.png`) that may be redistributed;
3. setup fixtures (`setup-fixtures.json`) bound to the exact payload bytes;
4. an evidence ledger (`evidence.json`) bound to the exact payload bytes, with
   all 22 rows honestly stated;
5. a `README.md` explaining the adapter, its sources and its limits;
6. the SDK command outputs ("receipts") you actually ran;
7. a `HANDOFF-RETURN.md` checklist with open questions.

### What the authoring AI must never do

- **Never launch a real game server against real worlds**, real player data or a
  production machine unless the human operator explicitly arranges it and tells
  you which disposable machine, folder and data to use.
- **Never sign a package.** Only Keldrin maintainers sign. The SDK cannot sign and
  supplies no keys; do not look for, request or invent signing material.
- **Never edit Keldrin itself** (the Keldrin app, Keldrin Service, Local Web,
  Remote Portal or the SDK). An adapter is data. If the game needs behavior that
  the existing vocabulary cannot express, report the gap; do not work around it.
- Never include credentials, tokens, passwords, real IP addresses, real player
  names, private file paths, saved worlds, game executables or vendor files you
  may not redistribute.
- Never invent facts: executables, arguments, ports, log lines, hashes, file
  sizes or save locations must come from official sources or from measurements
  the human operator arranged. A guessed SHA-256 is worse than a missing one.
- Never mark something Verified because it was declared, linted or synthetically
  tested (section 8).
- Never accept a game's EULA or licence on anyone's behalf.
- Never download or run game server files unless the human operator approves it;
  if measurements need a download (for example an exact SHA-256), ask first.

If the game's dedicated server is **not** distributed through Steam/SteamCMD,
say so to the human operator explicitly before you continue, and name the actual
official download source. A Steam store page for the game does not mean its
dedicated server is on SteamCMD.

### Known open questions in this handoff

These are collected here and repeated where they matter:

1. Use the newest SDK listed on the developer page (0.9.140 at the time of
   writing). A later SDK may change field sets; its README wins.
2. Every field in this file works on Keldrin 0.9.140. Set your package's
   `MinimumKeldrinVersion` to `0.9.140` and pass `--host-version 0.9.140`.
3. Who chooses `PublisherId`, `Sequence` and `PublishedAtUtc` for a third-party
   submission is not documented. All examples use `"keldrin-official"`.
4. The exact identifier grammar for manifest `Id` fields is enforced by
   `author-lint` but not written down for manifests. Fixture IDs have a
   documented rule (section 5.14).
5. The Minecraft Java example's README may still describe adapter 1.0.0; the
   payload quoted here is 1.0.1, sequence 3 (section 7.2).
6. The Icarus example folder has no evidence JSON.
7. The temporary password format is described two ways (section 5.5).
8. The game-named Stop kinds are not explicitly classified as restricted.
9. The format and naming of the quickstart's report directory are not documented
   here; include it only if it fits the bundle rules (section 9).
10. The archive bounds say at most 4 archive entries and 3 declared files; how the
    manifest and signature count toward those is not stated.

---

## 2. Mental model

### What a game adapter package is

A Keldrin game adapter package is a signed `.keldrin-game` ZIP. It contains the
adapter manifest (`manifest-v2.json`, the signed envelope around your payload)
plus a small number of declared asset files such as an icon. Your work is the
**payload**: one JSON object that declares facts about one game's dedicated
server. Adapters are **not executable plugins**. No package-supplied scripts,
arbitrary installer commands or open console channels are allowed. Every
behavior Keldrin performs is already compiled into the Keldrin release; the
adapter only selects and configures that behavior.

The payload declares:

- installation identity (Steam app ID or a pinned official HTTPS download);
- the executable allowlist and launch arguments;
- settings with defaults and limits, and how they are written into the game's
  own config files;
- ports, Windows Firewall rules and external-check probes;
- readiness evidence ("Game Ready");
- player evidence (counts, names, join/leave);
- approved save-data rules (backup, restore, import, export);
- presentation (name, description, colour, icon, log file shown to the operator).

### What Keldrin does with an installed adapter

The Keldrin Service is authoritative. The Keldrin app, Local Web, Remote Portal
and Discord all consume the same installed, verified contract. With an installed
adapter, Keldrin:

- **installs** the server (SteamCMD, or a pinned HTTPS ZIP with optional raw
  files) and checks declared prerequisites such as the Visual C++ runtime;
- presents **settings** generated from your declarations and writes them into the
  game's native config documents, preserving unknown fields;
- **launches** the declared executable with the declared arguments and tracks the
  exact process identity;
- reports **readiness** only from the declared current-run evidence;
- reports **players** only from the declared player evidence; unknown is shown
  as unknown, never as zero;
- runs stopped-server **backups/restore/import/export** with a safety backup,
  rollback and ten-backup retention, plus a nightly backup of servers observed
  running;
- creates executable-scoped **Windows Firewall** rules for the declared ports,
  maps router forwarding through **Managed Ports**, and runs **External** checks
  through Remote Portal;
- handles **game-binary updates** and **adapter updates**, preserving the
  operator's profile, settings and worlds.

Every game uses the same five health badges, in this order: **External, Game
Ready, Process, Managed Ports, Windows**. Unknown evidence shows `?`; not
applicable shows a dash. There is no LAN badge. Router forwarding never makes
External pass.

The default Stop is process-tree termination (`KillProcessTree`). It does **not**
guarantee a final in-memory save. Backups preserve only files the game already
wrote to disk.

### Unsigned candidate vs signed official package

| | Unsigned candidate (what you write) | Signed official package |
| --- | --- | --- |
| Who makes it | Any author | Keldrin maintainers only |
| Form | Payload JSON plus assets | `.keldrin-game` ZIP with signed manifest |
| Can it be installed? | No. Unsigned input cannot become an installed adapter. | Yes, once admitted through a compatible signed catalog |
| SDK checks | `author-lint`, `canonical-payload`, `test-contract --manifest` | `verify-package`, `test-contract --archive` |
| Assurance | Author claims only | Signature verified and compiled; still not game certification |

The SDK checks declarations. It does not install, sign, publish, launch a game or
certify gameplay. `AcceptanceVerifiedByTool` and `ReleaseCertified` are always
false.

A newer Gallery entry never silently replaces an installed adapter. Never edit
signed compatibility bounds or reset accepted catalog sequences.

---

## 3. Prerequisites

- **Windows.** The tooling is checked on Windows. Tooling checks on Windows do
  not establish a supported Linux Keldrin host.
- **.NET 10 runtime** (Microsoft .NET 10 runtime with a compatible 10.0 patch).
  The prebuilt CLI is framework-dependent; no runtime is bundled.
- **PowerShell 7** (`pwsh`) for the bundle's integrity and quickstart scripts.
  Use PowerShell 7 for redirecting CLI output too, so receipt files are UTF-8.
- Optional source rebuild only: .NET SDK 10.0.302 or the compatible latest patch
  recorded in the bundle. You do not need this to author an adapter.
- **The SDK ZIP.** Get the newest SDK listed on
  https://keldrin.ca/developers/game-adapters. At the time of writing that is
  https://keldrin.ca/keldrin/sdk/0.9.140/Keldrin-GameAdapterSdk-0.9.140-DeveloperPreview.zip
  (1,059,028 bytes, SHA-256
  `bcf934690919f46b9224f5d2673a8b7c1d7515a014cebee26e14bfece5674a5d`).
  If the page lists a newer one, use it and read its README for changes. Compare
  the downloaded ZIP's SHA-256 with the value published on that page before use.
- **Target version.** Set `MinimumKeldrinVersion` to `0.9.140` and pass
  `--host-version 0.9.140` to every command that accepts it. Every field in this
  file works on Keldrin 0.9.140.
- **Publisher keys.** `verify-package` trusts both official Keldrin publisher
  keys (the current host key and the original spare). You normally do not need
  `verify-package` at all.
- **Stop policies.** Packages may declare save-aware stop kinds bound to one
  game's exact official launch; see the Enshrouded stop-policy example
  (`examples/enshrouded/127`).

### Install and run the CLI

Extract the ZIP into a fresh, empty folder. From that folder:

```powershell
pwsh -NoProfile -File ./Test-BundleManifest.ps1
pwsh -NoProfile -File ./Run-Quickstart.ps1 -OutputDirectory ../sdk-reports
```

Choose a new report directory **outside** the extracted bundle. Do not build
inside the extraction; the integrity check rejects generated files.

Run commands with the prebuilt DLL in the bundle's `bin` folder:

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll help
dotnet ./bin/Keldrin.GameAdapterSdk.dll describe-contract
```

Read these first, in the extracted bundle: `README.txt`, `KNOWN_LIMITATIONS.txt`,
`docs/READ_THIS_FIRST.md`, then `docs/GAME_ADAPTER_SDK.md` and
`docs/NEW_GAME_INGESTION_GUIDE.md`. The bundle README takes precedence over dated
sections in the copied reference documents. The bundle also contains the
generated `docs/contract-<version>.json` and `docs/author-schema-<version>.json`,
and the examples under `examples/<game>/<version>/`.

---

## 4. Step-by-step workflow

From "I want game X" to the return bundle.

### 4.1 Research the dedicated server facts

Establish every fact from the game's official documentation, official download
metadata, shipped configuration files or measurements arranged by the human
operator. **Do not clone another game's definition and assume its behavior.** An
example shows syntax, not evidence that another game behaves the same way.

Record these in your research notes (they become `research/sources.md` in the
bundle, with URLs and the date you checked):

| Topic | What to establish |
| --- | --- |
| Distribution | Steam dedicated-server App ID (not the game's client store ID), or the exact official HTTPS download: URL, exact byte length, SHA-256, version, and for ZIPs every file you map (source path, destination, length, SHA-256). Public DNS, no redirects. Install directory name. Estimated disk requirement. |
| Executables | The executable launched; whether it is a direct executable or a launcher that hands off to a child process; every managed and auxiliary process path; working directory. |
| Arguments | Exact command-line arguments and how settings/ports appear in them. |
| Ports and protocols | Every port, TCP/UDP, adjacent ranges, query ports, which port clients use, which endpoint can safely answer a local query, and any relay/invite-code/NAT mode and the setting that selects it. |
| Firewall | Which executable owns each listening port; protocol, range, network profile (Domain/Private), and mode conditions. |
| Config files | The game's own config document format (JSON, sectioned INI, flat `key=value`), path, keys for each setting, whether the game must create the file on first run, and what unknown fields must be preserved. |
| Settings | Editable settings, defaults, limits, character rules, which are secrets (join/admin passwords), and whether a server/world name setting exists. Do not invent a password setting the game does not have. |
| Save locations | Save root(s), which files make up a world, whether one folder holds several worlds, include/exclude rules, whether the server must be stopped, how to validate a world. |
| Logs | The game's own file logger: the switch or config that enables it, directory, filename and rotation naming, encoding, line framing, flush behavior, retention, and whether it writes secrets. Prefer the game's own log files over capturing console output. |
| Readiness | The exact log line(s) or protocol response that means "ready for players" in the current run, and how a previous run's log is renamed, truncated or replaced at startup. |
| Players | Current player count source and name source (Steam A2S, GameSpy4, log events), exact join/leave line grammar, reconnect behavior, and whether chat can imitate an event line. |
| Stop behavior | What happens on process termination; whether the game saves on its own. Keldrin's ordinary Stop is process-tree termination with no final-save guarantee. |
| Prerequisites | Required Windows libraries (exact identity/version), official publisher. Only the supported kinds can be declared (section 5.13). |
| Licence/EULA | Whether the server requires explicit EULA acceptance; the official EULA URL. |
| Artwork | Official artwork source and whether it may be redistributed. If unsure, omit the icon and say so. |

If a fact is missing, stop and ask the human operator rather than guessing. A
process, a readiness record, a firewall rule, a router rule, an outside-LAN
response and a gameplay join are **separate facts**.

**Does it fit?** Only reuse existing mechanisms. If the game needs a new save
format, protocol, credential algorithm, console command channel, controller, or
any "game-specific provider", that requires a Keldrin update; record it as a gap
in `HANDOFF-RETURN.md`. Do not select another game's named provider (section 5).

### 4.2 Write the payload

1. Start from the example closest to your game's distribution (section 7):
   Icarus for a SteamCMD game, Minecraft Java for a pinned HTTPS download. Copy
   the **structure**, then replace every value with your researched facts.
2. Choose `MinimumKeldrinVersion` at least as high as the highest feature floor
   you use (section 5). Leave `MaximumKeldrinVersion` as `null` unless
   maintainers say otherwise.
3. Use only the generic primitives: `PortableDirectory` saves,
   `KillProcessTree` Stop, `ProbeSet` or `EventLog` players, `SteamA2s`,
   `GameSpy4`, `LogFile`/`TextPattern`, `UnrealReadyLogMarker` and the other
   generic probes. Set `SatelliteTransferSafety` to `null`.
4. Password settings: `Kind: "Password"`, `DefaultKind: "RandomPassword"`,
   `DefaultValue: null`, `MaximumLength` at least 12.
5. Name settings (server or world name): `DefaultKind: "RandomName"`,
   `DefaultValue: null`, `MaximumLength` at least 12, only where
   the game really has such a setting.
6. If readiness evidence is not yet known, you may use the honest first-test
   state (`Readiness.UnconfirmedOnly: true`, section 5.10). V05
   then stays Not Verified.
7. Save as UTF-8 **without** a byte-order mark. The SHA-256 binding covers exact
   bytes including any BOM.
8. Run `author-lint` (4.4) and fix every refusal before writing fixtures.

### 4.3 Write setup fixtures

Fixtures test the Setup rules (what a new server profile accepts) against your
exact payload bytes. Write them **after** the payload is final, because they
bind to its SHA-256.

1. Get the payload digest: run `author-lint` and copy `InputSha256`, or run
   `(Get-FileHash -Algorithm SHA256 <file>).Hash.ToLowerInvariant()`. It must be
   lowercase hex.
2. Write cases (section 5.14 has the format):
   - `default-setup` with `"Setup": {}` (tests defaults);
   - changed ports;
   - each setting at its limit and just past it;
   - port collisions (`DistinctFrom`);
   - unsafe values (path traversal, device names such as `CON`, absolute paths)
     for folder-name settings;
   - an unknown setting ID;
   - for an explicit-acceptance setting: missing, `false`, non-canonical `TRUE`
     must all be refused; `true` accepted.
3. For accepted cases, state `ExpectedActivePortIds`,
   `ExpectedActiveFirewallRuleIds` and `ExpectedArgumentCount`. Rejection cases
   must not state those.
4. Run `test-contract` until it exits 0.

If you edit the payload afterward, the digest changes: rebind fixtures and
evidence, and rerun everything.

### 4.4 Run each SDK command

Run from the extracted SDK folder in PowerShell 7. Use an explicit
`--host-version` equal to your target host (the public Keldrin version listed on
the developer page; 0.9.140 at the time of writing). Save every output as a
receipt.

```powershell
$b = "../return/<package-id>/<package-version>"
dotnet ./bin/Keldrin.GameAdapterSdk.dll describe-contract > "$b/receipts/describe-contract.json"
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-schema    > "$b/receipts/author-schema.json"
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-lint --manifest "$b/<package-id>-payload-v2.json" --host-version 0.9.140 > "$b/receipts/author-lint.json"
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-lint --manifest "$b/<package-id>-payload-v2.json" --host-version 0.9.140 --evidence "$b/evidence.json" > "$b/receipts/author-lint-with-evidence.json"
dotnet ./bin/Keldrin.GameAdapterSdk.dll canonical-payload --manifest "$b/<package-id>-payload-v2.json" > "$b/receipts/canonical-payload.json"
dotnet ./bin/Keldrin.GameAdapterSdk.dll test-contract --manifest "$b/<package-id>-payload-v2.json" --fixtures "$b/setup-fixtures.json" --host-version 0.9.140 > "$b/receipts/test-contract.json"
```

After each command, record the exit code (`$LASTEXITCODE`) in
`receipts/commands.txt` together with the exact command line, the SDK version and
the SDK ZIP SHA-256. `verify-package` needs a signed archive, which you will not
have; record it as "not run: no signed archive".

Section 6 explains each command and how to read its output.

### 4.5 Collect evidence

Write `evidence.json` (format in section 8) with all 22 rows. For an adapter
written from documentation only, every row is **Not Verified**, with a reason
saying what is declared and what still needs to be observed. Only mark a row
Verified if you have a real receipt of the exact operation, produced in a
setting the human operator arranged, and reference it by a relative path inside
the bundle. Then run `author-lint --evidence` to check the ledger binds and
parses.

---

## 5. Manifest field reference

Sources: the SDK's `author-schema` (structural Draft 2020-12 schema generated
from the actual manifest types), `describe-contract` (vocabulary, bounds and
feature floors) and the SDK documentation. The structural schema is **not** full
semantic validation; `author-lint` is authoritative. Run `describe-contract` from
your SDK to see its exact vocabulary.

General rules:

- Strict JSON: unknown properties, duplicate properties, numeric enum values and
  invalid UTF-8 are refused. Enum values are strings, spelled exactly.
- Maximum payload 256 KiB, JSON depth 24.
- Versions use canonical three-part spelling (`0.9.140`), no leading zeros.
- Timestamps use canonical UTC seconds: `yyyy-MM-ddTHH:mm:ssZ`, for example
  `2026-10-30T05:13:50Z`.
- The examples write every field, using `null`, `[]`, `0` or `false` where
  unused. Some newer optional fields are omitted when false or null
  (`RequiresExplicitAcceptance`, `SelectImportedSubdirectory`, `RawFiles`,
  `RequiredFiles`, `AmbiguousLinePrefixes`, `BinaryString`, `LiveExternalCheck`,
  `NativeFileOutput`, `OutputCapture`, `ConsoleOwner`, `StoreUrl`, `Choices`,
  `ValueSettingId`, `PortOffset`, `ReadyProbeMatch`, `UnconfirmedOnly`,
  `WorldPresentation`, `ExistingObject`). Omitting such a field keeps older
  canonical bytes unchanged. Follow the examples, and let `author-lint` decide.
- A feature's minimum host version is a declaration floor. Your
  `MinimumKeldrinVersion` must be at least the highest floor you use. Listing a
  feature does not promise every combination works on that host.
- Paths are relative. Install files live under the install root
  (`Runtime/SteamCMD/steamapps/common/<install dir>` for SteamCMD,
  `Runtime/Games/<package-id>` for HTTPS ZIP). Per-server data lives under
  `Data/Servers/{profile-id}/...`; `{profile-id}` is the only token shown in the
  examples.
- Enum values named after a specific game (for example `IcarusProspects`,
  `ValheimActiveSession`, `CoreKeeperGameInfo`, `WindRoseSessionLog`,
  `TerrariaOfficial1458`) are **identity-restricted**: they work only for that
  game's official package. Never select them for a new game.

### 5.1 Top-level fields

| Field | Kind | Notes |
| --- | --- | --- |
| `PackageId` | string, required | Lowercase, hyphenated in all examples (`icarus`, `minecraft-java`). |
| `PublisherId` | string, required | All examples use `keldrin-official`. **Open question for maintainers:** what a third-party submission should put here. |
| `PackageVersion` | string, required | Three-part adapter version, for example `1.0.0`. |
| `Sequence` | integer | Monotonic package sequence used by the catalog. Accepted sequences are never reset. **Open question for maintainers:** who assigns it for a new game. SDK docs describe example sequence/timestamp values as prospective author input, not a reserved signed identity. |
| `PublishedAtUtc` | string | Canonical UTC seconds. |
| `MinimumKeldrinVersion` | string, required | At least the highest feature floor used. |
| `MaximumKeldrinVersion` | string or null | Current references use `null` (no upper bound). |
| `Presentation` | object, required | 5.2 |
| `Distribution` | object, required | 5.3 |
| `Capabilities` | object, required | 5.4 |
| `Settings` | array, required | 5.5; at most 32 |
| `Ports` | array, required | 5.6; at most 16 |
| `Launch` | object, required | 5.7 |
| `Process` | object, required | 5.8 |
| `Probes` | array, required | 5.9; at most 64 |
| `Readiness` | object, required | 5.10 |
| `Players` | object, required | 5.11 |
| `SettingsDocuments` | array, required | 5.12; at most 8 |
| `SaveData` | object, required | 5.13 |
| `SatelliteTransferSafety` | object or null | Set `null`. Satellite transfer is parked; its named vocabulary is identity-restricted. |
| `Firewall` | array, required | 5.13; at most 16 |
| `Prerequisites` | array, required | 5.13; at most 8 |
| `Files` | array, required | 5.13; at most 3 declared files |

Package bounds from `describe-contract`: archive at most 1 MiB and 4 entries;
at most 3 declared files, each at most 256 KiB; total uncompressed at most
512 KiB. **Open question for maintainers:** how the manifest counts toward the
4-entry and 3-file limits.

### 5.2 Presentation

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `DisplayName` | string, required | Name shown in the Gallery and on tiles. |
| `Description` | string, required | Short honest description. State limits (for example, no final-save guarantee). |
| `TileColor` | string, required | Canonical hex such as `#ffd36b`. |
| `ConnectionSummary` | string, required | How players connect. |
| `EstimatedRequiredDiskBytes` | integer | Disk estimate. |
| `MaximumInstances` | integer | Examples use `1`. HTTPS ZIP requires exactly one profile per package. |
| `IconPath` | string or null | For example `assets/icon.png`; must match a `Files` entry. Use a PNG at most 32 KiB and at most 512 by 512 pixels, or it will be omitted from tiles. The codec accepts up to 256 KiB, but larger icons do not display. |
| `PrimaryLogRelativePath` | string or null | Log shown to the operator; under the profile data/log root or the exact game install. Do not invent a readiness probe just to show a log. |
| `StoreUrl` | string or null, optional | The game's store page, not the dedicated-server App ID. Steam form `https://store.steampowered.com/app/<client app id>/` (at most 100 characters). Official HTTPS website form, at most 512 characters, no query/fragment/credentials/IP literals. Presentation only. |

### 5.3 Distribution

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `Kind` | enum | `SteamCmd` or `HttpsZip`. |
| `SteamAppId` | integer | Nonzero dedicated-server App ID for `SteamCmd`; `0` for `HttpsZip`. |
| `InstallDirectoryRelativePath` | string, required | `Runtime/SteamCMD/steamapps/common/<dir>` for SteamCMD; `Runtime/Games/<package-id>` for HTTPS ZIP. |
| `ValidateBeforeLaunch` | boolean | `true` in all examples; required `true` for HTTPS ZIP. |
| `HttpsZip` | object or null | `null` for SteamCMD. |
| `HttpsZip.Url` | string, required | Public canonical DNS HTTPS URL; no credentials, query, fragment or redirects. |
| `HttpsZip.ArchiveLength` | integer | Exact byte length. ZIP at most 512 MiB, at most 4096 entries. |
| `HttpsZip.ArchiveSha256` | string, required | Exact SHA-256. |
| `HttpsZip.Version` | string, required | Version label for this pinned distribution. |
| `HttpsZip.MaximumExpandedBytes` | integer | Bound on the **whole** ZIP expansion (including unmapped entries) plus raw files; at most 2 GiB. |
| `HttpsZip.Files[]` | array, required | Mapped files: `SourceRelativePath` (inside the ZIP), `DestinationRelativePath`, `Length`, `Sha256`. At most 256 files in the final inventory. Executables must be exact mapped `.exe` entries; no scripts or hooks. |
| `HttpsZip.RawFiles[]` | array or null, optional | 1 to 8 pinned opaque objects copied intact (not extracted): `Url`, `DestinationRelativePath`, `Length`, `Sha256`. Shares the 512 MiB aggregate download bound. |

Pinned distributions do not discover a newer release. Updating the game or
runtime needs a new signed declaration.

### 5.4 Capabilities

All booleans: `RequiresTransientPasswordOnStart`, `SupportsWorldImport`,
`SupportsBackupRestore`, `SupportsConnectionTest`, `SupportsInviteCode`,
`SupportsWindowsFirewall`, `SupportsExternalAccessTest`.

Declare only what the payload actually supports, consistent with `SaveData`,
`Firewall` and `Readiness`. `SupportsConnectionTest` must be `false` with
`Readiness.UnconfirmedOnly: true`. `Firewall[].LiveExternalCheck` needs
`SupportsExternalAccessTest: true`. A declared capability is not evidence.

### 5.5 Settings

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `Id` | string, required | Lowercase hyphenated in examples (`server-name`). The IDs `password` and `admin-password` are the approved game credential IDs; other Password IDs are withheld from remote clients. |
| `DisplayName`, `HelpText` | string, required | Operator-facing text. |
| `Kind` | enum | `Text`, `Integer`, `Boolean`, `Password`. |
| `DefaultKind` | enum | `Literal`, `RandomPassword`, `RandomName`. |
| `DefaultValue` | string or null | Literal default as a string (`"8"`, `"false"`). `null` for RandomPassword/RandomName. |
| `Minimum`, `Maximum` | integer or null | Integer bounds. |
| `MaximumLength` | integer | Text/Password at most 512. `0` for Integer/Boolean in examples. |
| `Required` | boolean | |
| `CharacterPolicy` | enum | `DisplayText`, `AsciiToken`, `SingleDirectoryAscii` (required Text, length 1 to 64; ASCII letters, digits, underscore, hyphen and interior spaces; reserved device names such as `CON` refused). Examples use `AsciiToken` for Integer/Boolean. |
| `Constraints[]` | array, required | `{ "Kind": "DistinctFrom" or "NotContainedIn", "OtherSettingId": "..." }`. |
| `Choices[]` | array or null, optional | `{ "Value", "Label" }` pairs for a bounded Integer; must cover every integer in range, 2 to 32 values. |
| `RequiresExplicitAcceptance` | boolean, optional | Only on a required Boolean with `DefaultKind: "Literal"` and `DefaultValue: "false"`. Setup refuses until the operator explicitly sets `true`. Installing a package or previewing it is not consent. Use for EULAs. |

Password rule for new Setup: `Kind: "Password"`, `DefaultKind:
"RandomPassword"`, `DefaultValue: null`, `MaximumLength` at least 12. Otherwise
the SDK forces V03 to Not Verified. The generated value is a temporary setup
default, not a strong long-term credential.

**Open question for maintainers:** the ingestion guide describes the temporary
password as two capitalised words plus two digits (8 to 12 characters), while
the SDK's `PasswordDefaultPolicy` text describes "ChangeMe plus four
cryptographically selected digits". Either way, declare it as above.

RandomName rule: `Kind: "Text"`, `DefaultKind: "RandomName"`, `DefaultValue:
null`, `MaximumLength` at least 12. Generates `Keldrin ####`.
Supplied, stored or imported values are preserved. Do not use it for fixed maps,
presets, registration IDs or file names the game has not been shown to accept.

### 5.6 Ports

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `Id`, `DisplayName` | string, required | |
| `DefaultPort`, `MinimumPort`, `MaximumPort` | integer | 1 to 65535. |
| `RequiredSpan` | integer | Consecutive ports needed; at most 32. |
| `DistinctFrom[]` | array, required | Port IDs that may not overlap. |
| `When` | object or null | `{ "SettingId", "EqualsValue" }` makes the port conditional on a setting (connection modes). |
| `ValueSettingId` | string or null, optional | Binds the port to one existing required Integer setting with matching min/max/default. |

### 5.7 Launch

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `ExecutableRelativePath` | string, required | Must be in `Process.ManagedExecutableRelativePaths`. Stays under the install root. |
| `WorkingDirectoryRelativePath` | string, required | Install folder, or exactly `Data/Servers/{profile-id}` or a strict descendant. |
| `WindowState` | enum | `Normal` or `Minimized`. |
| `Arguments[]` | array, required | At most 128. Each `{ "Kind", "Value", "Prefix", "When" }`. `Kind`: `Literal`, `Setting` (Value is a setting ID), `Port` (Value is a port ID), `RootRelativePath` (Value is a relative path, resolved to an absolute path), `ProfileId`. `Prefix` is prepended, for example `-Port=`. `When` makes the argument conditional. |

### 5.8 Process

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `ManagedExecutableRelativePaths[]` | array, required | At most 8. Processes Keldrin owns. |
| `AuxiliaryExecutableRelativePaths[]` | array, required | At most 8. Child processes (for example an Unreal `-Win64-Shipping.exe`). |
| `StartupTimeoutSeconds` | integer | 1 to 600. |
| `LauncherHandoffTimeoutSeconds` | integer | 0 to 300. `0` for direct launch. |
| `StopKind` | enum | Use `KillProcessTree`. `CloseMainWindowThenKill` is also listed as an ordinary Stop kind. `ConsoleSaveExit` is deferred. `TerrariaStdinSaveExit` is identity-restricted. **Open question for maintainers:** `ValheimBreakSaveExit`, `CoreKeeperCloseSaveExit` and `EnshroudedBreakSaveExit` are named after games but not explicitly classified in the contract; do not select them. |
| `ConsoleOwner` | object or null | Set `null`. Identity-restricted (Terraria) or deferred. |
| `FirstRun` | object or null | `null` unless the game must generate its own config first. `Provider: "ProbeSet"` with `CompletionProbeIds`, `TimeoutSeconds`, `StopAfterCompletion` (must be `true`), used with a `NativeFirstRunThenPreserveUnknown` settings document. `WindRoseNativeIdentity` is identity-restricted. |
| `RegistrationWatchdog` | object or null | Set `null`; identity-restricted. |
| `OutputCapture` | object or null, optional | Console capture that depends on the Keldrin Service's pipes. `RelativePath` must equal `Presentation.PrimaryLogRelativePath` under `Data/Servers/{profile-id}/Logs/`; `Encoding` `Utf8` or `Utf16LittleEndian`; `MaximumFileBytes` 64 KiB to 16 MiB; `RetainedFileCount` 1 to 10; `MaximumLineCharacters` 256 to 16384. Prefer the game's own log file. |
| `NativeFileOutput` | object or null, optional | Shared direct-file stdout/stderr fallback. `OutputRelativePath` and `ErrorRelativePath`: distinct fixed ASCII `.log` leaves, 5 to 64 characters, under `Data/Servers/{profile-id}/NativeLogs/`; `RetainedFileCount` 1 to 10. Requires `KillProcessTree`, handoff `0`, no `OutputCapture`. No live byte-size cap; raw files are not sanitized at write time. Use only when the game's own logger is unsuitable, and say why. |

### 5.9 Probes

| Field | Kind | Notes |
| --- | --- | --- |
| `Id` | string, required | |
| `SourceKind` | enum | `LogFile`, `JsonFile`, `IniFile`, `FileSystemPath`, `SteamA2s`, `TextFile`, `GameSpy4`. |
| `SourceRelativePath` | string or null | File path for file sources; `null` for network sources. |
| `PortId` | string or null | For network sources (`SteamA2s`, `GameSpy4`): an unconditional declared port. |
| `PortOffset` | integer, optional | `SteamA2s` only, within `0..RequiredSpan-1`; nonzero allowed. |
| `MatchKind` | enum | See below. |
| `JsonPointer`, `IniSection`, `IniKey`, `ExpectedValue` | string or null | For JSON/INI probes. |
| `ValuePredicate` | enum | `Equals`, `Exists`, `NonEmptyText`, `AsciiToken`, `Hexadecimal32`, `CaptureText`, `CaptureAsciiToken`, `CaptureHexadecimal32`. |
| `MinimumLength`, `MaximumLength` | integer | Capture bounds; `0` when unused. |
| `CaptureName` | string or null | |
| `Pattern[]` | array, required | At most 16 parts. `{ "Kind", "Value" }` with Kind `Literal`, `Setting`, `Port`, `Capture`, `Ipv4Address`. No regex. |
| `AmbiguousLinePrefixes[]` | array or null, optional | 1 to 8 literal prefixes, each 1 to 128 characters, on `LogFile`/`TextPattern` `PlayerJoin`/`PlayerLeave` probes. A matching event line that starts with a prefix makes player state unknown instead of inventing a player. |
| `BinaryString` | object or null, optional | Saved-world name from one exact file under a `PortableDirectory` root: `RequiredBytes[]` (at most 8 `{ Offset, ExpectedHex }`), `OffsetPointerPosition`, `MinimumStringOffset`, `MaximumStringOffset` (at most 4096), `MaximumUtf8Bytes` (at most 512), `MaximumFileBytes` (at most 64 MiB). WorldName/CaptureText only. |
| `Result` | enum | `Ready`, `Failure`, `JoinCode`, `GameId`, `WorldName`, `PlayerJoin`, `PlayerLeave`, `FirstRunComplete`, `RegistrationSuccess`, `RegistrationFailure`. |
| `CurrentRunOnly` | boolean | `true` for log-based readiness and player events. |

Match kinds:

| MatchKind | Status | Use |
| --- | --- | --- |
| `TextPattern` | Generic | Exact line pattern in a log (`LogFile`). Readiness epoch and join/leave events. |
| `JsonPointer`, `IniKey`, `PathExists` | Generic | Values in config files or file existence. |
| `SteamA2sResponding` | Generic | Steam A2S query response with player roster. |
| `SteamA2sInfoResponding` | Generic | A2S INFO count only (no names). Readiness only if listed in `ReadyProbeIds`. |
| `UnrealWorldLogPattern` | Generic | WorldName only, after an anchored Unreal timestamp/frame prefix. Its own floor is not stated separately; it is documented with `WorldPresentation`. |
| `UnrealReadyLogMarker` | Generic | `LogFile`, `Result: Ready`, `CurrentRunOnly: true`, exactly one nonempty `Literal` part; compares the complete line body after the Unreal timestamp/frame prefix. |
| `BinaryString` | Generic | With `FileSystemPath`, saved-world name. |
| `GameSpy4Responding` | Generic | `SourceKind: GameSpy4`, `Result` `Ready` or `WorldName`, unconditional `PortId`, `PortOffset` 0, `CurrentRunOnly: false`, no file/pattern/capture fields. Local loopback full-status query; partial responses and timeouts are unavailable, never zero. Bind `Players.WorldNameProbeId` to a separate `WorldName` probe. |
| `ValheimActiveSession`, `CoreKeeperGameInfo` | Identity-restricted | Do not use. |

Log-based declarations must cope with large logs (the reader advances a bounded
cursor; a ready line may appear after more than 512 KiB of startup output),
truncation, rotation and the previous run's log being present. A static string
in a binary is a candidate, not proof that the line is emitted.

### 5.10 Readiness

| Field | Kind | Notes |
| --- | --- | --- |
| `ReadyProbeIds[]` | array, required | Probes that together mean Game Ready. |
| `FailureProbeIds[]` | array, required | Probes that mean startup failed. Failures take precedence. |
| `ReadyProbeMatch` | enum, optional | `All` (default) or `Any`. |
| `UnconfirmedOnly` | boolean, optional | Honest first-test state: `true` with empty ready/failure IDs, `ReadyProbeMatch: All`, `Capabilities.SupportsConnectionTest: false`, `ConsoleOwner: null`, ordinary Stop. Never becomes Ready. V05 stays Not Verified. |
| `UnconfirmedMessage`, `ReadyMessage` | string, required | Operator-facing text. Say what readiness does not prove. |

### 5.11 Players

| Field | Kind | Notes |
| --- | --- | --- |
| `Provider` | enum | `ProbeSet` (generic A2S/GameSpy4/log probes) or `EventLog`. `WindRoseSessionLog` is identity-restricted. |
| `ActivityLogProbeId` | string or null | `EventLog`: a current-run `LogFile`/`TextPattern` Ready epoch probe. |
| `CurrentPlayersProbeId` | string or null | `ProbeSet`: the count/roster probe. Must be `null` with `EventLog`. |
| `JoinProbeIds[]`, `LeaveProbeIds[]` | array, required | `EventLog`: at least one each, `PlayerJoin`/`PlayerLeave` on the same file as the epoch. |
| `MaximumPlayersSettingId` | string or null | Setting holding the player limit. |
| `JoinMetadataProbeId` | string or null | Probe that yields a join identifier. |
| `ConnectionIdentifierKind` | enum or null | `GameId`, `JoinCode`, `InviteCode`; matches `JoinMetadataProbeId`. |
| `WorldNameProbeId` | string or null | A `WorldName` probe. |
| `WorldPresentation` | object or null, optional | needs `WorldNameProbeId`. `{ "Label", "ValueLabels": [{ "Value", "Label" }] }`, at most 32 mappings. |

Every package must declare a player-evidence strategy. Counts and names are
different evidence: an A2S INFO count has no reliable roster. Absent names are
unknown, never inferred players.

### 5.12 SettingsDocuments

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `Id` | string, required | |
| `Kind` | enum | `Json`, `Ini` (sectioned), `FlatKeyValue` (unsectioned `key=value`, `Container` must be `null`). `RoleCredentials` is unregistered; do not use. |
| `RelativePath` | string, required | The game's config file path. |
| `WriteMode` | enum | `CreateOrPreserveUnknown`, `NativeFirstRunThenPreserveUnknown` (game creates it first; see `Process.FirstRun`), `ExistingOnly`. |
| `Container` | string or null | INI section name (Icarus uses `/Script/Icarus.DedicatedServerSettings`) or JSON container. |
| `Mappings[]` | array, required | At most 32. See below. |
| `RoleCredentials` | object or null | Set `null`. |

Mapping fields: `SourceKind`, `SourceValue`, `Destination` (INI/flat key, or
JSON pointer), `ValueKind` (`String`, `Integer`, `Boolean`), and optional
`TrueValue`/`FalseValue` and `ExistingObject`.

| SourceKind | SourceValue | Notes |
| --- | --- | --- |
| `Setting` | setting ID | Writes the setting value. |
| `Literal` | literal text | Fixed value, for example `enable-query=true`. |
| `SettingIsNonEmpty` | setting ID | Whether a setting is non-empty. |
| `BooleanSettingChoice` | Boolean setting ID | Writes `TrueValue` or `FalseValue`. |
| `RootRelativePath` | relative path | String only; approved install/profile path. |
| `Port` | port ID | Integer only; unconditional port. |

`ExistingObject` (JSON only): `{ "ArrayPointer",
"IdentityProperty" (at most 64), "IdentityValue" (at most 128) }` selects an
existing named object in a JSON array. It never creates documents, arrays,
objects, roles or permissions.

Unknown fields in the game's config files are preserved. Changes to config
mappings take effect at the next normal start.

### 5.13 SaveData, Firewall, Prerequisites, Files

**SaveData**

| Field | Kind | Notes and floors |
| --- | --- | --- |
| `SupportsBackup`, `SupportsRestore`, `SupportsImport` | boolean | Backup and restore are paired. |
| `RequiresStoppedServer` | boolean | `true`. |
| `RestoreKind` | enum | `MergeFiles` or `AtomicDirectorySwap`. |
| `Roots[]` | array, required | At most 8. |
| `Roots[].Id`, `DisplayName` | string, required | |
| `Roots[].Provider` | enum | Use `PortableDirectory`. All others (`CoreKeeperCompressedWorld`, `WindRoseRocksDb`, `ValheimWorldSet`, `IcarusProspects`, `ConanSqliteWorldSet`, `StarRuptureWorldSet`) are identity-restricted. |
| `Roots[].RelativePath` | string, required | Usually under `Data/Servers/{profile-id}/`. A literal strict subtree of the Steam install is allowed for a game with no save relocation (one root, `MaximumInstances: 1`). |
| `Roots[].IncludeGlobs[]`, `ExcludeGlobs[]` | array, required | `**/*` includes nested files; `*` is not recursive. Never use a flat selector while claiming a complete recursive dataset. |
| `Roots[].RequiredFiles[]` | array or null, optional | 1 to 16 `{ "RelativePath", "MinimumBytes" }` (0 to 4 GiB) that must exist in a saved set. |
| `Roots[].ImportSubdirectorySettingId` | string or null, optional | A required Text/SingleDirectoryAscii setting naming the selected world folder; import replaces only that child, siblings preserved. |
| `Roots[].SelectImportedSubdirectory` | boolean, optional | RandomName default allowed. Offers actual existing folders; import selects the imported folder atomically. |

Note: the SDK documentation describes the install-subtree rule as
"no import", while the Abiotic Factor example enables import on
such a root with `ImportSubdirectorySettingId`. Let `author-lint` decide and
ask maintainers if you rely on it.

Keep the game's native save location where practical. Pointing a save root at
an empty managed folder is not a migration.

**Firewall** (each row)

| Field | Kind | Notes |
| --- | --- | --- |
| `Id`, `DisplayName` | string, required | |
| `Protocol` | enum | `Tcp` or `Udp`. |
| `Direction` | enum | `Inbound`. |
| `PortId` | string, required | Declared port. |
| `StartOffset`, `EndOffset` | integer | Offsets within the port span. |
| `ExecutableRelativePath` | string, required | The executable that actually listens. |
| `NetworkProfiles[]` | array, required | `Domain`, `Private`. |
| `ExternalProbe` | enum | `TcpConnect`, `UdpDatagram`, `SteamA2s`. |
| `LiveExternalCheck` | boolean, optional | Opt-in live external check: `SupportsExternalAccessTest: true`, one exact offset, `TcpConnect` or `SteamA2s` (A2S must bind a declared `SteamA2sResponding` probe on the same port/offset). |
| `When` | object or null | Mode condition. |

Declare rules for the ports clients actually connect to. The Minecraft example
declares no rule for its local-only query port.

**Prerequisites** (each row): `Id`, `DisplayName`, `Kind`, `PathOrFileName`,
`RequiredAuthenticodePublisher` (string or null), `FailureMessage`. Kinds:

| Kind | Notes |
| --- | --- |
| `WindowsVisualCppX64` | `PathOrFileName: "14.0"`. Installed automatically when required. |
| `WindowsSystemFile` | The reviewed selection is `XINPUT1_3.dll` with publisher `Microsoft Corporation` (Icarus example). |
| `WindowsXnaFramework40X86` | Identity-restricted to Terraria. |
| `RootRelativeFile` | File check only; no automatic installer. |

Adapters supply no installer URL, command or elevation. A new library kind needs
a Keldrin update.

**Files** (each row): `Path` (for example `assets/icon.png`,
`docs/readme.txt`), `MediaType` (`image/png`, `text/plain; charset=utf-8`),
`Length`, `Sha256`. At most 3 declared files. Every declared file must be in the
bundle with exactly that length and hash.

### 5.14 Setup fixture format

Strict JSON, at most 64 KiB, depth 24.

| Field | Kind | Notes |
| --- | --- | --- |
| `SchemaVersion` | integer | `1`. |
| `InputSha256` | string | Lowercase SHA-256 of the exact payload (or signed archive) bytes. |
| `Cases[]` | array | 1 to 32 cases. |
| `Cases[].Id` | string | Unique. 1 to 64 characters: lowercase `a`-`z`, digits and hyphen; starts with a letter or digit; no trailing hyphen; no `--`. |
| `Cases[].Setup` | object | Non-null. `{}` tests defaults. Fields seen in the examples: `Settings` (map of setting ID to string value, at most 32, values at most 4096 characters), `Ports` (map of port ID to number, at most 16), `ManageWindowsFirewall` (boolean), `ProfileId` (GUID string). **Open question for maintainers:** the full Setup object shape is not documented beyond these. |
| `Cases[].ExpectAccepted` | boolean | Required. |
| `Cases[].ExpectedActivePortIds` | array, optional | Accepted cases only; at most 16 unique IDs; compared as a set. |
| `Cases[].ExpectedActiveFirewallRuleIds` | array, optional | Accepted cases only; same rules. |
| `Cases[].ExpectedArgumentCount` | integer, optional | Accepted cases only; 0 to 128. |

Setting values in fixtures are strings, including numbers and booleans
(`"8"`, `"true"`). Booleans must be canonical lowercase.

---

## 6. SDK command reference

All six commands read explicit inputs only, write JSON to stdout, and never
install, sign, publish, contact a network, launch a game or open evidence
references. Exit codes: `0` checks completed (for `test-contract`: all
expectations passed), `1` (`test-contract` only) at least one expectation
failed, `2` refused input or checks. Exit 0 alone is **not** approval: always
read `State`, `HostCompatible` and the assurance fields.

Omitting `--host-version` uses the SDK's own three-part build version, not your
target host. Always pass it on commands that accept it. Changing
`--host-version` does not teach an older SDK newer vocabulary.

| Command | Inputs | What it checks | What it does not prove |
| --- | --- | --- | --- |
| `describe-contract` | No options | Selected vocabulary, limits, feature floors and provider restrictions | Not a complete schema; not permission to use a restricted provider |
| `author-schema` | No options | Draft 2020-12 structural JSON Schema from the actual manifest types and strict serializer | Not semantic validation or certification |
| `author-lint` | `--manifest` (required), `--host-version`, `--evidence` | Strict unsigned payload parsing plus shared semantic checks; evidence binding and ledger | No verified package identity, signed compilation or runtime plan |
| `canonical-payload` | `--manifest` only | Validated compact unsigned payload bytes | Does not sign; new bytes need new digest bindings |
| `verify-package` | `--archive` (required), `--host-version`, `--evidence` | The archive's fixed official-key signature, host compatibility and signed compilation | Not installation, catalog admission, anti-rollback or game testing |
| `test-contract` | exactly one of `--manifest` or `--archive`; `--fixtures` (required); `--host-version` | Digest-bound synthetic Setup cases using the shared Setup rules | Does not launch a game or certify settings, saves, players or any ledger row |

### describe-contract

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll describe-contract
```

Returns `Vocabulary` (every enum), `PackageBounds`, `SelectedValidatorBounds`,
`Features` (each with `Availability`: `Generic`, `GenericOptIn`,
`IdentityRestricted`, `Deferred` or `Unregistered`, plus `MinimumHostVersion`
and `Constraints`) and `Caveats`. Use only `Generic` and `GenericOptIn` features
for a new game.

### author-schema

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-schema > author-schema.json
```

Use it in an editor for field names and enum values. Its `x-keldrin` block
states `SemanticValidationRequired: true`.

### author-lint

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-lint --manifest my-game-payload-v2.json --host-version 0.9.140
dotnet ./bin/Keldrin.GameAdapterSdk.dll author-lint --manifest my-game-payload-v2.json --host-version 0.9.140 --evidence evidence.json
```

Accepts the unsigned **payload**, not an envelope or catalog. A successful
report (abridged; real run of the Icarus example with SDK 0.9.140):

```json
{
  "State": "Validated",
  "Mode": "UnsignedAuthorLint",
  "InputSha256": "4e3905dcc787b377e9539a2c98b8c65eb2c8829075820da4ddbd2fe4e5bc8414",
  "PackageId": "icarus",
  "PackageVersion": "1.1.10",
  "HostVersion": "0.9.140",
  "SchemaChecked": true,
  "SignatureVerified": false,
  "CompilerChecked": false,
  "HostCompatible": true,
  "InstalledIdentityChecked": false,
  "AcceptanceVerifiedByTool": false,
  "ReleaseCertified": false,
  "EvidenceAssurance": "Author claims are not independently verified; references are not opened or executed.",
  "Capabilities": [
    { "Id": "V05", "Declared": true, "Limitation": "Declared readiness sources are not current-run/native readiness evidence." }
  ],
  "Ledger": [
    { "Id": "V01", "Title": "Signed Gallery, compatibility, Add and installed identity", "Status": "Not Verified",
      "Reason": "No input-bound author evidence supplied.", "Evidence": [], "OperatorReviewed": false, "Assurance": "NotVerified" }
  ],
  "PasswordDefaultPolicy": { "Status": "Verified", "Scope": "DeclarationOnly", "Reason": "..." },
  "AuthorContractChecked": true
}
```

(Trimmed: the real report lists eight capability rows and all 22 ledger rows.)

How to read it:

- `State: "Validated"` and `AuthorContractChecked: true`: the payload parsed and
  passed the shared author checks.
- `HostCompatible`: `true` only if `--host-version` is within your
  `MinimumKeldrinVersion`/`MaximumKeldrinVersion`. A payload linted with a
  `--host-version` below its minimum reports `HostCompatible: false` while
  `State` is still `Validated`. Check this field every time.
- `SignatureVerified` and `CompilerChecked` stay `false` for unsigned input.
- `Capabilities[]`: what you declared (V05, V06, V07, V10, V16, V17,
  `SettingsProvider`, `LifecycleProvider`) with the limitation of each
  declaration. `Declared: true` is not evidence.
- `Ledger[]`: all 22 rows. Without `--evidence` every row is Not Verified. With
  evidence, rows carry your status and `Assurance:
  "AuthorAttestedNotIndependentlyVerified"`.
- `PasswordDefaultPolicy`: `Verified` (declaration only) when every Password
  setting uses RandomPassword with MaximumLength at least 12; `Not Verified`
  otherwise, or when no password fields exist. An incompatible declaration forces
  V03 to Not Verified.

A refusal looks like this (exit code 2):

```json
{
  "State": "Refused",
  "Category": "InputOrValidation",
  "ReleaseCertified": false,
  "AcceptanceVerifiedByTool": false
}
```

Refusals give only a closed `Category` (possible values include `Arguments`, `Json`,
`DuplicateProperty`, `Size`, `HostVersion`, `InputShape`, `InputChanged`,
`Validation`, `InputOrValidation`, `Signature`, `HostCompatibility`,
`EvidenceBinding`, `EvidenceRow`). They never echo your values or paths. To find
the cause, bisect: check JSON syntax and duplicate keys, enum spelling, digest
binding, then remove recently added fields one at a time.

### canonical-payload

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll canonical-payload --manifest my-game-payload-v2.json
```

On success, stdout is only the compact canonical payload JSON, with no trailing
newline and no report wrapper. On failure, stdout is a refusal report, so check
the exit code before using the output. Canonical bytes have a different digest
from your authoring file. Keep your fixtures and evidence bound to your authoring
payload (as the SDK examples do); include the canonical output only as a
receipt. Shell redirection may append a line ending, so do not bind evidence to a
redirected copy without checking its bytes. Maintainers re-derive canonical
bytes themselves.

### verify-package

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll verify-package --archive some-package.keldrin-game --host-version 0.9.140
```

Only for signed archives. You will not have one for your own adapter. The SDK
bundle includes `verification/core-keeper-1.0.4.keldrin-game` as a sample; its
report shows `Mode: "SignedPackage"`, `SignatureVerified: true`,
`CompilerChecked: true`.

### test-contract

```powershell
dotnet ./bin/Keldrin.GameAdapterSdk.dll test-contract --manifest my-game-payload-v2.json --fixtures setup-fixtures.json --host-version 0.9.140
```

Real output for the Icarus example with SDK 0.9.140 (exit 0):

```json
{
  "State": "Passed",
  "Mode": "SyntheticContractReplay",
  "InputSha256": "4e3905dcc787b377e9539a2c98b8c65eb2c8829075820da4ddbd2fe4e5bc8414",
  "FixturesSha256": "572ada8e72fcf66b3af49f5123dce061d9c33124f7c4e44ed45b7d80cad71c03",
  "HostVersion": "0.9.140",
  "SignatureVerified": false,
  "CompilerChecked": false,
  "ReleaseCertified": false,
  "AcceptanceVerifiedByTool": false,
  "Cases": [
    { "Id": "default-setup", "Status": "PASS", "Reason": "ExpectedAcceptance" },
    { "Id": "changed-ports", "Status": "PASS", "Reason": "ExpectedAcceptance" }
  ]
}
```

Case reasons: `ExpectedAcceptance`, `ExpectedRefusal` (PASS);
`UnexpectedRefusal`, `UnexpectedAcceptance`, `ActivePortsMismatch`,
`ActiveFirewallRulesMismatch`, `ArgumentCountMismatch` (FAIL). The report shows
case IDs and reasons only, never settings or launch values. A fixture file bound
to a different payload is refused with `Category: "FixtureBindingOrBounds"`
(exit 2). Other fixture refusal categories: `FixtureCase`,
`FixtureRejectedCaseOutputs`, `FixtureExpectedIds`.

An invented bad setting rejected as expected is a Setup-rule check, not a game
test.

---

## 7. Worked examples

These are quoted from the SDK example folders. They are unsigned source
references. Their bytes are quoted unchanged because fixtures and evidence are
bound to them, so their `MinimumKeldrinVersion` and evidence paths keep the
values they were built with. Set yours to `0.9.140`. Their evidence statements belong to them; do not copy their claims.

### 7.1 Simple Steam game: Icarus (`examples/icarus/121/`)

**README summary.** Unsigned candidate Icarus 1.1.10, sequence 12, based on the
public 1.1.9 sequence 11 archive. The only functional
change sets `Presentation.PrimaryLogRelativePath` to the existing `-ABSLOG`
destination, so the operator sees the game's own log. Distribution, launch,
process, settings, saves, ports, prerequisites and capabilities are unchanged.
Input SHA-256 `4e3905dcc787b377e9539a2c98b8c65eb2c8829075820da4ddbd2fe4e5bc8414`.
Two setup fixtures exercise source contracts. No new game operation or complete
22-item acceptance is claimed; maintainers own verification, signing and
publication.

**What to copy and what not to copy.** Copy the shape: SteamCMD distribution,
settings written to an INI document, `Port` and `Setting` arguments, a
`SteamA2s` readiness/player probe on the query port, executable-scoped UDP
firewall rules, a reviewed DirectX prerequisite. **Do not copy** its save and
transfer providers: `SaveData.Roots[].Provider: "IcarusProspects"` and
`SatelliteTransferSafety.Provider: "IcarusLobbyAndSaveSet"` are
identity-restricted to Icarus. A new game uses `PortableDirectory` (as in the
Minecraft example) and `SatelliteTransferSafety: null`. Also note that this
example reads readiness from A2S with `CurrentRunOnly: false`; your readiness
evidence must come from your own game's measured behavior.

The Icarus example folder has no evidence JSON. **Open question for maintainers:**
whether one exists elsewhere. An evidence file for an adapter like this would
have all 22 rows Not Verified unless operation receipts exist (see 7.2 and
section 8).

`icarus-121-payload-v2.json` (complete, unmodified):

```json
{
  "PackageId": "icarus",
  "PublisherId": "keldrin-official",
  "PackageVersion": "1.1.10",
  "Sequence": 12,
  "PublishedAtUtc": "2026-10-30T05:13:50Z",
  "MinimumKeldrinVersion": "0.9.112",
  "MaximumKeldrinVersion": null,
  "Presentation": {
    "DisplayName": "Icarus",
    "Description": "Official Icarus dedicated server with Steam registration, portable prospects, and selectable empty-server map or lobby behavior.",
    "TileColor": "#ffd36b",
    "ConnectionSummary": "Direct connection uses configured game and Steam query UDP ports.",
    "EstimatedRequiredDiskBytes": 30000000000,
    "MaximumInstances": 1,
    "IconPath": null,
    "PrimaryLogRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved/Logs/Icarus.log"
  },
  "Distribution": {
    "Kind": "SteamCmd",
    "SteamAppId": 2089300,
    "InstallDirectoryRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server",
    "ValidateBeforeLaunch": true
  },
  "Capabilities": {
    "RequiresTransientPasswordOnStart": false,
    "SupportsWorldImport": true,
    "SupportsBackupRestore": true,
    "SupportsConnectionTest": true,
    "SupportsInviteCode": false,
    "SupportsWindowsFirewall": true,
    "SupportsExternalAccessTest": true
  },
  "Settings": [
    {
      "Id": "server-name",
      "DisplayName": "Server name",
      "HelpText": "Name shown in Icarus.",
      "Kind": "Text",
      "DefaultKind": "RandomName",
      "DefaultValue": null,
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 64,
      "Required": true,
      "CharacterPolicy": "DisplayText",
      "Constraints": []
    },
    {
      "Id": "password",
      "DisplayName": "Join password",
      "HelpText": "Password required to join.",
      "Kind": "Password",
      "DefaultKind": "RandomPassword",
      "DefaultValue": null,
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 64,
      "Required": true,
      "CharacterPolicy": "DisplayText",
      "Constraints": []
    },
    {
      "Id": "admin-password",
      "DisplayName": "Admin password",
      "HelpText": "Password for server administration.",
      "Kind": "Password",
      "DefaultKind": "RandomPassword",
      "DefaultValue": null,
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 64,
      "Required": true,
      "CharacterPolicy": "DisplayText",
      "Constraints": []
    },
    {
      "Id": "max-players",
      "DisplayName": "Maximum players",
      "HelpText": "Maximum simultaneous players.",
      "Kind": "Integer",
      "DefaultKind": "Literal",
      "DefaultValue": "8",
      "Minimum": 1,
      "Maximum": 8,
      "MaximumLength": 0,
      "Required": true,
      "CharacterPolicy": "AsciiToken",
      "Constraints": []
    },
    {
      "Id": "stay-on-loaded-map",
      "DisplayName": "Empty-server behavior",
      "HelpText": "Checked keeps the current prospect loaded after the last player leaves; unchecked returns to the Icarus lobby after 10 seconds.",
      "Kind": "Boolean",
      "DefaultKind": "Literal",
      "DefaultValue": "false",
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 0,
      "Required": true,
      "CharacterPolicy": "AsciiToken",
      "Constraints": []
    }
  ],
  "Ports": [
    {
      "Id": "game",
      "DisplayName": "Game port",
      "DefaultPort": 17777,
      "MinimumPort": 1,
      "MaximumPort": 65535,
      "RequiredSpan": 1,
      "DistinctFrom": [
        "query"
      ],
      "When": null
    },
    {
      "Id": "query",
      "DisplayName": "Query port",
      "DefaultPort": 27015,
      "MinimumPort": 1,
      "MaximumPort": 65535,
      "RequiredSpan": 1,
      "DistinctFrom": [
        "game"
      ],
      "When": null
    }
  ],
  "Launch": {
    "ExecutableRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/IcarusServer.exe",
    "WorkingDirectoryRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server",
    "WindowState": "Minimized",
    "Arguments": [
      {
        "Kind": "Setting",
        "Value": "server-name",
        "Prefix": "-SteamServerName=",
        "When": null
      },
      {
        "Kind": "Port",
        "Value": "game",
        "Prefix": "-Port=",
        "When": null
      },
      {
        "Kind": "Port",
        "Value": "query",
        "Prefix": "-QueryPort=",
        "When": null
      },
      {
        "Kind": "RootRelativePath",
        "Value": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus",
        "Prefix": "-UserDir=",
        "When": null
      },
      {
        "Kind": "Literal",
        "Value": "-Log",
        "Prefix": null,
        "When": null
      },
      {
        "Kind": "RootRelativePath",
        "Value": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved/Logs/Icarus.log",
        "Prefix": "-ABSLOG=",
        "When": null
      }
    ]
  },
  "Process": {
    "ManagedExecutableRelativePaths": [
      "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/IcarusServer.exe"
    ],
    "AuxiliaryExecutableRelativePaths": [
      "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Binaries/Win64/IcarusServer-Win64-Shipping.exe"
    ],
    "StartupTimeoutSeconds": 180,
    "LauncherHandoffTimeoutSeconds": 0,
    "StopKind": "KillProcessTree",
    "FirstRun": null,
    "RegistrationWatchdog": null
  },
  "Probes": [
    {
      "Id": "ready-a2s",
      "SourceKind": "SteamA2s",
      "SourceRelativePath": null,
      "PortId": "query",
      "MatchKind": "SteamA2sResponding",
      "JsonPointer": null,
      "IniSection": null,
      "IniKey": null,
      "ExpectedValue": null,
      "ValuePredicate": "Equals",
      "MinimumLength": 0,
      "MaximumLength": 0,
      "CaptureName": null,
      "Pattern": [],
      "Result": "Ready",
      "CurrentRunOnly": false
    }
  ],
  "Readiness": {
    "ReadyProbeIds": [
      "ready-a2s"
    ],
    "FailureProbeIds": [],
    "UnconfirmedMessage": "Icarus is running, but its local Steam query has not confirmed readiness yet.",
    "ReadyMessage": "Icarus answered its local Steam query and is ready for direct player connections."
  },
  "Players": {
    "Provider": "ProbeSet",
    "ActivityLogProbeId": null,
    "CurrentPlayersProbeId": "ready-a2s",
    "JoinProbeIds": [],
    "LeaveProbeIds": [],
    "MaximumPlayersSettingId": "max-players",
    "JoinMetadataProbeId": null,
    "ConnectionIdentifierKind": null,
    "WorldNameProbeId": null
  },
  "SettingsDocuments": [
    {
      "Id": "server-settings",
      "Kind": "Ini",
      "RelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved/Config/WindowsServer/ServerSettings.ini",
      "WriteMode": "CreateOrPreserveUnknown",
      "Container": "/Script/Icarus.DedicatedServerSettings",
      "Mappings": [
        {
          "SourceKind": "Setting",
          "SourceValue": "server-name",
          "Destination": "SessionName",
          "ValueKind": "String"
        },
        {
          "SourceKind": "Setting",
          "SourceValue": "password",
          "Destination": "JoinPassword",
          "ValueKind": "String"
        },
        {
          "SourceKind": "Setting",
          "SourceValue": "admin-password",
          "Destination": "AdminPassword",
          "ValueKind": "String"
        },
        {
          "SourceKind": "Setting",
          "SourceValue": "max-players",
          "Destination": "MaxPlayers",
          "ValueKind": "Integer"
        },
        {
          "SourceKind": "Setting",
          "SourceValue": "stay-on-loaded-map",
          "Destination": "ResumeProspect",
          "ValueKind": "Boolean"
        },
        {
          "SourceKind": "BooleanSettingChoice",
          "SourceValue": "stay-on-loaded-map",
          "Destination": "ShutdownIfNotJoinedFor",
          "ValueKind": "String",
          "TrueValue": "-1.000000",
          "FalseValue": "10"
        },
        {
          "SourceKind": "BooleanSettingChoice",
          "SourceValue": "stay-on-loaded-map",
          "Destination": "ShutdownIfEmptyFor",
          "ValueKind": "String",
          "TrueValue": "-1.000000",
          "FalseValue": "10"
        }
      ]
    }
  ],
  "SaveData": {
    "SupportsBackup": true,
    "SupportsRestore": true,
    "SupportsImport": true,
    "RequiresStoppedServer": true,
    "RestoreKind": "AtomicDirectorySwap",
    "Roots": [
      {
        "Id": "prospects",
        "DisplayName": "Icarus Prospects",
        "Provider": "IcarusProspects",
        "RelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved/PlayerData/DedicatedServer/Prospects",
        "IncludeGlobs": [],
        "ExcludeGlobs": []
      }
    ]
  },
  "SatelliteTransferSafety": {
    "Provider": "IcarusLobbyAndSaveSet",
    "LobbyLogRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved/Logs/Icarus.log",
    "SaveDataRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Saved",
    "EmptyServerDelaySeconds": 10
  },
  "Firewall": [
    {
      "Id": "game-udp",
      "DisplayName": "Icarus game UDP",
      "Protocol": "Udp",
      "Direction": "Inbound",
      "PortId": "game",
      "StartOffset": 0,
      "EndOffset": 0,
      "ExecutableRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Binaries/Win64/IcarusServer-Win64-Shipping.exe",
      "NetworkProfiles": [
        "Domain",
        "Private"
      ],
      "ExternalProbe": "UdpDatagram",
      "When": null
    },
    {
      "Id": "query-udp",
      "DisplayName": "Icarus Steam query UDP",
      "Protocol": "Udp",
      "Direction": "Inbound",
      "PortId": "query",
      "StartOffset": 0,
      "EndOffset": 0,
      "ExecutableRelativePath": "Runtime/SteamCMD/steamapps/common/Icarus Dedicated Server/Icarus/Binaries/Win64/IcarusServer-Win64-Shipping.exe",
      "NetworkProfiles": [
        "Domain",
        "Private"
      ],
      "ExternalProbe": "SteamA2s",
      "When": null
    }
  ],
  "Prerequisites": [
    {
      "Id": "directx-runtime",
      "DisplayName": "Microsoft DirectX runtime",
      "Kind": "WindowsSystemFile",
      "PathOrFileName": "XINPUT1_3.dll",
      "RequiredAuthenticodePublisher": "Microsoft Corporation",
      "FailureMessage": "Icarus requires the Microsoft DirectX runtime (XINPUT1_3.dll). Install the DirectX End-User Runtime, then retry."
    }
  ],
  "Files": []
}
```

`setup-fixtures-121.json` (complete, unmodified):

```json
{
  "SchemaVersion": 1,
  "InputSha256": "4e3905dcc787b377e9539a2c98b8c65eb2c8829075820da4ddbd2fe4e5bc8414",
  "Cases": [
    {
      "Id": "default-setup",
      "Setup": {},
      "ExpectAccepted": true,
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectedActiveFirewallRuleIds": [
        "game-udp",
        "query-udp"
      ],
      "ExpectedArgumentCount": 6
    },
    {
      "Id": "changed-ports",
      "Setup": {
        "Ports": {
          "query": 27016,
          "game": 17778
        }
      },
      "ExpectAccepted": true,
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectedActiveFirewallRuleIds": [
        "game-udp",
        "query-udp"
      ],
      "ExpectedArgumentCount": 6
    }
  ]
}
```

Note that `ExpectedArgumentCount: 6` matches the six `Launch.Arguments`, and
both firewall rules are active in both cases because neither has a `When`
condition.

### 7.2 Minecraft Java (`examples/minecraft-java/123/`): the parts that differ

**Version note (open question for maintainers).** The payload is adapter
**1.0.1, sequence 3**, SHA-256
`f2bc770a7ab0ceb916e8ae46e08b0341437be89f0ef34ca377bfb148cd5ceb24`; its fixtures
and evidence bind to that digest. The example README may still describe adapter
1.0.0; the payload and the quotes below are 1.0.1.

**README summary.** Minecraft Java Edition 26.3. The
server is **not** from Steam. The official Mojang server JAR and an Azul Zulu
JRE 25 are pinned by exact length and SHA-256 and composed by the bounded HTTPS
installer: 139 runtime files and 42 distinct legal-notice files are mapped from
the runtime ZIP (only byte-identical duplicate notices omitted, recorded in
`runtime-notice-deduplication.json`), plus one raw JAR, staying under the 256-file
cap. The expansion bound covers the whole runtime ZIP plus the JAR (250,410,879
bytes). No Java-from-PATH search, latest-version lookup, shell wrapper, console
channel or controller. The EULA Boolean is false by default and both Setup and
profile materialization refuse it unchecked. The bundled `java/bin/java.exe`
launches the absolute JAR path with `--universe` pointing at the profile's
Worlds folder. Server-list name and initial world name use `Keldrin ####`. No
join/admin password is declared because the game has none. Online authentication
stays enabled; **Use whitelist** defaults to true. Stopped backup/export/
import/restore covers the whole Worlds collection recursively.
**Process-tree Stop does not guarantee a final game save.** Readiness, count,
names and world name come from a local GameSpy4 query on UDP 25570, which is
not proof of an outside-LAN join; only game TCP 25565 gets a firewall rule. The
game's own Log4j2 `latest.log` is the displayed log; vendor rolling is not a
proven retention cap. Earlier exact-artifact isolated runs by maintainers passed
install, Start, Ready, a zero-player query, log creation, Stop and file-hash
preservation through backup/export/restore/import; these are not complete
22-item certification and do not establish a real player join. The icon is a
320 by 180 rendition of official artwork, 28,048 bytes; that is attribution,
not a licence, and public code-only SDK bundles omit it.

**What differs from Icarus:**

| Area | Icarus | Minecraft Java |
| --- | --- | --- |
| Distribution | `SteamCmd`, App ID | `HttpsZip` (runtime ZIP with mapped files) plus `RawFiles` (JAR); `SteamAppId: 0`; `Runtime/Games/minecraft-java` |
| Mechanisms beyond the basics | None | RawFiles, explicit acceptance, profile working directory, GameSpy4, official HTTPS StoreUrl |
| Executable | Game exe | Bundled `java.exe`; JAR passed as a `RootRelativePath` argument |
| Working directory | Install folder | `Data/Servers/{profile-id}/Native` (profile data) |
| Settings documents | One INI | Two `FlatKeyValue` files: `server.properties` and `eula.txt` |
| Consent | None | `accept-eula` with `RequiresExplicitAcceptance: true` |
| Passwords | Join and admin | None (game has no server password) |
| World folder | Fixed provider | `world-name` with `SingleDirectoryAscii` and RandomName, bound to `ImportSubdirectorySettingId` with `SelectImportedSubdirectory: true` |
| Saves | `IcarusProspects` (restricted) | `PortableDirectory`, `**/*`, `MergeFiles` |
| Readiness/players | `SteamA2sResponding` | `GameSpy4Responding` Ready probe plus a separate WorldName probe |
| Firewall | Two UDP rules | One TCP rule (game port); no rule for the local query port |
| Files | none | `assets/icon.png`, `docs/readme.txt` |

`minecraft-java-123-payload-v2.json` (content unchanged except one trim:
`Distribution.HttpsZip.Files` has 181 entries in the original; only the first two
are shown here. Reformatted with two-space indentation; key order preserved):

```json
{
  "PackageId": "minecraft-java",
  "PublisherId": "keldrin-official",
  "PackageVersion": "1.0.1",
  "Sequence": 3,
  "PublishedAtUtc": "2026-10-31T02:42:59Z",
  "MinimumKeldrinVersion": "0.9.123",
  "MaximumKeldrinVersion": null,
  "Presentation": {
    "DisplayName": "Minecraft Java",
    "Description": "Minecraft Java 26.3 with bundled JRE 25, explicit EULA acceptance, configurable ports, local query reporting and stopped world backup/import/export/restore. Process-tree Stop does not guarantee a final save.",
    "TileColor": "#62aa40",
    "ConnectionSummary": "Java Edition clients use the configured game TCP port. Query UDP is for local status only. Online authentication remains enabled; native allow-list restrictions apply. Firewall, router and external gameplay evidence remain separate.",
    "EstimatedRequiredDiskBytes": 1073741824,
    "MaximumInstances": 1,
    "IconPath": "assets/icon.png",
    "PrimaryLogRelativePath": "Data/Servers/{profile-id}/Native/logs/latest.log",
    "StoreUrl": "https://www.minecraft.net/en-us/about-minecraft"
  },
  "Distribution": {
    "Kind": "HttpsZip",
    "SteamAppId": 0,
    "InstallDirectoryRelativePath": "Runtime/Games/minecraft-java",
    "ValidateBeforeLaunch": true,
    "HttpsZip": {
      "Url": "https://cdn.azul.com/zulu/bin/zulu25.38.109-ca-jre25.0.5.0.3-win_x64.zip",
      "ArchiveLength": 58634366,
      "ArchiveSha256": "34ce17a0319cca98f76d1d4e7bc9e5bb13c5d76744a70cdbb82a2edcfd31f8d1",
      "Version": "26.3-jre25.0.5",
      "MaximumExpandedBytes": 250410879,
      "Files": [
        {
          "SourceRelativePath": "zulu25.38.109-ca-jre25.0.5.0.3-win_x64/bin/api-ms-win-core-console-l1-1-0.dll",
          "DestinationRelativePath": "java/bin/api-ms-win-core-console-l1-1-0.dll",
          "Length": 22056,
          "Sha256": "869ec6bc79d1da594b0db2cfa510472ca781ffd826acbe7716198415ea3a3605"
        },
        {
          "SourceRelativePath": "zulu25.38.109-ca-jre25.0.5.0.3-win_x64/bin/api-ms-win-core-console-l1-2-0.dll",
          "DestinationRelativePath": "java/bin/api-ms-win-core-console-l1-2-0.dll",
          "Length": 21960,
          "Sha256": "69a12b4beba7e6cf1a9b7263e2c6a494a60ddbbed0bd8c40920c51179f571d99"
        }
      ],
      "RawFiles": [
        {
          "DestinationRelativePath": "server.jar",
          "Length": 62294556,
          "Sha256": "d052f14d7a173734fba553711e5b570162e2f2a313267ee31a21b975a679be64",
          "Url": "https://piston-data.mojang.com/v1/objects/33680f5f2ac32864d6d7cf5e56a705fdb3e05f4c/server.jar"
        }
      ]
    }
  },
  "Capabilities": {
    "SupportsBackupRestore": true,
    "RequiresTransientPasswordOnStart": false,
    "SupportsConnectionTest": false,
    "SupportsExternalAccessTest": true,
    "SupportsInviteCode": false,
    "SupportsWorldImport": true,
    "SupportsWindowsFirewall": true
  },
  "Settings": [
    {
      "Id": "server-name",
      "DisplayName": "Server name",
      "HelpText": "Native message of the day shown in the server list.",
      "Kind": "Text",
      "DefaultKind": "RandomName",
      "DefaultValue": null,
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 64,
      "Required": true,
      "CharacterPolicy": "DisplayText",
      "Constraints": []
    },
    {
      "Id": "world-name",
      "DisplayName": "World save folder",
      "HelpText": "Native level-name. Existing worlds use actual folder choices; import selects its canonical folder without an NBT rename.",
      "Kind": "Text",
      "DefaultKind": "RandomName",
      "DefaultValue": null,
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 64,
      "Required": true,
      "CharacterPolicy": "SingleDirectoryAscii",
      "Constraints": []
    },
    {
      "Id": "max-players",
      "DisplayName": "Maximum players",
      "HelpText": "Native max-players; initial limit8. Candidate range is bounded by the host, not a measured performance guarantee.",
      "Kind": "Integer",
      "DefaultKind": "Literal",
      "DefaultValue": "8",
      "Minimum": 1,
      "Maximum": 64,
      "MaximumLength": 0,
      "Required": true,
      "CharacterPolicy": "AsciiToken",
      "Constraints": []
    },
    {
      "Id": "accept-eula",
      "DisplayName": "I accept the Minecraft EULA",
      "HelpText": "Review https://www.minecraft.net/eula before accepting. False by default; Add refuses until explicitly accepted.",
      "Kind": "Boolean",
      "DefaultKind": "Literal",
      "DefaultValue": "false",
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 0,
      "Required": true,
      "CharacterPolicy": "AsciiToken",
      "Constraints": [],
      "RequiresExplicitAcceptance": true
    },
    {
      "Id": "use-whitelist",
      "DisplayName": "Use whitelist",
      "HelpText": "Maps native white-list. Configure allowed names in native whitelist.json. Turning this off permits authenticated players; online-mode stays enabled.",
      "Kind": "Boolean",
      "DefaultKind": "Literal",
      "DefaultValue": "true",
      "Minimum": null,
      "Maximum": null,
      "MaximumLength": 0,
      "Required": true,
      "CharacterPolicy": "AsciiToken",
      "Constraints": []
    }
  ],
  "Ports": [
    {
      "Id": "game",
      "DisplayName": "Game TCP port",
      "DefaultPort": 25565,
      "MinimumPort": 1024,
      "MaximumPort": 65535,
      "RequiredSpan": 1,
      "DistinctFrom": [
        "query"
      ],
      "When": null
    },
    {
      "Id": "query",
      "DisplayName": "Local query UDP port",
      "DefaultPort": 25570,
      "MinimumPort": 1024,
      "MaximumPort": 65535,
      "RequiredSpan": 1,
      "DistinctFrom": [
        "game"
      ],
      "When": null
    }
  ],
  "Launch": {
    "WindowState": "Minimized",
    "Arguments": [
      {
        "When": null,
        "Prefix": null,
        "Kind": "Literal",
        "Value": "-Xms512M"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "Literal",
        "Value": "-Xmx2G"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "Literal",
        "Value": "-jar"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "RootRelativePath",
        "Value": "Runtime/Games/minecraft-java/server.jar"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "Literal",
        "Value": "--universe"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "RootRelativePath",
        "Value": "Data/Servers/{profile-id}/Worlds"
      },
      {
        "When": null,
        "Prefix": null,
        "Kind": "Literal",
        "Value": "nogui"
      }
    ],
    "WorkingDirectoryRelativePath": "Data/Servers/{profile-id}/Native",
    "ExecutableRelativePath": "Runtime/Games/minecraft-java/java/bin/java.exe"
  },
  "Process": {
    "ManagedExecutableRelativePaths": [
      "Runtime/Games/minecraft-java/java/bin/java.exe"
    ],
    "AuxiliaryExecutableRelativePaths": [],
    "FirstRun": null,
    "LauncherHandoffTimeoutSeconds": 0,
    "StartupTimeoutSeconds": 180,
    "StopKind": "KillProcessTree",
    "RegistrationWatchdog": null,
    "ConsoleOwner": null
  },
  "Probes": [
    {
      "Pattern": [],
      "JsonPointer": null,
      "IniSection": null,
      "CaptureName": null,
      "PortId": "query",
      "SourceKind": "GameSpy4",
      "MinimumLength": 0,
      "Id": "local-query",
      "ValuePredicate": "Equals",
      "Result": "Ready",
      "ExpectedValue": null,
      "MatchKind": "GameSpy4Responding",
      "CurrentRunOnly": false,
      "MaximumLength": 0,
      "SourceRelativePath": null,
      "IniKey": null
    },
    {
      "Pattern": [],
      "JsonPointer": null,
      "IniSection": null,
      "CaptureName": null,
      "PortId": "query",
      "SourceKind": "GameSpy4",
      "MinimumLength": 0,
      "Id": "query-world",
      "ValuePredicate": "Equals",
      "Result": "WorldName",
      "ExpectedValue": null,
      "MatchKind": "GameSpy4Responding",
      "CurrentRunOnly": false,
      "MaximumLength": 0,
      "SourceRelativePath": null,
      "IniKey": null
    }
  ],
  "Readiness": {
    "ReadyProbeMatch": "All",
    "ReadyProbeIds": [
      "local-query"
    ],
    "UnconfirmedOnly": false,
    "ReadyMessage": "Minecraft responded to its local query. Player joins and external gameplay are separate evidence.",
    "FailureProbeIds": [],
    "UnconfirmedMessage": "The local Minecraft query has not provided an accepted response."
  },
  "Players": {
    "Provider": "ProbeSet",
    "JoinMetadataProbeId": null,
    "ActivityLogProbeId": null,
    "JoinProbeIds": [],
    "ConnectionIdentifierKind": null,
    "LeaveProbeIds": [],
    "WorldPresentation": {
      "ValueLabels": [],
      "Label": "World"
    },
    "WorldNameProbeId": "query-world",
    "CurrentPlayersProbeId": "local-query",
    "MaximumPlayersSettingId": "max-players"
  },
  "SettingsDocuments": [
    {
      "Id": "properties",
      "Container": null,
      "Kind": "FlatKeyValue",
      "WriteMode": "CreateOrPreserveUnknown",
      "Mappings": [
        {
          "Destination": "motd",
          "ValueKind": "String",
          "SourceValue": "server-name",
          "SourceKind": "Setting"
        },
        {
          "Destination": "level-name",
          "ValueKind": "String",
          "SourceValue": "world-name",
          "SourceKind": "Setting"
        },
        {
          "Destination": "max-players",
          "ValueKind": "Integer",
          "SourceValue": "max-players",
          "SourceKind": "Setting"
        },
        {
          "Destination": "server-port",
          "ValueKind": "Integer",
          "SourceValue": "game",
          "SourceKind": "Port"
        },
        {
          "Destination": "query.port",
          "ValueKind": "Integer",
          "SourceValue": "query",
          "SourceKind": "Port"
        },
        {
          "Destination": "enable-query",
          "ValueKind": "Boolean",
          "SourceValue": "true",
          "SourceKind": "Literal"
        },
        {
          "Destination": "online-mode",
          "ValueKind": "Boolean",
          "SourceValue": "true",
          "SourceKind": "Literal"
        },
        {
          "SourceKind": "Setting",
          "Destination": "white-list",
          "SourceValue": "use-whitelist",
          "ValueKind": "Boolean"
        }
      ],
      "RelativePath": "Data/Servers/{profile-id}/Native/server.properties"
    },
    {
      "Id": "eula",
      "Container": null,
      "Kind": "FlatKeyValue",
      "WriteMode": "CreateOrPreserveUnknown",
      "Mappings": [
        {
          "Destination": "eula",
          "ValueKind": "Boolean",
          "SourceValue": "accept-eula",
          "SourceKind": "Setting"
        }
      ],
      "RelativePath": "Data/Servers/{profile-id}/Native/eula.txt"
    }
  ],
  "SaveData": {
    "Roots": [
      {
        "DisplayName": "Minecraft worlds",
        "Provider": "PortableDirectory",
        "IncludeGlobs": [
          "**/*"
        ],
        "RelativePath": "Data/Servers/{profile-id}/Worlds",
        "ExcludeGlobs": [],
        "Id": "worlds",
        "ImportSubdirectorySettingId": "world-name",
        "SelectImportedSubdirectory": true
      }
    ],
    "SupportsRestore": true,
    "RestoreKind": "MergeFiles",
    "RequiresStoppedServer": true,
    "SupportsBackup": true,
    "SupportsImport": true
  },
  "SatelliteTransferSafety": null,
  "Firewall": [
    {
      "When": null,
      "PortId": "game",
      "NetworkProfiles": [
        "Domain",
        "Private"
      ],
      "EndOffset": 0,
      "StartOffset": 0,
      "ExecutableRelativePath": "Runtime/Games/minecraft-java/java/bin/java.exe",
      "DisplayName": "Minecraft Java game TCP",
      "Direction": "Inbound",
      "ExternalProbe": "TcpConnect",
      "Id": "game-tcp",
      "Protocol": "Tcp"
    }
  ],
  "Prerequisites": [],
  "Files": [
    {
      "Sha256": "b92d8bbbe3fd03092f106a91f8f6e539a1cea9db738a91e1ca962b03ad0cf329",
      "MediaType": "image/png",
      "Path": "assets/icon.png",
      "Length": 28048
    },
    {
      "Sha256": "77166eaa58bb375c1b8ba6eaebd53659906da66b8d27a7a739279c6615b1e9ba",
      "MediaType": "text/plain; charset=utf-8",
      "Path": "docs/readme.txt",
      "Length": 2174
    }
  ]
}
```

`setup-fixtures-123.json` (complete, unmodified; 17 cases):

```json
{
  "InputSha256": "f2bc770a7ab0ceb916e8ae46e08b0341437be89f0ef34ca377bfb148cd5ceb24",
  "SchemaVersion": 1,
  "Cases": [
    {
      "Setup": {},
      "Id": "unchecked-default",
      "ExpectAccepted": false
    },
    {
      "Id": "explicit-acceptance",
      "ExpectedArgumentCount": 7,
      "Setup": {
        "Settings": {
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ]
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "false"
        }
      },
      "Id": "unchecked-explicit",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "TRUE"
        }
      },
      "Id": "noncanonical-acceptance",
      "ExpectAccepted": false
    },
    {
      "Id": "preserved-names",
      "ExpectedArgumentCount": 7,
      "Setup": {
        "Settings": {
          "server-name": "My Server",
          "world-name": "Imported World",
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ]
    },
    {
      "Setup": {
        "Ports": {
          "game": 25565,
          "query": 25565
        },
        "Settings": {
          "accept-eula": "true"
        }
      },
      "Id": "distinct-ports",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "true",
          "max-players": "0"
        }
      },
      "Id": "players-zero",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "true",
          "world-name": "../World"
        }
      },
      "Id": "world-traversal",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "true",
          "world-name": "CON"
        }
      },
      "Id": "world-device",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "true",
          "world-name": "C:/World"
        }
      },
      "Id": "world-absolute",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "Settings": {
          "accept-eula": "true",
          "unsupported-password": "hidden"
        }
      },
      "Id": "unknown-setting",
      "ExpectAccepted": false
    },
    {
      "Setup": {
        "ProfileId": "00000000-0000-0000-0000-000000000000",
        "Settings": {
          "accept-eula": "true"
        }
      },
      "Id": "empty-profile-id",
      "ExpectAccepted": false
    },
    {
      "Id": "alternate-game-port",
      "ExpectedArgumentCount": 7,
      "Setup": {
        "Ports": {
          "game": 25566,
          "query": 25571
        },
        "Settings": {
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ]
    },
    {
      "Id": "no-firewall",
      "ExpectedArgumentCount": 7,
      "Setup": {
        "ManageWindowsFirewall": false,
        "Settings": {
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ]
    },
    {
      "Id": "whitelist-true",
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ],
      "Setup": {
        "Settings": {
          "use-whitelist": "true",
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectedArgumentCount": 7
    },
    {
      "Id": "whitelist-false",
      "ExpectAccepted": true,
      "ExpectedActiveFirewallRuleIds": [
        "game-tcp"
      ],
      "Setup": {
        "Settings": {
          "use-whitelist": "false",
          "accept-eula": "true"
        }
      },
      "ExpectedActivePortIds": [
        "game",
        "query"
      ],
      "ExpectedArgumentCount": 7
    },
    {
      "Id": "whitelist-noncanonical",
      "ExpectAccepted": false,
      "Setup": {
        "Settings": {
          "use-whitelist": "False",
          "accept-eula": "true"
        }
      }
    }
  ]
}
```

Notice the fixture patterns: `unchecked-default`, `unchecked-explicit` and
`noncanonical-acceptance` prove the EULA gate; `world-traversal`,
`world-device` and `world-absolute` probe the folder-name policy;
`distinct-ports`, `players-zero` and `unknown-setting` probe bounds; the
`ExpectedArgumentCount` of 7 matches the seven launch arguments; and only
`game-tcp` is an active firewall rule.

`evidence-123.json` (trimmed: two of 22 rows shown. All 22 rows in the original
are `Not Verified`, each with a scoped reason and the same README reference):

```json
{
  "InputSha256": "f2bc770a7ab0ceb916e8ae46e08b0341437be89f0ef34ca377bfb148cd5ceb24",
  "SchemaVersion": 1,
  "Rows": [
    {
      "Evidence": [
        "tools/GameAdapterSdk/examples/minecraft-java/123/README.md"
      ],
      "OperatorReviewed": false,
      "Id": "V05",
      "Reason": "Not Verified: isolated sequence1 returned native Ready and world/count query evidence. Sequence2 readiness contract is unchanged, but the final signed candidate needs a repeated receipt.",
      "Status": "Not Verified"
    },
    {
      "Evidence": [
        "tools/GameAdapterSdk/examples/minecraft-java/123/README.md"
      ],
      "OperatorReviewed": false,
      "Id": "V17",
      "Reason": "Not Verified: sequence1 returned a native world name and zero-player snapshot. Sequence2 query contract is unchanged. Nonzero roster, joins/disconnects and history remain unverified.",
      "Status": "Not Verified"
    }
  ]
}
```

Even though maintainers observed several operations on an earlier exact
artifact, every row stays Not Verified because the changed candidate needs its
own receipt. That is the honesty standard expected of you.

`docs/readme.txt` in the Minecraft example is a declared package file (listed in
`Files`) that ships inside the signed package as operator-facing support text.

---

## 8. Evidence and honesty rules

### The three statuses

- **Verified**: an affirmative claim supported by a scoped reason and at least
  one evidence reference. The SDK does not inspect or certify those receipts.
- **Not Verified**: missing, incomplete or unperformed evidence for the exact
  operation. This is not automatically a measured failure.
- **NA**: not applicable, with an explicit reviewed reason and
  `OperatorReviewed: true`. An unfinished or unimplemented requested feature is
  **not** NA. Only use NA when the human operator or a maintainer has reviewed
  and agreed; otherwise use Not Verified.

### What counts as evidence

- A declaration, a passing `author-lint`, a passing `test-contract` or a static
  string inside a game binary is **not** evidence for any ledger row.
- A receipt of the actual operation on the exact candidate bytes can support
  Verified for the scope it demonstrates. Automatic checks, file-integrity
  comparisons, operation receipts and manual checks can all count; developer
  gameplay sign-off is not mandatory.
- Backup, restore, import and export can be verified mechanically (stopped
  writer, validated archives, hash comparisons, atomic publication, safety
  backup, rollback) without buying the game or joining it.
- A backup does not prove an unperformed restore, or that the game saved changes
  it had not yet written.
- A process, a Game Ready marker, a Windows Firewall rule, a Managed Ports rule,
  an External result and a gameplay join are separate facts.
- A count is not a roster. A zero from a timeout or partial response is unknown,
  not zero.
- Receipts from an earlier artifact do not verify a changed candidate.
- Synthetic log fixtures are useful parser checks; label them synthetic, never
  as observed game output. Recorded logs contributed by an authorized server
  owner must have credentials, addresses and personal IDs removed.

### The 22-item verification ledger

| ID | Title | Evidence needed |
| --- | --- | --- |
| V01 | Gallery and identity | Signed discovery, host compatibility, Add/Setup and exact installed identity. |
| V02 | Installation | Official distribution, validation, prerequisites, resource estimates and progress/error reporting. |
| V03 | Settings and secrets | Approved defaults and limits, game-written settings round trips, unknown-field preservation and credential isolation. |
| V04 | Lifecycle and persistence | Approved Stop policy, saved-file preservation and scoped lifecycle/reload evidence; process-tree Stop does not guarantee a final in-memory save. |
| V05 | Readiness and failures | Current-run process and Game Ready evidence remain distinct from Windows, Managed Ports and exact-host External results; no LAN badge. |
| V06 | World transfer | Mechanical integrity of declared ZIP/folder import/export, allowed membership and atomic selected-folder/file commit or rollback. No purchased-game or developer-join requirement for this file check. |
| V07 | Backup and restore | Backup catalog/create/restore, safety backup and exact declared file integrity. Gameplay reload evidence is separate. |
| V08 | Retention and recovery | Ten-backup retention, stopped-writer exclusion and interrupted save-publication recovery. |
| V09 | Nightly maintenance | Back up servers observed running since the preceding scheduled sleep, including those already stopped; retain ten backups. |
| V10 | Windows Firewall | Actual configured ports, executable-scoped rules and stale-rule cleanup. |
| V11 | Managed Ports | Conflicts, apply/takeover, mode applicability, refresh and confirmed safe old-rule deletion. |
| V12 | External checks | Remote Portal stopped nonce checks and separately supported safe live queries. |
| V13 | Game-binary updates | Stop/backup/preservation and conditional restart through the supported update path. |
| V14 | Adapter updates | Signed package updates preserve profile identity/settings and respect live/save-recovery constraints. |
| V15 | Notices and Activity | Update notices and durable progress, completion and failure records. |
| V16 | Diagnostics | Prefer supported game-written file logging. Verify exact source/naming, encoding/framing, event coverage, sanitization, rotation/retention and reconnection. Justify any capture fallback: NativeFileOutput uses shared files; OutputCapture has Service-lifetime limits. |
| V17 | Players and history | Counts separately from named rosters/events, supported identity grammar, reconnect and history; absent names are unknown, not inferred players. |
| V18 | Discord | Status, authorized commands/help, lifecycle/player reports, heartbeat and deduplication. |
| V19 | Reopen and recovery | Supported process adoption, scheduled maintenance/sleep and post-wake recovery. |
| V20 | Client consistency | Keldrin app, Local Web and actually published Portal capabilities agree; settings remain read-only remotely. Portal34-gated restore remains unavailable on33. |
| V21 | Removal | Stopped permanent removal, rollback/recovery and stale menu/command/help cleanup. |
| V22 | Release integrity | Source/artifact correspondence, regressions, smoke, pinned signatures, immutable objects before catalog and fresh served-download verification. |

These titles are from the public verification page. The SDK report uses
longer titles for the same IDs (for example V01 "Signed Gallery, compatibility,
Add and installed identity"); the IDs are what bind evidence.

### Evidence JSON format

```json
{
  "SchemaVersion": 1,
  "InputSha256": "EXACT_LOWERCASE_64_HEX_INPUT_DIGEST",
  "Rows": [
    {
      "Id": "V07",
      "Status": "Verified",
      "Reason": "Automated backup and restore receipts demonstrate the exact operation.",
      "Evidence": ["receipts/backup-restore.json"],
      "OperatorReviewed": false
    }
  ]
}
```

Rules (enforced by `author-lint --evidence`; file at most 64 KiB):

- `InputSha256` is the lowercase SHA-256 of the exact payload bytes linted,
  including any BOM. Any change to the payload requires rebinding.
- At most 22 rows, each ID once. Missing rows default to Not Verified. Write all
  22 anyway, so every row has a reason.
- `Status`: `Verified`, `Not Verified` or `NA` (`N/A` is accepted and normalized
  to `NA`).
- `Reason`: required, at most 384 characters, no control characters.
- `Evidence`: at most 8 references, each at most 128 characters. Relative ASCII
  paths only: segments of letters, digits, `.`, `-`, `_`; no leading `/`, no
  `\`, no `:`, no `..` or `.` segments, no URLs, no drive names. Verified needs
  at least one. References are not opened by the SDK; point them at files inside
  your return bundle.
- `OperatorReviewed`: boolean; must be `true` for NA.
- Reasons and references are public metadata. Never put passwords, player
  names, private paths, IP addresses, URLs with tokens or credentials in them.

A real Verified row from the Abiotic Factor example, for shape only (its
references are maintainer receipts, not files you will have):

```json
{
  "Id": "V07",
  "Status": "Verified",
  "Reason": "Mechanically verified backup catalog/create/export/restore with pre-restore safety backup and exact restored file/hash manifests through the production Service path. No purchased client or visual rejoin is required for this saved-file operation.",
  "Evidence": [
    "artifacts/AbioticEnshrouded-0.9.120/TestResults/native120-service-full.trx",
    "artifacts/AbioticEnshrouded-0.9.120/TestResults/native120-core-full.trx",
    "artifacts/AbioticEnshrouded-0.9.120/public-native-correspondence.json"
  ],
  "OperatorReviewed": false
}
```

For an adapter you wrote from documentation, a good Not Verified reason states
what is declared and what remains to be observed, for example: "Not Verified:
log readiness declares the exact line from the vendor documentation; no
current-run startup with a prior log present has been observed."

---

## 9. The return bundle

### Folder layout

```text
<package-id>/
  <package-version>/
    <package-id>-payload-v2.json      unsigned payload (authoring bytes)
    setup-fixtures.json               bound to the payload SHA-256
    setup-boundary-fixtures.json      optional, more cases (max 32 per file)
    evidence.json                     all 22 rows, bound to the payload SHA-256
    README.md                         adapter summary, sources, limits
    HANDOFF-RETURN.md                 checklist and open questions (template below)
    assets/
      icon.png                        only if declared in Files and redistributable
    docs/
      readme.txt                      only if declared in Files
    research/
      sources.md                      official sources with URLs and dates checked
    receipts/
      commands.txt                    exact command lines, exit codes, SDK version and ZIP SHA-256
      describe-contract.json
      author-schema.json
      author-lint.json
      author-lint-with-evidence.json
      canonical-payload.json
      test-contract.json
      test-contract-boundary.json     if a boundary fixture file exists
```

### Naming rules

- `<package-id>`: the payload's `PackageId`, lowercase letters, digits and
  hyphens, as in the examples (`minecraft-java`).
- `<package-version>`: the payload's `PackageVersion` (for example `1.0.0`).
  Maintainers may rename folders to match the SDK's example tree.
- The payload file is `<package-id>-payload-v2.json`. Other names are fixed as
  shown above.
- File and folder names use only `A`-`Z`, `a`-`z`, `0`-`9`, `_`, `.`, `/`, space
  and `-`, at most 180 characters per path. No `.` or `..` segments.
- All JSON, Markdown and text files are UTF-8 without BOM and without control
  characters other than tab, line feed and carriage return.
- Every declared `Files[]` entry exists at its `Path` with the exact `Length`
  and `Sha256`.

### Delivery format

Zip the `<package-id>/` folder. The bundle must fit the Keldrin.ca adapter
submission limits: at most **2 MiB** uploaded, **8 MiB** expanded and **128
files**; JSON, Markdown, text and PNG files only; no scripts, executables,
nested archives, links or encrypted entries. If the ZIP would exceed these
limits, drop the bulky optional receipts (`author-schema.json`,
`describe-contract.json` and the redirected canonical payload) first and note
that in `commands.txt`. If the quickstart produced a report directory, include
only a JSON or text summary from it if it fits these rules (open question 9).

The human operator either submits the ZIP privately at
https://keldrin.ca/feedback/adapters (verified, approved accounts; questions,
replies, status and revised uploads stay on the website) or hands it to a
maintainer directly. Include an asset licence or permission statement.

### HANDOFF-RETURN.md template

```markdown
# HANDOFF-RETURN: <Game name> adapter <package-id> <package-version>

## Identity
- PackageId / PackageVersion / Sequence (proposed): ...
- MinimumKeldrinVersion / MaximumKeldrinVersion: ...
- Payload SHA-256 (lowercase): ...
- SDK used: version ..., ZIP SHA-256 ..., target --host-version ...
- Distribution: SteamCMD app ... | HTTPS (not Steam; operator told on <date>)

## Results
- author-lint: State ..., HostCompatible ..., AuthorContractChecked ..., exit ...
- author-lint --evidence: State ..., exit ...
- test-contract: State ..., N cases, exit ...
- canonical-payload: exit ...
- verify-package: not run (no signed archive)

## Ledger summary
- Verified: (IDs, with receipts) or "none"
- NA: (IDs, with reviewer) or "none"
- Not Verified: (IDs)

## What was measured vs taken from documentation
- ...

## Gaps needing a Keldrin update (not worked around)
- ...

## Open questions for maintainers
- ...

## Assets and licensing
- Icon source, permission, size (bytes, pixels), SHA-256 ... or "no icon".

## Safety confirmations
- [ ] No credentials, tokens, real IPs, real player names, private paths.
- [ ] No game binaries, saved worlds or non-redistributable files.
- [ ] No game server launched against real worlds (or: operator-arranged test described in README).
- [ ] Nothing signed. Keldrin not modified.
```

### What the receiving maintainer does next

These steps are owned by Keldrin maintainers. They are listed so you know what
your bundle must support.

1. **Intake review.** Check redistribution rights, private data, and that every
   declaration traces to the research sources. Compare against the existing
   ingestion rules (the 22-item ledger and the package ownership boundary).
2. **Re-run the SDK** with the exact target host: `author-lint`,
   `test-contract`, ledger review. Evidence claims are reviewed against their
   actual receipts.
3. **Canonicalize and review assets.** Canonical output and every hash-bound
   declared asset are reviewed before signing. Fixtures and evidence are rebound
   to the bytes actually signed.
4. **Official signing** by an authorized maintainer. Then `verify-package` on the
   actual signed archive, and `test-contract --archive` with fixtures bound to
   the signed archive's digest (signed-archive and source digests differ). New
   or replaced artwork is checked through the shipping Keldrin Service thumbnail
   projection and the Keldrin app's decoder, not only the signature.
5. **Private test catalog.** A complete compatible signed catalog whose minimum
   host covers every package in it is prepared with explicit operator
   authorization, a fresh higher sequence, and preserved accounts, settings,
   worlds and catalog high-water state. It is never reset to accept an older
   catalog. This is not public release.
6. **Isolated tests.** Normal Keldrin Service operations on disposable data:
   install, settings, Start/Stop, readiness, players, backup/restore/import/
   export and ten-backup retention, recovery, firewall, Managed Ports, External,
   updates, Discord, client consistency (Keldrin app, Local Web, Remote Portal)
   and removal, recorded row by row in the 22-item ledger. EULA consent for an
   isolated test applies only to that test.
7. **Publication.** Separately authorized. Immutable package objects are
   published first and the catalog last, followed by fresh HTTPS download
   verification and an evidence note for the game. The Review status and questions are
   reported back on the website submission.

---

## 10. Final checklist before handing back

Complete every item. If an item cannot be completed, say why in
`HANDOFF-RETURN.md`.

**Facts**
- [ ] Every executable, argument, port, protocol, config key, save path, log line
      and hash traces to an official source or an operator-arranged measurement
      recorded in `research/sources.md`.
- [ ] No value was copied from another game's adapter without evidence for this
      game.
- [ ] If the server is not distributed through SteamCMD, the human operator was
      told explicitly.
- [ ] No EULA was accepted on anyone's behalf; any EULA uses
      `RequiresExplicitAcceptance: true` with default `"false"`.

**Payload**
- [ ] Only `Generic` or `GenericOptIn` features; no identity-restricted,
      deferred or unregistered provider; `SatelliteTransferSafety` is `null`;
      saves use `PortableDirectory`; Stop is `KillProcessTree` (or an approved
      ordinary kind).
- [ ] `MinimumKeldrinVersion` is at least the highest feature floor used, and the
      SDK you used knows every field.
- [ ] Password settings use RandomPassword, `DefaultValue: null`,
      `MaximumLength` at least 12; no invented password setting.
- [ ] Readiness uses measured current-run evidence, or honestly uses
      `UnconfirmedOnly: true`.
- [ ] Player evidence keeps counts and names separate; nothing infers zero.
- [ ] Every `Files[]` entry exists with exact length and SHA-256; icon PNG at
      most 32 KiB and 512 by 512 pixels; you may redistribute it.
- [ ] UTF-8 without BOM.

**SDK runs** (explicit `--host-version` on every command that accepts it)
- [ ] `author-lint`: `State: "Validated"`, `HostCompatible: true`, exit 0.
- [ ] `test-contract`: `State: "Passed"`, exit 0, includes default, boundary,
      collision and refusal cases.
- [ ] `author-lint --evidence`: `State: "Validated"`, exit 0, all 22 rows present.
- [ ] `canonical-payload`: exit 0.
- [ ] Fixtures and evidence `InputSha256` equal the final payload digest; all
      commands were rerun after the last payload edit.
- [ ] `receipts/commands.txt` lists every command line, exit code, the SDK
      version and SDK ZIP SHA-256.

**Evidence**
- [ ] No row is Verified without a receipt of the exact operation on these exact
      bytes, inside the bundle.
- [ ] No NA without an explicit reviewer and `OperatorReviewed: true`.
- [ ] Reasons at most 384 characters; references relative, at most 8 per row.

**Bundle**
- [ ] Layout and names as in section 9; ZIP at most 2 MiB, 8 MiB expanded,
      128 files; only JSON, Markdown, text and PNG.
- [ ] `README.md` states what works, what is declared only, and every limit
      (including that process-tree Stop does not guarantee a final save).
- [ ] `HANDOFF-RETURN.md` is filled in, including gaps and open questions.
- [ ] Scanned every file for credentials, tokens, real IP addresses (only
      documentation addresses such as 192.0.2.x, or 127.0.0.1/0.0.0.0 where the
      game requires them), real player names and private paths.
- [ ] Nothing was signed, no Keldrin code was changed, and no real game server
      was run against real worlds.
