Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ This repository accepts changes through pull requests. Keep each pull request fo

Do not add credentials, contributor tokens, replay evidence, cached bot sources, or ranked journals to Git. The client may open a result issue, but it must never receive credentials that can write repository content, branches, releases, packages, Pages, facts, or projections.

Changes to published envelope, configuration, or local-state contracts must be versioned and coordinated with [CAP-016](https://github.com/robocode-dev/tank-royale/tree/main/docs/capabilities/CAP-016-rumble-client).
Changes to published envelope, configuration, or local-state contracts must be versioned and coordinated with the capability document that owns this contract in the Tank Royale repository: [`CAP-016-rumble-client`](https://github.com/robocode-dev/tank-royale/tree/main/docs/capabilities/CAP-016-rumble-client).
61 changes: 45 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,46 @@

The Rumble Client runs local Tank Royale battles against the published Rumble catalog. Ranked mode validates the current engine and catalog pin, journals every completed result with replay evidence, and submits batches through the Rumble data repository's issue inbox. Practice mode never creates a ranked record or submission.

The project is currently being built under [Tank Royale change CH-012](https://github.com/robocode-dev/tank-royale/tree/main/changes/CH-012-create-rumble-client). The public contracts are owned by [CAP-016](https://github.com/robocode-dev/tank-royale/tree/main/docs/capabilities/CAP-016-rumble-client).
This project is part of the [Tank Royale](https://github.com/robocode-dev/tank-royale) ecosystem; the client's public contracts (configuration, journal, and submission format) are documented there.

Contributors may use the supported native distribution or the recommended container image. Docker and Podman run the same image, which supplies the complete Java, .NET, Python, and Node.js environment and is the isolation boundary for reviewed bot code; direct execution uses the same client contracts but runs bots with the contributor's host permissions. Production images are published only after Tank Royale releases the engine contracts required by ranked Rumble battles.
## Quickstart (Docker, recommended)

## Build
Docker is the recommended way to build and run the client: it supplies the complete Java, .NET, Python, and Node.js environment every ranked bot needs, and is the isolation boundary for running reviewed-but-untrusted bot code. Docker Engine or Docker Desktop is required for this path.

Install JDK 17 and keep a Tank Royale checkout containing BR-049 beside this repository, then run:
1. Clone this repository.
2. Build the image: `docker build --tag rumble-client:dev .`
3. Copy `rumble-client.example.json` to `rumble-client.json` and edit it — see [Configuration](#configuration) below. Never commit the resulting file.
4. Check your settings: `docker/rumble.sh validate rumble-client.json`
5. Check the bundled runtimes: `docker/rumble.sh runtimes`
6. Resolve the ranked catalog and prepare the bot cache: `docker/rumble.sh sync rumble-client.json`
7. Run one ranked battle: `docker/rumble.sh run rumble-client.json`
8. Submit pending results: `export RUMBLE_CLIENT_TOKEN=<your token>` then `docker/rumble.sh submit rumble-client.json`

On PowerShell, use `docker/rumble.ps1 <validate|runtimes|sync|run|submit> [config-path] [image]` instead — for example `docker/rumble.ps1 run rumble-client.json`, and set `$env:RUMBLE_CLIENT_TOKEN` before `submit`.

Every command runs the container read-only, with capabilities dropped and resource limits applied. Only `runtimes` blocks network access outright; `validate`, `sync`, `run`, and `submit` use normal outbound network because synchronization and submission need it, and `run` re-synchronizes the ranked snapshot before executing a battle.

The client tracks posted batches locally and only drops them once their receipt comment appears on the closed issue; retrying an already-accepted submission is acknowledged idempotently rather than double-submitted.

## Building `rumble-client` itself

Most contributors only need the Quickstart above. If you're changing this repository's own Java code, you need to build and test it, which still needs Gradle — but not installed on your machine. Run it inside a Gradle image matching this repository's pinned wrapper version (`gradle/wrapper/gradle-wrapper.properties`, currently 9.6.1), with your checkout bind-mounted:

```shell
./gradlew --no-configuration-cache -PtankRoyaleSource=../tank-royale build
docker run --rm -it -v "${PWD}:/workspace" -w /workspace gradle:9.6.1-jdk17 gradle build
```

On PowerShell, quote the property argument: `.\gradlew.bat --no-configuration-cache "-PtankRoyaleSource=../tank-royale" build`.
The same command works unchanged on PowerShell. Note this is a different Gradle version than the `gradle:8.14.3-jdk17` image the `Dockerfile`'s own build stage starts from — that stage still runs `./gradlew` inside it precisely so the wrapper's pinned 9.6.1 is what actually builds the release, regardless of the base image's bundled version. Keep the two in sync if either changes.

The source substitution is the development dependency path until the Runner API is part of a value-bearing Tank Royale release. It compiles the client against `dev.robocode.tankroyale:robocode-tankroyale-runner` without publishing an interim artifact. CI and the container build pin the accepted Tank Royale merge commit rather than following a moving branch. Configuration caching is disabled for source-substituted builds because the included Tank Royale build does not support it.
This repository currently depends on an unreleased Tank Royale Battle Runner version, built from a local Tank Royale checkout rather than a published Maven artifact — that's why CI and the `Dockerfile`'s own build stage pass `-PtankRoyaleSource=<path>`. To build against a local Tank Royale checkout the same way, mount it alongside your `rumble-client` checkout and add that property:

```shell
docker run --rm -it -v "${PWD}:/workspace" -v "${PWD}/../tank-royale:/tank-royale" -w /workspace gradle:9.6.1-jdk17 gradle -PtankRoyaleSource=/tank-royale build
```

This dependency becomes an ordinary published Maven Central artifact once Tank Royale releases the Battle Runner version this repository pins in `gradle.properties` — at that point this source-mount step stops being necessary.

If you already have JDK 17 and Gradle installed on your machine, the equivalent host commands work identically: `./gradlew build`, or `./gradlew -PtankRoyaleSource=../tank-royale build`.

The build produces native ZIP and TAR archives under `build/distributions/`. Run `./gradlew run --args="--check-runtimes"` to verify the required native installations; the check never installs or changes them.

Expand All @@ -28,7 +53,7 @@ The client validates configuration and can synchronize the current ranked input

## Configuration

Copy `rumble-client.example.json` to `rumble-client.json`. Ranked mode requires a registered `clientId`; practice mode may omit it. The optional `workDirectory` selects the local cache, journal, and replay-evidence root and defaults to `.rumble-client` beside the configuration file. Do not commit the resulting file or any token. A submission token is supplied at runtime only when issue-ops support is available.
Copy `rumble-client.example.json` to `rumble-client.json`. Ranked mode requires a registered `clientId` — see [`rumble-data`'s contributing guide](https://github.com/robocode-dev/rumble-data/blob/main/CONTRIBUTING.md) for the one-time registration pull request; practice mode may omit it. The optional `workDirectory` selects the local cache, journal, and replay-evidence root and defaults to `.rumble-client` beside the configuration file. Do not commit the resulting file or any token.

## Docker and Podman development image

Expand All @@ -43,27 +68,31 @@ podman build --tag rumble-client:dev .

To run the four-language container smoke check locally, build the sample-bot archives and run `CONTAINER_ENGINE=podman TANK_ROYALE_SOURCE=../tank-royale bash scripts/verify-container.sh`; Docker is the default engine.

Use the launcher scripts for configuration validation, runtime checks, and snapshot synchronization. The shell launcher selects Docker by default and accepts `CONTAINER_ENGINE=podman`; the PowerShell launcher accepts `-Engine podman` or the same `CONTAINER_ENGINE` environment variable:
Use the launcher scripts for configuration validation, runtime checks, snapshot synchronization, ranked battles, and result submission. The shell launcher selects Docker by default and accepts `CONTAINER_ENGINE=podman`; the PowerShell launcher accepts `-Engine podman` or the same `CONTAINER_ENGINE` environment variable:

```shell
./docker/rumble.sh runtimes
CONTAINER_ENGINE=podman ./docker/rumble.sh runtimes
./docker/rumble.sh run rumble-client.json
export RUMBLE_CLIENT_TOKEN=<your token>
./docker/rumble.sh submit rumble-client.json
```

```powershell
.\docker\rumble.ps1 runtimes
.\docker\rumble.ps1 runtimes -Engine podman
./docker/rumble.ps1 runtimes
./docker/rumble.ps1 runtimes -Engine podman
$env:CONTAINER_ENGINE = 'podman'
.\docker\rumble.ps1 runtimes
./docker/rumble.ps1 runtimes
./docker/rumble.ps1 run rumble-client.json
$env:RUMBLE_CLIENT_TOKEN = '<your token>'
./docker/rumble.ps1 submit rumble-client.json
```

For `validate` and `sync`, the launcher mounts only the configuration file and `.rumble-client` state directory. Keep the state directory writable; it contains the bot cache, journal, and replay evidence. The runtime check uses no network, while synchronization requires network access to the configured repositories.
For `validate`, `sync`, `run`, and `submit`, the launcher mounts the configuration file and `.rumble-client` state directory. Keep the state directory writable; it contains the bot cache, journal, and replay evidence. The `runtimes` check uses no network, while `validate`, `sync`, and `run` require network access to synchronize the configured repositories and `submit` requires network access to the GitHub Issues API. Submission forwards `RUMBLE_CLIENT_TOKEN` from the environment and never writes it to disk.

The image contains a pinned Tank Royale Python API and its runtime dependencies in an image-owned virtual environment; no host Python environment or package installation is required. When running bot archives directly through a containerized Battle Runner, mount the archive read-only for Java and Python. C# and TypeScript first-run dependency setup may need to write and change file permissions, so copy those archives into writable container storage such as `/tmp` before booting them. This is especially important for Windows bind mounts, where `chmod` can fail with `EPERM`.

If Podman Desktop on Windows reports `ssh-keygen` cannot be found, install or enable Windows OpenSSH and add `C:\Windows\System32\OpenSSH` to the user `PATH`, then restart the terminal and Podman Desktop.

Submission commands remain unavailable until their later CH-012 implementation tasks land. Their container launcher phases will run battles offline without a submission credential and submission online without starting bot code.
If Podman Desktop on Windows reports `ssh-keygen` cannot be found, install or enable Windows OpenSSH and add the OpenSSH installation directory to the user `PATH`, then restart the terminal and Podman Desktop.

## Contributing

Expand Down
16 changes: 13 additions & 3 deletions docker/rumble.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet('validate', 'runtimes', 'sync')]
[ValidateSet('validate', 'runtimes', 'sync', 'run', 'submit')]
[string] $Command,

[Parameter(Position = 1)]
Expand All @@ -25,6 +25,10 @@ $clientArguments = switch ($Command) {
'validate' { @('--validate-config', '/work/rumble-client.json') }
'runtimes' { @('--check-runtimes') }
'sync' { @('--sync', '/work/rumble-client.json') }
# --run re-synchronizes before executing a battle (same as --sync), so unlike
# runtimes/validate it cannot be run with --network none here.
'run' { @('--run', '/work/rumble-client.json') }
'submit' { @('--submit', '/work/rumble-client.json') }
}

$containerArguments = @(
Expand All @@ -45,10 +49,16 @@ if ($Command -eq 'runtimes') {
$stateDirectory = Join-Path $configurationDirectory '.rumble-client'
New-Item -ItemType Directory -Force -Path $stateDirectory | Out-Null
$containerArguments += @(
'--mount', "type=bind,source=$configurationPath,target=/work/rumble-client.json,readonly",
'--mount', "type=bind,source=$stateDirectory,target=/work/.rumble-client"
'--mount', "type=bind,source=${configurationPath},target=/work/rumble-client.json,readonly",
'--mount', "type=bind,source=${stateDirectory},target=/work/.rumble-client"
)
}
if ($Command -eq 'submit') {
if (-not (Test-Path env:RUMBLE_CLIENT_TOKEN) -or [string]::IsNullOrEmpty($env:RUMBLE_CLIENT_TOKEN)) {
throw 'RUMBLE_CLIENT_TOKEN must be set in the environment to submit results'
}
$containerArguments += @('--env', 'RUMBLE_CLIENT_TOKEN')
}
$containerArguments += $Image
$containerArguments += $clientArguments

Expand Down
41 changes: 26 additions & 15 deletions docker/rumble.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -eu

usage() {
echo "Usage: CONTAINER_ENGINE=docker|podman docker/rumble.sh <validate|runtimes|sync> [config-path] [image]" >&2
echo "Usage: CONTAINER_ENGINE=docker|podman docker/rumble.sh <validate|runtimes|sync|run|submit> [config-path] [image]" >&2
exit 2
}

Expand All @@ -11,34 +11,45 @@ config_path="${2:-rumble-client.json}"
image="${3:-rumble-client:dev}"
container_engine="${CONTAINER_ENGINE:-docker}"

case "$container_engine" in
case "${container_engine}" in
docker|podman) ;;
*) usage ;;
esac

case "$command_name" in
case "${command_name}" in
validate) client_arguments="--validate-config /work/rumble-client.json" ;;
runtimes) client_arguments="--check-runtimes" ;;
sync) client_arguments="--sync /work/rumble-client.json" ;;
# --run re-synchronizes before executing a battle (same as --sync), so unlike
# runtimes/validate it cannot be run with --network none here.
run) client_arguments="--run /work/rumble-client.json" ;;
submit) client_arguments="--submit /work/rumble-client.json" ;;
*) usage ;;
esac

if [ "$command_name" = "runtimes" ]; then
exec "$container_engine" run --rm --read-only --network none --tmpfs /tmp:rw,nosuid,nodev,size=1g \
if [ "${command_name}" = "runtimes" ]; then
exec "${container_engine}" run --rm --read-only --network none --tmpfs /tmp:rw,nosuid,nodev,size=1g \
--user "$(id -u):$(id -g)" \
--cpus 4 --memory 8g --pids-limit 512 --cap-drop ALL --security-opt no-new-privileges \
"$image" --check-runtimes
"${image}" --check-runtimes
fi

config_directory=$(CDPATH= cd -- "$(dirname -- "$config_path")" && pwd)
config_name=$(basename -- "$config_path")
absolute_config="$config_directory/$config_name"
state_directory="$config_directory/.rumble-client"
mkdir -p "$state_directory"
config_directory=$(CDPATH= cd -- "$(dirname -- "${config_path}")" && pwd)
config_name=$(basename -- "${config_path}")
absolute_config="${config_directory}/${config_name}"
state_directory="${config_directory}/.rumble-client"
mkdir -p "${state_directory}"

exec "$container_engine" run --rm --read-only --tmpfs /tmp:rw,nosuid,nodev,size=1g \
env_args=""
if [ "${command_name}" = "submit" ]; then
: "${RUMBLE_CLIENT_TOKEN:?RUMBLE_CLIENT_TOKEN must be set in the environment to submit results}"
env_args="--env RUMBLE_CLIENT_TOKEN"
fi

exec "${container_engine}" run --rm --read-only --tmpfs /tmp:rw,nosuid,nodev,size=1g \
--user "$(id -u):$(id -g)" \
--cpus 4 --memory 8g --pids-limit 512 --cap-drop ALL --security-opt no-new-privileges \
--mount "type=bind,source=$absolute_config,target=/work/rumble-client.json,readonly" \
--mount "type=bind,source=$state_directory,target=/work/.rumble-client" \
"$image" $client_arguments
--mount "type=bind,source=${absolute_config},target=/work/rumble-client.json,readonly" \
--mount "type=bind,source=${state_directory},target=/work/.rumble-client" \
${env_args} \
"${image}" ${client_arguments}
Loading